Skip to content

Commit f607bf7

Browse files
committed
markdown: Convert topic links generated by "#-mentions" to permalinks.
This commit converts the links generated by the markdown of the "#-mention" of topics to permalinks -- the links containing the "with" narrow operator, the operand being the last message of the channel and topic of the mention. Fixes part of zulip#21505
1 parent d1b2770 commit f607bf7

File tree

5 files changed

+238
-9
lines changed

5 files changed

+238
-9
lines changed

zerver/lib/markdown/__init__.py

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@
4848
from zerver.lib.markdown.fenced_code import FENCE_RE
4949
from zerver.lib.mention import (
5050
BEFORE_MENTION_ALLOWED_REGEX,
51+
ChannelTopicInfo,
5152
FullNameInfo,
5253
MentionBackend,
5354
MentionData,
@@ -130,6 +131,7 @@ class DbData:
130131
active_realm_emoji: dict[str, EmojiInfo]
131132
sent_by_bot: bool
132133
stream_names: dict[str, int]
134+
topic_info: dict[ChannelTopicInfo, int]
133135
translate_emoticons: bool
134136
user_upload_previews: dict[str, MarkdownImageMetadata]
135137

@@ -2045,6 +2047,13 @@ def handleMatch( # type: ignore[override] # https://github.com/python/mypy/issu
20452047

20462048

20472049
class StreamTopicPattern(StreamTopicMessageProcessor):
2050+
def get_with_operand(self, channel_topic: ChannelTopicInfo) -> int | None:
2051+
db_data: DbData | None = self.zmd.zulip_db_data
2052+
if db_data is None:
2053+
return None
2054+
with_operand = db_data.topic_info.get(channel_topic)
2055+
return with_operand
2056+
20482057
@override
20492058
def handleMatch( # type: ignore[override] # https://github.com/python/mypy/issues/10197
20502059
self, m: Match[str], data: str
@@ -2060,7 +2069,13 @@ def handleMatch( # type: ignore[override] # https://github.com/python/mypy/issu
20602069
el.set("data-stream-id", str(stream_id))
20612070
stream_url = encode_stream(stream_id, stream_name)
20622071
topic_url = hash_util_encode(topic_name)
2063-
link = f"/#narrow/channel/{stream_url}/topic/{topic_url}"
2072+
channel_topic_object = ChannelTopicInfo(stream_name, topic_name)
2073+
with_operand = self.get_with_operand(channel_topic_object)
2074+
if with_operand is not None:
2075+
link = f"/#narrow/channel/{stream_url}/topic/{topic_url}/with/{with_operand}"
2076+
else:
2077+
link = f"/#narrow/channel/{stream_url}/topic/{topic_url}"
2078+
20642079
el.set("href", link)
20652080
text = f"#{stream_name} > {topic_name}"
20662081
el.text = markdown.util.AtomicString(text)
@@ -2100,6 +2115,13 @@ def possible_linked_stream_names(content: str) -> set[str]:
21002115
}
21012116

21022117

2118+
def possible_linked_topics(content: str) -> set[ChannelTopicInfo]:
2119+
return {
2120+
ChannelTopicInfo(match.group("stream_name"), match.group("topic_name"))
2121+
for match in re.finditer(STREAM_TOPIC_LINK_REGEX, content, re.VERBOSE)
2122+
}
2123+
2124+
21032125
class AlertWordNotificationProcessor(markdown.preprocessors.Preprocessor):
21042126
allowed_before_punctuation = {" ", "\n", "(", '"', ".", ",", "'", ";", "[", "*", "`", ">"}
21052127
allowed_after_punctuation = {
@@ -2719,6 +2741,9 @@ def do_convert(
27192741
stream_names = possible_linked_stream_names(content)
27202742
stream_name_info = mention_data.get_stream_name_map(stream_names)
27212743

2744+
linked_stream_topic_data = possible_linked_topics(content)
2745+
topic_info = mention_data.get_topic_info_map(linked_stream_topic_data)
2746+
27222747
if content_has_emoji_syntax(content):
27232748
active_realm_emoji = get_name_keyed_dict_for_active_realm_emoji(message_realm.id)
27242749
else:
@@ -2732,6 +2757,7 @@ def do_convert(
27322757
realm_url=message_realm.url,
27332758
sent_by_bot=sent_by_bot,
27342759
stream_names=stream_name_info,
2760+
topic_info=topic_info,
27352761
translate_emoticons=translate_emoticons,
27362762
user_upload_previews=user_upload_previews,
27372763
)

zerver/lib/mention.py

Lines changed: 67 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
from django.db.models import Q
88
from django_stubs_ext import StrPromise
99

10+
from zerver.lib.topic import get_last_message_for_user_in_topic
1011
from zerver.lib.user_groups import get_recursive_group_members
1112
from zerver.lib.users import get_inaccessible_user_ids
1213
from zerver.models import NamedUserGroup, UserProfile
@@ -65,6 +66,19 @@ class PossibleMentions:
6566
message_has_stream_wildcards: bool
6667

6768

69+
@dataclass(frozen=True)
70+
class ChannelTopicInfo:
71+
channel_name: str
72+
topic_name: str
73+
74+
75+
@dataclass
76+
class ChannelInfo:
77+
channel_id: int
78+
recipient_id: int
79+
history_public_to_subscribers: bool
80+
81+
6882
class MentionBackend:
6983
# Be careful about reuse: MentionBackend contains caches which are
7084
# designed to only have the lifespan of a sender user (typically a
@@ -76,7 +90,8 @@ class MentionBackend:
7690
def __init__(self, realm_id: int) -> None:
7791
self.realm_id = realm_id
7892
self.user_cache: dict[tuple[int, str], FullNameInfo] = {}
79-
self.stream_cache: dict[str, int] = {}
93+
self.stream_cache: dict[str, ChannelInfo] = {}
94+
self.topic_cache: dict[ChannelTopicInfo, int] = {}
8095

8196
def get_full_name_info_list(
8297
self, user_filters: list[UserFilter], message_sender: UserProfile | None
@@ -148,7 +163,7 @@ def get_stream_name_map(self, stream_names: set[str]) -> dict[str, int]:
148163

149164
for stream_name in stream_names:
150165
if stream_name in self.stream_cache:
151-
result[stream_name] = self.stream_cache[stream_name]
166+
result[stream_name] = self.stream_cache[stream_name].channel_id
152167
else:
153168
unseen_stream_names.append(stream_name)
154169

@@ -165,15 +180,58 @@ def get_stream_name_map(self, stream_names: set[str]) -> dict[str, int]:
165180
.values(
166181
"id",
167182
"name",
183+
"recipient_id",
184+
"history_public_to_subscribers",
168185
)
169186
)
170187

171188
for row in rows:
172-
self.stream_cache[row["name"]] = row["id"]
189+
self.stream_cache[row["name"]] = ChannelInfo(
190+
row["id"], row["recipient_id"], row["history_public_to_subscribers"]
191+
)
173192
result[row["name"]] = row["id"]
174193

175194
return result
176195

196+
def get_topic_info_map(
197+
self, channel_topic: set[ChannelTopicInfo], message_sender: UserProfile | None
198+
) -> dict[ChannelTopicInfo, int]:
199+
if not channel_topic:
200+
return {}
201+
202+
result: dict[ChannelTopicInfo, int] = {}
203+
unseen_channel_topic: list[ChannelTopicInfo] = []
204+
205+
for channel_topic_object in channel_topic:
206+
if channel_topic_object in self.topic_cache:
207+
result[channel_topic_object] = self.topic_cache[channel_topic_object]
208+
else:
209+
unseen_channel_topic.append(channel_topic_object)
210+
211+
for channel_topic_object in unseen_channel_topic:
212+
channel_info = self.stream_cache.get(channel_topic_object.channel_name)
213+
214+
assert channel_info is not None
215+
recipient_id = channel_info.recipient_id
216+
topic_name = channel_topic_object.topic_name
217+
history_public_to_subscribers = channel_info.history_public_to_subscribers
218+
219+
topic_latest_message = get_last_message_for_user_in_topic(
220+
self.realm_id,
221+
message_sender,
222+
recipient_id,
223+
topic_name,
224+
history_public_to_subscribers,
225+
)
226+
227+
if topic_latest_message is None:
228+
continue
229+
230+
self.topic_cache[channel_topic_object] = topic_latest_message
231+
result[channel_topic_object] = topic_latest_message
232+
233+
return result
234+
177235

178236
def user_mention_matches_topic_wildcard(mention: str) -> bool:
179237
return mention in topic_wildcards
@@ -251,6 +309,7 @@ def __init__(
251309
) -> None:
252310
self.mention_backend = mention_backend
253311
realm_id = mention_backend.realm_id
312+
self.message_sender = message_sender
254313
mentions = possible_mentions(content)
255314
possible_mentions_info = get_possible_mentions_info(
256315
mention_backend, mentions.mention_texts, message_sender
@@ -307,6 +366,11 @@ def get_group_members(self, user_group_id: int) -> list[int]:
307366
def get_stream_name_map(self, stream_names: set[str]) -> dict[str, int]:
308367
return self.mention_backend.get_stream_name_map(stream_names)
309368

369+
def get_topic_info_map(
370+
self, channel_topic_names: set[ChannelTopicInfo]
371+
) -> dict[ChannelTopicInfo, int]:
372+
return self.mention_backend.get_topic_info_map(channel_topic_names, self.message_sender)
373+
310374

311375
def silent_mention_syntax_for_user(user_profile: UserProfile) -> str:
312376
return f"@_**{user_profile.full_name}|{user_profile.id}**"

zerver/lib/topic.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,34 @@ def messages_for_topic(
7676
)
7777

7878

79+
def get_last_message_for_user_in_topic(
80+
realm_id: int,
81+
user_profile: UserProfile | None,
82+
recipient_id: int,
83+
topic_name: str,
84+
history_public_to_subscribers: bool,
85+
) -> int | None:
86+
if history_public_to_subscribers:
87+
return (
88+
messages_for_topic(realm_id, recipient_id, topic_name)
89+
.values_list("id", flat=True)
90+
.last()
91+
)
92+
93+
elif user_profile is not None:
94+
return (
95+
UserMessage.objects.filter(
96+
user_profile=user_profile,
97+
message__recipient_id=recipient_id,
98+
message__subject__iexact=topic_name,
99+
)
100+
.values_list("message_id", flat=True)
101+
.last()
102+
)
103+
104+
return None
105+
106+
79107
def save_message_for_edit_use_case(message: Message) -> None:
80108
message.save(
81109
update_fields=[

0 commit comments

Comments
 (0)