message_store: Migrate message_store to typescript.

This commit is contained in:
evykassirer 2023-12-09 14:33:44 -08:00 committed by Tim Abbott
parent 5dc1d36f73
commit 24dc2e783d
18 changed files with 303 additions and 299 deletions

View File

@ -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"],

View File

@ -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.

View File

@ -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";

View File

@ -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;

View File

@ -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};

View File

@ -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<string, number>();

View File

@ -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);
}

View File

@ -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 <div>; this is

View File

@ -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);
}
}

277
web/src/message_store.ts Normal file
View File

@ -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<RawMessage & {type: "private"}, "flags">
| Omit<RawMessage & {type: "stream"}, "flags">
) & {
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<MessageWithBooleans & {type: "private"}, "reactions">
| Omit<MessageWithBooleans & {type: "stream"}, "reactions">
) & {
// Added in `reactions.set_clean_reactions`.
clean_reactions: Map<string, MessageCleanReaction>;
// 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);
}
}

View File

@ -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";

View File

@ -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;

View File

@ -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};

View File

@ -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;

View File

@ -1,4 +1,4 @@
import type {Message} from "./types";
import type {Message} from "./message_store";
let is_view_visible = false;

View File

@ -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<RawMessage & {type: "private"}, "flags">
| Omit<RawMessage & {type: "stream"}, "flags">
) & {
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<MessageWithBooleans & {type: "private"}, "reactions">
| Omit<MessageWithBooleans & {type: "stream"}, "reactions">
) & {
// Added in `reactions.set_clean_reactions`.
clean_reactions: Map<string, MessageCleanReaction>;
// 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;

View File

@ -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 {

View File

@ -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);