Compare commits

...
This repository has been archived on 2023-03-18. You can view files and clone it, but cannot push or open issues or pull requests.

8 Commits

Author SHA1 Message Date
Martin Brennan
bdaf01bbb0
DEV: Cleanup 2023-02-21 17:31:55 +10:00
Martin Brennan
c0daa8dbf7
DEV: Move reactions into chat-message-actions and live-panel 2023-02-21 15:27:22 +10:00
Martin Brennan
bf33c287be
Merge branch 'main' into dev/chat-message-actions-experiments 2023-02-21 10:36:52 +10:00
Martin Brennan
9b375c1bef
DEV: Fixes after merge 2023-02-20 11:28:38 +10:00
Martin Brennan
6f648e3c06
Merge branch 'main' into dev/chat-message-actions-experiments 2023-02-20 09:27:52 +10:00
Martin Brennan
7cb521e6e8 DEV: Minor fixes 2023-02-16 15:39:12 +10:00
Martin Brennan
dcf78439ac WIP: Moving things around, rely only on chat-message-actions 2023-02-16 15:26:36 +10:00
Martin Brennan
e6fb4345ff WIP: Refactoring chat message actions and trying new patterns 2023-02-16 11:27:39 +10:00
15 changed files with 410 additions and 280 deletions

View File

