Compare commits
8 Commits
main
...
dev/chat-m
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bdaf01bbb0 | ||
|
|
c0daa8dbf7 | ||
|
|
bf33c287be | ||
|
|
9b375c1bef | ||
|
|
6f648e3c06 | ||
|
|
7cb521e6e8 | ||
|
|
dcf78439ac | ||
|
|
e6fb4345ff |
@ -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)}}
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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]?.();
|
||||
}
|
||||
}
|
||||
|
||||
@ -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"
|
||||
/>
|
||||
|
||||
@ -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();
|
||||
}
|
||||
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
|
||||
@ -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}}
|
||||
|
||||
@ -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() {
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
120
plugins/chat/assets/javascripts/discourse/lib/chat-live-panel.js
Normal file
120
plugins/chat/assets/javascripts/discourse/lib/chat-live-panel.js
Normal 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;
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,3 @@
|
||||
import ChatLivePanel from "./chat-live-panel";
|
||||
|
||||
export default class ChatThreadLivePanel extends ChatLivePanel {}
|
||||
@ -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) {
|
||||
|
||||
@ -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"));
|
||||
|
||||
Reference in New Issue
Block a user