diff --git a/web/tests/markdown.test.js b/web/tests/markdown.test.js index 787bd51499..5bf8032740 100644 --- a/web/tests/markdown.test.js +++ b/web/tests/markdown.test.js @@ -115,6 +115,12 @@ people.add_active_user({ email: "ampampamp@zulip.com", }); +people.add_active_user({ + full_name: "Zoe", + user_id: 7, + email: "zoe@zulip.com", +}); + people.add_inaccessible_user(108); people.initialize_current_user(cordelia.user_id); diff --git a/zerver/lib/push_notifications.py b/zerver/lib/push_notifications.py index 35f629e13f..8113e7d5f6 100644 --- a/zerver/lib/push_notifications.py +++ b/zerver/lib/push_notifications.py @@ -922,11 +922,74 @@ def get_mobile_push_content(rendered_content: str) -> str: plain_text += elem.tail or "" return plain_text + def is_same_server_message_link(hash: str) -> bool: + # A same server message link always has category `narrow`, + # section `stream` or `dm`, and ends with `/near/`, + # where is a sequence of digits. + match = re.match(r"#([^/]+)", hash) + if match is None or match.group(1) != "narrow": + return False + + match = re.search(r"#narrow/([^/]+)", hash) + if match is None or not (match.group(1) == "stream" or match.group(1) == "dm"): + return False + + return re.search(r"/near/\d+$", hash) is not None + + def is_user_said_paragraph(element: lxml.html.HtmlElement) -> bool: + # The user said paragraph has these exact elements: + # 1. A user mention + # 2. A same server message link ("said") + # 3. A colon (:) + user_mention_elements = element.find_class("user-mention") + if len(user_mention_elements) != 1: + return False + + message_link_elements = [] + anchor_elements = element.cssselect("a[href]") + for elem in anchor_elements: + href = elem.get("href") + if is_same_server_message_link(href): + message_link_elements.append(elem) + + if len(message_link_elements) != 1: + return False + + remaining_text = ( + element.text_content() + .replace(user_mention_elements[0].text_content(), "") + .replace(message_link_elements[0].text_content(), "") + ) + return remaining_text.strip() == ":" + + def get_collapsible_status_array(elements: List[lxml.html.HtmlElement]) -> List[bool]: + collapsible_status: List[bool] = [ + element.tag == "blockquote" or is_user_said_paragraph(element) for element in elements + ] + return collapsible_status + + def potentially_collapse_quotes(element: lxml.html.HtmlElement) -> None: + children = element.getchildren() + collapsible_status = get_collapsible_status_array(children) + + if all(collapsible_status) or all(not x for x in collapsible_status): + return + + collapse_element = lxml.html.Element("p") + collapse_element.text = "[…]" + for index, child in enumerate(children): + if collapsible_status[index]: + if index > 0 and collapsible_status[index - 1]: + child.drop_tree() + else: + child.getparent().replace(child, collapse_element) + if settings.PUSH_NOTIFICATION_REDACT_CONTENT: return _("New message") elem = lxml.html.fragment_fromstring(rendered_content, create_parent=True) change_katex_to_raw_latex(elem) + potentially_collapse_quotes(elem) plain_text = process(elem) return plain_text diff --git a/zerver/tests/fixtures/markdown_test_cases.json b/zerver/tests/fixtures/markdown_test_cases.json index de4e070f5b..d5807dae8a 100644 --- a/zerver/tests/fixtures/markdown_test_cases.json +++ b/zerver/tests/fixtures/markdown_test_cases.json @@ -82,19 +82,19 @@ "name": "fenced_normal_quote", "input": "Hamlet said:\n~~~ quote\nTo be or **not** to be.\n\nThat is the question\n~~~", "expected_output": "

Hamlet said:

\n
\n

To be or not to be.

\n

That is the question

\n
", - "text_content": "Hamlet said:\n> To be or not to be.\n> That is the question\n" + "text_content": "Hamlet said:\n[…]" }, { "name": "fenced_nested_quote", "input": "Hamlet said:\n~~~ quote\nPolonius said:\n> This above all: to thine ownself be true,\nAnd it must follow, as the night the day,\nThou canst not then be false to any man.\n\nWhat good advice!\n~~~", "expected_output": "

Hamlet said:

\n
\n

Polonius said:

\n
\n

This above all: to thine ownself be true,
\nAnd it must follow, as the night the day,
\nThou canst not then be false to any man.

\n
\n

What good advice!

\n
", - "text_content": "Hamlet said:\n> Polonius said:\n> > This above all: to thine ownself be true,\n> > And it must follow, as the night the day,\n> > Thou canst not then be false to any man.\n> What good advice!\n" + "text_content": "Hamlet said:\n[…]" }, { "name": "fenced_complexly_nested_quote", "input": "I heard about this second hand...\n~~~ quote\n\nHe said:\n~~~ quote\nThe customer is complaining.\n\nThey looked at this code:\n``` \ndef hello(): print 'hello\n```\nThey would prefer:\n~~~\ndef hello()\n puts 'hello'\nend\n~~~\n\nPlease advise.\n~~~\n\nShe said:\n~~~ quote\nJust send them this:\n```\necho \"hello\n\"\n```\n~~~", "expected_output": "