@ -73,15 +73,13 @@
@setReplyTo={{action "setReplyTo"}}
@replyMessageClicked={{action "replyMessageClicked"}}
@editButtonClicked={{action "editButtonClicked"}}
@selectingMessages={{this.selectingMessages}}
@onStartSelectingMessages={{this.onStartSelectingMessages}}
@onSelectMessage={{this.onSelectMessage}}
@bulkSelectMessages={{this.bulkSelectMessages}}
@selectingMessages={{this.livePanel.selectingMessages}}
@fullPage={{this.fullPage}}
@afterReactionAdded={{action "reStickScrollIfNeeded"}}
@isHovered={{eq message.id this.hoveredMessageId}}
@onHoverMessage={{this.onHoverMessage}}
@isHovered={{eq message.id this.livePanel.hoveredMessageId}}
@onHoverMessage={{this.livePanel.hoverMessage}}
@resendStagedMessage={{this.resendStagedMessage}}
@messageActionsHandler={{this.messageActionsHandler}}
/>
{{/each}}
@ -118,12 +116,12 @@
</div>
{{/if}}
{{#if this.selectingMessages}}
{{#if this.livePanel.selectingMessages}}
<ChatSelectionManager
@selectedMessageIds={{this.selectedMessageIds}}
@selectedMessageIds={{this.livePanel.selectedMessageIds}}
@chatChannel={{this.chatChannel}}
@canModerate={{this.details.can_moderate}}
@cancelSelecting={{action "cancelSelecting"}}
@cancelSelecting={{action this.livePanel.cancelSelecting}}
/>
{{else}}
{{#if (or this.chatChannel.isDraft this.chatChannel.isFollowing)}}

View File

@ -1,6 +1,9 @@
import isElementInViewport from "discourse/lib/is-element-in-viewport";
import { getOwner } from "discourse-common/lib/get-owner";
import { cloneJSON } from "discourse-common/lib/object";
import ChatMessage from "discourse/plugins/chat/discourse/models/chat-message";
import ChatMessageActions from "discourse/plugins/chat/discourse/lib/chat-message-actions";
import ChatLivePanel from "discourse/plugins/chat/discourse/lib/chat-live-panel";
import Component from "@ember/component";
import discourseComputed, {
afterRender,
@ -48,7 +51,6 @@ export default Component.extend({
allPastMessagesLoaded: false,
sendingLoading: false,
selectingMessages: false,
stickyScroll: true,
stickyScrollTimer: null,
showChatQuoteSuccess: false,
@ -62,10 +64,13 @@ export default Component.extend({
messageLookup: null, // Object<Number, Message>
_unloadedReplyIds: null, // Array
_nextStagedMessageId: 0, // Iterate on every new message
_lastSelectedMessage: null,
lastSelectedMessage: null,
targetMessageId: null,
hasNewMessages: null,
livePanel: null,
messageActionsHandler: null,
chat: service(),
chatChannelsManager: service(),
router: service(),
@ -85,6 +90,12 @@ export default Component.extend({
this.set("_mentionWarningsSeen", {});
this.set("unreachableGroupMentions", []);
this.set("overMembersLimitGroupMentions", []);
this.livePanel = new ChatLivePanel(getOwner(this));
this.messageActionsHandler = new ChatMessageActions(
this.livePanel,
this.currentUser
);
},
didInsertElement() {
@ -175,6 +186,18 @@ export default Component.extend({
}
},
// TODO (martin) Not ideal....neither of these. We need both of these to only
// be tracked in one place. Messages should be be trackedarray here
// too or ideally just move messages onto the channel
@observes("messages")
onMessagesChange() {
this.livePanel.messages = this.messages;
},
@observes("details")
onDetailsChange() {
this.livePanel.details = this.details;
},
@discourseComputed("chatChannel.isDirectMessageChannel")
displayMembers(isDirectMessageChannel) {
return !isDirectMessageChannel;
@ -1139,7 +1162,7 @@ export default Component.extend({
this.messageLookup = {};
this.set("allPastMessagesLoaded", false);
this.set("registeredChatChannelId", null);
this.set("selectingMessages", false);
this.livePanel.cancelSelecting();
},
_resetAfterSend() {
@ -1224,52 +1247,11 @@ export default Component.extend({
return document.querySelector("#chat-progress-bar-container");
},
@discourseComputed("messages.@each.selected")
selectedMessageIds(messages) {
return messages.filter((m) => m.selected).map((m) => m.id);
},
@action
onStartSelectingMessages(message) {
this._lastSelectedMessage = message;
this.set("selectingMessages", true);
},
@action
cancelSelecting() {
this.set("selectingMessages", false);
this.messages.setEach("selected", false);
},
@action
onSelectMessage(message) {
this._lastSelectedMessage = message;
},
@action
navigateToIndex() {
this.router.transitionTo("chat.index");
},
@action
bulkSelectMessages(message, checked) {
const lastSelectedIndex = this._findIndexOfMessage(
this._lastSelectedMessage
);
const newlySelectedIndex = this._findIndexOfMessage(message);
const sortedIndices = [lastSelectedIndex, newlySelectedIndex].sort(
(a, b) => a - b
);
for (let i = sortedIndices[0]; i <= sortedIndices[1]; i++) {
this.messages[i].set("selected", checked);
}
},
_findIndexOfMessage(message) {
return this.messages.findIndex((m) => m.id === message.id);
},
@action
onCloseFullScreen() {
this.chatStateManager.prefersDrawer();
@ -1326,65 +1308,6 @@ export default Component.extend({
}
},
@action
onHoverMessage(message, options = {}, event) {
if (this.site.mobileView && options.desktopOnly) {
return;
}
if (message?.staged) {
return;
}
if (
this.hoveredMessageId &&
message?.id &&
this.hoveredMessageId === message?.id
) {
return;
}
if (event) {
if (
event.type === "mouseleave" &&
(event.toElement || event.relatedTarget)?.closest(
".chat-message-actions-desktop-anchor"
)
) {
return;
}
if (
event.type === "mouseenter" &&
(event.fromElement || event.relatedTarget)?.closest(
".chat-message-actions-desktop-anchor"
)
) {
this.set("hoveredMessageId", message?.id);
return;
}
}
this._onHoverMessageDebouncedHandler = discourseDebounce(
this,
this.debouncedOnHoverMessage,
message,
250
);
},
@bind
debouncedOnHoverMessage(message) {
if (this._selfDeleted) {
return;
}
this.set(
"hoveredMessageId",
message?.id && message.id !== this.hoveredMessageId ? message.id : null
);
},
_reportReplyingPresence(composerValue) {
if (this._selfDeleted) {
return;

View File

@ -9,7 +9,8 @@
{{#each @emojiReactions as |reaction|}}
<ChatMessageReaction
@reaction={{reaction}}
@react={{@messageActions.react}}
@messageActionsHandler={{@messageActionsHandler}}
@message={{@message}}
@class="show"
/>
{{/each}}
@ -56,7 +57,7 @@
@class="more-buttons"
@options={{hash icon="ellipsis-v" placement="left"}}
@content={{@secondaryButtons}}
@onChange={{action "handleSecondaryButtons"}}
@onChange={{action this.handleSecondaryButtons}}
/>
{{/if}}
</div>

View File

@ -46,6 +46,17 @@ export default class ChatMessageActionsDesktop extends Component {
@action
handleSecondaryButtons(id) {
if (id === "copyLinkToMessage") {
return this.args.messageActionsHandler.copyLink(this.args.message);
}
if (id === "selectMessage") {
return this.args.messageActionsHandler.selectMessage(
this.args.message,
true
);
}
this.args.messageActions?.[id]?.();
}
}

View File

@ -37,10 +37,7 @@
@translatedLabel={{button.name}}
@icon={{button.icon}}
@actionParam={{button.id}}
@action={{action
this.actAndCloseMenu
(get @messageActions button.id)
}}
@action={{action this.actAndCloseMenu button.id}}
/>
</li>
{{/each}}
@ -52,7 +49,8 @@
{{#each @emojiReactions as |reaction|}}
<ChatMessageReaction
@reaction={{reaction}}
@react={{@messageActions.react}}
@messageActionsHandler={{@messageActionsHandler}}
@message={{@message}}
@class="show"
/>
{{/each}}
@ -61,7 +59,7 @@
@class="btn-flat react-btn"
@action={{action
this.actAndCloseMenu
@messageActions.startReactionForMessageActions
"startReactionForMessageActions"
}}
@icon="discourse-emojis"
@title="chat.react"
@ -80,7 +78,7 @@
{{#if @messageCapabilities.canReply}}
<DButton
@class="chat-message-action reply-btn btn-flat"
@action={{action "actAndCloseMenu" @messageActions.reply}}
@action={{action this.actAndCloseMenu "reply"}}
@icon="reply"
@title="chat.reply"
/>

View File

@ -37,8 +37,18 @@ export default class ChatMessageActionsMobile extends Component {
}
@action
actAndCloseMenu(fn) {
fn?.();
actAndCloseMenu(fnId) {
if (fnId === "copyLinkToMessage") {
this.args.messageActionsHandler.copyLink(this.args.message);
return this.#onCloseMenu();
}
if (fnId === "selectMessage") {
this.args.messageActionsHandler.selectMessage(this.args.message, true);
return this.#onCloseMenu();
}
this.args.messageActions[fnId]?.();
this.#onCloseMenu();
}

View File

@ -10,7 +10,8 @@ export default class ChatMessageReaction extends Component {
reaction = null;
showUsersList = false;
tagName = "";
react = null;
message = null;
messageActionsHandler = null;
class = null;
didReceiveAttrs() {
@ -47,7 +48,11 @@ export default class ChatMessageReaction extends Component {
@action
handleClick() {
this?.react(this.reaction.emoji, this.reaction.reacted ? "remove" : "add");
this?.messageActionsHandler.react(
this.message,
this.reaction.emoji,
this.reaction.reacted ? "remove" : "add"
);
return false;
}

View File

@ -13,6 +13,7 @@
@emojiReactions={{this.emojiReactions}}
@secondaryButtons={{this.secondaryButtons}}
@messageActions={{this.messageActions}}
@messageActionsHandler={{@messageActionsHandler}}
@messageCapabilities={{this.messageCapabilities}}
@onHoverMessage={{@onHoverMessage}}
/>
@ -30,6 +31,7 @@
@emojiReactions={{this.emojiReactions}}
@secondaryButtons={{this.secondaryButtons}}
@messageActions={{this.messageActions}}
@messageActionsHandler={{@messageActionsHandler}}
@messageCapabilities={{this.messageCapabilities}}
/>
{{/in-element}}
@ -131,7 +133,7 @@
@uploads={{@message.uploads}}
@edited={{@message.edited}}
>
{{#if this.hasReactions}}
{{#if @message.hasReactions}}
<div class="chat-message-reaction-list">
{{#if this.reactionLabel}}
<div class="reaction-users-list">
@ -147,7 +149,8 @@
count=reactionAttrs.count
reacted=reactionAttrs.reacted
}}
@react={{this.react}}
@messageActionsHandler={{@messageActionsHandler}}
@message={{@message}}
@showUsersList={{true}}
/>
{{/each-in}}

View File

@ -1,15 +1,14 @@
import Bookmark from "discourse/models/bookmark";
import { openBookmarkModal } from "discourse/controllers/bookmark";
import { REACTIONS } from "discourse/plugins/chat/discourse/models/chat-message";
import { isTesting } from "discourse-common/config/environment";
import Component from "@glimmer/component";
import I18n from "I18n";
import getURL from "discourse-common/lib/get-url";
import optionalService from "discourse/lib/optional-service";
import { bind } from "discourse-common/utils/decorators";
import EmberObject, { action } from "@ember/object";
import { ajax } from "discourse/lib/ajax";
import { cancel, schedule } from "@ember/runloop";
import { clipboardCopy } from "discourse/lib/utilities";
import { inject as service } from "@ember/service";
import { popupAjaxError } from "discourse/lib/ajax-error";
import discourseLater from "discourse-common/lib/later";
@ -31,8 +30,6 @@ export function resetChatMessageDecorators() {
export const MENTION_KEYWORDS = ["here", "all"];
export const REACTIONS = { add: "add", remove: "remove" };
export default class ChatMessage extends Component {
@service site;
@service dialog;
@ -245,10 +242,7 @@ export default class ChatMessage extends Component {
get messageActions() {
return {
reply: this.reply,
react: this.react,
copyLinkToMessage: this.copyLinkToMessage,
edit: this.edit,
selectMessage: this.selectMessage,
flag: this.flag,
deleteMessage: this.deleteMessage,
restore: this.restore,
@ -400,12 +394,6 @@ export default class ChatMessage extends Component {
);
}
get hasReactions() {
return Object.values(this.args.message.get("reactions")).some(
(r) => r.count > 0
);
}
get mentionWarning() {
return this.args.message.get("mentionWarning");
}
@ -505,7 +493,11 @@ export default class ChatMessage extends Component {
return;
}
this.react(emoji, REACTIONS.remove);
this.args.messageActionsHandler.react(
this.args.message,
emoji,
REACTIONS.remove
);
}
@action
@ -514,17 +506,28 @@ export default class ChatMessage extends Component {
return;
}
this.react(emoji, REACTIONS.add);
this.args.messageActionsHandler.react(
this.args.message,
emoji,
REACTIONS.add
);
}
@bind
_handleReactionMessage(busData) {
const loadingReactionIndex = this._loadingReactions.indexOf(busData.emoji);
const loadingReactionIndex = this.args.message.loadingReactions.indexOf(
busData.emoji
);
if (loadingReactionIndex > -1) {
return this._loadingReactions.splice(loadingReactionIndex, 1);
return this.args.message.loadingReactions.splice(loadingReactionIndex, 1);
}
this._updateReactionsList(busData.emoji, busData.action, busData.user);
this.args.message.updateReactionsList(
busData.emoji,
busData.action,
busData.user,
this.currentUser.id === busData.user.id
);
this.args.afterReactionAdded();
}
@ -532,101 +535,6 @@ export default class ChatMessage extends Component {
return getOwner(this).lookup("capabilities:main");
}
@action
react(emoji, reactAction) {
if (
!this.args.canInteractWithChat ||
this._loadingReactions.includes(emoji)
) {
return;
}
if (this.capabilities.canVibrate && !isTesting()) {
navigator.vibrate(5);
}
if (this.site.mobileView) {
this.args.onHoverMessage(null);
}
this._loadingReactions.push(emoji);
this._updateReactionsList(emoji, reactAction, this.currentUser);
if (reactAction === REACTIONS.add) {
this.chatEmojiReactionStore.track(`:${emoji}:`);
}
return this._publishReaction(emoji, reactAction).then(() => {
// creating reaction will create a membership if not present
// so we will fully refresh if we were not members of the channel
// already
if (!this.args.chatChannel.isFollowing || this.args.chatChannel.isDraft) {
return this.args.chatChannelsManager
.getChannel(this.args.chatChannel.id)
.then((reactedChannel) => {
this.router.transitionTo("chat.channel", "-", reactedChannel.id);
});
}
});
}
_updateReactionsList(emoji, reactAction, user) {
const selfReacted = this.currentUser.id === user.id;
if (this.args.message.reactions[emoji]) {
if (
selfReacted &&
reactAction === REACTIONS.add &&
this.args.message.reactions[emoji].reacted
) {
// User is already has reaction added; do nothing
return false;
}
let newCount =
reactAction === REACTIONS.add
? this.args.message.reactions[emoji].count + 1
: this.args.message.reactions[emoji].count - 1;
this.args.message.reactions.set(`${emoji}.count`, newCount);
if (selfReacted) {
this.args.message.reactions.set(
`${emoji}.reacted`,
reactAction === REACTIONS.add
);
} else {
this.args.message.reactions[emoji].users.pushObject(user);
}
this.args.message.notifyPropertyChange("reactions");
} else {
if (reactAction === REACTIONS.add) {
this.args.message.reactions.set(emoji, {
count: 1,
reacted: selfReacted,
users: selfReacted ? [] : [user],
});
}
this.args.message.notifyPropertyChange("reactions");
}
}
_publishReaction(emoji, reactAction) {
return ajax(
`/chat/${this.args.message.chat_channel_id}/react/${this.args.message.id}`,
{
type: "PUT",
data: {
react_action: reactAction,
emoji,
},
}
).catch((errResult) => {
popupAjaxError(errResult);
this._updateReactionsList(emoji, REACTIONS.remove, this.currentUser);
});
}
// TODO(roman): For backwards-compatibility.
// Remove after the 3.0 release.
_legacyFlag() {
@ -754,42 +662,18 @@ export default class ChatMessage extends Component {
}
@action
selectMessage() {
this.args.message.set("selected", true);
this.args.onStartSelectingMessages(this.args.message);
}
@action
toggleChecked(e) {
if (e.shiftKey) {
this.args.bulkSelectMessages(this.args.message, e.target.checked);
toggleChecked(event) {
if (event.shiftKey) {
this.args.messageActionsHandler.bulkSelectMessages(
this.args.message,
event.target.checked
);
}
this.args.onSelectMessage(this.args.message);
}
@action
copyLinkToMessage() {
if (!this.messageContainer) {
return;
}
this.messageContainer
.querySelector(".link-to-message-btn")
?.classList?.add("copied");
const { protocol, host } = window.location;
let url = getURL(
`/chat/c/-/${this.args.message.chat_channel_id}/${this.args.message.id}`
this.args.messageActionsHandler.selectMessage(
this.args.message,
event.target.checked
);
url = url.indexOf("/") === 0 ? protocol + "//" + host + url : url;
clipboardCopy(url);
discourseLater(() => {
this.messageContainer
?.querySelector(".link-to-message-btn")
?.classList?.remove("copied");
}, 250);
}
get emojiReactions() {

View File

@ -1,13 +1,29 @@
import Component from "@glimmer/component";
import { getOwner } from "discourse-common/lib/get-owner";
import I18n from "I18n";
import { inject as service } from "@ember/service";
import ChatMessageActions from "discourse/plugins/chat/discourse/lib/chat-message-actions";
import ChatThreadLivePanel from "discourse/plugins/chat/discourse/lib/chat-thread-live-panel";
export default class ChatThreadPanel extends Component {
export default class ChatThread extends Component {
@service siteSettings;
@service currentUser;
@service chat;
@service router;
livePanel = null;
messageActionsHandler = null;
constructor() {
super(...arguments);
this.livePanel = new ChatThreadLivePanel(getOwner(this));
this.messageActionsHandler = new ChatMessageActions(
this.livePanel,
this.currentUser
);
}
get thread() {
return this.chat.activeChannel.activeThread;
}

View File

@ -0,0 +1,120 @@
import { tracked } from "@glimmer/tracking";
import discourseDebounce from "discourse-common/lib/debounce";
import { bind } from "discourse-common/utils/decorators";
import { inject as service } from "@ember/service";
import { action } from "@ember/object";
import { TrackedArray } from "@ember-compat/tracked-built-ins";
import { setOwner } from "@ember/application";
import { getOwner } from "discourse-common/lib/get-owner";
export default class ChatLivePanel {
@service chat;
@service chatChannelsManager;
@service chatEmojiReactionStore;
@service router;
@service site;
@tracked messages = new TrackedArray();
@tracked details = null;
@tracked selectingMessages;
@tracked lastSelectedMessage;
@tracked hoveredMessageId;
constructor(owner) {
setOwner(this, owner);
}
get capabilities() {
return getOwner(this).lookup("capabilities:main");
}
get canInteractWithChat() {
return !this.details.user_silenced;
}
get selectedMessageIds() {
return this.messages.filterBy("selected").mapBy("id");
}
onSelectMessage(message) {
this.lastSelectedMessage = message;
this.selectingMessages = true;
}
onReactMessage() {
// creating reaction will create a membership if not present
// so we will fully refresh if we were not members of the channel
// already
if (
!this.chat.activeChannel.isFollowing ||
this.chat.activeChannel.isDraft
) {
return this.chatChannelsManager
.getChannel(this.chat.activeChannel)
.then((reactedChannel) => {
this.router.transitionTo("chat.channel", "-", reactedChannel.id);
});
}
}
@action
hoverMessage(message, options = {}, event) {
if (this.site.mobileView && options.desktopOnly) {
return;
}
if (message?.staged) {
return;
}
if (
this.hoveredMessageId &&
message?.id &&
this.hoveredMessageId === message?.id
) {
return;
}
if (event) {
if (
event.type === "mouseleave" &&
(event.toElement || event.relatedTarget)?.closest(
".chat-message-actions-desktop-anchor"
)
) {
return;
}
if (
event.type === "mouseenter" &&
(event.fromElement || event.relatedTarget)?.closest(
".chat-message-actions-desktop-anchor"
)
) {
this.hoveredMessageId = message?.id;
return;
}
}
this._onHoverMessageDebouncedHandler = discourseDebounce(
this,
this._debouncedOnHoverMessage,
message,
250
);
}
@action
cancelSelecting() {
this.selectingMessages = false;
this.lastSelectedMessage = null;
this.messages.setEach("selected", false);
}
@bind
_debouncedOnHoverMessage(message) {
this.hoveredMessageId =
message?.id && message.id !== this.hoveredMessageId ? message.id : null;
}
}

View File

@ -0,0 +1,84 @@
import getURL from "discourse-common/lib/get-url";
import { bind } from "discourse-common/utils/decorators";
import { isTesting } from "discourse-common/config/environment";
import { clipboardCopy } from "discourse/lib/utilities";
import { REACTIONS } from "discourse/plugins/chat/discourse/models/chat-message";
export default class ChatMessageActions {
livePanel = null;
currentUser = null;
constructor(livePanel, currentUser) {
this.livePanel = livePanel;
this.currentUser = currentUser;
}
copyLink(message) {
const { protocol, host } = window.location;
let url = getURL(`/chat/c/-/${message.chat_channel_id}/${message.id}`);
url = url.indexOf("/") === 0 ? protocol + "//" + host + url : url;
clipboardCopy(url);
}
selectMessage(message, checked) {
message.set("selected", checked);
this.livePanel.onSelectMessage(message);
}
bulkSelectMessages(message, checked) {
const lastSelectedIndex = this.#findIndexOfMessage(
this.livePanel.lastSelectedMessage
);
const newlySelectedIndex = this.#findIndexOfMessage(message);
const sortedIndices = [lastSelectedIndex, newlySelectedIndex].sort(
(a, b) => a - b
);
for (let i = sortedIndices[0]; i <= sortedIndices[1]; i++) {
this.livePanel.messages[i].set("selected", checked);
}
}
@bind
react(message, emoji, reactAction) {
if (
!this.livePanel.canInteractWithChat ||
message.loadingReactions.includes(emoji)
) {
return;
}
if (this.livePanel.capabilities.canVibrate && !isTesting()) {
navigator.vibrate(5);
}
if (this.livePanel.site.mobileView) {
this.livePanel.hoverMessage(null);
}
message.loadingReactions.push(emoji);
message.updateReactionsList(emoji, reactAction, this.currentUser, true);
if (reactAction === REACTIONS.add) {
this.livePanel.chatEmojiReactionStore.track(`:${emoji}:`);
}
return message
.publishReaction(emoji, reactAction)
.then(() => {
this.livePanel.onReactMessage();
})
.catch(() => {
message.updateReactionsList(
emoji,
REACTIONS.remove,
this.currentUser,
true
);
});
}
#findIndexOfMessage(message) {
return this.livePanel.messages.findIndex((m) => m.id === message.id);
}
}

View File

@ -0,0 +1,3 @@
import ChatLivePanel from "./chat-live-panel";
export default class ChatThreadLivePanel extends ChatLivePanel {}

View File

@ -1,8 +1,78 @@
import RestModel from "discourse/models/rest";
import { TrackedObject } from "@ember-compat/tracked-built-ins";
import { ajax } from "discourse/lib/ajax";
import { popupAjaxError } from "discourse/lib/ajax-error";
import { tracked } from "@glimmer/tracking";
import User from "discourse/models/user";
import EmberObject from "@ember/object";
export default class ChatMessage extends RestModel {}
export const REACTIONS = { add: "add", remove: "remove" };
export default class ChatMessage extends RestModel {
@tracked selected;
@tracked reactions;
loadingReactions = [];
// deep TrackedObject c.f. https://github.com/emberjs/ember.js/issues/18988#issuecomment-837670880
init() {
this._super(...arguments);
this.reactions = new TrackedObject(this._reactions);
Object.keys(this.reactions).forEach((emoji) => {
this.reactions[emoji] = new TrackedObject(this.reactions[emoji]);
});
}
get hasReactions() {
return Object.values(this.reactions).some((r) => r.count > 0);
}
// TODO (martin) Not ideal, this should have a chat API controller endpoint
// and be moved to that service.
publishReaction(emoji, reactAction) {
return ajax(`/chat/${this.chat_channel_id}/react/${this.id}`, {
type: "PUT",
data: {
react_action: reactAction,
emoji,
},
}).catch((errResult) => {
popupAjaxError(errResult);
});
}
updateReactionsList(emoji, reactAction, user, selfReacted) {
if (this.reactions[emoji]) {
if (
selfReacted &&
reactAction === REACTIONS.add &&
this.reactions[emoji].reacted
) {
// User is already has reaction added; do nothing
return false;
}
let newCount =
reactAction === REACTIONS.add
? this.reactions[emoji].count + 1
: this.reactions[emoji].count - 1;
this.reactions[emoji].count = newCount;
if (selfReacted) {
this.reactions[emoji].reacted = reactAction === REACTIONS.add;
} else {
this.reactions[emoji].users.pushObject(user);
}
} else {
if (reactAction === REACTIONS.add) {
this.reactions[emoji] = new TrackedObject({
count: 1,
reacted: selfReacted,
users: selfReacted ? [] : [user],
});
}
}
}
}
ChatMessage.reopenClass({
create(args = {}) {
@ -13,7 +83,8 @@ ChatMessage.reopenClass({
},
_initReactions(args) {
args.reactions = EmberObject.create(args.reactions || {});
args._reactions = args.reactions || {};
delete args.reactions;
},
_initUserModel(args) {

View File

@ -71,12 +71,15 @@ module("Discourse Chat | Component | chat-message-reaction", function (hooks) {
test("click action", async function (assert) {
this.set("count", 0);
this.set("react", () => {
this.set("count", 1);
this.set("message", { id: 1 });
this.set("messageActionsHandler", {
react: () => {
this.set("count", 1);
},
});
await render(hbs`
<ChatMessageReaction class="show" @reaction={{hash emoji="heart" count=this.count}} @react={{this.react}} />
<ChatMessageReaction class="show" @reaction={{hash emoji="heart" count=this.count}} @message={{this.message}} @messageActionsHandler={{this.messageActionsHandler}} />
`);
assert.false(exists(".chat-message-reaction .count"));