diff --git a/frontend_tests/node_tests/markdown.js b/frontend_tests/node_tests/markdown.js index 9fb27268a8..9cc06e1dfb 100644 --- a/frontend_tests/node_tests/markdown.js +++ b/frontend_tests/node_tests/markdown.js @@ -447,6 +447,16 @@ test("marked", () => { expected: '

@Brother of Bobby|123

', }, + { + input: "@**|106** valid user id.", + expected: + '

@Brother of Bobby|123 valid user id.

', + }, + { + input: "@**|123|106** comes under user|id case.", + expected: "

@**|123|106** comes under user|id case.

", + }, + {input: "@**|1234** invalid id.", expected: "

@**|1234** invalid id.

"}, {input: "T\n@hamletcharacters", expected: "

T
\n@hamletcharacters

"}, { input: "T\n@*hamletcharacters*", diff --git a/static/images/help/markdown-mentions.png b/static/images/help/markdown-mentions.png index d3a70b6ca8..ba0a35ee79 100644 Binary files a/static/images/help/markdown-mentions.png and b/static/images/help/markdown-mentions.png differ diff --git a/static/js/markdown.js b/static/js/markdown.js index 0b1867323d..d833f2a1bd 100644 --- a/static/js/markdown.js +++ b/static/js/markdown.js @@ -109,7 +109,7 @@ export function apply_markdown(message) { let full_name; let user_id; - const id_regex = /(.+)\|(\d+)$/g; // For @**user|id** syntax + const id_regex = /^(.+)?\|(\d+)$/; // For @**user|id** and @**|id** syntax const match = id_regex.exec(mention); if (match) { @@ -131,9 +131,20 @@ export function apply_markdown(message) { full_name = match[1]; user_id = Number.parseInt(match[2], 10); - if (!helpers.is_valid_full_name_and_user_id(full_name, user_id)) { - user_id = undefined; - full_name = undefined; + if (full_name === undefined) { + // For @**|id** syntax + if (!helpers.is_valid_user_id(user_id)) { + // silently ignore invalid user id. + user_id = undefined; + } else { + full_name = helpers.get_actual_name_from_user_id(user_id); + } + } else { + // For @**user|id** syntax + if (!helpers.is_valid_full_name_and_user_id(full_name, user_id)) { + user_id = undefined; + full_name = undefined; + } } } diff --git a/static/js/markdown_config.js b/static/js/markdown_config.js index 371a4f39fc..505776b3d4 100644 --- a/static/js/markdown_config.js +++ b/static/js/markdown_config.js @@ -31,6 +31,7 @@ export const get_helpers = () => ({ get_user_id_from_name: people.get_user_id_from_name, is_valid_full_name_and_user_id: people.is_valid_full_name_and_user_id, my_user_id: people.my_current_user_id, + is_valid_user_id: people.is_known_user_id, // user groups get_user_group_from_name: user_groups.get_user_group_from_name, diff --git a/templates/zerver/help/format-your-message-using-markdown.md b/templates/zerver/help/format-your-message-using-markdown.md index cb3021d5b7..4db72fbd0b 100644 --- a/templates/zerver/help/format-your-message-using-markdown.md +++ b/templates/zerver/help/format-your-message-using-markdown.md @@ -189,14 +189,17 @@ You can also [add custom emoji](/help/add-custom-emoji). ## Mentions Learn more about mentions [here](/help/mention-a-user-or-group). -The numbers will be added automatically by the typeahead if needed for disambiguation. ``` -Users: @**Polonius** or @**Zoe|2132** (two asterisks) +Users: @**Polonius** or @**aaron|26** or @**|26** (two asterisks) User group: @*support team* (one asterisk) -Silent mention: @_**Polonius** (@_ instead of @) +Silent mention: @_**Polonius** or @_**|26** (@_ instead of @) ``` +The variants with numbers use user IDs, and are intended for +disambiguation (if multiple users have the same name) and bots (for +the variant that only contains the user ID). + ![Markdown mentions](/static/images/help/markdown-mentions.png) ## Status Messages diff --git a/zerver/lib/markdown/__init__.py b/zerver/lib/markdown/__init__.py index 9ab0ca4d0a..0334f4c4bf 100644 --- a/zerver/lib/markdown/__init__.py +++ b/zerver/lib/markdown/__init__.py @@ -1884,7 +1884,7 @@ class UserMentionPattern(markdown.inlinepatterns.InlineProcessor): wildcard = mention.user_mention_matches_wildcard(name) - id_syntax_match = re.match(r".+\|(?P\d+)$", name) + id_syntax_match = re.match(r"(.+)?\|(?P\d+)$", name) if id_syntax_match: id = int(id_syntax_match.group("user_id")) user = db_data["mention_data"].get_user_by_id(id) @@ -2445,18 +2445,25 @@ def get_possible_mentions_info(realm_id: int, mention_texts: Set[str]) -> List[F if not mention_texts: return [] - # Remove the trailing part of the `name|id` mention syntax, - # thus storing only full names in full_names. full_names = set() - name_re = r"(?P.+)\|\d+$" + mention_ids: Set[int] = set() + + name_re = r"(?P.+)?\|(?P\d+)$" for mention_text in mention_texts: name_syntax_match = re.match(name_re, mention_text) if name_syntax_match: - full_names.add(name_syntax_match.group("full_name")) + full_name = name_syntax_match.group("full_name") + mention_id = name_syntax_match.group("mention_id") + if full_name: + full_names.add(name_syntax_match.group("full_name")) + else: + mention_ids.add(int(mention_id)) else: full_names.add(mention_text) q_list = {Q(full_name__iexact=full_name) for full_name in full_names} + id_q_list = {Q(id=id) for id in mention_ids} + q_list |= id_q_list rows = ( UserProfile.objects.filter( diff --git a/zerver/tests/test_markdown.py b/zerver/tests/test_markdown.py index 258883c017..1bd4f59528 100644 --- a/zerver/tests/test_markdown.py +++ b/zerver/tests/test_markdown.py @@ -1830,6 +1830,13 @@ class MarkdownTest(ZulipTestCase): ) self.assertEqual(msg.mentions_user_ids, {user_profile.id}) + content = f"@**|{user_id}**" + self.assertEqual( + render_markdown(msg, content), + '

' "@King Hamlet

", + ) + self.assertEqual(msg.mentions_user_ids, {user_profile.id}) + def test_mention_silent(self) -> None: sender_user_profile = self.example_user("othello") user_profile = self.example_user("hamlet") @@ -1875,18 +1882,30 @@ class MarkdownTest(ZulipTestCase): ) self.assertEqual(msg.mentions_user_ids, set()) + content = f"@_**|123456789** and @_**|{user_id}**" + self.assertEqual( + render_markdown(msg, content), + "

@_|123456789 and " + '' + "King Hamlet

", + ) + self.assertEqual(msg.mentions_user_ids, set()) + def test_possible_mentions(self) -> None: def assert_mentions(content: str, names: Set[str], has_wildcards: bool = False) -> None: self.assertEqual(possible_mentions(content), (names, has_wildcards)) + aaron = self.example_user("aaron") + assert_mentions("", set()) assert_mentions("boring", set()) assert_mentions("@**all**", set(), True) assert_mentions("smush@**steve**smush", set()) assert_mentions( - "Hello @**King Hamlet** and @**Cordelia Lear**\n@**Foo van Barson|1234** @**all**", - {"King Hamlet", "Cordelia Lear", "Foo van Barson|1234"}, + f"Hello @**King Hamlet**, @**|{aaron.id}** and @**Cordelia Lear**\n@**Foo van Barson|1234** @**all**", + {"King Hamlet", f"|{aaron.id}", "Cordelia Lear", "Foo van Barson|1234"}, True, )