topics: Handle case only topic name change.

Fixes https://chat.zulip.org/#narrow/channel/9-issues/topic/.F0.9F.A4.B7.20topic.20capitalization

A real topic name change happens when the name change regardless of the
case is different. But, in case of case only name change, we want to
preserve the case while not actually moving messages.

This commit makes sure that this case is handled separately instead of
handling it in the move messages workflow.

After this commit, the topic name case change will reflect immediately
in the recipient row and the left sidebar. The case change will be
reflected immediately in the Recent conversations view but not in the
Inbox view after this commit. We will fix that in further commits.
This commit is contained in:
Shubham Padia 2026-02-16 17:49:50 +00:00 committed by Tim Abbott
parent 28c8cd2e2e
commit 33a6598c00
4 changed files with 141 additions and 6 deletions

View File

@ -418,6 +418,7 @@ function topic_resolve_toggled(new_topic: string, original_topic: string): boole
function get_post_edit_topic(
topic_edited: boolean,
only_topic_case_changed: boolean,
event: UpdateMessageEvent,
new_topic: string | undefined,
anchor_message: Message | undefined,
@ -425,7 +426,7 @@ function get_post_edit_topic(
const pre_edit_topic = util.get_edit_event_orig_topic(event);
assert(pre_edit_topic !== undefined);
if (topic_edited) {
if (topic_edited || only_topic_case_changed) {
assert(new_topic !== undefined);
return new_topic;
}
@ -536,11 +537,17 @@ export function update_messages(events: UpdateMessageEvent[]): void {
// A topic or stream edit may affect multiple messages, listed in
// event.message_ids. event.message_id is still the first message
// where the user initiated the edit.
const topic_edited = new_topic !== undefined;
const orig_topic = util.get_edit_event_orig_topic(event);
const only_topic_case_changed =
new_topic !== undefined &&
orig_topic !== undefined &&
util.lower_same(new_topic, orig_topic) &&
new_topic !== orig_topic;
const topic_edited = new_topic !== undefined && !only_topic_case_changed;
const stream_changed = new_stream_id !== undefined;
const stream_archived = old_stream === undefined;
if (!topic_edited && !stream_changed) {
if (!topic_edited && !stream_changed && !only_topic_case_changed) {
// If the topic or stream of the anchor message was changed,
// it will be rerendered if present in any rendered list.
//
@ -549,10 +556,30 @@ export function update_messages(events: UpdateMessageEvent[]): void {
if (anchor_message !== undefined) {
messages_to_rerender.push(anchor_message);
}
} else if (only_topic_case_changed && !stream_changed) {
assert(old_stream_id !== undefined);
assert(orig_topic !== undefined);
assert(new_topic !== undefined);
// Update each message to reflect the new case.
for (const message_id of event.message_ids) {
const message = message_store.get(message_id);
if (message === undefined) {
continue;
}
assert(message.type === "stream");
message.topic = new_topic;
assert(event.topic_links !== undefined);
message.topic_links = event.topic_links;
messages_to_rerender.push(message);
}
// Update name case in the sidebar.
stream_topic_history.update_topic_name_case(old_stream_id, orig_topic, new_topic);
} else {
// We must be moving stream messages.
assert(old_stream_id !== undefined);
const orig_topic = util.get_edit_event_orig_topic(event);
assert(orig_topic !== undefined);
const going_forward_change =
@ -874,13 +901,14 @@ export function update_messages(events: UpdateMessageEvent[]): void {
alert_words.process_message(anchor_message);
}
if (topic_edited || stream_changed) {
// We must be moving stream messages.
if (topic_edited || stream_changed || only_topic_case_changed) {
// We must be moving stream messages or changing the case of a topic.
assert(old_stream_id !== undefined);
const pre_edit_topic = util.get_edit_event_orig_topic(event);
assert(pre_edit_topic !== undefined);
const post_edit_topic = get_post_edit_topic(
topic_edited,
only_topic_case_changed,
event,
new_topic,
anchor_message,

View File

@ -299,6 +299,23 @@ export function remove_messages(opts: {
}
}
export function update_topic_name_case(
stream_id: number,
old_topic_name: string,
new_topic_name: string,
): void {
const history = stream_dict.get(stream_id);
if (!history) {
return;
}
const existing_topic = history.topics.get(old_topic_name);
if (!existing_topic) {
return;
}
existing_topic.pretty_name = new_topic_name;
}
export function find_or_create(stream_id: number): PerStreamHistory {
let history = stream_dict.get(stream_id);

View File

@ -24,6 +24,8 @@ message_lists.current = {};
message_lists.all_rendered_message_lists = () => [message_lists.current];
const people = zrequire("people");
const linkifiers = zrequire("linkifiers");
const markdown = zrequire("markdown");
const message_events = zrequire("message_events");
const message_helper = zrequire("message_helper");
const {set_realm} = zrequire("state_data");
@ -179,3 +181,70 @@ run_test("update_messages", ({override, override_rewire}) => {
},
]);
});
run_test(
"update_messages case-only topic name change updates the topic name in UI",
({override_rewire}) => {
override_rewire(message_events, "update_views_filtered_on_message_property", () => {}, {
unused: false,
});
linkifiers.initialize([]);
markdown.initialize({
get_linkifier_map: linkifiers.get_linkifier_map,
});
const old_topic = "main failing";
const new_topic = "MAIN failing";
const raw_message = {
id: 222,
display_recipient: denmark.name,
flags: [],
sender_id: alice.user_id,
stream_id: denmark.stream_id,
topic: old_topic,
topic_links: markdown.get_topic_links(old_topic),
type: "stream",
reactions: [],
submessages: [],
avatar_url: `/avatar/${alice.user_id}`,
};
const original_message = message_helper.process_new_message({
type: "server_message",
raw_message,
}).message;
message_lists.current.view = {};
let rendered_msgs;
message_lists.current.view.rerender_messages = (msgs_to_rerender) => {
rendered_msgs = msgs_to_rerender;
};
const new_topic_links = markdown.get_topic_links(new_topic);
const events = [
{
message_id: original_message.id,
message_ids: [original_message.id],
user_id: alice.user_id,
flags: [],
edit_timestamp: 1700000000,
stream_id: denmark.stream_id,
orig_subject: old_topic,
topic: new_topic,
topic_links: new_topic_links,
rendering_only: false,
},
];
const $message_edit_history_modal = $.create("#message-edit-history");
$message_edit_history_modal.set_parents_result(".micromodal", $.create("micromodal"));
message_events.update_messages(events);
assert.equal(original_message.topic, new_topic);
assert.deepEqual(original_message.topic_links, new_topic_links);
const topic_names = stream_topic_history.get_recent_topic_names(denmark.stream_id);
assert.equal(topic_names[0], new_topic);
assert.deepEqual(rendered_msgs, [original_message]);
},
);

View File

@ -314,6 +314,27 @@ test("test_stream_has_resolved_topics", () => {
);
});
test("update_topic_name_case", () => {
const stream_id = 90;
// No history for this stream_id, should return without error.
stream_topic_history.update_topic_name_case(stream_id, "old topic", "new topic");
stream_topic_history.add_message({
stream_id,
message_id: 900,
topic_name: "known topic",
});
// Topic key is missing, should return without changing data.
stream_topic_history.update_topic_name_case(stream_id, "missing topic", "whatever");
assert.deepEqual(stream_topic_history.get_recent_topic_names(stream_id), ["known topic"]);
// Topic key is present, should update pretty name.
stream_topic_history.update_topic_name_case(stream_id, "known topic", "Known Topic");
assert.deepEqual(stream_topic_history.get_recent_topic_names(stream_id), ["Known Topic"]);
});
test("server_history_end_to_end", () => {
stream_topic_history.reset();