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); + } + + +} 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.selectedContent}} - + {{else}} {{i18n "chat.incoming_webhooks.channel_placeholder"}} {{/if}} diff --git a/plugins/chat/assets/javascripts/discourse/components/chat-channel-chooser-row.hbs b/plugins/chat/assets/javascripts/discourse/components/chat-channel-chooser-row.hbs index 75983bf11cc..349fb3e8346 100644 --- a/plugins/chat/assets/javascripts/discourse/components/chat-channel-chooser-row.hbs +++ b/plugins/chat/assets/javascripts/discourse/components/chat-channel-chooser-row.hbs @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/plugins/chat/assets/javascripts/discourse/components/chat-channel-info.gjs b/plugins/chat/assets/javascripts/discourse/components/chat-channel-info.gjs index 3bc5690ee73..e7f7693c2bb 100644 --- a/plugins/chat/assets/javascripts/discourse/components/chat-channel-info.gjs +++ b/plugins/chat/assets/javascripts/discourse/components/chat-channel-info.gjs @@ -5,9 +5,9 @@ import { inject as service } from "@ember/service"; import DButton from "discourse/components/d-button"; import icon from "discourse-common/helpers/d-icon"; import I18n from "discourse-i18n"; +import ChannelTitle from "discourse/plugins/chat/discourse/components/channel-title"; import ChatModalEditChannelName from "discourse/plugins/chat/discourse/components/chat/modal/edit-channel-name"; 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 ChatChannelMessageEmojiPicker extends Component { @service chatChannelInfoRouteOriginManager; @@ -64,7 +64,7 @@ export default class ChatChannelMessageEmojiPicker extends Component { {{/if}}
- + {{#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 { } 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 @channel.tracking.unreadCount}} {{@channel.tracking.unreadCount}} {{/if}} - +
{{/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"); + + +} 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(); + } +