Files
discourse/plugins/chat/assets/javascripts/discourse/models/chat-message.js
David Taylor a673004777 DEV: Modernize chat getOwner usage (#23671)
See 8958b4f76af85ddc89c8a3b6434dcfbd4274d569 for motivation
2023-09-26 18:05:34 +01:00

380 lines
9.6 KiB
JavaScript

import User from "discourse/models/user";
import { cached, tracked } from "@glimmer/tracking";
import { TrackedArray, TrackedObject } from "@ember-compat/tracked-built-ins";
import ChatMessageReaction from "discourse/plugins/chat/discourse/models/chat-message-reaction";
import Bookmark from "discourse/models/bookmark";
import I18n from "I18n";
import { generateCookFunction, parseMentions } from "discourse/lib/text";
import transformAutolinks from "discourse/plugins/chat/discourse/lib/transform-auto-links";
import { getOwnerWithFallback } from "discourse-common/lib/get-owner";
import discourseLater from "discourse-common/lib/later";
export default class ChatMessage {
static cookFunction = null;
static create(channel, args = {}) {
return new ChatMessage(channel, args);
}
static createDraftMessage(channel, args = {}) {
args.draft = true;
return ChatMessage.create(channel, args);
}
@tracked id;
@tracked error;
@tracked selected;
@tracked channel;
@tracked staged;
@tracked draftSaved;
@tracked draft;
@tracked createdAt;
@tracked uploads;
@tracked excerpt;
@tracked reactions;
@tracked reviewableId;
@tracked user;
@tracked inReplyTo;
@tracked expanded = true;
@tracked bookmark;
@tracked userFlagStatus;
@tracked hidden;
@tracked version = 0;
@tracked edited;
@tracked editing;
@tracked chatWebhookEvent = new TrackedObject();
@tracked mentionWarning;
@tracked availableFlags;
@tracked newest;
@tracked highlighted;
@tracked firstOfResults;
@tracked message;
@tracked manager;
@tracked deletedById;
@tracked _deletedAt;
@tracked _cooked;
@tracked _thread;
constructor(channel, args = {}) {
this.id = args.id;
this.channel = channel;
this.manager = args.manager;
this.newest = args.newest || false;
this.draftSaved = args.draftSaved || args.draft_saved || false;
this.firstOfResults = args.firstOfResults || args.first_of_results || false;
this.staged = args.staged || false;
this.edited = args.edited || false;
this.editing = args.editing || false;
this.availableFlags = args.availableFlags || args.available_flags;
this.hidden = args.hidden || false;
this.chatWebhookEvent = args.chatWebhookEvent || args.chat_webhook_event;
this.createdAt = args.created_at
? new Date(args.created_at)
: new Date(args.createdAt);
this.deletedById = args.deletedById || args.deleted_by_id;
this._deletedAt = args.deletedAt || args.deleted_at;
this.expanded =
this.hidden || this._deletedAt ? false : args.expanded || true;
this.excerpt = args.excerpt;
this.reviewableId = args.reviewableId || args.reviewable_id;
this.userFlagStatus = args.userFlagStatus || args.user_flag_status;
this.draft = args.draft;
this.message = args.message || "";
this._cooked = args.cooked || "";
this.inReplyTo =
args.inReplyTo ||
(args.in_reply_to || args.replyToMsg
? ChatMessage.create(channel, args.in_reply_to || args.replyToMsg)
: null);
this.reactions = this.#initChatMessageReactionModel(args.reactions);
this.uploads = new TrackedArray(args.uploads || []);
this.user = this.#initUserModel(args.user);
this.bookmark = args.bookmark ? Bookmark.create(args.bookmark) : null;
this.mentionedUsers = this.#initMentionedUsers(args.mentioned_users);
if (args.thread) {
this.thread = args.thread;
}
}
get persisted() {
return !!this.id && !this.staged;
}
get replyable() {
return !this.staged && !this.error;
}
get editable() {
return !this.staged && !this.error;
}
get thread() {
return this._thread;
}
set thread(thread) {
this._thread = this.channel.threadsManager.add(this.channel, thread, {
replace: true,
});
}
get deletedAt() {
return this._deletedAt;
}
set deletedAt(value) {
this._deletedAt = value;
this.incrementVersion();
return this._deletedAt;
}
get cooked() {
return this._cooked;
}
set cooked(newCooked) {
// some markdown is cooked differently on the server-side, e.g.
// quotes, avatar images etc.
if (newCooked !== this._cooked) {
this._cooked = newCooked;
this.incrementVersion();
}
}
async cook() {
if (this.isDestroyed || this.isDestroying) {
return;
}
await this.#ensureCookFunctionInitialized();
this.cooked = ChatMessage.cookFunction(this.message);
}
get read() {
return this.channel.currentUserMembership?.lastReadMessageId >= this.id;
}
@cached
get firstMessageOfTheDayAt() {
if (!this.previousMessage) {
return this.#startOfDay(this.createdAt);
}
if (
!this.#areDatesOnSameDay(this.previousMessage.createdAt, this.createdAt)
) {
return this.#startOfDay(this.createdAt);
}
}
@cached
get formattedFirstMessageDate() {
if (this.firstMessageOfTheDayAt) {
return this.#calendarDate(this.firstMessageOfTheDayAt);
}
}
#calendarDate(date) {
return moment(date).calendar(moment(), {
sameDay: `[${I18n.t("chat.chat_message_separator.today")}]`,
lastDay: `[${I18n.t("chat.chat_message_separator.yesterday")}]`,
lastWeek: "LL",
sameElse: "LL",
});
}
@cached
get index() {
return this.manager?.messages?.indexOf(this);
}
@cached
get previousMessage() {
return this.manager?.messages?.objectAt?.(this.index - 1);
}
@cached
get nextMessage() {
return this.manager?.messages?.objectAt?.(this.index + 1);
}
highlight() {
this.highlighted = true;
discourseLater(() => {
if (this.isDestroying || this.isDestroyed) {
return;
}
this.highlighted = false;
}, 2000);
}
incrementVersion() {
this.version++;
}
async parseMentions() {
return await parseMentions(this.message, this.#markdownOptions);
}
toJSONDraft() {
if (
this.message?.length === 0 &&
this.uploads?.length === 0 &&
!this.inReplyTo
) {
return null;
}
const data = {};
if (this.uploads?.length > 0) {
data.uploads = this.uploads;
}
if (this.message?.length > 0) {
data.message = this.message;
}
if (this.inReplyTo) {
data.replyToMsg = {
id: this.inReplyTo.id,
excerpt: this.inReplyTo.excerpt,
user: {
id: this.inReplyTo.user.id,
name: this.inReplyTo.user.name,
avatar_template: this.inReplyTo.user.avatar_template,
username: this.inReplyTo.user.username,
},
};
}
if (this.editing) {
data.editing = true;
data.id = this.id;
data.excerpt = this.excerpt;
}
return JSON.stringify(data);
}
react(emoji, action, actor, currentUserId) {
const selfReaction = actor.id === currentUserId;
const existingReaction = this.reactions.find(
(reaction) => reaction.emoji === emoji
);
if (existingReaction) {
if (action === "add") {
if (selfReaction && existingReaction.reacted) {
return;
}
// we might receive a message bus event while loading a channel who would
// already have the reaction added to the message
if (existingReaction.users.find((user) => user.id === actor.id)) {
return;
}
existingReaction.count = existingReaction.count + 1;
if (selfReaction) {
existingReaction.reacted = true;
}
existingReaction.users.pushObject(actor);
} else {
const existingUserReaction = existingReaction.users.find(
(user) => user.id === actor.id
);
if (!existingUserReaction) {
return;
}
if (selfReaction) {
existingReaction.reacted = false;
}
if (existingReaction.count === 1) {
this.reactions.removeObject(existingReaction);
} else {
existingReaction.count = existingReaction.count - 1;
existingReaction.users.removeObject(existingUserReaction);
}
}
} else {
if (action === "add") {
this.reactions.pushObject(
ChatMessageReaction.create({
count: 1,
emoji,
reacted: selfReaction,
users: [actor],
})
);
}
}
}
async #ensureCookFunctionInitialized() {
if (ChatMessage.cookFunction) {
return;
}
const cookFunction = await generateCookFunction(this.#markdownOptions);
ChatMessage.cookFunction = (raw) => {
return transformAutolinks(cookFunction(raw));
};
}
get #markdownOptions() {
const site = getOwnerWithFallback(this).lookup("service:site");
return {
featuresOverride:
site.markdown_additional_options?.chat?.limited_pretty_text_features,
markdownItRules:
site.markdown_additional_options?.chat
?.limited_pretty_text_markdown_rules,
hashtagTypesInPriorityOrder:
site.hashtag_configurations?.["chat-composer"],
hashtagIcons: site.hashtag_icons,
};
}
#initChatMessageReactionModel(reactions = []) {
return reactions.map((reaction) => ChatMessageReaction.create(reaction));
}
#initMentionedUsers(mentionedUsers) {
const map = new Map();
if (mentionedUsers) {
mentionedUsers.forEach((userData) => {
const user = User.create(userData);
map.set(user.id, user);
});
}
return map;
}
#initUserModel(user) {
if (!user || user instanceof User) {
return user;
}
return User.create(user);
}
#areDatesOnSameDay(a, b) {
return (
a.getFullYear() === b.getFullYear() &&
a.getMonth() === b.getMonth() &&
a.getDate() === b.getDate()
);
}
#startOfDay(date) {
return moment(date).startOf("day").format();
}
}