diff --git a/tools/linter_lib/custom_check.py b/tools/linter_lib/custom_check.py index 1599123dff..01c7b18698 100644 --- a/tools/linter_lib/custom_check.py +++ b/tools/linter_lib/custom_check.py @@ -119,7 +119,12 @@ js_rules = RuleList( rules=[ { "pattern": "subject|SUBJECT", - "exclude": {"web/src/types.ts", "web/src/util.ts", "web/tests/"}, + "exclude": { + "web/src/message_store.ts", + "web/src/types.ts", + "web/src/util.ts", + "web/tests/", + }, "exclude_pattern": "emails", "description": "avoid subject in JS code", "good_lines": ["topic_name"], diff --git a/web/src/alert_words.ts b/web/src/alert_words.ts index 255f8c0c24..51102c0d22 100644 --- a/web/src/alert_words.ts +++ b/web/src/alert_words.ts @@ -1,7 +1,7 @@ import _ from "lodash"; +import type {Message} from "./message_store"; import * as people from "./people"; -import type {Message} from "./types"; // For simplicity, we use a list for our internal // data, since that matches what the server sends us. diff --git a/web/src/compose_fade_helper.ts b/web/src/compose_fade_helper.ts index 5f6507191a..9b42738d65 100644 --- a/web/src/compose_fade_helper.ts +++ b/web/src/compose_fade_helper.ts @@ -1,8 +1,8 @@ import assert from "minimalistic-assert"; +import type {Message} from "./message_store"; import * as stream_data from "./stream_data"; import * as sub_store from "./sub_store"; -import type {Message} from "./types"; import type {Recipient} from "./util"; import * as util from "./util"; diff --git a/web/src/fetch_status.ts b/web/src/fetch_status.ts index 49265ae312..a553564853 100644 --- a/web/src/fetch_status.ts +++ b/web/src/fetch_status.ts @@ -1,5 +1,5 @@ import * as message_feed_loading from "./message_feed_loading"; -import type {RawMessage} from "./types"; +import type {RawMessage} from "./message_store"; function max_id_for_messages(messages: RawMessage[]): number { let max_id = 0; diff --git a/web/src/hash_util.ts b/web/src/hash_util.ts index 73dd6d81c2..1d1bd9c6cb 100644 --- a/web/src/hash_util.ts +++ b/web/src/hash_util.ts @@ -1,10 +1,10 @@ import * as internal_url from "../shared/src/internal_url"; +import type {Message} from "./message_store"; import * as people from "./people"; import * as stream_data from "./stream_data"; import * as sub_store from "./sub_store"; import type {StreamSubscription} from "./sub_store"; -import type {Message} from "./types"; import type {UserGroup} from "./user_groups"; type Operator = {operator: string; operand: string; negated?: boolean}; diff --git a/web/src/huddle_data.ts b/web/src/huddle_data.ts index 5e0014ed80..9dfae66064 100644 --- a/web/src/huddle_data.ts +++ b/web/src/huddle_data.ts @@ -1,7 +1,7 @@ import _ from "lodash"; +import type {Message} from "./message_store"; import * as people from "./people"; -import type {Message} from "./types"; const huddle_timestamps = new Map(); diff --git a/web/src/message_live_update.js b/web/src/message_live_update.js index 193cf09727..d59dfaf511 100644 --- a/web/src/message_live_update.js +++ b/web/src/message_live_update.js @@ -66,23 +66,23 @@ export function update_starred_view(message_id, new_value) { } export function update_stream_name(stream_id, new_name) { - message_store.update_property("stream_name", new_name, {stream_id}); + message_store.update_stream_name(stream_id, new_name); rerender_messages_view(); } export function update_user_full_name(user_id, full_name) { - message_store.update_property("sender_full_name", full_name, {user_id}); + message_store.update_sender_full_name(user_id, full_name); rerender_messages_view_for_user(user_id); } export function update_avatar(user_id, avatar_url) { let url = avatar_url; url = people.format_small_avatar_url(url); - message_store.update_property("small_avatar_url", url, {user_id}); + message_store.update_small_avatar_url(user_id, url); rerender_messages_view_for_user(user_id); } export function update_user_status_emoji(user_id, status_emoji_info) { - message_store.update_property("status_emoji_info", status_emoji_info, {user_id}); + message_store.update_status_emoji_info(user_id, status_emoji_info); rerender_messages_view_for_user(user_id); } diff --git a/web/src/message_parser.ts b/web/src/message_parser.ts index eb5c6d708b..211a13fa5c 100644 --- a/web/src/message_parser.ts +++ b/web/src/message_parser.ts @@ -1,11 +1,7 @@ // We only use jquery for parsing. import $ from "jquery"; -// TODO: Move this to message_store when it is -// converted to TypeScript. -type Message = { - content: string; -}; +import type {Message} from "./message_store"; // We need to check if the message content contains the specified HTML // elements. We wrap the message.content in a
; this is diff --git a/web/src/message_store.js b/web/src/message_store.js deleted file mode 100644 index 87b0abf61c..0000000000 --- a/web/src/message_store.js +++ /dev/null @@ -1,137 +0,0 @@ -import * as blueslip from "./blueslip"; -import * as people from "./people"; - -const stored_messages = new Map(); - -export function update_message_cache(message) { - // You should only call this from message_helper (or in tests). - stored_messages.set(message.id, message); -} - -export function get_cached_message(message_id) { - // You should only call this from message_helper. - // Use the get() wrapper below for most other use cases. - return stored_messages.get(message_id); -} - -export function clear_for_testing() { - stored_messages.clear(); -} - -export function get(message_id) { - if (message_id === undefined || message_id === null) { - blueslip.error("message_store.get got bad value", {message_id}); - return undefined; - } - - if (typeof message_id !== "number") { - blueslip.error("message_store got non-number", {message_id}); - - // Try to soldier on, assuming the caller treats message - // ids as strings. - message_id = Number.parseFloat(message_id); - } - - return stored_messages.get(message_id); -} - -export function get_pm_emails(message) { - const user_ids = people.pm_with_user_ids(message); - const emails = user_ids - .map((user_id) => { - const person = people.maybe_get_user_by_id(user_id); - if (!person) { - blueslip.error("Unknown user id", {user_id}); - return "?"; - } - return person.email; - }) - .sort(); - - return emails.join(", "); -} - -export function get_pm_full_names(message) { - const user_ids = people.pm_with_user_ids(message); - const names = people.get_display_full_names(user_ids).sort(); - - return names.join(", "); -} - -export function set_message_booleans(message) { - const flags = message.flags || []; - - function convert_flag(flag_name) { - return flags.includes(flag_name); - } - - message.unread = !convert_flag("read"); - message.historical = convert_flag("historical"); - message.starred = convert_flag("starred"); - message.mentioned = - convert_flag("mentioned") || - convert_flag("stream_wildcard_mentioned") || - convert_flag("topic_wildcard_mentioned"); - message.mentioned_me_directly = convert_flag("mentioned"); - message.stream_wildcard_mentioned = convert_flag("stream_wildcard_mentioned"); - message.topic_wildcard_mentioned = convert_flag("topic_wildcard_mentioned"); - message.collapsed = convert_flag("collapsed"); - message.alerted = convert_flag("has_alert_word"); - - // Once we have set boolean flags here, the `flags` attribute is - // just a distraction, so we delete it. (All the downstream code - // uses booleans.) - delete message.flags; -} - -export function update_booleans(message, flags) { - // When we get server flags for local echo or message edits, - // we are vulnerable to race conditions, so only update flags - // that are driven by message content. - function convert_flag(flag_name) { - return flags.includes(flag_name); - } - - message.mentioned = - convert_flag("mentioned") || - convert_flag("stream_wildcard_mentioned") || - convert_flag("topic_wildcard_mentioned"); - message.mentioned_me_directly = convert_flag("mentioned"); - message.stream_wildcard_mentioned = convert_flag("stream_wildcard_mentioned"); - message.topic_wildcard_mentioned = convert_flag("topic_wildcard_mentioned"); - message.alerted = convert_flag("has_alert_word"); -} - -export function update_property(property, value, info) { - switch (property) { - case "sender_full_name": - case "small_avatar_url": - for (const msg of stored_messages.values()) { - if (msg.sender_id && msg.sender_id === info.user_id) { - msg[property] = value; - } - } - break; - case "stream_name": - for (const msg of stored_messages.values()) { - if (msg.stream_id && msg.stream_id === info.stream_id) { - msg.display_recipient = value; - } - } - break; - case "status_emoji_info": - for (const msg of stored_messages.values()) { - if (msg.sender_id && msg.sender_id === info.user_id) { - msg[property] = value; - } - } - break; - } -} - -export function reify_message_id({old_id, new_id}) { - if (stored_messages.has(old_id)) { - stored_messages.set(new_id, stored_messages.get(old_id)); - stored_messages.delete(old_id); - } -} diff --git a/web/src/message_store.ts b/web/src/message_store.ts new file mode 100644 index 0000000000..dfe6f6b905 --- /dev/null +++ b/web/src/message_store.ts @@ -0,0 +1,277 @@ +import * as blueslip from "./blueslip"; +import * as people from "./people"; +import type {Submessage, TopicLink} from "./types"; + +const stored_messages = new Map(); + +export type MatchedMessage = { + match_content?: string; + match_subject?: string; +}; + +export type MessageReactionType = "unicode_emoji" | "realm_emoji" | "zulip_extra_emoji"; + +export type DisplayRecipientUser = { + email: string; + full_name: string; + id: number; + is_mirror_dummy: boolean; + unknown_local_echo_user?: boolean; +}; + +export type DisplayRecipient = string | DisplayRecipientUser[]; + +export type MessageEditHistoryEntry = { + user_id: number | null; + timestamp: number; + prev_content?: string; + prev_rendered_content?: string; + prev_rendered_content_version?: number; + prev_stream?: number; + prev_topic?: string; + stream?: number; + topic?: string; +}; + +export type MessageReaction = { + emoji_name: string; + emoji_code: string; + reaction_type: MessageReactionType; + user_id: number; +}; + +export type RawMessage = { + avatar_url: string | null; + client: string; + content: string; + content_type: "text/html"; + display_recipient: DisplayRecipient; + edit_history?: MessageEditHistoryEntry[]; + id: number; + is_me_message: boolean; + last_edit_timestamp?: number; + reactions: MessageReaction[]; + recipient_id: number; + sender_email: string; + sender_full_name: string; + sender_id: number; + sender_realm_str: string; + submessages: Submessage[]; + timestamp: number; + flags: string[]; +} & ( + | { + type: "private"; + } + | { + type: "stream"; + stream_id: number; + subject: string; + topic_links: TopicLink[]; + } +) & + MatchedMessage; + +// We add these boolean properties to Raw message in `message_store.set_message_booleans` method. +export type MessageWithBooleans = ( + | Omit + | Omit +) & { + unread: boolean; + historical: boolean; + starred: boolean; + mentioned: boolean; + mentioned_me_directly: boolean; + stream_wildcard_mentioned: boolean; + topic_wildcard_mentioned: boolean; + collapsed: boolean; + alerted: boolean; +}; + +export type MessageCleanReaction = { + class: string; + count: number; + emoji_alt_code: boolean; + emoji_code: string; + emoji_name: string; + is_realm_emoji: boolean; + label: string; + local_id: string; + reaction_type: string; + user_ids: number[]; + vote_text: string; +}; + +export type Message = ( + | Omit + | Omit +) & { + // Added in `reactions.set_clean_reactions`. + clean_reactions: Map; + + // Added in `message_helper.process_new_message`. + sent_by_me: boolean; + reply_to: string; + display_reply_to?: string; + + // These properties are used in `message_list_view.js`. + starred_status: string; + message_reactions: MessageCleanReaction[]; + url: string; + + // Used in `markdown.js`, `server_events.js`, and `set_message_booleans` + flags?: string[]; +} & ( + | { + type: "private"; + is_private: true; + is_stream: false; + pm_with_url: string; + to_user_ids: string; + } + | { + type: "stream"; + is_private: false; + is_stream: true; + stream: string; + topic: string; + } + ); + +export function update_message_cache(message: Message): void { + // You should only call this from message_helper (or in tests). + stored_messages.set(message.id, message); +} + +export function get_cached_message(message_id: number): Message { + // You should only call this from message_helper. + // Use the get() wrapper below for most other use cases. + return stored_messages.get(message_id); +} + +export function clear_for_testing(): void { + stored_messages.clear(); +} + +export function get(message_id: number): Message | undefined { + if (message_id === undefined || message_id === null) { + blueslip.error("message_store.get got bad value", {message_id}); + return undefined; + } + + if (typeof message_id !== "number") { + blueslip.error("message_store got non-number", {message_id}); + + // Try to soldier on, assuming the caller treats message + // ids as strings. + message_id = Number.parseFloat(message_id); + } + + return stored_messages.get(message_id); +} + +export function get_pm_emails(message: Message): string { + const user_ids = people.pm_with_user_ids(message) ?? []; + const emails = user_ids + .map((user_id) => { + const person = people.maybe_get_user_by_id(user_id); + if (!person) { + blueslip.error("Unknown user id", {user_id}); + return "?"; + } + return person.email; + }) + .sort(); + + return emails.join(", "); +} + +export function get_pm_full_names(message: Message): string { + const user_ids = people.pm_with_user_ids(message) ?? []; + const names = people.get_display_full_names(user_ids).sort(); + + return names.join(", "); +} + +export function set_message_booleans(message: Message): void { + const flags = message.flags || []; + + function convert_flag(flag_name: string): boolean { + return flags.includes(flag_name); + } + + message.unread = !convert_flag("read"); + message.historical = convert_flag("historical"); + message.starred = convert_flag("starred"); + message.mentioned = + convert_flag("mentioned") || + convert_flag("stream_wildcard_mentioned") || + convert_flag("topic_wildcard_mentioned"); + message.mentioned_me_directly = convert_flag("mentioned"); + message.stream_wildcard_mentioned = convert_flag("stream_wildcard_mentioned"); + message.topic_wildcard_mentioned = convert_flag("topic_wildcard_mentioned"); + message.collapsed = convert_flag("collapsed"); + message.alerted = convert_flag("has_alert_word"); + + // Once we have set boolean flags here, the `flags` attribute is + // just a distraction, so we delete it. (All the downstream code + // uses booleans.) + delete message.flags; +} + +export function update_booleans(message: Message, flags: string[]): void { + // When we get server flags for local echo or message edits, + // we are vulnerable to race conditions, so only update flags + // that are driven by message content. + function convert_flag(flag_name: string): boolean { + return flags.includes(flag_name); + } + + message.mentioned = + convert_flag("mentioned") || + convert_flag("stream_wildcard_mentioned") || + convert_flag("topic_wildcard_mentioned"); + message.mentioned_me_directly = convert_flag("mentioned"); + message.stream_wildcard_mentioned = convert_flag("stream_wildcard_mentioned"); + message.topic_wildcard_mentioned = convert_flag("topic_wildcard_mentioned"); + message.alerted = convert_flag("has_alert_word"); +} + +export function update_sender_full_name(user_id: number, new_name: string): void { + for (const msg of stored_messages.values()) { + if (msg.sender_id && msg.sender_id === user_id) { + msg.sender_full_name = new_name; + } + } +} + +export function update_small_avatar_url(user_id: number, new_url: string): void { + for (const msg of stored_messages.values()) { + if (msg.sender_id && msg.sender_id === user_id) { + msg.small_avatar_url = new_url; + } + } +} + +export function update_stream_name(stream_id: number, new_name: string): void { + for (const msg of stored_messages.values()) { + if (msg.stream_id && msg.stream_id === stream_id) { + msg.display_recipient = new_name; + } + } +} + +export function update_status_emoji_info(user_id: number, new_info: string): void { + for (const msg of stored_messages.values()) { + if (msg.sender_id && msg.sender_id === user_id) { + msg.status_emoji_info = new_info; + } + } +} + +export function reify_message_id({old_id, new_id}: {old_id: number; new_id: number}): void { + if (stored_messages.has(old_id)) { + stored_messages.set(new_id, stored_messages.get(old_id)); + stored_messages.delete(old_id); + } +} diff --git a/web/src/people.ts b/web/src/people.ts index 733937957f..94629b0537 100644 --- a/web/src/people.ts +++ b/web/src/people.ts @@ -6,6 +6,7 @@ import * as typeahead from "../shared/src/typeahead"; import * as blueslip from "./blueslip"; import {FoldDict} from "./fold_dict"; import {$t} from "./i18n"; +import type {DisplayRecipientUser, Message, MessageWithBooleans} from "./message_store"; import * as message_user_ids from "./message_user_ids"; import * as muted_users from "./muted_users"; import {page_params} from "./page_params"; @@ -13,7 +14,6 @@ import * as reload_state from "./reload_state"; import * as settings_config from "./settings_config"; import * as settings_data from "./settings_data"; import * as timerender from "./timerender"; -import type {DisplayRecipientUser, Message, MessageWithBooleans} from "./types"; import {user_settings} from "./user_settings"; import * as util from "./util"; diff --git a/web/src/pm_conversations.ts b/web/src/pm_conversations.ts index 1315fe8f7e..7c574299b0 100644 --- a/web/src/pm_conversations.ts +++ b/web/src/pm_conversations.ts @@ -1,7 +1,7 @@ import {FoldDict} from "./fold_dict"; +import type {Message} from "./message_store"; import * as muted_users from "./muted_users"; import * as people from "./people"; -import type {Message} from "./types"; type PMConversation = { user_ids_string: string; diff --git a/web/src/poll_widget.ts b/web/src/poll_widget.ts index ab2c118350..6ec0f0133a 100644 --- a/web/src/poll_widget.ts +++ b/web/src/poll_widget.ts @@ -13,8 +13,8 @@ import render_widgets_poll_widget_results from "../templates/widgets/poll_widget import * as blueslip from "./blueslip"; import {$t} from "./i18n"; import * as keydown_util from "./keydown_util"; +import type {Message} from "./message_store"; import * as people from "./people"; -import type {Message} from "./types"; type Event = {sender_id: number; data: InboundData}; diff --git a/web/src/recent_view_data.ts b/web/src/recent_view_data.ts index ffe078e66f..9f862d2205 100644 --- a/web/src/recent_view_data.ts +++ b/web/src/recent_view_data.ts @@ -1,6 +1,6 @@ +import type {Message} from "./message_store"; import * as people from "./people"; import {get_key_from_message} from "./recent_view_util"; -import type {Message} from "./types"; export type ConversationData = { last_msg_id: number; diff --git a/web/src/recent_view_util.ts b/web/src/recent_view_util.ts index 6d0f69e7c4..43cbed4a32 100644 --- a/web/src/recent_view_util.ts +++ b/web/src/recent_view_util.ts @@ -1,4 +1,4 @@ -import type {Message} from "./types"; +import type {Message} from "./message_store"; let is_view_visible = false; diff --git a/web/src/types.ts b/web/src/types.ts index 36145ed66e..bc1bb2a706 100644 --- a/web/src/types.ts +++ b/web/src/types.ts @@ -1,43 +1,3 @@ -// TODO/typescript: Move this to message_store -export type MatchedMessage = { - match_content?: string; - match_subject?: string; -}; - -// TODO/typescript: Move this to message_store -export type MessageReactionType = "unicode_emoji" | "realm_emoji" | "zulip_extra_emoji"; - -// TODO/typescript: Move these types to message_store - -export type DisplayRecipientUser = { - email: string; - full_name: string; - id: number; - is_mirror_dummy: boolean; - unknown_local_echo_user?: boolean; -}; - -export type DisplayRecipient = string | DisplayRecipientUser[]; - -export type MessageEditHistoryEntry = { - user_id: number | null; - timestamp: number; - prev_content?: string; - prev_rendered_content?: string; - prev_rendered_content_version?: number; - prev_stream?: number; - prev_topic?: string; - stream?: number; - topic?: string; -}; - -export type MessageReaction = { - emoji_name: string; - emoji_code: string; - reaction_type: MessageReactionType; - user_id: number; -}; - // TODO/typescript: Move this to submessage.js export type Submessage = { id: number; @@ -53,104 +13,6 @@ export type TopicLink = { url: string; }; -// TODO/typescript: Move this to message_store -export type RawMessage = { - avatar_url: string | null; - client: string; - content: string; - content_type: "text/html"; - display_recipient: DisplayRecipient; - edit_history?: MessageEditHistoryEntry[]; - id: number; - is_me_message: boolean; - last_edit_timestamp?: number; - reactions: MessageReaction[]; - recipient_id: number; - sender_email: string; - sender_full_name: string; - sender_id: number; - sender_realm_str: string; - submessages: Submessage[]; - timestamp: number; - flags: string[]; -} & ( - | { - type: "private"; - } - | { - type: "stream"; - stream_id: number; - subject: string; - topic_links: TopicLink[]; - } -) & - MatchedMessage; - -// We add these boolean properties to Raw message in `message_store.set_message_booleans` method. -export type MessageWithBooleans = ( - | Omit - | Omit -) & { - unread: boolean; - historical: boolean; - starred: boolean; - mentioned: boolean; - mentioned_me_directly: boolean; - stream_wildcard_mentioned: boolean; - topic_wildcard_mentioned: boolean; - collapsed: boolean; - alerted: boolean; -}; - -// TODO/typescript: Move this to message_store -export type MessageCleanReaction = { - class: string; - count: number; - emoji_alt_code: boolean; - emoji_code: string; - emoji_name: string; - is_realm_emoji: boolean; - label: string; - local_id: string; - reaction_type: string; - user_ids: number[]; - vote_text: string; -}; - -// TODO/typescript: Move this to message_store -export type Message = ( - | Omit - | Omit -) & { - // Added in `reactions.set_clean_reactions`. - clean_reactions: Map; - - // Added in `message_helper.process_new_message`. - sent_by_me: boolean; - reply_to: string; - display_reply_to?: string; - - // These properties are used in `message_list_view.js`. - starred_status: string; - message_reactions: MessageCleanReaction[]; - url: string; -} & ( - | { - type: "private"; - is_private: true; - is_stream: false; - pm_with_url: string; - to_user_ids: string; - } - | { - type: "stream"; - is_private: false; - is_stream: true; - stream: string; - topic: string; - } - ); - // TODO/typescript: Move this to server_events_dispatch export type UserGroupUpdateEvent = { id: number; diff --git a/web/src/util.ts b/web/src/util.ts index 7310132ba7..e8b088e9e8 100644 --- a/web/src/util.ts +++ b/web/src/util.ts @@ -2,7 +2,8 @@ import _ from "lodash"; import * as blueslip from "./blueslip"; import {$t} from "./i18n"; -import type {MatchedMessage, Message, RawMessage, UpdateMessageEvent} from "./types"; +import type {MatchedMessage, Message, RawMessage} from "./message_store"; +import type {UpdateMessageEvent} from "./types"; // From MDN: https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Math/random export function random_int(min: number, max: number): number { diff --git a/web/tests/message_store.test.js b/web/tests/message_store.test.js index eef5e455cd..a7f1f1fa49 100644 --- a/web/tests/message_store.test.js +++ b/web/tests/message_store.test.js @@ -336,13 +336,13 @@ test("update_property", () => { assert.equal(message1.sender_full_name, alice.full_name); assert.equal(message2.sender_full_name, bob.full_name); - message_store.update_property("sender_full_name", "Bobby", {user_id: bob.user_id}); + message_store.update_sender_full_name(bob.user_id, "Bobby"); assert.equal(message1.sender_full_name, alice.full_name); assert.equal(message2.sender_full_name, "Bobby"); assert.equal(message1.small_avatar_url, "alice_url"); assert.equal(message2.small_avatar_url, "bob_url"); - message_store.update_property("small_avatar_url", "bobby_url", {user_id: bob.user_id}); + message_store.update_small_avatar_url(bob.user_id, "bobby_url"); assert.equal(message1.small_avatar_url, "alice_url"); assert.equal(message2.small_avatar_url, "bobby_url"); @@ -350,7 +350,7 @@ test("update_property", () => { assert.equal(message1.display_recipient, devel.name); assert.equal(message2.stream_id, denmark.stream_id); assert.equal(message2.display_recipient, denmark.name); - message_store.update_property("stream_name", "Prod", {stream_id: devel.stream_id}); + message_store.update_stream_name(devel.stream_id, "Prod"); assert.equal(message1.stream_id, devel.stream_id); assert.equal(message1.display_recipient, "Prod"); assert.equal(message2.stream_id, denmark.stream_id);