mirror of
https://github.com/zulip/zulip.git
synced 2025-10-22 20:42:14 +00:00
I confirmed this isn't used in any .hbs templates, so it doesn't seem to be being used anywhere.
356 lines
12 KiB
TypeScript
356 lines
12 KiB
TypeScript
import _ from "lodash";
|
|
import * as z from "zod/mini";
|
|
|
|
import * as blueslip from "./blueslip.ts";
|
|
import * as people from "./people.ts";
|
|
import {topic_link_schema} from "./types.ts";
|
|
import type {UserStatusEmojiInfo} from "./user_status.ts";
|
|
import * as util from "./util.ts";
|
|
|
|
const stored_messages = new Map<number, Message>();
|
|
|
|
const matched_message_schema = z.object({
|
|
match_content: z.optional(z.string()),
|
|
match_subject: z.optional(z.string()),
|
|
});
|
|
|
|
export type MatchedMessage = z.infer<typeof matched_message_schema>;
|
|
|
|
const message_reaction_type_schema = z.enum(["unicode_emoji", "realm_emoji", "zulip_extra_emoji"]);
|
|
|
|
export type MessageReactionType = z.infer<typeof message_reaction_type_schema>;
|
|
|
|
const display_recipient_user_schema = z.object({
|
|
email: z.string(),
|
|
full_name: z.string(),
|
|
id: z.number(),
|
|
});
|
|
|
|
export type DisplayRecipientUser = z.infer<typeof display_recipient_user_schema>;
|
|
|
|
const display_recipient_schema = z.union([z.string(), z.array(display_recipient_user_schema)]);
|
|
|
|
export type DisplayRecipient = z.infer<typeof display_recipient_schema>;
|
|
|
|
const message_edit_history_entry_schema = z.object({
|
|
user_id: z.nullable(z.number()),
|
|
timestamp: z.number(),
|
|
prev_content: z.optional(z.string()),
|
|
prev_rendered_content: z.optional(z.string()),
|
|
prev_stream: z.optional(z.number()),
|
|
prev_topic: z.optional(z.string()),
|
|
stream: z.optional(z.number()),
|
|
topic: z.optional(z.string()),
|
|
});
|
|
|
|
export type MessageEditHistoryEntry = z.infer<typeof message_edit_history_entry_schema>;
|
|
|
|
const message_reaction_schema = z.object({
|
|
emoji_name: z.string(),
|
|
emoji_code: z.string(),
|
|
reaction_type: message_reaction_type_schema,
|
|
user_id: z.number(),
|
|
});
|
|
|
|
export type MessageReaction = z.infer<typeof message_reaction_schema>;
|
|
|
|
export const submessage_schema = z.object({
|
|
id: z.number(),
|
|
sender_id: z.number(),
|
|
message_id: z.number(),
|
|
content: z.string(),
|
|
msg_type: z.string(),
|
|
});
|
|
|
|
export const raw_message_schema = z.intersection(
|
|
z.intersection(
|
|
z.object({
|
|
avatar_url: z.nullable(z.string()),
|
|
client: z.string(),
|
|
content: z.string(),
|
|
content_type: z.literal("text/html"),
|
|
display_recipient: display_recipient_schema,
|
|
edit_history: z.optional(z.array(message_edit_history_entry_schema)),
|
|
id: z.number(),
|
|
is_me_message: z.boolean(),
|
|
last_edit_timestamp: z.optional(z.number()),
|
|
last_moved_timestamp: z.optional(z.number()),
|
|
reactions: z.array(message_reaction_schema),
|
|
sender_email: z.string(),
|
|
sender_full_name: z.string(),
|
|
sender_id: z.number(),
|
|
sender_realm_str: z.string(),
|
|
submessages: z.array(submessage_schema),
|
|
timestamp: z.number(),
|
|
flags: z.array(z.string()),
|
|
}),
|
|
z.discriminatedUnion("type", [
|
|
z.object({
|
|
type: z.literal("private"),
|
|
topic_links: z.optional(z.array(z.undefined())),
|
|
}),
|
|
z.object({
|
|
type: z.literal("stream"),
|
|
stream_id: z.number(),
|
|
// Messages that come from the server use `subject`.
|
|
// Messages that come from `send_message` use `topic`.
|
|
subject: z.optional(z.string()),
|
|
topic: z.optional(z.string()),
|
|
topic_links: z.array(topic_link_schema),
|
|
}),
|
|
]),
|
|
),
|
|
matched_message_schema,
|
|
);
|
|
|
|
export type RawMessage = z.infer<typeof raw_message_schema>;
|
|
|
|
// We add these boolean properties to Raw message in
|
|
// `message_store.convert_raw_message_to_message_with_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;
|
|
condensed?: 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: "zulip_extra_emoji" | "realm_emoji" | "unicode_emoji";
|
|
user_ids: number[];
|
|
vote_text: string;
|
|
};
|
|
|
|
export type Message = (
|
|
| Omit<MessageWithBooleans & {type: "private"}, "reactions">
|
|
| Omit<MessageWithBooleans & {type: "stream"}, "reactions" | "subject">
|
|
) & {
|
|
clean_reactions: Map<string, MessageCleanReaction>;
|
|
|
|
// Local echo state cluster of fields.
|
|
locally_echoed?: boolean;
|
|
failed_request?: boolean;
|
|
show_slow_send_spinner?: boolean;
|
|
resend?: boolean;
|
|
local_id?: string;
|
|
|
|
// The original markup for the message, which we'll have if we
|
|
// sent it or if we fetched it (usually, because the current user
|
|
// tried to edit the message).
|
|
raw_content?: string | undefined;
|
|
|
|
// Added in `message_helper.process_new_message`.
|
|
sent_by_me: boolean;
|
|
reply_to: string;
|
|
|
|
// These properties are set and used in `message_list_view.ts`.
|
|
// TODO: It would be nice if we could not store these on the message
|
|
// object and only reference them within `message_list_view`.
|
|
message_reactions?: MessageCleanReaction[];
|
|
url?: string;
|
|
|
|
// Used in `markdown.js`, `server_events.js`, and
|
|
// `convert_raw_message_to_message_with_booleans`
|
|
flags?: string[];
|
|
|
|
small_avatar_url?: string | null; // Used in `message_avatar.hbs`
|
|
status_emoji_info?: UserStatusEmojiInfo | undefined; // Used in `message_body.hbs`
|
|
|
|
local_edit_timestamp?: number; // Used for edited messages
|
|
|
|
notification_sent?: boolean; // Used in message_notifications
|
|
} & (
|
|
| {
|
|
type: "private";
|
|
is_private: true;
|
|
is_stream: false;
|
|
pm_with_url: string;
|
|
to_user_ids: string;
|
|
display_reply_to: string;
|
|
}
|
|
| {
|
|
type: "stream";
|
|
is_private: false;
|
|
is_stream: true;
|
|
stream: string;
|
|
topic: string;
|
|
display_reply_to: undefined;
|
|
}
|
|
);
|
|
|
|
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 | undefined {
|
|
// 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 {
|
|
return stored_messages.get(message_id);
|
|
}
|
|
|
|
export function get_pm_emails(message: Message | MessageWithBooleans): 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(user_ids: number[]): string {
|
|
user_ids = people.sorted_other_user_ids(user_ids);
|
|
const names = people.get_display_full_names(user_ids);
|
|
const sorted_names = names.sort(util.make_strcmp());
|
|
|
|
return sorted_names.join(", ");
|
|
}
|
|
|
|
export function convert_raw_message_to_message_with_booleans(
|
|
message: RawMessage,
|
|
): MessageWithBooleans {
|
|
const flags = message.flags ?? [];
|
|
|
|
function convert_flag(flag_name: string): boolean {
|
|
return flags.includes(flag_name);
|
|
}
|
|
|
|
const converted_flags = {
|
|
unread: !convert_flag("read"),
|
|
historical: convert_flag("historical"),
|
|
starred: convert_flag("starred"),
|
|
mentioned:
|
|
convert_flag("mentioned") ||
|
|
convert_flag("stream_wildcard_mentioned") ||
|
|
convert_flag("topic_wildcard_mentioned"),
|
|
mentioned_me_directly: convert_flag("mentioned"),
|
|
stream_wildcard_mentioned: convert_flag("stream_wildcard_mentioned"),
|
|
topic_wildcard_mentioned: convert_flag("topic_wildcard_mentioned"),
|
|
collapsed: convert_flag("collapsed"),
|
|
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.)
|
|
|
|
// We have to return these separately because of how the `MessageWithBooleans`
|
|
// type is set up.
|
|
if (message.type === "private") {
|
|
return {
|
|
..._.omit(message, "flags"),
|
|
...converted_flags,
|
|
};
|
|
}
|
|
return {
|
|
..._.omit(message, "flags"),
|
|
...converted_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 | null): 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.type === "stream" && msg.stream_id === stream_id) {
|
|
msg.display_recipient = new_name;
|
|
}
|
|
}
|
|
}
|
|
|
|
export function update_status_emoji_info(
|
|
user_id: number,
|
|
new_info: UserStatusEmojiInfo | undefined,
|
|
): 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 {
|
|
const message = stored_messages.get(old_id);
|
|
if (message !== undefined) {
|
|
message.id = new_id;
|
|
message.locally_echoed = false;
|
|
stored_messages.set(new_id, message);
|
|
stored_messages.delete(old_id);
|
|
}
|
|
}
|
|
|
|
export function remove(message_ids: number[]): void {
|
|
for (const message_id of message_ids) {
|
|
stored_messages.delete(message_id);
|
|
}
|
|
}
|
|
|
|
export function get_message_ids_in_stream(stream_id: number): number[] {
|
|
return [...stored_messages.values()]
|
|
.filter((message) => message.type === "stream" && message.stream_id === stream_id)
|
|
.map((message) => message.id);
|
|
}
|