DEV: rework the chat-live-pane (#20519)
This PR is introducing glimmer usage in the chat-live-pane, for components but also for models. RestModel usage has been dropped in favor of native classes. Other changes/additions in this PR: sticky dates, scrolling will now keep the date separator of the current section at the top of the screen better unread management, marking a channel as unread will correctly mark the correct message and not mark the whole channel as read. Tracking state will also now correctly return unread count and unread mentions. adds an animation on bottom arrow better scrolling behavior, we should now always correctly keep the scroll position while loading more reactions are now more reactive, and will update their tooltip without needed to close/reopen it skeleton has been improved with placeholder images and reactions when making a reaction on the desktop message actions, the menu won't move anymore simplify logic and stop maintaining a list of unloaded messages
This commit is contained in:
parent
e08a0b509d
commit
6b0aeced7e
|
@ -9,7 +9,7 @@ class Chat::Api::ChatChannelsMembershipsController < Chat::Api::ChatChannelsCont
|
||||||
|
|
||||||
memberships =
|
memberships =
|
||||||
ChatChannelMembershipsQuery.call(
|
ChatChannelMembershipsQuery.call(
|
||||||
channel_from_params,
|
channel: channel_from_params,
|
||||||
offset: offset,
|
offset: offset,
|
||||||
limit: limit,
|
limit: limit,
|
||||||
username: params[:username],
|
username: params[:username],
|
||||||
|
|
|
@ -223,7 +223,7 @@ class ChatMessage < ActiveRecord::Base
|
||||||
end
|
end
|
||||||
|
|
||||||
def url
|
def url
|
||||||
"/chat/message/#{self.id}"
|
"/chat/c/-/#{self.chat_channel_id}/#{self.id}"
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
class ChatChannelMembershipsQuery
|
class ChatChannelMembershipsQuery
|
||||||
def self.call(channel, limit: 50, offset: 0, username: nil, count_only: false)
|
def self.call(channel:, limit: 50, offset: 0, username: nil, count_only: false)
|
||||||
query =
|
query =
|
||||||
UserChatChannelMembership
|
UserChatChannelMembership
|
||||||
.joins(:user)
|
.joins(:user)
|
||||||
|
@ -42,6 +42,6 @@ class ChatChannelMembershipsQuery
|
||||||
end
|
end
|
||||||
|
|
||||||
def self.count(channel)
|
def self.count(channel)
|
||||||
call(channel, count_only: true)
|
call(channel: channel, count_only: true)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -0,0 +1,40 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class ChatChannelUnreadsQuery
|
||||||
|
def self.call(channel_id:, user_id:)
|
||||||
|
sql = <<~SQL
|
||||||
|
SELECT (
|
||||||
|
SELECT COUNT(*) AS unread_count
|
||||||
|
FROM chat_messages
|
||||||
|
INNER JOIN chat_channels ON chat_channels.id = chat_messages.chat_channel_id
|
||||||
|
INNER JOIN user_chat_channel_memberships ON user_chat_channel_memberships.chat_channel_id = chat_channels.id
|
||||||
|
WHERE chat_channels.id = :channel_id
|
||||||
|
AND chat_messages.user_id != :user_id
|
||||||
|
AND user_chat_channel_memberships.user_id = :user_id
|
||||||
|
AND chat_messages.id > COALESCE(user_chat_channel_memberships.last_read_message_id, 0)
|
||||||
|
AND chat_messages.deleted_at IS NULL
|
||||||
|
) AS unread_count,
|
||||||
|
(
|
||||||
|
SELECT COUNT(*) AS mention_count
|
||||||
|
FROM notifications
|
||||||
|
INNER JOIN user_chat_channel_memberships ON user_chat_channel_memberships.chat_channel_id = :channel_id
|
||||||
|
AND user_chat_channel_memberships.user_id = :user_id
|
||||||
|
WHERE NOT read
|
||||||
|
AND notifications.user_id = :user_id
|
||||||
|
AND notifications.notification_type = :notification_type
|
||||||
|
AND (data::json->>'chat_message_id')::bigint > COALESCE(user_chat_channel_memberships.last_read_message_id, 0)
|
||||||
|
AND (data::json->>'chat_channel_id')::bigint = :channel_id
|
||||||
|
) AS mention_count;
|
||||||
|
SQL
|
||||||
|
|
||||||
|
DB
|
||||||
|
.query(
|
||||||
|
sql,
|
||||||
|
channel_id: channel_id,
|
||||||
|
user_id: user_id,
|
||||||
|
notification_type: Notification.types[:chat_mention],
|
||||||
|
)
|
||||||
|
.first
|
||||||
|
.to_h
|
||||||
|
end
|
||||||
|
end
|
|
@ -110,6 +110,7 @@ class ChatChannelSerializer < ApplicationSerializer
|
||||||
def meta
|
def meta
|
||||||
{
|
{
|
||||||
message_bus_last_ids: {
|
message_bus_last_ids: {
|
||||||
|
channel_message_bus_last_id: MessageBus.last_id("/chat/#{object.id}"),
|
||||||
new_messages:
|
new_messages:
|
||||||
@opts[:new_messages_message_bus_last_id] ||
|
@opts[:new_messages_message_bus_last_id] ||
|
||||||
MessageBus.last_id(ChatPublisher.new_messages_message_bus_channel(object.id)),
|
MessageBus.last_id(ChatPublisher.new_messages_message_bus_channel(object.id)),
|
||||||
|
|
|
@ -35,23 +35,23 @@ class ChatMessageSerializer < ApplicationSerializer
|
||||||
end
|
end
|
||||||
|
|
||||||
def reactions
|
def reactions
|
||||||
reactions_hash = {}
|
|
||||||
object
|
object
|
||||||
.reactions
|
.reactions
|
||||||
.group_by(&:emoji)
|
.group_by(&:emoji)
|
||||||
.each do |emoji, reactions|
|
.map do |emoji, reactions|
|
||||||
users = reactions[0..5].map(&:user).filter { |user| user.id != scope&.user&.id }[0..4]
|
|
||||||
|
|
||||||
next unless Emoji.exists?(emoji)
|
next unless Emoji.exists?(emoji)
|
||||||
|
|
||||||
reactions_hash[emoji] = {
|
users = reactions.take(5).map(&:user)
|
||||||
|
|
||||||
|
{
|
||||||
|
emoji: emoji,
|
||||||
count: reactions.count,
|
count: reactions.count,
|
||||||
users:
|
users:
|
||||||
ActiveModel::ArraySerializer.new(users, each_serializer: BasicUserSerializer).as_json,
|
ActiveModel::ArraySerializer.new(users, each_serializer: BasicUserSerializer).as_json,
|
||||||
reacted: users_reactions.include?(emoji),
|
reacted: users_reactions.include?(emoji),
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
reactions_hash
|
.compact
|
||||||
end
|
end
|
||||||
|
|
||||||
def include_reactions?
|
def include_reactions?
|
||||||
|
|
|
@ -16,6 +16,7 @@ class ChatViewSerializer < ApplicationSerializer
|
||||||
|
|
||||||
def meta
|
def meta
|
||||||
meta_hash = {
|
meta_hash = {
|
||||||
|
channel_id: object.chat_channel.id,
|
||||||
can_flag: scope.can_flag_in_chat_channel?(object.chat_channel),
|
can_flag: scope.can_flag_in_chat_channel?(object.chat_channel),
|
||||||
channel_status: object.chat_channel.status,
|
channel_status: object.chat_channel.status,
|
||||||
user_silenced: !scope.can_create_chat_message?,
|
user_silenced: !scope.can_create_chat_message?,
|
||||||
|
|
|
@ -12,7 +12,7 @@ module ChatPublisher
|
||||||
{ scope: anonymous_guardian, root: :chat_message },
|
{ scope: anonymous_guardian, root: :chat_message },
|
||||||
).as_json
|
).as_json
|
||||||
content[:type] = :sent
|
content[:type] = :sent
|
||||||
content[:stagedId] = staged_id
|
content[:staged_id] = staged_id
|
||||||
permissions = permissions(chat_channel)
|
permissions = permissions(chat_channel)
|
||||||
|
|
||||||
MessageBus.publish("/chat/#{chat_channel.id}", content.as_json, permissions)
|
MessageBus.publish("/chat/#{chat_channel.id}", content.as_json, permissions)
|
||||||
|
@ -133,9 +133,13 @@ module ChatPublisher
|
||||||
end
|
end
|
||||||
|
|
||||||
def self.publish_user_tracking_state(user, chat_channel_id, chat_message_id)
|
def self.publish_user_tracking_state(user, chat_channel_id, chat_message_id)
|
||||||
|
data = { chat_channel_id: chat_channel_id, chat_message_id: chat_message_id }.merge(
|
||||||
|
ChatChannelUnreadsQuery.call(channel_id: chat_channel_id, user_id: user.id),
|
||||||
|
)
|
||||||
|
|
||||||
MessageBus.publish(
|
MessageBus.publish(
|
||||||
self.user_tracking_state_message_bus_channel(user.id),
|
self.user_tracking_state_message_bus_channel(user.id),
|
||||||
{ chat_channel_id: chat_channel_id, chat_message_id: chat_message_id.to_i }.as_json,
|
data.as_json,
|
||||||
user_ids: [user.id],
|
user_ids: [user.id],
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
|
@ -1,24 +1,30 @@
|
||||||
import { bind } from "discourse-common/utils/decorators";
|
import { bind } from "discourse-common/utils/decorators";
|
||||||
import Component from "@ember/component";
|
import Component from "@glimmer/component";
|
||||||
import { action, computed } from "@ember/object";
|
import { action } from "@ember/object";
|
||||||
import { schedule } from "@ember/runloop";
|
import { schedule } from "@ember/runloop";
|
||||||
import { inject as service } from "@ember/service";
|
import { inject as service } from "@ember/service";
|
||||||
import { and, empty } from "@ember/object/computed";
|
|
||||||
|
|
||||||
export default class ChannelsList extends Component {
|
export default class ChannelsList extends Component {
|
||||||
@service chat;
|
@service chat;
|
||||||
@service router;
|
@service router;
|
||||||
@service chatStateManager;
|
@service chatStateManager;
|
||||||
@service chatChannelsManager;
|
@service chatChannelsManager;
|
||||||
tagName = "";
|
@service site;
|
||||||
inSidebar = false;
|
@service session;
|
||||||
toggleSection = null;
|
@service currentUser;
|
||||||
@empty("chatChannelsManager.publicMessageChannels")
|
|
||||||
publicMessageChannelsEmpty;
|
get showMobileDirectMessageButton() {
|
||||||
@and("site.mobileView", "showDirectMessageChannels")
|
return this.site.mobileView && this.showDirectMessageChannels;
|
||||||
showMobileDirectMessageButton;
|
}
|
||||||
|
|
||||||
|
get inSidebar() {
|
||||||
|
return this.args.inSidebar ?? false;
|
||||||
|
}
|
||||||
|
|
||||||
|
get publicMessageChannelsEmpty() {
|
||||||
|
return this.chatChannelsManager.publicMessageChannels?.length === 0;
|
||||||
|
}
|
||||||
|
|
||||||
@computed("canCreateDirectMessageChannel")
|
|
||||||
get createDirectMessageChannelLabel() {
|
get createDirectMessageChannelLabel() {
|
||||||
if (!this.canCreateDirectMessageChannel) {
|
if (!this.canCreateDirectMessageChannel) {
|
||||||
return "chat.direct_messages.cannot_create";
|
return "chat.direct_messages.cannot_create";
|
||||||
|
@ -27,10 +33,6 @@ export default class ChannelsList extends Component {
|
||||||
return "chat.direct_messages.new";
|
return "chat.direct_messages.new";
|
||||||
}
|
}
|
||||||
|
|
||||||
@computed(
|
|
||||||
"canCreateDirectMessageChannel",
|
|
||||||
"chatChannelsManager.directMessageChannels"
|
|
||||||
)
|
|
||||||
get showDirectMessageChannels() {
|
get showDirectMessageChannels() {
|
||||||
return (
|
return (
|
||||||
this.canCreateDirectMessageChannel ||
|
this.canCreateDirectMessageChannel ||
|
||||||
|
@ -42,17 +44,12 @@ export default class ChannelsList extends Component {
|
||||||
return this.chat.userCanDirectMessage;
|
return this.chat.userCanDirectMessage;
|
||||||
}
|
}
|
||||||
|
|
||||||
@computed("inSidebar")
|
|
||||||
get publicChannelClasses() {
|
get publicChannelClasses() {
|
||||||
return `channels-list-container public-channels ${
|
return `channels-list-container public-channels ${
|
||||||
this.inSidebar ? "collapsible-sidebar-section" : ""
|
this.inSidebar ? "collapsible-sidebar-section" : ""
|
||||||
}`;
|
}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
@computed(
|
|
||||||
"publicMessageChannelsEmpty",
|
|
||||||
"currentUser.{staff,has_joinable_public_channels}"
|
|
||||||
)
|
|
||||||
get displayPublicChannels() {
|
get displayPublicChannels() {
|
||||||
if (this.publicMessageChannelsEmpty) {
|
if (this.publicMessageChannelsEmpty) {
|
||||||
return (
|
return (
|
||||||
|
@ -64,7 +61,6 @@ export default class ChannelsList extends Component {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@computed("inSidebar")
|
|
||||||
get directMessageChannelClasses() {
|
get directMessageChannelClasses() {
|
||||||
return `channels-list-container direct-message-channels ${
|
return `channels-list-container direct-message-channels ${
|
||||||
this.inSidebar ? "collapsible-sidebar-section" : ""
|
this.inSidebar ? "collapsible-sidebar-section" : ""
|
||||||
|
@ -73,7 +69,7 @@ export default class ChannelsList extends Component {
|
||||||
|
|
||||||
@action
|
@action
|
||||||
toggleChannelSection(section) {
|
toggleChannelSection(section) {
|
||||||
this.toggleSection(section);
|
this.args.toggleSection(section);
|
||||||
}
|
}
|
||||||
|
|
||||||
didRender() {
|
didRender() {
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
{{this.lastMessageFormatedDate}}
|
{{this.lastMessageFormatedDate}}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{{#if @unreadIndicator}}
|
{{#if this.unreadIndicator}}
|
||||||
<ChatChannelUnreadIndicator @channel={{@channel}} />
|
<ChatChannelUnreadIndicator @channel={{@channel}} />
|
||||||
{{/if}}
|
{{/if}}
|
||||||
</div>
|
</div>
|
|
@ -1,18 +1,18 @@
|
||||||
import Component from "@glimmer/component";
|
import Component from "@glimmer/component";
|
||||||
|
|
||||||
export default class ChatChannelMetadata extends Component {
|
export default class ChatChannelMetadata extends Component {
|
||||||
unreadIndicator = false;
|
get unreadIndicator() {
|
||||||
|
return this.args.unreadIndicator ?? false;
|
||||||
|
}
|
||||||
|
|
||||||
get lastMessageFormatedDate() {
|
get lastMessageFormatedDate() {
|
||||||
return moment(this.args.channel.get("last_message_sent_at")).calendar(
|
return moment(this.args.channel.lastMessageSentAt).calendar(null, {
|
||||||
null,
|
sameDay: "LT",
|
||||||
{
|
nextDay: "[Tomorrow]",
|
||||||
sameDay: "LT",
|
nextWeek: "dddd",
|
||||||
nextDay: "[Tomorrow]",
|
lastDay: "[Yesterday]",
|
||||||
nextWeek: "dddd",
|
lastWeek: "dddd",
|
||||||
lastDay: "[Yesterday]",
|
sameElse: "l",
|
||||||
lastWeek: "dddd",
|
});
|
||||||
sameElse: "l",
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,15 +4,15 @@
|
||||||
(unless this.hasDescription "-no-description")
|
(unless this.hasDescription "-no-description")
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<ChatChannelTitle @channel={{this.channel}} />
|
<ChatChannelTitle @channel={{@channel}} />
|
||||||
{{#if this.hasDescription}}
|
{{#if this.hasDescription}}
|
||||||
<p class="chat-channel-preview-card__description">
|
<p class="chat-channel-preview-card__description">
|
||||||
{{this.channel.description}}
|
{{@channel.description}}
|
||||||
</p>
|
</p>
|
||||||
{{/if}}
|
{{/if}}
|
||||||
{{#if this.showJoinButton}}
|
{{#if this.showJoinButton}}
|
||||||
<ToggleChannelMembershipButton
|
<ToggleChannelMembershipButton
|
||||||
@channel={{this.channel}}
|
@channel={{@channel}}
|
||||||
@options={{hash joinClass="btn-primary"}}
|
@options={{hash joinClass="btn-primary"}}
|
||||||
/>
|
/>
|
||||||
{{/if}}
|
{{/if}}
|
||||||
|
|
|
@ -1,19 +1,15 @@
|
||||||
import Component from "@ember/component";
|
import Component from "@glimmer/component";
|
||||||
import { isEmpty } from "@ember/utils";
|
import { isEmpty } from "@ember/utils";
|
||||||
import { computed } from "@ember/object";
|
|
||||||
import { readOnly } from "@ember/object/computed";
|
|
||||||
import { inject as service } from "@ember/service";
|
import { inject as service } from "@ember/service";
|
||||||
|
|
||||||
export default class ChatChannelPreviewCard extends Component {
|
export default class ChatChannelPreviewCard extends Component {
|
||||||
@service chat;
|
@service chat;
|
||||||
tagName = "";
|
|
||||||
|
|
||||||
channel = null;
|
get showJoinButton() {
|
||||||
|
return this.args.channel?.isOpen;
|
||||||
|
}
|
||||||
|
|
||||||
@readOnly("channel.isOpen") showJoinButton;
|
|
||||||
|
|
||||||
@computed("channel.description")
|
|
||||||
get hasDescription() {
|
get hasDescription() {
|
||||||
return !isEmpty(this.channel.description);
|
return !isEmpty(this.args.channel?.description);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -194,7 +194,7 @@ export default Component.extend({
|
||||||
|
|
||||||
getChannelsWithFilter(filter, opts = { excludeActiveChannel: true }) {
|
getChannelsWithFilter(filter, opts = { excludeActiveChannel: true }) {
|
||||||
let sortedChannels = this.chatChannelsManager.channels.sort((a, b) => {
|
let sortedChannels = this.chatChannelsManager.channels.sort((a, b) => {
|
||||||
return new Date(a.last_message_sent_at) > new Date(b.last_message_sent_at)
|
return new Date(a.lastMessageSentAt) > new Date(b.lastMessageSentAt)
|
||||||
? -1
|
? -1
|
||||||
: 1;
|
: 1;
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,17 +1,17 @@
|
||||||
{{#if this.buttons.length}}
|
{{#if @buttons.length}}
|
||||||
<DPopover
|
<DPopover
|
||||||
@class="chat-composer-dropdown"
|
@class="chat-composer-dropdown"
|
||||||
@options={{hash arrow=null}}
|
@options={{hash arrow=null}}
|
||||||
as |state|
|
as |state|
|
||||||
>
|
>
|
||||||
<FlatButton
|
<FlatButton
|
||||||
@disabled={{this.isDisabled}}
|
@disabled={{@isDisabled}}
|
||||||
@class="chat-composer-dropdown__trigger-btn d-popover-trigger"
|
@class="chat-composer-dropdown__trigger-btn d-popover-trigger"
|
||||||
@title="chat.composer.toggle_toolbar"
|
@title="chat.composer.toggle_toolbar"
|
||||||
@icon={{if state.isExpanded "times" "plus"}}
|
@icon={{if state.isExpanded "times" "plus"}}
|
||||||
/>
|
/>
|
||||||
<ul class="chat-composer-dropdown__list">
|
<ul class="chat-composer-dropdown__list">
|
||||||
{{#each this.buttons as |button|}}
|
{{#each @buttons as |button|}}
|
||||||
<li class="chat-composer-dropdown__item {{button.id}}">
|
<li class="chat-composer-dropdown__item {{button.id}}">
|
||||||
<DButton
|
<DButton
|
||||||
@class={{concat "chat-composer-dropdown__action-btn " button.id}}
|
@class={{concat "chat-composer-dropdown__action-btn " button.id}}
|
||||||
|
|
|
@ -1,7 +0,0 @@
|
||||||
import Component from "@ember/component";
|
|
||||||
|
|
||||||
export default class ChatComposerDropdown extends Component {
|
|
||||||
tagName = "";
|
|
||||||
buttons = null;
|
|
||||||
isDisabled = false;
|
|
||||||
}
|
|
|
@ -5,6 +5,7 @@ import { inject as service } from "@ember/service";
|
||||||
import UppyMediaOptimization from "discourse/lib/uppy-media-optimization-plugin";
|
import UppyMediaOptimization from "discourse/lib/uppy-media-optimization-plugin";
|
||||||
import discourseComputed, { bind } from "discourse-common/utils/decorators";
|
import discourseComputed, { bind } from "discourse-common/utils/decorators";
|
||||||
import UppyUploadMixin from "discourse/mixins/uppy-upload";
|
import UppyUploadMixin from "discourse/mixins/uppy-upload";
|
||||||
|
import { cloneJSON } from "discourse-common/lib/object";
|
||||||
|
|
||||||
export default Component.extend(UppyUploadMixin, {
|
export default Component.extend(UppyUploadMixin, {
|
||||||
classNames: ["chat-composer-uploads"],
|
classNames: ["chat-composer-uploads"],
|
||||||
|
@ -12,16 +13,25 @@ export default Component.extend(UppyUploadMixin, {
|
||||||
chatStateManager: service(),
|
chatStateManager: service(),
|
||||||
id: "chat-composer-uploader",
|
id: "chat-composer-uploader",
|
||||||
type: "chat-composer",
|
type: "chat-composer",
|
||||||
|
existingUploads: null,
|
||||||
uploads: null,
|
uploads: null,
|
||||||
useMultipartUploadsIfAvailable: true,
|
useMultipartUploadsIfAvailable: true,
|
||||||
|
|
||||||
init() {
|
init() {
|
||||||
this._super(...arguments);
|
this._super(...arguments);
|
||||||
this.setProperties({
|
this.setProperties({
|
||||||
uploads: [],
|
|
||||||
fileInputSelector: `#${this.fileUploadElementId}`,
|
fileInputSelector: `#${this.fileUploadElementId}`,
|
||||||
});
|
});
|
||||||
this.appEvents.on("chat-composer:load-uploads", this, "_loadUploads");
|
},
|
||||||
|
|
||||||
|
didReceiveAttrs() {
|
||||||
|
this._super(...arguments);
|
||||||
|
|
||||||
|
this.set(
|
||||||
|
"uploads",
|
||||||
|
this.existingUploads ? cloneJSON(this.existingUploads) : []
|
||||||
|
);
|
||||||
|
this._uppyInstance?.cancelAll();
|
||||||
},
|
},
|
||||||
|
|
||||||
didInsertElement() {
|
didInsertElement() {
|
||||||
|
@ -32,7 +42,7 @@ export default Component.extend(UppyUploadMixin, {
|
||||||
|
|
||||||
willDestroyElement() {
|
willDestroyElement() {
|
||||||
this._super(...arguments);
|
this._super(...arguments);
|
||||||
this.appEvents.off("chat-composer:load-uploads", this, "_loadUploads");
|
|
||||||
this.composerInputEl?.removeEventListener(
|
this.composerInputEl?.removeEventListener(
|
||||||
"paste",
|
"paste",
|
||||||
this._pasteEventListener
|
this._pasteEventListener
|
||||||
|
@ -81,11 +91,6 @@ export default Component.extend(UppyUploadMixin, {
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
_loadUploads(uploads) {
|
|
||||||
this._uppyInstance?.cancelAll();
|
|
||||||
this.set("uploads", uploads);
|
|
||||||
},
|
|
||||||
|
|
||||||
_uppyReady() {
|
_uppyReady() {
|
||||||
if (this.siteSettings.composer_media_optimization_image_enabled) {
|
if (this.siteSettings.composer_media_optimization_image_enabled) {
|
||||||
this._useUploadPlugin(UppyMediaOptimization, {
|
this._useUploadPlugin(UppyMediaOptimization, {
|
||||||
|
|
|
@ -79,7 +79,11 @@
|
||||||
{{#if this.canAttachUploads}}
|
{{#if this.canAttachUploads}}
|
||||||
<ChatComposerUploads
|
<ChatComposerUploads
|
||||||
@fileUploadElementId={{this.fileUploadElementId}}
|
@fileUploadElementId={{this.fileUploadElementId}}
|
||||||
@onUploadChanged={{action "uploadsChanged"}}
|
@onUploadChanged={{this.uploadsChanged}}
|
||||||
|
@existingUploads={{or
|
||||||
|
this.chatChannel.draft.uploads
|
||||||
|
this.editingMessage.uploads
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
{{/if}}
|
{{/if}}
|
||||||
|
|
||||||
|
|
|
@ -29,11 +29,10 @@ const THROTTLE_MS = 150;
|
||||||
|
|
||||||
export default Component.extend(TextareaTextManipulation, {
|
export default Component.extend(TextareaTextManipulation, {
|
||||||
chatChannel: null,
|
chatChannel: null,
|
||||||
lastChatChannelId: null,
|
|
||||||
chat: service(),
|
chat: service(),
|
||||||
classNames: ["chat-composer-container"],
|
classNames: ["chat-composer-container"],
|
||||||
classNameBindings: ["emojiPickerVisible:with-emoji-picker"],
|
classNameBindings: ["emojiPickerVisible:with-emoji-picker"],
|
||||||
userSilenced: readOnly("details.user_silenced"),
|
userSilenced: readOnly("chatChannel.userSilenced"),
|
||||||
chatEmojiReactionStore: service("chat-emoji-reaction-store"),
|
chatEmojiReactionStore: service("chat-emoji-reaction-store"),
|
||||||
chatEmojiPickerManager: service("chat-emoji-picker-manager"),
|
chatEmojiPickerManager: service("chat-emoji-picker-manager"),
|
||||||
chatStateManager: service("chat-state-manager"),
|
chatStateManager: service("chat-state-manager"),
|
||||||
|
@ -220,18 +219,18 @@ export default Component.extend(TextareaTextManipulation, {
|
||||||
|
|
||||||
if (
|
if (
|
||||||
!this.editingMessage &&
|
!this.editingMessage &&
|
||||||
this.draft &&
|
this.chatChannel?.draft &&
|
||||||
this.chatChannel?.canModifyMessages(this.currentUser)
|
this.chatChannel?.canModifyMessages(this.currentUser)
|
||||||
) {
|
) {
|
||||||
// uses uploads from draft here...
|
// uses uploads from draft here...
|
||||||
this.setProperties({
|
this.setProperties({
|
||||||
value: this.draft.value,
|
value: this.chatChannel.draft.message,
|
||||||
replyToMsg: this.draft.replyToMsg,
|
replyToMsg: this.chatChannel.draft.replyToMsg,
|
||||||
});
|
});
|
||||||
|
|
||||||
this._captureMentions();
|
this._captureMentions();
|
||||||
this._syncUploads(this.draft.uploads);
|
this._syncUploads(this.chatChannel.draft.uploads);
|
||||||
this.setInReplyToMsg(this.draft.replyToMsg);
|
this.setInReplyToMsg(this.chatChannel.draft.replyToMsg);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.editingMessage && !this.loading) {
|
if (this.editingMessage && !this.loading) {
|
||||||
|
@ -244,7 +243,6 @@ export default Component.extend(TextareaTextManipulation, {
|
||||||
this._focusTextArea({ ensureAtEnd: true, resizeTextarea: false });
|
this._focusTextArea({ ensureAtEnd: true, resizeTextarea: false });
|
||||||
}
|
}
|
||||||
|
|
||||||
this.set("lastChatChannelId", this.chatChannel.id);
|
|
||||||
this.resizeTextarea();
|
this.resizeTextarea();
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -271,7 +269,6 @@ export default Component.extend(TextareaTextManipulation, {
|
||||||
}
|
}
|
||||||
|
|
||||||
this.set("_uploads", cloneJSON(newUploads));
|
this.set("_uploads", cloneJSON(newUploads));
|
||||||
this.appEvents.trigger("chat-composer:load-uploads", this._uploads);
|
|
||||||
},
|
},
|
||||||
|
|
||||||
_inProgressUploadsChanged(inProgressUploads) {
|
_inProgressUploadsChanged(inProgressUploads) {
|
||||||
|
@ -286,7 +283,7 @@ export default Component.extend(TextareaTextManipulation, {
|
||||||
|
|
||||||
_replyToMsgChanged(replyToMsg) {
|
_replyToMsgChanged(replyToMsg) {
|
||||||
this.set("replyToMsg", replyToMsg);
|
this.set("replyToMsg", replyToMsg);
|
||||||
this.onValueChange?.(this.value, this._uploads, replyToMsg);
|
this.onValueChange?.({ replyToMsg });
|
||||||
},
|
},
|
||||||
|
|
||||||
@action
|
@action
|
||||||
|
@ -302,12 +299,14 @@ export default Component.extend(TextareaTextManipulation, {
|
||||||
|
|
||||||
@bind
|
@bind
|
||||||
_handleTextareaInput() {
|
_handleTextareaInput() {
|
||||||
this.onValueChange?.(this.value, this._uploads, this.replyToMsg);
|
this.onValueChange?.({ value: this.value });
|
||||||
},
|
},
|
||||||
|
|
||||||
@bind
|
@bind
|
||||||
_captureMentions() {
|
_captureMentions() {
|
||||||
this.chatComposerWarningsTracker.trackMentions(this.value);
|
if (this.value) {
|
||||||
|
this.chatComposerWarningsTracker.trackMentions(this.value);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
@bind
|
@bind
|
||||||
|
@ -699,7 +698,7 @@ export default Component.extend(TextareaTextManipulation, {
|
||||||
cancelReplyTo() {
|
cancelReplyTo() {
|
||||||
this.set("replyToMsg", null);
|
this.set("replyToMsg", null);
|
||||||
this.setInReplyToMsg(null);
|
this.setInReplyToMsg(null);
|
||||||
this.onValueChange?.(this.value, this._uploads, this.replyToMsg);
|
this.onValueChange?.({ replyToMsg: null });
|
||||||
},
|
},
|
||||||
|
|
||||||
@action
|
@action
|
||||||
|
@ -722,7 +721,7 @@ export default Component.extend(TextareaTextManipulation, {
|
||||||
@action
|
@action
|
||||||
uploadsChanged(uploads) {
|
uploadsChanged(uploads) {
|
||||||
this.set("_uploads", cloneJSON(uploads));
|
this.set("_uploads", cloneJSON(uploads));
|
||||||
this.onValueChange?.(this.value, this._uploads, this.replyToMsg);
|
this.onValueChange?.({ uploads: this._uploads });
|
||||||
},
|
},
|
||||||
|
|
||||||
@action
|
@action
|
||||||
|
|
|
@ -19,9 +19,6 @@
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{{#if this.previewedChannel}}
|
{{#if this.previewedChannel}}
|
||||||
<ChatLivePane
|
<ChatLivePane @channel={{this.previewedChannel}} @includeHeader={{false}} />
|
||||||
@chatChannel={{this.previewedChannel}}
|
|
||||||
@includeHeader={{false}}
|
|
||||||
/>
|
|
||||||
{{/if}}
|
{{/if}}
|
||||||
</div>
|
</div>
|
|
@ -18,7 +18,7 @@
|
||||||
{{#if this.chat.activeChannel}}
|
{{#if this.chat.activeChannel}}
|
||||||
<ChatLivePane
|
<ChatLivePane
|
||||||
@targetMessageId={{readonly @params.messageId}}
|
@targetMessageId={{readonly @params.messageId}}
|
||||||
@chatChannel={{this.chat.activeChannel}}
|
@channel={{this.chat.activeChannel}}
|
||||||
/>
|
/>
|
||||||
{{/if}}
|
{{/if}}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -65,6 +65,10 @@ export default class ChatEmojiPicker extends Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
get flatEmojis() {
|
get flatEmojis() {
|
||||||
|
if (!this.chatEmojiPickerManager.emojis) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
// eslint-disable-next-line no-unused-vars
|
// eslint-disable-next-line no-unused-vars
|
||||||
let { favorites, ...rest } = this.chatEmojiPickerManager.emojis;
|
let { favorites, ...rest } = this.chatEmojiPickerManager.emojis;
|
||||||
return Object.values(rest).flat();
|
return Object.values(rest).flat();
|
||||||
|
|
|
@ -0,0 +1,46 @@
|
||||||
|
{{#if
|
||||||
|
(and
|
||||||
|
this.chatStateManager.isFullPageActive this.displayed (not @channel.isDraft)
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
<div
|
||||||
|
class={{concat-class
|
||||||
|
"chat-full-page-header"
|
||||||
|
(unless @channel.isFollowing "-not-following")
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div class="chat-channel-header-details">
|
||||||
|
{{#if this.site.mobileView}}
|
||||||
|
<div class="chat-full-page-header__left-actions">
|
||||||
|
<LinkTo
|
||||||
|
@route="chat"
|
||||||
|
class="chat-full-page-header__back-btn no-text btn-flat"
|
||||||
|
>
|
||||||
|
{{d-icon "chevron-left"}}
|
||||||
|
</LinkTo>
|
||||||
|
</div>
|
||||||
|
{{/if}}
|
||||||
|
|
||||||
|
<LinkTo
|
||||||
|
@route="chat.channel.info"
|
||||||
|
@models={{@channel.routeModels}}
|
||||||
|
class="chat-channel-title-wrapper"
|
||||||
|
>
|
||||||
|
<ChatChannelTitle @channel={{@channel}} />
|
||||||
|
</LinkTo>
|
||||||
|
|
||||||
|
{{#if this.site.desktopView}}
|
||||||
|
<div class="chat-full-page-header__right-actions">
|
||||||
|
<DButton
|
||||||
|
@icon="discourse-compress"
|
||||||
|
@title="chat.close_full_page"
|
||||||
|
class="open-drawer-btn btn-flat no-text"
|
||||||
|
@action={{@onCloseFullScreen}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{{/if}}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<ChatChannelStatus @channel={{@channel}} />
|
||||||
|
{{/if}}
|
|
@ -0,0 +1,11 @@
|
||||||
|
import { inject as service } from "@ember/service";
|
||||||
|
import Component from "@glimmer/component";
|
||||||
|
|
||||||
|
export default class ChatFullPageHeader extends Component {
|
||||||
|
@service site;
|
||||||
|
@service chatStateManager;
|
||||||
|
|
||||||
|
get displayed() {
|
||||||
|
return this.args.displayed ?? true;
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,148 +1,118 @@
|
||||||
{{#if (and this.chatStateManager.isFullPageActive this.includeHeader)}}
|
|
||||||
<div
|
|
||||||
class="chat-full-page-header
|
|
||||||
{{unless this.chatChannel.isFollowing '-not-following'}}"
|
|
||||||
>
|
|
||||||
<div class="chat-channel-header-details">
|
|
||||||
{{#if this.site.mobileView}}
|
|
||||||
<div class="chat-full-page-header__left-actions">
|
|
||||||
<DButton
|
|
||||||
@class="chat-full-page-header__back-btn no-text btn-flat"
|
|
||||||
@icon="chevron-left"
|
|
||||||
@action={{this.onBackClick}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
{{/if}}
|
|
||||||
|
|
||||||
<LinkTo
|
|
||||||
@route="chat.channel.info"
|
|
||||||
@models={{this.chatChannel.routeModels}}
|
|
||||||
class="chat-channel-title-wrapper"
|
|
||||||
>
|
|
||||||
<ChatChannelTitle @channel={{this.chatChannel}} />
|
|
||||||
</LinkTo>
|
|
||||||
|
|
||||||
{{#if this.showCloseFullScreenBtn}}
|
|
||||||
<div class="chat-full-page-header__right-actions">
|
|
||||||
<DButton
|
|
||||||
@icon="discourse-compress"
|
|
||||||
@title="chat.close_full_page"
|
|
||||||
class="open-drawer-btn btn-flat no-text"
|
|
||||||
@action={{action this.onCloseFullScreen}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
{{/if}}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<ChatChannelStatus @channel={{this.chatChannel}} />
|
|
||||||
{{/if}}
|
|
||||||
|
|
||||||
<ChatRetentionReminder @chatChannel={{this.chatChannel}} />
|
|
||||||
|
|
||||||
<ChatMentionWarnings />
|
|
||||||
|
|
||||||
<div class="chat-message-actions-mobile-anchor"></div>
|
|
||||||
<div
|
<div
|
||||||
class={{concat-class
|
class={{concat-class
|
||||||
"chat-message-emoji-picker-anchor"
|
"chat-live-pane"
|
||||||
(if
|
(if this.loading "loading")
|
||||||
(and
|
(if this.sendingLoading "sending-loading")
|
||||||
this.chatEmojiPickerManager.opened
|
|
||||||
(eq this.chatEmojiPickerManager.context "chat-message")
|
|
||||||
)
|
|
||||||
"-opened"
|
|
||||||
)
|
|
||||||
}}
|
}}
|
||||||
|
{{did-insert this.setupListeners}}
|
||||||
|
{{will-destroy this.teardownListeners}}
|
||||||
|
{{did-insert this.updateChannel}}
|
||||||
|
{{did-update this.loadMessages @targetMessageId}}
|
||||||
|
{{did-update this.updateChannel @channel.id}}
|
||||||
|
{{did-insert this.addAutoFocusEventListener}}
|
||||||
|
{{will-destroy this.removeAutoFocusEventListener}}
|
||||||
>
|
>
|
||||||
</div>
|
<ChatFullPageHeader
|
||||||
|
@channel={{@channel}}
|
||||||
<div class="chat-messages-scroll chat-messages-container">
|
@onCloseFullScreen={{this.onCloseFullScreen}}
|
||||||
<div class="chat-message-actions-desktop-anchor"></div>
|
@displayed={{this.includeHeader}}
|
||||||
<div class="chat-messages-container">
|
|
||||||
{{#if (or this.loading this.loadingMorePast)}}
|
|
||||||
<ChatSkeleton @tagName="" />
|
|
||||||
{{/if}}
|
|
||||||
|
|
||||||
{{#each this.messages as |message|}}
|
|
||||||
<ChatMessage
|
|
||||||
@message={{message}}
|
|
||||||
@canInteractWithChat={{this.canInteractWithChat}}
|
|
||||||
@details={{this.details}}
|
|
||||||
@chatChannel={{this.chatChannel}}
|
|
||||||
@setReplyTo={{action "setReplyTo"}}
|
|
||||||
@replyMessageClicked={{action "replyMessageClicked"}}
|
|
||||||
@editButtonClicked={{action "editButtonClicked"}}
|
|
||||||
@selectingMessages={{this.selectingMessages}}
|
|
||||||
@onStartSelectingMessages={{this.onStartSelectingMessages}}
|
|
||||||
@onSelectMessage={{this.onSelectMessage}}
|
|
||||||
@bulkSelectMessages={{this.bulkSelectMessages}}
|
|
||||||
@fullPage={{this.fullPage}}
|
|
||||||
@afterReactionAdded={{action "reStickScrollIfNeeded"}}
|
|
||||||
@isHovered={{eq message.id this.hoveredMessageId}}
|
|
||||||
@onHoverMessage={{this.onHoverMessage}}
|
|
||||||
@resendStagedMessage={{this.resendStagedMessage}}
|
|
||||||
/>
|
|
||||||
{{/each}}
|
|
||||||
|
|
||||||
{{#if this.loadingMoreFuture}}
|
|
||||||
<ChatSkeleton @tagName="" />
|
|
||||||
{{/if}}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{{#if this.allPastMessagesLoaded}}
|
|
||||||
<div class="all-loaded-message">
|
|
||||||
{{i18n "chat.all_loaded"}}
|
|
||||||
</div>
|
|
||||||
{{/if}}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{{#if this.showScrollToBottomBtn}}
|
|
||||||
<div class="scroll-stick-wrap">
|
|
||||||
<a
|
|
||||||
href
|
|
||||||
title={{i18n "chat.scroll_to_bottom"}}
|
|
||||||
class={{concat-class
|
|
||||||
"btn"
|
|
||||||
"btn-flat"
|
|
||||||
"chat-scroll-to-bottom"
|
|
||||||
(if this.hasNewMessages "unread-messages")
|
|
||||||
}}
|
|
||||||
{{on "click" (action "restickScrolling")}}
|
|
||||||
>
|
|
||||||
{{#if this.hasNewMessages}}
|
|
||||||
{{i18n "chat.scroll_to_new_messages"}}
|
|
||||||
{{/if}}
|
|
||||||
{{d-icon "arrow-down"}}
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
{{/if}}
|
|
||||||
|
|
||||||
{{#if this.selectingMessages}}
|
|
||||||
<ChatSelectionManager
|
|
||||||
@selectedMessageIds={{this.selectedMessageIds}}
|
|
||||||
@chatChannel={{this.chatChannel}}
|
|
||||||
@canModerate={{this.details.can_moderate}}
|
|
||||||
@cancelSelecting={{action "cancelSelecting"}}
|
|
||||||
/>
|
/>
|
||||||
{{else}}
|
|
||||||
{{#if (or this.chatChannel.isDraft this.chatChannel.isFollowing)}}
|
<ChatRetentionReminder @channel={{@channel}} />
|
||||||
<ChatComposer
|
|
||||||
@draft={{this.draft}}
|
<ChatMentionWarnings />
|
||||||
@details={{this.details}}
|
|
||||||
@canInteractWithChat={{this.canInteractWithChat}}
|
<div class="chat-message-actions-mobile-anchor"></div>
|
||||||
@sendMessage={{action "sendMessage"}}
|
|
||||||
@editMessage={{action "editMessage"}}
|
<div
|
||||||
@setReplyTo={{action "setReplyTo"}}
|
class={{concat-class
|
||||||
@loading={{this.sendingLoading}}
|
"chat-message-emoji-picker-anchor"
|
||||||
@editingMessage={{readonly this.editingMessage}}
|
(if
|
||||||
@onCancelEditing={{this.cancelEditing}}
|
(and
|
||||||
@setInReplyToMsg={{this.setInReplyToMsg}}
|
this.chatEmojiPickerManager.opened
|
||||||
@onEditLastMessageRequested={{this.editLastMessageRequested}}
|
(eq this.chatEmojiPickerManager.context "chat-message")
|
||||||
@onValueChange={{action "composerValueChanged"}}
|
)
|
||||||
@chatChannel={{this.chatChannel}}
|
"-opened"
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
></div>
|
||||||
|
|
||||||
|
<div class="chat-messages-scroll chat-messages-container">
|
||||||
|
<div class="chat-message-actions-desktop-anchor"></div>
|
||||||
|
<div class="chat-messages-container">
|
||||||
|
|
||||||
|
{{#if this.loadingMorePast}}
|
||||||
|
<ChatSkeleton
|
||||||
|
@onInsert={{this.onDidInsertSkeleton}}
|
||||||
|
@onDestroy={{this.onDestroySkeleton}}
|
||||||
|
/>
|
||||||
|
{{/if}}
|
||||||
|
|
||||||
|
{{#each @channel.messages key="id" as |message|}}
|
||||||
|
<ChatMessage
|
||||||
|
@message={{message}}
|
||||||
|
@canInteractWithChat={{this.canInteractWithChat}}
|
||||||
|
@channel={{@channel}}
|
||||||
|
@setReplyTo={{this.setReplyTo}}
|
||||||
|
@replyMessageClicked={{this.replyMessageClicked}}
|
||||||
|
@editButtonClicked={{this.editButtonClicked}}
|
||||||
|
@selectingMessages={{this.selectingMessages}}
|
||||||
|
@onStartSelectingMessages={{this.onStartSelectingMessages}}
|
||||||
|
@onSelectMessage={{this.onSelectMessage}}
|
||||||
|
@bulkSelectMessages={{this.bulkSelectMessages}}
|
||||||
|
@isHovered={{eq message.id this.hoveredMessageId}}
|
||||||
|
@onHoverMessage={{this.onHoverMessage}}
|
||||||
|
@resendStagedMessage={{this.resendStagedMessage}}
|
||||||
|
@didShowMessage={{this.didShowMessage}}
|
||||||
|
@didHideMessage={{this.didHideMessage}}
|
||||||
|
/>
|
||||||
|
{{/each}}
|
||||||
|
|
||||||
|
{{#if (or this.loadingMoreFuture)}}
|
||||||
|
<ChatSkeleton
|
||||||
|
@onInsert={{this.onDidInsertSkeleton}}
|
||||||
|
@onDestroy={{this.onDestroySkeleton}}
|
||||||
|
/>
|
||||||
|
{{/if}}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{#if (and this.loadedOnce (not @channel.canLoadMorePast))}}
|
||||||
|
<div class="all-loaded-message">
|
||||||
|
{{i18n "chat.all_loaded"}}
|
||||||
|
</div>
|
||||||
|
{{/if}}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<ChatScrollToBottomArrow
|
||||||
|
@scrollToBottom={{this.scrollToBottom}}
|
||||||
|
@hasNewMessages={{this.hasNewMessages}}
|
||||||
|
@isAlmostDocked={{this.isAlmostDocked}}
|
||||||
|
@channel={{@channel}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{{#if this.selectingMessages}}
|
||||||
|
<ChatSelectionManager
|
||||||
|
@selectedMessageIds={{this.selectedMessageIds}}
|
||||||
|
@chatChannel={{@channel}}
|
||||||
|
@cancelSelecting={{this.cancelSelecting}}
|
||||||
/>
|
/>
|
||||||
{{else}}
|
{{else}}
|
||||||
<ChatChannelPreviewCard @channel={{this.chatChannel}} />
|
{{#if (or @channel.isDraft @channel.isFollowing)}}
|
||||||
|
<ChatComposer
|
||||||
|
@canInteractWithChat={{this.canInteractWithChat}}
|
||||||
|
@sendMessage={{this.sendMessage}}
|
||||||
|
@editMessage={{this.editMessage}}
|
||||||
|
@setReplyTo={{this.setReplyTo}}
|
||||||
|
@loading={{this.sendingLoading}}
|
||||||
|
@editingMessage={{readonly this.editingMessage}}
|
||||||
|
@onCancelEditing={{this.cancelEditing}}
|
||||||
|
@setInReplyToMsg={{this.setInReplyToMsg}}
|
||||||
|
@onEditLastMessageRequested={{this.editLastMessageRequested}}
|
||||||
|
@onValueChange={{this.composerValueChanged}}
|
||||||
|
@chatChannel={{@channel}}
|
||||||
|
/>
|
||||||
|
{{else}}
|
||||||
|
<ChatChannelPreviewCard @channel={{@channel}} />
|
||||||
|
{{/if}}
|
||||||
{{/if}}
|
{{/if}}
|
||||||
{{/if}}
|
</div>
|
File diff suppressed because it is too large
Load Diff
|
@ -6,11 +6,11 @@
|
||||||
>
|
>
|
||||||
<div class="chat-message-actions">
|
<div class="chat-message-actions">
|
||||||
{{#if this.chatStateManager.isFullPageActive}}
|
{{#if this.chatStateManager.isFullPageActive}}
|
||||||
{{#each @emojiReactions as |reaction|}}
|
{{#each @emojiReactions key="emoji" as |reaction|}}
|
||||||
<ChatMessageReaction
|
<ChatMessageReaction
|
||||||
@reaction={{reaction}}
|
@reaction={{reaction}}
|
||||||
@react={{@messageActions.react}}
|
@react={{@messageActions.react}}
|
||||||
@class="show"
|
@showCount={{false}}
|
||||||
/>
|
/>
|
||||||
{{/each}}
|
{{/each}}
|
||||||
{{/if}}
|
{{/if}}
|
||||||
|
|
|
@ -31,6 +31,7 @@ export default class ChatMessageActionsDesktop extends Component {
|
||||||
),
|
),
|
||||||
{
|
{
|
||||||
placement: "top-end",
|
placement: "top-end",
|
||||||
|
strategy: "fixed",
|
||||||
modifiers: [
|
modifiers: [
|
||||||
{ name: "hide", enabled: true },
|
{ name: "hide", enabled: true },
|
||||||
{ name: "eventListeners", options: { scroll: false } },
|
{ name: "eventListeners", options: { scroll: false } },
|
||||||
|
|
|
@ -53,7 +53,7 @@
|
||||||
<ChatMessageReaction
|
<ChatMessageReaction
|
||||||
@reaction={{reaction}}
|
@reaction={{reaction}}
|
||||||
@react={{@messageActions.react}}
|
@react={{@messageActions.react}}
|
||||||
@class="show"
|
@showCount={{false}}
|
||||||
/>
|
/>
|
||||||
{{/each}}
|
{{/each}}
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
<div class="chat-message-avatar">
|
<div class="chat-message-avatar">
|
||||||
{{#if @message.chat_webhook_event.emoji}}
|
{{#if @message.chatWebhookEvent.emoji}}
|
||||||
<ChatEmojiAvatar @emoji={{@message.chat_webhook_event.emoji}} />
|
<ChatEmojiAvatar @emoji={{@message.chatWebhookEvent.emoji}} />
|
||||||
{{else}}
|
{{else}}
|
||||||
<ChatUserAvatar @user={{@message.user}} @avatarSize="medium" />
|
<ChatUserAvatar @user={{@message.user}} @avatarSize="medium" />
|
||||||
{{/if}}
|
{{/if}}
|
||||||
|
|
|
@ -1,5 +0,0 @@
|
||||||
import Component from "@ember/component";
|
|
||||||
|
|
||||||
export default class ChatMessageAvatar extends Component {
|
|
||||||
tagName = "";
|
|
||||||
}
|
|
|
@ -1,10 +1,10 @@
|
||||||
<div class="chat-message-collapser">
|
<div class="chat-message-collapser">
|
||||||
{{#if this.hasUploads}}
|
{{#if this.hasUploads}}
|
||||||
{{html-safe this.cooked}}
|
{{html-safe @cooked}}
|
||||||
|
|
||||||
<Collapser @header={{this.uploadsHeader}}>
|
<Collapser @header={{this.uploadsHeader}}>
|
||||||
<div class="chat-uploads">
|
<div class="chat-uploads">
|
||||||
{{#each this.uploads as |upload|}}
|
{{#each @uploads as |upload|}}
|
||||||
<ChatUpload @upload={{upload}} />
|
<ChatUpload @upload={{upload}} />
|
||||||
{{/each}}
|
{{/each}}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,28 +1,20 @@
|
||||||
import Component from "@ember/component";
|
import Component from "@glimmer/component";
|
||||||
import { computed } from "@ember/object";
|
|
||||||
import { htmlSafe } from "@ember/template";
|
import { htmlSafe } from "@ember/template";
|
||||||
import { escapeExpression } from "discourse/lib/utilities";
|
import { escapeExpression } from "discourse/lib/utilities";
|
||||||
import domFromString from "discourse-common/lib/dom-from-string";
|
import domFromString from "discourse-common/lib/dom-from-string";
|
||||||
import I18n from "I18n";
|
import I18n from "I18n";
|
||||||
|
|
||||||
export default class ChatMessageCollapser extends Component {
|
export default class ChatMessageCollapser extends Component {
|
||||||
tagName = "";
|
|
||||||
collapsed = false;
|
|
||||||
uploads = null;
|
|
||||||
cooked = null;
|
|
||||||
|
|
||||||
@computed("uploads")
|
|
||||||
get hasUploads() {
|
get hasUploads() {
|
||||||
return hasUploads(this.uploads);
|
return hasUploads(this.args.uploads);
|
||||||
}
|
}
|
||||||
|
|
||||||
@computed("uploads")
|
|
||||||
get uploadsHeader() {
|
get uploadsHeader() {
|
||||||
let name = "";
|
let name = "";
|
||||||
if (this.uploads.length === 1) {
|
if (this.args.uploads.length === 1) {
|
||||||
name = this.uploads[0].original_filename;
|
name = this.args.uploads[0].original_filename;
|
||||||
} else {
|
} else {
|
||||||
name = I18n.t("chat.uploaded_files", { count: this.uploads.length });
|
name = I18n.t("chat.uploaded_files", { count: this.args.uploads.length });
|
||||||
}
|
}
|
||||||
return htmlSafe(
|
return htmlSafe(
|
||||||
`<span class="chat-message-collapser-link-small">${escapeExpression(
|
`<span class="chat-message-collapser-link-small">${escapeExpression(
|
||||||
|
@ -31,9 +23,10 @@ export default class ChatMessageCollapser extends Component {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@computed("cooked")
|
|
||||||
get cookedBodies() {
|
get cookedBodies() {
|
||||||
const elements = Array.prototype.slice.call(domFromString(this.cooked));
|
const elements = Array.prototype.slice.call(
|
||||||
|
domFromString(this.args.cooked)
|
||||||
|
);
|
||||||
|
|
||||||
if (hasYoutube(elements)) {
|
if (hasYoutube(elements)) {
|
||||||
return this.youtubeCooked(elements);
|
return this.youtubeCooked(elements);
|
||||||
|
|
|
@ -0,0 +1,19 @@
|
||||||
|
{{#if @message.inReplyTo}}
|
||||||
|
<LinkTo
|
||||||
|
@route={{this.route}}
|
||||||
|
@models={{this.model}}
|
||||||
|
class="chat-reply is-direct-reply"
|
||||||
|
>
|
||||||
|
{{d-icon "share" title="chat.in_reply_to"}}
|
||||||
|
|
||||||
|
{{#if @message.inReplyTo.chatWebhookEvent.emoji}}
|
||||||
|
<ChatEmojiAvatar @emoji={{@message.inReplyTo.chatWebhookEvent.emoji}} />
|
||||||
|
{{else}}
|
||||||
|
<ChatUserAvatar @user={{@message.inReplyTo.user}} />
|
||||||
|
{{/if}}
|
||||||
|
|
||||||
|
<span class="chat-reply__excerpt">
|
||||||
|
{{replace-emoji @message.inReplyTo.excerpt}}
|
||||||
|
</span>
|
||||||
|
</LinkTo>
|
||||||
|
{{/if}}
|
|
@ -0,0 +1,32 @@
|
||||||
|
import Component from "@glimmer/component";
|
||||||
|
import { inject as service } from "@ember/service";
|
||||||
|
|
||||||
|
export default class ChatMessageInReplyToIndicator extends Component {
|
||||||
|
@service router;
|
||||||
|
|
||||||
|
get route() {
|
||||||
|
if (this.hasThread) {
|
||||||
|
return "chat.channel.thread";
|
||||||
|
} else {
|
||||||
|
return "chat.channel.near-message";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
get model() {
|
||||||
|
if (this.hasThread) {
|
||||||
|
return [this.args.message.threadId];
|
||||||
|
} else {
|
||||||
|
return [
|
||||||
|
...this.args.message.channel.routeModels,
|
||||||
|
this.args.message.inReplyTo.id,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
get hasThread() {
|
||||||
|
return (
|
||||||
|
this.args.message?.channel?.get("threading_enabled") &&
|
||||||
|
this.args.message?.threadId
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -3,15 +3,15 @@
|
||||||
{{did-insert this.trackStatus}}
|
{{did-insert this.trackStatus}}
|
||||||
{{will-destroy this.stopTrackingStatus}}
|
{{will-destroy this.stopTrackingStatus}}
|
||||||
>
|
>
|
||||||
{{#if @message.chat_webhook_event}}
|
{{#if @message.chatWebhookEvent}}
|
||||||
{{#if @message.chat_webhook_event.username}}
|
{{#if @message.chatWebhookEvent.username}}
|
||||||
<span
|
<span
|
||||||
class={{concat-class
|
class={{concat-class
|
||||||
"chat-message-info__username"
|
"chat-message-info__username"
|
||||||
this.usernameClasses
|
this.usernameClasses
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{{@message.chat_webhook_event.username}}
|
{{@message.chatWebhookEvent.username}}
|
||||||
</span>
|
</span>
|
||||||
{{/if}}
|
{{/if}}
|
||||||
|
|
||||||
|
@ -49,8 +49,8 @@
|
||||||
|
|
||||||
{{#if this.isFlagged}}
|
{{#if this.isFlagged}}
|
||||||
<span class="chat-message-info__flag">
|
<span class="chat-message-info__flag">
|
||||||
{{#if @message.reviewable_id}}
|
{{#if @message.reviewableId}}
|
||||||
<LinkTo @route="review.show" @model={{@message.reviewable_id}}>
|
<LinkTo @route="review.show" @model={{@message.reviewableId}}>
|
||||||
{{d-icon "flag" title="chat.flagged"}}
|
{{d-icon "flag" title="chat.flagged"}}
|
||||||
</LinkTo>
|
</LinkTo>
|
||||||
{{else}}
|
{{else}}
|
||||||
|
|
|
@ -48,10 +48,7 @@ export default class ChatMessageInfo extends Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
get isFlagged() {
|
get isFlagged() {
|
||||||
return (
|
return this.#message?.reviewableId || this.#message?.userFlagStatus === 0;
|
||||||
this.#message?.get("reviewable_id") ||
|
|
||||||
this.#message?.get("user_flag_status") === 0
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
get prioritizeName() {
|
get prioritizeName() {
|
||||||
|
@ -66,7 +63,7 @@ export default class ChatMessageInfo extends Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
get #user() {
|
get #user() {
|
||||||
return this.#message?.get("user");
|
return this.#message?.user;
|
||||||
}
|
}
|
||||||
|
|
||||||
get #message() {
|
get #message() {
|
||||||
|
|
|
@ -1,17 +1,17 @@
|
||||||
<div class="chat-message-left-gutter">
|
<div class="chat-message-left-gutter">
|
||||||
{{#if @message.reviewable_id}}
|
{{#if @message.reviewableId}}
|
||||||
<LinkTo
|
<LinkTo
|
||||||
@route="review.show"
|
@route="review.show"
|
||||||
@model={{@message.reviewable_id}}
|
@model={{@message.reviewableId}}
|
||||||
class="chat-message-left-gutter__flag"
|
class="chat-message-left-gutter__flag"
|
||||||
>
|
>
|
||||||
{{d-icon "flag" title="chat.flagged"}}
|
{{d-icon "flag" title="chat.flagged"}}
|
||||||
</LinkTo>
|
</LinkTo>
|
||||||
{{else if (eq @message.user_flag_status 0)}}
|
{{else if (eq @message.userFlagStatus 0)}}
|
||||||
<div class="chat-message-left-gutter__flag">
|
<div class="chat-message-left-gutter__flag">
|
||||||
{{d-icon "flag" title="chat.you_flagged"}}
|
{{d-icon "flag" title="chat.you_flagged"}}
|
||||||
</div>
|
</div>
|
||||||
{{else}}
|
{{else if this.site.desktopView}}
|
||||||
<span class="chat-message-left-gutter__date">
|
<span class="chat-message-left-gutter__date">
|
||||||
{{format-chat-date @message "tiny"}}
|
{{format-chat-date @message "tiny"}}
|
||||||
</span>
|
</span>
|
||||||
|
|
|
@ -0,0 +1,6 @@
|
||||||
|
import Component from "@glimmer/component";
|
||||||
|
import { inject as service } from "@ember/service";
|
||||||
|
|
||||||
|
export default class ChatMessageLeftGutter extends Component {
|
||||||
|
@service site;
|
||||||
|
}
|
|
@ -19,7 +19,7 @@
|
||||||
@class="btn-primary"
|
@class="btn-primary"
|
||||||
@icon="sign-out-alt"
|
@icon="sign-out-alt"
|
||||||
@disabled={{this.disableMoveButton}}
|
@disabled={{this.disableMoveButton}}
|
||||||
@action={{action "moveMessages"}}
|
@action={{this.moveMessages}}
|
||||||
@label="chat.move_to_channel.confirm_move"
|
@label="chat.move_to_channel.confirm_move"
|
||||||
@id="chat-confirm-move-messages-to-channel"
|
@id="chat-confirm-move-messages-to-channel"
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -1,17 +1,18 @@
|
||||||
{{#if (and this.reaction this.emojiUrl)}}
|
{{#if (and @reaction this.emojiUrl)}}
|
||||||
<button
|
<button
|
||||||
id={{this.componentId}}
|
|
||||||
type="button"
|
type="button"
|
||||||
{{on "click" (action "handleClick")}}
|
{{on "click" this.handleClick}}
|
||||||
tabindex="0"
|
tabindex="0"
|
||||||
class={{concat-class
|
class={{concat-class
|
||||||
this.class
|
|
||||||
"chat-message-reaction"
|
"chat-message-reaction"
|
||||||
(if this.reaction.reacted "reacted")
|
(if @reaction.reacted "reacted")
|
||||||
(if this.reaction.count "show")
|
|
||||||
}}
|
}}
|
||||||
data-emoji-name={{this.reaction.emoji}}
|
data-emoji-name={{@reaction.emoji}}
|
||||||
|
data-tippy-content={{this.popoverContent}}
|
||||||
title={{this.emojiString}}
|
title={{this.emojiString}}
|
||||||
|
{{did-insert this.setupTooltip}}
|
||||||
|
{{will-destroy this.teardownTooltip}}
|
||||||
|
{{did-update this.refreshTooltip this.popoverContent}}
|
||||||
>
|
>
|
||||||
<img
|
<img
|
||||||
loading="lazy"
|
loading="lazy"
|
||||||
|
@ -22,8 +23,8 @@
|
||||||
src={{this.emojiUrl}}
|
src={{this.emojiUrl}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{{#if this.reaction.count}}
|
{{#if (and this.showCount @reaction.count)}}
|
||||||
<span class="count">{{this.reaction.count}}</span>
|
<span class="count">{{@reaction.count}}</span>
|
||||||
{{/if}}
|
{{/if}}
|
||||||
</button>
|
</button>
|
||||||
{{/if}}
|
{{/if}}
|
|
@ -1,95 +1,73 @@
|
||||||
import { guidFor } from "@ember/object/internals";
|
import Component from "@glimmer/component";
|
||||||
import Component from "@ember/component";
|
import { action } from "@ember/object";
|
||||||
import { action, computed } from "@ember/object";
|
|
||||||
import { emojiUnescape, emojiUrlFor } from "discourse/lib/text";
|
import { emojiUnescape, emojiUrlFor } from "discourse/lib/text";
|
||||||
import setupPopover from "discourse/lib/d-popover";
|
|
||||||
import I18n from "I18n";
|
import I18n from "I18n";
|
||||||
import { schedule } from "@ember/runloop";
|
import { schedule } from "@ember/runloop";
|
||||||
|
import { inject as service } from "@ember/service";
|
||||||
|
import setupPopover from "discourse/lib/d-popover";
|
||||||
|
|
||||||
export default class ChatMessageReaction extends Component {
|
export default class ChatMessageReaction extends Component {
|
||||||
reaction = null;
|
@service currentUser;
|
||||||
showUsersList = false;
|
|
||||||
tagName = "";
|
|
||||||
react = null;
|
|
||||||
class = null;
|
|
||||||
|
|
||||||
didReceiveAttrs() {
|
get showCount() {
|
||||||
this._super(...arguments);
|
return this.args.showCount ?? true;
|
||||||
|
}
|
||||||
|
|
||||||
if (this.showUsersList) {
|
@action
|
||||||
|
setupTooltip(element) {
|
||||||
|
if (this.args.showTooltip) {
|
||||||
schedule("afterRender", () => {
|
schedule("afterRender", () => {
|
||||||
this._popover?.destroy();
|
this._tippyInstance?.destroy();
|
||||||
this._popover = this._setupPopover();
|
this._tippyInstance = setupPopover(element, {
|
||||||
|
interactive: false,
|
||||||
|
allowHTML: true,
|
||||||
|
delay: 250,
|
||||||
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
willDestroyElement() {
|
@action
|
||||||
this._super(...arguments);
|
teardownTooltip() {
|
||||||
|
this._tippyInstance?.destroy();
|
||||||
this._popover?.destroy();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@computed
|
@action
|
||||||
get componentId() {
|
refreshTooltip() {
|
||||||
return guidFor(this);
|
this._tippyInstance?.setContent(this.popoverContent);
|
||||||
}
|
}
|
||||||
|
|
||||||
@computed("reaction.emoji")
|
|
||||||
get emojiString() {
|
get emojiString() {
|
||||||
return `:${this.reaction.emoji}:`;
|
return `:${this.args.reaction.emoji}:`;
|
||||||
}
|
}
|
||||||
|
|
||||||
@computed("reaction.emoji")
|
|
||||||
get emojiUrl() {
|
get emojiUrl() {
|
||||||
return emojiUrlFor(this.reaction.emoji);
|
return emojiUrlFor(this.args.reaction.emoji);
|
||||||
}
|
}
|
||||||
|
|
||||||
@action
|
@action
|
||||||
handleClick() {
|
handleClick() {
|
||||||
this?.react(this.reaction.emoji, this.reaction.reacted ? "remove" : "add");
|
this.args.react?.(
|
||||||
|
this.args.reaction.emoji,
|
||||||
|
this.args.reaction.reacted ? "remove" : "add"
|
||||||
|
);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
_setupPopover() {
|
get popoverContent() {
|
||||||
const target = document.getElementById(this.componentId);
|
if (!this.args.reaction.count || !this.args.reaction.users?.length) {
|
||||||
|
|
||||||
if (!target) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const popover = setupPopover(target, {
|
return emojiUnescape(
|
||||||
interactive: false,
|
this.args.reaction.reacted
|
||||||
allowHTML: true,
|
? this.#reactionTextWithSelf
|
||||||
delay: 250,
|
: this.#reactionText
|
||||||
content: emojiUnescape(this.popoverContent),
|
);
|
||||||
onClickOutside(instance) {
|
|
||||||
instance.hide();
|
|
||||||
},
|
|
||||||
onTrigger(instance, event) {
|
|
||||||
// ensures we close other reactions popovers when triggering one
|
|
||||||
document
|
|
||||||
.querySelectorAll(".chat-message-reaction")
|
|
||||||
.forEach((chatMessageReaction) => {
|
|
||||||
chatMessageReaction?._tippy?.hide();
|
|
||||||
});
|
|
||||||
|
|
||||||
event.stopPropagation();
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
return popover?.id ? popover : null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@computed("reaction")
|
get #reactionTextWithSelf() {
|
||||||
get popoverContent() {
|
const reactionCount = this.args.reaction.count;
|
||||||
return this.reaction.reacted
|
|
||||||
? this._reactionTextWithSelf()
|
|
||||||
: this._reactionText();
|
|
||||||
}
|
|
||||||
|
|
||||||
_reactionTextWithSelf() {
|
|
||||||
const reactionCount = this.reaction.count;
|
|
||||||
|
|
||||||
if (reactionCount === 0) {
|
if (reactionCount === 0) {
|
||||||
return;
|
return;
|
||||||
|
@ -97,55 +75,55 @@ export default class ChatMessageReaction extends Component {
|
||||||
|
|
||||||
if (reactionCount === 1) {
|
if (reactionCount === 1) {
|
||||||
return I18n.t("chat.reactions.only_you", {
|
return I18n.t("chat.reactions.only_you", {
|
||||||
emoji: this.reaction.emoji,
|
emoji: this.args.reaction.emoji,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const maxUsernames = 4;
|
const maxUsernames = 5;
|
||||||
const usernames = this.reaction.users
|
const usernames = this.args.reaction.users
|
||||||
|
.filter((user) => user.id !== this.currentUser?.id)
|
||||||
.slice(0, maxUsernames)
|
.slice(0, maxUsernames)
|
||||||
.mapBy("username");
|
.mapBy("username");
|
||||||
|
|
||||||
if (reactionCount === 2) {
|
if (reactionCount === 2) {
|
||||||
return I18n.t("chat.reactions.you_and_single_user", {
|
return I18n.t("chat.reactions.you_and_single_user", {
|
||||||
emoji: this.reaction.emoji,
|
emoji: this.args.reaction.emoji,
|
||||||
username: usernames.pop(),
|
username: usernames.pop(),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// `-1` because the current user ("you") isn't included in `usernames`
|
const unnamedUserCount = reactionCount - usernames.length;
|
||||||
const unnamedUserCount = reactionCount - usernames.length - 1;
|
|
||||||
|
|
||||||
if (unnamedUserCount > 0) {
|
if (unnamedUserCount > 0) {
|
||||||
return I18n.t("chat.reactions.you_multiple_users_and_more", {
|
return I18n.t("chat.reactions.you_multiple_users_and_more", {
|
||||||
emoji: this.reaction.emoji,
|
emoji: this.args.reaction.emoji,
|
||||||
commaSeparatedUsernames: this._joinUsernames(usernames),
|
commaSeparatedUsernames: this._joinUsernames(usernames),
|
||||||
count: unnamedUserCount,
|
count: unnamedUserCount,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return I18n.t("chat.reactions.you_and_multiple_users", {
|
return I18n.t("chat.reactions.you_and_multiple_users", {
|
||||||
emoji: this.reaction.emoji,
|
emoji: this.args.reaction.emoji,
|
||||||
username: usernames.pop(),
|
username: usernames.pop(),
|
||||||
commaSeparatedUsernames: this._joinUsernames(usernames),
|
commaSeparatedUsernames: this._joinUsernames(usernames),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
_reactionText() {
|
get #reactionText() {
|
||||||
const reactionCount = this.reaction.count;
|
const reactionCount = this.args.reaction.count;
|
||||||
|
|
||||||
if (reactionCount === 0) {
|
if (reactionCount === 0) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const maxUsernames = 5;
|
const maxUsernames = 5;
|
||||||
const usernames = this.reaction.users
|
const usernames = this.args.reaction.users
|
||||||
|
.filter((user) => user.id !== this.currentUser?.id)
|
||||||
.slice(0, maxUsernames)
|
.slice(0, maxUsernames)
|
||||||
.mapBy("username");
|
.mapBy("username");
|
||||||
|
|
||||||
if (reactionCount === 1) {
|
if (reactionCount === 1) {
|
||||||
return I18n.t("chat.reactions.single_user", {
|
return I18n.t("chat.reactions.single_user", {
|
||||||
emoji: this.reaction.emoji,
|
emoji: this.args.reaction.emoji,
|
||||||
username: usernames.pop(),
|
username: usernames.pop(),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -154,14 +132,14 @@ export default class ChatMessageReaction extends Component {
|
||||||
|
|
||||||
if (unnamedUserCount > 0) {
|
if (unnamedUserCount > 0) {
|
||||||
return I18n.t("chat.reactions.multiple_users_and_more", {
|
return I18n.t("chat.reactions.multiple_users_and_more", {
|
||||||
emoji: this.reaction.emoji,
|
emoji: this.args.reaction.emoji,
|
||||||
commaSeparatedUsernames: this._joinUsernames(usernames),
|
commaSeparatedUsernames: this._joinUsernames(usernames),
|
||||||
count: unnamedUserCount,
|
count: unnamedUserCount,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return I18n.t("chat.reactions.multiple_users", {
|
return I18n.t("chat.reactions.multiple_users", {
|
||||||
emoji: this.reaction.emoji,
|
emoji: this.args.reaction.emoji,
|
||||||
username: usernames.pop(),
|
username: usernames.pop(),
|
||||||
commaSeparatedUsernames: this._joinUsernames(usernames),
|
commaSeparatedUsernames: this._joinUsernames(usernames),
|
||||||
});
|
});
|
||||||
|
|
|
@ -0,0 +1,26 @@
|
||||||
|
{{#if @message.firstMessageOfTheDayAt}}
|
||||||
|
<div
|
||||||
|
class={{concat-class
|
||||||
|
"chat-message-separator-date"
|
||||||
|
(if @message.newest "last-visit")
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="chat-message-separator__text-container"
|
||||||
|
{{chat/track-message-separator-date}}
|
||||||
|
>
|
||||||
|
<span class="chat-message-separator__text">
|
||||||
|
{{@message.firstMessageOfTheDayAt}}
|
||||||
|
|
||||||
|
{{#if @message.newest}}
|
||||||
|
-
|
||||||
|
{{i18n "chat.last_visit"}}
|
||||||
|
{{/if}}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="chat-message-separator__line-container">
|
||||||
|
<div class="chat-message-separator__line"></div>
|
||||||
|
</div>
|
||||||
|
{{/if}}
|
|
@ -0,0 +1,13 @@
|
||||||
|
{{#if (and @message.newest (not @message.firstMessageOfTheDayAt))}}
|
||||||
|
<div class="chat-message-separator-new">
|
||||||
|
<div class="chat-message-separator__text-container">
|
||||||
|
<span class="chat-message-separator__text">
|
||||||
|
{{i18n "chat.last_visit"}}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="chat-message-separator__line-container">
|
||||||
|
<div class="chat-message-separator__line"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{{/if}}
|
|
@ -1,15 +0,0 @@
|
||||||
{{#if this.message.newestMessage}}
|
|
||||||
<div class="chat-message-separator new-message">
|
|
||||||
<div class="divider"></div>
|
|
||||||
<span class="text">
|
|
||||||
{{i18n "chat.new_messages"}}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
{{else if this.message.firstMessageOfTheDayAt}}
|
|
||||||
<div class="chat-message-separator first-daily-message">
|
|
||||||
<div class="divider"></div>
|
|
||||||
<span class="text">
|
|
||||||
{{this.message.firstMessageOfTheDayAt}}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
{{/if}}
|
|
|
@ -1,5 +0,0 @@
|
||||||
import Component from "@ember/component";
|
|
||||||
|
|
||||||
export default Component.extend({
|
|
||||||
tagName: "",
|
|
||||||
});
|
|
|
@ -1,11 +1,11 @@
|
||||||
<div class="chat-message-text">
|
<div class="chat-message-text">
|
||||||
{{#if this.isCollapsible}}
|
{{#if this.isCollapsible}}
|
||||||
<ChatMessageCollapser @cooked={{this.cooked}} @uploads={{this.uploads}} />
|
<ChatMessageCollapser @cooked={{@cooked}} @uploads={{@uploads}} />
|
||||||
{{else}}
|
{{else}}
|
||||||
{{html-safe this.cooked}}
|
{{html-safe @cooked}}
|
||||||
{{/if}}
|
{{/if}}
|
||||||
|
|
||||||
{{#if this.edited}}
|
{{#if this.isEdited}}
|
||||||
<span class="chat-message-edited">({{i18n "chat.edited"}})</span>
|
<span class="chat-message-edited">({{i18n "chat.edited"}})</span>
|
||||||
{{/if}}
|
{{/if}}
|
||||||
|
|
||||||
|
|
|
@ -1,15 +1,12 @@
|
||||||
import Component from "@ember/component";
|
import Component from "@glimmer/component";
|
||||||
import { computed } from "@ember/object";
|
|
||||||
import { isCollapsible } from "discourse/plugins/chat/discourse/components/chat-message-collapser";
|
import { isCollapsible } from "discourse/plugins/chat/discourse/components/chat-message-collapser";
|
||||||
|
|
||||||
export default class ChatMessageText extends Component {
|
export default class ChatMessageText extends Component {
|
||||||
tagName = "";
|
get isEdited() {
|
||||||
cooked = null;
|
return this.args.edited ?? false;
|
||||||
uploads = null;
|
}
|
||||||
edited = false;
|
|
||||||
|
|
||||||
@computed("cooked", "uploads.[]")
|
|
||||||
get isCollapsible() {
|
get isCollapsible() {
|
||||||
return isCollapsible(this.cooked, this.uploads);
|
return isCollapsible(this.args.cooked, this.args.uploads);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
{{! template-lint-disable no-invalid-interactive }}
|
{{! template-lint-disable no-invalid-interactive }}
|
||||||
|
|
||||||
<ChatMessageSeparator @message={{@message}} />
|
<ChatMessageSeparatorDate @message={{@message}} />
|
||||||
|
<ChatMessageSeparatorNew @message={{@message}} />
|
||||||
|
|
||||||
{{#if
|
{{#if
|
||||||
(and
|
(and
|
||||||
|
@ -40,19 +41,23 @@
|
||||||
{{did-insert this.setMessageActionsAnchors}}
|
{{did-insert this.setMessageActionsAnchors}}
|
||||||
{{did-insert this.decorateCookedMessage}}
|
{{did-insert this.decorateCookedMessage}}
|
||||||
{{did-update this.decorateCookedMessage @message.id}}
|
{{did-update this.decorateCookedMessage @message.id}}
|
||||||
|
{{did-update this.decorateCookedMessage @message.version}}
|
||||||
{{on "touchmove" this.handleTouchMove passive=true}}
|
{{on "touchmove" this.handleTouchMove passive=true}}
|
||||||
{{on "touchstart" this.handleTouchStart passive=true}}
|
{{on "touchstart" this.handleTouchStart passive=true}}
|
||||||
{{on "touchend" this.handleTouchEnd passive=true}}
|
{{on "touchend" this.handleTouchEnd passive=true}}
|
||||||
{{on "mouseenter" (fn @onHoverMessage @message (hash desktopOnly=true))}}
|
{{on "mouseenter" (fn @onHoverMessage @message (hash desktopOnly=true))}}
|
||||||
{{on "mouseleave" (fn @onHoverMessage null (hash desktopOnly=true))}}
|
{{on "mouseleave" (fn @onHoverMessage null (hash desktopOnly=true))}}
|
||||||
{{chat/track-message-visibility}}
|
|
||||||
class={{concat-class
|
class={{concat-class
|
||||||
"chat-message-container"
|
"chat-message-container"
|
||||||
(if @isHovered "is-hovered")
|
(if @isHovered "is-hovered")
|
||||||
(if @selectingMessages "selecting-messages")
|
(if @selectingMessages "selecting-messages")
|
||||||
|
(if @message.highlighted "highlighted")
|
||||||
|
}}
|
||||||
|
data-id={{@message.id}}
|
||||||
|
{{chat/track-message
|
||||||
|
(fn @didShowMessage @message)
|
||||||
|
(fn @didHideMessage @message)
|
||||||
}}
|
}}
|
||||||
data-id={{or @message.id @message.stagedId}}
|
|
||||||
data-staged-id={{if @message.staged @message.stagedId}}
|
|
||||||
>
|
>
|
||||||
{{#if this.show}}
|
{{#if this.show}}
|
||||||
{{#if @selectingMessages}}
|
{{#if @selectingMessages}}
|
||||||
|
@ -85,35 +90,17 @@
|
||||||
class={{concat-class
|
class={{concat-class
|
||||||
"chat-message"
|
"chat-message"
|
||||||
(if @message.staged "chat-message-staged")
|
(if @message.staged "chat-message-staged")
|
||||||
(if @message.deleted_at "deleted")
|
(if @message.deletedAt "deleted")
|
||||||
(if @message.in_reply_to "is-reply")
|
(if (and @message.inReplyTo (not this.hideReplyToInfo)) "is-reply")
|
||||||
(if this.hideUserInfo "user-info-hidden")
|
(if this.hideUserInfo "user-info-hidden")
|
||||||
(if @message.error "errored")
|
(if @message.error "errored")
|
||||||
(if @message.bookmark "chat-message-bookmarked")
|
(if @message.bookmark "chat-message-bookmarked")
|
||||||
(if @isHovered "chat-message-selected")
|
(if @isHovered "chat-message-selected")
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{{#if @message.in_reply_to}}
|
{{#unless this.hideReplyToInfo}}
|
||||||
<div
|
<ChatMessageInReplyToIndicator @message={{@message}} />
|
||||||
role="button"
|
{{/unless}}
|
||||||
onclick={{action this.viewReplyOrThread}}
|
|
||||||
class="chat-reply is-direct-reply"
|
|
||||||
>
|
|
||||||
{{d-icon "share" title="chat.in_reply_to"}}
|
|
||||||
|
|
||||||
{{#if @message.in_reply_to.chat_webhook_event.emoji}}
|
|
||||||
<ChatEmojiAvatar
|
|
||||||
@emoji={{@message.in_reply_to.chat_webhook_event.emoji}}
|
|
||||||
/>
|
|
||||||
{{else}}
|
|
||||||
<ChatUserAvatar @user={{@message.in_reply_to.user}} />
|
|
||||||
{{/if}}
|
|
||||||
|
|
||||||
<span class="chat-reply__excerpt">
|
|
||||||
{{replace-emoji @message.in_reply_to.excerpt}}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
{{/if}}
|
|
||||||
|
|
||||||
{{#if this.hideUserInfo}}
|
{{#if this.hideUserInfo}}
|
||||||
<ChatMessageLeftGutter @message={{@message}} />
|
<ChatMessageLeftGutter @message={{@message}} />
|
||||||
|
@ -131,7 +118,7 @@
|
||||||
@uploads={{@message.uploads}}
|
@uploads={{@message.uploads}}
|
||||||
@edited={{@message.edited}}
|
@edited={{@message.edited}}
|
||||||
>
|
>
|
||||||
{{#if this.hasReactions}}
|
{{#if @message.reactions.length}}
|
||||||
<div class="chat-message-reaction-list">
|
<div class="chat-message-reaction-list">
|
||||||
{{#if this.reactionLabel}}
|
{{#if this.reactionLabel}}
|
||||||
<div class="reaction-users-list">
|
<div class="reaction-users-list">
|
||||||
|
@ -139,18 +126,13 @@
|
||||||
</div>
|
</div>
|
||||||
{{/if}}
|
{{/if}}
|
||||||
|
|
||||||
{{#each-in @message.reactions as |emoji reactionAttrs|}}
|
{{#each @message.reactions as |reaction|}}
|
||||||
<ChatMessageReaction
|
<ChatMessageReaction
|
||||||
@reaction={{hash
|
@reaction={{reaction}}
|
||||||
emoji=emoji
|
|
||||||
users=reactionAttrs.users
|
|
||||||
count=reactionAttrs.count
|
|
||||||
reacted=reactionAttrs.reacted
|
|
||||||
}}
|
|
||||||
@react={{this.react}}
|
@react={{this.react}}
|
||||||
@showUsersList={{true}}
|
@showTooltip={{true}}
|
||||||
/>
|
/>
|
||||||
{{/each-in}}
|
{{/each}}
|
||||||
|
|
||||||
{{#if @canInteractWithChat}}
|
{{#if @canInteractWithChat}}
|
||||||
{{#unless this.site.mobileView}}
|
{{#unless this.site.mobileView}}
|
||||||
|
@ -189,7 +171,7 @@
|
||||||
|
|
||||||
{{#if this.mentionWarning}}
|
{{#if this.mentionWarning}}
|
||||||
<div class="alert alert-info chat-message-mention-warning">
|
<div class="alert alert-info chat-message-mention-warning">
|
||||||
{{#if this.mentionWarning.invitationSent}}
|
{{#if this.mentionWarning.invitation_sent}}
|
||||||
{{d-icon "check"}}
|
{{d-icon "check"}}
|
||||||
<span>
|
<span>
|
||||||
{{i18n
|
{{i18n
|
||||||
|
|
|
@ -5,8 +5,7 @@ import Component from "@glimmer/component";
|
||||||
import I18n from "I18n";
|
import I18n from "I18n";
|
||||||
import getURL from "discourse-common/lib/get-url";
|
import getURL from "discourse-common/lib/get-url";
|
||||||
import optionalService from "discourse/lib/optional-service";
|
import optionalService from "discourse/lib/optional-service";
|
||||||
import { bind } from "discourse-common/utils/decorators";
|
import { action } from "@ember/object";
|
||||||
import EmberObject, { action } from "@ember/object";
|
|
||||||
import { ajax } from "discourse/lib/ajax";
|
import { ajax } from "discourse/lib/ajax";
|
||||||
import { cancel, schedule } from "@ember/runloop";
|
import { cancel, schedule } from "@ember/runloop";
|
||||||
import { clipboardCopy } from "discourse/lib/utilities";
|
import { clipboardCopy } from "discourse/lib/utilities";
|
||||||
|
@ -18,6 +17,7 @@ import showModal from "discourse/lib/show-modal";
|
||||||
import ChatMessageFlag from "discourse/plugins/chat/discourse/lib/chat-message-flag";
|
import ChatMessageFlag from "discourse/plugins/chat/discourse/lib/chat-message-flag";
|
||||||
import { tracked } from "@glimmer/tracking";
|
import { tracked } from "@glimmer/tracking";
|
||||||
import { getOwner } from "discourse-common/lib/get-owner";
|
import { getOwner } from "discourse-common/lib/get-owner";
|
||||||
|
import ChatMessageReaction from "discourse/plugins/chat/discourse/models/chat-message-reaction";
|
||||||
|
|
||||||
let _chatMessageDecorators = [];
|
let _chatMessageDecorators = [];
|
||||||
|
|
||||||
|
@ -50,37 +50,24 @@ export default class ChatMessage extends Component {
|
||||||
@optionalService adminTools;
|
@optionalService adminTools;
|
||||||
|
|
||||||
cachedFavoritesReactions = null;
|
cachedFavoritesReactions = null;
|
||||||
|
reacting = false;
|
||||||
_hasSubscribedToAppEvents = false;
|
|
||||||
_loadingReactions = [];
|
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
super(...arguments);
|
super(...arguments);
|
||||||
|
|
||||||
this.args.message.id
|
|
||||||
? this._subscribeToAppEvents()
|
|
||||||
: this._waitForIdToBePopulated();
|
|
||||||
|
|
||||||
if (this.args.message.bookmark) {
|
|
||||||
this.args.message.set(
|
|
||||||
"bookmark",
|
|
||||||
Bookmark.create(this.args.message.bookmark)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.cachedFavoritesReactions = this.chatEmojiReactionStore.favorites;
|
this.cachedFavoritesReactions = this.chatEmojiReactionStore.favorites;
|
||||||
}
|
}
|
||||||
|
|
||||||
get deletedAndCollapsed() {
|
get deletedAndCollapsed() {
|
||||||
return this.args.message?.get("deleted_at") && this.collapsed;
|
return this.args.message?.deletedAt && this.collapsed;
|
||||||
}
|
}
|
||||||
|
|
||||||
get hiddenAndCollapsed() {
|
get hiddenAndCollapsed() {
|
||||||
return this.args.message?.get("hidden") && this.collapsed;
|
return this.args.message?.hidden && this.collapsed;
|
||||||
}
|
}
|
||||||
|
|
||||||
get collapsed() {
|
get collapsed() {
|
||||||
return !this.args.message?.get("expanded");
|
return !this.args.message?.expanded;
|
||||||
}
|
}
|
||||||
|
|
||||||
@action
|
@action
|
||||||
|
@ -97,32 +84,9 @@ export default class ChatMessage extends Component {
|
||||||
|
|
||||||
@action
|
@action
|
||||||
teardownChatMessage() {
|
teardownChatMessage() {
|
||||||
if (this.args.message?.stagedId) {
|
|
||||||
this.appEvents.off(
|
|
||||||
`chat-message-staged-${this.args.message.stagedId}:id-populated`,
|
|
||||||
this,
|
|
||||||
"_subscribeToAppEvents"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.appEvents.off("chat:refresh-message", this, "_refreshedMessage");
|
|
||||||
|
|
||||||
this.appEvents.off(
|
|
||||||
`chat-message-${this.args.message.id}:reaction`,
|
|
||||||
this,
|
|
||||||
"_handleReactionMessage"
|
|
||||||
);
|
|
||||||
|
|
||||||
cancel(this._invitationSentTimer);
|
cancel(this._invitationSentTimer);
|
||||||
}
|
}
|
||||||
|
|
||||||
@bind
|
|
||||||
_refreshedMessage(message) {
|
|
||||||
if (message.id === this.args.message.id) {
|
|
||||||
this.decorateCookedMessage();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@action
|
@action
|
||||||
decorateCookedMessage() {
|
decorateCookedMessage() {
|
||||||
schedule("afterRender", () => {
|
schedule("afterRender", () => {
|
||||||
|
@ -131,45 +95,22 @@ export default class ChatMessage extends Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
_chatMessageDecorators.forEach((decorator) => {
|
_chatMessageDecorators.forEach((decorator) => {
|
||||||
decorator.call(this, this.messageContainer, this.args.chatChannel);
|
decorator.call(this, this.messageContainer, this.args.channel);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
get messageContainer() {
|
get messageContainer() {
|
||||||
const id = this.args.message?.id || this.args.message?.stagedId;
|
const id = this.args.message?.id;
|
||||||
return (
|
if (id) {
|
||||||
id && document.querySelector(`.chat-message-container[data-id='${id}']`)
|
return document.querySelector(`.chat-message-container[data-id='${id}']`);
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
_subscribeToAppEvents() {
|
|
||||||
if (!this.args.message.id || this._hasSubscribedToAppEvents) {
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
this.appEvents.on("chat:refresh-message", this, "_refreshedMessage");
|
|
||||||
|
|
||||||
this.appEvents.on(
|
|
||||||
`chat-message-${this.args.message.id}:reaction`,
|
|
||||||
this,
|
|
||||||
"_handleReactionMessage"
|
|
||||||
);
|
|
||||||
this._hasSubscribedToAppEvents = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
_waitForIdToBePopulated() {
|
|
||||||
this.appEvents.on(
|
|
||||||
`chat-message-staged-${this.args.message.stagedId}:id-populated`,
|
|
||||||
this,
|
|
||||||
"_subscribeToAppEvents"
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
get showActions() {
|
get showActions() {
|
||||||
return (
|
return (
|
||||||
this.args.canInteractWithChat &&
|
this.args.canInteractWithChat &&
|
||||||
!this.args.message?.get("staged") &&
|
!this.args.message?.staged &&
|
||||||
this.args.isHovered
|
this.args.isHovered
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -270,17 +211,16 @@ export default class ChatMessage extends Component {
|
||||||
|
|
||||||
get hasThread() {
|
get hasThread() {
|
||||||
return (
|
return (
|
||||||
this.args.chatChannel?.get("threading_enabled") &&
|
this.args.channel?.get("threading_enabled") && this.args.message?.threadId
|
||||||
this.args.message?.get("thread_id")
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
get show() {
|
get show() {
|
||||||
return (
|
return (
|
||||||
!this.args.message?.get("deleted_at") ||
|
!this.args.message?.deletedAt ||
|
||||||
this.currentUser.id === this.args.message?.get("user.id") ||
|
this.currentUser.id === this.args.message?.user?.id ||
|
||||||
this.currentUser.staff ||
|
this.currentUser.staff ||
|
||||||
this.args.details?.can_moderate
|
this.args.channel?.canModerate
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -331,83 +271,97 @@ export default class ChatMessage extends Component {
|
||||||
|
|
||||||
get hideUserInfo() {
|
get hideUserInfo() {
|
||||||
return (
|
return (
|
||||||
this.args.message?.get("hideUserInfo") &&
|
!this.args.message?.chatWebhookEvent &&
|
||||||
!this.args.message?.get("chat_webhook_event")
|
!this.args.message?.inReplyTo &&
|
||||||
|
!this.args.message?.previousMessage?.deletedAt &&
|
||||||
|
Math.abs(
|
||||||
|
new Date(this.args.message?.createdAt) -
|
||||||
|
new Date(this.args.message?.createdAt)
|
||||||
|
) < 300000 && // If the time between messages is over 5 minutes, break.
|
||||||
|
this.args.message?.user?.id ===
|
||||||
|
this.args.message?.previousMessage?.user?.id
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
get hideReplyToInfo() {
|
||||||
|
return (
|
||||||
|
this.args.message?.inReplyTo?.id ===
|
||||||
|
this.args.message?.previousMessage?.id
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
get showEditButton() {
|
get showEditButton() {
|
||||||
return (
|
return (
|
||||||
!this.args.message?.get("deleted_at") &&
|
!this.args.message?.deletedAt &&
|
||||||
this.currentUser?.id === this.args.message?.get("user.id") &&
|
this.currentUser?.id === this.args.message?.user?.id &&
|
||||||
this.args.chatChannel?.canModifyMessages?.(this.currentUser)
|
this.args.channel?.canModifyMessages?.(this.currentUser)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
get canFlagMessage() {
|
get canFlagMessage() {
|
||||||
return (
|
return (
|
||||||
this.currentUser?.id !== this.args.message?.get("user.id") &&
|
this.currentUser?.id !== this.args.message?.user?.id &&
|
||||||
this.args.message?.get("user_flag_status") === undefined &&
|
!this.args.channel?.isDirectMessageChannel &&
|
||||||
this.args.details?.can_flag &&
|
this.args.message?.userFlagStatus === undefined &&
|
||||||
!this.args.message?.get("chat_webhook_event") &&
|
this.args.channel?.canFlag &&
|
||||||
!this.args.message?.get("deleted_at")
|
!this.args.message?.chatWebhookEvent &&
|
||||||
|
!this.args.message?.deletedAt
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
get canManageDeletion() {
|
get canManageDeletion() {
|
||||||
return this.currentUser?.id === this.args.message.get("user.id")
|
return this.currentUser?.id === this.args.message.user.id
|
||||||
? this.args.details?.can_delete_self
|
? this.args.channel?.canDeleteSelf
|
||||||
: this.args.details?.can_delete_others;
|
: this.args.channel?.canDeleteOthers;
|
||||||
}
|
}
|
||||||
|
|
||||||
get canReply() {
|
get canReply() {
|
||||||
return (
|
return (
|
||||||
!this.args.message?.get("deleted_at") &&
|
!this.args.message?.deletedAt &&
|
||||||
this.args.chatChannel?.canModifyMessages?.(this.currentUser)
|
this.args.channel?.canModifyMessages?.(this.currentUser)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
get canReact() {
|
get canReact() {
|
||||||
return (
|
return (
|
||||||
!this.args.message?.get("deleted_at") &&
|
!this.args.message?.deletedAt &&
|
||||||
this.args.chatChannel?.canModifyMessages?.(this.currentUser)
|
this.args.channel?.canModifyMessages?.(this.currentUser)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
get showDeleteButton() {
|
get showDeleteButton() {
|
||||||
return (
|
return (
|
||||||
this.canManageDeletion &&
|
this.canManageDeletion &&
|
||||||
!this.args.message?.get("deleted_at") &&
|
!this.args.message?.deletedAt &&
|
||||||
this.args.chatChannel?.canModifyMessages?.(this.currentUser)
|
this.args.channel?.canModifyMessages?.(this.currentUser)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
get showRestoreButton() {
|
get showRestoreButton() {
|
||||||
return (
|
return (
|
||||||
this.canManageDeletion &&
|
this.canManageDeletion &&
|
||||||
this.args.message?.get("deleted_at") &&
|
this.args.message?.deletedAt &&
|
||||||
this.args.chatChannel?.canModifyMessages?.(this.currentUser)
|
this.args.channel?.canModifyMessages?.(this.currentUser)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
get showBookmarkButton() {
|
get showBookmarkButton() {
|
||||||
return this.args.chatChannel?.canModifyMessages?.(this.currentUser);
|
return this.args.channel?.canModifyMessages?.(this.currentUser);
|
||||||
}
|
}
|
||||||
|
|
||||||
get showRebakeButton() {
|
get showRebakeButton() {
|
||||||
return (
|
return (
|
||||||
this.currentUser?.staff &&
|
this.currentUser?.staff &&
|
||||||
this.args.chatChannel?.canModifyMessages?.(this.currentUser)
|
this.args.channel?.canModifyMessages?.(this.currentUser)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
get hasReactions() {
|
get hasReactions() {
|
||||||
return Object.values(this.args.message.get("reactions")).some(
|
return Object.values(this.args.message.reactions).some((r) => r.count > 0);
|
||||||
(r) => r.count > 0
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
get mentionWarning() {
|
get mentionWarning() {
|
||||||
return this.args.message.get("mentionWarning");
|
return this.args.message.mentionWarning;
|
||||||
}
|
}
|
||||||
|
|
||||||
get mentionedCannotSeeText() {
|
get mentionedCannotSeeText() {
|
||||||
|
@ -464,13 +418,13 @@ export default class ChatMessage extends Component {
|
||||||
inviteMentioned() {
|
inviteMentioned() {
|
||||||
const userIds = this.mentionWarning.without_membership.mapBy("id");
|
const userIds = this.mentionWarning.without_membership.mapBy("id");
|
||||||
|
|
||||||
ajax(`/chat/${this.args.message.chat_channel_id}/invite`, {
|
ajax(`/chat/${this.args.message.channelId}/invite`, {
|
||||||
method: "PUT",
|
method: "PUT",
|
||||||
data: { user_ids: userIds, chat_message_id: this.args.message.id },
|
data: { user_ids: userIds, chat_message_id: this.args.message.id },
|
||||||
}).then(() => {
|
}).then(() => {
|
||||||
this.args.message.set("mentionWarning.invitationSent", true);
|
this.args.message.mentionWarning.set("invitationSent", true);
|
||||||
this._invitationSentTimer = discourseLater(() => {
|
this._invitationSentTimer = discourseLater(() => {
|
||||||
this.args.message.set("mentionWarning", null);
|
this.dismissMentionWarning();
|
||||||
}, 3000);
|
}, 3000);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -479,7 +433,7 @@ export default class ChatMessage extends Component {
|
||||||
|
|
||||||
@action
|
@action
|
||||||
dismissMentionWarning() {
|
dismissMentionWarning() {
|
||||||
this.args.message.set("mentionWarning", null);
|
this.args.message.mentionWarning = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@action
|
@action
|
||||||
|
@ -517,27 +471,17 @@ export default class ChatMessage extends Component {
|
||||||
this.react(emoji, REACTIONS.add);
|
this.react(emoji, REACTIONS.add);
|
||||||
}
|
}
|
||||||
|
|
||||||
@bind
|
|
||||||
_handleReactionMessage(busData) {
|
|
||||||
const loadingReactionIndex = this._loadingReactions.indexOf(busData.emoji);
|
|
||||||
if (loadingReactionIndex > -1) {
|
|
||||||
return this._loadingReactions.splice(loadingReactionIndex, 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
this._updateReactionsList(busData.emoji, busData.action, busData.user);
|
|
||||||
this.args.afterReactionAdded();
|
|
||||||
}
|
|
||||||
|
|
||||||
get capabilities() {
|
get capabilities() {
|
||||||
return getOwner(this).lookup("capabilities:main");
|
return getOwner(this).lookup("capabilities:main");
|
||||||
}
|
}
|
||||||
|
|
||||||
@action
|
@action
|
||||||
react(emoji, reactAction) {
|
react(emoji, reactAction) {
|
||||||
if (
|
if (!this.args.canInteractWithChat) {
|
||||||
!this.args.canInteractWithChat ||
|
return;
|
||||||
this._loadingReactions.includes(emoji)
|
}
|
||||||
) {
|
|
||||||
|
if (this.reacting) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -549,71 +493,21 @@ export default class ChatMessage extends Component {
|
||||||
this.args.onHoverMessage(null);
|
this.args.onHoverMessage(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
this._loadingReactions.push(emoji);
|
|
||||||
this._updateReactionsList(emoji, reactAction, this.currentUser);
|
|
||||||
|
|
||||||
if (reactAction === REACTIONS.add) {
|
if (reactAction === REACTIONS.add) {
|
||||||
this.chatEmojiReactionStore.track(`:${emoji}:`);
|
this.chatEmojiReactionStore.track(`:${emoji}:`);
|
||||||
}
|
}
|
||||||
|
|
||||||
return this._publishReaction(emoji, reactAction).then(() => {
|
this.reacting = true;
|
||||||
// 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) {
|
this.args.message.react(
|
||||||
const selfReacted = this.currentUser.id === user.id;
|
emoji,
|
||||||
if (this.args.message.reactions[emoji]) {
|
reactAction,
|
||||||
if (
|
this.currentUser,
|
||||||
selfReacted &&
|
this.currentUser.id
|
||||||
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(
|
return ajax(
|
||||||
`/chat/${this.args.message.chat_channel_id}/react/${this.args.message.id}`,
|
`/chat/${this.args.message.channelId}/react/${this.args.message.id}`,
|
||||||
{
|
{
|
||||||
type: "PUT",
|
type: "PUT",
|
||||||
data: {
|
data: {
|
||||||
|
@ -621,10 +515,19 @@ export default class ChatMessage extends Component {
|
||||||
emoji,
|
emoji,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
).catch((errResult) => {
|
)
|
||||||
popupAjaxError(errResult);
|
.catch((errResult) => {
|
||||||
this._updateReactionsList(emoji, REACTIONS.remove, this.currentUser);
|
popupAjaxError(errResult);
|
||||||
});
|
this.args.message.react(
|
||||||
|
emoji,
|
||||||
|
REACTIONS.remove,
|
||||||
|
this.currentUser,
|
||||||
|
this.currentUser.id
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
this.reacting = false;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO(roman): For backwards-compatibility.
|
// TODO(roman): For backwards-compatibility.
|
||||||
|
@ -651,17 +554,6 @@ export default class ChatMessage extends Component {
|
||||||
this.args.setReplyTo(this.args.message.id);
|
this.args.setReplyTo(this.args.message.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
viewReplyOrThread() {
|
|
||||||
if (this.hasThread) {
|
|
||||||
this.router.transitionTo(
|
|
||||||
"chat.channel.thread",
|
|
||||||
this.args.message.thread_id
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
this.args.replyMessageClicked(this.args.message.in_reply_to);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@action
|
@action
|
||||||
edit() {
|
edit() {
|
||||||
this.args.editButtonClicked(this.args.message.id);
|
this.args.editButtonClicked(this.args.message.id);
|
||||||
|
@ -673,12 +565,11 @@ export default class ChatMessage extends Component {
|
||||||
requirejs.entries["discourse/lib/flag-targets/flag"];
|
requirejs.entries["discourse/lib/flag-targets/flag"];
|
||||||
|
|
||||||
if (targetFlagSupported) {
|
if (targetFlagSupported) {
|
||||||
const model = EmberObject.create(this.args.message);
|
const model = this.args.message;
|
||||||
model.set("username", model.get("user.username"));
|
model.username = model.user?.username;
|
||||||
model.set("user_id", model.get("user.id"));
|
model.user_id = model.user?.id;
|
||||||
let controller = showModal("flag", { model });
|
let controller = showModal("flag", { model });
|
||||||
|
controller.set("flagTarget", new ChatMessageFlag());
|
||||||
controller.setProperties({ flagTarget: new ChatMessageFlag() });
|
|
||||||
} else {
|
} else {
|
||||||
this._legacyFlag();
|
this._legacyFlag();
|
||||||
}
|
}
|
||||||
|
@ -686,13 +577,13 @@ export default class ChatMessage extends Component {
|
||||||
|
|
||||||
@action
|
@action
|
||||||
expand() {
|
expand() {
|
||||||
this.args.message.set("expanded", true);
|
this.args.message.expanded = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@action
|
@action
|
||||||
restore() {
|
restore() {
|
||||||
return ajax(
|
return ajax(
|
||||||
`/chat/${this.args.message.chat_channel_id}/restore/${this.args.message.id}`,
|
`/chat/${this.args.message.channelId}/restore/${this.args.message.id}`,
|
||||||
{
|
{
|
||||||
type: "PUT",
|
type: "PUT",
|
||||||
}
|
}
|
||||||
|
@ -701,10 +592,7 @@ export default class ChatMessage extends Component {
|
||||||
|
|
||||||
@action
|
@action
|
||||||
openThread() {
|
openThread() {
|
||||||
this.router.transitionTo(
|
this.router.transitionTo("chat.channel.thread", this.args.message.threadId);
|
||||||
"chat.channel.thread",
|
|
||||||
this.args.message.thread_id
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@action
|
@action
|
||||||
|
@ -719,7 +607,7 @@ export default class ChatMessage extends Component {
|
||||||
{
|
{
|
||||||
onAfterSave: (savedData) => {
|
onAfterSave: (savedData) => {
|
||||||
const bookmark = Bookmark.create(savedData);
|
const bookmark = Bookmark.create(savedData);
|
||||||
this.args.message.set("bookmark", bookmark);
|
this.args.message.bookmark = bookmark;
|
||||||
this.appEvents.trigger(
|
this.appEvents.trigger(
|
||||||
"bookmarks:changed",
|
"bookmarks:changed",
|
||||||
savedData,
|
savedData,
|
||||||
|
@ -727,7 +615,7 @@ export default class ChatMessage extends Component {
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
onAfterDelete: () => {
|
onAfterDelete: () => {
|
||||||
this.args.message.set("bookmark", null);
|
this.args.message.bookmark = null;
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
@ -736,7 +624,7 @@ export default class ChatMessage extends Component {
|
||||||
@action
|
@action
|
||||||
rebakeMessage() {
|
rebakeMessage() {
|
||||||
return ajax(
|
return ajax(
|
||||||
`/chat/${this.args.message.chat_channel_id}/${this.args.message.id}/rebake`,
|
`/chat/${this.args.message.channelId}/${this.args.message.id}/rebake`,
|
||||||
{
|
{
|
||||||
type: "PUT",
|
type: "PUT",
|
||||||
}
|
}
|
||||||
|
@ -746,7 +634,7 @@ export default class ChatMessage extends Component {
|
||||||
@action
|
@action
|
||||||
deleteMessage() {
|
deleteMessage() {
|
||||||
return ajax(
|
return ajax(
|
||||||
`/chat/${this.args.message.chat_channel_id}/${this.args.message.id}`,
|
`/chat/${this.args.message.channelId}/${this.args.message.id}`,
|
||||||
{
|
{
|
||||||
type: "DELETE",
|
type: "DELETE",
|
||||||
}
|
}
|
||||||
|
@ -755,7 +643,7 @@ export default class ChatMessage extends Component {
|
||||||
|
|
||||||
@action
|
@action
|
||||||
selectMessage() {
|
selectMessage() {
|
||||||
this.args.message.set("selected", true);
|
this.args.message.selected = true;
|
||||||
this.args.onStartSelectingMessages(this.args.message);
|
this.args.onStartSelectingMessages(this.args.message);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -780,7 +668,7 @@ export default class ChatMessage extends Component {
|
||||||
|
|
||||||
const { protocol, host } = window.location;
|
const { protocol, host } = window.location;
|
||||||
let url = getURL(
|
let url = getURL(
|
||||||
`/chat/c/-/${this.args.message.chat_channel_id}/${this.args.message.id}`
|
`/chat/c/-/${this.args.message.channelId}/${this.args.message.id}`
|
||||||
);
|
);
|
||||||
url = url.indexOf("/") === 0 ? protocol + "//" + host + url : url;
|
url = url.indexOf("/") === 0 ? protocol + "//" + host + url : url;
|
||||||
clipboardCopy(url);
|
clipboardCopy(url);
|
||||||
|
@ -793,25 +681,22 @@ export default class ChatMessage extends Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
get emojiReactions() {
|
get emojiReactions() {
|
||||||
const favorites = this.cachedFavoritesReactions;
|
let favorites = this.cachedFavoritesReactions;
|
||||||
|
|
||||||
// may be a {} if no defaults defined in some production builds
|
// may be a {} if no defaults defined in some production builds
|
||||||
if (!favorites || !favorites.slice) {
|
if (!favorites || !favorites.slice) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
const userReactions = Object.keys(this.args.message.reactions || {}).filter(
|
|
||||||
(key) => {
|
|
||||||
return this.args.message.reactions[key].reacted;
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
return favorites.slice(0, 3).map((emoji) => {
|
return favorites.slice(0, 3).map((emoji) => {
|
||||||
if (userReactions.includes(emoji)) {
|
return (
|
||||||
return { emoji, reacted: true };
|
this.args.message.reactions.find(
|
||||||
} else {
|
(reaction) => reaction.emoji === emoji
|
||||||
return { emoji, reacted: false };
|
) ||
|
||||||
}
|
ChatMessageReaction.create({
|
||||||
|
emoji,
|
||||||
|
})
|
||||||
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{{#if this.show}}
|
{{#if this.show}}
|
||||||
<div class="chat-retention-reminder">
|
<div class="chat-retention-reminder">
|
||||||
<ChatRetentionReminderText @channel={{this.chatChannel}} />
|
<ChatRetentionReminderText @channel={{@channel}} />
|
||||||
<DButton
|
<DButton
|
||||||
@class="btn-flat dismiss-btn"
|
@class="btn-flat dismiss-btn"
|
||||||
@action={{this.dismiss}}
|
@action={{this.dismiss}}
|
||||||
|
|
|
@ -1,39 +1,34 @@
|
||||||
import Component from "@ember/component";
|
import Component from "@glimmer/component";
|
||||||
import discourseComputed from "discourse-common/utils/decorators";
|
|
||||||
import { action } from "@ember/object";
|
import { action } from "@ember/object";
|
||||||
import { ajax } from "discourse/lib/ajax";
|
import { ajax } from "discourse/lib/ajax";
|
||||||
import { popupAjaxError } from "discourse/lib/ajax-error";
|
import { popupAjaxError } from "discourse/lib/ajax-error";
|
||||||
|
import { inject as service } from "@ember/service";
|
||||||
|
|
||||||
export default Component.extend({
|
export default class ChatRetentionReminder extends Component {
|
||||||
tagName: "",
|
@service currentUser;
|
||||||
loading: false,
|
|
||||||
|
|
||||||
@discourseComputed(
|
get show() {
|
||||||
"chatChannel.chatable_type",
|
|
||||||
"currentUser.{needs_dm_retention_reminder,needs_channel_retention_reminder}"
|
|
||||||
)
|
|
||||||
show() {
|
|
||||||
return (
|
return (
|
||||||
!this.chatChannel.isDraft &&
|
!this.args.channel?.isDraft &&
|
||||||
((this.chatChannel.isDirectMessageChannel &&
|
((this.args.channel?.isDirectMessageChannel &&
|
||||||
this.currentUser.needs_dm_retention_reminder) ||
|
this.currentUser?.get("needs_dm_retention_reminder")) ||
|
||||||
(this.chatChannel.isCategoryChannel &&
|
(this.args.channel?.isCategoryChannel &&
|
||||||
this.currentUser.needs_channel_retention_reminder))
|
this.currentUser?.get("needs_channel_retention_reminder")))
|
||||||
);
|
);
|
||||||
},
|
}
|
||||||
|
|
||||||
@action
|
@action
|
||||||
dismiss() {
|
dismiss() {
|
||||||
return ajax("/chat/dismiss-retention-reminder", {
|
return ajax("/chat/dismiss-retention-reminder", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
data: { chatable_type: this.chatChannel.chatable_type },
|
data: { chatable_type: this.args.channel.chatableType },
|
||||||
})
|
})
|
||||||
.then(() => {
|
.then(() => {
|
||||||
const field = this.chatChannel.isDirectMessageChannel
|
const field = this.args.channel.isDirectMessageChannel
|
||||||
? "needs_dm_retention_reminder"
|
? "needs_dm_retention_reminder"
|
||||||
: "needs_channel_retention_reminder";
|
: "needs_channel_retention_reminder";
|
||||||
this.currentUser.set(field, false);
|
this.currentUser.set(field, false);
|
||||||
})
|
})
|
||||||
.catch(popupAjaxError);
|
.catch(popupAjaxError);
|
||||||
},
|
}
|
||||||
});
|
}
|
||||||
|
|
|
@ -0,0 +1,23 @@
|
||||||
|
<div class="scroll-stick-wrap">
|
||||||
|
<DButton
|
||||||
|
class={{concat-class
|
||||||
|
"btn-flat"
|
||||||
|
"chat-scroll-to-bottom"
|
||||||
|
(if
|
||||||
|
(or (not @isAlmostDocked) @hasNewMessages @channel.canLoadMoreFuture)
|
||||||
|
"visible"
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
@action={{@scrollToBottom}}
|
||||||
|
>
|
||||||
|
<span class="chat-scroll-to-bottom__arrow">
|
||||||
|
{{d-icon "arrow-down"}}
|
||||||
|
|
||||||
|
{{#if @hasNewMessages}}
|
||||||
|
<span class="chat-scroll-to-bottom__text">
|
||||||
|
{{i18n "chat.scroll_to_new_messages"}}
|
||||||
|
</span>
|
||||||
|
{{/if}}
|
||||||
|
</span>
|
||||||
|
</DButton>
|
||||||
|
</div>
|
|
@ -19,16 +19,17 @@ export default class AdminCustomizeColorsShowController extends Component {
|
||||||
chatCopySuccess = false;
|
chatCopySuccess = false;
|
||||||
showChatCopySuccess = false;
|
showChatCopySuccess = false;
|
||||||
cancelSelecting = null;
|
cancelSelecting = null;
|
||||||
canModerate = false;
|
|
||||||
|
|
||||||
@computed("selectedMessageIds.length")
|
@computed("selectedMessageIds.length")
|
||||||
get anyMessagesSelected() {
|
get anyMessagesSelected() {
|
||||||
return this.selectedMessageIds.length > 0;
|
return this.selectedMessageIds.length > 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@computed("chatChannel.isDirectMessageChannel", "canModerate")
|
@computed("chatChannel.isDirectMessageChannel", "chatChannel.canModerate")
|
||||||
get showMoveMessageButton() {
|
get showMoveMessageButton() {
|
||||||
return !this.chatChannel.isDirectMessageChannel && this.canModerate;
|
return (
|
||||||
|
!this.chatChannel.isDirectMessageChannel && this.chatChannel.canModerate
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@bind
|
@bind
|
||||||
|
|
|
@ -1,13 +1,31 @@
|
||||||
<div class="chat-skeleton -animation">
|
<div
|
||||||
{{#each this.placeholders as |rows|}}
|
class="chat-skeleton -animation"
|
||||||
|
{{did-insert @onInsert}}
|
||||||
|
{{will-destroy @onDestroy}}
|
||||||
|
>
|
||||||
|
{{#each this.placeholders as |placeholder|}}
|
||||||
<div class="chat-skeleton__body">
|
<div class="chat-skeleton__body">
|
||||||
<div class="chat-skeleton__message">
|
<div class="chat-skeleton__message">
|
||||||
<div class="chat-skeleton__message-avatar"></div>
|
<div class="chat-skeleton__message-avatar"></div>
|
||||||
<div class="chat-skeleton__message-poster"></div>
|
<div class="chat-skeleton__message-poster"></div>
|
||||||
<div class="chat-skeleton__message-content">
|
<div class="chat-skeleton__message-content">
|
||||||
{{#each rows as |row|}}
|
{{#if placeholder.image}}
|
||||||
<div class="chat-skeleton__message-msg" style={{row}}></div>
|
<div class="chat-skeleton__message-img"></div>
|
||||||
{{/each}}
|
{{/if}}
|
||||||
|
|
||||||
|
<div class="chat-skeleton__message-text">
|
||||||
|
{{#each placeholder.rows as |row|}}
|
||||||
|
<div class="chat-skeleton__message-msg" style={{row}}></div>
|
||||||
|
{{/each}}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{#if placeholder.reactions}}
|
||||||
|
<div class="chat-skeleton__message-reactions">
|
||||||
|
{{#each placeholder.reactions}}
|
||||||
|
<div class="chat-skeleton__message-reaction"></div>
|
||||||
|
{{/each}}
|
||||||
|
</div>
|
||||||
|
{{/if}}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -4,9 +4,13 @@ import { htmlSafe } from "@ember/template";
|
||||||
export default class ChatSkeleton extends Component {
|
export default class ChatSkeleton extends Component {
|
||||||
get placeholders() {
|
get placeholders() {
|
||||||
return Array.from({ length: 15 }, () => {
|
return Array.from({ length: 15 }, () => {
|
||||||
return Array.from({ length: this.#randomIntFromInterval(1, 5) }, () => {
|
return {
|
||||||
return htmlSafe(`width: ${this.#randomIntFromInterval(20, 95)}%`);
|
image: this.#randomIntFromInterval(1, 10) === 5,
|
||||||
});
|
rows: Array.from({ length: this.#randomIntFromInterval(1, 5) }, () => {
|
||||||
|
return htmlSafe(`width: ${this.#randomIntFromInterval(20, 95)}%`);
|
||||||
|
}),
|
||||||
|
reactions: Array.from({ length: this.#randomIntFromInterval(0, 3) }),
|
||||||
|
};
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
{{#if this.chat.activeChannel}}
|
{{#if this.chat.activeChannel}}
|
||||||
<ChatLivePane
|
<ChatLivePane
|
||||||
@chatChannel={{this.chat.activeChannel}}
|
@channel={{this.chat.activeChannel}}
|
||||||
@onBackClick={{action "navigateToIndex"}}
|
|
||||||
@targetMessageId={{readonly @targetMessageId}}
|
@targetMessageId={{readonly @targetMessageId}}
|
||||||
/>
|
/>
|
||||||
{{/if}}
|
{{/if}}
|
|
@ -1,79 +1,6 @@
|
||||||
import Component from "@ember/component";
|
import Component from "@glimmer/component";
|
||||||
import { bind } from "discourse-common/utils/decorators";
|
|
||||||
import { action } from "@ember/object";
|
|
||||||
import { inject as service } from "@ember/service";
|
import { inject as service } from "@ember/service";
|
||||||
|
|
||||||
export default Component.extend({
|
export default class FullPageChat extends Component {
|
||||||
tagName: "",
|
@service chat;
|
||||||
router: service(),
|
}
|
||||||
chat: service(),
|
|
||||||
|
|
||||||
init() {
|
|
||||||
this._super(...arguments);
|
|
||||||
},
|
|
||||||
|
|
||||||
didInsertElement() {
|
|
||||||
this._super(...arguments);
|
|
||||||
|
|
||||||
this._scrollSidebarToBottom();
|
|
||||||
document.addEventListener("keydown", this._autoFocusChatComposer);
|
|
||||||
},
|
|
||||||
|
|
||||||
willDestroyElement() {
|
|
||||||
this._super(...arguments);
|
|
||||||
|
|
||||||
document.removeEventListener("keydown", this._autoFocusChatComposer);
|
|
||||||
},
|
|
||||||
|
|
||||||
@bind
|
|
||||||
_autoFocusChatComposer(event) {
|
|
||||||
if (
|
|
||||||
!event.key ||
|
|
||||||
// Handles things like Enter, Tab, Shift
|
|
||||||
event.key.length > 1 ||
|
|
||||||
// Don't need to focus if the user is beginning a shortcut.
|
|
||||||
event.metaKey ||
|
|
||||||
event.ctrlKey ||
|
|
||||||
// Space's key comes through as ' ' so it's not covered by event.key
|
|
||||||
event.code === "Space" ||
|
|
||||||
// ? is used for the keyboard shortcut modal
|
|
||||||
event.key === "?"
|
|
||||||
) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
|
||||||
!event.target ||
|
|
||||||
/^(INPUT|TEXTAREA|SELECT)$/.test(event.target.tagName)
|
|
||||||
) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
event.preventDefault();
|
|
||||||
event.stopPropagation();
|
|
||||||
|
|
||||||
const composer = document.querySelector(".chat-composer-input");
|
|
||||||
if (composer && !this.chat.activeChannel.isDraft) {
|
|
||||||
this.appEvents.trigger("chat:insert-text", event.key);
|
|
||||||
composer.focus();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
_scrollSidebarToBottom() {
|
|
||||||
if (!this.teamsSidebarOn) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const sidebarScroll = document.querySelector(
|
|
||||||
".sidebar-container .scroll-wrapper"
|
|
||||||
);
|
|
||||||
if (sidebarScroll) {
|
|
||||||
sidebarScroll.scrollTop = sidebarScroll.scrollHeight;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
@action
|
|
||||||
navigateToIndex() {
|
|
||||||
this.router.transitionTo("chat.index");
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
|
@ -1,10 +1,11 @@
|
||||||
import Controller from "@ember/controller";
|
import Controller from "@ember/controller";
|
||||||
import { inject as service } from "@ember/service";
|
import { inject as service } from "@ember/service";
|
||||||
|
import { tracked } from "@glimmer/tracking";
|
||||||
|
|
||||||
export default class ChatChannelController extends Controller {
|
export default class ChatChannelController extends Controller {
|
||||||
@service chat;
|
@service chat;
|
||||||
|
|
||||||
targetMessageId = null;
|
@tracked targetMessageId = null;
|
||||||
|
|
||||||
// Backwards-compatibility
|
// Backwards-compatibility
|
||||||
queryParams = ["messageId"];
|
queryParams = ["messageId"];
|
||||||
|
|
|
@ -36,6 +36,7 @@ export default class CreateChannelController extends Controller.extend(
|
||||||
categoryPermissionsHint = null;
|
categoryPermissionsHint = null;
|
||||||
autoJoinUsers = null;
|
autoJoinUsers = null;
|
||||||
autoJoinWarning = "";
|
autoJoinWarning = "";
|
||||||
|
loadingPermissionHint = false;
|
||||||
|
|
||||||
@notEmpty("category") categorySelected;
|
@notEmpty("category") categorySelected;
|
||||||
@gt("siteSettings.max_chat_auto_joined_users", 0) autoJoinAvailable;
|
@gt("siteSettings.max_chat_auto_joined_users", 0) autoJoinAvailable;
|
||||||
|
@ -153,6 +154,8 @@ export default class CreateChannelController extends Controller.extend(
|
||||||
if (category) {
|
if (category) {
|
||||||
const fullSlug = this._buildCategorySlug(category);
|
const fullSlug = this._buildCategorySlug(category);
|
||||||
|
|
||||||
|
this.set("loadingPermissionHint", true);
|
||||||
|
|
||||||
return this.chatApi
|
return this.chatApi
|
||||||
.categoryPermissions(category.id)
|
.categoryPermissions(category.id)
|
||||||
.then((catPermissions) => {
|
.then((catPermissions) => {
|
||||||
|
@ -194,6 +197,9 @@ export default class CreateChannelController extends Controller.extend(
|
||||||
}
|
}
|
||||||
|
|
||||||
this.set("categoryPermissionsHint", htmlSafe(hint));
|
this.set("categoryPermissionsHint", htmlSafe(hint));
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
this.set("loadingPermissionHint", false);
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
this.set("categoryPermissionsHint", DEFAULT_HINT);
|
this.set("categoryPermissionsHint", DEFAULT_HINT);
|
||||||
|
|
|
@ -7,8 +7,8 @@ import User from "discourse/models/user";
|
||||||
registerUnbound("format-chat-date", function (message, mode) {
|
registerUnbound("format-chat-date", function (message, mode) {
|
||||||
const currentUser = User.current();
|
const currentUser = User.current();
|
||||||
const tz = currentUser ? currentUser.user_option.timezone : moment.tz.guess();
|
const tz = currentUser ? currentUser.user_option.timezone : moment.tz.guess();
|
||||||
const date = moment(new Date(message.created_at), tz);
|
const date = moment(new Date(message.createdAt), tz);
|
||||||
const url = getURL(`/chat/c/-/${message.chat_channel_id}/${message.id}`);
|
const url = getURL(`/chat/c/-/${message.channelId}/${message.id}`);
|
||||||
const title = date.format(I18n.t("dates.long_with_year"));
|
const title = date.format(I18n.t("dates.long_with_year"));
|
||||||
|
|
||||||
const display =
|
const display =
|
||||||
|
|
|
@ -0,0 +1,32 @@
|
||||||
|
import ChatMessage from "discourse/plugins/chat/discourse/models/chat-message";
|
||||||
|
import { generateCookFunction } from "discourse/lib/text";
|
||||||
|
import simpleCategoryHashMentionTransform from "discourse/plugins/chat/discourse/lib/simple-category-hash-mention-transform";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: "chat-cook-function",
|
||||||
|
|
||||||
|
before: "chat-setup",
|
||||||
|
|
||||||
|
initialize(container) {
|
||||||
|
const site = container.lookup("service:site");
|
||||||
|
|
||||||
|
const markdownOptions = {
|
||||||
|
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,
|
||||||
|
};
|
||||||
|
|
||||||
|
generateCookFunction(markdownOptions).then((cookFunction) => {
|
||||||
|
ChatMessage.cookFunction = (raw) => {
|
||||||
|
return simpleCategoryHashMentionTransform(
|
||||||
|
cookFunction(raw),
|
||||||
|
site.categories
|
||||||
|
);
|
||||||
|
};
|
||||||
|
});
|
||||||
|
},
|
||||||
|
};
|
|
@ -10,6 +10,7 @@ const MIN_REFRESH_DURATION_MS = 180000; // 3 minutes
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: "chat-setup",
|
name: "chat-setup",
|
||||||
|
|
||||||
initialize(container) {
|
initialize(container) {
|
||||||
this.chatService = container.lookup("service:chat");
|
this.chatService = container.lookup("service:chat");
|
||||||
this.siteSettings = container.lookup("service:site-settings");
|
this.siteSettings = container.lookup("service:site-settings");
|
||||||
|
@ -19,6 +20,7 @@ export default {
|
||||||
if (!this.chatService.userCanChat) {
|
if (!this.chatService.userCanChat) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
withPluginApi("0.12.1", (api) => {
|
withPluginApi("0.12.1", (api) => {
|
||||||
api.registerChatComposerButton({
|
api.registerChatComposerButton({
|
||||||
id: "chat-upload-btn",
|
id: "chat-upload-btn",
|
||||||
|
|
|
@ -38,7 +38,7 @@ export default class ChatMessageFlag {
|
||||||
let flagsAvailable = site.flagTypes;
|
let flagsAvailable = site.flagTypes;
|
||||||
|
|
||||||
flagsAvailable = flagsAvailable.filter((flag) => {
|
flagsAvailable = flagsAvailable.filter((flag) => {
|
||||||
return model.available_flags.includes(flag.name_key);
|
return model.availableFlags.includes(flag.name_key);
|
||||||
});
|
});
|
||||||
|
|
||||||
// "message user" option should be at the top
|
// "message user" option should be at the top
|
||||||
|
|
|
@ -7,6 +7,7 @@ import { tracked } from "@glimmer/tracking";
|
||||||
import slugifyChannel from "discourse/plugins/chat/discourse/lib/slugify-channel";
|
import slugifyChannel from "discourse/plugins/chat/discourse/lib/slugify-channel";
|
||||||
import ChatThreadsManager from "discourse/plugins/chat/discourse/lib/chat-threads-manager";
|
import ChatThreadsManager from "discourse/plugins/chat/discourse/lib/chat-threads-manager";
|
||||||
import { getOwner } from "discourse-common/lib/get-owner";
|
import { getOwner } from "discourse-common/lib/get-owner";
|
||||||
|
import { TrackedArray } from "@ember-compat/tracked-built-ins";
|
||||||
|
|
||||||
export const CHATABLE_TYPES = {
|
export const CHATABLE_TYPES = {
|
||||||
directMessageChannel: "DirectMessage",
|
directMessageChannel: "DirectMessage",
|
||||||
|
@ -54,6 +55,16 @@ export default class ChatChannel extends RestModel {
|
||||||
@tracked chatableType;
|
@tracked chatableType;
|
||||||
@tracked status;
|
@tracked status;
|
||||||
@tracked activeThread;
|
@tracked activeThread;
|
||||||
|
@tracked messages = new TrackedArray();
|
||||||
|
@tracked lastMessageSentAt;
|
||||||
|
@tracked canDeleteOthers;
|
||||||
|
@tracked canDeleteSelf;
|
||||||
|
@tracked canFlag;
|
||||||
|
@tracked canLoadMoreFuture;
|
||||||
|
@tracked canLoadMorePast;
|
||||||
|
@tracked canModerate;
|
||||||
|
@tracked userSilenced;
|
||||||
|
@tracked draft;
|
||||||
|
|
||||||
threadsManager = new ChatThreadsManager(getOwner(this));
|
threadsManager = new ChatThreadsManager(getOwner(this));
|
||||||
|
|
||||||
|
@ -74,11 +85,11 @@ export default class ChatChannel extends RestModel {
|
||||||
}
|
}
|
||||||
|
|
||||||
get isDirectMessageChannel() {
|
get isDirectMessageChannel() {
|
||||||
return this.chatable_type === CHATABLE_TYPES.directMessageChannel;
|
return this.chatableType === CHATABLE_TYPES.directMessageChannel;
|
||||||
}
|
}
|
||||||
|
|
||||||
get isCategoryChannel() {
|
get isCategoryChannel() {
|
||||||
return this.chatable_type === CHATABLE_TYPES.categoryChannel;
|
return this.chatableType === CHATABLE_TYPES.categoryChannel;
|
||||||
}
|
}
|
||||||
|
|
||||||
get isOpen() {
|
get isOpen() {
|
||||||
|
@ -105,6 +116,56 @@ export default class ChatChannel extends RestModel {
|
||||||
return this.currentUserMembership.following;
|
return this.currentUserMembership.following;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get visibleMessages() {
|
||||||
|
return this.messages.filter((message) => message.visible);
|
||||||
|
}
|
||||||
|
|
||||||
|
set details(details) {
|
||||||
|
this.canDeleteOthers = details.can_delete_others ?? false;
|
||||||
|
this.canDeleteSelf = details.can_delete_self ?? false;
|
||||||
|
this.canFlag = details.can_flag ?? false;
|
||||||
|
this.canModerate = details.can_moderate ?? false;
|
||||||
|
if (details.can_load_more_future !== undefined) {
|
||||||
|
this.canLoadMoreFuture = details.can_load_more_future;
|
||||||
|
}
|
||||||
|
if (details.can_load_more_past !== undefined) {
|
||||||
|
this.canLoadMorePast = details.can_load_more_past;
|
||||||
|
}
|
||||||
|
this.userSilenced = details.user_silenced ?? false;
|
||||||
|
this.status = details.channel_status;
|
||||||
|
this.channelMessageBusLastId = details.channel_message_bus_last_id;
|
||||||
|
}
|
||||||
|
|
||||||
|
clearMessages() {
|
||||||
|
this.messages.clear();
|
||||||
|
|
||||||
|
this.canLoadMoreFuture = null;
|
||||||
|
this.canLoadMorePast = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
addMessages(messages = []) {
|
||||||
|
this.messages = this.messages
|
||||||
|
.concat(messages)
|
||||||
|
.uniqBy("id")
|
||||||
|
.sortBy("createdAt");
|
||||||
|
}
|
||||||
|
|
||||||
|
findMessage(messageId) {
|
||||||
|
return this.messages.find(
|
||||||
|
(message) => message.id === parseInt(messageId, 10)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
removeMessage(message) {
|
||||||
|
return this.messages.removeObject(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
findStagedMessage(stagedMessageId) {
|
||||||
|
return this.messages.find(
|
||||||
|
(message) => message.staged && message.id === stagedMessageId
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
canModifyMessages(user) {
|
canModifyMessages(user) {
|
||||||
if (user.staff) {
|
if (user.staff) {
|
||||||
return !STAFF_READONLY_STATUSES.includes(this.status);
|
return !STAFF_READONLY_STATUSES.includes(this.status);
|
||||||
|
@ -127,6 +188,10 @@ export default class ChatChannel extends RestModel {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (this.currentUserMembership.last_read_message_id >= messageId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
return ajax(`/chat/${this.id}/read/${messageId}.json`, {
|
return ajax(`/chat/${this.id}/read/${messageId}.json`, {
|
||||||
method: "PUT",
|
method: "PUT",
|
||||||
}).then(() => {
|
}).then(() => {
|
||||||
|
@ -142,12 +207,17 @@ ChatChannel.reopenClass({
|
||||||
this._initUserModels(args);
|
this._initUserModels(args);
|
||||||
this._initUserMembership(args);
|
this._initUserMembership(args);
|
||||||
|
|
||||||
args.chatableType = args.chatable_type;
|
this._remapKey(args, "chatable_type", "chatableType");
|
||||||
args.membershipsCount = args.memberships_count;
|
this._remapKey(args, "memberships_count", "membershipsCount");
|
||||||
|
this._remapKey(args, "last_message_sent_at", "lastMessageSentAt");
|
||||||
|
|
||||||
return this._super(args);
|
return this._super(args);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
_remapKey(obj, oldKey, newKey) {
|
||||||
|
delete Object.assign(obj, { [newKey]: obj[oldKey] })[oldKey];
|
||||||
|
},
|
||||||
|
|
||||||
_initUserModels(args) {
|
_initUserModels(args) {
|
||||||
if (args.chatable?.users?.length) {
|
if (args.chatable?.users?.length) {
|
||||||
for (let i = 0; i < args.chatable?.users?.length; i++) {
|
for (let i = 0; i < args.chatable?.users?.length; i++) {
|
||||||
|
|
|
@ -0,0 +1,62 @@
|
||||||
|
import { tracked } from "@glimmer/tracking";
|
||||||
|
|
||||||
|
export default class ChatMessageDraft {
|
||||||
|
static create(args = {}) {
|
||||||
|
return new ChatMessageDraft(args ?? {});
|
||||||
|
}
|
||||||
|
|
||||||
|
@tracked uploads;
|
||||||
|
@tracked message;
|
||||||
|
@tracked _replyToMsg;
|
||||||
|
|
||||||
|
constructor(args = {}) {
|
||||||
|
this.message = args.message ?? "";
|
||||||
|
this.uploads = args.uploads ?? [];
|
||||||
|
this.replyToMsg = args.replyToMsg;
|
||||||
|
}
|
||||||
|
|
||||||
|
get replyToMsg() {
|
||||||
|
return this._replyToMsg;
|
||||||
|
}
|
||||||
|
|
||||||
|
set replyToMsg(message) {
|
||||||
|
this._replyToMsg = message
|
||||||
|
? {
|
||||||
|
id: message.id,
|
||||||
|
excerpt: message.excerpt,
|
||||||
|
user: {
|
||||||
|
id: message.user.id,
|
||||||
|
name: message.user.name,
|
||||||
|
avatar_template: message.user.avatar_template,
|
||||||
|
username: message.user.username,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
: null;
|
||||||
|
}
|
||||||
|
|
||||||
|
toJSON() {
|
||||||
|
if (
|
||||||
|
this.message?.length === 0 &&
|
||||||
|
this.uploads?.length === 0 &&
|
||||||
|
!this.replyToMsg
|
||||||
|
) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = {};
|
||||||
|
|
||||||
|
if (this.uploads?.length > 0) {
|
||||||
|
data.uploads = this.uploads;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.message?.length > 0) {
|
||||||
|
data.message = this.message;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.replyToMsg) {
|
||||||
|
data.replyToMsg = this.replyToMsg;
|
||||||
|
}
|
||||||
|
|
||||||
|
return JSON.stringify(data);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,33 @@
|
||||||
|
import { tracked } from "@glimmer/tracking";
|
||||||
|
import User from "discourse/models/user";
|
||||||
|
import { TrackedArray } from "@ember-compat/tracked-built-ins";
|
||||||
|
|
||||||
|
export default class ChatMessageReaction {
|
||||||
|
static create(args = {}) {
|
||||||
|
return new ChatMessageReaction(args);
|
||||||
|
}
|
||||||
|
|
||||||
|
@tracked count = 0;
|
||||||
|
@tracked reacted = false;
|
||||||
|
@tracked users = [];
|
||||||
|
|
||||||
|
constructor(args = {}) {
|
||||||
|
this.messageId = args.messageId;
|
||||||
|
this.count = args.count;
|
||||||
|
this.emoji = args.emoji;
|
||||||
|
this.users = this.#initUsersModels(args.users);
|
||||||
|
this.reacted = args.reacted;
|
||||||
|
}
|
||||||
|
|
||||||
|
#initUsersModels(users = []) {
|
||||||
|
return new TrackedArray(
|
||||||
|
users.map((user) => {
|
||||||
|
if (user instanceof User) {
|
||||||
|
return user;
|
||||||
|
}
|
||||||
|
|
||||||
|
return User.create(user);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,26 +1,191 @@
|
||||||
import RestModel from "discourse/models/rest";
|
|
||||||
import User from "discourse/models/user";
|
import User from "discourse/models/user";
|
||||||
import EmberObject from "@ember/object";
|
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 guid from "pretty-text/guid";
|
||||||
|
|
||||||
export default class ChatMessage extends RestModel {}
|
export default class ChatMessage {
|
||||||
|
static cookFunction = null;
|
||||||
|
|
||||||
ChatMessage.reopenClass({
|
static create(channel, args = {}) {
|
||||||
create(args = {}) {
|
return new ChatMessage(channel, args);
|
||||||
this._initReactions(args);
|
}
|
||||||
this._initUserModel(args);
|
|
||||||
|
|
||||||
return this._super(args);
|
static createStagedMessage(channel, args = {}) {
|
||||||
},
|
args.id = guid();
|
||||||
|
args.staged = true;
|
||||||
|
return new ChatMessage(channel, args);
|
||||||
|
}
|
||||||
|
|
||||||
_initReactions(args) {
|
@tracked id;
|
||||||
args.reactions = EmberObject.create(args.reactions || {});
|
@tracked error;
|
||||||
},
|
@tracked selected;
|
||||||
|
@tracked channel;
|
||||||
|
@tracked staged = false;
|
||||||
|
@tracked channelId;
|
||||||
|
@tracked createdAt;
|
||||||
|
@tracked deletedAt;
|
||||||
|
@tracked uploads;
|
||||||
|
@tracked excerpt;
|
||||||
|
@tracked message;
|
||||||
|
@tracked threadId;
|
||||||
|
@tracked reactions;
|
||||||
|
@tracked reviewableId;
|
||||||
|
@tracked user;
|
||||||
|
@tracked cooked;
|
||||||
|
@tracked inReplyTo;
|
||||||
|
@tracked expanded;
|
||||||
|
@tracked bookmark;
|
||||||
|
@tracked userFlagStatus;
|
||||||
|
@tracked hidden;
|
||||||
|
@tracked version = 0;
|
||||||
|
@tracked edited;
|
||||||
|
@tracked chatWebhookEvent = new TrackedObject();
|
||||||
|
@tracked mentionWarning;
|
||||||
|
@tracked availableFlags;
|
||||||
|
@tracked newest = false;
|
||||||
|
@tracked highlighted = false;
|
||||||
|
|
||||||
_initUserModel(args) {
|
constructor(channel, args = {}) {
|
||||||
if (!args.user || args.user instanceof User) {
|
this.channel = channel;
|
||||||
return;
|
this.id = args.id;
|
||||||
|
this.newest = args.newest;
|
||||||
|
this.staged = args.staged;
|
||||||
|
this.edited = args.edited;
|
||||||
|
this.availableFlags = args.available_flags;
|
||||||
|
this.hidden = args.hidden;
|
||||||
|
this.threadId = args.thread_id;
|
||||||
|
this.channelId = args.chat_channel_id;
|
||||||
|
this.chatWebhookEvent = args.chat_webhook_event;
|
||||||
|
this.createdAt = args.created_at;
|
||||||
|
this.deletedAt = args.deleted_at;
|
||||||
|
this.excerpt = args.excerpt;
|
||||||
|
this.reviewableId = args.reviewable_id;
|
||||||
|
this.userFlagStatus = args.user_flag_status;
|
||||||
|
this.inReplyTo = args.in_reply_to
|
||||||
|
? ChatMessage.create(channel, args.in_reply_to)
|
||||||
|
: null;
|
||||||
|
this.message = args.message;
|
||||||
|
this.cooked = args.cooked || ChatMessage.cookFunction(this.message);
|
||||||
|
this.reactions = this.#initChatMessageReactionModel(
|
||||||
|
args.id,
|
||||||
|
args.reactions
|
||||||
|
);
|
||||||
|
this.uploads = new TrackedArray(args.uploads || []);
|
||||||
|
this.user = this.#initUserModel(args.user);
|
||||||
|
this.bookmark = args.bookmark ? Bookmark.create(args.bookmark) : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
get read() {
|
||||||
|
return this.channel.currentUserMembership?.last_read_message_id >= this.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
get firstMessageOfTheDayAt() {
|
||||||
|
if (!this.previousMessage) {
|
||||||
|
return this.#calendarDate(this.createdAt);
|
||||||
}
|
}
|
||||||
|
|
||||||
args.user = User.create(args.user);
|
if (
|
||||||
},
|
!this.#areDatesOnSameDay(
|
||||||
});
|
new Date(this.previousMessage.createdAt),
|
||||||
|
new Date(this.createdAt)
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
return this.#calendarDate(this.createdAt);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#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.channel.messages.indexOf(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
@cached
|
||||||
|
get previousMessage() {
|
||||||
|
return this.channel?.messages?.objectAt?.(this.index - 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
@cached
|
||||||
|
get nextMessage() {
|
||||||
|
return this.channel?.messages?.objectAt?.(this.index + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
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 false;
|
||||||
|
}
|
||||||
|
|
||||||
|
existingReaction.count = existingReaction.count + 1;
|
||||||
|
if (selfReaction) {
|
||||||
|
existingReaction.reacted = true;
|
||||||
|
}
|
||||||
|
existingReaction.users.pushObject(actor);
|
||||||
|
} else {
|
||||||
|
existingReaction.count = existingReaction.count - 1;
|
||||||
|
|
||||||
|
if (selfReaction) {
|
||||||
|
existingReaction.reacted = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (existingReaction.count === 0) {
|
||||||
|
this.reactions.removeObject(existingReaction);
|
||||||
|
} else {
|
||||||
|
existingReaction.users.removeObject(
|
||||||
|
existingReaction.users.find((user) => user.id === actor.id)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (action === "add") {
|
||||||
|
this.reactions.pushObject(
|
||||||
|
ChatMessageReaction.create({
|
||||||
|
count: 1,
|
||||||
|
emoji,
|
||||||
|
reacted: selfReaction,
|
||||||
|
users: [actor],
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#initChatMessageReactionModel(messageId, reactions = []) {
|
||||||
|
return reactions.map((reaction) =>
|
||||||
|
ChatMessageReaction.create(Object.assign({ messageId }, reaction))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#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()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,35 @@
|
||||||
|
import Modifier from "ember-modifier";
|
||||||
|
import { registerDestructor } from "@ember/destroyable";
|
||||||
|
|
||||||
|
const IS_PINNED_CLASS = "is-pinned";
|
||||||
|
|
||||||
|
/*
|
||||||
|
This modifier is used to track the date separator in the chat message list.
|
||||||
|
The trick is to have an element with `top: -1px` which will stop fully intersecting
|
||||||
|
as soon as it's scrolled a little bit.
|
||||||
|
*/
|
||||||
|
export default class ChatTrackMessageSeparatorDate extends Modifier {
|
||||||
|
constructor(owner, args) {
|
||||||
|
super(owner, args);
|
||||||
|
registerDestructor(this, (instance) => instance.cleanup());
|
||||||
|
}
|
||||||
|
|
||||||
|
modify(element) {
|
||||||
|
this.intersectionObserver = new IntersectionObserver(
|
||||||
|
([event]) => {
|
||||||
|
if (event.isIntersecting && event.intersectionRatio < 1) {
|
||||||
|
event.target.classList.add(IS_PINNED_CLASS);
|
||||||
|
} else {
|
||||||
|
event.target.classList.remove(IS_PINNED_CLASS);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ threshold: [0, 1] }
|
||||||
|
);
|
||||||
|
|
||||||
|
this.intersectionObserver.observe(element);
|
||||||
|
}
|
||||||
|
|
||||||
|
cleanup() {
|
||||||
|
this.intersectionObserver?.disconnect();
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,23 +0,0 @@
|
||||||
import Modifier from "ember-modifier";
|
|
||||||
import { inject as service } from "@ember/service";
|
|
||||||
import { registerDestructor } from "@ember/destroyable";
|
|
||||||
|
|
||||||
export default class TrackMessageVisibility extends Modifier {
|
|
||||||
@service chatMessageVisibilityObserver;
|
|
||||||
|
|
||||||
element = null;
|
|
||||||
|
|
||||||
constructor(owner, args) {
|
|
||||||
super(owner, args);
|
|
||||||
registerDestructor(this, (instance) => instance.cleanup());
|
|
||||||
}
|
|
||||||
|
|
||||||
modify(element) {
|
|
||||||
this.element = element;
|
|
||||||
this.chatMessageVisibilityObserver.observe(element);
|
|
||||||
}
|
|
||||||
|
|
||||||
cleanup() {
|
|
||||||
this.chatMessageVisibilityObserver.unobserve(this.element);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -0,0 +1,43 @@
|
||||||
|
import Modifier from "ember-modifier";
|
||||||
|
import { registerDestructor } from "@ember/destroyable";
|
||||||
|
import { bind } from "discourse-common/utils/decorators";
|
||||||
|
|
||||||
|
export default class ChatTrackMessage extends Modifier {
|
||||||
|
visibleCallback = null;
|
||||||
|
notVisibleCallback = null;
|
||||||
|
|
||||||
|
constructor(owner, args) {
|
||||||
|
super(owner, args);
|
||||||
|
registerDestructor(this, (instance) => instance.cleanup());
|
||||||
|
}
|
||||||
|
|
||||||
|
modify(element, [visibleCallback, notVisibleCallback]) {
|
||||||
|
this.visibleCallback = visibleCallback;
|
||||||
|
this.notVisibleCallback = notVisibleCallback;
|
||||||
|
|
||||||
|
this.intersectionObserver = new IntersectionObserver(
|
||||||
|
this._intersectionObserverCallback,
|
||||||
|
{
|
||||||
|
root: document,
|
||||||
|
threshold: 0.9,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
this.intersectionObserver.observe(element);
|
||||||
|
}
|
||||||
|
|
||||||
|
cleanup() {
|
||||||
|
this.intersectionObserver?.disconnect();
|
||||||
|
}
|
||||||
|
|
||||||
|
@bind
|
||||||
|
_intersectionObserverCallback(entries) {
|
||||||
|
entries.forEach((entry) => {
|
||||||
|
if (entry.isIntersecting) {
|
||||||
|
this.visibleCallback?.();
|
||||||
|
} else {
|
||||||
|
this.notVisibleCallback?.();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
|
@ -10,6 +10,10 @@ export default class ChatChannelRoute extends DiscourseRoute {
|
||||||
|
|
||||||
@action
|
@action
|
||||||
willTransition(transition) {
|
willTransition(transition) {
|
||||||
|
// Technically we could keep messages to avoid re-fetching them, but
|
||||||
|
// it's not worth the complexity for now
|
||||||
|
this.chat.activeChannel?.clearMessages();
|
||||||
|
|
||||||
this.chat.activeChannel.activeThread = null;
|
this.chat.activeChannel.activeThread = null;
|
||||||
this.chatStateManager.closeSidePanel();
|
this.chatStateManager.closeSidePanel();
|
||||||
|
|
||||||
|
|
|
@ -233,6 +233,39 @@ export default class ChatApi extends Service {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns messages of a channel, from the last message or a specificed target.
|
||||||
|
* @param {number} channelId - The ID of the channel.
|
||||||
|
* @param {object} data - Params of the query.
|
||||||
|
* @param {integer} data.targetMessageId - ID of the targeted message.
|
||||||
|
* @param {integer} data.messageId - ID of the targeted message.
|
||||||
|
* @param {integer} data.direction - Fetch past or future messages.
|
||||||
|
* @param {integer} data.pageSize - Max number of messages to fetch.
|
||||||
|
* @returns {Promise}
|
||||||
|
*/
|
||||||
|
async messages(channelId, data = {}) {
|
||||||
|
let path;
|
||||||
|
const args = {};
|
||||||
|
|
||||||
|
if (data.targetMessageId) {
|
||||||
|
path = `/chat/lookup/${data.targetMessageId}`;
|
||||||
|
args.chat_channel_id = channelId;
|
||||||
|
} else {
|
||||||
|
args.page_size = data.pageSize;
|
||||||
|
path = `/chat/${channelId}/messages`;
|
||||||
|
|
||||||
|
if (data.messageId) {
|
||||||
|
args.message_id = data.messageId;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data.direction) {
|
||||||
|
args.direction = data.direction;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ajax(path, { data: args });
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Update notifications settings of current user for a channel.
|
* Update notifications settings of current user for a channel.
|
||||||
* @param {number} channelId - The ID of the channel.
|
* @param {number} channelId - The ID of the channel.
|
||||||
|
|
|
@ -42,6 +42,14 @@ export default class ChatChannelsManager extends Service {
|
||||||
this.#cache(model);
|
this.#cache(model);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
channelObject.meta?.message_bus_last_ids?.channel_message_bus_last_id !==
|
||||||
|
undefined
|
||||||
|
) {
|
||||||
|
model.channelMessageBusLastId =
|
||||||
|
channelObject.meta.message_bus_last_ids.channel_message_bus_last_id;
|
||||||
|
}
|
||||||
|
|
||||||
return model;
|
return model;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -138,8 +146,7 @@ export default class ChatChannelsManager extends Service {
|
||||||
const unreadCountA = a.currentUserMembership.unread_count || 0;
|
const unreadCountA = a.currentUserMembership.unread_count || 0;
|
||||||
const unreadCountB = b.currentUserMembership.unread_count || 0;
|
const unreadCountB = b.currentUserMembership.unread_count || 0;
|
||||||
if (unreadCountA === unreadCountB) {
|
if (unreadCountA === unreadCountB) {
|
||||||
return new Date(a.get("last_message_sent_at")) >
|
return new Date(a.lastMessageSentAt) > new Date(b.lastMessageSentAt)
|
||||||
new Date(b.get("last_message_sent_at"))
|
|
||||||
? -1
|
? -1
|
||||||
: 1;
|
: 1;
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -1,63 +0,0 @@
|
||||||
import Service, { inject as service } from "@ember/service";
|
|
||||||
import { isTesting } from "discourse-common/config/environment";
|
|
||||||
import { bind } from "discourse-common/utils/decorators";
|
|
||||||
|
|
||||||
export default class ChatMessageVisibilityObserver extends Service {
|
|
||||||
@service chat;
|
|
||||||
|
|
||||||
intersectionObserver = new IntersectionObserver(
|
|
||||||
this._intersectionObserverCallback,
|
|
||||||
{
|
|
||||||
root: document,
|
|
||||||
rootMargin: "-10px",
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
mutationObserver = new MutationObserver(this._mutationObserverCallback, {
|
|
||||||
root: document,
|
|
||||||
rootMargin: "-10px",
|
|
||||||
});
|
|
||||||
|
|
||||||
willDestroy() {
|
|
||||||
this.intersectionObserver.disconnect();
|
|
||||||
this.mutationObserver.disconnect();
|
|
||||||
}
|
|
||||||
|
|
||||||
@bind
|
|
||||||
_intersectionObserverCallback(entries) {
|
|
||||||
entries.forEach((entry) => {
|
|
||||||
entry.target.dataset.visible = entry.isIntersecting;
|
|
||||||
|
|
||||||
if (
|
|
||||||
!entry.target.dataset.stagedId &&
|
|
||||||
entry.isIntersecting &&
|
|
||||||
!isTesting()
|
|
||||||
) {
|
|
||||||
this.chat.updateLastReadMessage();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
@bind
|
|
||||||
_mutationObserverCallback(mutationList) {
|
|
||||||
mutationList.forEach((mutation) => {
|
|
||||||
const data = mutation.target.dataset;
|
|
||||||
if (data.id && data.visible && !data.stagedId) {
|
|
||||||
this.chat.updateLastReadMessage();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
observe(element) {
|
|
||||||
this.intersectionObserver.observe(element);
|
|
||||||
this.mutationObserver.observe(element, {
|
|
||||||
attributes: true,
|
|
||||||
attributeOldValue: true,
|
|
||||||
attributeFilter: ["data-staged-id"],
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
unobserve(element) {
|
|
||||||
this.intersectionObserver.unobserve(element);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -154,7 +154,7 @@ export default class ChatSubscriptionsManager extends Service {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
channel.set("last_message_sent_at", new Date());
|
channel.lastMessageSentAt = new Date();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -185,13 +185,14 @@ export default class ChatSubscriptionsManager extends Service {
|
||||||
_onUserTrackingStateUpdate(busData) {
|
_onUserTrackingStateUpdate(busData) {
|
||||||
this.chatChannelsManager.find(busData.chat_channel_id).then((channel) => {
|
this.chatChannelsManager.find(busData.chat_channel_id).then((channel) => {
|
||||||
if (
|
if (
|
||||||
channel?.currentUserMembership?.last_read_message_id <=
|
!channel?.currentUserMembership?.last_read_message_id ||
|
||||||
busData.chat_message_id
|
parseInt(channel?.currentUserMembership?.last_read_message_id, 10) <=
|
||||||
|
busData.chat_message_id
|
||||||
) {
|
) {
|
||||||
channel.currentUserMembership.last_read_message_id =
|
channel.currentUserMembership.last_read_message_id =
|
||||||
busData.chat_message_id;
|
busData.chat_message_id;
|
||||||
channel.currentUserMembership.unread_count = 0;
|
channel.currentUserMembership.unread_count = busData.unread_count;
|
||||||
channel.currentUserMembership.unread_mentions = 0;
|
channel.currentUserMembership.unread_mentions = busData.unread_mentions;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,29 +3,18 @@ import { tracked } from "@glimmer/tracking";
|
||||||
import userSearch from "discourse/lib/user-search";
|
import userSearch from "discourse/lib/user-search";
|
||||||
import { popupAjaxError } from "discourse/lib/ajax-error";
|
import { popupAjaxError } from "discourse/lib/ajax-error";
|
||||||
import Service, { inject as service } from "@ember/service";
|
import Service, { inject as service } from "@ember/service";
|
||||||
import Site from "discourse/models/site";
|
|
||||||
import { ajax } from "discourse/lib/ajax";
|
import { ajax } from "discourse/lib/ajax";
|
||||||
import { generateCookFunction } from "discourse/lib/text";
|
|
||||||
import { cancel, next } from "@ember/runloop";
|
import { cancel, next } from "@ember/runloop";
|
||||||
import { and } from "@ember/object/computed";
|
import { and } from "@ember/object/computed";
|
||||||
import { computed } from "@ember/object";
|
import { computed } from "@ember/object";
|
||||||
import { Promise } from "rsvp";
|
|
||||||
import simpleCategoryHashMentionTransform from "discourse/plugins/chat/discourse/lib/simple-category-hash-mention-transform";
|
|
||||||
import discourseDebounce from "discourse-common/lib/debounce";
|
|
||||||
import discourseLater from "discourse-common/lib/later";
|
import discourseLater from "discourse-common/lib/later";
|
||||||
import userPresent from "discourse/lib/user-presence";
|
import ChatMessageDraft from "discourse/plugins/chat/discourse/models/chat-message-draft";
|
||||||
|
|
||||||
export const LIST_VIEW = "list_view";
|
|
||||||
export const CHAT_VIEW = "chat_view";
|
|
||||||
export const DRAFT_CHANNEL_VIEW = "draft_channel_view";
|
|
||||||
|
|
||||||
const CHAT_ONLINE_OPTIONS = {
|
const CHAT_ONLINE_OPTIONS = {
|
||||||
userUnseenTime: 300000, // 5 minutes seconds with no interaction
|
userUnseenTime: 300000, // 5 minutes seconds with no interaction
|
||||||
browserHiddenTime: 300000, // Or the browser has been in the background for 5 minutes
|
browserHiddenTime: 300000, // Or the browser has been in the background for 5 minutes
|
||||||
};
|
};
|
||||||
|
|
||||||
const READ_INTERVAL = 1000;
|
|
||||||
|
|
||||||
export default class Chat extends Service {
|
export default class Chat extends Service {
|
||||||
@service appEvents;
|
@service appEvents;
|
||||||
@service chatNotificationManager;
|
@service chatNotificationManager;
|
||||||
|
@ -64,13 +53,6 @@ export default class Chat extends Service {
|
||||||
|
|
||||||
if (this.userCanChat) {
|
if (this.userCanChat) {
|
||||||
this.presenceChannel = this.presence.getChannel("/chat/online");
|
this.presenceChannel = this.presence.getChannel("/chat/online");
|
||||||
this.draftStore = {};
|
|
||||||
|
|
||||||
if (this.currentUser.chat_drafts) {
|
|
||||||
this.currentUser.chat_drafts.forEach((draft) => {
|
|
||||||
this.draftStore[draft.channel_id] = JSON.parse(draft.data);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -103,6 +85,16 @@ export default class Chat extends Service {
|
||||||
[...channels.public_channels, ...channels.direct_message_channels].forEach(
|
[...channels.public_channels, ...channels.direct_message_channels].forEach(
|
||||||
(channelObject) => {
|
(channelObject) => {
|
||||||
const channel = this.chatChannelsManager.store(channelObject);
|
const channel = this.chatChannelsManager.store(channelObject);
|
||||||
|
|
||||||
|
if (this.currentUser.chat_drafts) {
|
||||||
|
const storedDraft = this.currentUser.chat_drafts.find(
|
||||||
|
(draft) => draft.channel_id === channel.id
|
||||||
|
);
|
||||||
|
channel.draft = ChatMessageDraft.create(
|
||||||
|
storedDraft ? JSON.parse(storedDraft.data) : null
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return this.chatChannelsManager.follow(channel);
|
return this.chatChannelsManager.follow(channel);
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
@ -116,33 +108,6 @@ export default class Chat extends Service {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
loadCookFunction(categories) {
|
|
||||||
if (this.cook) {
|
|
||||||
return Promise.resolve(this.cook);
|
|
||||||
}
|
|
||||||
|
|
||||||
const markdownOptions = {
|
|
||||||
featuresOverride: Site.currentProp(
|
|
||||||
"markdown_additional_options.chat.limited_pretty_text_features"
|
|
||||||
),
|
|
||||||
markdownItRules: Site.currentProp(
|
|
||||||
"markdown_additional_options.chat.limited_pretty_text_markdown_rules"
|
|
||||||
),
|
|
||||||
hashtagTypesInPriorityOrder:
|
|
||||||
this.site.hashtag_configurations["chat-composer"],
|
|
||||||
hashtagIcons: this.site.hashtag_icons,
|
|
||||||
};
|
|
||||||
|
|
||||||
return generateCookFunction(markdownOptions).then((cookFunction) => {
|
|
||||||
return this.set("cook", (raw) => {
|
|
||||||
return simpleCategoryHashMentionTransform(
|
|
||||||
cookFunction(raw),
|
|
||||||
categories
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
updatePresence() {
|
updatePresence() {
|
||||||
next(() => {
|
next(() => {
|
||||||
if (this.isDestroyed || this.isDestroying) {
|
if (this.isDestroyed || this.isDestroying) {
|
||||||
|
@ -277,10 +242,6 @@ export default class Chat extends Service {
|
||||||
: this.router.transitionTo("chat.channel", ...channel.routeModels);
|
: this.router.transitionTo("chat.channel", ...channel.routeModels);
|
||||||
}
|
}
|
||||||
|
|
||||||
_fireOpenMessageAppEvent(messageId) {
|
|
||||||
this.appEvents.trigger("chat-live-pane:highlight-message", messageId);
|
|
||||||
}
|
|
||||||
|
|
||||||
async followChannel(channel) {
|
async followChannel(channel) {
|
||||||
return this.chatChannelsManager.follow(channel);
|
return this.chatChannelsManager.follow(channel);
|
||||||
}
|
}
|
||||||
|
@ -327,84 +288,6 @@ export default class Chat extends Service {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
_saveDraft(channelId, draft) {
|
|
||||||
const data = { chat_channel_id: channelId };
|
|
||||||
if (draft) {
|
|
||||||
data.data = JSON.stringify(draft);
|
|
||||||
}
|
|
||||||
|
|
||||||
ajax("/chat/drafts.json", { type: "POST", data, ignoreUnsent: false })
|
|
||||||
.then(() => {
|
|
||||||
this.markNetworkAsReliable();
|
|
||||||
})
|
|
||||||
.catch((error) => {
|
|
||||||
// we ignore a draft which can't be saved because it's too big
|
|
||||||
// and only deal with network error for now
|
|
||||||
if (!error.jqXHR?.responseJSON?.errors?.length) {
|
|
||||||
this.markNetworkAsUnreliable();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
setDraftForChannel(channel, draft) {
|
|
||||||
if (
|
|
||||||
draft &&
|
|
||||||
(draft.value || draft.uploads.length > 0 || draft.replyToMsg)
|
|
||||||
) {
|
|
||||||
this.draftStore[channel.id] = draft;
|
|
||||||
} else {
|
|
||||||
delete this.draftStore[channel.id];
|
|
||||||
draft = null; // _saveDraft will destroy draft
|
|
||||||
}
|
|
||||||
|
|
||||||
discourseDebounce(this, this._saveDraft, channel.id, draft, 2000);
|
|
||||||
}
|
|
||||||
|
|
||||||
getDraftForChannel(channelId) {
|
|
||||||
return (
|
|
||||||
this.draftStore[channelId] || {
|
|
||||||
value: "",
|
|
||||||
uploads: [],
|
|
||||||
replyToMsg: null,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
updateLastReadMessage() {
|
|
||||||
discourseDebounce(this, this._queuedReadMessageUpdate, READ_INTERVAL);
|
|
||||||
}
|
|
||||||
|
|
||||||
_queuedReadMessageUpdate() {
|
|
||||||
const visibleMessages = document.querySelectorAll(
|
|
||||||
".chat-message-container[data-visible=true]"
|
|
||||||
);
|
|
||||||
const channel = this.activeChannel;
|
|
||||||
|
|
||||||
if (
|
|
||||||
!channel?.isFollowing ||
|
|
||||||
visibleMessages?.length === 0 ||
|
|
||||||
!userPresent()
|
|
||||||
) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const latestUnreadMsgId = parseInt(
|
|
||||||
visibleMessages[visibleMessages.length - 1].dataset.id,
|
|
||||||
10
|
|
||||||
);
|
|
||||||
|
|
||||||
const membership = channel.currentUserMembership;
|
|
||||||
const hasUnreadMessages =
|
|
||||||
latestUnreadMsgId > membership.last_read_message_id;
|
|
||||||
if (
|
|
||||||
hasUnreadMessages ||
|
|
||||||
membership.unread_count > 0 ||
|
|
||||||
membership.unread_mentions > 0
|
|
||||||
) {
|
|
||||||
channel.updateLastReadMessage(latestUnreadMsgId);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
addToolbarButton() {
|
addToolbarButton() {
|
||||||
deprecated(
|
deprecated(
|
||||||
"Use the new chat API `api.registerChatComposerButton` instead of `chat.addToolbarButton`"
|
"Use the new chat API `api.registerChatComposerButton` instead of `chat.addToolbarButton`"
|
||||||
|
|
|
@ -54,7 +54,12 @@
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{{#if this.categoryPermissionsHint}}
|
{{#if this.categoryPermissionsHint}}
|
||||||
<div class="create-channel-hint">
|
<div
|
||||||
|
class={{concat-class
|
||||||
|
"create-channel-hint"
|
||||||
|
(if this.loadingPermissionHint "loading-permissions")
|
||||||
|
}}
|
||||||
|
>
|
||||||
{{this.categoryPermissionsHint}}
|
{{this.categoryPermissionsHint}}
|
||||||
</div>
|
</div>
|
||||||
{{/if}}
|
{{/if}}
|
||||||
|
|
|
@ -5,6 +5,7 @@
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
z-index: 3;
|
||||||
|
|
||||||
&.-no-description {
|
&.-no-description {
|
||||||
.chat-channel-title {
|
.chat-channel-title {
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
.chat-composer-container {
|
.chat-composer-container {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
z-index: 3;
|
||||||
|
background-color: var(--secondary);
|
||||||
|
|
||||||
#chat-full-page-uploader,
|
#chat-full-page-uploader,
|
||||||
#chat-widget-uploader {
|
#chat-widget-uploader {
|
||||||
|
|
|
@ -6,10 +6,6 @@
|
||||||
.chat-message-actions {
|
.chat-message-actions {
|
||||||
.chat-message-reaction {
|
.chat-message-reaction {
|
||||||
@include chat-reaction;
|
@include chat-reaction;
|
||||||
|
|
||||||
&:not(.show) {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,42 +1,96 @@
|
||||||
.chat-message-separator {
|
.chat-message-separator {
|
||||||
@include unselectable;
|
@include unselectable;
|
||||||
margin: 0.25rem 0 0.25rem 1rem;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
font-size: var(--font-down-1);
|
|
||||||
position: relative;
|
|
||||||
transform: translateZ(0);
|
|
||||||
position: relative;
|
|
||||||
|
|
||||||
&.new-message {
|
&-new {
|
||||||
color: var(--danger-medium);
|
position: relative;
|
||||||
|
padding: 20px 0;
|
||||||
|
|
||||||
.divider {
|
.chat-message-separator__text-container {
|
||||||
background-color: var(--danger-medium);
|
text-align: center;
|
||||||
|
position: absolute;
|
||||||
|
height: 40px;
|
||||||
|
width: 100%;
|
||||||
|
box-sizing: border-box;
|
||||||
|
z-index: 1;
|
||||||
|
top: 0;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
|
||||||
|
.chat-message-separator__text {
|
||||||
|
color: var(--danger-medium);
|
||||||
|
background-color: var(--secondary);
|
||||||
|
padding: 0.25rem 0.5rem;
|
||||||
|
font-size: var(--font-down-1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-message-separator__line-container {
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
.chat-message-separator__line {
|
||||||
|
border-top: 1px solid var(--danger-medium);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&.first-daily-message {
|
&-date {
|
||||||
.text {
|
|
||||||
color: var(--secondary-low);
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
|
|
||||||
.divider {
|
|
||||||
background-color: var(--secondary-high);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.text {
|
|
||||||
margin: 0 auto;
|
|
||||||
padding: 0 0.75rem;
|
|
||||||
z-index: 1;
|
|
||||||
background: var(--secondary);
|
|
||||||
}
|
|
||||||
|
|
||||||
.divider {
|
|
||||||
position: absolute;
|
position: absolute;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 1px;
|
z-index: 2;
|
||||||
top: 50%;
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
justify-content: center;
|
||||||
|
pointer-events: none;
|
||||||
|
|
||||||
|
&.last-visit {
|
||||||
|
.chat-message-separator__text {
|
||||||
|
color: var(--danger-medium);
|
||||||
|
}
|
||||||
|
|
||||||
|
& + .chat-message-separator__line-container {
|
||||||
|
.chat-message-separator__line {
|
||||||
|
border-color: var(--danger-medium);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-message-separator__text-container {
|
||||||
|
padding-top: 7px;
|
||||||
|
position: sticky;
|
||||||
|
top: -1px;
|
||||||
|
|
||||||
|
&.is-pinned {
|
||||||
|
.chat-message-separator__text {
|
||||||
|
border: 1px solid var(--primary-medium);
|
||||||
|
border-radius: 3px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-message-separator__text {
|
||||||
|
@include unselectable;
|
||||||
|
background-color: var(--secondary);
|
||||||
|
border: 1px solid transparent;
|
||||||
|
color: var(--secondary-low);
|
||||||
|
font-size: var(--font-down-1);
|
||||||
|
padding: 0.25rem 0.5rem;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
& + .chat-message-separator__line-container {
|
||||||
|
padding: 20px 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
|
||||||
|
.chat-message-separator__line {
|
||||||
|
border-top: 1px solid var(--secondary-high);
|
||||||
|
left: 0;
|
||||||
|
margin: 0 0 -1px;
|
||||||
|
position: relative;
|
||||||
|
right: 0;
|
||||||
|
top: -1px;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -42,6 +42,10 @@
|
||||||
background: var(--primary-low);
|
background: var(--primary-low);
|
||||||
border-color: var(--primary-low-mid);
|
border-color: var(--primary-low-mid);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&:focus {
|
||||||
|
background: none;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.emoji {
|
.emoji {
|
||||||
|
@ -60,10 +64,6 @@
|
||||||
|
|
||||||
.chat-message-reaction {
|
.chat-message-reaction {
|
||||||
@include chat-reaction;
|
@include chat-reaction;
|
||||||
|
|
||||||
&:not(.show) {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
&.chat-action {
|
&.chat-action {
|
||||||
|
@ -82,21 +82,6 @@
|
||||||
background-color: var(--danger-hover);
|
background-color: var(--danger-hover);
|
||||||
}
|
}
|
||||||
|
|
||||||
&.transition-slow {
|
|
||||||
transition: 2s linear background-color;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.user-info-hidden {
|
|
||||||
.chat-time {
|
|
||||||
color: var(--secondary-medium);
|
|
||||||
flex-shrink: 0;
|
|
||||||
font-size: var(--font-down-2);
|
|
||||||
margin-top: 0.4em;
|
|
||||||
display: none;
|
|
||||||
width: var(--message-left-width);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&.is-reply {
|
&.is-reply {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: var(--message-left-width) 1fr;
|
grid-template-columns: var(--message-left-width) 1fr;
|
||||||
|
@ -254,6 +239,14 @@
|
||||||
|
|
||||||
.chat-message.chat-message-bookmarked {
|
.chat-message.chat-message-bookmarked {
|
||||||
background: var(--highlight-bg);
|
background: var(--highlight-bg);
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: var(--highlight-medium);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-message.chat-message-staged {
|
||||||
|
opacity: 0.6;
|
||||||
}
|
}
|
||||||
|
|
||||||
.not-mobile-device & .chat-message-reaction-list .chat-message-react-btn {
|
.not-mobile-device & .chat-message-reaction-list .chat-message-react-btn {
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
$radius: 10px;
|
$radius: 3px;
|
||||||
|
|
||||||
.chat-skeleton {
|
.chat-skeleton {
|
||||||
height: auto;
|
height: auto;
|
||||||
|
@ -55,11 +55,35 @@ $radius: 10px;
|
||||||
&__message-content {
|
&__message-content {
|
||||||
grid-area: content;
|
grid-area: content;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
padding: 10px 0;
|
||||||
}
|
}
|
||||||
&__message-msg {
|
|
||||||
height: 13px;
|
&__message-reactions {
|
||||||
|
display: flex;
|
||||||
|
padding: 5px 0 0 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__message-reaction {
|
||||||
|
background-color: var(--primary-100);
|
||||||
|
width: 32px;
|
||||||
|
height: 18px;
|
||||||
border-radius: $radius;
|
border-radius: $radius;
|
||||||
margin: 5px 0;
|
|
||||||
|
& + & {
|
||||||
|
margin-left: 0.5rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__message-text {
|
||||||
|
display: flex;
|
||||||
|
padding: 0;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__message-msg {
|
||||||
|
height: 10px;
|
||||||
|
border-radius: $radius;
|
||||||
|
margin: 2px 0;
|
||||||
|
|
||||||
.chat-skeleton__body:nth-of-type(odd) & {
|
.chat-skeleton__body:nth-of-type(odd) & {
|
||||||
background-color: var(--primary-100);
|
background-color: var(--primary-100);
|
||||||
|
@ -69,6 +93,14 @@ $radius: 10px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&__message-img {
|
||||||
|
height: 80px;
|
||||||
|
border-radius: $radius;
|
||||||
|
margin: 2px 0;
|
||||||
|
width: 200px;
|
||||||
|
background-color: var(--primary-100);
|
||||||
|
}
|
||||||
|
|
||||||
*[class^="chat-skeleton__message-"] {
|
*[class^="chat-skeleton__message-"] {
|
||||||
position: relative;
|
position: relative;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
@ -78,7 +110,7 @@ $radius: 10px;
|
||||||
position: relative;
|
position: relative;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
|
||||||
*[class^="chat-skeleton__message-"]:not(.chat-skeleton__message-content):after {
|
*[class^="chat-skeleton__message-"]:not(.chat-skeleton__message-content):not(.chat-skeleton__message-text):not(.chat-skeleton__message-reactions):after {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 0;
|
top: 0;
|
||||||
right: 0;
|
right: 0;
|
||||||
|
|
|
@ -144,6 +144,7 @@ $float-height: 530px;
|
||||||
.chat-messages-container {
|
.chat-messages-container {
|
||||||
word-wrap: break-word;
|
word-wrap: break-word;
|
||||||
white-space: normal;
|
white-space: normal;
|
||||||
|
position: relative;
|
||||||
|
|
||||||
.chat-message-container {
|
.chat-message-container {
|
||||||
display: grid;
|
display: grid;
|
||||||
|
@ -283,6 +284,8 @@ $float-height: 530px;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column-reverse;
|
flex-direction: column-reverse;
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
|
margin: 0 3px 0 0;
|
||||||
|
will-change: transform;
|
||||||
|
|
||||||
&::-webkit-scrollbar {
|
&::-webkit-scrollbar {
|
||||||
width: 15px;
|
width: 15px;
|
||||||
|
@ -323,37 +326,68 @@ $float-height: 530px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.chat-scroll-to-bottom {
|
.chat-scroll-to-bottom {
|
||||||
background: var(--primary-medium);
|
left: calc(50% - calc(32px / 2));
|
||||||
bottom: 1em;
|
align-items: center;
|
||||||
border-radius: 100%;
|
justify-content: center;
|
||||||
left: 50%;
|
|
||||||
opacity: 50%;
|
|
||||||
padding: 0.5em;
|
|
||||||
position: absolute;
|
position: absolute;
|
||||||
transform: translateX(-50%);
|
z-index: 1;
|
||||||
z-index: 2;
|
flex-direction: column;
|
||||||
|
bottom: -75px;
|
||||||
|
background: none;
|
||||||
|
opacity: 0;
|
||||||
|
transition: opacity 0.25s ease, transform 0.5s ease;
|
||||||
|
transform: scale(0.1);
|
||||||
|
padding: 0;
|
||||||
|
|
||||||
&:hover {
|
> * {
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover,
|
||||||
|
&:active,
|
||||||
|
&:focus {
|
||||||
|
background: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.visible {
|
||||||
|
transform: translateY(-75px) scale(1);
|
||||||
|
opacity: 0.8;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__text {
|
||||||
|
color: var(--secondary);
|
||||||
|
padding: 0.5rem;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
background: var(--primary-medium);
|
background: var(--primary-medium);
|
||||||
opacity: 100%;
|
border-radius: 3px;
|
||||||
|
text-align: center;
|
||||||
|
font-size: var(--font-down-1);
|
||||||
|
bottom: 40px;
|
||||||
|
position: absolute;
|
||||||
}
|
}
|
||||||
|
|
||||||
.d-icon {
|
&__arrow {
|
||||||
color: var(--primary);
|
display: flex;
|
||||||
margin: 0;
|
background: var(--primary-medium);
|
||||||
}
|
border-radius: 100%;
|
||||||
|
align-items: center;
|
||||||
&.unread-messages {
|
justify-content: center;
|
||||||
opacity: 85%;
|
height: 32px;
|
||||||
border-radius: 0;
|
width: 32px;
|
||||||
transition: border-radius 0.1s linear;
|
position: relative;
|
||||||
|
|
||||||
&:hover {
|
|
||||||
opacity: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.d-icon {
|
.d-icon {
|
||||||
margin: 0 0 0 0.5em;
|
color: var(--secondary);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
opacity: 1;
|
||||||
|
|
||||||
|
.chat-scroll-to-bottom__arrow {
|
||||||
|
.d-icon {
|
||||||
|
color: var(--secondary);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
.chat-composer-container {
|
.chat-composer-container {
|
||||||
.chat-composer {
|
.chat-composer {
|
||||||
margin: 0.25rem 10px 0 10px;
|
margin: 0.25rem 5px 0 5px;
|
||||||
}
|
}
|
||||||
html.keyboard-visible .footer-nav-ipad & {
|
html.keyboard-visible .footer-nav-ipad & {
|
||||||
margin: 0.25rem 10px 1rem 10px;
|
margin: 0.25rem 10px 1rem 10px;
|
||||||
|
|
|
@ -53,6 +53,25 @@
|
||||||
|
|
||||||
.chat-message.user-info-hidden {
|
.chat-message.user-info-hidden {
|
||||||
padding: 0.15em 1em;
|
padding: 0.15em 1em;
|
||||||
|
|
||||||
|
.chat-time {
|
||||||
|
color: var(--secondary-medium);
|
||||||
|
flex-shrink: 0;
|
||||||
|
font-size: var(--font-down-2);
|
||||||
|
margin-top: 0.4em;
|
||||||
|
display: none;
|
||||||
|
width: var(--message-left-width);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
.chat-message-left-gutter__bookmark {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-time {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Full Page Styling in Core
|
// Full Page Styling in Core
|
||||||
|
|
|
@ -22,6 +22,8 @@
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
|
|
||||||
.selected-message-reply {
|
.selected-message-reply {
|
||||||
|
margin-left: 5px;
|
||||||
|
|
||||||
&:not(.is-expanded) {
|
&:not(.is-expanded) {
|
||||||
@include ellipsis;
|
@include ellipsis;
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,7 +4,3 @@
|
||||||
.replying-text {
|
.replying-text {
|
||||||
@include unselectable;
|
@include unselectable;
|
||||||
}
|
}
|
||||||
|
|
||||||
.chat-message-container {
|
|
||||||
transform: translateZ(0);
|
|
||||||
}
|
|
||||||
|
|
|
@ -108,7 +108,7 @@ en:
|
||||||
in_reply_to: "In reply to"
|
in_reply_to: "In reply to"
|
||||||
heading: "Chat"
|
heading: "Chat"
|
||||||
join: "Join"
|
join: "Join"
|
||||||
new_messages: "new messages"
|
last_visit: "last visit"
|
||||||
mention_warning:
|
mention_warning:
|
||||||
dismiss: "dismiss"
|
dismiss: "dismiss"
|
||||||
cannot_see: "%{username} can't access this channel and was not notified."
|
cannot_see: "%{username} can't access this channel and was not notified."
|
||||||
|
|
|
@ -247,6 +247,7 @@ after_initialize do
|
||||||
load File.expand_path("../app/controllers/api/hints_controller.rb", __FILE__)
|
load File.expand_path("../app/controllers/api/hints_controller.rb", __FILE__)
|
||||||
load File.expand_path("../app/controllers/api/chat_channel_threads_controller.rb", __FILE__)
|
load File.expand_path("../app/controllers/api/chat_channel_threads_controller.rb", __FILE__)
|
||||||
load File.expand_path("../app/controllers/api/chat_chatables_controller.rb", __FILE__)
|
load File.expand_path("../app/controllers/api/chat_chatables_controller.rb", __FILE__)
|
||||||
|
load File.expand_path("../app/queries/chat_channel_unreads_query.rb", __FILE__)
|
||||||
load File.expand_path("../app/queries/chat_channel_memberships_query.rb", __FILE__)
|
load File.expand_path("../app/queries/chat_channel_memberships_query.rb", __FILE__)
|
||||||
|
|
||||||
if Discourse.allow_dev_populate?
|
if Discourse.allow_dev_populate?
|
||||||
|
|
|
@ -17,7 +17,7 @@ describe ChatChannelMembershipsQuery do
|
||||||
|
|
||||||
context "when no memberships exists" do
|
context "when no memberships exists" do
|
||||||
it "returns an empty array" do
|
it "returns an empty array" do
|
||||||
expect(described_class.call(channel_1)).to eq([])
|
expect(described_class.call(channel: channel_1)).to eq([])
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -28,7 +28,7 @@ describe ChatChannelMembershipsQuery do
|
||||||
end
|
end
|
||||||
|
|
||||||
it "returns the memberships" do
|
it "returns the memberships" do
|
||||||
memberships = described_class.call(channel_1)
|
memberships = described_class.call(channel: channel_1)
|
||||||
|
|
||||||
expect(memberships.pluck(:user_id)).to contain_exactly(user_1.id, user_2.id)
|
expect(memberships.pluck(:user_id)).to contain_exactly(user_1.id, user_2.id)
|
||||||
end
|
end
|
||||||
|
@ -49,7 +49,7 @@ describe ChatChannelMembershipsQuery do
|
||||||
end
|
end
|
||||||
|
|
||||||
it "lists the user" do
|
it "lists the user" do
|
||||||
memberships = described_class.call(channel_1)
|
memberships = described_class.call(channel: channel_1)
|
||||||
|
|
||||||
expect(memberships.pluck(:user_id)).to include(user_1.id)
|
expect(memberships.pluck(:user_id)).to include(user_1.id)
|
||||||
end
|
end
|
||||||
|
@ -62,14 +62,16 @@ describe ChatChannelMembershipsQuery do
|
||||||
permission_type: CategoryGroup.permission_types[:full],
|
permission_type: CategoryGroup.permission_types[:full],
|
||||||
)
|
)
|
||||||
|
|
||||||
expect(described_class.call(channel_1).pluck(:user_id)).to contain_exactly(user_1.id)
|
expect(described_class.call(channel: channel_1).pluck(:user_id)).to contain_exactly(
|
||||||
|
user_1.id,
|
||||||
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
it "returns the membership if the user still has access through a staff group" do
|
it "returns the membership if the user still has access through a staff group" do
|
||||||
chatters_group.remove(user_1)
|
chatters_group.remove(user_1)
|
||||||
Group.find_by(id: Group::AUTO_GROUPS[:staff]).add(user_1)
|
Group.find_by(id: Group::AUTO_GROUPS[:staff]).add(user_1)
|
||||||
|
|
||||||
memberships = described_class.call(channel_1)
|
memberships = described_class.call(channel: channel_1)
|
||||||
|
|
||||||
expect(memberships.pluck(:user_id)).to include(user_1.id)
|
expect(memberships.pluck(:user_id)).to include(user_1.id)
|
||||||
end
|
end
|
||||||
|
@ -77,7 +79,7 @@ describe ChatChannelMembershipsQuery do
|
||||||
|
|
||||||
context "when membership doesn’t exist" do
|
context "when membership doesn’t exist" do
|
||||||
it "doesn’t list the user" do
|
it "doesn’t list the user" do
|
||||||
memberships = described_class.call(channel_1)
|
memberships = described_class.call(channel: channel_1)
|
||||||
|
|
||||||
expect(memberships.pluck(:user_id)).to be_empty
|
expect(memberships.pluck(:user_id)).to be_empty
|
||||||
end
|
end
|
||||||
|
@ -91,7 +93,7 @@ describe ChatChannelMembershipsQuery do
|
||||||
end
|
end
|
||||||
|
|
||||||
it "doesn’t list the user" do
|
it "doesn’t list the user" do
|
||||||
memberships = described_class.call(channel_1)
|
memberships = described_class.call(channel: channel_1)
|
||||||
|
|
||||||
expect(memberships).to be_empty
|
expect(memberships).to be_empty
|
||||||
end
|
end
|
||||||
|
@ -99,7 +101,7 @@ describe ChatChannelMembershipsQuery do
|
||||||
|
|
||||||
context "when membership doesn’t exist" do
|
context "when membership doesn’t exist" do
|
||||||
it "doesn’t list the user" do
|
it "doesn’t list the user" do
|
||||||
memberships = described_class.call(channel_1)
|
memberships = described_class.call(channel: channel_1)
|
||||||
|
|
||||||
expect(memberships).to be_empty
|
expect(memberships).to be_empty
|
||||||
end
|
end
|
||||||
|
@ -114,7 +116,7 @@ describe ChatChannelMembershipsQuery do
|
||||||
end
|
end
|
||||||
|
|
||||||
it "returns an empty array" do
|
it "returns an empty array" do
|
||||||
expect(described_class.call(channel_1)).to eq([])
|
expect(described_class.call(channel: channel_1)).to eq([])
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -122,7 +124,7 @@ describe ChatChannelMembershipsQuery do
|
||||||
fab!(:channel_1) { Fabricate(:direct_message_channel, users: [user_1, user_2]) }
|
fab!(:channel_1) { Fabricate(:direct_message_channel, users: [user_1, user_2]) }
|
||||||
|
|
||||||
it "returns the memberships" do
|
it "returns the memberships" do
|
||||||
memberships = described_class.call(channel_1)
|
memberships = described_class.call(channel: channel_1)
|
||||||
|
|
||||||
expect(memberships.pluck(:user_id)).to contain_exactly(user_1.id, user_2.id)
|
expect(memberships.pluck(:user_id)).to contain_exactly(user_1.id, user_2.id)
|
||||||
end
|
end
|
||||||
|
@ -139,7 +141,7 @@ describe ChatChannelMembershipsQuery do
|
||||||
|
|
||||||
describe "offset param" do
|
describe "offset param" do
|
||||||
it "offsets the results" do
|
it "offsets the results" do
|
||||||
memberships = described_class.call(channel_1, offset: 1)
|
memberships = described_class.call(channel: channel_1, offset: 1)
|
||||||
|
|
||||||
expect(memberships.length).to eq(1)
|
expect(memberships.length).to eq(1)
|
||||||
end
|
end
|
||||||
|
@ -147,7 +149,7 @@ describe ChatChannelMembershipsQuery do
|
||||||
|
|
||||||
describe "limit param" do
|
describe "limit param" do
|
||||||
it "limits the results" do
|
it "limits the results" do
|
||||||
memberships = described_class.call(channel_1, limit: 1)
|
memberships = described_class.call(channel: channel_1, limit: 1)
|
||||||
|
|
||||||
expect(memberships.length).to eq(1)
|
expect(memberships.length).to eq(1)
|
||||||
end
|
end
|
||||||
|
@ -163,7 +165,7 @@ describe ChatChannelMembershipsQuery do
|
||||||
end
|
end
|
||||||
|
|
||||||
it "filters the results" do
|
it "filters the results" do
|
||||||
memberships = described_class.call(channel_1, username: user_1.username)
|
memberships = described_class.call(channel: channel_1, username: user_1.username)
|
||||||
|
|
||||||
expect(memberships.length).to eq(1)
|
expect(memberships.length).to eq(1)
|
||||||
expect(memberships[0].user).to eq(user_1)
|
expect(memberships[0].user).to eq(user_1)
|
||||||
|
@ -182,7 +184,7 @@ describe ChatChannelMembershipsQuery do
|
||||||
before { SiteSetting.prioritize_username_in_ux = true }
|
before { SiteSetting.prioritize_username_in_ux = true }
|
||||||
|
|
||||||
it "is using ascending order on username" do
|
it "is using ascending order on username" do
|
||||||
memberships = described_class.call(channel_1)
|
memberships = described_class.call(channel: channel_1)
|
||||||
|
|
||||||
expect(memberships[0].user).to eq(user_1)
|
expect(memberships[0].user).to eq(user_1)
|
||||||
expect(memberships[1].user).to eq(user_2)
|
expect(memberships[1].user).to eq(user_2)
|
||||||
|
@ -193,7 +195,7 @@ describe ChatChannelMembershipsQuery do
|
||||||
before { SiteSetting.prioritize_username_in_ux = false }
|
before { SiteSetting.prioritize_username_in_ux = false }
|
||||||
|
|
||||||
it "is using ascending order on name" do
|
it "is using ascending order on name" do
|
||||||
memberships = described_class.call(channel_1)
|
memberships = described_class.call(channel: channel_1)
|
||||||
|
|
||||||
expect(memberships[0].user).to eq(user_2)
|
expect(memberships[0].user).to eq(user_2)
|
||||||
expect(memberships[1].user).to eq(user_1)
|
expect(memberships[1].user).to eq(user_1)
|
||||||
|
@ -203,7 +205,7 @@ describe ChatChannelMembershipsQuery do
|
||||||
before { SiteSetting.enable_names = false }
|
before { SiteSetting.enable_names = false }
|
||||||
|
|
||||||
it "is using ascending order on username" do
|
it "is using ascending order on username" do
|
||||||
memberships = described_class.call(channel_1)
|
memberships = described_class.call(channel: channel_1)
|
||||||
|
|
||||||
expect(memberships[0].user).to eq(user_1)
|
expect(memberships[0].user).to eq(user_1)
|
||||||
expect(memberships[1].user).to eq(user_2)
|
expect(memberships[1].user).to eq(user_2)
|
||||||
|
@ -222,7 +224,7 @@ describe ChatChannelMembershipsQuery do
|
||||||
end
|
end
|
||||||
|
|
||||||
it "doesn’t list staged users" do
|
it "doesn’t list staged users" do
|
||||||
memberships = described_class.call(channel_1)
|
memberships = described_class.call(channel: channel_1)
|
||||||
expect(memberships).to be_blank
|
expect(memberships).to be_blank
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -242,7 +244,7 @@ describe ChatChannelMembershipsQuery do
|
||||||
end
|
end
|
||||||
|
|
||||||
it "doesn’t list suspended users" do
|
it "doesn’t list suspended users" do
|
||||||
memberships = described_class.call(channel_1)
|
memberships = described_class.call(channel: channel_1)
|
||||||
expect(memberships).to be_blank
|
expect(memberships).to be_blank
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -260,7 +262,7 @@ describe ChatChannelMembershipsQuery do
|
||||||
end
|
end
|
||||||
|
|
||||||
it "doesn’t list inactive users" do
|
it "doesn’t list inactive users" do
|
||||||
memberships = described_class.call(channel_1)
|
memberships = described_class.call(channel: channel_1)
|
||||||
expect(memberships).to be_blank
|
expect(memberships).to be_blank
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -0,0 +1,51 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require "rails_helper"
|
||||||
|
|
||||||
|
describe ChatChannelUnreadsQuery do
|
||||||
|
fab!(:channel_1) { Fabricate(:category_channel) }
|
||||||
|
fab!(:current_user) { Fabricate(:user) }
|
||||||
|
|
||||||
|
before do
|
||||||
|
SiteSetting.chat_enabled = true
|
||||||
|
SiteSetting.chat_allowed_groups = Group::AUTO_GROUPS[:everyone]
|
||||||
|
channel_1.add(current_user)
|
||||||
|
end
|
||||||
|
|
||||||
|
context "with unread message" do
|
||||||
|
it "returns a correct unread count" do
|
||||||
|
Fabricate(:chat_message, chat_channel: channel_1)
|
||||||
|
|
||||||
|
expect(described_class.call(channel_id: channel_1.id, user_id: current_user.id)).to eq(
|
||||||
|
{ mention_count: 0, unread_count: 1 },
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context "with unread mentions" do
|
||||||
|
before { Jobs.run_immediately! }
|
||||||
|
|
||||||
|
it "returns a correct unread mention" do
|
||||||
|
message = Fabricate(:chat_message)
|
||||||
|
notification =
|
||||||
|
Notification.create!(
|
||||||
|
notification_type: Notification.types[:chat_mention],
|
||||||
|
user_id: current_user.id,
|
||||||
|
data: { chat_message_id: message.id, chat_channel_id: channel_1.id }.to_json,
|
||||||
|
)
|
||||||
|
ChatMention.create!(notification: notification, user: current_user, chat_message: message)
|
||||||
|
|
||||||
|
expect(described_class.call(channel_id: channel_1.id, user_id: current_user.id)).to eq(
|
||||||
|
{ mention_count: 1, unread_count: 0 },
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context "with nothing unread" do
|
||||||
|
it "returns a correct state" do
|
||||||
|
expect(described_class.call(channel_id: channel_1.id, user_id: current_user.id)).to eq(
|
||||||
|
{ mention_count: 0, unread_count: 0 },
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -126,15 +126,17 @@ RSpec.describe Chat::ChatController do
|
||||||
it "correctly marks reactions as 'reacted' for the current_user" do
|
it "correctly marks reactions as 'reacted' for the current_user" do
|
||||||
heart_emoji = ":heart:"
|
heart_emoji = ":heart:"
|
||||||
smile_emoji = ":smile"
|
smile_emoji = ":smile"
|
||||||
|
|
||||||
last_message = chat_channel.chat_messages.last
|
last_message = chat_channel.chat_messages.last
|
||||||
last_message.reactions.create(user: user, emoji: heart_emoji)
|
last_message.reactions.create(user: user, emoji: heart_emoji)
|
||||||
last_message.reactions.create(user: admin, emoji: smile_emoji)
|
last_message.reactions.create(user: admin, emoji: smile_emoji)
|
||||||
|
|
||||||
get "/chat/#{chat_channel.id}/messages.json", params: { page_size: page_size }
|
get "/chat/#{chat_channel.id}/messages.json", params: { page_size: page_size }
|
||||||
|
|
||||||
reactions = response.parsed_body["chat_messages"].last["reactions"]
|
reactions = response.parsed_body["chat_messages"].last["reactions"]
|
||||||
expect(reactions[heart_emoji]["reacted"]).to be true
|
heart_reaction = reactions.find { |r| r["emoji"] == heart_emoji }
|
||||||
expect(reactions[smile_emoji]["reacted"]).to be false
|
expect(heart_reaction["reacted"]).to be true
|
||||||
|
smile_reaction = reactions.find { |r| r["emoji"] == smile_emoji }
|
||||||
|
expect(smile_reaction["reacted"]).to be false
|
||||||
end
|
end
|
||||||
|
|
||||||
it "sends the last message bus id for the channel" do
|
it "sends the last message bus id for the channel" do
|
||||||
|
|
|
@ -21,12 +21,14 @@ describe ChatMessageSerializer do
|
||||||
it "doesn’t return the reaction" do
|
it "doesn’t return the reaction" do
|
||||||
Emoji.clear_cache
|
Emoji.clear_cache
|
||||||
|
|
||||||
expect(subject.as_json[:reactions]["trout"]).to be_present
|
trout_reaction = subject.as_json[:reactions].find { |r| r[:emoji] == "trout" }
|
||||||
|
expect(trout_reaction).to be_present
|
||||||
|
|
||||||
custom_emoji.destroy!
|
custom_emoji.destroy!
|
||||||
Emoji.clear_cache
|
Emoji.clear_cache
|
||||||
|
|
||||||
expect(subject.as_json[:reactions]["trout"]).to_not be_present
|
trout_reaction = subject.as_json[:reactions].find { |r| r[:emoji] == "trout" }
|
||||||
|
expect(trout_reaction).to_not be_present
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -37,7 +37,7 @@ RSpec.describe "Chat channel", type: :system, js: true do
|
||||||
chat.visit_channel(channel_1)
|
chat.visit_channel(channel_1)
|
||||||
expect(channel).to have_no_loading_skeleton
|
expect(channel).to have_no_loading_skeleton
|
||||||
channel.send_message("aaaaaaaaaaaaaaaaaaaa")
|
channel.send_message("aaaaaaaaaaaaaaaaaaaa")
|
||||||
expect(page).to have_no_css("[data-staged-id]")
|
expect(page).to have_no_css(".chat-message-staged")
|
||||||
last_message = find(".chat-message-container:last-child")
|
last_message = find(".chat-message-container:last-child")
|
||||||
last_message.hover
|
last_message.hover
|
||||||
|
|
||||||
|
@ -183,7 +183,7 @@ RSpec.describe "Chat channel", type: :system, js: true do
|
||||||
it "shows a date separator" do
|
it "shows a date separator" do
|
||||||
chat.visit_channel(channel_1)
|
chat.visit_channel(channel_1)
|
||||||
|
|
||||||
expect(page).to have_selector(".first-daily-message", text: "Today")
|
expect(page).to have_selector(".chat-message-separator__text", text: "Today")
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -81,6 +81,7 @@ RSpec.describe "Create channel", type: :system, js: true do
|
||||||
chat_page.visit_browse
|
chat_page.visit_browse
|
||||||
chat_page.new_channel_button.click
|
chat_page.new_channel_button.click
|
||||||
channel_modal.select_category(private_category_1)
|
channel_modal.select_category(private_category_1)
|
||||||
|
expect(page).to have_no_css(".loading-permissions")
|
||||||
|
|
||||||
expect(channel_modal.create_channel_hint["innerHTML"].strip).to include(
|
expect(channel_modal.create_channel_hint["innerHTML"].strip).to include(
|
||||||
"<script>e</script>",
|
"<script>e</script>",
|
||||||
|
|
|
@ -18,7 +18,7 @@ RSpec.describe "Deleted message", type: :system, js: true do
|
||||||
chat_page.visit_channel(channel_1)
|
chat_page.visit_channel(channel_1)
|
||||||
expect(channel_page).to have_no_loading_skeleton
|
expect(channel_page).to have_no_loading_skeleton
|
||||||
channel_page.send_message("aaaaaaaaaaaaaaaaaaaa")
|
channel_page.send_message("aaaaaaaaaaaaaaaaaaaa")
|
||||||
expect(page).to have_no_css("[data-staged-id]")
|
expect(page).to have_no_css(".chat-message-staged")
|
||||||
last_message = find(".chat-message-container:last-child")
|
last_message = find(".chat-message-container:last-child")
|
||||||
channel_page.delete_message(OpenStruct.new(id: last_message["data-id"]))
|
channel_page.delete_message(OpenStruct.new(id: last_message["data-id"]))
|
||||||
|
|
||||||
|
|
|
@ -3,6 +3,7 @@
|
||||||
RSpec.describe "Drawer", type: :system, js: true do
|
RSpec.describe "Drawer", type: :system, js: true do
|
||||||
fab!(:current_user) { Fabricate(:admin) }
|
fab!(:current_user) { Fabricate(:admin) }
|
||||||
let(:chat_page) { PageObjects::Pages::Chat.new }
|
let(:chat_page) { PageObjects::Pages::Chat.new }
|
||||||
|
let(:channel_page) { PageObjects::Pages::ChatChannel.new }
|
||||||
let(:drawer) { PageObjects::Pages::ChatDrawer.new }
|
let(:drawer) { PageObjects::Pages::ChatDrawer.new }
|
||||||
|
|
||||||
before do
|
before do
|
||||||
|
@ -52,4 +53,39 @@ RSpec.describe "Drawer", type: :system, js: true do
|
||||||
expect(page.find(".chat-drawer").native.style("height")).to eq("530px")
|
expect(page.find(".chat-drawer").native.style("height")).to eq("530px")
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
context "when going from drawer to full page" do
|
||||||
|
fab!(:channel_1) { Fabricate(:chat_channel) }
|
||||||
|
fab!(:channel_2) { Fabricate(:chat_channel) }
|
||||||
|
fab!(:user_1) { Fabricate(:user) }
|
||||||
|
|
||||||
|
before do
|
||||||
|
channel_1.add(current_user)
|
||||||
|
channel_2.add(current_user)
|
||||||
|
channel_1.add(user_1)
|
||||||
|
channel_2.add(user_1)
|
||||||
|
end
|
||||||
|
|
||||||
|
it "correctly resets subscriptions" do
|
||||||
|
visit("/")
|
||||||
|
|
||||||
|
chat_page.open_from_header
|
||||||
|
drawer.maximize
|
||||||
|
chat_page.minimize_full_page
|
||||||
|
drawer.maximize
|
||||||
|
|
||||||
|
using_session("user_1") do |session|
|
||||||
|
sign_in(user_1)
|
||||||
|
chat_page.visit_channel(channel_1)
|
||||||
|
channel_page.send_message("onlyonce")
|
||||||
|
session.quit
|
||||||
|
end
|
||||||
|
|
||||||
|
expect(page).to have_content("onlyonce", count: 1)
|
||||||
|
|
||||||
|
chat_page.visit_channel(channel_2)
|
||||||
|
|
||||||
|
expect(page).to have_content("onlyonce", count: 0)
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -32,7 +32,7 @@ RSpec.describe "Flag message", type: :system, js: true do
|
||||||
|
|
||||||
context "when direct message channel" do
|
context "when direct message channel" do
|
||||||
fab!(:dm_channel_1) { Fabricate(:direct_message_channel, users: [current_user]) }
|
fab!(:dm_channel_1) { Fabricate(:direct_message_channel, users: [current_user]) }
|
||||||
fab!(:message_1) { Fabricate(:chat_message, chat_channel: dm_channel_1, user: current_user) }
|
fab!(:message_1) { Fabricate(:chat_message, chat_channel: dm_channel_1) }
|
||||||
|
|
||||||
it "doesn’t allow to flag a message" do
|
it "doesn’t allow to flag a message" do
|
||||||
chat.visit_channel(dm_channel_1)
|
chat.visit_channel(dm_channel_1)
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue