mirror of
https://github.com/zulip/zulip.git
synced 2025-10-23 04:52:12 +00:00
message_link: Add support for empty string topic in syntax.
This commit adds support for empty string as a valid topic name in syntax for linking to channel messages. The server stores it after empty string is replaced with `realm_empty_topic_display_name` and wrapped with an <em> tag. The web client parses the rendered_content and updates the topic_name part in the HTML with topic_name in user's language + wraps it in a <span> tag with 'empty-topic-display' css class.
This commit is contained in:
committed by
Tim Abbott
parent
53dc5198c6
commit
0788942a68
@@ -20,6 +20,12 @@ format used by the Zulip server that they are interacting with.
|
||||
|
||||
## Changes in Zulip 10.0
|
||||
|
||||
**Feature level 346**
|
||||
|
||||
* [Markdown message formatting](/api/message-formatting#links-to-channels-topics-and-messages):
|
||||
Added support for empty string as a valid topic name in syntaxes
|
||||
for linking to topics and messages.
|
||||
|
||||
**Feature level 345**
|
||||
|
||||
* `POST /remotes/server/register/transfer`,
|
||||
|
@@ -55,7 +55,31 @@ the channel had been renamed. That field is **deprecated**, because
|
||||
displaying an updated value for the most common forms of this syntax
|
||||
requires parsing the URL to get the topic to use anyway.
|
||||
|
||||
**Changes**: In Zulip 10.0 (feature level 319), added Markdown syntax
|
||||
When a topic is an empty string, it is replaced with
|
||||
`realm_empty_topic_display_name` found in the [`POST /register`](/api/register-queue)
|
||||
response and wrapped with the `<em>` tag.
|
||||
|
||||
Sample HTML formats with `"realm_empty_topic_display_name": "general chat"`
|
||||
are as follows:
|
||||
```html
|
||||
<!-- Syntax: #**announce>** -->
|
||||
<a class="stream-topic" data-stream-id="9"
|
||||
href="/#narrow/channel/9-announce/topic/">
|
||||
#announce > <em>general chat</em>
|
||||
</a>
|
||||
|
||||
<!-- Syntax: #**announce>@214** -->
|
||||
<a class="message-link"
|
||||
href="/#narrow/channel/9-announce/topic//near/214">
|
||||
#announce > <em>general chat</em> @ 💬
|
||||
</a>
|
||||
```
|
||||
|
||||
**Changes**: Before Zulip 10.0 (feature level 346), empty string
|
||||
was not a valid topic name in syntaxes for linking to topics and
|
||||
messages.
|
||||
|
||||
In Zulip 10.0 (feature level 319), added Markdown syntax
|
||||
for linking to a specific message in a conversation. Declared the
|
||||
`data-stream-id` field to be deprecated as detailed above.
|
||||
|
||||
|
@@ -34,7 +34,7 @@ DESKTOP_WARNING_VERSION = "5.9.3"
|
||||
# new level means in api_docs/changelog.md, as well as "**Changes**"
|
||||
# entries in the endpoint's documentation in `zulip.yaml`.
|
||||
|
||||
API_FEATURE_LEVEL = 345 # Last bumped for push notifications transfer.
|
||||
API_FEATURE_LEVEL = 346 # Last bumped for topic="" support in link to topics/messages.
|
||||
|
||||
# Bump the minor PROVISION_VERSION to indicate that folks should provision
|
||||
# only when going from an old version of the code to a newer version. Bump
|
||||
|
@@ -673,13 +673,14 @@ function handleStreamTopicMessage({
|
||||
stream_topic_hash: (stream_id: number, topic: string) => string;
|
||||
}): string | undefined {
|
||||
const stream = get_stream_by_name(stream_name);
|
||||
if (stream === undefined || !topic) {
|
||||
if (stream === undefined) {
|
||||
return undefined;
|
||||
}
|
||||
const href = stream_topic_hash(stream.stream_id, topic) + "/near/" + message_id;
|
||||
return render_channel_message_link({
|
||||
channel_name: stream.name,
|
||||
topic_display_name: topic,
|
||||
topic_display_name: util.get_final_topic_display_name(topic),
|
||||
is_empty_string_topic: topic === "",
|
||||
href,
|
||||
});
|
||||
}
|
||||
|
@@ -3,6 +3,7 @@ import {isValid, parseISO} from "date-fns";
|
||||
import $ from "jquery";
|
||||
import assert from "minimalistic-assert";
|
||||
|
||||
import render_channel_message_link from "../templates/channel_message_link.hbs";
|
||||
import code_buttons_container from "../templates/code_buttons_container.hbs";
|
||||
import render_markdown_timestamp from "../templates/markdown_timestamp.hbs";
|
||||
import render_mention_content_wrapper from "../templates/mention_content_wrapper.hbs";
|
||||
@@ -235,7 +236,7 @@ export const update_elements = ($content: JQuery): void => {
|
||||
}
|
||||
});
|
||||
|
||||
$content.find("a.stream-topic").each(function (): void {
|
||||
$content.find("a.stream-topic, a.message-link").each(function (): void {
|
||||
const narrow_url = $(this).attr("href");
|
||||
assert(narrow_url !== undefined);
|
||||
const channel_topic = hash_util.decode_stream_topic_from_url(
|
||||
@@ -251,14 +252,22 @@ export const update_elements = ($content: JQuery): void => {
|
||||
const topic_name = channel_topic.topic_name;
|
||||
assert(topic_name !== undefined);
|
||||
const topic_display_name = util.get_final_topic_display_name(topic_name);
|
||||
const topic_link_html = render_topic_link({
|
||||
channel_id: channel_topic.stream_id,
|
||||
const context = {
|
||||
channel_name,
|
||||
topic_display_name,
|
||||
is_empty_string_topic: topic_name === "",
|
||||
href: narrow_url,
|
||||
});
|
||||
$(this).replaceWith($(topic_link_html));
|
||||
};
|
||||
if ($(this).hasClass("stream-topic")) {
|
||||
const topic_link_html = render_topic_link({
|
||||
channel_id: channel_topic.stream_id,
|
||||
...context,
|
||||
});
|
||||
$(this).replaceWith($(topic_link_html));
|
||||
} else {
|
||||
const message_link_html = render_channel_message_link(context);
|
||||
$(this).replaceWith($(message_link_html));
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
@@ -1,2 +1,13 @@
|
||||
<a class="message-link" href="{{href}}">#{{channel_name}} > {{topic_display_name}} @ 💬</a>
|
||||
{{~!-- squash whitespace --~}}
|
||||
{{#if is_empty_string_topic}}
|
||||
<a class="message-link" href="{{href}}">
|
||||
{{~!-- squash whitespace --~}}
|
||||
#{{channel_name}} > <span class="empty-topic-display">{{topic_display_name}}</span> @ 💬
|
||||
{{~!-- squash whitespace --~}}
|
||||
</a>
|
||||
{{~else}}
|
||||
<a class="message-link" href="{{href}}">
|
||||
{{~!-- squash whitespace --~}}
|
||||
#{{channel_name}} > {{topic_display_name}} @ 💬
|
||||
{{~!-- squash whitespace --~}}
|
||||
</a>
|
||||
{{~/if}}
|
||||
|
@@ -426,6 +426,10 @@ test("marked", ({override}) => {
|
||||
expected:
|
||||
'<p>Look at <a class="message-link" href="#narrow/channel/1-Denmark/topic/message_link/near/100">#Denmark > message_link @ 💬</a></p>',
|
||||
},
|
||||
{
|
||||
input: "Look at #**Denmark>@100**",
|
||||
expected: `<p>Look at <a class="message-link" href="#narrow/channel/1-Denmark/topic//near/100">#Denmark > <span class="empty-topic-display">translated: ${REALM_EMPTY_TOPIC_DISPLAY_NAME}</span> @ 💬</a></p>`,
|
||||
},
|
||||
{
|
||||
input: "Look at #**Unknown>message_link@100**",
|
||||
expected: "<p>Look at #**Unknown>message_link@100**</p>",
|
||||
|
@@ -143,7 +143,7 @@ const get_content_element = () => {
|
||||
$content.set_find_results(".topic-mention", $array([]));
|
||||
$content.set_find_results(".user-group-mention", $array([]));
|
||||
$content.set_find_results("a.stream", $array([]));
|
||||
$content.set_find_results("a.stream-topic", $array([]));
|
||||
$content.set_find_results("a.stream-topic, a.message-link", $array([]));
|
||||
$content.set_find_results("time", $array([]));
|
||||
$content.set_find_results("span.timestamp-error", $array([]));
|
||||
$content.set_find_results(".emoji", $array([]));
|
||||
@@ -422,10 +422,11 @@ run_test("stream-links", ({mock_template}) => {
|
||||
`/#narrow/channel/${stream.stream_id}-random/topic/topic.20name.20.3E.20still.20the.20topic.20name`,
|
||||
);
|
||||
$stream_topic.replaceWith = noop;
|
||||
$stream_topic.hasClass = (class_name) => class_name === "stream-topic";
|
||||
$stream_topic.text("#random > topic name > still the topic name");
|
||||
|
||||
$content.set_find_results("a.stream", $array([$stream]));
|
||||
$content.set_find_results("a.stream-topic", $array([$stream_topic]));
|
||||
$content.set_find_results("a.stream-topic, a.message-link", $array([$stream_topic]));
|
||||
|
||||
let topic_link_context;
|
||||
let topic_link_rendered_html;
|
||||
@@ -460,8 +461,9 @@ run_test("topic-link (empty string topic)", ({mock_template}) => {
|
||||
$channel_topic.set_find_results(".highlight", false);
|
||||
$channel_topic.attr("href", `/#narrow/channel/${stream.stream_id}-random/topic/`);
|
||||
$channel_topic.replaceWith = noop;
|
||||
$channel_topic.hasClass = (class_name) => class_name === "stream-topic";
|
||||
$channel_topic.html(`#random > <em>${REALM_EMPTY_TOPIC_DISPLAY_NAME}</em>`);
|
||||
$content.set_find_results("a.stream-topic", $array([$channel_topic]));
|
||||
$content.set_find_results("a.stream-topic, a.message-link", $array([$channel_topic]));
|
||||
|
||||
let topic_link_context;
|
||||
let topic_link_rendered_html;
|
||||
@@ -487,6 +489,45 @@ run_test("topic-link (empty string topic)", ({mock_template}) => {
|
||||
assert.ok(topic_link_rendered_html.includes("empty-topic-display"));
|
||||
});
|
||||
|
||||
run_test("message-links", ({mock_template}) => {
|
||||
// Setup
|
||||
const $content = get_content_element();
|
||||
const $channel_topic_message = $.create("a.message-link");
|
||||
$channel_topic_message.set_find_results(".highlight", false);
|
||||
$channel_topic_message.attr(
|
||||
"href",
|
||||
`/#narrow/channel/${stream.stream_id}-${stream.name}/topic//near/123`,
|
||||
);
|
||||
$channel_topic_message.replaceWith = noop;
|
||||
$channel_topic_message.hasClass = (class_name) => class_name === "message-link";
|
||||
$channel_topic_message.html(
|
||||
`#${stream.name} > <em>${REALM_EMPTY_TOPIC_DISPLAY_NAME}</em> @ 💬`,
|
||||
);
|
||||
$content.set_find_results("a.stream-topic, a.message-link", $array([$channel_topic_message]));
|
||||
|
||||
let channel_message_link_context;
|
||||
let channel_message_link_rendered_html;
|
||||
mock_template("channel_message_link.hbs", true, (data, html) => {
|
||||
channel_message_link_context = data;
|
||||
channel_message_link_rendered_html = html;
|
||||
return html;
|
||||
});
|
||||
|
||||
// Initial assert
|
||||
assert.equal($channel_topic_message.html(), "#test > <em>general chat</em> @ 💬");
|
||||
|
||||
rm.update_elements($content);
|
||||
|
||||
// Final asserts
|
||||
assert.deepEqual(channel_message_link_context, {
|
||||
channel_name: stream.name,
|
||||
topic_display_name: `translated: ${REALM_EMPTY_TOPIC_DISPLAY_NAME}`,
|
||||
is_empty_string_topic: true,
|
||||
href: `/#narrow/channel/${stream.stream_id}-test/topic//near/123`,
|
||||
});
|
||||
assert.ok(channel_message_link_rendered_html.includes("empty-topic-display"));
|
||||
});
|
||||
|
||||
run_test("timestamp without time", () => {
|
||||
const $content = get_content_element();
|
||||
const $timestamp = $.create("timestamp without actual time");
|
||||
|
@@ -545,7 +545,7 @@ inline.zulip = merge({}, inline.breaks, {
|
||||
unicodeemoji: possible_emoji_regex,
|
||||
usermention: /^@(_?)(?:\*\*([^\*]+)\*\*)/, // Match potentially multi-word string between @** **
|
||||
groupmention: /^@(_?)(?:\*([^\*]+)\*)/, // Match multi-word string between @* *
|
||||
stream_topic_message: /^#\*\*([^\*>]+)>([^\*]+)@(\d+)\*\*/,
|
||||
stream_topic_message: /^#\*\*([^\*>]+)>([^\*]*)@(\d+)\*\*/,
|
||||
stream_topic: /^#\*\*([^\*>]+)>([^\*]*)\*\*/,
|
||||
stream: /^#\*\*([^\*]+)\*\*/,
|
||||
tex: /^(\$\$([^\n_$](\\\$|[^\n$])*)\$\$(?!\$))\B/,
|
||||
|
@@ -200,7 +200,7 @@ STREAM_TOPIC_MESSAGE_LINK_REGEX = rf"""
|
||||
\#\*\* # and after hash sign followed by double asterisks
|
||||
(?P<stream_name>[^\*>]+) # stream name can contain anything except >
|
||||
> # > acts as separator
|
||||
(?P<topic_name>[^\*]+) # topic name can contain anything
|
||||
(?P<topic_name>[^\*]*) # topic name can be an empty string or contain anything
|
||||
@
|
||||
(?P<message_id>\d+) # message id
|
||||
\*\* # ends by double asterisks
|
||||
@@ -2097,8 +2097,17 @@ class StreamTopicMessagePattern(StreamTopicMessageProcessor):
|
||||
topic_url = hash_util_encode(topic_name)
|
||||
link = f"/#narrow/channel/{stream_url}/topic/{topic_url}/near/{message_id}"
|
||||
el.set("href", link)
|
||||
text = f"#{stream_name} > {topic_name} @ 💬"
|
||||
el.text = markdown.util.AtomicString(text)
|
||||
|
||||
if topic_name == "":
|
||||
topic_el = Element("em")
|
||||
topic_el.text = Message.EMPTY_TOPIC_FALLBACK_NAME
|
||||
el.text = markdown.util.AtomicString(f"#{stream_name} > ")
|
||||
el.append(topic_el)
|
||||
topic_el.tail = markdown.util.AtomicString(" @ 💬")
|
||||
else:
|
||||
text = f"#{stream_name} > {topic_name} @ 💬"
|
||||
el.text = markdown.util.AtomicString(text)
|
||||
|
||||
return el, m.start(), m.end()
|
||||
|
||||
|
||||
|
@@ -3171,6 +3171,20 @@ class MarkdownStreamMentionTests(ZulipTestCase):
|
||||
".</p>",
|
||||
)
|
||||
|
||||
def test_empty_string_topic_message_link(self) -> None:
|
||||
denmark = get_stream("Denmark", get_realm("zulip"))
|
||||
sender = self.example_user("othello")
|
||||
msg = Message(
|
||||
sender=sender,
|
||||
sending_client=get_client("test"),
|
||||
realm=sender.realm,
|
||||
)
|
||||
content = "#**Denmark>@123**"
|
||||
self.assertEqual(
|
||||
render_message_markdown(msg, content).rendered_content,
|
||||
f'<p><a class="message-link" href="/#narrow/channel/{denmark.id}-{denmark.name}/topic//near/123">#{denmark.name} > <em>{Message.EMPTY_TOPIC_FALLBACK_NAME}</em> @ 💬</a></p>',
|
||||
)
|
||||
|
||||
def test_possible_stream_names(self) -> None:
|
||||
content = """#**test here**
|
||||
This mentions #**Denmark** too.
|
||||
|
Reference in New Issue
Block a user