I heard about this second hand...

\n
\n

He said:

\n
\n

The customer is complaining.

\n

They looked at this code:

\n
def hello(): print 'hello\n
\n

They would prefer:

\n
\n

def hello()
\n puts 'hello'
\nend

\n
\n

Please advise.

\n
She said:\n~~~ quote\nJust send them this:\n```\necho "hello\n"\n```\n
", - "text_content": "I heard about this second hand...\n> He said:\n> > The customer is complaining.\n> > They looked at this code:\n> > def hello(): print 'hello\n> > They would prefer:\n> def hello()\n> puts 'hello'\n> end\n\nPlease advise.\nShe said:\n~~~ quote\nJust send them this:\n```\necho \"hello\n\"\n```\n" + "text_content": "I heard about this second hand...\n[…]Please advise.\nShe said:\n~~~ quote\nJust send them this:\n```\necho \"hello\n\"\n```\n" }, { "name": "fenced_quotes_inside_mathblock", @@ -130,7 +130,7 @@ "name": "multiple_blockquote_with_quote_and_reply", "input": "Hamlet said:\n~~~quote\nCan we talk ?\n~~~\n> Yes !!\n\nWelcome to Open Source.", "expected_output": "

Hamlet said:

\n
\n

Can we talk ?

\n
\n
\n

Yes !!

\n
\n

Welcome to Open Source.

", - "text_content": "Hamlet said:\n> Can we talk ?\n\n> Yes !!\n\nWelcome to Open Source." + "text_content": "Hamlet said:\n[…]\nWelcome to Open Source." }, { "name": "fenced_quote_with_hashtag", @@ -142,7 +142,7 @@ "name": "fenced_quote_and_list", "input": "Before:\n* One.\n* Two.\n\n```quote\nLists should work after a quote\n```\n\nAfter:\n* One.\n* Two.", "expected_output": "

Before:

\n\n
\n

Lists should work after a quote

\n
\n

After:

\n", - "text_content": "Before:\n\nOne.\nTwo.\n\n> Lists should work after a quote\n\nAfter:\n\nOne.\nTwo.\n" + "text_content": "Before:\n\nOne.\nTwo.\n\n[…]After:\n\nOne.\nTwo.\n" }, { "name": "dangerous_block_having_x", @@ -982,7 +982,7 @@ "name": "spoilers_block_quote", "input": "~~~quote\n```spoiler header\ncontent\n```\noutside spoiler\n~~~\noutside quote", "expected_output": "
\n
\n

header

\n
\n

content

\n
\n

outside spoiler

\n
\n

outside quote

", - "text_content": "> header (…)\n> outside spoiler\n\noutside quote" + "text_content": "[…]outside quote" }, { "name": "spoilers_with_header_markdown", @@ -1047,6 +1047,26 @@ "input": "```php\n $var = function($var, 'string');\n```\nPHP\n```css+php\n $var = function($var, 'string');\n```\nCSS + PHP\n```html+php\n $var = function($var, 'string');\n```\nHTML + PHP\n```javascript+php\n $var = function($var, 'string');\n```\nJAVASCRIPT + PHP\n```xml+php\n $var = function($var, 'string');\n```\nXML + PHP\n```php\n
  $var = function($var, 'string');\n
\n

PHP

\n
  $var = function($var, 'string');\n
\n

CSS + PHP

\n
  $var = function($var, 'string');\n
\n

HTML + PHP

\n
  $var = function($var, 'string');\n
\n

JAVASCRIPT + PHP

\n
  $var = function($var, 'string');\n
\n

XML + PHP

\n
  <?php\n  $var = function($var, 'string');\n
\n

WITH MARKER

", "marked_expected_output": "
  $var = function($var, 'string');\n
\n

PHP

\n
  $var = function($var, 'string');\n
\n

CSS + PHP

\n
  $var = function($var, 'string');\n
\n

HTML + PHP

\n
  $var = function($var, 'string');\n
\n

JAVASCRIPT + PHP

\n
  $var = function($var, 'string');\n
\n

XML + PHP

\n
  <?php\n  $var = function($var, 'string');\n
\n

WITH MARKER

" + }, + { + "name": "normal_quote_and_reply", + "input": "@_**Zoe|7** [said](http://zulip.testserver/#narrow/stream/13-social/topic/party/near/103):\n```quote\nHow are you?\n```\n\nGreat", + "expected_output": "

Zoe said:

\n
\n

How are you?

\n
\n

Great

", + "marked_expected_output": "

Zoe said:

\n
\n

How are you?

\n
\n

Great

", + "text_content": "[…]\nGreat" + }, + { + "name": "user_mention_with_no_message_link", + "input": "Hello @_**Zoe|7** see [this](http://zulip.testserver/#narrow/search/database).", + "expected_output": "

Hello Zoe see this.

", + "marked_expected_output": "

Hello Zoe see this.

", + "text_content": "Hello Zoe see this." + }, + { + "name": "user_mention_with_no_narrow_link", + "input": "Hello @_**Zoe|7** see [this](https://google.com).", + "expected_output": "

Hello Zoe see this.

", + "text_content": "Hello Zoe see this." } ], "linkify_tests": [