mirror of
https://github.com/discourse/discourse.git
synced 2025-03-09 14:34:35 +00:00
DEV: rework the chat-live-pane
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
e206bd8907
commit
67c0498f64
plugins/chat
app
controllers/api
models
queries
serializers
services
assets
javascripts/discourse
components
channels-list.jschat-channel-metadata.hbschat-channel-metadata.jschat-channel-preview-card.hbschat-channel-preview-card.jschat-channel-selector-modal-inner.jschat-composer-dropdown.hbschat-composer-dropdown.jschat-composer-uploads.jschat-composer.hbschat-composer.jschat-draft-channel-screen.hbs
chat-drawer
chat-emoji-picker.jschat-full-page-header.hbschat-full-page-header.jschat-live-pane.hbschat-live-pane.jschat-message-actions-desktop.hbschat-message-actions-desktop.jschat-message-actions-mobile.hbschat-message-avatar.hbschat-message-avatar.jschat-message-collapser.hbschat-message-collapser.jschat-message-in-reply-to-indicator.hbschat-message-in-reply-to-indicator.jschat-message-info.hbschat-message-info.jschat-message-left-gutter.hbschat-message-left-gutter.jschat-message-move-to-channel-modal-inner.hbschat-message-reaction.hbschat-message-reaction.jschat-message-separator-date.hbschat-message-separator-new.hbschat-message-separator.hbschat-message-separator.jschat-message-text.hbschat-message-text.jschat-message.hbschat-message.jschat-retention-reminder.hbschat-retention-reminder.jschat-scroll-to-bottom-arrow.hbschat-selection-manager.jschat-skeleton.hbschat-skeleton.jsfull-page-chat.hbsfull-page-chat.jscontrollers
helpers
initializers
lib
models
modifiers/chat
routes
services
chat-api.jschat-channels-manager.jschat-message-visibility-observer.jschat-subscriptions-manager.jschat.js
templates/modal
stylesheets
config/locales
plugin.rbspec
@ -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
|
||||||
|
40
plugins/chat/app/queries/chat_channel_unreads_query.rb
Normal file
40
plugins/chat/app/queries/chat_channel_unreads_query.rb
Normal file
@ -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) {
|
||||||
@ -307,7 +304,9 @@ export default Component.extend(TextareaTextManipulation, {
|
|||||||
|
|
||||||
@bind
|
@bind
|
||||||
_captureMentions() {
|
_captureMentions() {
|
||||||
this.chatComposerWarningsTracker.trackMentions(this.value);
|
if (this.value) {
|
||||||
|
this.chatComposerWarningsTracker.trackMentions(this.value);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
@bind
|
@bind
|
||||||
|
@ -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,115 @@
|
|||||||
{{#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 @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);
|
||||||
|
19
plugins/chat/assets/javascripts/discourse/components/chat-message-in-reply-to-indicator.hbs
Normal file
19
plugins/chat/assets/javascripts/discourse/components/chat-message-in-reply-to-indicator.hbs
Normal file
@ -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}}
|
32
plugins/chat/assets/javascripts/discourse/components/chat-message-in-reply-to-indicator.js
Normal file
32
plugins/chat/assets/javascripts/discourse/components/chat-message-in-reply-to-indicator.js
Normal file
@ -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,22 @@
|
|||||||
{{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 @selectingMessages "selecting-messages")
|
(if @selectingMessages "selecting-messages")
|
||||||
}}
|
}}
|
||||||
data-id={{or @message.id @message.stagedId}}
|
data-id={{@message.id}}
|
||||||
data-staged-id={{if @message.staged @message.stagedId}}
|
data-staged-id={{if @message.staged @message.stagedId}}
|
||||||
|
{{chat/track-message
|
||||||
|
(fn @didShowMessage @message)
|
||||||
|
(fn @didHideMessage @message)
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
{{#if this.show}}
|
{{#if this.show}}
|
||||||
{{#if @selectingMessages}}
|
{{#if @selectingMessages}}
|
||||||
@ -85,35 +89,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 +117,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 +125,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 +170,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,29 @@ 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");
|
const stagedId = this.args.message?.stagedId;
|
||||||
|
if (stagedId) {
|
||||||
this.appEvents.on(
|
return document.querySelector(
|
||||||
`chat-message-${this.args.message.id}:reaction`,
|
`.chat-message-container[data-staged-id='${stagedId}']`
|
||||||
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 +218,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 +278,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 +425,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 +440,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 +478,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 +500,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 +522,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 +561,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 +572,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 +584,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 +599,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 +614,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 +622,7 @@ export default class ChatMessage extends Component {
|
|||||||
);
|
);
|
||||||
},
|
},
|
||||||
onAfterDelete: () => {
|
onAfterDelete: () => {
|
||||||
this.args.message.set("bookmark", null);
|
this.args.message.bookmark = null;
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
@ -736,7 +631,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 +641,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 +650,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 +675,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 +688,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?.needs_dm_retention_reminder) ||
|
||||||
(this.chatChannel.isCategoryChannel &&
|
(this.args.channel?.isCategoryChannel &&
|
||||||
this.currentUser.needs_channel_retention_reminder))
|
this.currentUser?.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}}
|
||||||
|
>
|
||||||
|
{{#if @hasNewMessages}}
|
||||||
|
<span class="chat-scroll-to-bottom__text">
|
||||||
|
{{i18n "chat.scroll_to_new_messages"}}
|
||||||
|
</span>
|
||||||
|
{{/if}}
|
||||||
|
|
||||||
|
<span class="chat-scroll-to-bottom__arrow">
|
||||||
|
{{d-icon "arrow-down"}}
|
||||||
|
</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,57 @@ 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
appendMessages(messages) {
|
||||||
|
this.messages.pushObjects(messages);
|
||||||
|
}
|
||||||
|
|
||||||
|
prependMessages(messages) {
|
||||||
|
this.messages.unshiftObjects(messages);
|
||||||
|
}
|
||||||
|
|
||||||
|
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.stagedId === 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 +189,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 +208,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,193 @@
|
|||||||
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.staged_id = guid();
|
||||||
|
return new ChatMessage(channel, args);
|
||||||
|
}
|
||||||
|
|
||||||
_initReactions(args) {
|
@tracked id;
|
||||||
args.reactions = EmberObject.create(args.reactions || {});
|
@tracked error;
|
||||||
},
|
@tracked selected;
|
||||||
|
@tracked channel;
|
||||||
|
@tracked stagedId;
|
||||||
|
@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;
|
||||||
|
|
||||||
_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.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.stagedId = args.staged_id;
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
get staged() {
|
||||||
|
return this.stagedId?.length > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
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()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
35
plugins/chat/assets/javascripts/discourse/modifiers/chat/track-message-separator-date.js
Normal file
35
plugins/chat/assets/javascripts/discourse/modifiers/chat/track-message-separator-date.js
Normal file
@ -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: 1;
|
||||||
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 {
|
||||||
@ -57,13 +61,11 @@
|
|||||||
background-color: var(--secondary);
|
background-color: var(--secondary);
|
||||||
display: flex;
|
display: flex;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
|
content-visibility: auto;
|
||||||
|
contain-intrinsic-size: auto 200px;
|
||||||
|
|
||||||
.chat-message-reaction {
|
.chat-message-reaction {
|
||||||
@include chat-reaction;
|
@include chat-reaction;
|
||||||
|
|
||||||
&:not(.show) {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
&.chat-action {
|
&.chat-action {
|
||||||
@ -86,17 +88,6 @@
|
|||||||
transition: 2s linear background-color;
|
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 +245,10 @@
|
|||||||
|
|
||||||
.chat-message.chat-message-bookmarked {
|
.chat-message.chat-message-bookmarked {
|
||||||
background: var(--highlight-bg);
|
background: var(--highlight-bg);
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: var(--highlight-medium);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.not-mobile-device & .chat-message-reaction-list .chat-message-react-btn {
|
.not-mobile-device & .chat-message-reaction-list .chat-message-react-btn {
|
||||||
@ -284,7 +279,6 @@
|
|||||||
font-style: italic;
|
font-style: italic;
|
||||||
}
|
}
|
||||||
|
|
||||||
.chat-message-container.is-hovered,
|
|
||||||
.chat-message.chat-message-selected {
|
.chat-message.chat-message-selected {
|
||||||
background: var(--primary-very-low);
|
background: var(--primary-very-low);
|
||||||
}
|
}
|
||||||
|
@ -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-reactions {
|
||||||
|
display: flex;
|
||||||
|
padding: 5px 0 0 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__message-reaction {
|
||||||
|
background-color: var(--primary-100);
|
||||||
|
width: 32px;
|
||||||
|
height: 18px;
|
||||||
|
border-radius: $radius;
|
||||||
|
|
||||||
|
& + & {
|
||||||
|
margin-left: 0.5rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__message-text {
|
||||||
|
display: flex;
|
||||||
|
padding: 5px 0;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
&__message-msg {
|
&__message-msg {
|
||||||
height: 13px;
|
height: 13px;
|
||||||
border-radius: $radius;
|
border-radius: $radius;
|
||||||
margin: 5px 0;
|
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,65 @@ $float-height: 530px;
|
|||||||
}
|
}
|
||||||
|
|
||||||
.chat-scroll-to-bottom {
|
.chat-scroll-to-bottom {
|
||||||
background: var(--primary-medium);
|
left: calc(50% - calc(45px / 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: 5px;
|
||||||
|
|
||||||
&: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);
|
||||||
}
|
}
|
||||||
|
|
||||||
.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: 35px;
|
||||||
border-radius: 0;
|
width: 35px;
|
||||||
transition: border-radius 0.1s linear;
|
|
||||||
|
|
||||||
&: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
|
||||||
|
51
plugins/chat/spec/queries/chat_channel_unreads_query_spec.rb
Normal file
51
plugins/chat/spec/queries/chat_channel_unreads_query_spec.rb
Normal file
@ -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
|
||||||
|
@ -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>",
|
||||||
|
@ -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)
|
||||||
|
60
plugins/chat/spec/system/message_user_info.rb
Normal file
60
plugins/chat/spec/system/message_user_info.rb
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
RSpec.describe "Sticky date", type: :system, js: true do
|
||||||
|
fab!(:current_user) { Fabricate(:user) }
|
||||||
|
fab!(:channel_1) { Fabricate(:category_channel) }
|
||||||
|
|
||||||
|
let(:chat_page) { PageObjects::Pages::Chat.new }
|
||||||
|
|
||||||
|
before do
|
||||||
|
chat_system_bootstrap
|
||||||
|
sign_in(current_user)
|
||||||
|
end
|
||||||
|
|
||||||
|
context "when previous message is from a different user" do
|
||||||
|
fab!(:message_1) { Fabricate(:chat_message, chat_channel: channel_1) }
|
||||||
|
fab!(:message_2) { Fabricate(:chat_message, chat_channel: channel_1) }
|
||||||
|
|
||||||
|
it "shows user info on the message" do
|
||||||
|
chat_page.visit_channel(channel_1)
|
||||||
|
|
||||||
|
expect(page.find("[data-id='#{message_2.id}']")).to have_css(".chat-message-avatar")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context "when previous message is from the same user" do
|
||||||
|
fab!(:message_1) { Fabricate(:chat_message, chat_channel: channel_1, user: current_user) }
|
||||||
|
fab!(:message_2) { Fabricate(:chat_message, chat_channel: channel_1, user: current_user) }
|
||||||
|
|
||||||
|
it "doesn’t show user info on the message" do
|
||||||
|
chat_page.visit_channel(channel_1)
|
||||||
|
|
||||||
|
expect(page.find("[data-id='#{message_2.id}']")).to have_no_css(".chat-message-avatar")
|
||||||
|
end
|
||||||
|
|
||||||
|
context "when previous message is old" do
|
||||||
|
fab!(:message_1) do
|
||||||
|
Fabricate(
|
||||||
|
:chat_message,
|
||||||
|
chat_channel: channel_1,
|
||||||
|
user: current_user,
|
||||||
|
created_at: DateTime.parse("2018-11-10 17:00"),
|
||||||
|
)
|
||||||
|
end
|
||||||
|
fab!(:message_2) do
|
||||||
|
Fabricate(
|
||||||
|
:chat_message,
|
||||||
|
chat_channel: channel_1,
|
||||||
|
user: current_user,
|
||||||
|
created_at: DateTime.parse("2018-11-10 17:30"),
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
it "shows user info on the message" do
|
||||||
|
chat_page.visit_channel(channel_1)
|
||||||
|
|
||||||
|
expect(page.find("[data-id='#{message_2.id}']")).to have_no_css(".chat-message-avatar")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
@ -60,8 +60,9 @@ RSpec.describe "Navigating to message", type: :system, js: true do
|
|||||||
|
|
||||||
it "highlights the correct message after using the bottom arrow" do
|
it "highlights the correct message after using the bottom arrow" do
|
||||||
chat_page.visit_channel(channel_1)
|
chat_page.visit_channel(channel_1)
|
||||||
|
|
||||||
click_link(link)
|
click_link(link)
|
||||||
click_link(I18n.t("js.chat.scroll_to_bottom"))
|
click_button(class: "chat-scroll-to-bottom")
|
||||||
click_link(link)
|
click_link(link)
|
||||||
|
|
||||||
expect(page).to have_css(
|
expect(page).to have_css(
|
||||||
@ -149,8 +150,9 @@ RSpec.describe "Navigating to message", type: :system, js: true do
|
|||||||
visit("/")
|
visit("/")
|
||||||
chat_page.open_from_header
|
chat_page.open_from_header
|
||||||
chat_drawer_page.open_channel(channel_1)
|
chat_drawer_page.open_channel(channel_1)
|
||||||
|
|
||||||
click_link(link)
|
click_link(link)
|
||||||
click_link(I18n.t("js.chat.scroll_to_bottom"))
|
click_button(class: "chat-scroll-to-bottom")
|
||||||
click_link(link)
|
click_link(link)
|
||||||
|
|
||||||
expect(page).to have_css(
|
expect(page).to have_css(
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user