Skip to content

Commit

Permalink
markdown: Convert topic links generated by "#-mentions" to permalinks.
Browse files Browse the repository at this point in the history
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.

Part of zulip#21505
  • Loading branch information
roanster007 committed Oct 30, 2024
1 parent 3fe1e55 commit 21c92f4
Show file tree
Hide file tree
Showing 5 changed files with 238 additions and 9 deletions.
28 changes: 27 additions & 1 deletion zerver/lib/markdown/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@
from zerver.lib.markdown.fenced_code import FENCE_RE
from zerver.lib.mention import (
BEFORE_MENTION_ALLOWED_REGEX,
ChannelTopicInfo,
FullNameInfo,
MentionBackend,
MentionData,
Expand Down Expand Up @@ -129,6 +130,7 @@ class DbData:
active_realm_emoji: dict[str, EmojiInfo]
sent_by_bot: bool
stream_names: dict[str, int]
topic_info: dict[ChannelTopicInfo, int]
translate_emoticons: bool
user_upload_previews: dict[str, MarkdownImageMetadata]

Expand Down Expand Up @@ -2024,6 +2026,13 @@ def find_stream_id(self, name: str) -> int | None:
stream_id = db_data.stream_names.get(name)
return stream_id

def get_with_operand(self, channel_topic: ChannelTopicInfo) -> int | None:
db_data: DbData | None = self.zmd.zulip_db_data
if db_data is None:
return None
with_operand = db_data.topic_info.get(channel_topic)
return with_operand

@override
def handleMatch( # type: ignore[override] # https://github.com/python/mypy/issues/10197
self, m: Match[str], data: str
Expand All @@ -2039,7 +2048,13 @@ def handleMatch( # type: ignore[override] # https://github.com/python/mypy/issu
el.set("data-stream-id", str(stream_id))
stream_url = encode_stream(stream_id, stream_name)
topic_url = hash_util_encode(topic_name)
link = f"/#narrow/channel/{stream_url}/topic/{topic_url}"
channel_topic_object = ChannelTopicInfo(stream_name, topic_name)
with_operand = self.get_with_operand(channel_topic_object)
if with_operand is not None:
link = f"/#narrow/channel/{stream_url}/topic/{topic_url}/with/{with_operand}"
else:
link = f"/#narrow/channel/{stream_url}/topic/{topic_url}"

el.set("href", link)
text = f"#{stream_name} > {topic_name}"
el.text = markdown.util.AtomicString(text)
Expand All @@ -2056,6 +2071,13 @@ def possible_linked_stream_names(content: str) -> set[str]:
}


def possible_linked_topics(content: str) -> set[ChannelTopicInfo]:
return {
ChannelTopicInfo(match.group("stream_name"), match.group("topic_name"))
for match in re.finditer(STREAM_TOPIC_LINK_REGEX, content, re.VERBOSE)
}


class AlertWordNotificationProcessor(markdown.preprocessors.Preprocessor):
allowed_before_punctuation = {" ", "\n", "(", '"', ".", ",", "'", ";", "[", "*", "`", ">"}
allowed_after_punctuation = {
Expand Down Expand Up @@ -2670,6 +2692,9 @@ def do_convert(
stream_names = possible_linked_stream_names(content)
stream_name_info = mention_data.get_stream_name_map(stream_names)

linked_stream_topic_data = possible_linked_topics(content)
topic_info = mention_data.get_topic_info_map(linked_stream_topic_data)

if content_has_emoji_syntax(content):
active_realm_emoji = get_name_keyed_dict_for_active_realm_emoji(message_realm.id)
else:
Expand All @@ -2683,6 +2708,7 @@ def do_convert(
realm_url=message_realm.url,
sent_by_bot=sent_by_bot,
stream_names=stream_name_info,
topic_info=topic_info,
translate_emoticons=translate_emoticons,
user_upload_previews=user_upload_previews,
)
Expand Down
70 changes: 67 additions & 3 deletions zerver/lib/mention.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
from django.conf import settings
from django.db.models import Prefetch, Q

from zerver.lib.topic import get_last_message_for_user_in_topic
from zerver.lib.users import get_inaccessible_user_ids
from zerver.models import NamedUserGroup, UserProfile
from zerver.models.streams import get_linkable_streams
Expand Down Expand Up @@ -62,6 +63,19 @@ class PossibleMentions:
message_has_stream_wildcards: bool


@dataclass(frozen=True)
class ChannelTopicInfo:
channel_name: str
topic_name: str


@dataclass
class ChannelInfo:
channel_id: int
recipient_id: int
history_public_to_subscribers: bool


class MentionBackend:
# Be careful about reuse: MentionBackend contains caches which are
# designed to only have the lifespan of a sender user (typically a
Expand All @@ -73,7 +87,8 @@ class MentionBackend:
def __init__(self, realm_id: int) -> None:
self.realm_id = realm_id
self.user_cache: dict[tuple[int, str], FullNameInfo] = {}
self.stream_cache: dict[str, int] = {}
self.stream_cache: dict[str, ChannelInfo] = {}
self.topic_cache: dict[ChannelTopicInfo, int] = {}

def get_full_name_info_list(
self, user_filters: list[UserFilter], message_sender: UserProfile | None
Expand Down Expand Up @@ -145,7 +160,7 @@ def get_stream_name_map(self, stream_names: set[str]) -> dict[str, int]:

for stream_name in stream_names:
if stream_name in self.stream_cache:
result[stream_name] = self.stream_cache[stream_name]
result[stream_name] = self.stream_cache[stream_name].channel_id
else:
unseen_stream_names.append(stream_name)

Expand All @@ -162,15 +177,58 @@ def get_stream_name_map(self, stream_names: set[str]) -> dict[str, int]:
.values(
"id",
"name",
"recipient_id",
"history_public_to_subscribers",
)
)

for row in rows:
self.stream_cache[row["name"]] = row["id"]
self.stream_cache[row["name"]] = ChannelInfo(
row["id"], row["recipient_id"], row["history_public_to_subscribers"]
)
result[row["name"]] = row["id"]

return result

def get_topic_info_map(
self, channel_topic: set[ChannelTopicInfo], message_sender: UserProfile | None
) -> dict[ChannelTopicInfo, int]:
if not channel_topic:
return {}

result: dict[ChannelTopicInfo, int] = {}
unseen_channel_topic: list[ChannelTopicInfo] = []

for channel_topic_object in channel_topic:
if channel_topic_object in self.topic_cache:
result[channel_topic_object] = self.topic_cache[channel_topic_object]
else:
unseen_channel_topic.append(channel_topic_object)

for channel_topic_object in unseen_channel_topic:
channel_info = self.stream_cache.get(channel_topic_object.channel_name)

assert channel_info is not None
recipient_id = channel_info.recipient_id
topic_name = channel_topic_object.topic_name
history_public_to_subscribers = channel_info.history_public_to_subscribers

topic_latest_message = get_last_message_for_user_in_topic(
self.realm_id,
message_sender,
recipient_id,
topic_name,
history_public_to_subscribers,
)

if topic_latest_message is None:
continue

self.topic_cache[channel_topic_object] = topic_latest_message
result[channel_topic_object] = topic_latest_message

return result


def user_mention_matches_topic_wildcard(mention: str) -> bool:
return mention in topic_wildcards
Expand Down Expand Up @@ -248,6 +306,7 @@ def __init__(
) -> None:
self.mention_backend = mention_backend
realm_id = mention_backend.realm_id
self.message_sender = message_sender
mentions = possible_mentions(content)
possible_mentions_info = get_possible_mentions_info(
mention_backend, mentions.mention_texts, message_sender
Expand Down Expand Up @@ -312,6 +371,11 @@ def get_group_members(self, user_group_id: int) -> list[int]:
def get_stream_name_map(self, stream_names: set[str]) -> dict[str, int]:
return self.mention_backend.get_stream_name_map(stream_names)

def get_topic_info_map(
self, channel_topic_names: set[ChannelTopicInfo]
) -> dict[ChannelTopicInfo, int]:
return self.mention_backend.get_topic_info_map(channel_topic_names, self.message_sender)


def silent_mention_syntax_for_user(user_profile: UserProfile) -> str:
return f"@_**{user_profile.full_name}|{user_profile.id}**"
28 changes: 28 additions & 0 deletions zerver/lib/topic.py
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,34 @@ def messages_for_topic(
)


def get_last_message_for_user_in_topic(
realm_id: int,
user_profile: UserProfile | None,
recipient_id: int,
topic_name: str,
history_public_to_subscribers: bool,
) -> int | None:
if history_public_to_subscribers:
return (
messages_for_topic(realm_id, recipient_id, topic_name)
.values_list("id", flat=True)
.last()
)

elif user_profile is not None:
return (
UserMessage.objects.filter(
user_profile=user_profile,
message__recipient_id=recipient_id,
message__subject__iexact=topic_name,
)
.values_list("message_id", flat=True)
.last()
)

return None


def save_message_for_edit_use_case(message: Message) -> None:
message.save(
update_fields=[
Expand Down
Loading

0 comments on commit 21c92f4

Please sign in to comment.