diff --git a/plugins/chat/assets/javascripts/discourse/components/chat-skeleton.js b/plugins/chat/assets/javascripts/discourse/components/chat-skeleton.js
index 3710d8dcc9b..6af83cf2e41 100644
--- a/plugins/chat/assets/javascripts/discourse/components/chat-skeleton.js
+++ b/plugins/chat/assets/javascripts/discourse/components/chat-skeleton.js
@@ -4,9 +4,13 @@ import { htmlSafe } from "@ember/template";
export default class ChatSkeleton extends Component {
get placeholders() {
return Array.from({ length: 15 }, () => {
- return Array.from({ length: this.#randomIntFromInterval(1, 5) }, () => {
- return htmlSafe(`width: ${this.#randomIntFromInterval(20, 95)}%`);
- });
+ return {
+ 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) }),
+ };
});
}
diff --git a/plugins/chat/assets/javascripts/discourse/components/full-page-chat.hbs b/plugins/chat/assets/javascripts/discourse/components/full-page-chat.hbs
index 33661579b6c..c5c56e5fb51 100644
--- a/plugins/chat/assets/javascripts/discourse/components/full-page-chat.hbs
+++ b/plugins/chat/assets/javascripts/discourse/components/full-page-chat.hbs
@@ -1,7 +1,6 @@
{{#if this.chat.activeChannel}}
{{/if}}
\ No newline at end of file
diff --git a/plugins/chat/assets/javascripts/discourse/components/full-page-chat.js b/plugins/chat/assets/javascripts/discourse/components/full-page-chat.js
index 1fb7ad3c57e..94d9c7039f7 100644
--- a/plugins/chat/assets/javascripts/discourse/components/full-page-chat.js
+++ b/plugins/chat/assets/javascripts/discourse/components/full-page-chat.js
@@ -1,79 +1,6 @@
-import Component from "@ember/component";
-import { bind } from "discourse-common/utils/decorators";
-import { action } from "@ember/object";
+import Component from "@glimmer/component";
import { inject as service } from "@ember/service";
-export default Component.extend({
- tagName: "",
- 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");
- },
-});
+export default class FullPageChat extends Component {
+ @service chat;
+}
diff --git a/plugins/chat/assets/javascripts/discourse/controllers/chat-channel.js b/plugins/chat/assets/javascripts/discourse/controllers/chat-channel.js
index 734778d843b..7984545c101 100644
--- a/plugins/chat/assets/javascripts/discourse/controllers/chat-channel.js
+++ b/plugins/chat/assets/javascripts/discourse/controllers/chat-channel.js
@@ -1,10 +1,11 @@
import Controller from "@ember/controller";
import { inject as service } from "@ember/service";
+import { tracked } from "@glimmer/tracking";
export default class ChatChannelController extends Controller {
@service chat;
- targetMessageId = null;
+ @tracked targetMessageId = null;
// Backwards-compatibility
queryParams = ["messageId"];
diff --git a/plugins/chat/assets/javascripts/discourse/controllers/create-channel.js b/plugins/chat/assets/javascripts/discourse/controllers/create-channel.js
index 17d698cb8db..31bc13b5514 100644
--- a/plugins/chat/assets/javascripts/discourse/controllers/create-channel.js
+++ b/plugins/chat/assets/javascripts/discourse/controllers/create-channel.js
@@ -36,6 +36,7 @@ export default class CreateChannelController extends Controller.extend(
categoryPermissionsHint = null;
autoJoinUsers = null;
autoJoinWarning = "";
+ loadingPermissionHint = false;
@notEmpty("category") categorySelected;
@gt("siteSettings.max_chat_auto_joined_users", 0) autoJoinAvailable;
@@ -153,6 +154,8 @@ export default class CreateChannelController extends Controller.extend(
if (category) {
const fullSlug = this._buildCategorySlug(category);
+ this.set("loadingPermissionHint", true);
+
return this.chatApi
.categoryPermissions(category.id)
.then((catPermissions) => {
@@ -194,6 +197,9 @@ export default class CreateChannelController extends Controller.extend(
}
this.set("categoryPermissionsHint", htmlSafe(hint));
+ })
+ .finally(() => {
+ this.set("loadingPermissionHint", false);
});
} else {
this.set("categoryPermissionsHint", DEFAULT_HINT);
diff --git a/plugins/chat/assets/javascripts/discourse/helpers/format-chat-date.js b/plugins/chat/assets/javascripts/discourse/helpers/format-chat-date.js
index c31a86ef042..5d91f205e4f 100644
--- a/plugins/chat/assets/javascripts/discourse/helpers/format-chat-date.js
+++ b/plugins/chat/assets/javascripts/discourse/helpers/format-chat-date.js
@@ -7,8 +7,8 @@ import User from "discourse/models/user";
registerUnbound("format-chat-date", function (message, mode) {
const currentUser = User.current();
const tz = currentUser ? currentUser.user_option.timezone : moment.tz.guess();
- const date = moment(new Date(message.created_at), tz);
- const url = getURL(`/chat/c/-/${message.chat_channel_id}/${message.id}`);
+ const date = moment(new Date(message.createdAt), tz);
+ const url = getURL(`/chat/c/-/${message.channelId}/${message.id}`);
const title = date.format(I18n.t("dates.long_with_year"));
const display =
diff --git a/plugins/chat/assets/javascripts/discourse/initializers/chat-cook-function.js b/plugins/chat/assets/javascripts/discourse/initializers/chat-cook-function.js
new file mode 100644
index 00000000000..f628c478633
--- /dev/null
+++ b/plugins/chat/assets/javascripts/discourse/initializers/chat-cook-function.js
@@ -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
+ );
+ };
+ });
+ },
+};
diff --git a/plugins/chat/assets/javascripts/discourse/initializers/chat-setup.js b/plugins/chat/assets/javascripts/discourse/initializers/chat-setup.js
index 69ee6ab84ee..b0ad79f345f 100644
--- a/plugins/chat/assets/javascripts/discourse/initializers/chat-setup.js
+++ b/plugins/chat/assets/javascripts/discourse/initializers/chat-setup.js
@@ -10,6 +10,7 @@ const MIN_REFRESH_DURATION_MS = 180000; // 3 minutes
export default {
name: "chat-setup",
+
initialize(container) {
this.chatService = container.lookup("service:chat");
this.siteSettings = container.lookup("service:site-settings");
@@ -19,6 +20,7 @@ export default {
if (!this.chatService.userCanChat) {
return;
}
+
withPluginApi("0.12.1", (api) => {
api.registerChatComposerButton({
id: "chat-upload-btn",
diff --git a/plugins/chat/assets/javascripts/discourse/lib/chat-message-flag.js b/plugins/chat/assets/javascripts/discourse/lib/chat-message-flag.js
index 60a20c2206a..9bd86b4ab40 100644
--- a/plugins/chat/assets/javascripts/discourse/lib/chat-message-flag.js
+++ b/plugins/chat/assets/javascripts/discourse/lib/chat-message-flag.js
@@ -38,7 +38,7 @@ export default class ChatMessageFlag {
let flagsAvailable = site.flagTypes;
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
diff --git a/plugins/chat/assets/javascripts/discourse/models/chat-channel.js b/plugins/chat/assets/javascripts/discourse/models/chat-channel.js
index c6e75e9edd3..15be14f7dd7 100644
--- a/plugins/chat/assets/javascripts/discourse/models/chat-channel.js
+++ b/plugins/chat/assets/javascripts/discourse/models/chat-channel.js
@@ -7,6 +7,7 @@ import { tracked } from "@glimmer/tracking";
import slugifyChannel from "discourse/plugins/chat/discourse/lib/slugify-channel";
import ChatThreadsManager from "discourse/plugins/chat/discourse/lib/chat-threads-manager";
import { getOwner } from "discourse-common/lib/get-owner";
+import { TrackedArray } from "@ember-compat/tracked-built-ins";
export const CHATABLE_TYPES = {
directMessageChannel: "DirectMessage",
@@ -54,6 +55,16 @@ export default class ChatChannel extends RestModel {
@tracked chatableType;
@tracked status;
@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));
@@ -74,11 +85,11 @@ export default class ChatChannel extends RestModel {
}
get isDirectMessageChannel() {
- return this.chatable_type === CHATABLE_TYPES.directMessageChannel;
+ return this.chatableType === CHATABLE_TYPES.directMessageChannel;
}
get isCategoryChannel() {
- return this.chatable_type === CHATABLE_TYPES.categoryChannel;
+ return this.chatableType === CHATABLE_TYPES.categoryChannel;
}
get isOpen() {
@@ -105,6 +116,57 @@ export default class ChatChannel extends RestModel {
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) {
if (user.staff) {
return !STAFF_READONLY_STATUSES.includes(this.status);
@@ -127,6 +189,10 @@ export default class ChatChannel extends RestModel {
return;
}
+ if (this.currentUserMembership.last_read_message_id >= messageId) {
+ return;
+ }
+
return ajax(`/chat/${this.id}/read/${messageId}.json`, {
method: "PUT",
}).then(() => {
@@ -142,12 +208,17 @@ ChatChannel.reopenClass({
this._initUserModels(args);
this._initUserMembership(args);
- args.chatableType = args.chatable_type;
- args.membershipsCount = args.memberships_count;
+ this._remapKey(args, "chatable_type", "chatableType");
+ this._remapKey(args, "memberships_count", "membershipsCount");
+ this._remapKey(args, "last_message_sent_at", "lastMessageSentAt");
return this._super(args);
},
+ _remapKey(obj, oldKey, newKey) {
+ delete Object.assign(obj, { [newKey]: obj[oldKey] })[oldKey];
+ },
+
_initUserModels(args) {
if (args.chatable?.users?.length) {
for (let i = 0; i < args.chatable?.users?.length; i++) {
diff --git a/plugins/chat/assets/javascripts/discourse/models/chat-message-draft.js b/plugins/chat/assets/javascripts/discourse/models/chat-message-draft.js
new file mode 100644
index 00000000000..00709add3f3
--- /dev/null
+++ b/plugins/chat/assets/javascripts/discourse/models/chat-message-draft.js
@@ -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);
+ }
+}
diff --git a/plugins/chat/assets/javascripts/discourse/models/chat-message-reaction.js b/plugins/chat/assets/javascripts/discourse/models/chat-message-reaction.js
new file mode 100644
index 00000000000..db1b7a6cecb
--- /dev/null
+++ b/plugins/chat/assets/javascripts/discourse/models/chat-message-reaction.js
@@ -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);
+ })
+ );
+ }
+}
diff --git a/plugins/chat/assets/javascripts/discourse/models/chat-message.js b/plugins/chat/assets/javascripts/discourse/models/chat-message.js
index 8d0c644b5f7..c11f9b23c7d 100644
--- a/plugins/chat/assets/javascripts/discourse/models/chat-message.js
+++ b/plugins/chat/assets/javascripts/discourse/models/chat-message.js
@@ -1,26 +1,193 @@
-import RestModel from "discourse/models/rest";
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({
- create(args = {}) {
- this._initReactions(args);
- this._initUserModel(args);
+ static create(channel, args = {}) {
+ return new ChatMessage(channel, args);
+ }
- return this._super(args);
- },
+ static createStagedMessage(channel, args = {}) {
+ args.staged_id = guid();
+ return new ChatMessage(channel, args);
+ }
- _initReactions(args) {
- args.reactions = EmberObject.create(args.reactions || {});
- },
+ @tracked id;
+ @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) {
- if (!args.user || args.user instanceof User) {
- return;
+ constructor(channel, args = {}) {
+ this.channel = channel;
+ 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()
+ );
+ }
+}
diff --git a/plugins/chat/assets/javascripts/discourse/modifiers/chat/track-message-separator-date.js b/plugins/chat/assets/javascripts/discourse/modifiers/chat/track-message-separator-date.js
new file mode 100644
index 00000000000..71a19434521
--- /dev/null
+++ b/plugins/chat/assets/javascripts/discourse/modifiers/chat/track-message-separator-date.js
@@ -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();
+ }
+}
diff --git a/plugins/chat/assets/javascripts/discourse/modifiers/chat/track-message-visibility.js b/plugins/chat/assets/javascripts/discourse/modifiers/chat/track-message-visibility.js
deleted file mode 100644
index 10474b067cf..00000000000
--- a/plugins/chat/assets/javascripts/discourse/modifiers/chat/track-message-visibility.js
+++ /dev/null
@@ -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);
- }
-}
diff --git a/plugins/chat/assets/javascripts/discourse/modifiers/chat/track-message.js b/plugins/chat/assets/javascripts/discourse/modifiers/chat/track-message.js
new file mode 100644
index 00000000000..469188eaa32
--- /dev/null
+++ b/plugins/chat/assets/javascripts/discourse/modifiers/chat/track-message.js
@@ -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?.();
+ }
+ });
+ }
+}
diff --git a/plugins/chat/assets/javascripts/discourse/routes/chat-channel.js b/plugins/chat/assets/javascripts/discourse/routes/chat-channel.js
index 69ea1c3b3f8..34ca9343de6 100644
--- a/plugins/chat/assets/javascripts/discourse/routes/chat-channel.js
+++ b/plugins/chat/assets/javascripts/discourse/routes/chat-channel.js
@@ -10,6 +10,10 @@ export default class ChatChannelRoute extends DiscourseRoute {
@action
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.chatStateManager.closeSidePanel();
diff --git a/plugins/chat/assets/javascripts/discourse/services/chat-api.js b/plugins/chat/assets/javascripts/discourse/services/chat-api.js
index 35e680031b3..4b51143681b 100644
--- a/plugins/chat/assets/javascripts/discourse/services/chat-api.js
+++ b/plugins/chat/assets/javascripts/discourse/services/chat-api.js
@@ -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.
* @param {number} channelId - The ID of the channel.
diff --git a/plugins/chat/assets/javascripts/discourse/services/chat-channels-manager.js b/plugins/chat/assets/javascripts/discourse/services/chat-channels-manager.js
index e70655e3585..47a4fb88d8b 100644
--- a/plugins/chat/assets/javascripts/discourse/services/chat-channels-manager.js
+++ b/plugins/chat/assets/javascripts/discourse/services/chat-channels-manager.js
@@ -42,6 +42,14 @@ export default class ChatChannelsManager extends Service {
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;
}
@@ -138,8 +146,7 @@ export default class ChatChannelsManager extends Service {
const unreadCountA = a.currentUserMembership.unread_count || 0;
const unreadCountB = b.currentUserMembership.unread_count || 0;
if (unreadCountA === unreadCountB) {
- return new Date(a.get("last_message_sent_at")) >
- new Date(b.get("last_message_sent_at"))
+ return new Date(a.lastMessageSentAt) > new Date(b.lastMessageSentAt)
? -1
: 1;
} else {
diff --git a/plugins/chat/assets/javascripts/discourse/services/chat-message-visibility-observer.js b/plugins/chat/assets/javascripts/discourse/services/chat-message-visibility-observer.js
deleted file mode 100644
index a5a77a1d4b0..00000000000
--- a/plugins/chat/assets/javascripts/discourse/services/chat-message-visibility-observer.js
+++ /dev/null
@@ -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);
- }
-}
diff --git a/plugins/chat/assets/javascripts/discourse/services/chat-subscriptions-manager.js b/plugins/chat/assets/javascripts/discourse/services/chat-subscriptions-manager.js
index 42644d9189c..8430d083746 100644
--- a/plugins/chat/assets/javascripts/discourse/services/chat-subscriptions-manager.js
+++ b/plugins/chat/assets/javascripts/discourse/services/chat-subscriptions-manager.js
@@ -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) {
this.chatChannelsManager.find(busData.chat_channel_id).then((channel) => {
if (
- channel?.currentUserMembership?.last_read_message_id <=
- busData.chat_message_id
+ !channel?.currentUserMembership?.last_read_message_id ||
+ parseInt(channel?.currentUserMembership?.last_read_message_id, 10) <=
+ busData.chat_message_id
) {
channel.currentUserMembership.last_read_message_id =
busData.chat_message_id;
- channel.currentUserMembership.unread_count = 0;
- channel.currentUserMembership.unread_mentions = 0;
+ channel.currentUserMembership.unread_count = busData.unread_count;
+ channel.currentUserMembership.unread_mentions = busData.unread_mentions;
}
});
}
diff --git a/plugins/chat/assets/javascripts/discourse/services/chat.js b/plugins/chat/assets/javascripts/discourse/services/chat.js
index e500e84f754..59a182617e8 100644
--- a/plugins/chat/assets/javascripts/discourse/services/chat.js
+++ b/plugins/chat/assets/javascripts/discourse/services/chat.js
@@ -3,29 +3,18 @@ import { tracked } from "@glimmer/tracking";
import userSearch from "discourse/lib/user-search";
import { popupAjaxError } from "discourse/lib/ajax-error";
import Service, { inject as service } from "@ember/service";
-import Site from "discourse/models/site";
import { ajax } from "discourse/lib/ajax";
-import { generateCookFunction } from "discourse/lib/text";
import { cancel, next } from "@ember/runloop";
import { and } from "@ember/object/computed";
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 userPresent from "discourse/lib/user-presence";
-
-export const LIST_VIEW = "list_view";
-export const CHAT_VIEW = "chat_view";
-export const DRAFT_CHANNEL_VIEW = "draft_channel_view";
+import ChatMessageDraft from "discourse/plugins/chat/discourse/models/chat-message-draft";
const CHAT_ONLINE_OPTIONS = {
userUnseenTime: 300000, // 5 minutes seconds with no interaction
browserHiddenTime: 300000, // Or the browser has been in the background for 5 minutes
};
-const READ_INTERVAL = 1000;
-
export default class Chat extends Service {
@service appEvents;
@service chatNotificationManager;
@@ -64,13 +53,6 @@ export default class Chat extends Service {
if (this.userCanChat) {
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(
(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);
}
);
@@ -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() {
next(() => {
if (this.isDestroyed || this.isDestroying) {
@@ -277,10 +242,6 @@ export default class Chat extends Service {
: this.router.transitionTo("chat.channel", ...channel.routeModels);
}
- _fireOpenMessageAppEvent(messageId) {
- this.appEvents.trigger("chat-live-pane:highlight-message", messageId);
- }
-
async followChannel(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() {
deprecated(
"Use the new chat API `api.registerChatComposerButton` instead of `chat.addToolbarButton`"
diff --git a/plugins/chat/assets/javascripts/discourse/templates/modal/create-channel.hbs b/plugins/chat/assets/javascripts/discourse/templates/modal/create-channel.hbs
index db7dc6fc342..ecc01d51c09 100644
--- a/plugins/chat/assets/javascripts/discourse/templates/modal/create-channel.hbs
+++ b/plugins/chat/assets/javascripts/discourse/templates/modal/create-channel.hbs
@@ -54,7 +54,12 @@
/>
{{#if this.categoryPermissionsHint}}
-
+
{{this.categoryPermissionsHint}}
{{/if}}
diff --git a/plugins/chat/assets/stylesheets/common/chat-channel-preview-card.scss b/plugins/chat/assets/stylesheets/common/chat-channel-preview-card.scss
index 34035311c73..f59c30e85d1 100644
--- a/plugins/chat/assets/stylesheets/common/chat-channel-preview-card.scss
+++ b/plugins/chat/assets/stylesheets/common/chat-channel-preview-card.scss
@@ -5,6 +5,7 @@
display: flex;
flex-direction: column;
align-items: center;
+ z-index: 3;
&.-no-description {
.chat-channel-title {
diff --git a/plugins/chat/assets/stylesheets/common/chat-composer.scss b/plugins/chat/assets/stylesheets/common/chat-composer.scss
index 9ae341f2473..057c6014f7b 100644
--- a/plugins/chat/assets/stylesheets/common/chat-composer.scss
+++ b/plugins/chat/assets/stylesheets/common/chat-composer.scss
@@ -1,6 +1,8 @@
.chat-composer-container {
display: flex;
flex-direction: column;
+ z-index: 3;
+ background-color: var(--secondary);
#chat-full-page-uploader,
#chat-widget-uploader {
diff --git a/plugins/chat/assets/stylesheets/common/chat-message-actions.scss b/plugins/chat/assets/stylesheets/common/chat-message-actions.scss
index 815d561d643..6ed3e37b13f 100644
--- a/plugins/chat/assets/stylesheets/common/chat-message-actions.scss
+++ b/plugins/chat/assets/stylesheets/common/chat-message-actions.scss
@@ -6,10 +6,6 @@
.chat-message-actions {
.chat-message-reaction {
@include chat-reaction;
-
- &:not(.show) {
- display: none;
- }
}
}
diff --git a/plugins/chat/assets/stylesheets/common/chat-message-separator.scss b/plugins/chat/assets/stylesheets/common/chat-message-separator.scss
index e918d0c850a..c9d00b079dc 100644
--- a/plugins/chat/assets/stylesheets/common/chat-message-separator.scss
+++ b/plugins/chat/assets/stylesheets/common/chat-message-separator.scss
@@ -1,42 +1,96 @@
.chat-message-separator {
@include unselectable;
- margin: 0.25rem 0 0.25rem 1rem;
display: flex;
- font-size: var(--font-down-1);
- position: relative;
- transform: translateZ(0);
- position: relative;
- &.new-message {
- color: var(--danger-medium);
+ &-new {
+ position: relative;
+ padding: 20px 0;
- .divider {
- background-color: var(--danger-medium);
+ .chat-message-separator__text-container {
+ 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 {
- .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 {
+ &-date {
position: absolute;
width: 100%;
- height: 1px;
- top: 50%;
+ z-index: 1;
+ 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;
+ }
+ }
}
}
diff --git a/plugins/chat/assets/stylesheets/common/chat-message.scss b/plugins/chat/assets/stylesheets/common/chat-message.scss
index df79c8b70ca..4b1641b343c 100644
--- a/plugins/chat/assets/stylesheets/common/chat-message.scss
+++ b/plugins/chat/assets/stylesheets/common/chat-message.scss
@@ -42,6 +42,10 @@
background: var(--primary-low);
border-color: var(--primary-low-mid);
}
+
+ &:focus {
+ background: none;
+ }
}
.emoji {
@@ -57,13 +61,11 @@
background-color: var(--secondary);
display: flex;
min-width: 0;
+ content-visibility: auto;
+ contain-intrinsic-size: auto 200px;
.chat-message-reaction {
@include chat-reaction;
-
- &:not(.show) {
- display: none;
- }
}
&.chat-action {
@@ -86,17 +88,6 @@
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 {
display: grid;
grid-template-columns: var(--message-left-width) 1fr;
@@ -254,6 +245,10 @@
.chat-message.chat-message-bookmarked {
background: var(--highlight-bg);
+
+ &:hover {
+ background: var(--highlight-medium);
+ }
}
.not-mobile-device & .chat-message-reaction-list .chat-message-react-btn {
@@ -284,7 +279,6 @@
font-style: italic;
}
-.chat-message-container.is-hovered,
.chat-message.chat-message-selected {
background: var(--primary-very-low);
}
diff --git a/plugins/chat/assets/stylesheets/common/chat-skeleton.scss b/plugins/chat/assets/stylesheets/common/chat-skeleton.scss
index 19eed6f1459..cd5e79b3eb0 100644
--- a/plugins/chat/assets/stylesheets/common/chat-skeleton.scss
+++ b/plugins/chat/assets/stylesheets/common/chat-skeleton.scss
@@ -1,4 +1,4 @@
-$radius: 10px;
+$radius: 3px;
.chat-skeleton {
height: auto;
@@ -55,11 +55,35 @@ $radius: 10px;
&__message-content {
grid-area: content;
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 {
height: 13px;
border-radius: $radius;
- margin: 5px 0;
+ margin: 2px 0;
.chat-skeleton__body:nth-of-type(odd) & {
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-"] {
position: relative;
overflow: hidden;
@@ -78,7 +110,7 @@ $radius: 10px;
position: relative;
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;
top: 0;
right: 0;
diff --git a/plugins/chat/assets/stylesheets/common/common.scss b/plugins/chat/assets/stylesheets/common/common.scss
index ba3529b856b..5f7ee16413a 100644
--- a/plugins/chat/assets/stylesheets/common/common.scss
+++ b/plugins/chat/assets/stylesheets/common/common.scss
@@ -144,6 +144,7 @@ $float-height: 530px;
.chat-messages-container {
word-wrap: break-word;
white-space: normal;
+ position: relative;
.chat-message-container {
display: grid;
@@ -283,6 +284,8 @@ $float-height: 530px;
display: flex;
flex-direction: column-reverse;
z-index: 1;
+ margin: 0 3px 0 0;
+ will-change: transform;
&::-webkit-scrollbar {
width: 15px;
@@ -323,37 +326,65 @@ $float-height: 530px;
}
.chat-scroll-to-bottom {
- background: var(--primary-medium);
- bottom: 1em;
- border-radius: 100%;
- left: 50%;
- opacity: 50%;
- padding: 0.5em;
+ left: calc(50% - calc(45px / 2));
+ align-items: center;
+ justify-content: center;
position: absolute;
- transform: translateX(-50%);
- z-index: 2;
+ z-index: 1;
+ 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);
- opacity: 100%;
+ border-radius: 3px;
+ text-align: center;
+ font-size: var(--font-down-1);
}
- .d-icon {
- color: var(--primary);
- margin: 0;
- }
-
- &.unread-messages {
- opacity: 85%;
- border-radius: 0;
- transition: border-radius 0.1s linear;
-
- &:hover {
- opacity: 100%;
- }
+ &__arrow {
+ display: flex;
+ background: var(--primary-medium);
+ border-radius: 100%;
+ align-items: center;
+ justify-content: center;
+ height: 35px;
+ width: 35px;
.d-icon {
- margin: 0 0 0 0.5em;
+ color: var(--secondary);
+ }
+ }
+
+ &:hover {
+ opacity: 1;
+
+ .chat-scroll-to-bottom__arrow {
+ .d-icon {
+ color: var(--secondary);
+ }
}
}
}
diff --git a/plugins/chat/assets/stylesheets/desktop/chat-composer.scss b/plugins/chat/assets/stylesheets/desktop/chat-composer.scss
index 3095d1851ad..c6af087e68e 100644
--- a/plugins/chat/assets/stylesheets/desktop/chat-composer.scss
+++ b/plugins/chat/assets/stylesheets/desktop/chat-composer.scss
@@ -1,6 +1,6 @@
.chat-composer-container {
.chat-composer {
- margin: 0.25rem 10px 0 10px;
+ margin: 0.25rem 5px 0 5px;
}
html.keyboard-visible .footer-nav-ipad & {
margin: 0.25rem 10px 1rem 10px;
diff --git a/plugins/chat/assets/stylesheets/desktop/desktop.scss b/plugins/chat/assets/stylesheets/desktop/desktop.scss
index ea281cbe347..3214e03ceb9 100644
--- a/plugins/chat/assets/stylesheets/desktop/desktop.scss
+++ b/plugins/chat/assets/stylesheets/desktop/desktop.scss
@@ -53,6 +53,25 @@
.chat-message.user-info-hidden {
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
diff --git a/plugins/chat/assets/stylesheets/mobile/chat-message-actions.scss b/plugins/chat/assets/stylesheets/mobile/chat-message-actions.scss
index e8361c052e2..733341f0e28 100644
--- a/plugins/chat/assets/stylesheets/mobile/chat-message-actions.scss
+++ b/plugins/chat/assets/stylesheets/mobile/chat-message-actions.scss
@@ -22,6 +22,8 @@
border-radius: 8px;
.selected-message-reply {
+ margin-left: 5px;
+
&:not(.is-expanded) {
@include ellipsis;
}
diff --git a/plugins/chat/assets/stylesheets/mobile/chat-message.scss b/plugins/chat/assets/stylesheets/mobile/chat-message.scss
index c3517ab9858..20c267d2e8c 100644
--- a/plugins/chat/assets/stylesheets/mobile/chat-message.scss
+++ b/plugins/chat/assets/stylesheets/mobile/chat-message.scss
@@ -4,7 +4,3 @@
.replying-text {
@include unselectable;
}
-
-.chat-message-container {
- transform: translateZ(0);
-}
diff --git a/plugins/chat/config/locales/client.en.yml b/plugins/chat/config/locales/client.en.yml
index af2a3c06436..1eadba98f47 100644
--- a/plugins/chat/config/locales/client.en.yml
+++ b/plugins/chat/config/locales/client.en.yml
@@ -108,7 +108,7 @@ en:
in_reply_to: "In reply to"
heading: "Chat"
join: "Join"
- new_messages: "new messages"
+ last_visit: "last visit"
mention_warning:
dismiss: "dismiss"
cannot_see: "%{username} can't access this channel and was not notified."
diff --git a/plugins/chat/plugin.rb b/plugins/chat/plugin.rb
index d14913e05b3..5f85f631e87 100644
--- a/plugins/chat/plugin.rb
+++ b/plugins/chat/plugin.rb
@@ -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/chat_channel_threads_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__)
if Discourse.allow_dev_populate?
diff --git a/plugins/chat/spec/queries/chat_channel_memberships_query_spec.rb b/plugins/chat/spec/queries/chat_channel_memberships_query_spec.rb
index 409349c0ef9..38f43f6d38d 100644
--- a/plugins/chat/spec/queries/chat_channel_memberships_query_spec.rb
+++ b/plugins/chat/spec/queries/chat_channel_memberships_query_spec.rb
@@ -17,7 +17,7 @@ describe ChatChannelMembershipsQuery do
context "when no memberships exists" 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
@@ -28,7 +28,7 @@ describe ChatChannelMembershipsQuery do
end
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)
end
@@ -49,7 +49,7 @@ describe ChatChannelMembershipsQuery do
end
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)
end
@@ -62,14 +62,16 @@ describe ChatChannelMembershipsQuery do
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
it "returns the membership if the user still has access through a staff group" do
chatters_group.remove(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)
end
@@ -77,7 +79,7 @@ describe ChatChannelMembershipsQuery do
context "when membership doesn’t exist" 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
end
@@ -91,7 +93,7 @@ describe ChatChannelMembershipsQuery do
end
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
end
@@ -99,7 +101,7 @@ describe ChatChannelMembershipsQuery do
context "when membership doesn’t exist" 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
end
@@ -114,7 +116,7 @@ describe ChatChannelMembershipsQuery do
end
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
@@ -122,7 +124,7 @@ describe ChatChannelMembershipsQuery do
fab!(:channel_1) { Fabricate(:direct_message_channel, users: [user_1, user_2]) }
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)
end
@@ -139,7 +141,7 @@ describe ChatChannelMembershipsQuery do
describe "offset param" 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)
end
@@ -147,7 +149,7 @@ describe ChatChannelMembershipsQuery do
describe "limit param" 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)
end
@@ -163,7 +165,7 @@ describe ChatChannelMembershipsQuery do
end
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[0].user).to eq(user_1)
@@ -182,7 +184,7 @@ describe ChatChannelMembershipsQuery do
before { SiteSetting.prioritize_username_in_ux = true }
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[1].user).to eq(user_2)
@@ -193,7 +195,7 @@ describe ChatChannelMembershipsQuery do
before { SiteSetting.prioritize_username_in_ux = false }
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[1].user).to eq(user_1)
@@ -203,7 +205,7 @@ describe ChatChannelMembershipsQuery do
before { SiteSetting.enable_names = false }
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[1].user).to eq(user_2)
@@ -222,7 +224,7 @@ describe ChatChannelMembershipsQuery do
end
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
end
end
@@ -242,7 +244,7 @@ describe ChatChannelMembershipsQuery do
end
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
end
end
@@ -260,7 +262,7 @@ describe ChatChannelMembershipsQuery do
end
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
end
end
diff --git a/plugins/chat/spec/queries/chat_channel_unreads_query_spec.rb b/plugins/chat/spec/queries/chat_channel_unreads_query_spec.rb
new file mode 100644
index 00000000000..fea379ceb46
--- /dev/null
+++ b/plugins/chat/spec/queries/chat_channel_unreads_query_spec.rb
@@ -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
diff --git a/plugins/chat/spec/requests/chat_controller_spec.rb b/plugins/chat/spec/requests/chat_controller_spec.rb
index d0fcbfd19ab..92d0517036d 100644
--- a/plugins/chat/spec/requests/chat_controller_spec.rb
+++ b/plugins/chat/spec/requests/chat_controller_spec.rb
@@ -126,15 +126,17 @@ RSpec.describe Chat::ChatController do
it "correctly marks reactions as 'reacted' for the current_user" do
heart_emoji = ":heart:"
smile_emoji = ":smile"
-
last_message = chat_channel.chat_messages.last
last_message.reactions.create(user: user, emoji: heart_emoji)
last_message.reactions.create(user: admin, emoji: smile_emoji)
get "/chat/#{chat_channel.id}/messages.json", params: { page_size: page_size }
+
reactions = response.parsed_body["chat_messages"].last["reactions"]
- expect(reactions[heart_emoji]["reacted"]).to be true
- expect(reactions[smile_emoji]["reacted"]).to be false
+ heart_reaction = reactions.find { |r| r["emoji"] == heart_emoji }
+ expect(heart_reaction["reacted"]).to be true
+ smile_reaction = reactions.find { |r| r["emoji"] == smile_emoji }
+ expect(smile_reaction["reacted"]).to be false
end
it "sends the last message bus id for the channel" do
diff --git a/plugins/chat/spec/serializer/chat_message_serializer_spec.rb b/plugins/chat/spec/serializer/chat_message_serializer_spec.rb
index ea97d0310de..67f11368c2d 100644
--- a/plugins/chat/spec/serializer/chat_message_serializer_spec.rb
+++ b/plugins/chat/spec/serializer/chat_message_serializer_spec.rb
@@ -21,12 +21,14 @@ describe ChatMessageSerializer do
it "doesn’t return the reaction" do
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!
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
diff --git a/plugins/chat/spec/system/chat_channel_spec.rb b/plugins/chat/spec/system/chat_channel_spec.rb
index 39ecd2b9682..bacd3a69427 100644
--- a/plugins/chat/spec/system/chat_channel_spec.rb
+++ b/plugins/chat/spec/system/chat_channel_spec.rb
@@ -183,7 +183,7 @@ RSpec.describe "Chat channel", type: :system, js: true do
it "shows a date separator" do
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
diff --git a/plugins/chat/spec/system/create_channel_spec.rb b/plugins/chat/spec/system/create_channel_spec.rb
index f6fd4e4da09..f1cfa2e8f87 100644
--- a/plugins/chat/spec/system/create_channel_spec.rb
+++ b/plugins/chat/spec/system/create_channel_spec.rb
@@ -81,6 +81,7 @@ RSpec.describe "Create channel", type: :system, js: true do
chat_page.visit_browse
chat_page.new_channel_button.click
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(
"<script>e</script>",
diff --git a/plugins/chat/spec/system/flag_message_spec.rb b/plugins/chat/spec/system/flag_message_spec.rb
index 8b40ba93cd8..a7afd6af14b 100644
--- a/plugins/chat/spec/system/flag_message_spec.rb
+++ b/plugins/chat/spec/system/flag_message_spec.rb
@@ -32,7 +32,7 @@ RSpec.describe "Flag message", type: :system, js: true do
context "when direct message channel" do
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
chat.visit_channel(dm_channel_1)
diff --git a/plugins/chat/spec/system/message_user_info.rb b/plugins/chat/spec/system/message_user_info.rb
new file mode 100644
index 00000000000..be97ab1c0d9
--- /dev/null
+++ b/plugins/chat/spec/system/message_user_info.rb
@@ -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
diff --git a/plugins/chat/spec/system/navigating_to_message_spec.rb b/plugins/chat/spec/system/navigating_to_message_spec.rb
index 9dae4fe8b81..c3a3899eb43 100644
--- a/plugins/chat/spec/system/navigating_to_message_spec.rb
+++ b/plugins/chat/spec/system/navigating_to_message_spec.rb
@@ -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
chat_page.visit_channel(channel_1)
+
click_link(link)
- click_link(I18n.t("js.chat.scroll_to_bottom"))
+ click_button(class: "chat-scroll-to-bottom")
click_link(link)
expect(page).to have_css(
@@ -149,8 +150,9 @@ RSpec.describe "Navigating to message", type: :system, js: true do
visit("/")
chat_page.open_from_header
chat_drawer_page.open_channel(channel_1)
+
click_link(link)
- click_link(I18n.t("js.chat.scroll_to_bottom"))
+ click_button(class: "chat-scroll-to-bottom")
click_link(link)
expect(page).to have_css(
diff --git a/plugins/chat/spec/system/shortcuts/chat_composer_spec.rb b/plugins/chat/spec/system/shortcuts/chat_composer_spec.rb
index dddfa46baa9..a7dd347750e 100644
--- a/plugins/chat/spec/system/shortcuts/chat_composer_spec.rb
+++ b/plugins/chat/spec/system/shortcuts/chat_composer_spec.rb
@@ -5,6 +5,7 @@ RSpec.describe "Shortcuts | chat composer", type: :system, js: true do
fab!(:current_user) { Fabricate(:user) }
let(:chat) { PageObjects::Pages::Chat.new }
+ let(:channel_page) { PageObjects::Pages::ChatChannel.new }
KEY_MODIFIER = RUBY_PLATFORM =~ /darwin/i ? :meta : :control
@@ -63,8 +64,9 @@ RSpec.describe "Shortcuts | chat composer", type: :system, js: true do
it "edits last editable message" do
chat.visit_channel(channel_1)
+ expect(channel_page).to have_message(id: message_1.id)
- within(".chat-composer-input") { |composer| composer.send_keys(:arrow_up) }
+ find(".chat-composer-input").send_keys(:arrow_up)
expect(page.find(".chat-composer-message-details")).to have_content(message_1.message)
end
diff --git a/plugins/chat/spec/system/sticky_date_spec.rb b/plugins/chat/spec/system/sticky_date_spec.rb
new file mode 100644
index 00000000000..9f043afa9f9
--- /dev/null
+++ b/plugins/chat/spec/system/sticky_date_spec.rb
@@ -0,0 +1,32 @@
+# 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
+ channel_1.add(current_user)
+ 20.times { Fabricate(:chat_message, chat_channel: channel_1, created_at: 1.day.ago) }
+ 25.times { Fabricate(:chat_message, chat_channel: channel_1) }
+ sign_in(current_user)
+ end
+
+ context "when today separator is out of screen" do
+ it "shows it as a sticky date" do
+ chat_page.visit_channel(channel_1)
+
+ expect(page.find(".chat-message-separator__text-container.is-pinned")).to have_content(
+ I18n.t("js.chat.chat_message_separator.today"),
+ )
+ expect(page).to have_css(
+ ".chat-message-separator__text-container:not(.is-pinned)",
+ visible: :hidden,
+ text:
+ "#{I18n.t("js.chat.chat_message_separator.yesterday")} - #{I18n.t("js.chat.last_visit")}",
+ )
+ end
+ end
+end
diff --git a/plugins/chat/spec/system/uploads_spec.rb b/plugins/chat/spec/system/uploads_spec.rb
index a012d29b86c..09dee0e400d 100644
--- a/plugins/chat/spec/system/uploads_spec.rb
+++ b/plugins/chat/spec/system/uploads_spec.rb
@@ -36,12 +36,21 @@ describe "Uploading files in chat messages", type: :system, js: true do
it "allows uploading multiple files" do
chat.visit_channel(channel_1)
+
file_path_1 = file_from_fixtures("logo.png", "images").path
- file_path_2 = file_from_fixtures("logo.jpg", "images").path
- attach_file([file_path_1, file_path_2]) do
+ attach_file([file_path_1]) do
channel.open_action_menu
channel.click_action_button("chat-upload-btn")
+ find(".chat-composer-input").click
end
+
+ file_path_2 = file_from_fixtures("logo.jpg", "images").path
+ attach_file([file_path_2]) do
+ channel.open_action_menu
+ channel.click_action_button("chat-upload-btn")
+ find(".chat-composer-input").click
+ end
+
expect(page).to have_css(".chat-composer-upload .preview .preview-img", count: 2)
channel.send_message("upload testing")
diff --git a/plugins/chat/test/javascripts/components/chat-channel-metadata-test.js b/plugins/chat/test/javascripts/components/chat-channel-metadata-test.js
index 4a8c7f391c8..db01ba779e9 100644
--- a/plugins/chat/test/javascripts/components/chat-channel-metadata-test.js
+++ b/plugins/chat/test/javascripts/components/chat-channel-metadata-test.js
@@ -9,16 +9,17 @@ module("Discourse Chat | Component | chat-channel-metadata", function (hooks) {
setupRenderingTest(hooks);
test("displays last message sent at", async function (assert) {
- let lastMessageSentAt = moment().subtract(1, "day");
+ let lastMessageSentAt = moment().subtract(1, "day").format();
this.channel = fabricators.directMessageChatChannel({
last_message_sent_at: lastMessageSentAt,
});
+
await render(hbs`
`);
assert.dom(".chat-channel-metadata__date").hasText("Yesterday");
lastMessageSentAt = moment();
- this.channel.set("last_message_sent_at", lastMessageSentAt);
+ this.channel.lastMessageSentAt = lastMessageSentAt;
await render(hbs`
`);
assert
diff --git a/plugins/chat/test/javascripts/components/chat-channel-row-test.js b/plugins/chat/test/javascripts/components/chat-channel-row-test.js
index 0c10cad2158..624d574da95 100644
--- a/plugins/chat/test/javascripts/components/chat-channel-row-test.js
+++ b/plugins/chat/test/javascripts/components/chat-channel-row-test.js
@@ -51,9 +51,7 @@ module("Discourse Chat | Component | chat-channel-row", function (hooks) {
assert
.dom(".chat-channel-metadata")
- .hasText(
- moment(this.categoryChatChannel.last_message_sent_at).format("l")
- );
+ .hasText(moment(this.categoryChatChannel.lastMessageSentAt).format("l"));
});
test("renders membership toggling button when necessary", async function (assert) {
diff --git a/plugins/chat/test/javascripts/components/chat-composer-uploads-test.js b/plugins/chat/test/javascripts/components/chat-composer-uploads-test.js
index b256887fff8..1a3872bd2c2 100644
--- a/plugins/chat/test/javascripts/components/chat-composer-uploads-test.js
+++ b/plugins/chat/test/javascripts/components/chat-composer-uploads-test.js
@@ -8,7 +8,6 @@ import {
import hbs from "htmlbars-inline-precompile";
import { click, render, settled, waitFor } from "@ember/test-helpers";
import { module, test } from "qunit";
-import { run } from "@ember/runloop";
const fakeUpload = {
type: ".png",
@@ -47,12 +46,11 @@ module("Discourse Chat | Component | chat-composer-uploads", function (hooks) {
setupRenderingTest(hooks);
test("loading uploads from an outside source (e.g. draft or editing message)", async function (assert) {
- await render(hbs`
-
- `);
+ this.existingUploads = [fakeUpload];
- this.appEvents = this.container.lookup("service:appEvents");
- this.appEvents.trigger("chat-composer:load-uploads", [fakeUpload]);
+ await render(hbs`
+
+ `);
await settled();
assert.strictEqual(count(".chat-composer-upload"), 1);
@@ -61,10 +59,7 @@ module("Discourse Chat | Component | chat-composer-uploads", function (hooks) {
test("upload starts and completes", async function (assert) {
setupUploadPretender();
- this.set("changedUploads", null);
- this.set("onUploadChanged", (uploads) => {
- this.set("changedUploads", uploads);
- });
+ this.set("onUploadChanged", () => {});
await render(hbs`
@@ -80,34 +75,31 @@ module("Discourse Chat | Component | chat-composer-uploads", function (hooks) {
done();
}
);
-
this.appEvents.trigger(
"upload-mixin:chat-composer-uploader:add-files",
createFile("avatar.png")
);
await waitFor(".chat-composer-upload");
- assert.strictEqual(count(".chat-composer-upload"), 1);
+
+ assert.dom(".chat-composer-upload").exists({ count: 1 });
});
test("removing a completed upload", async function (assert) {
this.set("changedUploads", null);
- this.set("onUploadChanged", (uploads) => {
- this.set("changedUploads", uploads);
- });
+ this.set("onUploadChanged", () => {});
+
+ this.existingUploads = [fakeUpload];
await render(hbs`
-
+
`);
- this.appEvents = this.container.lookup("service:appEvents");
- run(() =>
- this.appEvents.trigger("chat-composer:load-uploads", [fakeUpload])
- );
- assert.strictEqual(count(".chat-composer-upload"), 1);
+ assert.dom(".chat-composer-upload").exists({ count: 1 });
await click(".remove-upload");
- assert.strictEqual(count(".chat-composer-upload"), 0);
+
+ assert.dom(".chat-composer-upload").exists({ count: 0 });
});
test("cancelling in progress upload", async function (assert) {
diff --git a/plugins/chat/test/javascripts/components/chat-live-pane-test.js b/plugins/chat/test/javascripts/components/chat-live-pane-test.js
deleted file mode 100644
index 244439eb38e..00000000000
--- a/plugins/chat/test/javascripts/components/chat-live-pane-test.js
+++ /dev/null
@@ -1,44 +0,0 @@
-import { setupRenderingTest } from "discourse/tests/helpers/component-test";
-import { exists } from "discourse/tests/helpers/qunit-helpers";
-import hbs from "htmlbars-inline-precompile";
-import { module, test } from "qunit";
-import fabricators from "../helpers/fabricators";
-import { render } from "@ember/test-helpers";
-import pretender, { response } from "discourse/tests/helpers/create-pretender";
-import MockPresenceChannel from "../helpers/mock-presence-channel";
-
-function mockChat(context) {
- const mock = context.container.lookup("service:chat");
- mock.draftStore = {};
- mock.currentUser = context.currentUser;
- mock.presenceChannel = MockPresenceChannel.create();
- return mock;
-}
-
-module("Discourse Chat | Component | chat-live-pane", function (hooks) {
- setupRenderingTest(hooks);
-
- hooks.beforeEach(function () {
- this.set("chat", mockChat(this));
- this.set("channel", fabricators.chatChannel());
- });
-
- test("Shows skeleton when loading", async function (assert) {
- pretender.get(`/chat/chat_channels.json`, () => response(this.channel));
- pretender.get(`/chat/:id/messages.json`, () =>
- response({ chat_messages: [], meta: { can_delete_self: true } })
- );
-
- await render(
- hbs`
`
- );
-
- assert.true(exists(".chat-skeleton"));
-
- await render(
- hbs`
`
- );
-
- assert.true(exists(".chat-skeleton"));
- });
-});
diff --git a/plugins/chat/test/javascripts/components/chat-message-avatar-test.js b/plugins/chat/test/javascripts/components/chat-message-avatar-test.js
index 56244f9803b..dff366aec8b 100644
--- a/plugins/chat/test/javascripts/components/chat-message-avatar-test.js
+++ b/plugins/chat/test/javascripts/components/chat-message-avatar-test.js
@@ -3,12 +3,16 @@ import hbs from "htmlbars-inline-precompile";
import { exists, query } from "discourse/tests/helpers/qunit-helpers";
import { module, test } from "qunit";
import { render } from "@ember/test-helpers";
+import ChatMessage from "discourse/plugins/chat/discourse/models/chat-message";
+import fabricators from "../helpers/fabricators";
module("Discourse Chat | Component | chat-message-avatar", function (hooks) {
setupRenderingTest(hooks);
test("chat_webhook_event", async function (assert) {
- this.set("message", { chat_webhook_event: { emoji: ":heart:" } });
+ this.message = ChatMessage.create(fabricators.chatChannel(), {
+ chat_webhook_event: { emoji: ":heart:" },
+ });
await render(hbs`
`);
@@ -16,7 +20,9 @@ module("Discourse Chat | Component | chat-message-avatar", function (hooks) {
});
test("user", async function (assert) {
- this.set("message", { user: { username: "discobot" } });
+ this.message = ChatMessage.create(fabricators.chatChannel(), {
+ user: { username: "discobot" },
+ });
await render(hbs`
`);
diff --git a/plugins/chat/test/javascripts/components/chat-message-info-test.js b/plugins/chat/test/javascripts/components/chat-message-info-test.js
index 2875e2a73cc..f633d71645f 100644
--- a/plugins/chat/test/javascripts/components/chat-message-info-test.js
+++ b/plugins/chat/test/javascripts/components/chat-message-info-test.js
@@ -6,21 +6,21 @@ import I18n from "I18n";
import { module, test } from "qunit";
import { render } from "@ember/test-helpers";
import ChatMessage from "discourse/plugins/chat/discourse/models/chat-message";
+import fabricators from "../helpers/fabricators";
module("Discourse Chat | Component | chat-message-info", function (hooks) {
setupRenderingTest(hooks);
test("chat_webhook_event", async function (assert) {
- this.set(
- "message",
- ChatMessage.create({ chat_webhook_event: { username: "discobot" } })
- );
+ this.message = ChatMessage.create(fabricators.chatChannel(), {
+ chat_webhook_event: { username: "discobot" },
+ });
await render(hbs`
`);
assert.strictEqual(
query(".chat-message-info__username").innerText.trim(),
- this.message.chat_webhook_event.username
+ this.message.chatWebhookEvent.username
);
assert.strictEqual(
query(".chat-message-info__bot-indicator").textContent.trim(),
@@ -29,7 +29,9 @@ module("Discourse Chat | Component | chat-message-info", function (hooks) {
});
test("user", async function (assert) {
- this.set("message", ChatMessage.create({ user: { username: "discobot" } }));
+ this.message = ChatMessage.create(fabricators.chatChannel(), {
+ user: { username: "discobot" },
+ });
await render(hbs`
`);
@@ -40,13 +42,10 @@ module("Discourse Chat | Component | chat-message-info", function (hooks) {
});
test("date", async function (assert) {
- this.set(
- "message",
- ChatMessage.create({
- user: { username: "discobot" },
- created_at: moment(),
- })
- );
+ this.message = ChatMessage.create(fabricators.chatChannel(), {
+ user: { username: "discobot" },
+ created_at: moment(),
+ });
await render(hbs`
`);
@@ -54,16 +53,13 @@ module("Discourse Chat | Component | chat-message-info", function (hooks) {
});
test("bookmark (with reminder)", async function (assert) {
- this.set(
- "message",
- ChatMessage.create({
- user: { username: "discobot" },
- bookmark: Bookmark.create({
- reminder_at: moment(),
- name: "some name",
- }),
- })
- );
+ this.message = ChatMessage.create(fabricators.chatChannel(), {
+ user: { username: "discobot" },
+ bookmark: Bookmark.create({
+ reminder_at: moment(),
+ name: "some name",
+ }),
+ });
await render(hbs`
`);
@@ -73,15 +69,12 @@ module("Discourse Chat | Component | chat-message-info", function (hooks) {
});
test("bookmark (no reminder)", async function (assert) {
- this.set(
- "message",
- ChatMessage.create({
- user: { username: "discobot" },
- bookmark: Bookmark.create({
- name: "some name",
- }),
- })
- );
+ this.message = ChatMessage.create(fabricators.chatChannel(), {
+ user: { username: "discobot" },
+ bookmark: Bookmark.create({
+ name: "some name",
+ }),
+ });
await render(hbs`
`);
@@ -90,7 +83,9 @@ module("Discourse Chat | Component | chat-message-info", function (hooks) {
test("user status", async function (assert) {
const status = { description: "off to dentist", emoji: "tooth" };
- this.set("message", ChatMessage.create({ user: { status } }));
+ this.message = ChatMessage.create(fabricators.chatChannel(), {
+ user: { status },
+ });
await render(hbs`
`);
@@ -98,13 +93,10 @@ module("Discourse Chat | Component | chat-message-info", function (hooks) {
});
test("reviewable", async function (assert) {
- this.set(
- "message",
- ChatMessage.create({
- user: { username: "discobot" },
- user_flag_status: 0,
- })
- );
+ this.message = ChatMessage.create(fabricators.chatChannel(), {
+ user: { username: "discobot" },
+ user_flag_status: 0,
+ });
await render(hbs`
`);
@@ -113,13 +105,12 @@ module("Discourse Chat | Component | chat-message-info", function (hooks) {
I18n.t("chat.you_flagged")
);
- this.set(
- "message",
- ChatMessage.create({
- user: { username: "discobot" },
- reviewable_id: 1,
- })
- );
+ this.message = ChatMessage.create(fabricators.chatChannel(), {
+ user: { username: "discobot" },
+ reviewable_id: 1,
+ });
+
+ await render(hbs`
`);
assert.strictEqual(
query(".chat-message-info__flag a .svg-icon-title").title,
@@ -128,18 +119,15 @@ module("Discourse Chat | Component | chat-message-info", function (hooks) {
});
test("with username classes", async function (assert) {
- this.set(
- "message",
- ChatMessage.create({
- user: {
- username: "discobot",
- admin: true,
- moderator: true,
- new_user: true,
- primary_group_name: "foo",
- },
- })
- );
+ this.message = ChatMessage.create(fabricators.chatChannel(), {
+ user: {
+ username: "discobot",
+ admin: true,
+ moderator: true,
+ new_user: true,
+ primary_group_name: "foo",
+ },
+ });
await render(hbs`
`);
@@ -151,7 +139,9 @@ module("Discourse Chat | Component | chat-message-info", function (hooks) {
});
test("without username classes", async function (assert) {
- this.set("message", ChatMessage.create({ user: { username: "discobot" } }));
+ this.message = ChatMessage.create(fabricators.chatChannel(), {
+ user: { username: "discobot" },
+ });
await render(hbs`
`);
diff --git a/plugins/chat/test/javascripts/components/chat-message-reaction-test.js b/plugins/chat/test/javascripts/components/chat-message-reaction-test.js
index 86f1ca04c7f..4a640b949dc 100644
--- a/plugins/chat/test/javascripts/components/chat-message-reaction-test.js
+++ b/plugins/chat/test/javascripts/components/chat-message-reaction-test.js
@@ -7,14 +7,6 @@ import { module, test } from "qunit";
module("Discourse Chat | Component | chat-message-reaction", function (hooks) {
setupRenderingTest(hooks);
- test("accepts arbitrary class property", async function (assert) {
- await render(hbs`
-
- `);
-
- assert.true(exists(".chat-message-reaction.foo"));
- });
-
test("adds reacted class when user reacted", async function (assert) {
await render(hbs`
@@ -29,19 +21,6 @@ module("Discourse Chat | Component | chat-message-reaction", function (hooks) {
assert.true(exists(`.chat-message-reaction[data-emoji-name="heart"]`));
});
- test("adds show class when count is positive", async function (assert) {
- this.set("count", 0);
-
- await render(hbs`
-
- `);
-
- assert.false(exists(".chat-message-reaction.show"));
-
- this.set("count", 1);
- assert.true(exists(".chat-message-reaction.show"));
- });
-
test("title/alt attributes", async function (assert) {
await render(hbs`
`);
diff --git a/plugins/chat/test/javascripts/components/chat-message-separator-date-test.js b/plugins/chat/test/javascripts/components/chat-message-separator-date-test.js
new file mode 100644
index 00000000000..415413df7ac
--- /dev/null
+++ b/plugins/chat/test/javascripts/components/chat-message-separator-date-test.js
@@ -0,0 +1,24 @@
+import { setupRenderingTest } from "discourse/tests/helpers/component-test";
+import { query } from "discourse/tests/helpers/qunit-helpers";
+import hbs from "htmlbars-inline-precompile";
+import { module, test } from "qunit";
+import { render } from "@ember/test-helpers";
+
+module(
+ "Discourse Chat | Component | chat-message-separator-date",
+ function (hooks) {
+ setupRenderingTest(hooks);
+
+ test("first message of the day", async function (assert) {
+ this.set("date", moment().format("LLL"));
+ this.set("message", { firstMessageOfTheDayAt: this.date });
+
+ await render(hbs`
`);
+
+ assert.strictEqual(
+ query(".chat-message-separator-date").innerText.trim(),
+ this.date
+ );
+ });
+ }
+);
diff --git a/plugins/chat/test/javascripts/components/chat-message-separator-new-test.js b/plugins/chat/test/javascripts/components/chat-message-separator-new-test.js
new file mode 100644
index 00000000000..ee91a122043
--- /dev/null
+++ b/plugins/chat/test/javascripts/components/chat-message-separator-new-test.js
@@ -0,0 +1,24 @@
+import { setupRenderingTest } from "discourse/tests/helpers/component-test";
+import { query } from "discourse/tests/helpers/qunit-helpers";
+import hbs from "htmlbars-inline-precompile";
+import I18n from "I18n";
+import { module, test } from "qunit";
+import { render } from "@ember/test-helpers";
+
+module(
+ "Discourse Chat | Component | chat-message-separator-new",
+ function (hooks) {
+ setupRenderingTest(hooks);
+
+ test("newest message", async function (assert) {
+ this.set("message", { newest: true });
+
+ await render(hbs`
`);
+
+ assert.strictEqual(
+ query(".chat-message-separator-new").innerText.trim(),
+ I18n.t("chat.last_visit")
+ );
+ });
+ }
+);
diff --git a/plugins/chat/test/javascripts/components/chat-message-separator-test.js b/plugins/chat/test/javascripts/components/chat-message-separator-test.js
deleted file mode 100644
index 4b4aad0e565..00000000000
--- a/plugins/chat/test/javascripts/components/chat-message-separator-test.js
+++ /dev/null
@@ -1,35 +0,0 @@
-import { setupRenderingTest } from "discourse/tests/helpers/component-test";
-import { query } from "discourse/tests/helpers/qunit-helpers";
-import hbs from "htmlbars-inline-precompile";
-import I18n from "I18n";
-import { module, test } from "qunit";
-import { render } from "@ember/test-helpers";
-
-module("Discourse Chat | Component | chat-message-separator", function (hooks) {
- setupRenderingTest(hooks);
-
- test("newest message", async function (assert) {
- this.set("message", { newestMessage: true });
-
- await render(hbs`
`);
-
- assert.strictEqual(
- query(".chat-message-separator.new-message .text").innerText.trim(),
- I18n.t("chat.new_messages")
- );
- });
-
- test("first message of the day", async function (assert) {
- this.set("date", moment().format("LLL"));
- this.set("message", { firstMessageOfTheDayAt: this.date });
-
- await render(hbs`
`);
-
- assert.strictEqual(
- query(
- ".chat-message-separator.first-daily-message .text"
- ).innerText.trim(),
- this.date
- );
- });
-});
diff --git a/plugins/chat/test/javascripts/components/chat-message-test.js b/plugins/chat/test/javascripts/components/chat-message-test.js
index 3dbaf2737e0..7ffa9da1d08 100644
--- a/plugins/chat/test/javascripts/components/chat-message-test.js
+++ b/plugins/chat/test/javascripts/components/chat-message-test.js
@@ -1,5 +1,5 @@
import User from "discourse/models/user";
-import { render, waitFor } from "@ember/test-helpers";
+import { render } from "@ember/test-helpers";
import ChatMessage from "discourse/plugins/chat/discourse/models/chat-message";
import { exists } from "discourse/tests/helpers/qunit-helpers";
import { setupRenderingTest } from "discourse/tests/helpers/component-test";
@@ -21,9 +21,16 @@ module("Discourse Chat | Component | chat-message", function (hooks) {
unread_count: 0,
muted: false,
},
+ canInteractWithChat: true,
+ canDeleteSelf: true,
+ canDeleteOthers: true,
+ canFlag: true,
+ userSilenced: false,
+ canModerate: true,
});
return {
message: ChatMessage.create(
+ chatChannel,
Object.assign(
{
id: 178,
@@ -38,14 +45,6 @@ module("Discourse Chat | Component | chat-message", function (hooks) {
messageData
)
),
- canInteractWithChat: true,
- details: {
- can_delete_self: true,
- can_delete_others: true,
- can_flag: true,
- user_silenced: false,
- can_moderate: true,
- },
chatChannel,
setReplyTo: () => {},
replyMessageClicked: () => {},
@@ -55,8 +54,9 @@ module("Discourse Chat | Component | chat-message", function (hooks) {
onStartSelectingMessages: () => {},
onSelectMessage: () => {},
bulkSelectMessages: () => {},
- afterReactionAdded: () => {},
onHoverMessage: () => {},
+ didShowMessage: () => {},
+ didHideMessage: () => {},
};
}
@@ -64,8 +64,7 @@ module("Discourse Chat | Component | chat-message", function (hooks) {
`;
@@ -90,6 +90,7 @@ module("Discourse Chat | Component | chat-message", function (hooks) {
test("Deleted message", async function (assert) {
this.setProperties(generateMessageProps({ deleted_at: moment() }));
await render(template);
+
assert.true(
exists(".chat-message-deleted .chat-message-expand"),
"has the correct deleted css class and expand button within"
@@ -104,16 +105,4 @@ module("Discourse Chat | Component | chat-message", function (hooks) {
"has the correct hidden css class and expand button within"
);
});
-
- test("Message marked as visible", async function (assert) {
- this.setProperties(generateMessageProps());
-
- await render(template);
- await waitFor("div[data-visible=true]");
-
- assert.true(
- exists(".chat-message-container[data-visible=true]"),
- "message is marked as visible"
- );
- });
});
diff --git a/plugins/chat/test/javascripts/components/chat-retention-reminder-test.js b/plugins/chat/test/javascripts/components/chat-retention-reminder-test.js
index cb707d011f5..820e28a8308 100644
--- a/plugins/chat/test/javascripts/components/chat-retention-reminder-test.js
+++ b/plugins/chat/test/javascripts/components/chat-retention-reminder-test.js
@@ -14,9 +14,7 @@ module(
this.channel = ChatChannel.create({ chatable_type: "Category" });
this.currentUser.set("needs_channel_retention_reminder", true);
- await render(
- hbs`
`
- );
+ await render(hbs`
`);
assert.dom(".chat-retention-reminder").includesText(
I18n.t("chat.retention_reminders.public", {
diff --git a/plugins/chat/test/javascripts/helpers/fabricators.js b/plugins/chat/test/javascripts/helpers/fabricators.js
index 924685b6e6e..07cf409ee24 100644
--- a/plugins/chat/test/javascripts/helpers/fabricators.js
+++ b/plugins/chat/test/javascripts/helpers/fabricators.js
@@ -3,6 +3,7 @@ import ChatChannel, {
} from "discourse/plugins/chat/discourse/models/chat-channel";
import EmberObject from "@ember/object";
import { Fabricator } from "./fabricator";
+import ChatMessage from "discourse/plugins/chat/discourse/models/chat-message";
const userFabricator = Fabricator(EmberObject, {
id: 1,
@@ -38,7 +39,7 @@ export default {
},
}),
- chatChannelMessage: Fabricator(EmberObject, {
+ chatChannelMessage: Fabricator(ChatMessage, {
id: 1,
chat_channel_id: 1,
user_id: 1,
diff --git a/plugins/chat/test/javascripts/modifiers/track-message-visibility-test.js b/plugins/chat/test/javascripts/modifiers/track-message-visibility-test.js
deleted file mode 100644
index e05981401b3..00000000000
--- a/plugins/chat/test/javascripts/modifiers/track-message-visibility-test.js
+++ /dev/null
@@ -1,36 +0,0 @@
-import { render, waitFor } from "@ember/test-helpers";
-import { exists } from "discourse/tests/helpers/qunit-helpers";
-import { setupRenderingTest } from "discourse/tests/helpers/component-test";
-import hbs from "htmlbars-inline-precompile";
-import { module, test } from "qunit";
-
-module(
- "Discourse Chat | Modifier | track-message-visibility",
- function (hooks) {
- setupRenderingTest(hooks);
-
- test("Marks message as visible when it intersects with the viewport", async function (assert) {
- const template = hbs`
`;
-
- await render(template);
- await waitFor("div[data-visible=true]");
-
- assert.ok(
- exists("div[data-visible=true]"),
- "message is marked as visible"
- );
- });
-
- test("Marks message as visible when it doesn't intersect with the viewport", async function (assert) {
- const template = hbs`
`;
-
- await render(template);
- await waitFor("div[data-visible=false]");
-
- assert.ok(
- exists("div[data-visible=false]"),
- "message is not marked as visible"
- );
- });
- }
-);
diff --git a/plugins/chat/test/javascripts/unit/helpers/format-chat-date-test.js b/plugins/chat/test/javascripts/unit/helpers/format-chat-date-test.js
index 081732a054e..f6acc11a457 100644
--- a/plugins/chat/test/javascripts/unit/helpers/format-chat-date-test.js
+++ b/plugins/chat/test/javascripts/unit/helpers/format-chat-date-test.js
@@ -3,12 +3,19 @@ import hbs from "htmlbars-inline-precompile";
import { render } from "@ember/test-helpers";
import { setupRenderingTest } from "discourse/tests/helpers/component-test";
import { query } from "discourse/tests/helpers/qunit-helpers";
+import fabricators from "../../helpers/fabricators";
+import ChatMessage from "discourse/plugins/chat/discourse/models/chat-message";
module("Discourse Chat | Unit | Helpers | format-chat-date", function (hooks) {
setupRenderingTest(hooks);
test("link to chat message", async function (assert) {
- this.set("message", { id: 1, chat_channel_id: 1 });
+ const channel = fabricators.chatChannel();
+ this.message = ChatMessage.create(channel, {
+ id: 1,
+ chat_channel_id: channel.id,
+ });
+
await render(hbs`{{format-chat-date this.message}}`);
assert.equal(query(".chat-time").getAttribute("href"), "/chat/c/-/1/1");