diff --git a/plugins/chat/app/controllers/chat/api/current_user_threads_controller.rb b/plugins/chat/app/controllers/chat/api/current_user_threads_controller.rb
new file mode 100644
index 00000000000..5a2b5577990
--- /dev/null
+++ b/plugins/chat/app/controllers/chat/api/current_user_threads_controller.rb
@@ -0,0 +1,24 @@
+# frozen_string_literal: true
+
+class Chat::Api::CurrentUserThreadsController < Chat::ApiController
+ def index
+ with_service(::Chat::LookupUserThreads) do
+ on_success do
+ render_serialized(
+ ::Chat::ThreadsView.new(
+ user: guardian.user,
+ threads: result.threads,
+ channel: result.channel,
+ tracking: result.tracking,
+ memberships: result.memberships,
+ load_more_url: result.load_more_url,
+ threads_participants: result.participants,
+ ),
+ ::Chat::ThreadListSerializer,
+ root: false,
+ )
+ end
+ on_model_not_found(:threads) { render json: success_json.merge(threads: []) }
+ end
+ end
+end
diff --git a/plugins/chat/app/serializers/chat/thread_list_serializer.rb b/plugins/chat/app/serializers/chat/thread_list_serializer.rb
index 3454c4d25a8..0d3070a4ec5 100644
--- a/plugins/chat/app/serializers/chat/thread_list_serializer.rb
+++ b/plugins/chat/app/serializers/chat/thread_list_serializer.rb
@@ -11,6 +11,7 @@ module Chat
scope: scope,
membership: object.memberships.find { |m| m.thread_id == thread.id },
include_thread_preview: true,
+ include_channel: true,
include_thread_original_message: true,
participants: object.threads_participants[thread.id],
root: nil,
@@ -23,7 +24,9 @@ module Chat
end
def meta
- { channel_id: object.channel.id, load_more_url: object.load_more_url }
+ meta = { load_more_url: object.load_more_url }
+ meta[:channel_id] = object.channel.id if object.channel
+ meta
end
end
end
diff --git a/plugins/chat/app/serializers/chat/thread_serializer.rb b/plugins/chat/app/serializers/chat/thread_serializer.rb
index 8ba6372a2da..dcbf93e02ef 100644
--- a/plugins/chat/app/serializers/chat/thread_serializer.rb
+++ b/plugins/chat/app/serializers/chat/thread_serializer.rb
@@ -8,6 +8,7 @@ module Chat
:title,
:status,
:channel_id,
+ :channel,
:meta,
:reply_count,
:current_user_membership,
@@ -22,6 +23,14 @@ module Chat
@current_user_membership = opts[:membership]
end
+ def include_channel?
+ @options[:include_channel].presence || false
+ end
+
+ def channel
+ ::Chat::ChannelSerializer.new(object.channel, scope: scope, root: false)
+ end
+
def include_original_message?
@opts[:include_thread_original_message].presence || true
end
diff --git a/plugins/chat/app/services/chat/lookup_channel_threads.rb b/plugins/chat/app/services/chat/lookup_channel_threads.rb
index 83bb9b7b81e..aa7664d9484 100644
--- a/plugins/chat/app/services/chat/lookup_channel_threads.rb
+++ b/plugins/chat/app/services/chat/lookup_channel_threads.rb
@@ -69,7 +69,6 @@ module Chat
def fetch_threads(guardian:, channel:, **)
::Chat::Thread
- .strict_loading
.includes(
:channel,
:user_chat_thread_memberships,
@@ -93,28 +92,26 @@ module Chat
user: :user_status,
],
)
- .joins(:user_chat_thread_memberships, :original_message)
.joins(
- "LEFT JOIN chat_messages AS last_message ON last_message.id = chat_threads.last_message_id",
+ "LEFT JOIN user_chat_thread_memberships ON chat_threads.id = user_chat_thread_memberships.thread_id AND user_chat_thread_memberships.user_id = #{guardian.user.id} AND user_chat_thread_memberships.notification_level NOT IN (#{::Chat::UserChatThreadMembership.notification_levels[:muted]})",
)
- .where("user_chat_thread_memberships.user_id = ?", guardian.user.id)
- .where(
- "user_chat_thread_memberships.notification_level IN (?)",
- [
- ::Chat::UserChatThreadMembership.notification_levels[:normal],
- ::Chat::UserChatThreadMembership.notification_levels[:tracking],
- ],
+ .joins(
+ "LEFT JOIN chat_messages AS last_message ON chat_threads.last_message_id = last_message.id",
)
- .where("chat_threads.channel_id = ?", channel.id)
+ .joins(
+ "INNER JOIN chat_messages AS original_message ON chat_threads.original_message_id = original_message.id",
+ )
+ .where(channel_id: channel.id)
+ .where("original_message.chat_channel_id = chat_threads.channel_id")
+ .where("original_message.deleted_at IS NULL")
+ .where("last_message.chat_channel_id = chat_threads.channel_id")
.where("last_message.deleted_at IS NULL")
+ .where("chat_threads.replies_count > 0")
+ .order(
+ "CASE WHEN user_chat_thread_memberships.last_read_message_id IS NULL OR user_chat_thread_memberships.last_read_message_id < chat_threads.last_message_id THEN true ELSE false END DESC, last_message.created_at DESC",
+ )
.limit(context.limit)
.offset(context.offset)
- .order(
- "CASE WHEN (
- chat_threads.last_message_id > user_chat_thread_memberships.last_read_message_id OR
- user_chat_thread_memberships.last_read_message_id IS NULL
- ) THEN 0 ELSE 1 END, last_message.created_at DESC",
- )
end
def fetch_tracking(guardian:, threads:, **)
diff --git a/plugins/chat/app/services/chat/lookup_user_threads.rb b/plugins/chat/app/services/chat/lookup_user_threads.rb
new file mode 100644
index 00000000000..139306a0d9d
--- /dev/null
+++ b/plugins/chat/app/services/chat/lookup_user_threads.rb
@@ -0,0 +1,145 @@
+# frozen_string_literal: true
+
+module Chat
+ # Gets a list of threads for a user.
+ #
+ # Only threads that the user is a member of with a notification level
+ # of normal or tracking will be returned.
+ #
+ # @example
+ # Chat::LookupUserThreads.call(guardian: guardian, limit: 5, offset: 2)
+ #
+ class LookupUserThreads
+ include Service::Base
+
+ THREADS_LIMIT = 10
+
+ # @!method call(guardian:, limit: nil, offset: nil)
+ # @param [Guardian] guardian
+ # @param [Integer] limit
+ # @param [Integer] offset
+ # @return [Service::Base::Context]
+
+ contract
+ step :set_limit
+ step :set_offset
+ model :threads
+ step :fetch_tracking
+ step :fetch_memberships
+ step :fetch_participants
+ step :build_load_more_url
+
+ # @!visibility private
+ class Contract
+ attribute :limit, :integer
+ attribute :offset, :integer
+ end
+
+ private
+
+ def set_limit(contract:, **)
+ context.limit = (contract.limit || THREADS_LIMIT).to_i.clamp(1, THREADS_LIMIT)
+ end
+
+ def set_offset(contract:, **)
+ context.offset = [contract.offset || 0, 0].max
+ end
+
+ def fetch_threads(guardian:, **)
+ ::Chat::Thread
+ .includes(
+ :channel,
+ :user_chat_thread_memberships,
+ original_message_user: :user_status,
+ last_message: [
+ :uploads,
+ :chat_webhook_event,
+ :chat_channel,
+ chat_mentions: {
+ user: :user_status,
+ },
+ user: :user_status,
+ ],
+ original_message: [
+ :uploads,
+ :chat_webhook_event,
+ :chat_channel,
+ chat_mentions: {
+ user: :user_status,
+ },
+ user: :user_status,
+ ],
+ )
+ .joins(
+ "INNER JOIN user_chat_thread_memberships ON chat_threads.id = user_chat_thread_memberships.thread_id",
+ )
+ .joins(
+ "LEFT JOIN chat_messages AS last_message ON chat_threads.last_message_id = last_message.id",
+ )
+ .joins(
+ "INNER JOIN chat_messages AS original_message ON chat_threads.original_message_id = original_message.id",
+ )
+ .where(
+ channel_id:
+ ::Chat::Channel
+ .joins(:user_chat_channel_memberships)
+ .where(user_chat_channel_memberships: { user_id: guardian.user.id, following: true })
+ .where.not("user_chat_channel_memberships.muted")
+ .where(
+ {
+ chatable_type: "Category",
+ threading_enabled: true,
+ status: ::Chat::Channel.statuses[:open],
+ },
+ )
+ .select(:id),
+ )
+ .where("original_message.chat_channel_id = chat_threads.channel_id")
+ .where("original_message.deleted_at IS NULL")
+ .where("last_message.chat_channel_id = chat_threads.channel_id")
+ .where("last_message.deleted_at IS NULL")
+ .where("chat_threads.replies_count > 0")
+ .where("user_chat_thread_memberships.user_id = ?", guardian.user.id)
+ .where(
+ "user_chat_thread_memberships.notification_level IN (?)",
+ [
+ ::Chat::UserChatThreadMembership.notification_levels[:normal],
+ ::Chat::UserChatThreadMembership.notification_levels[:tracking],
+ ],
+ )
+ .order(
+ "CASE WHEN user_chat_thread_memberships.last_read_message_id IS NULL OR user_chat_thread_memberships.last_read_message_id < chat_threads.last_message_id THEN true ELSE false END DESC, last_message.created_at DESC",
+ )
+ .limit(context.limit)
+ .offset(context.offset)
+ end
+
+ def fetch_tracking(guardian:, threads:, **)
+ context.tracking =
+ ::Chat::TrackingStateReportQuery.call(
+ guardian: guardian,
+ thread_ids: threads.map(&:id),
+ include_threads: true,
+ ).thread_tracking
+ end
+
+ def fetch_memberships(guardian:, threads:, **)
+ context.memberships =
+ ::Chat::UserChatThreadMembership.where(
+ thread_id: threads.map(&:id),
+ user_id: guardian.user.id,
+ )
+ end
+
+ def fetch_participants(threads:, **)
+ context.participants = ::Chat::ThreadParticipantQuery.call(thread_ids: threads.map(&:id))
+ end
+
+ def build_load_more_url(contract:, **)
+ load_more_params = { limit: context.limit, offset: context.offset + context.limit }.to_query
+
+ context.load_more_url =
+ ::URI::HTTP.build(path: "/chat/api/me/threads", query: load_more_params).request_uri
+ end
+ end
+end
diff --git a/plugins/chat/assets/javascripts/discourse/chat-route-map.js b/plugins/chat/assets/javascripts/discourse/chat-route-map.js
index 2131b6b82cd..e795d21a2c4 100644
--- a/plugins/chat/assets/javascripts/discourse/chat-route-map.js
+++ b/plugins/chat/assets/javascripts/discourse/chat-route-map.js
@@ -8,6 +8,8 @@ export default function () {
});
});
+ this.route("threads", { path: "/threads" });
+
this.route(
"channel.info",
{ path: "/c/:channelTitle/:channelId/info" },
diff --git a/plugins/chat/assets/javascripts/discourse/components/channel-title/index.gjs b/plugins/chat/assets/javascripts/discourse/components/channel-title/index.gjs
new file mode 100644
index 00000000000..b33add9d8c0
--- /dev/null
+++ b/plugins/chat/assets/javascripts/discourse/components/channel-title/index.gjs
@@ -0,0 +1,111 @@
+import Component from "@glimmer/component";
+import { get, hash } from "@ember/helper";
+import { inject as service } from "@ember/service";
+import { htmlSafe } from "@ember/template";
+import PluginOutlet from "discourse/components/plugin-outlet";
+import UserStatusMessage from "discourse/components/user-status-message";
+import replaceEmoji from "discourse/helpers/replace-emoji";
+import icon from "discourse-common/helpers/d-icon";
+import ChatUserAvatar from "discourse/plugins/chat/discourse/components/chat-user-avatar";
+
+export default class ChatChannelTitle extends Component {
+ @service currentUser;
+
+ get firstUser() {
+ return this.args.channel.chatable.users[0];
+ }
+
+ get users() {
+ return this.args.channel.chatable.users;
+ }
+
+ get groupDirectMessage() {
+ return (
+ this.args.channel.isDirectMessageChannel &&
+ this.args.channel.chatable.group
+ );
+ }
+
+ get groupsDirectMessageTitle() {
+ return this.args.channel.title || this.usernames;
+ }
+
+ get usernames() {
+ return this.users.mapBy("username").join(", ");
+ }
+
+ get channelColorStyle() {
+ return htmlSafe(`color: #${this.args.channel.chatable.color}`);
+ }
+
+ get showUserStatus() {
+ return !!(this.users.length === 1 && this.users[0].status);
+ }
+
+
+ {{#if @channel.isDirectMessageChannel}}
+
+ {{#if this.groupDirectMessage}}
+
+ {{@channel.membershipsCount}}
+
+ {{else}}
+
+
+
+ {{/if}}
+
+
+
+ {{#if this.groupDirectMessage}}
+
+ {{this.groupsDirectMessageTitle}}
+
+ {{else}}
+
+ {{this.firstUser.username}}
+
+ {{#if this.showUserStatus}}
+
+ {{/if}}
+
+ {{/if}}
+
+
+
+
+ {{#if (has-block)}}
+ {{yield}}
+ {{/if}}
+
+ {{else if @channel.isCategoryChannel}}
+
+
+ {{icon "d-chat"}}
+ {{#if @channel.chatable.read_restricted}}
+ {{icon "lock" class="chat-channel-title__restricted-category-icon"}}
+ {{/if}}
+
+
+ {{replaceEmoji @channel.title}}
+
+
+ {{#if (has-block)}}
+ {{yield}}
+ {{/if}}
+
+ {{/if}}
+
+}
diff --git a/plugins/chat/assets/javascripts/discourse/components/channels-list.gjs b/plugins/chat/assets/javascripts/discourse/components/channels-list.gjs
index 44114c8fb61..ae12313e11c 100644
--- a/plugins/chat/assets/javascripts/discourse/components/channels-list.gjs
+++ b/plugins/chat/assets/javascripts/discourse/components/channels-list.gjs
@@ -100,6 +100,12 @@ export default class ChannelsList extends Component {
}`;
}
+ get hasUnreadThreads() {
+ return this.chatChannelsManager.publicMessageChannels.some(
+ (channel) => channel.unreadThreadsCount > 0
+ );
+ }
+
@action
toggleChannelSection(section) {
this.args.toggleSection(section);
@@ -160,6 +166,24 @@ export default class ChannelsList extends Component {
{{didInsert this.computeHasScrollbar}}
{{onResize this.computeResizedEntries}}
>
+
+
+
+
+
+ {{dIcon "discourse-threads" class="chat__user-threads-row__icon"}}
+ {{i18n "chat.my_threads.title"}}
+
+
+ {{#if this.hasUnreadThreads}}
+
+ {{/if}}
+
+
+
+
{{#if this.displayPublicChannels}}
{{#if this.inSidebar}}
diff --git a/plugins/chat/assets/javascripts/discourse/components/chat-channel-chooser-header.hbs b/plugins/chat/assets/javascripts/discourse/components/chat-channel-chooser-header.hbs
index 7bbe54bd071..39dc5c2d9eb 100644
--- a/plugins/chat/assets/javascripts/discourse/components/chat-channel-chooser-header.hbs
+++ b/plugins/chat/assets/javascripts/discourse/components/chat-channel-chooser-header.hbs
@@ -1,6 +1,6 @@
-
+
{{#if this.canEditChannel}}
-
+
{{#if this.hasDescription}}
{{@channel.description}}
diff --git a/plugins/chat/assets/javascripts/discourse/components/chat-channel-row.gjs b/plugins/chat/assets/javascripts/discourse/components/chat-channel-row.gjs
index f8f1d1366e5..a95b186d82b 100644
--- a/plugins/chat/assets/javascripts/discourse/components/chat-channel-row.gjs
+++ b/plugins/chat/assets/javascripts/discourse/components/chat-channel-row.gjs
@@ -15,8 +15,8 @@ import { bind } from "discourse-common/utils/decorators";
import I18n from "discourse-i18n";
import and from "truth-helpers/helpers/and";
import eq from "truth-helpers/helpers/eq";
+import ChannelTitle from "discourse/plugins/chat/discourse/components/channel-title";
import ChatChannelMetadata from "discourse/plugins/chat/discourse/components/chat-channel-metadata";
-import ChatChannelTitle from "discourse/plugins/chat/discourse/components/chat-channel-title";
import ToggleChannelMembershipButton from "discourse/plugins/chat/discourse/components/toggle-channel-membership-button";
const FADEOUT_CLASS = "-fade-out";
@@ -184,7 +184,7 @@ export default class ChatChannelRow extends Component {
{{(if this.shouldReset (modifier this.onReset))}}
style={{this.rowStyle}}
>
-
+
{{#if
diff --git a/plugins/chat/assets/javascripts/discourse/components/chat-channel-title.gjs b/plugins/chat/assets/javascripts/discourse/components/chat-channel-title.gjs
index b33add9d8c0..1b3f8a8d8c2 100644
--- a/plugins/chat/assets/javascripts/discourse/components/chat-channel-title.gjs
+++ b/plugins/chat/assets/javascripts/discourse/components/chat-channel-title.gjs
@@ -1,111 +1,8 @@
import Component from "@glimmer/component";
-import { get, hash } from "@ember/helper";
-import { inject as service } from "@ember/service";
-import { htmlSafe } from "@ember/template";
-import PluginOutlet from "discourse/components/plugin-outlet";
-import UserStatusMessage from "discourse/components/user-status-message";
-import replaceEmoji from "discourse/helpers/replace-emoji";
-import icon from "discourse-common/helpers/d-icon";
-import ChatUserAvatar from "discourse/plugins/chat/discourse/components/chat-user-avatar";
-
-export default class ChatChannelTitle extends Component {
- @service currentUser;
-
- get firstUser() {
- return this.args.channel.chatable.users[0];
- }
-
- get users() {
- return this.args.channel.chatable.users;
- }
-
- get groupDirectMessage() {
- return (
- this.args.channel.isDirectMessageChannel &&
- this.args.channel.chatable.group
- );
- }
-
- get groupsDirectMessageTitle() {
- return this.args.channel.title || this.usernames;
- }
-
- get usernames() {
- return this.users.mapBy("username").join(", ");
- }
-
- get channelColorStyle() {
- return htmlSafe(`color: #${this.args.channel.chatable.color}`);
- }
-
- get showUserStatus() {
- return !!(this.users.length === 1 && this.users[0].status);
- }
+import ChannelTitle from "discourse/plugins/chat/discourse/components/channel-title";
+export default class OldChatChannelTitle extends Component {
- {{#if @channel.isDirectMessageChannel}}
-
- {{#if this.groupDirectMessage}}
-
- {{@channel.membershipsCount}}
-
- {{else}}
-
-
-
- {{/if}}
-
-
-
- {{#if this.groupDirectMessage}}
-
- {{this.groupsDirectMessageTitle}}
-
- {{else}}
-
- {{this.firstUser.username}}
-
- {{#if this.showUserStatus}}
-
- {{/if}}
-
- {{/if}}
-
-
-
-
- {{#if (has-block)}}
- {{yield}}
- {{/if}}
-
- {{else if @channel.isCategoryChannel}}
-
-
- {{icon "d-chat"}}
- {{#if @channel.chatable.read_restricted}}
- {{icon "lock" class="chat-channel-title__restricted-category-icon"}}
- {{/if}}
-
-
- {{replaceEmoji @channel.title}}
-
-
- {{#if (has-block)}}
- {{yield}}
- {{/if}}
-
- {{/if}}
+
}
diff --git a/plugins/chat/assets/javascripts/discourse/components/chat-drawer/threads.gjs b/plugins/chat/assets/javascripts/discourse/components/chat-drawer/channel-threads.gjs
similarity index 97%
rename from plugins/chat/assets/javascripts/discourse/components/chat-drawer/threads.gjs
rename to plugins/chat/assets/javascripts/discourse/components/chat-drawer/channel-threads.gjs
index 5c0fe0ca2c4..fa2a0013a13 100644
--- a/plugins/chat/assets/javascripts/discourse/components/chat-drawer/threads.gjs
+++ b/plugins/chat/assets/javascripts/discourse/components/chat-drawer/channel-threads.gjs
@@ -10,7 +10,7 @@ import ChatDrawerHeaderRightActions from "discourse/plugins/chat/discourse/compo
import ChatDrawerHeaderTitle from "discourse/plugins/chat/discourse/components/chat-drawer/header/title";
import ChatThreadList from "discourse/plugins/chat/discourse/components/chat-thread-list";
-export default class ChatDrawerThreads extends Component {
+export default class ChatDrawerChannelThreads extends Component {
@service appEvents;
@service chat;
@service chatStateManager;
diff --git a/plugins/chat/assets/javascripts/discourse/components/chat-drawer/header/channel-title.gjs b/plugins/chat/assets/javascripts/discourse/components/chat-drawer/header/channel-title.gjs
index 9c8afafa725..8acd4089387 100644
--- a/plugins/chat/assets/javascripts/discourse/components/chat-drawer/header/channel-title.gjs
+++ b/plugins/chat/assets/javascripts/discourse/components/chat-drawer/header/channel-title.gjs
@@ -2,7 +2,7 @@ import Component from "@glimmer/component";
import { on } from "@ember/modifier";
import { LinkTo } from "@ember/routing";
import { inject as service } from "@ember/service";
-import ChatChannelTitle from "../../chat-channel-title";
+import ChannelTitle from "../../channel-title";
export default class ChatDrawerChannelHeaderTitle extends Component {
@service chatStateManager;
@@ -20,7 +20,7 @@ export default class ChatDrawerChannelHeaderTitle extends Component {
class="chat-drawer-header__title"
>
{{else}}
@@ -30,13 +30,13 @@ export default class ChatDrawerChannelHeaderTitle extends Component {
class="chat-drawer-header__title"
>
{{/if}}
diff --git a/plugins/chat/assets/javascripts/discourse/components/chat-drawer/thread.gjs b/plugins/chat/assets/javascripts/discourse/components/chat-drawer/thread.gjs
index c5a62a93e76..d41f2ba39c5 100644
--- a/plugins/chat/assets/javascripts/discourse/components/chat-drawer/thread.gjs
+++ b/plugins/chat/assets/javascripts/discourse/components/chat-drawer/thread.gjs
@@ -27,6 +27,10 @@ export default class ChatDrawerThread extends Component {
if (this.chatHistory.previousRoute?.name === "chat.channel.threads") {
link.title = I18n.t("chat.return_to_threads_list");
link.route = "chat.channel.threads";
+ } else if (this.chatHistory.previousRoute?.name === "chat.threads") {
+ link.title = I18n.t("chat.my_threads.title");
+ link.route = "chat.threads";
+ link.models = [];
} else {
link.title = I18n.t("chat.return_to_channel");
link.route = "chat.channel";
diff --git a/plugins/chat/assets/javascripts/discourse/components/chat-drawer/threads/index.gjs b/plugins/chat/assets/javascripts/discourse/components/chat-drawer/threads/index.gjs
new file mode 100644
index 00000000000..2f0668a1a15
--- /dev/null
+++ b/plugins/chat/assets/javascripts/discourse/components/chat-drawer/threads/index.gjs
@@ -0,0 +1,47 @@
+import Component from "@glimmer/component";
+import { inject as service } from "@ember/service";
+import I18n from "discourse-i18n";
+import ChatDrawerHeader from "discourse/plugins/chat/discourse/components/chat-drawer/header";
+import ChatDrawerHeaderBackLink from "discourse/plugins/chat/discourse/components/chat-drawer/header/back-link";
+import ChatDrawerHeaderRightActions from "discourse/plugins/chat/discourse/components/chat-drawer/header/right-actions";
+import ChatDrawerHeaderTitle from "discourse/plugins/chat/discourse/components/chat-drawer/header/title";
+import UserThreads from "discourse/plugins/chat/discourse/components/user-threads";
+
+export default class ChatDrawerThreads extends Component {
+ @service appEvents;
+ @service chat;
+ @service chatStateManager;
+ @service chatChannelsManager;
+
+ backLinkTitle = I18n.t("chat.return_to_list");
+
+
+
+
+ {{#if this.chatStateManager.isDrawerExpanded}}
+
+ {{/if}}
+
+
+
+
+
+
+ {{#if this.chatStateManager.isDrawerExpanded}}
+
+
+
+ {{/if}}
+
+}
diff --git a/plugins/chat/assets/javascripts/discourse/components/chat-full-page-header.gjs b/plugins/chat/assets/javascripts/discourse/components/chat-full-page-header.gjs
index 9e7180c643f..16e5346fbe5 100644
--- a/plugins/chat/assets/javascripts/discourse/components/chat-full-page-header.gjs
+++ b/plugins/chat/assets/javascripts/discourse/components/chat-full-page-header.gjs
@@ -1,4 +1,5 @@
import Component from "@glimmer/component";
+import { on } from "@ember/modifier";
import { action } from "@ember/object";
import { LinkTo } from "@ember/routing";
import { inject as service } from "@ember/service";
@@ -7,10 +8,10 @@ import concatClass from "discourse/helpers/concat-class";
import icon from "discourse-common/helpers/d-icon";
import and from "truth-helpers/helpers/and";
import or from "truth-helpers/helpers/or";
+import ChannelTitle from "discourse/plugins/chat/discourse/components/channel-title";
import ChatModalEditChannelName from "discourse/plugins/chat/discourse/components/chat/modal/edit-channel-name";
import ThreadsListButton from "discourse/plugins/chat/discourse/components/chat/thread/threads-list-button";
import ChatChannelStatus from "discourse/plugins/chat/discourse/components/chat-channel-status";
-import ChatChannelTitle from "discourse/plugins/chat/discourse/components/chat-channel-title";
export default class ChatFullPageHeader extends Component {
@service chatStateManager;
@@ -38,13 +39,20 @@ export default class ChatFullPageHeader extends Component {
});
}
+ @action
+ trapMouse(event) {
+ event.stopPropagation();
+ }
+
+ {{! template-lint-disable no-invalid-interactive }}
{{#if (and this.chatStateManager.isFullPageActive this.displayed)}}
diff --git a/plugins/chat/assets/javascripts/discourse/components/thread-title/index.gjs b/plugins/chat/assets/javascripts/discourse/components/thread-title/index.gjs
new file mode 100644
index 00000000000..9b823415a34
--- /dev/null
+++ b/plugins/chat/assets/javascripts/discourse/components/thread-title/index.gjs
@@ -0,0 +1,27 @@
+import Component from "@glimmer/component";
+import { htmlSafe } from "@ember/template";
+import replaceEmoji from "discourse/helpers/replace-emoji";
+import { escapeExpression } from "discourse/lib/utilities";
+import ThreadUnreadIndicator from "discourse/plugins/chat/discourse/components/thread-unread-indicator";
+
+export default class ChatThreadTitle extends Component {
+ get title() {
+ if (this.args.thread.title) {
+ return replaceEmoji(htmlSafe(escapeExpression(this.args.thread.title)));
+ } else {
+ return replaceEmoji(htmlSafe(this.args.thread.originalMessage.excerpt));
+ }
+ }
+
+
+
+
+
+ {{this.title}}
+
+
+
+
+
+
+}
diff --git a/plugins/chat/assets/javascripts/discourse/components/chat/thread-list/item/unread-indicator.gjs b/plugins/chat/assets/javascripts/discourse/components/thread-unread-indicator/index.gjs
similarity index 88%
rename from plugins/chat/assets/javascripts/discourse/components/chat/thread-list/item/unread-indicator.gjs
rename to plugins/chat/assets/javascripts/discourse/components/thread-unread-indicator/index.gjs
index 3d1d87943e7..6fe990964f3 100644
--- a/plugins/chat/assets/javascripts/discourse/components/chat/thread-list/item/unread-indicator.gjs
+++ b/plugins/chat/assets/javascripts/discourse/components/thread-unread-indicator/index.gjs
@@ -1,6 +1,6 @@
import Component from "@glimmer/component";
-export default class ChatThreadListItemUnreadIndicator extends Component {
+export default class ChatThreadUnreadIndicator extends Component {
get unreadCount() {
return this.args.thread.tracking.unreadCount;
}
diff --git a/plugins/chat/assets/javascripts/discourse/components/user-threads/index.gjs b/plugins/chat/assets/javascripts/discourse/components/user-threads/index.gjs
new file mode 100644
index 00000000000..baca79be4e6
--- /dev/null
+++ b/plugins/chat/assets/javascripts/discourse/components/user-threads/index.gjs
@@ -0,0 +1,107 @@
+import Component from "@glimmer/component";
+import { cached } from "@glimmer/tracking";
+import { action } from "@ember/object";
+import { inject as service } from "@ember/service";
+import { modifier } from "ember-modifier";
+import ConditionalLoadingSpinner from "discourse/components/conditional-loading-spinner";
+import isElementInViewport from "discourse/lib/is-element-in-viewport";
+import { INPUT_DELAY } from "discourse-common/config/environment";
+import discourseDebounce from "discourse-common/lib/debounce";
+import { bind } from "discourse-common/utils/decorators";
+import ChannelTitle from "discourse/plugins/chat/discourse/components/channel-title";
+import ThreadIndicator from "discourse/plugins/chat/discourse/components/chat-message-thread-indicator";
+import ThreadTitle from "discourse/plugins/chat/discourse/components/thread-title";
+import ChatChannel from "discourse/plugins/chat/discourse/models/chat-channel";
+import ChatThread from "discourse/plugins/chat/discourse/models/chat-thread";
+
+export default class UserThreads extends Component {
+ @service chat;
+ @service chatApi;
+ @service router;
+
+ loadMore = modifier((element) => {
+ this.intersectionObserver = new IntersectionObserver(this.loadThreads);
+ this.intersectionObserver.observe(element);
+
+ return () => {
+ this.intersectionObserver.disconnect();
+ };
+ });
+
+ fill = modifier((element) => {
+ this.resizeObserver = new ResizeObserver(() => {
+ if (isElementInViewport(element)) {
+ this.loadThreads();
+ }
+ });
+
+ this.resizeObserver.observe(element);
+
+ return () => {
+ this.resizeObserver.disconnect();
+ };
+ });
+
+ @cached
+ get threadsCollection() {
+ return this.chatApi.userThreads(this.handleLoadedThreads);
+ }
+
+ @action
+ loadThreads() {
+ discourseDebounce(this, this.debouncedLoadThreads, INPUT_DELAY);
+ }
+
+ async debouncedLoadThreads() {
+ await this.threadsCollection.load({ limit: 10 });
+ }
+
+ @bind
+ handleLoadedThreads(result) {
+ return result.threads.map((threadObject) => {
+ const channel = ChatChannel.create(threadObject.channel);
+ const thread = ChatThread.create(channel, threadObject);
+ const tracking = result.tracking[thread.id];
+ if (tracking) {
+ thread.tracking.mentionCount = tracking.mention_count;
+ thread.tracking.unreadCount = tracking.unread_count;
+ }
+ return thread;
+ });
+ }
+
+
+
+
+ {{#each this.threadsCollection.items as |thread|}}
+
+ {{/each}}
+
+
+
+
+
+
+
+
+
+}
diff --git a/plugins/chat/assets/javascripts/discourse/initializers/chat-sidebar.js b/plugins/chat/assets/javascripts/discourse/initializers/chat-sidebar.js
index e724d8f830b..9e997b83ecd 100644
--- a/plugins/chat/assets/javascripts/discourse/initializers/chat-sidebar.js
+++ b/plugins/chat/assets/javascripts/discourse/initializers/chat-sidebar.js
@@ -42,6 +42,59 @@ export default {
});
withPluginApi("1.3.0", (api) => {
+ api.addSidebarSection(
+ (BaseCustomSidebarSection, BaseCustomSidebarSectionLink) => {
+ const SidebarChatMyThreadsSectionLink = class extends BaseCustomSidebarSectionLink {
+ route = "chat.threads";
+ text = I18n.t("chat.my_threads.title");
+ title = I18n.t("chat.my_threads.title");
+ name = "user-threads";
+ prefixType = "icon";
+ prefixValue = "discourse-threads";
+ suffixType = "icon";
+ suffixCSSClass = "unread";
+
+ constructor() {
+ super(...arguments);
+
+ if (container.isDestroyed) {
+ return;
+ }
+
+ this.chatChannelsManager = container.lookup(
+ "service:chat-channels-manager"
+ );
+ }
+
+ get suffixValue() {
+ return this.chatChannelsManager.publicMessageChannels.some(
+ (channel) => channel.unreadThreadsCount > 0
+ )
+ ? "circle"
+ : "";
+ }
+ };
+
+ const SidebarChatMyThreadsSection = class extends BaseCustomSidebarSection {
+ // we only show `My Threads` link
+ hideSectionHeader = true;
+
+ name = "user-threads";
+
+ // sidebar API doesn’t let you have undefined values
+ // even if you don't show the section’s header
+ title = "";
+
+ get links() {
+ return [new SidebarChatMyThreadsSectionLink()];
+ }
+ };
+
+ return SidebarChatMyThreadsSection;
+ },
+ CHAT_PANEL
+ );
+
if (this.siteSettings.enable_public_channels) {
api.addSidebarSection(
(BaseCustomSidebarSection, BaseCustomSidebarSectionLink) => {
diff --git a/plugins/chat/assets/javascripts/discourse/models/chat-channel.js b/plugins/chat/assets/javascripts/discourse/models/chat-channel.js
index ee93f91f850..a04f8dcd7ad 100644
--- a/plugins/chat/assets/javascripts/discourse/models/chat-channel.js
+++ b/plugins/chat/assets/javascripts/discourse/models/chat-channel.js
@@ -110,6 +110,10 @@ export default class ChatChannel {
).length;
}
+ get unreadThreadsCount() {
+ return Array.from(this.threadsManager.unreadThreadOverview.values()).length;
+ }
+
updateLastViewedAt() {
this.currentUserMembership.lastViewedAt = new Date();
}
diff --git a/plugins/chat/assets/javascripts/discourse/models/chat-thread.js b/plugins/chat/assets/javascripts/discourse/models/chat-thread.js
index 58871f2131c..9df1277db9d 100644
--- a/plugins/chat/assets/javascripts/discourse/models/chat-thread.js
+++ b/plugins/chat/assets/javascripts/discourse/models/chat-thread.js
@@ -45,6 +45,10 @@ export default class ChatThread {
? ChatMessage.create(channel, args.original_message)
: null;
+ if (this.originalMessage) {
+ this.originalMessage.thread = this;
+ }
+
this.title = args.title;
if (args.current_user_membership) {
diff --git a/plugins/chat/assets/javascripts/discourse/routes/chat-threads.js b/plugins/chat/assets/javascripts/discourse/routes/chat-threads.js
new file mode 100644
index 00000000000..fa720bcc825
--- /dev/null
+++ b/plugins/chat/assets/javascripts/discourse/routes/chat-threads.js
@@ -0,0 +1,10 @@
+import { inject as service } from "@ember/service";
+import DiscourseRoute from "discourse/routes/discourse";
+
+export default class ChatChannelThreads extends DiscourseRoute {
+ @service chat;
+
+ activate() {
+ this.chat.activeChannel = null;
+ }
+}
diff --git a/plugins/chat/assets/javascripts/discourse/routes/chat.js b/plugins/chat/assets/javascripts/discourse/routes/chat.js
index e4ffe65c6c8..5f9c3473575 100644
--- a/plugins/chat/assets/javascripts/discourse/routes/chat.js
+++ b/plugins/chat/assets/javascripts/discourse/routes/chat.js
@@ -28,6 +28,7 @@ export default class ChatRoute extends DiscourseRoute {
const INTERCEPTABLE_ROUTES = [
"chat.channel",
+ "chat.threads",
"chat.channel.thread",
"chat.channel.thread.index",
"chat.channel.thread.near-message",
diff --git a/plugins/chat/assets/javascripts/discourse/services/chat-api.js b/plugins/chat/assets/javascripts/discourse/services/chat-api.js
index b32c55770a0..a57fbbd81ce 100644
--- a/plugins/chat/assets/javascripts/discourse/services/chat-api.js
+++ b/plugins/chat/assets/javascripts/discourse/services/chat-api.js
@@ -273,7 +273,7 @@ export default class ChatApi extends Service {
* @returns {Promise}
*/
listCurrentUserChannels() {
- return this.#getRequest("/channels/me");
+ return this.#getRequest("/me/channels");
}
/**
@@ -308,6 +308,15 @@ export default class ChatApi extends Service {
return this.#deleteRequest(`/channels/${channelId}/memberships/me`);
}
+ /**
+ * Get the list of tracked threads for the current user.
+ *
+ * @returns {Promise}
+ */
+ userThreads(handler) {
+ return new Collection(`${this.#basePath}/me/threads`, handler);
+ }
+
/**
* 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-drawer-router.js b/plugins/chat/assets/javascripts/discourse/services/chat-drawer-router.js
index 74df18df021..372fb410f3e 100644
--- a/plugins/chat/assets/javascripts/discourse/services/chat-drawer-router.js
+++ b/plugins/chat/assets/javascripts/discourse/services/chat-drawer-router.js
@@ -1,6 +1,7 @@
import { tracked } from "@glimmer/tracking";
import Service, { inject as service } from "@ember/service";
import ChatDrawerChannel from "discourse/plugins/chat/discourse/components/chat-drawer/channel";
+import ChatDrawerChannelThreads from "discourse/plugins/chat/discourse/components/chat-drawer/channel-threads";
import ChatDrawerIndex from "discourse/plugins/chat/discourse/components/chat-drawer/index";
import ChatDrawerThread from "discourse/plugins/chat/discourse/components/chat-drawer/thread";
import ChatDrawerThreads from "discourse/plugins/chat/discourse/components/chat-drawer/threads";
@@ -36,13 +37,16 @@ const ROUTES = {
},
},
"chat.channel.threads": {
- name: ChatDrawerThreads,
+ name: ChatDrawerChannelThreads,
extractParams: (route) => {
return {
channelId: route.parent.params.channelId,
};
},
},
+ "chat.threads": {
+ name: ChatDrawerThreads,
+ },
chat: { name: ChatDrawerIndex },
"chat.channel.near-message": {
name: ChatDrawerChannel,
diff --git a/plugins/chat/assets/javascripts/discourse/templates/admin-plugins-chat.hbs b/plugins/chat/assets/javascripts/discourse/templates/admin-plugins-chat.hbs
index dc1d5efd71d..a7f3a9a2dd1 100644
--- a/plugins/chat/assets/javascripts/discourse/templates/admin-plugins-chat.hbs
+++ b/plugins/chat/assets/javascripts/discourse/templates/admin-plugins-chat.hbs
@@ -168,7 +168,7 @@
{{/if}}
-
+
{{webhook.description}}
diff --git a/plugins/chat/assets/javascripts/discourse/templates/chat-threads.hbs b/plugins/chat/assets/javascripts/discourse/templates/chat-threads.hbs
new file mode 100644
index 00000000000..af4ef75da3c
--- /dev/null
+++ b/plugins/chat/assets/javascripts/discourse/templates/chat-threads.hbs
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/plugins/chat/assets/stylesheets/common/base-common.scss b/plugins/chat/assets/stylesheets/common/base-common.scss
index a7f3489a39d..03ab3f32f83 100644
--- a/plugins/chat/assets/stylesheets/common/base-common.scss
+++ b/plugins/chat/assets/stylesheets/common/base-common.scss
@@ -198,7 +198,6 @@ body.has-full-page-chat {
background: var(--d-content-background);
.chat-full-page-header {
- border-top: 1px solid var(--primary-low);
border-bottom: 1px solid var(--primary-low);
background: var(--secondary);
z-index: 3;
@@ -363,7 +362,6 @@ html.has-full-page-chat {
.main-chat-outlet {
min-height: 0;
- overflow: hidden;
}
}
}
diff --git a/plugins/chat/assets/stylesheets/common/chat-channel-title.scss b/plugins/chat/assets/stylesheets/common/chat-channel-title.scss
index f6a951e8446..1e5f228710b 100644
--- a/plugins/chat/assets/stylesheets/common/chat-channel-title.scss
+++ b/plugins/chat/assets/stylesheets/common/chat-channel-title.scss
@@ -42,6 +42,10 @@
}
}
+ .d-icon {
+ position: unset;
+ }
+
.d-icon-lock {
margin-right: 0.25em;
}
diff --git a/plugins/chat/assets/stylesheets/common/chat-drawer.scss b/plugins/chat/assets/stylesheets/common/chat-drawer.scss
index ec1ca9bc93d..63024e30251 100644
--- a/plugins/chat/assets/stylesheets/common/chat-drawer.scss
+++ b/plugins/chat/assets/stylesheets/common/chat-drawer.scss
@@ -260,9 +260,12 @@ a.chat-drawer-header__title {
}
.chat-drawer-content {
+ @include chat-scrollbar();
box-sizing: border-box;
height: 100%;
min-height: 1px;
padding-bottom: 0.25em;
position: relative;
+ overflow-y: auto;
+ overscroll-behavior: contain;
}
diff --git a/plugins/chat/assets/stylesheets/common/chat-navbar.scss b/plugins/chat/assets/stylesheets/common/chat-navbar.scss
new file mode 100644
index 00000000000..435ef752f6c
--- /dev/null
+++ b/plugins/chat/assets/stylesheets/common/chat-navbar.scss
@@ -0,0 +1,23 @@
+.chat-navbar {
+ flex-shrink: 0;
+ display: flex;
+ align-items: center;
+ width: 100%;
+
+ &-container {
+ padding-inline: 1rem;
+ position: sticky;
+ top: var(--header-offset);
+ border-bottom: 1px solid var(--primary-low);
+ background: var(--secondary);
+ height: 50px;
+ box-sizing: border-box;
+ display: flex;
+ z-index: z("header") - 1;
+ }
+}
+
+.chat-navbar__right-actions {
+ list-style: none;
+ margin-left: auto;
+}
diff --git a/plugins/chat/assets/stylesheets/common/chat-thread-list-item.scss b/plugins/chat/assets/stylesheets/common/chat-thread-list-item.scss
index 3d010ed2de9..21e84ebfeb4 100644
--- a/plugins/chat/assets/stylesheets/common/chat-thread-list-item.scss
+++ b/plugins/chat/assets/stylesheets/common/chat-thread-list-item.scss
@@ -120,11 +120,6 @@
&__unread-indicator {
flex: 0 0 auto;
-
- .chat-thread-list-item-unread-indicator__number {
- color: var(--primary);
- font-size: var(--font-up-1);
- }
}
&__open-button {
diff --git a/plugins/chat/assets/stylesheets/common/chat-thread-title.scss b/plugins/chat/assets/stylesheets/common/chat-thread-title.scss
new file mode 100644
index 00000000000..e229d0f6dd5
--- /dev/null
+++ b/plugins/chat/assets/stylesheets/common/chat-thread-title.scss
@@ -0,0 +1,7 @@
+.chat__thread-title {
+ display: flex;
+
+ .chat-thread-list-item-unread-indicator {
+ margin-left: 0.5rem;
+ }
+}
diff --git a/plugins/chat/assets/stylesheets/common/chat-thread-unread-indicator.scss b/plugins/chat/assets/stylesheets/common/chat-thread-unread-indicator.scss
index eaf893aa528..b46064a0565 100644
--- a/plugins/chat/assets/stylesheets/common/chat-thread-unread-indicator.scss
+++ b/plugins/chat/assets/stylesheets/common/chat-thread-unread-indicator.scss
@@ -9,4 +9,5 @@
padding: 0.21em 0.42em;
border-radius: 1em;
min-width: 0.6em;
+ align-self: center;
}
diff --git a/plugins/chat/assets/stylesheets/common/chat-unread-indicator.scss b/plugins/chat/assets/stylesheets/common/chat-unread-indicator.scss
index 9759542783b..9d57b336193 100644
--- a/plugins/chat/assets/stylesheets/common/chat-unread-indicator.scss
+++ b/plugins/chat/assets/stylesheets/common/chat-unread-indicator.scss
@@ -21,3 +21,7 @@
line-height: var(--line-height-small);
}
}
+
+.chat__unread-indicator {
+ @include chat-unread-indicator;
+}
diff --git a/plugins/chat/assets/stylesheets/common/chat-user-avatar.scss b/plugins/chat/assets/stylesheets/common/chat-user-avatar.scss
index 76bd0f0a4cb..3788debe698 100644
--- a/plugins/chat/assets/stylesheets/common/chat-user-avatar.scss
+++ b/plugins/chat/assets/stylesheets/common/chat-user-avatar.scss
@@ -17,7 +17,6 @@
}
&__container {
- position: relative;
padding: 1px; // for is-online box-shadow effect, preventing cutoff
.avatar {
diff --git a/plugins/chat/assets/stylesheets/common/chat-user-threads-row.scss b/plugins/chat/assets/stylesheets/common/chat-user-threads-row.scss
new file mode 100644
index 00000000000..c0e17c87ceb
--- /dev/null
+++ b/plugins/chat/assets/stylesheets/common/chat-user-threads-row.scss
@@ -0,0 +1,32 @@
+.chat__user-threads-row {
+ display: flex;
+ align-items: center;
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ width: 100%;
+
+ .chat__unread-indicator {
+ width: 8px;
+ height: 8px;
+ }
+
+ &-container {
+ height: 3.6em;
+ padding: 0 0.5rem;
+ margin: 0.5rem 0rem 0 0.5rem;
+ box-sizing: border-box;
+ display: flex;
+
+ &:hover {
+ background: var(--primary-low);
+ }
+
+ .chat__user-threads-row__icon {
+ color: var(--primary);
+ }
+ .chat__user-threads-row__title {
+ color: var(--primary);
+ }
+ }
+}
diff --git a/plugins/chat/assets/stylesheets/common/chat-user-threads.scss b/plugins/chat/assets/stylesheets/common/chat-user-threads.scss
new file mode 100644
index 00000000000..f38c1118a09
--- /dev/null
+++ b/plugins/chat/assets/stylesheets/common/chat-user-threads.scss
@@ -0,0 +1,23 @@
+.chat__user-threads__thread-indicator {
+ padding-top: 1rem;
+
+ .chat-message-thread-indicator {
+ margin: 0;
+ }
+}
+
+.chat__user-threads__title {
+ .chat-channel-title__name {
+ font-size: var(--font-down-1);
+ color: var(--primary-medium);
+ }
+}
+
+.chat__user-threads__thread {
+ padding-bottom: 1rem;
+ border-bottom: 1px solid var(--primary-low);
+
+ &-container {
+ padding: 1rem 1rem 0 1rem;
+ }
+}
diff --git a/plugins/chat/assets/stylesheets/common/index.scss b/plugins/chat/assets/stylesheets/common/index.scss
index 220f88a4cbf..337cd189f72 100644
--- a/plugins/chat/assets/stylesheets/common/index.scss
+++ b/plugins/chat/assets/stylesheets/common/index.scss
@@ -65,3 +65,7 @@
@import "chat-channel-row";
@import "chat-channel-members";
@import "chat-channel-settings";
+@import "chat-user-threads";
+@import "chat-navbar";
+@import "chat-user-threads-row";
+@import "chat-thread-title";
diff --git a/plugins/chat/assets/stylesheets/common/sidebar-extensions.scss b/plugins/chat/assets/stylesheets/common/sidebar-extensions.scss
index 6d2c572e8f6..234c4436dc3 100644
--- a/plugins/chat/assets/stylesheets/common/sidebar-extensions.scss
+++ b/plugins/chat/assets/stylesheets/common/sidebar-extensions.scss
@@ -35,6 +35,12 @@
}
}
+ .sidebar-section-wrapper.sidebar-section[data-section-name="my-threads"] {
+ .sidebar-section-content {
+ padding-bottom: 0;
+ }
+ }
+
.sidebar-container {
.channels-list {
color: var(--primary);
diff --git a/plugins/chat/assets/stylesheets/desktop/chat-message-thread-indicator.scss b/plugins/chat/assets/stylesheets/desktop/chat-message-thread-indicator.scss
index 57a6fadc01d..9863732a557 100644
--- a/plugins/chat/assets/stylesheets/desktop/chat-message-thread-indicator.scss
+++ b/plugins/chat/assets/stylesheets/desktop/chat-message-thread-indicator.scss
@@ -16,7 +16,7 @@
"avatar info info participants"
"excerpt excerpt excerpt replies";
&__replies-count {
- align-self: flex-start;
+ align-self: center;
grid-area: replies;
justify-content: flex-end;
}
diff --git a/plugins/chat/config/locales/client.en.yml b/plugins/chat/config/locales/client.en.yml
index 474396d7c8b..9163cf14e81 100644
--- a/plugins/chat/config/locales/client.en.yml
+++ b/plugins/chat/config/locales/client.en.yml
@@ -491,6 +491,9 @@ en:
create_export: "Create export"
export_has_started: "The export has started. You'll receive a PM when it's ready."
+ my_threads:
+ title: My threads
+
direct_messages:
title: "Personal chat"
new: "Create a personal chat"
diff --git a/plugins/chat/config/routes.rb b/plugins/chat/config/routes.rb
index c3dd707746e..9f1f902164b 100644
--- a/plugins/chat/config/routes.rb
+++ b/plugins/chat/config/routes.rb
@@ -4,7 +4,8 @@ Chat::Engine.routes.draw do
namespace :api, defaults: { format: :json } do
get "/chatables" => "chatables#index"
get "/channels" => "channels#index"
- get "/channels/me" => "current_user_channels#index"
+ get "/me/channels" => "current_user_channels#index"
+ get "/me/threads" => "current_user_threads#index"
post "/channels" => "channels#create"
put "/channels/read/" => "reads#update_all"
put "/channels/:channel_id/read/:message_id" => "reads#update"
@@ -72,6 +73,7 @@ Chat::Engine.routes.draw do
# chat_controller routes
get "/" => "chat#respond"
+ get "/threads" => "chat#respond"
get "/browse" => "chat#respond"
get "/browse/all" => "chat#respond"
get "/browse/closed" => "chat#respond"
diff --git a/plugins/chat/db/migrate/20231207135641_add_user_chat_thread_memberships_on_thread_id_user_id_index.rb b/plugins/chat/db/migrate/20231207135641_add_user_chat_thread_memberships_on_thread_id_user_id_index.rb
new file mode 100644
index 00000000000..b446d845c6e
--- /dev/null
+++ b/plugins/chat/db/migrate/20231207135641_add_user_chat_thread_memberships_on_thread_id_user_id_index.rb
@@ -0,0 +1,22 @@
+# frozen_string_literal: true
+
+class AddUserChatThreadMembershipsOnThreadIdUserIdIndex < ActiveRecord::Migration[7.0]
+ disable_ddl_transaction!
+
+ def up
+ execute <<~SQL
+ DROP INDEX CONCURRENTLY IF EXISTS idx_user_chat_thread_memberships_on_thread_id_user_id
+ SQL
+
+ execute <<~SQL
+ CREATE INDEX CONCURRENTLY idx_user_chat_thread_memberships_on_thread_id_user_id
+ ON user_chat_thread_memberships (thread_id, user_id);
+ SQL
+ end
+
+ def down
+ execute <<~SQL
+ DROP INDEX CONCURRENTLY IF EXISTS idx_user_chat_thread_memberships_on_thread_id_user_id
+ SQL
+ end
+end
diff --git a/plugins/chat/spec/fabricators/chat_fabricator.rb b/plugins/chat/spec/fabricators/chat_fabricator.rb
index 2d9cbfe9180..ff1e9a2f74d 100644
--- a/plugins/chat/spec/fabricators/chat_fabricator.rb
+++ b/plugins/chat/spec/fabricators/chat_fabricator.rb
@@ -220,12 +220,16 @@ Fabricator(:chat_thread, class_name: "Chat::Thread") do
thread.add(thread.original_message_user)
if transients[:with_replies]
- Fabricate.times(
- transients[:with_replies],
- :chat_message,
- thread: thread,
- use_service: transients[:use_service],
- )
+ Fabricate
+ .times(
+ transients[:with_replies],
+ :chat_message,
+ thread: thread,
+ use_service: transients[:use_service],
+ )
+ .each { |message| thread.add(message.user) }
+
+ thread.update!(replies_count: transients[:with_replies])
end
end
end
diff --git a/plugins/chat/spec/requests/chat/api/channel_threads_controller_spec.rb b/plugins/chat/spec/requests/chat/api/channel_threads_controller_spec.rb
index ccabfaf758b..03beeae1f07 100644
--- a/plugins/chat/spec/requests/chat/api/channel_threads_controller_spec.rb
+++ b/plugins/chat/spec/requests/chat/api/channel_threads_controller_spec.rb
@@ -84,9 +84,9 @@ RSpec.describe Chat::Api::ChannelThreadsController do
end
describe "index" do
- fab!(:thread_1) { Fabricate(:chat_thread, channel: public_channel) }
- fab!(:thread_2) { Fabricate(:chat_thread, channel: public_channel) }
- fab!(:thread_3) { Fabricate(:chat_thread, channel: public_channel) }
+ fab!(:thread_1) { Fabricate(:chat_thread, channel: public_channel, with_replies: 1) }
+ fab!(:thread_2) { Fabricate(:chat_thread, channel: public_channel, with_replies: 1) }
+ fab!(:thread_3) { Fabricate(:chat_thread, channel: public_channel, with_replies: 1) }
fab!(:message_1) do
Fabricate(
:chat_message,
@@ -111,11 +111,11 @@ RSpec.describe Chat::Api::ChannelThreadsController do
thread_3.add(current_user)
end
- it "returns the threads the user has sent messages in for the channel" do
+ it "returns the threads of the channel" do
get "/chat/api/channels/#{public_channel.id}/threads"
expect(response.status).to eq(200)
expect(response.parsed_body["threads"].map { |thread| thread["id"] }).to eq(
- [thread_3.id, thread_1.id],
+ [thread_3.id, thread_2.id, thread_1.id],
)
end
diff --git a/plugins/chat/spec/requests/chat/api/current_user_channels_spec.rb b/plugins/chat/spec/requests/chat/api/current_user_channels_spec.rb
index 60b514747c4..12e9f187793 100644
--- a/plugins/chat/spec/requests/chat/api/current_user_channels_spec.rb
+++ b/plugins/chat/spec/requests/chat/api/current_user_channels_spec.rb
@@ -13,7 +13,7 @@ describe Chat::Api::CurrentUserChannelsController do
describe "#index" do
context "as anonymous user" do
it "returns an error" do
- get "/chat/api/channels/me"
+ get "/chat/api/me/channels"
expect(response.status).to eq(403)
end
end
@@ -25,7 +25,7 @@ describe Chat::Api::CurrentUserChannelsController do
end
it "returns an error" do
- get "/chat/api/channels/me"
+ get "/chat/api/me/channels"
expect(response.status).to eq(403)
end
@@ -39,7 +39,7 @@ describe Chat::Api::CurrentUserChannelsController do
it "returns public channels with memberships" do
channel = Fabricate(:category_channel)
channel.add(current_user)
- get "/chat/api/channels/me"
+ get "/chat/api/me/channels"
expect(response.parsed_body["public_channels"][0]["id"]).to eq(channel.id)
end
@@ -49,7 +49,7 @@ describe Chat::Api::CurrentUserChannelsController do
channel = Fabricate(:private_category_channel, group: group)
group.add(current_user)
channel.add(current_user)
- get "/chat/api/channels/me"
+ get "/chat/api/me/channels"
expect(response.parsed_body["public_channels"][0]["id"]).to eq(channel.id)
end
@@ -58,21 +58,21 @@ describe Chat::Api::CurrentUserChannelsController do
group = Fabricate(:group)
channel = Fabricate(:private_category_channel, group: group)
channel.add(current_user) # TODO: we should error here
- get "/chat/api/channels/me"
+ get "/chat/api/me/channels"
expect(response.parsed_body["public_channels"]).to be_blank
end
it "returns dm channels you are part of" do
dm_channel = Fabricate(:direct_message_channel, users: [current_user])
- get "/chat/api/channels/me"
+ get "/chat/api/me/channels"
expect(response.parsed_body["direct_message_channels"][0]["id"]).to eq(dm_channel.id)
end
it "doesn’t return dm channels from other users" do
Fabricate(:direct_message_channel)
- get "/chat/api/channels/me"
+ get "/chat/api/me/channels"
expect(response.parsed_body["direct_message_channels"]).to be_blank
end
@@ -81,7 +81,7 @@ describe Chat::Api::CurrentUserChannelsController do
Fabricate(:direct_message_channel, users: [current_user])
channel = Fabricate(:category_channel)
channel.add(current_user)
- get "/chat/api/channels/me"
+ get "/chat/api/me/channels"
expect(response.status).to eq(200)
@@ -112,7 +112,7 @@ describe Chat::Api::CurrentUserChannelsController do
channel = Fabricate(:category_channel)
channel.add(current_user)
channel.chatable.destroy!
- get "/chat/api/channels/me"
+ get "/chat/api/me/channels"
expect(response.status).to eq(200)
expect(response.parsed_body["public_channels"]).to be_blank
@@ -123,7 +123,7 @@ describe Chat::Api::CurrentUserChannelsController do
it "doesn’t return the channel" do
channel = Fabricate(:direct_message_channel, users: [current_user])
channel.chatable.destroy!
- get "/chat/api/channels/me"
+ get "/chat/api/me/channels"
expect(response.status).to eq(200)
expect(response.parsed_body["direct_message_channels"]).to be_blank
diff --git a/plugins/chat/spec/requests/chat/api/current_user_threads_spec.rb b/plugins/chat/spec/requests/chat/api/current_user_threads_spec.rb
new file mode 100644
index 00000000000..1e8577ba01f
--- /dev/null
+++ b/plugins/chat/spec/requests/chat/api/current_user_threads_spec.rb
@@ -0,0 +1,32 @@
+# frozen_string_literal: true
+
+require "rails_helper"
+
+describe Chat::Api::CurrentUserThreadsController do
+ fab!(:current_user) { Fabricate(:user) }
+
+ before do
+ SiteSetting.chat_enabled = true
+ SiteSetting.chat_allowed_groups = Group::AUTO_GROUPS[:everyone]
+ sign_in(current_user)
+ end
+
+ describe "#index" do
+ describe "success" do
+ it "works" do
+ get "/chat/api/me/threads"
+
+ expect(response.status).to eq(200)
+ end
+ end
+
+ context "when threads are not found" do
+ it "returns a 200 with empty threads" do
+ get "/chat/api/me/threads"
+
+ expect(response.status).to eq(200)
+ expect(response.parsed_body["threads"]).to eq([])
+ end
+ end
+ end
+end
diff --git a/plugins/chat/spec/services/chat/lookup_channel_threads_spec.rb b/plugins/chat/spec/services/chat/lookup_channel_threads_spec.rb
index 2a874918639..19146ec5ff0 100644
--- a/plugins/chat/spec/services/chat/lookup_channel_threads_spec.rb
+++ b/plugins/chat/spec/services/chat/lookup_channel_threads_spec.rb
@@ -113,11 +113,14 @@ RSpec.describe ::Chat::LookupChannelThreads do
before do
[thread_1, thread_2, thread_3].each.with_index do |t, index|
t.original_message.update!(created_at: (index + 1).weeks.ago)
+ t.update!(replies_count: 2)
t.add(current_user)
end
end
describe "model - threads" do
+ before { channel_1.add(current_user) }
+
it { is_expected.to be_a_success }
it "orders threads by the last reply created_at timestamp" do
@@ -153,9 +156,13 @@ RSpec.describe ::Chat::LookupChannelThreads do
it "sorts very old unreads to top over recency, and sorts both unreads and other threads by recency" do
thread_4 = Fabricate(:chat_thread, channel: channel_1)
+ thread_4.update!(replies_count: 2)
thread_5 = Fabricate(:chat_thread, channel: channel_1)
+ thread_5.update!(replies_count: 2)
thread_6 = Fabricate(:chat_thread, channel: channel_1)
+ thread_6.update!(replies_count: 2)
thread_7 = Fabricate(:chat_thread, channel: channel_1)
+ thread_7.update!(replies_count: 2)
[thread_4, thread_5, thread_6, thread_7].each do |t|
t.add(current_user)
@@ -202,15 +209,23 @@ RSpec.describe ::Chat::LookupChannelThreads do
expect(result.threads.map(&:id)).to eq([thread_1.id, thread_2.id, thread_3.id])
end
- it "only returns threads where the user has their thread notification level as tracking or regular" do
+ it "returns every threads of the channel, no matter the tracking notification level or membership" do
thread_4 = Fabricate(:chat_thread, channel: channel_1)
- thread_4.add(current_user)
- thread_4.membership_for(current_user).update!(
+ thread_4.update!(replies_count: 2)
+
+ expect(result.threads.map(&:id)).to match_array(
+ [thread_1.id, thread_2.id, thread_3.id, thread_4.id],
+ )
+ end
+
+ it "doesnt return muted threads" do
+ thread = Fabricate(:chat_thread, channel: channel_1)
+ thread.add(current_user)
+ thread.membership_for(current_user).update!(
notification_level: ::Chat::UserChatThreadMembership.notification_levels[:muted],
)
- Fabricate(:chat_thread, channel: channel_1)
- expect(result.threads.map(&:id)).to eq([thread_1.id, thread_2.id, thread_3.id])
+ expect(result.threads.map(&:id)).to_not include(thread.id)
end
it "does not count deleted messages for sort order" do
diff --git a/plugins/chat/spec/services/chat/lookup_user_threads_spec.rb b/plugins/chat/spec/services/chat/lookup_user_threads_spec.rb
new file mode 100644
index 00000000000..7fd34cc23d6
--- /dev/null
+++ b/plugins/chat/spec/services/chat/lookup_user_threads_spec.rb
@@ -0,0 +1,171 @@
+# frozen_string_literal: true
+
+RSpec.describe ::Chat::LookupUserThreads do
+ subject(:result) { described_class.call(params) }
+
+ fab!(:current_user) { Fabricate(:user) }
+ fab!(:channel_1) { Fabricate(:chat_channel, threading_enabled: true) }
+
+ let(:guardian) { Guardian.new(current_user) }
+ let(:channel_id) { channel_1.id }
+ let(:limit) { 10 }
+ let(:offset) { 0 }
+ let(:params) { { guardian: guardian, limit: limit, offset: offset } }
+
+ before { channel_1.add(current_user) }
+
+ context "when all steps pass" do
+ it "returns threads" do
+ thread_1 =
+ Fabricate(:chat_thread, channel: channel_1, with_replies: 1).tap do |thread|
+ thread.add(current_user)
+ end
+
+ expect(result.threads).to eq([thread_1])
+ end
+
+ it "limits results by default" do
+ Fabricate
+ .times(11, :chat_thread, channel: channel_1, with_replies: 1)
+ .each { |thread| thread.add(current_user) }
+
+ expect(result.threads.length).to eq(10)
+ end
+
+ it "can limit results" do
+ params[:limit] = 1
+
+ Fabricate
+ .times(2, :chat_thread, channel: channel_1, with_replies: 1)
+ .each { |thread| thread.add(current_user) }
+
+ expect(result.threads.length).to eq(params[:limit])
+ end
+
+ it "limits to 1 at least" do
+ params[:limit] = 0
+
+ Fabricate(:chat_thread, channel: channel_1, with_replies: 1).tap do |thread|
+ thread.add(current_user)
+ end
+
+ expect(result.threads.length).to eq(1)
+ end
+
+ it "has a max limit" do
+ params[:limit] = 11
+
+ Fabricate
+ .times(11, :chat_thread, channel: channel_1, with_replies: 1)
+ .each { |thread| thread.add(current_user) }
+
+ expect(result.threads.length).to eq(10)
+ end
+
+ it "can offset" do
+ params[:offset] = 1
+
+ threads =
+ Fabricate
+ .times(2, :chat_thread, channel: channel_1, with_replies: 1)
+ .each { |thread| thread.add(current_user) }
+
+ # 0 because we sort by last_message.created_at, so the last created thread is the first one
+ expect(result.threads).to eq([threads[0]])
+ end
+
+ it "has a min offset" do
+ params[:offset] = -99
+
+ threads =
+ Fabricate
+ .times(2, :chat_thread, channel: channel_1, with_replies: 1)
+ .each { |thread| thread.add(current_user) }
+
+ # 0 because we sort by last_message.created_at, so the last created thread is the first one
+ expect(result.threads.length).to eq(2)
+ end
+
+ it "fetches tracking" do
+ thread_1 =
+ Fabricate(:chat_thread, channel: channel_1, with_replies: 1).tap do |thread|
+ thread.add(current_user)
+ end
+
+ expect(result.tracking).to eq(
+ ::Chat::TrackingStateReportQuery.call(
+ guardian: current_user.guardian,
+ thread_ids: [thread_1.id],
+ include_threads: true,
+ ).thread_tracking,
+ )
+ end
+
+ it "fetches memberships" do
+ thread_1 =
+ Fabricate(:chat_thread, channel: channel_1, with_replies: 1).tap do |thread|
+ thread.add(current_user)
+ end
+
+ expect(result.memberships).to eq([thread_1.membership_for(current_user)])
+ end
+
+ it "fetches participants" do
+ thread_1 =
+ Fabricate(:chat_thread, channel: channel_1, with_replies: 1).tap do |thread|
+ thread.add(current_user)
+ end
+
+ expect(result.participants).to eq(
+ ::Chat::ThreadParticipantQuery.call(thread_ids: [thread_1.id]),
+ )
+ end
+
+ it "builds a load_more_url" do
+ Fabricate(:chat_thread, channel: channel_1, with_replies: 1).tap do |thread|
+ thread.add(current_user)
+ end
+
+ expect(result.load_more_url).to eq("/chat/api/me/threads?limit=10&offset=10")
+ end
+ end
+
+ it "doesn't return threads with no replies" do
+ thread_1 = Fabricate(:chat_thread, channel: channel_1)
+ thread_1.add(current_user)
+
+ expect(result.threads).to eq([])
+ end
+
+ it "doesn't return threads with no membership" do
+ thread_1 = Fabricate(:chat_thread, channel: channel_1, with_replies: 1)
+
+ expect(result.threads).to eq([])
+ end
+
+ it "doesn't return threads when the channel has not threading enabled" do
+ channel_1.update!(threading_enabled: false)
+ thread_1 = Fabricate(:chat_thread, channel: channel_1, with_replies: 1)
+ thread_1.add(current_user)
+
+ expect(result.threads).to eq([])
+ end
+
+ it "doesn't return muted threads" do
+ thread_1 = Fabricate(:chat_thread, channel: channel_1, with_replies: 1)
+ thread_1.add(current_user)
+ thread_1.membership_for(current_user).update!(
+ notification_level: ::Chat::UserChatThreadMembership.notification_levels[:muted],
+ )
+
+ expect(result.threads).to eq([])
+ end
+
+ it "doesn't return threads when the channel it not open" do
+ channel_1.update!(status: Chat::Channel.statuses[:closed])
+ thread_1 = Fabricate(:chat_thread, channel: channel_1, with_replies: 1)
+ thread_1.add(current_user)
+
+ expect(result.threads).to eq([])
+ end
+end
diff --git a/plugins/chat/spec/system/navigation_spec.rb b/plugins/chat/spec/system/navigation_spec.rb
index 043e53c26db..19baa9be96d 100644
--- a/plugins/chat/spec/system/navigation_spec.rb
+++ b/plugins/chat/spec/system/navigation_spec.rb
@@ -141,7 +141,7 @@ RSpec.describe "Navigation", type: :system do
thread_list_page.open_thread(thread)
expect(side_panel_page).to have_open_thread(thread)
expect(thread_page).to have_back_link_to_thread_list(category_channel)
- thread_page.back_to_previous_route
+ thread_page.back
expect(page).to have_current_path("#{category_channel.relative_url}/t")
expect(thread_list_page).to have_loaded
end
@@ -157,7 +157,7 @@ RSpec.describe "Navigation", type: :system do
thread_list_page.open_thread(thread)
expect(side_panel_page).to have_open_thread(thread)
expect(thread_page).to have_back_link_to_thread_list(category_channel)
- thread_page.back_to_previous_route
+ thread_page.back
expect(page).to have_current_path("#{category_channel.relative_url}/t")
expect(thread_list_page).to have_loaded
end
@@ -173,7 +173,7 @@ RSpec.describe "Navigation", type: :system do
channel_page.message_thread_indicator(thread.original_message).click
expect(side_panel_page).to have_open_thread(thread)
expect(thread_page).to have_back_link_to_thread_list(category_channel)
- thread_page.back_to_previous_route
+ thread_page.back
expect(page).to have_current_path("#{category_channel.relative_url}/t")
expect(thread_list_page).to have_loaded
end
@@ -188,7 +188,7 @@ RSpec.describe "Navigation", type: :system do
channel_page.message_thread_indicator(thread.original_message).click
expect(side_panel_page).to have_open_thread(thread)
expect(thread_page).to have_back_link_to_channel(category_channel)
- thread_page.back_to_previous_route
+ thread_page.back
expect(page).to have_current_path("#{category_channel.relative_url}")
expect(side_panel_page).to be_closed
end
diff --git a/plugins/chat/spec/system/page_objects/chat/chat.rb b/plugins/chat/spec/system/page_objects/chat/chat.rb
index 257c5e2156a..bb67a349731 100644
--- a/plugins/chat/spec/system/page_objects/chat/chat.rb
+++ b/plugins/chat/spec/system/page_objects/chat/chat.rb
@@ -53,6 +53,11 @@ module PageObjects
has_finished_loading?
end
+ def visit_user_threads
+ visit("/chat/threads")
+ has_no_css?(".spinner")
+ end
+
def visit_thread(thread)
visit(thread.url)
has_css?(".chat-thread:not(.loading)[data-id=\"#{thread.id}\"]")
diff --git a/plugins/chat/spec/system/page_objects/chat/chat_thread.rb b/plugins/chat/spec/system/page_objects/chat/chat_thread.rb
index 7ce96b3ebe9..af4d241051a 100644
--- a/plugins/chat/spec/system/page_objects/chat/chat_thread.rb
+++ b/plugins/chat/spec/system/page_objects/chat/chat_thread.rb
@@ -68,7 +68,7 @@ module PageObjects
header.has_css?(".chat-thread__back-to-previous-route[href='#{channel.relative_url}']")
end
- def back_to_previous_route
+ def back
header.find(".chat-thread__back-to-previous-route").click
end
diff --git a/plugins/chat/spec/system/page_objects/chat/user_threads.rb b/plugins/chat/spec/system/page_objects/chat/user_threads.rb
new file mode 100644
index 00000000000..4e837f23cfa
--- /dev/null
+++ b/plugins/chat/spec/system/page_objects/chat/user_threads.rb
@@ -0,0 +1,16 @@
+# frozen_string_literal: true
+
+module PageObjects
+ module Pages
+ class UserThreads < PageObjects::Pages::Base
+ def has_threads?(count: nil)
+ has_no_css?(".spinner")
+ has_css?(".chat__user-threads__thread-container", count: count)
+ end
+
+ def open_thread(thread)
+ find(".chat__user-threads__thread-container[data-id='#{thread.id}']").click
+ end
+ end
+ end
+end
diff --git a/plugins/chat/spec/system/page_objects/chat_drawer/chat_drawer.rb b/plugins/chat/spec/system/page_objects/chat_drawer/chat_drawer.rb
index 8a71764a2a9..a3dca65eb2b 100644
--- a/plugins/chat/spec/system/page_objects/chat_drawer/chat_drawer.rb
+++ b/plugins/chat/spec/system/page_objects/chat_drawer/chat_drawer.rb
@@ -37,6 +37,22 @@ module PageObjects
channel_index.has_no_unread_channel?(channel)
end
+ def has_user_threads_section?
+ has_css?(".chat__user-threads-row-container[href='/chat/threads']")
+ end
+
+ def has_unread_user_threads?
+ has_css?(".chat__user-threads-row .chat__unread-indicator")
+ end
+
+ def has_no_unread_user_threads?
+ has_no_css?(".chat__user-threads-row .chat__unread-indicator")
+ end
+
+ def click_user_threads
+ find(".chat__user-threads-row").click
+ end
+
def maximize
mouseout
find("#{VISIBLE_DRAWER} .chat-drawer-header__full-screen-btn").click
diff --git a/plugins/chat/spec/system/page_objects/sidebar/sidebar.rb b/plugins/chat/spec/system/page_objects/sidebar/sidebar.rb
index 3d4bc229250..6e718ae4e3d 100644
--- a/plugins/chat/spec/system/page_objects/sidebar/sidebar.rb
+++ b/plugins/chat/spec/system/page_objects/sidebar/sidebar.rb
@@ -37,6 +37,25 @@ module PageObjects
self
end
+ def has_user_threads_section?
+ has_css?(
+ ".sidebar-section-link[data-link-name='user-threads'][href='/chat/threads']",
+ text: I18n.t("js.chat.my_threads.title"),
+ )
+ end
+
+ def has_unread_user_threads?
+ has_css?(
+ ".sidebar-section-link[data-link-name='user-threads'] .sidebar-section-link-suffix.icon.unread",
+ )
+ end
+
+ def has_no_unread_user_threads?
+ has_no_css?(
+ ".sidebar-section-link[data-link-name='user-threads'] .sidebar-section-link-suffix.icon.unread",
+ )
+ end
+
def has_unread_channel?(channel)
has_css?(".sidebar-section-link.channel-#{channel.id} .sidebar-section-link-suffix.unread")
end
diff --git a/plugins/chat/spec/system/reply_to_message/mobile_spec.rb b/plugins/chat/spec/system/reply_to_message/mobile_spec.rb
index a0c1922f13d..4673b6da689 100644
--- a/plugins/chat/spec/system/reply_to_message/mobile_spec.rb
+++ b/plugins/chat/spec/system/reply_to_message/mobile_spec.rb
@@ -35,7 +35,7 @@ RSpec.describe "Reply to message - channel - mobile", type: :system, mobile: tru
expect(thread_page.messages).to have_message(text: text, persisted: true)
- thread_page.back_to_previous_route
+ thread_page.back
expect(channel_page).to have_thread_indicator(original_message)
end
@@ -69,7 +69,7 @@ RSpec.describe "Reply to message - channel - mobile", type: :system, mobile: tru
expect(thread_page.messages).to have_message(text: message_1.message)
expect(thread_page.messages).to have_message(text: "reply to message")
- thread_page.back_to_previous_route
+ thread_page.back
expect(channel_page.message_thread_indicator(original_message)).to have_reply_count(2)
expect(channel_page.messages).to have_no_message(text: "reply to message")
diff --git a/plugins/chat/spec/system/thread_list/full_page_spec.rb b/plugins/chat/spec/system/thread_list/full_page_spec.rb
index 6a47d6cc9fc..69a0f4f2ac7 100644
--- a/plugins/chat/spec/system/thread_list/full_page_spec.rb
+++ b/plugins/chat/spec/system/thread_list/full_page_spec.rb
@@ -30,19 +30,19 @@ describe "Thread list in side panel | full page", type: :system do
before { chat_system_user_bootstrap(user: other_user, channel: channel) }
- it "does not show existing threads in the channel if the user is not tracking them" do
- Fabricate(:chat_thread, original_message: thread_om, channel: channel, use_service: true)
+ it "it shows threads in the channel even if the user is not tracking them" do
+ thread_1 =
+ Fabricate(
+ :chat_thread,
+ original_message: thread_om,
+ channel: channel,
+ with_replies: 1,
+ use_service: true,
+ )
chat_page.visit_channel(channel)
channel_page.open_thread_list
- expect(page).to have_content(I18n.t("js.chat.threads.none"))
- end
- it "does not show new threads in the channel in the thread list if the user is not tracking them" do
- chat_page.visit_channel(channel)
- Fabricate(:chat_message, chat_channel: channel, in_reply_to: thread_om, use_service: true)
- channel_page.open_thread_list
-
- expect(page).to have_content(I18n.t("js.chat.threads.none"))
+ expect(thread_list_page).to have_thread(thread_1)
end
describe "when the user creates a new thread" do
diff --git a/plugins/chat/spec/system/thread_tracking/full_page_spec.rb b/plugins/chat/spec/system/thread_tracking/full_page_spec.rb
index 9f871ddf375..13b54c4ac68 100644
--- a/plugins/chat/spec/system/thread_tracking/full_page_spec.rb
+++ b/plugins/chat/spec/system/thread_tracking/full_page_spec.rb
@@ -49,7 +49,7 @@ describe "Thread tracking state | full page", type: :system do
expect(thread_page).to have_no_unread_list_indicator
- thread_page.back_to_previous_route
+ thread_page.back
expect(thread_list_page).to have_no_unread_item(thread.id)
end
diff --git a/plugins/chat/spec/system/user_threads_spec.rb b/plugins/chat/spec/system/user_threads_spec.rb
new file mode 100644
index 00000000000..e244c413b94
--- /dev/null
+++ b/plugins/chat/spec/system/user_threads_spec.rb
@@ -0,0 +1,194 @@
+# frozen_string_literal: true
+
+RSpec.describe "User threads", type: :system do
+ fab!(:current_user) { Fabricate(:user) }
+ fab!(:channel_1) { Fabricate(:chat_channel, threading_enabled: true) }
+
+ let(:chat_page) { PageObjects::Pages::Chat.new }
+ let(:thread_page) { PageObjects::Pages::ChatThread.new }
+ let(:sidebar_page) { PageObjects::Pages::Sidebar.new }
+ let(:drawer_page) { PageObjects::Pages::ChatDrawer.new }
+ let(:channel_page) { PageObjects::Pages::ChatChannel.new }
+ let(:user_threads_page) { PageObjects::Pages::UserThreads.new }
+
+ before do
+ chat_system_bootstrap
+ sign_in(current_user)
+ end
+
+ context "when in sidebar" do
+ it "shows a link to user threads" do
+ visit("/")
+
+ expect(sidebar_page).to have_user_threads_section
+ end
+
+ context "when user has unreads" do
+ before do
+ chat_thread_chain_bootstrap(channel: channel_1, users: [current_user, Fabricate(:user)])
+ end
+
+ xit "has an unread indicator" do
+ visit("/")
+
+ expect(sidebar_page).to have_unread_user_threads
+ end
+ end
+
+ it "has no unread indicator when user has no unreads" do
+ visit("/")
+
+ expect(sidebar_page).to have_no_unread_user_threads
+ end
+
+ it "lists threads" do
+ Fabricate
+ .times(5, :chat_channel, threading_enabled: true)
+ .each do |channel|
+ chat_thread_chain_bootstrap(
+ channel: channel,
+ users: [current_user, Fabricate(:user)],
+ messages_count: 2,
+ )
+ end
+
+ chat_page.visit_user_threads
+
+ expect(user_threads_page).to have_threads(count: 5)
+ end
+
+ it "can load more threads" do
+ Fabricate
+ .times(20, :chat_channel, threading_enabled: true)
+ .each do |channel|
+ chat_thread_chain_bootstrap(
+ channel: channel,
+ users: [current_user, Fabricate(:user)],
+ messages_count: 2,
+ )
+ end
+
+ chat_page.visit_user_threads
+
+ expect(user_threads_page).to have_threads(count: 10)
+
+ page.execute_script("window.scrollTo(0, document.body.scrollHeight)")
+
+ expect(user_threads_page).to have_threads(count: 20)
+ end
+
+ it "can open a thread" do
+ chat_thread_chain_bootstrap(channel: channel_1, users: [current_user, Fabricate(:user)])
+
+ chat_page.visit_user_threads
+ user_threads_page.open_thread(channel_1.threads.first)
+
+ expect(chat_page).to have_current_path(
+ "/chat/c/#{channel_1.slug}/#{channel_1.id}/t/#{channel_1.threads.first.id} ",
+ )
+ end
+
+ it "navigating back from a thread opens the user threads" do
+ chat_thread_chain_bootstrap(channel: channel_1, users: [current_user, Fabricate(:user)])
+
+ chat_page.visit_user_threads
+ user_threads_page.open_thread(channel_1.threads.first)
+ thread_page.back
+
+ expect(user_threads_page).to have_threads
+ end
+ end
+
+ context "when in drawer" do
+ it "shows a link to user threads" do
+ visit("/")
+ chat_page.open_from_header
+
+ expect(drawer_page).to have_user_threads_section
+ end
+
+ context "when user has unreads" do
+ before do
+ chat_thread_chain_bootstrap(channel: channel_1, users: [current_user, Fabricate(:user)])
+ end
+
+ xit "has an unread indicator" do
+ visit("/")
+ chat_page.open_from_header
+
+ expect(drawer_page).to have_unread_user_threads
+ end
+ end
+
+ it "has no unread indicator when user has no unreads" do
+ visit("/")
+
+ expect(sidebar_page).to have_no_unread_user_threads
+ end
+
+ it "lists threads" do
+ Fabricate
+ .times(5, :chat_channel, threading_enabled: true)
+ .each do |channel|
+ chat_thread_chain_bootstrap(
+ channel: channel,
+ users: [current_user, Fabricate(:user)],
+ messages_count: 2,
+ )
+ end
+
+ visit("/")
+ chat_page.open_from_header
+ drawer_page.click_user_threads
+
+ expect(user_threads_page).to have_threads(count: 5)
+ end
+
+ it "can load more threads" do
+ Fabricate
+ .times(20, :chat_channel, threading_enabled: true)
+ .each do |channel|
+ chat_thread_chain_bootstrap(
+ channel: channel,
+ users: [current_user, Fabricate(:user)],
+ messages_count: 2,
+ )
+ end
+
+ visit("/")
+ chat_page.open_from_header
+ drawer_page.click_user_threads
+
+ expect(user_threads_page).to have_threads(count: 10)
+
+ page.execute_script(
+ "document.querySelector('.chat-drawer-content').scrollTo(0, document.querySelector('.chat-drawer-content').scrollHeight)",
+ )
+
+ expect(user_threads_page).to have_threads(count: 20)
+ end
+
+ it "can open a thread" do
+ chat_thread_chain_bootstrap(channel: channel_1, users: [current_user, Fabricate(:user)])
+
+ visit("/")
+ chat_page.open_from_header
+ drawer_page.click_user_threads
+ user_threads_page.open_thread(channel_1.threads.first)
+
+ expect(drawer_page).to have_open_thread(channel_1.threads.first)
+ end
+
+ it "navigating back from a thread opens the user threads" do
+ chat_thread_chain_bootstrap(channel: channel_1, users: [current_user, Fabricate(:user)])
+
+ visit("/")
+ chat_page.open_from_header
+ drawer_page.click_user_threads
+ user_threads_page.open_thread(channel_1.threads.first)
+ drawer_page.back
+
+ expect(user_threads_page).to have_threads
+ end
+ end
+end
diff --git a/plugins/chat/test/javascripts/components/chat-channel-title-test.js b/plugins/chat/test/javascripts/components/channel-title-test.js
similarity index 83%
rename from plugins/chat/test/javascripts/components/chat-channel-title-test.js
rename to plugins/chat/test/javascripts/components/channel-title-test.js
index 004b59aaf31..b8fb26b6ed6 100644
--- a/plugins/chat/test/javascripts/components/chat-channel-title-test.js
+++ b/plugins/chat/test/javascripts/components/channel-title-test.js
@@ -6,13 +6,13 @@ import { exists, query } from "discourse/tests/helpers/qunit-helpers";
import fabricators from "discourse/plugins/chat/discourse/lib/fabricators";
import { CHATABLE_TYPES } from "discourse/plugins/chat/discourse/models/chat-channel";
-module("Discourse Chat | Component | chat-channel-title", function (hooks) {
+module("Discourse Chat | Component | ", function (hooks) {
setupRenderingTest(hooks);
test("category channel", async function (assert) {
this.channel = fabricators.channel();
- await render(hbs``);
+ await render(hbs``);
assert.strictEqual(
query(".chat-channel-title__category-badge").getAttribute("style"),
@@ -30,7 +30,7 @@ module("Discourse Chat | Component | chat-channel-title", function (hooks) {
title: "evil
",
});
- await render(hbs``);
+ await render(hbs``);
assert.false(exists(".xss"));
});
@@ -40,7 +40,7 @@ module("Discourse Chat | Component | chat-channel-title", function (hooks) {
chatable: fabricators.category({ read_restricted: true }),
});
- await render(hbs``);
+ await render(hbs``);
assert.true(exists(".d-icon-lock"));
});
@@ -50,7 +50,7 @@ module("Discourse Chat | Component | chat-channel-title", function (hooks) {
chatable: fabricators.category({ read_restricted: false }),
});
- await render(hbs``);
+ await render(hbs``);
assert.false(exists(".d-icon-lock"));
});
@@ -62,7 +62,7 @@ module("Discourse Chat | Component | chat-channel-title", function (hooks) {
}),
});
- await render(hbs``);
+ await render(hbs``);
const user = this.channel.chatable.users[0];
@@ -79,7 +79,7 @@ module("Discourse Chat | Component | chat-channel-title", function (hooks) {
});
this.channel.chatable.group = true;
- await render(hbs``);
+ await render(hbs``);
const users = this.channel.chatable.users;
diff --git a/plugins/chat/test/javascripts/components/chat-message-collapser-test.js b/plugins/chat/test/javascripts/components/chat-message-collapser-test.js
index 56e6c50f4e3..8e6941e7c50 100644
--- a/plugins/chat/test/javascripts/components/chat-message-collapser-test.js
+++ b/plugins/chat/test/javascripts/components/chat-message-collapser-test.js
@@ -429,7 +429,7 @@ module(
);
assert.true(
queryAll(".chat-message-collapser-link-small")[1].innerHTML.includes(
- "%3Cscript%3Esomeeviltitle%3C/script%3E"
+ "<script>someeviltitle</script>"
)
);
});