From d2e24f95699e4ce370a541579a0c849dfe4a51c2 Mon Sep 17 00:00:00 2001 From: Joffrey JAFFEUX Date: Wed, 21 Dec 2022 13:21:02 +0100 Subject: [PATCH] DEV: start glimmer-ification and optimisations of chat plugin (#19531) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Note this is a very large PR, and some of it could have been splited, but keeping it one chunk made it to merge conflicts and to revert if necessary. Actual new code logic is also not that much, as most of the changes are removing js tests, adding system specs or moving things around. To make it possible this commit is doing the following changes: - converting (and adding new) existing js acceptances tests into system tests. This change was necessary to ensure as little regressions as possible while changing paradigm - moving away from store. Using glimmer and tracked properties requires to have class objects everywhere and as a result works well with models. However store/adapters are suffering from many bugs and limitations. As a workaround the `chat-api` and `chat-channels-manager` are an answer to this problem by encapsulating backend calls and frontend storage logic; while still using js models. - dropping `appEvents` as much as possible. Using tracked properties and a better local storage of channel models, allows to be much more reactive and doesn’t require arbitrary manual updates everywhere in the app. - while working on replacing store, the existing work of a chat api (backend) has been continued to support more cases. - removing code from the `chat` service to separate concerns, `chat-subscriptions-manager` and `chat-channels-manager`, being the largest examples of where the code has been rewritten/moved. Future wok: - improve behavior when closing/deleting a channel, it's already slightly buggy on live, it's rare enough that it's not a big issue, but should be improved - improve page objects used in chat - move more endpoints to the API - finish temporarily skipped tests - extract more code from the `chat` service - use glimmer for `chat-messages` - separate concerns in `chat-live-pane` - eventually add js tests for `chat-api`, `chat-channels-manager` and `chat-subscriptions-manager`, they are indirectly heavy tested through system tests but it would be nice to at least test the public API --- .../subscribe-user-notifications.js | 6 +- app/serializers/user_status_serializer.rb | 6 +- .../chat_channel_memberships_controller.rb | 20 - ...annel_notifications_settings_controller.rb | 12 - .../api/chat_channels_archives_controller.rb | 42 + .../api/chat_channels_controller.rb | 160 +- ...nels_current_user_membership_controller.rb | 21 + ..._user_notifications_settings_controller.rb | 15 + .../chat_channels_memberships_controller.rb | 29 + ...chat_channels_messages_moves_controller.rb | 35 + .../api/chat_channels_status_controller.rb | 18 + .../api/chat_chatables_controller.rb | 82 + .../chat_current_user_channels_controller.rb | 8 + .../controllers/chat_channels_controller.rb | 250 --- .../chat/app/controllers/chat_controller.rb | 41 +- .../controllers/direct_messages_controller.rb | 4 +- .../app/jobs/regular/chat_channel_archive.rb | 14 +- .../serializers/chat_channel_serializer.rb | 22 +- .../structured_channel_serializer.rb | 79 +- plugins/chat/app/services/chat_publisher.rb | 19 +- .../discourse/adapters/chat-message.js | 2 + .../discourse/components/channels-list.hbs | 43 +- .../discourse/components/channels-list.js | 30 +- .../discourse/components/chat-browse-view.hbs | 21 +- .../discourse/components/chat-browse-view.js | 73 +- .../components/chat-channel-about-view.hbs | 1 - .../components/chat-channel-about-view.js | 8 - .../chat-channel-archive-modal-inner.js | 24 +- .../components/chat-channel-archive-status.js | 27 +- .../components/chat-channel-card.hbs | 43 +- .../discourse/components/chat-channel-card.js | 9 +- .../chat-channel-delete-modal-inner.js | 19 +- .../components/chat-channel-members-view.hbs | 12 +- .../components/chat-channel-members-view.js | 64 +- .../components/chat-channel-preview-card.hbs | 1 - .../components/chat-channel-preview-card.js | 9 +- .../discourse/components/chat-channel-row.hbs | 2 +- .../discourse/components/chat-channel-row.js | 6 +- .../chat-channel-selector-modal-inner.js | 84 +- .../components/chat-channel-settings-view.hbs | 14 +- .../components/chat-channel-settings-view.js | 37 +- .../components/chat-channel-toggle-view.js | 15 +- .../chat-channel-unread-indicator.hbs | 17 +- .../chat-channel-unread-indicator.js | 46 - .../components/chat-draft-channel-screen.js | 2 +- .../discourse/components/chat-drawer.js | 43 +- .../chat-header-icon-unread-indicator.hbs | 9 + .../chat-header-icon-unread-indicator.js | 6 + .../discourse/components/chat-header-icon.hbs | 21 + .../discourse/components/chat-header-icon.js | 31 + .../discourse/components/chat-live-pane.hbs | 1 + .../discourse/components/chat-live-pane.js | 44 +- ...at-message-move-to-channel-modal-inner.hbs | 16 +- ...hat-message-move-to-channel-modal-inner.js | 33 +- .../discourse/components/chat-message.hbs | 1 + .../discourse/components/chat-message.js | 13 +- .../components/chat-to-topic-selector.js | 6 +- .../discourse/components/full-page-chat.js | 11 - .../toggle-channel-membership-button.hbs | 2 +- .../toggle-channel-membership-button.js | 59 +- .../chat-channel-edit-description.js | 14 +- .../controllers/chat-channel-edit-title.js | 15 +- .../controllers/chat-channel-info-about.js | 4 +- .../controllers/chat-channel-info.js | 23 +- .../discourse/controllers/create-channel.js | 57 +- .../discourse/initializers/chat-setup.js | 32 +- .../discourse/initializers/chat-sidebar.js | 136 +- .../javascripts/discourse/lib/chat-api.js | 95 - .../discourse/models/chat-channel.js | 56 +- .../models/user-chat-channel-membership.js | 29 +- .../discourse/routes/chat-browse-archived.js | 9 + .../discourse/routes/chat-channel-by-name.js | 4 +- .../routes/chat-channel-info-about.js | 2 +- .../routes/chat-channel-info-index.js | 4 +- .../routes/chat-channel-info-members.js | 8 +- .../routes/chat-channel-info-settings.js | 9 + .../discourse/routes/chat-channel.js | 41 +- .../discourse/routes/chat-index.js | 33 +- .../discourse/services/chat-api.js | 242 +++ .../services/chat-channels-manager.js | 136 ++ .../chat-message-visibility-observer.js | 6 +- .../discourse/services/chat-state-manager.js | 4 + .../services/chat-subscriptions-manager.js | 265 +++ .../javascripts/discourse/services/chat.js | 742 +------ .../templates/chat-channel-index.hbs | 2 +- .../templates/chat-channel-info-about.hbs | 6 +- .../templates/chat-channel-info-members.hbs | 2 +- .../templates/chat-channel-info-settings.hbs | 2 +- .../discourse/templates/chat-channel-info.hbs | 20 +- .../templates/modal/create-channel.hbs | 8 +- .../discourse/widgets/chat-header-icon.js | 91 +- .../assets/stylesheets/common/common.scss | 32 +- .../assets/stylesheets/mobile/mobile.scss | 2 +- .../stylesheets/sidebar-extensions.scss | 2 +- plugins/chat/config/locales/client.en.yml | 4 +- .../chat/lib/chat_channel_archive_service.rb | 1 + plugins/chat/lib/chat_message_reactor.rb | 5 +- plugins/chat/plugin.rb | 53 +- .../chat/spec/fabricators/chat_fabricator.rb | 26 +- .../regular/auto_join_channel_batch_spec.rb | 2 +- .../jobs/regular/chat_notify_watching_spec.rb | 4 +- .../spec/lib/chat_channel_fetcher_spec.rb | 29 +- .../direct_message_channel_creator_spec.rb | 14 +- plugins/chat/spec/plugin_helper.rb | 9 +- plugins/chat/spec/plugin_spec.rb | 5 - .../chat_channel_memberships_query_spec.rb | 23 +- .../api/category_chatables_controller_spec.rb | 34 +- .../api/chat_channel_memberships_spec.rb | 39 - ..._notifications_settings_controller_spec.rb | 145 -- .../chat_channels_archives_controller_spec.rb | 145 ++ .../api/chat_channels_controller_spec.rb | 369 +++- ...t_channels_current_user_membership_spec.rb | 151 ++ ..._notifications_settings_controller_spec.rb | 140 ++ .../chat_channels_moves_controller_spec.rb | 123 ++ .../chat_channels_status_controller_spec.rb | 89 + .../api/chat_chatables_controller_spec.rb | 201 ++ .../api/chat_current_user_channels_spec.rb | 134 ++ .../requests/api/hints_controller_spec.rb | 65 +- .../requests/chat_channel_controller_spec.rb | 849 -------- .../spec/requests/chat_controller_spec.rb | 131 +- .../categories_controller_spec.rb | 0 .../{ => core_ext}/email_controller_spec.rb | 0 .../{ => core_ext}/users_controller_spec.rb | 0 .../direct_messages_controller_spec.rb | 4 +- .../structured_channel_serializer_spec.rb | 19 +- .../api/schemas/category_chat_channel.json | 26 +- .../examples/channel_access_example.rb | 9 +- plugins/chat/spec/system/anonymous_spec.rb | 15 + .../chat/spec/system/archive_channel_spec.rb | 124 ++ .../chat/spec/system/bookmark_message_spec.rb | 45 + plugins/chat/spec/system/browse_page_spec.rb | 190 ++ .../spec/system/channel_about_page_spec.rb | 111 + .../spec/system/channel_info_pages_spec.rb | 38 + .../spec/system/channel_members_page_spec.rb | 69 + .../system/channel_selector_modal_spec.rb | 79 + .../spec/system/channel_settings_page_spec.rb | 186 ++ plugins/chat/spec/system/chat_channel_spec.rb | 200 ++ .../chat/spec/system/chat_composer_spec.rb | 119 ++ .../chat/spec/system/closed_channel_spec.rb | 62 + .../chat/spec/system/create_channel_spec.rb | 111 + .../chat/spec/system/deleted_channel_spec.rb | 21 + .../chat/spec/system/edited_message_spec.rb | 33 + plugins/chat/spec/system/flag_message_spec.rb | 44 + plugins/chat/spec/system/jit_messages_spec.rb | 63 + .../spec/system/list_channels/mobile_spec.rb | 106 + .../system/list_channels/no_sidebar_spec.rb | 100 + .../spec/system/list_channels/sidebar_spec.rb | 129 ++ .../message_notifications_mobile_spec.rb | 195 ++ ...message_notifications_with_sidebar_spec.rb | 183 ++ .../system/move_message_to_channel_spec.rb | 95 + plugins/chat/spec/system/navigation_spec.rb | 8 +- .../spec/system/page_objects/chat/chat.rb | 33 +- .../system/page_objects/chat/chat_channel.rb | 80 + .../page_objects/chat_drawer/chat_drawer.rb | 1 + .../system/page_objects/sidebar/sidebar.rb | 5 + .../chat/spec/system/react_to_message_spec.rb | 144 ++ plugins/chat/spec/system/read_only_spec.rb | 58 + .../spec/system/receiving_message_spec.rb | 51 - .../spec/system/replying_indicator_spec.rb | 35 + .../system/shortcuts/chat_composer_spec.rb | 72 + .../chat/spec/system/shortcuts/drawer_spec.rb | 37 +- .../spec/system/shortcuts/sidebar_spec.rb | 50 + .../system/sidebar_navigation_menu_spec.rb | 187 ++ plugins/chat/spec/system/sidebars_spec.rb | 24 +- .../chat/spec/system/silenced_user_spec.rb | 44 + .../spec/system/unfollow_dm_channel_spec.rb | 38 + plugins/chat/spec/system/user_card_spec.rb | 76 + .../spec/system/user_chat_preferences_spec.rb | 34 + .../user_menu_notifications/sidebar_spec.rb | 185 ++ .../spec/system/user_status/sidebar_spec.rb | 44 + .../chat/spec/system/visit_channel_spec.rb | 185 ++ .../acceptance/chat-browse-test.js | 132 -- .../acceptance/chat-channel-info-test.js | 71 - .../acceptance/chat-channel-slug-test.js | 24 - .../acceptance/chat-channels-list-test.js | 62 - .../acceptance/chat-composer-test.js | 72 +- .../acceptance/chat-flagging-test.js | 96 - .../chat-keyboard-shortcuts-test.js | 290 --- .../chat-live-pane-collapse-test.js | 6 +- .../acceptance/chat-live-pane-mobile-test.js | 93 - .../chat-live-pane-silenced-user-test.js | 93 - .../acceptance/chat-live-pane-test.js | 294 +-- .../chat-message-bookmarking-test.js | 165 -- .../acceptance/chat-message-test.js | 99 - .../chat-move-message-to-channel-test.js | 183 -- .../acceptance/chat-preferences-test.js | 34 - .../acceptance/chat-quoting-test.js | 183 -- .../chat-sidebar-user-status-test.js | 127 -- .../acceptance/chat-status-test.js | 111 - .../test/javascripts/acceptance/chat-test.js | 1794 ----------------- .../acceptance/chat-transcript-test.js | 597 ------ .../chat-user-menu-notifications-test.js | 252 --- .../composer-hashtag-autocomplete-test.js | 68 - .../acceptance/core-sidebar-test.js | 777 ------- .../acceptance/create-channel-test.js | 186 -- .../delete-chat-channel-modal-test.js | 49 - .../acceptance/mobile-chat-test.js | 75 - .../acceptance/user-card-chat-test.js | 113 -- .../chat-channel-about-view-test.js | 149 -- .../components/chat-channel-card-test.js | 8 +- .../chat-channel-members-view-test.js | 140 -- .../components/chat-channel-metadata-test.js | 5 +- .../components/chat-channel-row-test.js | 10 +- .../chat-channel-settings-view-test.js | 290 --- .../chat-channel-toggle-view-test.js | 104 - .../chat-channel-unread-indicator-test.js | 91 - .../components/sidebar-channels-test.js | 78 - .../javascripts/helpers/chat-pretenders.js | 12 +- .../test/javascripts/helpers/chat-stub.js | 26 - .../javascripts/unit/services/chat-test.js | 296 --- 210 files changed, 6980 insertions(+), 10753 deletions(-) delete mode 100644 plugins/chat/app/controllers/api/chat_channel_memberships_controller.rb delete mode 100644 plugins/chat/app/controllers/api/chat_channel_notifications_settings_controller.rb create mode 100644 plugins/chat/app/controllers/api/chat_channels_archives_controller.rb create mode 100644 plugins/chat/app/controllers/api/chat_channels_current_user_membership_controller.rb create mode 100644 plugins/chat/app/controllers/api/chat_channels_current_user_notifications_settings_controller.rb create mode 100644 plugins/chat/app/controllers/api/chat_channels_memberships_controller.rb create mode 100644 plugins/chat/app/controllers/api/chat_channels_messages_moves_controller.rb create mode 100644 plugins/chat/app/controllers/api/chat_channels_status_controller.rb create mode 100644 plugins/chat/app/controllers/api/chat_chatables_controller.rb create mode 100644 plugins/chat/app/controllers/api/chat_current_user_channels_controller.rb delete mode 100644 plugins/chat/app/controllers/chat_channels_controller.rb delete mode 100644 plugins/chat/assets/javascripts/discourse/components/chat-channel-unread-indicator.js create mode 100644 plugins/chat/assets/javascripts/discourse/components/chat-header-icon-unread-indicator.hbs create mode 100644 plugins/chat/assets/javascripts/discourse/components/chat-header-icon-unread-indicator.js create mode 100644 plugins/chat/assets/javascripts/discourse/components/chat-header-icon.hbs create mode 100644 plugins/chat/assets/javascripts/discourse/components/chat-header-icon.js delete mode 100644 plugins/chat/assets/javascripts/discourse/lib/chat-api.js create mode 100644 plugins/chat/assets/javascripts/discourse/routes/chat-browse-archived.js create mode 100644 plugins/chat/assets/javascripts/discourse/routes/chat-channel-info-settings.js create mode 100644 plugins/chat/assets/javascripts/discourse/services/chat-api.js create mode 100644 plugins/chat/assets/javascripts/discourse/services/chat-channels-manager.js create mode 100644 plugins/chat/assets/javascripts/discourse/services/chat-subscriptions-manager.js delete mode 100644 plugins/chat/spec/requests/api/chat_channel_memberships_spec.rb delete mode 100644 plugins/chat/spec/requests/api/chat_channel_notifications_settings_controller_spec.rb create mode 100644 plugins/chat/spec/requests/api/chat_channels_archives_controller_spec.rb create mode 100644 plugins/chat/spec/requests/api/chat_channels_current_user_membership_spec.rb create mode 100644 plugins/chat/spec/requests/api/chat_channels_current_user_notifications_settings_controller_spec.rb create mode 100644 plugins/chat/spec/requests/api/chat_channels_moves_controller_spec.rb create mode 100644 plugins/chat/spec/requests/api/chat_channels_status_controller_spec.rb create mode 100644 plugins/chat/spec/requests/api/chat_chatables_controller_spec.rb create mode 100644 plugins/chat/spec/requests/api/chat_current_user_channels_spec.rb delete mode 100644 plugins/chat/spec/requests/chat_channel_controller_spec.rb rename plugins/chat/spec/requests/{ => core_ext}/categories_controller_spec.rb (100%) rename plugins/chat/spec/requests/{ => core_ext}/email_controller_spec.rb (100%) rename plugins/chat/spec/requests/{ => core_ext}/users_controller_spec.rb (100%) create mode 100644 plugins/chat/spec/system/anonymous_spec.rb create mode 100644 plugins/chat/spec/system/archive_channel_spec.rb create mode 100644 plugins/chat/spec/system/bookmark_message_spec.rb create mode 100644 plugins/chat/spec/system/browse_page_spec.rb create mode 100644 plugins/chat/spec/system/channel_about_page_spec.rb create mode 100644 plugins/chat/spec/system/channel_info_pages_spec.rb create mode 100644 plugins/chat/spec/system/channel_members_page_spec.rb create mode 100644 plugins/chat/spec/system/channel_selector_modal_spec.rb create mode 100644 plugins/chat/spec/system/channel_settings_page_spec.rb create mode 100644 plugins/chat/spec/system/chat_channel_spec.rb create mode 100644 plugins/chat/spec/system/chat_composer_spec.rb create mode 100644 plugins/chat/spec/system/closed_channel_spec.rb create mode 100644 plugins/chat/spec/system/create_channel_spec.rb create mode 100644 plugins/chat/spec/system/deleted_channel_spec.rb create mode 100644 plugins/chat/spec/system/edited_message_spec.rb create mode 100644 plugins/chat/spec/system/flag_message_spec.rb create mode 100644 plugins/chat/spec/system/jit_messages_spec.rb create mode 100644 plugins/chat/spec/system/list_channels/mobile_spec.rb create mode 100644 plugins/chat/spec/system/list_channels/no_sidebar_spec.rb create mode 100644 plugins/chat/spec/system/list_channels/sidebar_spec.rb create mode 100644 plugins/chat/spec/system/message_notifications_mobile_spec.rb create mode 100644 plugins/chat/spec/system/message_notifications_with_sidebar_spec.rb create mode 100644 plugins/chat/spec/system/move_message_to_channel_spec.rb create mode 100644 plugins/chat/spec/system/react_to_message_spec.rb create mode 100644 plugins/chat/spec/system/read_only_spec.rb delete mode 100644 plugins/chat/spec/system/receiving_message_spec.rb create mode 100644 plugins/chat/spec/system/replying_indicator_spec.rb create mode 100644 plugins/chat/spec/system/shortcuts/chat_composer_spec.rb create mode 100644 plugins/chat/spec/system/shortcuts/sidebar_spec.rb create mode 100644 plugins/chat/spec/system/sidebar_navigation_menu_spec.rb create mode 100644 plugins/chat/spec/system/silenced_user_spec.rb create mode 100644 plugins/chat/spec/system/unfollow_dm_channel_spec.rb create mode 100644 plugins/chat/spec/system/user_card_spec.rb create mode 100644 plugins/chat/spec/system/user_chat_preferences_spec.rb create mode 100644 plugins/chat/spec/system/user_menu_notifications/sidebar_spec.rb create mode 100644 plugins/chat/spec/system/user_status/sidebar_spec.rb create mode 100644 plugins/chat/spec/system/visit_channel_spec.rb delete mode 100644 plugins/chat/test/javascripts/acceptance/chat-browse-test.js delete mode 100644 plugins/chat/test/javascripts/acceptance/chat-channel-info-test.js delete mode 100644 plugins/chat/test/javascripts/acceptance/chat-channel-slug-test.js delete mode 100644 plugins/chat/test/javascripts/acceptance/chat-channels-list-test.js delete mode 100644 plugins/chat/test/javascripts/acceptance/chat-flagging-test.js delete mode 100644 plugins/chat/test/javascripts/acceptance/chat-keyboard-shortcuts-test.js delete mode 100644 plugins/chat/test/javascripts/acceptance/chat-live-pane-mobile-test.js delete mode 100644 plugins/chat/test/javascripts/acceptance/chat-live-pane-silenced-user-test.js delete mode 100644 plugins/chat/test/javascripts/acceptance/chat-message-bookmarking-test.js delete mode 100644 plugins/chat/test/javascripts/acceptance/chat-message-test.js delete mode 100644 plugins/chat/test/javascripts/acceptance/chat-move-message-to-channel-test.js delete mode 100644 plugins/chat/test/javascripts/acceptance/chat-preferences-test.js delete mode 100644 plugins/chat/test/javascripts/acceptance/chat-quoting-test.js delete mode 100644 plugins/chat/test/javascripts/acceptance/chat-sidebar-user-status-test.js delete mode 100644 plugins/chat/test/javascripts/acceptance/chat-status-test.js delete mode 100644 plugins/chat/test/javascripts/acceptance/chat-test.js delete mode 100644 plugins/chat/test/javascripts/acceptance/chat-transcript-test.js delete mode 100644 plugins/chat/test/javascripts/acceptance/chat-user-menu-notifications-test.js delete mode 100644 plugins/chat/test/javascripts/acceptance/composer-hashtag-autocomplete-test.js delete mode 100644 plugins/chat/test/javascripts/acceptance/core-sidebar-test.js delete mode 100644 plugins/chat/test/javascripts/acceptance/create-channel-test.js delete mode 100644 plugins/chat/test/javascripts/acceptance/delete-chat-channel-modal-test.js delete mode 100644 plugins/chat/test/javascripts/acceptance/mobile-chat-test.js delete mode 100644 plugins/chat/test/javascripts/acceptance/user-card-chat-test.js delete mode 100644 plugins/chat/test/javascripts/components/chat-channel-about-view-test.js delete mode 100644 plugins/chat/test/javascripts/components/chat-channel-members-view-test.js delete mode 100644 plugins/chat/test/javascripts/components/chat-channel-settings-view-test.js delete mode 100644 plugins/chat/test/javascripts/components/chat-channel-toggle-view-test.js delete mode 100644 plugins/chat/test/javascripts/components/chat-channel-unread-indicator-test.js delete mode 100644 plugins/chat/test/javascripts/components/sidebar-channels-test.js delete mode 100644 plugins/chat/test/javascripts/helpers/chat-stub.js delete mode 100644 plugins/chat/test/javascripts/unit/services/chat-test.js diff --git a/app/assets/javascripts/discourse/app/initializers/subscribe-user-notifications.js b/app/assets/javascripts/discourse/app/initializers/subscribe-user-notifications.js index 94fb9385ce5..e963f2fa003 100644 --- a/app/assets/javascripts/discourse/app/initializers/subscribe-user-notifications.js +++ b/app/assets/javascripts/discourse/app/initializers/subscribe-user-notifications.js @@ -58,7 +58,11 @@ export default { this.onDoNotDisturb ); - this.messageBus.subscribe(`/user-status`, this.onUserStatus); + this.messageBus.subscribe( + `/user-status`, + this.onUserStatus, + this.currentUser.status?.message_bus_last_id + ); this.messageBus.subscribe("/categories", this.onCategories); diff --git a/app/serializers/user_status_serializer.rb b/app/serializers/user_status_serializer.rb index 2866398d1b2..aba929520c2 100644 --- a/app/serializers/user_status_serializer.rb +++ b/app/serializers/user_status_serializer.rb @@ -1,5 +1,9 @@ # frozen_string_literal: true class UserStatusSerializer < ApplicationSerializer - attributes :description, :emoji, :ends_at + attributes :description, :emoji, :ends_at, :message_bus_last_id + + def message_bus_last_id + MessageBus.last_id("/user-status") + end end diff --git a/plugins/chat/app/controllers/api/chat_channel_memberships_controller.rb b/plugins/chat/app/controllers/api/chat_channel_memberships_controller.rb deleted file mode 100644 index 727811c9ca6..00000000000 --- a/plugins/chat/app/controllers/api/chat_channel_memberships_controller.rb +++ /dev/null @@ -1,20 +0,0 @@ -# frozen_string_literal: true - -class Chat::Api::ChatChannelMembershipsController < Chat::Api::ChatChannelsController - def index - channel = find_chat_channel - - offset = (params[:offset] || 0).to_i - limit = (params[:limit] || 50).to_i.clamp(1, 50) - - memberships = - ChatChannelMembershipsQuery.call( - channel, - offset: offset, - limit: limit, - username: params[:username], - ) - - render_serialized(memberships, UserChatChannelMembershipSerializer, root: false) - end -end diff --git a/plugins/chat/app/controllers/api/chat_channel_notifications_settings_controller.rb b/plugins/chat/app/controllers/api/chat_channel_notifications_settings_controller.rb deleted file mode 100644 index 57c0055424d..00000000000 --- a/plugins/chat/app/controllers/api/chat_channel_notifications_settings_controller.rb +++ /dev/null @@ -1,12 +0,0 @@ -# frozen_string_literal: true - -MEMBERSHIP_EDITABLE_PARAMS = %i[muted desktop_notification_level mobile_notification_level] - -class Chat::Api::ChatChannelNotificationsSettingsController < Chat::Api::ChatChannelsController - def update - settings_params = params.permit(MEMBERSHIP_EDITABLE_PARAMS) - membership = find_membership - membership.update!(settings_params.to_h) - render_serialized(membership, UserChatChannelMembershipSerializer, root: false) - end -end diff --git a/plugins/chat/app/controllers/api/chat_channels_archives_controller.rb b/plugins/chat/app/controllers/api/chat_channels_archives_controller.rb new file mode 100644 index 00000000000..759348ef4ec --- /dev/null +++ b/plugins/chat/app/controllers/api/chat_channels_archives_controller.rb @@ -0,0 +1,42 @@ +# frozen_string_literal: true + +class Chat::Api::ChatChannelsArchivesController < Chat::Api::ChatChannelsController + def create + existing_archive = channel_from_params.chat_channel_archive + + if existing_archive.present? + guardian.ensure_can_change_channel_status!(channel_from_params, :archived) + raise Discourse::InvalidAccess if !existing_archive.failed? + Chat::ChatChannelArchiveService.retry_archive_process(chat_channel: channel_from_params) + else + archive_params = + params + .require(:archive) + .tap do |ca| + ca.require(:type) + ca.permit(:title, :topic_id, :category_id, tags: []) + end + + new_topic = archive_params[:type] == "new_topic" + raise Discourse::InvalidParameters if new_topic && archive_params[:title].blank? + raise Discourse::InvalidParameters if !new_topic && archive_params[:topic_id].blank? + + if !guardian.can_change_channel_status?(channel_from_params, :read_only) + raise Discourse::InvalidAccess.new(I18n.t("chat.errors.channel_cannot_be_archived")) + end + + Chat::ChatChannelArchiveService.begin_archive_process( + chat_channel: channel_from_params, + acting_user: current_user, + topic_params: { + topic_id: archive_params[:topic_id], + topic_title: archive_params[:title], + category_id: archive_params[:category_id], + tags: archive_params[:tags], + }, + ) + end + + render json: success_json + end +end diff --git a/plugins/chat/app/controllers/api/chat_channels_controller.rb b/plugins/chat/app/controllers/api/chat_channels_controller.rb index 6658b381eb7..9e70a95d015 100644 --- a/plugins/chat/app/controllers/api/chat_channels_controller.rb +++ b/plugins/chat/app/controllers/api/chat_channels_controller.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true -CHAT_CHANNEL_EDITABLE_PARAMS = %i[name description] -CATEGORY_CHAT_CHANNEL_EDITABLE_PARAMS = %i[auto_join_users allow_channel_wide_mentions] +CHANNEL_EDITABLE_PARAMS = %i[name description] +CATEGORY_CHANNEL_EDITABLE_PARAMS = %i[auto_join_users allow_channel_wide_mentions] class Chat::Api::ChatChannelsController < Chat::Api def index @@ -9,6 +9,9 @@ class Chat::Api::ChatChannelsController < Chat::Api params.permit(:filter, :limit, :offset), ).symbolize_keys! + options[:offset] = options[:offset].to_i + options[:limit] = (options[:limit] || 25).to_i + memberships = Chat::ChatChannelMembershipManager.all_for_user(current_user) channels = Chat::ChatChannelFetcher.secured_public_channels(guardian, memberships, options) @@ -20,73 +23,166 @@ class Chat::Api::ChatChannelsController < Chat::Api membership: memberships.find { |membership| membership.chat_channel_id == channel.id }, ) end - render json: serialized_channels, root: false + + pagination_options = + options.slice(:offset, :limit, :filter).merge(offset: options[:offset] + options[:limit]) + pagination_params = pagination_options.map { |k, v| "#{k}=#{v}" }.join("&") + render json: serialized_channels, + root: "channels", + meta: { + load_more_url: "/chat/api/channels?#{pagination_params}", + } + end + + def destroy + confirmation = params.require(:channel).require(:name_confirmation)&.downcase + guardian.ensure_can_delete_chat_channel! + + if channel_from_params.title(current_user).downcase != confirmation + raise Discourse::InvalidParameters.new(:name_confirmation) + end + + begin + ChatChannel.transaction do + channel_from_params.trash!(current_user) + StaffActionLogger.new(current_user).log_custom( + "chat_channel_delete", + { + chat_channel_id: channel_from_params.id, + chat_channel_name: channel_from_params.title(current_user), + }, + ) + end + rescue ActiveRecord::Rollback + return render_json_error(I18n.t("chat.errors.delete_channel_failed")) + end + + Jobs.enqueue(:chat_channel_delete, { chat_channel_id: channel_from_params.id }) + render json: success_json + end + + def create + channel_params = + params.require(:channel).permit(:chatable_id, :name, :description, :auto_join_users) + + guardian.ensure_can_create_chat_channel! + if channel_params[:name].length > SiteSetting.max_topic_title_length + raise Discourse::InvalidParameters.new(:name) + end + + if ChatChannel.exists?( + chatable_type: "Category", + chatable_id: channel_params[:chatable_id], + name: channel_params[:name], + ) + raise Discourse::InvalidParameters.new(I18n.t("chat.errors.channel_exists_for_category")) + end + + chatable = Category.find_by(id: channel_params[:chatable_id]) + raise Discourse::NotFound unless chatable + + auto_join_users = + ActiveRecord::Type::Boolean.new.deserialize(channel_params[:auto_join_users]) || false + + channel = + chatable.create_chat_channel!( + name: channel_params[:name], + description: channel_params[:description], + user_count: 1, + auto_join_users: auto_join_users, + ) + + channel.user_chat_channel_memberships.create!(user: current_user, following: true) + + if channel.auto_join_users + Chat::ChatChannelMembershipManager.new(channel).enforce_automatic_channel_memberships + end + + render_serialized( + channel, + ChatChannelSerializer, + membership: channel.membership_for(current_user), + root: "channel", + ) + end + + def show + render_serialized( + channel_from_params, + ChatChannelSerializer, + membership: channel_from_params.membership_for(current_user), + root: "channel", + ) end def update guardian.ensure_can_edit_chat_channel! - chat_channel = find_chat_channel - - if chat_channel.direct_message_channel? + if channel_from_params.direct_message_channel? raise Discourse::InvalidParameters.new( I18n.t("chat.errors.cant_update_direct_message_channel"), ) end - params_to_edit = editable_params(params, chat_channel) + params_to_edit = editable_params(params, channel_from_params) params_to_edit.each { |k, v| params_to_edit[k] = nil if params_to_edit[k].blank? } if ActiveRecord::Type::Boolean.new.deserialize(params_to_edit[:auto_join_users]) - auto_join_limiter(chat_channel).performed! + auto_join_limiter(channel_from_params).performed! end - chat_channel.update!(params_to_edit) + channel_from_params.update!(params_to_edit) - ChatPublisher.publish_chat_channel_edit(chat_channel, current_user) + ChatPublisher.publish_chat_channel_edit(channel_from_params, current_user) - if chat_channel.category_channel? && chat_channel.auto_join_users - Chat::ChatChannelMembershipManager.new(chat_channel).enforce_automatic_channel_memberships + if channel_from_params.category_channel? && channel_from_params.auto_join_users + Chat::ChatChannelMembershipManager.new( + channel_from_params, + ).enforce_automatic_channel_memberships end render_serialized( - chat_channel, + channel_from_params, ChatChannelSerializer, - root: false, - membership: chat_channel.membership_for(current_user), + root: "channel", + membership: channel_from_params.membership_for(current_user), ) end private - def find_chat_channel - chat_channel = ChatChannel.find(params.require(:chat_channel_id)) - guardian.ensure_can_join_chat_channel!(chat_channel) - chat_channel + def channel_from_params + @channel ||= + begin + channel = ChatChannel.find(params.require(:channel_id)) + guardian.ensure_can_preview_chat_channel!(channel) + channel + end end - def find_membership - chat_channel = find_chat_channel - membership = Chat::ChatChannelMembershipManager.new(chat_channel).find_for_user(current_user) - raise Discourse::NotFound if membership.blank? - membership + def membership_from_params + @membership ||= + begin + membership = + Chat::ChatChannelMembershipManager.new(channel_from_params).find_for_user(current_user) + raise Discourse::NotFound if membership.blank? + membership + end end - def auto_join_limiter(chat_channel) + def auto_join_limiter(channel) RateLimiter.new( current_user, - "auto_join_users_channel_#{chat_channel.id}", + "auto_join_users_channel_#{channel.id}", 1, 3.minutes, apply_limit_to_staff: true, ) end - def editable_params(params, chat_channel) - permitted_params = CHAT_CHANNEL_EDITABLE_PARAMS - - permitted_params += CATEGORY_CHAT_CHANNEL_EDITABLE_PARAMS if chat_channel.category_channel? - - params.permit(*permitted_params) + def editable_params(params, channel) + permitted_params = CHANNEL_EDITABLE_PARAMS + permitted_params += CATEGORY_CHANNEL_EDITABLE_PARAMS if channel.category_channel? + params.require(:channel).permit(*permitted_params) end end diff --git a/plugins/chat/app/controllers/api/chat_channels_current_user_membership_controller.rb b/plugins/chat/app/controllers/api/chat_channels_current_user_membership_controller.rb new file mode 100644 index 00000000000..91422f9d673 --- /dev/null +++ b/plugins/chat/app/controllers/api/chat_channels_current_user_membership_controller.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true + +class Chat::Api::ChatChannelsCurrentUserMembershipController < Chat::Api::ChatChannelsController + def create + guardian.ensure_can_join_chat_channel!(channel_from_params) + + render_serialized( + channel_from_params.add(current_user), + UserChatChannelMembershipSerializer, + root: "membership", + ) + end + + def destroy + render_serialized( + channel_from_params.remove(current_user), + UserChatChannelMembershipSerializer, + root: "membership", + ) + end +end diff --git a/plugins/chat/app/controllers/api/chat_channels_current_user_notifications_settings_controller.rb b/plugins/chat/app/controllers/api/chat_channels_current_user_notifications_settings_controller.rb new file mode 100644 index 00000000000..d9a8f4ac57e --- /dev/null +++ b/plugins/chat/app/controllers/api/chat_channels_current_user_notifications_settings_controller.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +MEMBERSHIP_EDITABLE_PARAMS = %i[muted desktop_notification_level mobile_notification_level] + +class Chat::Api::ChatChannelsCurrentUserNotificationsSettingsController < Chat::Api::ChatChannelsController + def update + settings_params = params.require(:notifications_settings).permit(MEMBERSHIP_EDITABLE_PARAMS) + membership_from_params.update!(settings_params.to_h) + render_serialized( + membership_from_params, + UserChatChannelMembershipSerializer, + root: "membership", + ) + end +end diff --git a/plugins/chat/app/controllers/api/chat_channels_memberships_controller.rb b/plugins/chat/app/controllers/api/chat_channels_memberships_controller.rb new file mode 100644 index 00000000000..c50a30735e7 --- /dev/null +++ b/plugins/chat/app/controllers/api/chat_channels_memberships_controller.rb @@ -0,0 +1,29 @@ +# frozen_string_literal: true + +class Chat::Api::ChatChannelsMembershipsController < Chat::Api::ChatChannelsController + def index + params.permit(:username, :offset, :limit) + + offset = params[:offset].to_i + limit = (params[:limit] || 50).to_i.clamp(1, 50) + + memberships = + ChatChannelMembershipsQuery.call( + channel_from_params, + offset: offset, + limit: limit, + username: params[:username], + ) + + render_serialized( + memberships, + UserChatChannelMembershipSerializer, + root: "memberships", + meta: { + total_rows: channel_from_params.user_count, + load_more_url: + "/chat/api/channels/#{channel_from_params.id}/memberships?offset=#{offset + limit}&limit=#{limit}&username=#{params[:username]}", + }, + ) + end +end diff --git a/plugins/chat/app/controllers/api/chat_channels_messages_moves_controller.rb b/plugins/chat/app/controllers/api/chat_channels_messages_moves_controller.rb new file mode 100644 index 00000000000..d0d3ff1777f --- /dev/null +++ b/plugins/chat/app/controllers/api/chat_channels_messages_moves_controller.rb @@ -0,0 +1,35 @@ +# frozen_string_literal: true + +class Chat::Api::ChatChannelsMessagesMovesController < Chat::Api::ChatChannelsController + def create + move_params = params.require(:move) + move_params.require(:message_ids) + move_params.require(:destination_channel_id) + + raise Discourse::InvalidAccess if !guardian.can_move_chat_messages?(channel_from_params) + destination_channel = + Chat::ChatChannelFetcher.find_with_access_check( + move_params[:destination_channel_id], + guardian, + ) + + begin + message_ids = move_params[:message_ids].map(&:to_i) + moved_messages = + Chat::MessageMover.new( + acting_user: current_user, + source_channel: channel_from_params, + message_ids: message_ids, + ).move_to_channel(destination_channel) + rescue Chat::MessageMover::NoMessagesFound, Chat::MessageMover::InvalidChannel => err + return render_json_error(err.message) + end + + render json: + success_json.merge( + destination_channel_id: destination_channel.id, + destination_channel_title: destination_channel.title(current_user), + first_moved_message_id: moved_messages.first.id, + ) + end +end diff --git a/plugins/chat/app/controllers/api/chat_channels_status_controller.rb b/plugins/chat/app/controllers/api/chat_channels_status_controller.rb new file mode 100644 index 00000000000..78b1ac3f2cb --- /dev/null +++ b/plugins/chat/app/controllers/api/chat_channels_status_controller.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +class Chat::Api::ChatChannelsStatusController < Chat::Api::ChatChannelsController + def update + status = params.require(:status) + + # we only want to use this endpoint for open/closed status changes, + # the others are more "special" and are handled by the archive endpoint + if !ChatChannel.statuses.keys.include?(status) || status == "read_only" || status == "archive" + raise Discourse::InvalidParameters + end + + guardian.ensure_can_change_channel_status!(channel_from_params, status.to_sym) + channel_from_params.public_send("#{status}!", current_user) + + render_serialized(channel_from_params, ChatChannelSerializer, root: "channel") + end +end diff --git a/plugins/chat/app/controllers/api/chat_chatables_controller.rb b/plugins/chat/app/controllers/api/chat_chatables_controller.rb new file mode 100644 index 00000000000..9eaec32b89b --- /dev/null +++ b/plugins/chat/app/controllers/api/chat_chatables_controller.rb @@ -0,0 +1,82 @@ +# frozen_string_literal: true + +class Chat::Api::ChatChatablesController < Chat::Api + def index + params.require(:filter) + filter = params[:filter].downcase + + memberships = Chat::ChatChannelMembershipManager.all_for_user(current_user) + public_channels = + Chat::ChatChannelFetcher.secured_public_channels( + guardian, + memberships, + filter: filter, + status: :open, + ) + + users = User.joins(:user_option).where.not(id: current_user.id) + if !Chat.allowed_group_ids.include?(Group::AUTO_GROUPS[:everyone]) + users = + users + .joins(:groups) + .where(groups: { id: Chat.allowed_group_ids }) + .or(users.joins(:groups).staff) + end + + users = users.where(user_option: { chat_enabled: true }) + like_filter = "%#{filter}%" + if SiteSetting.prioritize_username_in_ux || !SiteSetting.enable_names + users = users.where("users.username_lower ILIKE ?", like_filter) + else + users = + users.where( + "LOWER(users.name) ILIKE ? OR users.username_lower ILIKE ?", + like_filter, + like_filter, + ) + end + + users = users.limit(25).uniq + + direct_message_channels = + if users.count > 0 + # FIXME: investigate the cost of this query + ChatChannel + .includes(chatable: :users) + .joins(direct_message: :direct_message_users) + .group(1) + .having( + "ARRAY[?] <@ ARRAY_AGG(user_id) AND ARRAY[?] && ARRAY_AGG(user_id)", + [current_user.id], + users.map(&:id), + ) + else + [] + end + + user_ids_with_channel = [] + direct_message_channels.each do |dm_channel| + user_ids = dm_channel.chatable.users.map(&:id) + user_ids_with_channel.concat(user_ids) if user_ids.count < 3 + end + + users_without_channel = users.filter { |u| !user_ids_with_channel.include?(u.id) } + + if current_user.username.downcase.start_with?(filter) + # We filtered out the current user for the query earlier, but check to see + # if they should be included, and add. + users_without_channel << current_user + end + + render_serialized( + { + public_channels: public_channels, + direct_message_channels: direct_message_channels, + users: users_without_channel, + memberships: memberships, + }, + ChatChannelSearchSerializer, + root: false, + ) + end +end diff --git a/plugins/chat/app/controllers/api/chat_current_user_channels_controller.rb b/plugins/chat/app/controllers/api/chat_current_user_channels_controller.rb new file mode 100644 index 00000000000..ecc01163606 --- /dev/null +++ b/plugins/chat/app/controllers/api/chat_current_user_channels_controller.rb @@ -0,0 +1,8 @@ +# frozen_string_literal: true + +class Chat::Api::ChatCurrentUserChannelsController < Chat::Api + def index + structured = Chat::ChatChannelFetcher.structured(guardian) + render_serialized(structured, ChatChannelIndexSerializer, root: false) + end +end diff --git a/plugins/chat/app/controllers/chat_channels_controller.rb b/plugins/chat/app/controllers/chat_channels_controller.rb deleted file mode 100644 index cecc5b2f1fd..00000000000 --- a/plugins/chat/app/controllers/chat_channels_controller.rb +++ /dev/null @@ -1,250 +0,0 @@ -# frozen_string_literal: true - -class Chat::ChatChannelsController < Chat::ChatBaseController - before_action :set_channel_and_chatable_with_access_check, except: %i[index create search] - - def index - structured = Chat::ChatChannelFetcher.structured(guardian) - render_serialized(structured, ChatChannelIndexSerializer, root: false) - end - - def show - render_serialized( - @chat_channel, - ChatChannelSerializer, - membership: @chat_channel.membership_for(current_user), - root: false, - ) - end - - def follow - membership = @chat_channel.add(current_user) - - render_serialized(@chat_channel, ChatChannelSerializer, membership: membership, root: false) - end - - def unfollow - membership = @chat_channel.remove(current_user) - - render_serialized(@chat_channel, ChatChannelSerializer, membership: membership, root: false) - end - - def create - params.require(%i[id name]) - guardian.ensure_can_create_chat_channel! - if params[:name].length > SiteSetting.max_topic_title_length - raise Discourse::InvalidParameters.new(:name) - end - - exists = - ChatChannel.exists?(chatable_type: "Category", chatable_id: params[:id], name: params[:name]) - if exists - raise Discourse::InvalidParameters.new(I18n.t("chat.errors.channel_exists_for_category")) - end - - chatable = Category.find_by(id: params[:id]) - raise Discourse::NotFound unless chatable - - auto_join_users = ActiveRecord::Type::Boolean.new.deserialize(params[:auto_join_users]) || false - - chat_channel = - chatable.create_chat_channel!( - name: params[:name], - description: params[:description], - user_count: 1, - auto_join_users: auto_join_users, - ) - chat_channel.user_chat_channel_memberships.create!(user: current_user, following: true) - - if chat_channel.auto_join_users - Chat::ChatChannelMembershipManager.new(chat_channel).enforce_automatic_channel_memberships - end - - render_serialized( - chat_channel, - ChatChannelSerializer, - membership: chat_channel.membership_for(current_user), - ) - end - - def edit - guardian.ensure_can_edit_chat_channel! - if (params[:name]&.length || 0) > SiteSetting.max_topic_title_length - raise Discourse::InvalidParameters.new(:name) - end - - chat_channel = ChatChannel.find_by(id: params[:chat_channel_id]) - raise Discourse::NotFound unless chat_channel - - chat_channel.name = params[:name] if params[:name] - chat_channel.description = params[:description] if params[:description] - chat_channel.save! - - ChatPublisher.publish_chat_channel_edit(chat_channel, current_user) - render_serialized( - chat_channel, - ChatChannelSerializer, - membership: chat_channel.membership_for(current_user), - ) - end - - def search - params.require(:filter) - filter = params[:filter]&.downcase - memberships = Chat::ChatChannelMembershipManager.all_for_user(current_user) - public_channels = - Chat::ChatChannelFetcher.secured_public_channels( - guardian, - memberships, - filter: filter, - status: :open, - ) - - users = User.joins(:user_option).where.not(id: current_user.id) - if !Chat.allowed_group_ids.include?(Group::AUTO_GROUPS[:everyone]) - users = - users - .joins(:groups) - .where(groups: { id: Chat.allowed_group_ids }) - .or(users.joins(:groups).staff) - end - - users = users.where(user_option: { chat_enabled: true }) - like_filter = "%#{filter}%" - if SiteSetting.prioritize_username_in_ux || !SiteSetting.enable_names - users = users.where("users.username_lower ILIKE ?", like_filter) - else - users = - users.where( - "LOWER(users.name) ILIKE ? OR users.username_lower ILIKE ?", - like_filter, - like_filter, - ) - end - - users = users.limit(25).uniq - - direct_message_channels = - ( - if users.count > 0 - ChatChannel - .includes(chatable: :users) - .joins(direct_message: :direct_message_users) - .group(1) - .having( - "ARRAY[?] <@ ARRAY_AGG(user_id) AND ARRAY[?] && ARRAY_AGG(user_id)", - [current_user.id], - users.map(&:id), - ) - else - [] - end - ) - - user_ids_with_channel = [] - direct_message_channels.each do |dm_channel| - user_ids = dm_channel.chatable.users.map(&:id) - user_ids_with_channel.concat(user_ids) if user_ids.count < 3 - end - - users_without_channel = users.filter { |u| !user_ids_with_channel.include?(u.id) } - - if current_user.username.downcase.start_with?(filter) - # We filtered out the current user for the query earlier, but check to see - # if they should be included, and add. - users_without_channel << current_user - end - - render_serialized( - { - public_channels: public_channels, - direct_message_channels: direct_message_channels, - users: users_without_channel, - memberships: memberships, - }, - ChatChannelSearchSerializer, - root: false, - ) - end - - def archive - params.require(:type) - - if params[:type] == "newTopic" ? params[:title].blank? : params[:topic_id].blank? - raise Discourse::InvalidParameters - end - - if !guardian.can_change_channel_status?(@chat_channel, :read_only) - raise Discourse::InvalidAccess.new(I18n.t("chat.errors.channel_cannot_be_archived")) - end - - Chat::ChatChannelArchiveService.begin_archive_process( - chat_channel: @chat_channel, - acting_user: current_user, - topic_params: { - topic_id: params[:topic_id], - topic_title: params[:title], - category_id: params[:category_id], - tags: params[:tags], - }, - ) - - render json: success_json - end - - def retry_archive - guardian.ensure_can_change_channel_status!(@chat_channel, :archived) - - archive = @chat_channel.chat_channel_archive - raise Discourse::NotFound if archive.blank? - raise Discourse::InvalidAccess if !archive.failed? - - Chat::ChatChannelArchiveService.retry_archive_process(chat_channel: @chat_channel) - - render json: success_json - end - - def change_status - params.require(:status) - - # we only want to use this endpoint for open/closed status changes, - # the others are more "special" and are handled by the archive endpoint - if !ChatChannel.statuses.keys.include?(params[:status]) || params[:status] == "read_only" || - params[:status] == "archive" - raise Discourse::InvalidParameters - end - - guardian.ensure_can_change_channel_status!(@chat_channel, params[:status].to_sym) - @chat_channel.public_send("#{params[:status]}!", current_user) - - render json: success_json - end - - def destroy - params.require(:channel_name_confirmation) - - guardian.ensure_can_delete_chat_channel! - - if @chat_channel.title(current_user).downcase != params[:channel_name_confirmation].downcase - raise Discourse::InvalidParameters.new(:channel_name_confirmation) - end - - begin - ChatChannel.transaction do - @chat_channel.trash!(current_user) - StaffActionLogger.new(current_user).log_custom( - "chat_channel_delete", - { - chat_channel_id: @chat_channel.id, - chat_channel_name: @chat_channel.title(current_user), - }, - ) - end - rescue ActiveRecord::Rollback - return render_json_error(I18n.t("chat.errors.delete_channel_failed")) - end - - Jobs.enqueue(:chat_channel_delete, { chat_channel_id: @chat_channel.id }) - render json: success_json - end -end diff --git a/plugins/chat/app/controllers/chat_controller.rb b/plugins/chat/app/controllers/chat_controller.rb index c4ad14e13dd..6b7d74d020b 100644 --- a/plugins/chat/app/controllers/chat_controller.rb +++ b/plugins/chat/app/controllers/chat_controller.rb @@ -110,7 +110,9 @@ class Chat::ChatController < Chat::ChatBaseController return render_json_error(chat_message_creator.error) if chat_message_creator.failed? - @user_chat_channel_membership.update(last_read_message_id: chat_message_creator.chat_message.id) + @user_chat_channel_membership.update!( + last_read_message_id: chat_message_creator.chat_message.id, + ) if @chat_channel.direct_message_channel? # If any of the channel users is ignoring, muting, or preventing DMs from @@ -123,14 +125,15 @@ class Chat::ChatController < Chat::ChatBaseController ).allowing_actor_communication if user_ids_allowing_communication.any? - @chat_channel - .user_chat_channel_memberships - .where(user_id: user_ids_allowing_communication) - .update_all(following: true) ChatPublisher.publish_new_channel( @chat_channel, @chat_channel.chatable.users.where(id: user_ids_allowing_communication), ) + + @chat_channel + .user_chat_channel_memberships + .where(user_id: user_ids_allowing_communication) + .update_all(following: true) end end @@ -398,34 +401,6 @@ class Chat::ChatController < Chat::ChatBaseController render json: success_json.merge(markdown: markdown) end - def move_messages_to_channel - params.require(:message_ids) - params.require(:destination_channel_id) - - raise Discourse::InvalidAccess if !guardian.can_move_chat_messages?(@chat_channel) - destination_channel = - Chat::ChatChannelFetcher.find_with_access_check(params[:destination_channel_id], guardian) - - begin - message_ids = params[:message_ids].map(&:to_i) - moved_messages = - Chat::MessageMover.new( - acting_user: current_user, - source_channel: @chat_channel, - message_ids: message_ids, - ).move_to_channel(destination_channel) - rescue Chat::MessageMover::NoMessagesFound, Chat::MessageMover::InvalidChannel => err - return render_json_error(err.message) - end - - render json: - success_json.merge( - destination_channel_id: destination_channel.id, - destination_channel_title: destination_channel.title(current_user), - first_moved_message_id: moved_messages.first.id, - ) - end - def flag RateLimiter.new(current_user, "flag_chat_message", 4, 1.minutes).performed! diff --git a/plugins/chat/app/controllers/direct_messages_controller.rb b/plugins/chat/app/controllers/direct_messages_controller.rb index fb07f66874b..b0100a95a89 100644 --- a/plugins/chat/app/controllers/direct_messages_controller.rb +++ b/plugins/chat/app/controllers/direct_messages_controller.rb @@ -13,7 +13,7 @@ class Chat::DirectMessagesController < Chat::ChatBaseController render_serialized( chat_channel, ChatChannelSerializer, - root: "chat_channel", + root: "channel", membership: chat_channel.membership_for(current_user), ) rescue Chat::DirectMessageChannelCreator::NotAllowed => err @@ -31,7 +31,7 @@ class Chat::DirectMessagesController < Chat::ChatBaseController render_serialized( chat_channel, ChatChannelSerializer, - root: "chat_channel", + root: "channel", membership: chat_channel.membership_for(current_user), ) else diff --git a/plugins/chat/app/jobs/regular/chat_channel_archive.rb b/plugins/chat/app/jobs/regular/chat_channel_archive.rb index c5eb878d33b..33e270dd220 100644 --- a/plugins/chat/app/jobs/regular/chat_channel_archive.rb +++ b/plugins/chat/app/jobs/regular/chat_channel_archive.rb @@ -15,7 +15,19 @@ module Jobs return end - return if channel_archive.complete? + if channel_archive.complete? + channel_archive.chat_channel.update!(status: :archived) + + ChatPublisher.publish_archive_status( + channel_archive.chat_channel, + archive_status: :success, + archived_messages: channel_archive.archived_messages, + archive_topic_id: channel_archive.destination_topic_id, + total_messages: channel_archive.total_messages, + ) + + return + end DistributedMutex.synchronize( "archive_chat_channel_#{channel_archive.chat_channel_id}", diff --git a/plugins/chat/app/serializers/chat_channel_serializer.rb b/plugins/chat/app/serializers/chat_channel_serializer.rb index fe321c7fa0e..9193334177d 100644 --- a/plugins/chat/app/serializers/chat_channel_serializer.rb +++ b/plugins/chat/app/serializers/chat_channel_serializer.rb @@ -20,7 +20,7 @@ class ChatChannelSerializer < ApplicationSerializer :archive_topic_id, :memberships_count, :current_user_membership, - :message_bus_last_ids + :meta def initialize(object, opts) super(object, opts) @@ -61,7 +61,7 @@ class ChatChannelSerializer < ApplicationSerializer end def include_archive_status? - scope.is_staff? && object.archived? && archive.present? + scope.is_staff? && (object.archived? || archive&.failed?) && archive.present? end def archive_completed @@ -88,8 +88,11 @@ class ChatChannelSerializer < ApplicationSerializer scope.can_edit_chat_channel? end + def include_current_user_membership? + @current_user_membership.present? + end + def current_user_membership - return if !@current_user_membership @current_user_membership.chat_channel = object UserChatChannelMembershipSerializer.new( @current_user_membership, @@ -98,10 +101,17 @@ class ChatChannelSerializer < ApplicationSerializer ).as_json end - def message_bus_last_ids + def meta { - new_messages: @opts[:new_messages_message_bus_last_id] || MessageBus.last_id(ChatPublisher.new_messages_message_bus_channel(object.id)), - new_mentions: @opts[:new_mentions_message_bus_last_id] || MessageBus.last_id(ChatPublisher.new_mentions_message_bus_channel(object.id)), + message_bus_last_ids: { + new_messages: + @opts[:new_messages_message_bus_last_id] || + MessageBus.last_id(ChatPublisher.new_messages_message_bus_channel(object.id)), + new_mentions: + @opts[:new_mentions_message_bus_last_id] || + MessageBus.last_id(ChatPublisher.new_mentions_message_bus_channel(object.id)), + archive_status: MessageBus.last_id("/chat/channel-archive-status"), + }, } end diff --git a/plugins/chat/app/serializers/structured_channel_serializer.rb b/plugins/chat/app/serializers/structured_channel_serializer.rb index 8ecbc6103d8..a197c36c62e 100644 --- a/plugins/chat/app/serializers/structured_channel_serializer.rb +++ b/plugins/chat/app/serializers/structured_channel_serializer.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true class StructuredChannelSerializer < ApplicationSerializer - attributes :public_channels, :direct_message_channels, :message_bus_last_ids + attributes :public_channels, :direct_message_channels, :meta def public_channels object[:public_channels].map do |channel| @@ -10,8 +10,10 @@ class StructuredChannelSerializer < ApplicationSerializer root: nil, scope: scope, membership: channel_membership(channel.id), - new_messages_message_bus_last_id: chat_message_bus_last_ids[ChatPublisher.new_messages_message_bus_channel(channel.id)], - new_mentions_message_bus_last_id: chat_message_bus_last_ids[ChatPublisher.new_mentions_message_bus_channel(channel.id)] + new_messages_message_bus_last_id: + chat_message_bus_last_ids[ChatPublisher.new_messages_message_bus_channel(channel.id)], + new_mentions_message_bus_last_id: + chat_message_bus_last_ids[ChatPublisher.new_mentions_message_bus_channel(channel.id)], ) end end @@ -23,8 +25,10 @@ class StructuredChannelSerializer < ApplicationSerializer root: nil, scope: scope, membership: channel_membership(channel.id), - new_messages_message_bus_last_id: chat_message_bus_last_ids[ChatPublisher.new_messages_message_bus_channel(channel.id)], - new_mentions_message_bus_last_id: chat_message_bus_last_ids[ChatPublisher.new_mentions_message_bus_channel(channel.id)] + new_messages_message_bus_last_id: + chat_message_bus_last_ids[ChatPublisher.new_messages_message_bus_channel(channel.id)], + new_mentions_message_bus_last_id: + chat_message_bus_last_ids[ChatPublisher.new_mentions_message_bus_channel(channel.id)], ) end end @@ -34,47 +38,54 @@ class StructuredChannelSerializer < ApplicationSerializer object[:memberships].find { |membership| membership.chat_channel_id == channel_id } end - def message_bus_last_ids - ids = { - channel_metadata: chat_message_bus_last_ids[ChatPublisher::CHANNEL_METADATA_MESSAGE_BUS_CHANNEL], + def meta + last_ids = { + channel_metadata: + chat_message_bus_last_ids[ChatPublisher::CHANNEL_METADATA_MESSAGE_BUS_CHANNEL], channel_edits: chat_message_bus_last_ids[ChatPublisher::CHANNEL_EDITS_MESSAGE_BUS_CHANNEL], channel_status: chat_message_bus_last_ids[ChatPublisher::CHANNEL_STATUS_MESSAGE_BUS_CHANNEL], - new_channel: chat_message_bus_last_ids[ChatPublisher::NEW_CHANNEL_MESSAGE_BUS_CHANNEL] + new_channel: chat_message_bus_last_ids[ChatPublisher::NEW_CHANNEL_MESSAGE_BUS_CHANNEL], } - if id = chat_message_bus_last_ids[ChatPublisher.user_tracking_state_message_bus_channel(scope.user.id)] - ids[:user_tracking_state] = id + if id = + chat_message_bus_last_ids[ + ChatPublisher.user_tracking_state_message_bus_channel(scope.user.id) + ] + last_ids[:user_tracking_state] = id end - ids + { message_bus_last_ids: last_ids } end private def chat_message_bus_last_ids - @chat_message_bus_last_ids ||= begin - message_bus_channels = [ - ChatPublisher::CHANNEL_METADATA_MESSAGE_BUS_CHANNEL, - ChatPublisher::CHANNEL_EDITS_MESSAGE_BUS_CHANNEL, - ChatPublisher::CHANNEL_STATUS_MESSAGE_BUS_CHANNEL, - ChatPublisher::NEW_CHANNEL_MESSAGE_BUS_CHANNEL, - ] + @chat_message_bus_last_ids ||= + begin + message_bus_channels = [ + ChatPublisher::CHANNEL_METADATA_MESSAGE_BUS_CHANNEL, + ChatPublisher::CHANNEL_EDITS_MESSAGE_BUS_CHANNEL, + ChatPublisher::CHANNEL_STATUS_MESSAGE_BUS_CHANNEL, + ChatPublisher::NEW_CHANNEL_MESSAGE_BUS_CHANNEL, + ] - if !scope.anonymous? - message_bus_channels.push(ChatPublisher.user_tracking_state_message_bus_channel(scope.user.id)) + if !scope.anonymous? + message_bus_channels.push( + ChatPublisher.user_tracking_state_message_bus_channel(scope.user.id), + ) + end + + object[:public_channels].each do |channel| + message_bus_channels.push(ChatPublisher.new_messages_message_bus_channel(channel.id)) + message_bus_channels.push(ChatPublisher.new_mentions_message_bus_channel(channel.id)) + end + + object[:direct_message_channels].each do |channel| + message_bus_channels.push(ChatPublisher.new_messages_message_bus_channel(channel.id)) + message_bus_channels.push(ChatPublisher.new_mentions_message_bus_channel(channel.id)) + end + + MessageBus.last_ids(*message_bus_channels) end - - object[:public_channels].each do |channel| - message_bus_channels.push(ChatPublisher.new_messages_message_bus_channel(channel.id)) - message_bus_channels.push(ChatPublisher.new_mentions_message_bus_channel(channel.id)) - end - - object[:direct_message_channels].each do |channel| - message_bus_channels.push(ChatPublisher.new_messages_message_bus_channel(channel.id)) - message_bus_channels.push(ChatPublisher.new_mentions_message_bus_channel(channel.id)) - end - - MessageBus.last_ids(*message_bus_channels) - end end end diff --git a/plugins/chat/app/services/chat_publisher.rb b/plugins/chat/app/services/chat_publisher.rb index 5a1027d04d4..6dff51e6566 100644 --- a/plugins/chat/app/services/chat_publisher.rb +++ b/plugins/chat/app/services/chat_publisher.rb @@ -20,6 +20,7 @@ module ChatPublisher MessageBus.publish( self.new_messages_message_bus_channel(chat_channel.id), { + channel_id: chat_channel.id, message_id: chat_message.id, user_id: chat_message.user.id, username: chat_message.user.username, @@ -145,7 +146,7 @@ module ChatPublisher def self.publish_new_mention(user_id, chat_channel_id, chat_message_id) MessageBus.publish( self.new_mentions_message_bus_channel(chat_channel_id), - { message_id: chat_message_id }.as_json, + { message_id: chat_message_id, channel_id: chat_channel_id }.as_json, user_ids: [user_id], ) end @@ -154,13 +155,20 @@ module ChatPublisher def self.publish_new_channel(chat_channel, users) users.each do |user| + # FIXME: This could generate a lot of queries depending on the amount of users + membership = chat_channel.membership_for(user) + + # TODO: this event is problematic as some code will update the membership before calling it + # and other code will update it after calling it + # it means frontend must handle logic for both cases serialized_channel = ChatChannelSerializer.new( chat_channel, scope: Guardian.new(user), # We need a guardian here for direct messages - root: :chat_channel, - membership: chat_channel.membership_for(user), + root: :channel, + membership: membership, ).as_json + MessageBus.publish(NEW_CHANNEL_MESSAGE_BUS_CHANNEL, serialized_channel, user_ids: [user.id]) end end @@ -179,9 +187,10 @@ module ChatPublisher type: :mention_warning, chat_message_id: chat_message.id, cannot_see: cannot_chat_users.map { |u| { username: u.username, id: u.id } }.as_json, - without_membership: without_membership.map { |u| { username: u.username, id: u.id } }.as_json, + without_membership: + without_membership.map { |u| { username: u.username, id: u.id } }.as_json, groups_with_too_many_members: too_many_members.map(&:name).as_json, - group_mentions_disabled: mentions_disabled.map(&:name).as_json + group_mentions_disabled: mentions_disabled.map(&:name).as_json, }, user_ids: [user_id], ) diff --git a/plugins/chat/assets/javascripts/discourse/adapters/chat-message.js b/plugins/chat/assets/javascripts/discourse/adapters/chat-message.js index 51f4b36f251..400f1d389be 100644 --- a/plugins/chat/assets/javascripts/discourse/adapters/chat-message.js +++ b/plugins/chat/assets/javascripts/discourse/adapters/chat-message.js @@ -1,6 +1,8 @@ import RESTAdapter from "discourse/adapters/rest"; export default class ChatMessage extends RESTAdapter { + jsonMode = true; + pathFor(store, type, findArgs) { if (findArgs.targetMessageId) { return `/chat/lookup/${findArgs.targetMessageId}.json?chat_channel_id=${findArgs.channelId}`; diff --git a/plugins/chat/assets/javascripts/discourse/components/channels-list.hbs b/plugins/chat/assets/javascripts/discourse/components/channels-list.hbs index 4247db6e27d..a4100a897fa 100644 --- a/plugins/chat/assets/javascripts/discourse/components/channels-list.hbs +++ b/plugins/chat/assets/javascripts/discourse/components/channels-list.hbs @@ -1,5 +1,11 @@ -{{#if (and this.showMobileDirectMessageButton this.canCreateDirectMessageChannel)}} - +{{#if + (and this.showMobileDirectMessageButton this.canCreateDirectMessageChannel) +}} + {{d-icon "plus"}} {{/if}} @@ -8,7 +14,10 @@ role="region" aria-label={{i18n "chat.aria_roles.channels_list"}} class="channels-list" - {{on "scroll" (if this.chatStateManager.isFullPageActive this.storeScrollPosition (noop))}} + {{on + "scroll" + (if this.chatStateManager.isFullPageActive this.storeScrollPosition (noop)) + }} > {{#if this.displayPublicChannels}}
@@ -26,7 +35,11 @@ {{/if}} {{i18n "chat.chat_channels"}} - + {{d-icon "pencil-alt"}}
@@ -40,7 +53,7 @@
{{else}} - {{#each this.publicChannels as |channel|}} + {{#each this.chatChannelsManager.publicMessageChannels as |channel|}} {{i18n "chat.direct_messages.title"}} - {{#if (and this.canCreateDirectMessageChannel (not this.showMobileDirectMessageButton))}} - + {{#if + (and + this.canCreateDirectMessageChannel + (not this.showMobileDirectMessageButton) + ) + }} + {{d-icon "plus"}} {{/if}} @@ -84,11 +106,8 @@ {{/if}}
- {{#each this.sortedDirectMessageChannels as |channel|}} - + {{#each this.chatChannelsManager.truncatedDirectMessageChannels as |channel|}} + {{/each}}
diff --git a/plugins/chat/assets/javascripts/discourse/components/channels-list.js b/plugins/chat/assets/javascripts/discourse/components/channels-list.js index 796db2a1b94..9cac0ff735e 100644 --- a/plugins/chat/assets/javascripts/discourse/components/channels-list.js +++ b/plugins/chat/assets/javascripts/discourse/components/channels-list.js @@ -3,18 +3,18 @@ import Component from "@ember/component"; import { action, computed } from "@ember/object"; import { schedule } from "@ember/runloop"; import { inject as service } from "@ember/service"; -import { and, empty, reads } from "@ember/object/computed"; +import { and, empty } from "@ember/object/computed"; export default class ChannelsList extends Component { @service chat; @service router; @service chatStateManager; + @service chatChannelsManager; tagName = ""; inSidebar = false; toggleSection = null; - @reads("chat.publicChannels.[]") publicChannels; - @reads("chat.directMessageChannels.[]") directMessageChannels; - @empty("publicChannels") publicChannelsEmpty; + @empty("chatChannelsManager.publicMessageChannels") + publicMessageChannelsEmpty; @and("site.mobileView", "showDirectMessageChannels") showMobileDirectMessageButton; @@ -27,11 +27,14 @@ export default class ChannelsList extends Component { return "chat.direct_messages.new"; } - @computed("canCreateDirectMessageChannel", "directMessageChannels") + @computed( + "canCreateDirectMessageChannel", + "chatChannelsManager.directMessageChannels" + ) get showDirectMessageChannels() { return ( this.canCreateDirectMessageChannel || - this.directMessageChannels?.length > 0 + this.chatChannelsManager.directMessageChannels?.length > 0 ); } @@ -39,17 +42,6 @@ export default class ChannelsList extends Component { return this.chat.userCanDirectMessage; } - @computed("directMessageChannels.@each.last_message_sent_at") - get sortedDirectMessageChannels() { - if (!this.directMessageChannels?.length) { - return []; - } - - return this.chat.truncateDirectMessageChannels( - this.chat.sortDirectMessageChannels(this.directMessageChannels) - ); - } - @computed("inSidebar") get publicChannelClasses() { return `channels-list-container public-channels ${ @@ -58,11 +50,11 @@ export default class ChannelsList extends Component { } @computed( - "publicChannelsEmpty", + "publicMessageChannelsEmpty", "currentUser.{staff,has_joinable_public_channels}" ) get displayPublicChannels() { - if (this.publicChannelsEmpty) { + if (this.publicMessageChannelsEmpty) { return ( this.currentUser?.staff || this.currentUser?.has_joinable_public_channels diff --git a/plugins/chat/assets/javascripts/discourse/components/chat-browse-view.hbs b/plugins/chat/assets/javascripts/discourse/components/chat-browse-view.hbs index 04b7b31cf6c..5098f17b76e 100644 --- a/plugins/chat/assets/javascripts/discourse/components/chat-browse-view.hbs +++ b/plugins/chat/assets/javascripts/discourse/components/chat-browse-view.hbs @@ -1,12 +1,16 @@ {{#if this.chatProgressBarContainer}} {{#in-element this.chatProgressBarContainer}} - + {{/in-element}} {{/if}}
{{#if this.site.mobileView}} - + {{d-icon "chevron-left"}} {{/if}} @@ -17,7 +21,10 @@ {{/if}} @@ -49,7 +56,7 @@ />
- {{#if (and (not this.channels.length) (not this.isLoading))}} + {{#if (and (not this.channelsCollection.length) (not this.channelsCollection.loading))}}
{{i18n "chat.empty_state.title"}}
@@ -60,16 +67,16 @@
- {{else if this.channels.length}} + {{else if this.channelsCollection.length}}
- {{#each this.channels as |channel|}} + {{#each this.channelsCollection as |channel|}} {{/each}}
- {{#unless this.isLoading}} + {{#unless this.channelsCollection.loading}} {{/unless}}
diff --git a/plugins/chat/assets/javascripts/discourse/components/chat-browse-view.js b/plugins/chat/assets/javascripts/discourse/components/chat-browse-view.js index a9d66616cdf..39ea226ef68 100644 --- a/plugins/chat/assets/javascripts/discourse/components/chat-browse-view.js +++ b/plugins/chat/assets/javascripts/discourse/components/chat-browse-view.js @@ -1,62 +1,28 @@ import { INPUT_DELAY } from "discourse-common/config/environment"; import Component from "@ember/component"; import { action, computed } from "@ember/object"; -import { tracked } from "@glimmer/tracking"; import { schedule } from "@ember/runloop"; import { inject as service } from "@ember/service"; -import ChatApi from "discourse/plugins/chat/discourse/lib/chat-api"; import discourseDebounce from "discourse-common/lib/debounce"; -import { bind } from "discourse-common/utils/decorators"; import showModal from "discourse/lib/show-modal"; const TABS = ["all", "open", "closed", "archived"]; -const PER_PAGE = 20; export default class ChatBrowseView extends Component { - @service router; - @tracked isLoading = false; - @tracked channels = []; + @service chatApi; tagName = ""; - offset = 0; - canLoadMore = true; - didReceiveAttrs() { this._super(...arguments); - this.channels = []; - this.canLoadMore = true; - this.offset = 0; - this.fetchChannels(); - } - - async fetchChannels(params) { - if (this.isLoading || !this.canLoadMore) { - return; + if (!this.channelsCollection) { + this.set("channelsCollection", this.chatApi.channels()); } - this.isLoading = true; - - try { - const results = await ChatApi.chatChannels({ - limit: PER_PAGE, - offset: this.offset, - status: this.status, - filter: this.filter, - ...params, - }); - - if (results.length) { - this.channels.pushObjects(results); - } - - if (results.length < PER_PAGE) { - this.canLoadMore = false; - } - } finally { - this.offset = this.offset + PER_PAGE; - this.isLoading = false; - } + this.channelsCollection.load({ + filter: this.filter, + status: this.status, + }); } @computed("siteSettings.chat_allow_archiving_channels") @@ -74,19 +40,20 @@ export default class ChatBrowseView extends Component { @action onScroll() { - if (this.isLoading) { - return; - } - - discourseDebounce(this, this.fetchChannels, INPUT_DELAY); + discourseDebounce( + this, + this.channelsCollection.loadMore, + { filter: this.filter, status: this.status }, + INPUT_DELAY + ); } @action debouncedFiltering(event) { discourseDebounce( this, - this.filterChannels, - event.target.value, + this.channelsCollection.load, + { filter: event.target.value, status: this.status }, INPUT_DELAY ); } @@ -100,14 +67,4 @@ export default class ChatBrowseView extends Component { focusFilterInput(input) { schedule("afterRender", () => input?.focus()); } - - @bind - filterChannels(filter) { - this.canLoadMore = true; - this.filter = filter; - this.channels = []; - this.offset = 0; - - this.fetchChannels(); - } } diff --git a/plugins/chat/assets/javascripts/discourse/components/chat-channel-about-view.hbs b/plugins/chat/assets/javascripts/discourse/components/chat-channel-about-view.hbs index 323bc228731..889ec62b31f 100644 --- a/plugins/chat/assets/javascripts/discourse/components/chat-channel-about-view.hbs +++ b/plugins/chat/assets/javascripts/discourse/components/chat-channel-about-view.hbs @@ -59,7 +59,6 @@
diff --git a/plugins/chat/assets/javascripts/discourse/components/chat-channel-about-view.js b/plugins/chat/assets/javascripts/discourse/components/chat-channel-about-view.js index 3b4d038645d..0537ff51bd2 100644 --- a/plugins/chat/assets/javascripts/discourse/components/chat-channel-about-view.js +++ b/plugins/chat/assets/javascripts/discourse/components/chat-channel-about-view.js @@ -1,5 +1,4 @@ import Component from "@ember/component"; -import { action } from "@ember/object"; import { inject as service } from "@ember/service"; export default class ChatChannelAboutView extends Component { @@ -9,11 +8,4 @@ export default class ChatChannelAboutView extends Component { onEditChatChannelTitle = null; onEditChatChannelDescription = null; isLoading = false; - - @action - afterMembershipToggle() { - this.chat.forceRefreshChannels().then(() => { - this.chat.openChannel(this.channel); - }); - } } diff --git a/plugins/chat/assets/javascripts/discourse/components/chat-channel-archive-modal-inner.js b/plugins/chat/assets/javascripts/discourse/components/chat-channel-archive-modal-inner.js index 2569653fac3..57a800b4127 100644 --- a/plugins/chat/assets/javascripts/discourse/components/chat-channel-archive-modal-inner.js +++ b/plugins/chat/assets/javascripts/discourse/components/chat-channel-archive-modal-inner.js @@ -5,7 +5,6 @@ import { isEmpty } from "@ember/utils"; import discourseComputed from "discourse-common/utils/decorators"; import { action } from "@ember/object"; import { equal } from "@ember/object/computed"; -import { ajax } from "discourse/lib/ajax"; import { inject as service } from "@ember/service"; import { popupAjaxError } from "discourse/lib/ajax-error"; import { @@ -14,13 +13,15 @@ import { } from "discourse/plugins/chat/discourse/components/chat-to-topic-selector"; import { CHANNEL_STATUSES } from "discourse/plugins/chat/discourse/models/chat-channel"; import { htmlSafe } from "@ember/template"; +import ModalFunctionality from "discourse/mixins/modal-functionality"; -export default Component.extend({ +export default Component.extend(ModalFunctionality, { chat: service(), + chatApi: service(), tagName: "", chatChannel: null, - selection: "newTopic", + selection: NEW_TOPIC_SELECTION, newTopic: equal("selection", NEW_TOPIC_SELECTION), existingTopic: equal("selection", EXISTING_TOPIC_SELECTION), @@ -34,18 +35,12 @@ export default Component.extend({ @action archiveChannel() { this.set("saving", true); - return ajax({ - url: `/chat/chat_channels/${this.chatChannel.id}/archive.json`, - type: "PUT", - data: this._data(), - }) - .then(() => { - this.appEvents.trigger("modal-body:flash", { - text: I18n.t("chat.channel_archive.process_started"), - messageClass: "success", - }); - this.chatChannel.set("status", CHANNEL_STATUSES.archived); + return this.chatApi + .createChannelArchive(this.chatChannel.id, this._data()) + .then((result) => { + this.flash(I18n.t("chat.channel_archive.process_started"), "success"); + result.target.status = CHANNEL_STATUSES.archived; discourseLater(() => { this.closeModal(); @@ -58,7 +53,6 @@ export default Component.extend({ _data() { const data = { type: this.selection, - chat_channel_id: this.chatChannel.id, }; if (this.newTopic) { data.title = this.topicTitle; diff --git a/plugins/chat/assets/javascripts/discourse/components/chat-channel-archive-status.js b/plugins/chat/assets/javascripts/discourse/components/chat-channel-archive-status.js index e8f20834527..7f76393cdcc 100644 --- a/plugins/chat/assets/javascripts/discourse/components/chat-channel-archive-status.js +++ b/plugins/chat/assets/javascripts/discourse/components/chat-channel-archive-status.js @@ -2,14 +2,15 @@ import Component from "@ember/component"; import { htmlSafe } from "@ember/template"; import I18n from "I18n"; import { popupAjaxError } from "discourse/lib/ajax-error"; -import { ajax } from "discourse/lib/ajax"; import getURL from "discourse-common/lib/get-url"; import { action } from "@ember/object"; import discourseComputed, { bind } from "discourse-common/utils/decorators"; +import { inject as service } from "@ember/service"; export default Component.extend({ channel: null, tagName: "", + chatApi: service(), @discourseComputed( "channel.status", @@ -43,26 +44,32 @@ export default Component.extend({ @action retryArchive() { - return ajax({ - url: `/chat/chat_channels/${this.channel.id}/retry_archive.json`, - type: "PUT", - }) - .then(() => { - this.channel.set("archive_failed", false); - }) + return this.chatApi + .createChannelArchive(this.channel.id) .catch(popupAjaxError); }, didInsertElement() { this._super(...arguments); + if (this.currentUser.admin) { - this.messageBus.subscribe("/chat/channel-archive-status", this.onMessage); + this.messageBus.subscribe( + "/chat/channel-archive-status", + this.onMessage, + this.channel.meta.message_bus_last_ids.archive_status + ); } }, willDestroyElement() { this._super(...arguments); - this.messageBus.unsubscribe("/chat/channel-archive-status", this.onMessage); + + if (this.currentUser.admin) { + this.messageBus.unsubscribe( + "/chat/channel-archive-status", + this.onMessage + ); + } }, _getTopicUrl() { diff --git a/plugins/chat/assets/javascripts/discourse/components/chat-channel-card.hbs b/plugins/chat/assets/javascripts/discourse/components/chat-channel-card.hbs index 411dc94ea38..beb29b614df 100644 --- a/plugins/chat/assets/javascripts/discourse/components/chat-channel-card.hbs +++ b/plugins/chat/assets/javascripts/discourse/components/chat-channel-card.hbs @@ -1,33 +1,34 @@ -{{#if this.channel}} +{{#if @channel}}
- {{replace-emoji this.channel.escapedTitle}} + {{replace-emoji @channel.escapedTitle}} - {{#if this.channel.chatable.read_restricted}} + {{#if @channel.chatable.read_restricted}} {{d-icon "lock" class="chat-channel-card__read-restricted"}} {{/if}}
- {{#if this.channel.current_user_membership.muted}} + {{#if @channel.currentUserMembership.muted}} @@ -47,32 +48,30 @@
- {{#if this.channel.description}} + {{#if @channel.description}}
- {{replace-emoji this.channel.escapedDescription}} + {{replace-emoji @channel.escapedDescription}}
{{/if}}
- {{#if this.channel.isFollowing}} + {{#if @channel.isFollowing}}
{{i18n "chat.joined"}}
- {{else if this.channel.isJoinable}} + {{else if @channel.isJoinable}} {{/if}} - {{#if (gt this.channel.membershipsCount 0)}} + {{#if (gt @channel.membershipsCount 0)}} {{i18n "chat.channel.memberships_count" - count=this.channel.membershipsCount + count=@channel.membershipsCount }} {{/if}} diff --git a/plugins/chat/assets/javascripts/discourse/components/chat-channel-card.js b/plugins/chat/assets/javascripts/discourse/components/chat-channel-card.js index 3392fe9f059..36a8f64e98b 100644 --- a/plugins/chat/assets/javascripts/discourse/components/chat-channel-card.js +++ b/plugins/chat/assets/javascripts/discourse/components/chat-channel-card.js @@ -1,13 +1,6 @@ -import Component from "@ember/component"; -import { action } from "@ember/object"; +import Component from "@glimmer/component"; import { inject as service } from "@ember/service"; export default class ChatChannelCard extends Component { @service chat; - tagName = ""; - - @action - afterMembershipToggle() { - this.chat.forceRefreshChannels(); - } } diff --git a/plugins/chat/assets/javascripts/discourse/components/chat-channel-delete-modal-inner.js b/plugins/chat/assets/javascripts/discourse/components/chat-channel-delete-modal-inner.js index 3f38523f186..4943ae1e34b 100644 --- a/plugins/chat/assets/javascripts/discourse/components/chat-channel-delete-modal-inner.js +++ b/plugins/chat/assets/javascripts/discourse/components/chat-channel-delete-modal-inner.js @@ -3,14 +3,15 @@ import { isEmpty } from "@ember/utils"; import I18n from "I18n"; import discourseComputed from "discourse-common/utils/decorators"; import { action } from "@ember/object"; -import { ajax } from "discourse/lib/ajax"; import { inject as service } from "@ember/service"; import { popupAjaxError } from "discourse/lib/ajax-error"; import discourseLater from "discourse-common/lib/later"; import { htmlSafe } from "@ember/template"; +import ModalFunctionality from "discourse/mixins/modal-functionality"; -export default Component.extend({ +export default Component.extend(ModalFunctionality, { chat: service(), + chatApi: service(), router: service(), tagName: "", chatChannel: null, @@ -37,16 +38,14 @@ export default Component.extend({ @action deleteChannel() { this.set("deleting", true); - return ajax(`/chat/chat_channels/${this.chatChannel.id}.json`, { - method: "DELETE", - data: { channel_name_confirmation: this.channelNameConfirmation }, - }) + + return this.chatApi + .destroyChannel(this.chatChannel.id, { + name_confirmation: this.channelNameConfirmation, + }) .then(() => { this.set("confirmed", true); - this.appEvents.trigger("modal-body:flash", { - text: I18n.t("chat.channel_delete.process_started"), - messageClass: "success", - }); + this.flash(I18n.t("chat.channel_delete.process_started"), "success"); discourseLater(() => { this.closeModal(); diff --git a/plugins/chat/assets/javascripts/discourse/components/chat-channel-members-view.hbs b/plugins/chat/assets/javascripts/discourse/components/chat-channel-members-view.hbs index 90834e6051b..5acfc7a5267 100644 --- a/plugins/chat/assets/javascripts/discourse/components/chat-channel-members-view.hbs +++ b/plugins/chat/assets/javascripts/discourse/components/chat-channel-members-view.hbs @@ -1,6 +1,6 @@ {{#if this.chatProgressBarContainer}} {{#in-element this.chatProgressBarContainer}} - + {{/in-element}} {{/if}} @@ -22,15 +22,15 @@ >
- {{#each this.members as |member|}} + {{#each this.members as |membership|}} - - + + {{else}} {{#unless this.isFetchingMembers}} diff --git a/plugins/chat/assets/javascripts/discourse/components/chat-channel-members-view.js b/plugins/chat/assets/javascripts/discourse/components/chat-channel-members-view.js index 49907dbd68c..dea4cad847d 100644 --- a/plugins/chat/assets/javascripts/discourse/components/chat-channel-members-view.js +++ b/plugins/chat/assets/javascripts/discourse/components/chat-channel-members-view.js @@ -1,24 +1,20 @@ -import { isEmpty } from "@ember/utils"; import { INPUT_DELAY } from "discourse-common/config/environment"; import Component from "@ember/component"; import { action } from "@ember/object"; import { schedule } from "@ember/runloop"; -import ChatApi from "discourse/plugins/chat/discourse/lib/chat-api"; import discourseDebounce from "discourse-common/lib/debounce"; - -const LIMIT = 50; +import { inject as service } from "@ember/service"; export default class ChatChannelMembersView extends Component { + @service chatApi; + tagName = ""; channel = null; - members = null; isSearchFocused = false; - isFetchingMembers = false; onlineUsers = null; - offset = 0; filter = null; inputSelector = "channel-members-view__search-input"; - canLoadMore = true; + members = null; didInsertElement() { this._super(...arguments); @@ -28,14 +24,15 @@ export default class ChatChannelMembersView extends Component { } this._focusSearch(); - this.set("members", []); - this.fetchMembers(); + this.set("members", this.chatApi.listChannelMemberships(this.channel.id)); + this.members.load(); this.appEvents.on("chat:refresh-channel-members", this, "onFilterMembers"); } willDestroyElement() { this._super(...arguments); + this.appEvents.off("chat:refresh-channel-members", this, "onFilterMembers"); } @@ -46,59 +43,18 @@ export default class ChatChannelMembersView extends Component { @action onFilterMembers(username) { this.set("filter", username); - this.set("offset", 0); - this.set("canLoadMore", true); discourseDebounce( this, - this.fetchMembers, - this.filter, - this.offset, + this.members.load, + { username: this.filter }, INPUT_DELAY ); } @action loadMore() { - if (!this.canLoadMore) { - return; - } - - discourseDebounce( - this, - this.fetchMembers, - this.filter, - this.offset, - INPUT_DELAY - ); - } - - fetchMembersHandler(id, params = {}) { - return ChatApi.chatChannelMemberships(id, params); - } - - fetchMembers(filter = null, offset = 0) { - this.set("isFetchingMembers", true); - - return this.fetchMembersHandler(this.channel.id, { - username: filter, - offset, - }) - .then((response) => { - if (this.offset === 0) { - this.set("members", []); - } - - if (isEmpty(response)) { - this.set("canLoadMore", false); - } else { - this.set("offset", this.offset + LIMIT); - this.members.pushObjects(response); - } - }) - .finally(() => { - this.set("isFetchingMembers", false); - }); + discourseDebounce(this, this.members.loadMore, INPUT_DELAY); } _focusSearch() { diff --git a/plugins/chat/assets/javascripts/discourse/components/chat-channel-preview-card.hbs b/plugins/chat/assets/javascripts/discourse/components/chat-channel-preview-card.hbs index 2e7f8f58005..eed8d6c84c2 100644 --- a/plugins/chat/assets/javascripts/discourse/components/chat-channel-preview-card.hbs +++ b/plugins/chat/assets/javascripts/discourse/components/chat-channel-preview-card.hbs @@ -13,7 +13,6 @@ {{#if this.showJoinButton}} {{/if}} diff --git a/plugins/chat/assets/javascripts/discourse/components/chat-channel-preview-card.js b/plugins/chat/assets/javascripts/discourse/components/chat-channel-preview-card.js index 23be8f9a672..954313febe9 100644 --- a/plugins/chat/assets/javascripts/discourse/components/chat-channel-preview-card.js +++ b/plugins/chat/assets/javascripts/discourse/components/chat-channel-preview-card.js @@ -1,6 +1,6 @@ import Component from "@ember/component"; import { isEmpty } from "@ember/utils"; -import { action, computed } from "@ember/object"; +import { computed } from "@ember/object"; import { readOnly } from "@ember/object/computed"; import { inject as service } from "@ember/service"; @@ -16,11 +16,4 @@ export default class ChatChannelPreviewCard extends Component { get hasDescription() { return !isEmpty(this.channel.description); } - - @action - afterMembershipToggle() { - this.chat.forceRefreshChannels().then(() => { - this.chat.openChannel(this.channel); - }); - } } diff --git a/plugins/chat/assets/javascripts/discourse/components/chat-channel-row.hbs b/plugins/chat/assets/javascripts/discourse/components/chat-channel-row.hbs index 83776ff9f77..e77ee34205f 100644 --- a/plugins/chat/assets/javascripts/discourse/components/chat-channel-row.hbs +++ b/plugins/chat/assets/javascripts/discourse/components/chat-channel-row.hbs @@ -4,7 +4,7 @@ class={{concat-class "chat-channel-row" (if @channel.focused "focused") - (if @channel.current_user_membership.muted "muted") + (if @channel.currentUserMembership.muted "muted") (if @options.leaveButton "can-leave") (if (eq this.chat.activeChannel.id @channel.id) "active") (if this.channelHasUnread "has-unread") diff --git a/plugins/chat/assets/javascripts/discourse/components/chat-channel-row.js b/plugins/chat/assets/javascripts/discourse/components/chat-channel-row.js index e703be69381..3fe267f6f63 100644 --- a/plugins/chat/assets/javascripts/discourse/components/chat-channel-row.js +++ b/plugins/chat/assets/javascripts/discourse/components/chat-channel-row.js @@ -19,11 +19,7 @@ export default class ChatChannelRow extends Component { } get channelHasUnread() { - return ( - this.currentUser.get( - `chat_channel_tracking_state.${this.args.channel?.id}.unread_count` - ) > 0 - ); + return this.args.channel.currentUserMembership.unread_count > 0; } get #firstDirectMessageUser() { diff --git a/plugins/chat/assets/javascripts/discourse/components/chat-channel-selector-modal-inner.js b/plugins/chat/assets/javascripts/discourse/components/chat-channel-selector-modal-inner.js index bfd46d64c0e..1937ff6bdae 100644 --- a/plugins/chat/assets/javascripts/discourse/components/chat-channel-selector-modal-inner.js +++ b/plugins/chat/assets/javascripts/discourse/components/chat-channel-selector-modal-inner.js @@ -17,24 +17,24 @@ export default Component.extend({ channels: null, searchIndex: 0, loading: false, - - init() { - this._super(...arguments); - this.appEvents.on("chat-channel-selector-modal:close", this.close); - this.getInitialChannels(); - }, + chatChannelsManager: service(), didInsertElement() { this._super(...arguments); + + this.appEvents.on("chat-channel-selector-modal:close", this.close); document.addEventListener("keyup", this.onKeyUp); document .getElementById("chat-channel-selector-modal-inner") ?.addEventListener("mouseover", this.mouseover); document.getElementById("chat-channel-selector-input")?.focus(); + + this.getInitialChannels(); }, willDestroyElement() { this._super(...arguments); + this.appEvents.off("chat-channel-selector-modal:close", this.close); document.removeEventListener("keyup", this.onKeyUp); document @@ -101,16 +101,17 @@ export default Component.extend({ switchChannel(channel) { if (channel.user) { return this.fetchOrCreateChannelForUser(channel).then((response) => { - this.chat - .startTrackingChannel(ChatChannel.create(response.chat_channel)) - .then((newlyTracked) => { - this.chat.openChannel(newlyTracked); - this.close(); - }); + const newChannel = this.chatChannelsManager.store(response.channel); + return this.chatChannelsManager.follow(newChannel).then((c) => { + this.chat.openChannel(c); + this.close(); + }); }); } else { - this.chat.openChannel(channel); - this.close(); + return this.chatChannelsManager.follow(channel).then((c) => { + this.chat.openChannel(c); + this.close(); + }); } }, @@ -135,7 +136,7 @@ export default Component.extend({ searchIndex: this.searchIndex + 1, }); const thisSearchIndex = this.searchIndex; - ajax("/chat/chat_channels/search", { data: { filter } }) + ajax("/chat/api/chatables", { data: { filter } }) .then((searchModel) => { if (this.searchIndex === thisSearchIndex) { this.set("searchModel", searchModel); @@ -149,7 +150,11 @@ export default Component.extend({ } }); this.setProperties({ - channels: channels.map((channel) => ChatChannel.create(channel)), + channels: channels.map((channel) => { + return channel.user + ? ChatChannel.create(channel) + : this.chatChannelsManager.store(channel); + }), loading: false, }); this.focusFirstChannel(this.channels); @@ -160,10 +165,9 @@ export default Component.extend({ @action getInitialChannels() { - return this.chat.getChannelsWithFilter(this.filter).then((channels) => { - this.focusFirstChannel(channels); - this.set("channels", channels); - }); + const channels = this.getChannelsWithFilter(this.filter); + this.set("channels", channels); + this.focusFirstChannel(channels); }, @action @@ -178,4 +182,44 @@ export default Component.extend({ channels.forEach((c) => c.set("focused", false)); channels[0]?.set("focused", true); }, + + getChannelsWithFilter(filter, opts = { excludeActiveChannel: true }) { + let sortedChannels = this.chatChannelsManager.channels.sort((a, b) => { + return new Date(a.last_message_sent_at) > new Date(b.last_message_sent_at) + ? -1 + : 1; + }); + + const trimmedFilter = filter.trim(); + const lowerCasedFilter = filter.toLowerCase(); + const { activeChannel } = this; + + return sortedChannels.filter((channel) => { + if ( + opts.excludeActiveChannel && + activeChannel && + activeChannel.id === channel.id + ) { + return false; + } + if (!trimmedFilter.length) { + return true; + } + + if (channel.isDirectMessageChannel) { + let userFound = false; + channel.chatable.users.forEach((user) => { + if ( + user.username.toLowerCase().includes(lowerCasedFilter) || + user.name?.toLowerCase().includes(lowerCasedFilter) + ) { + return (userFound = true); + } + }); + return userFound; + } else { + return channel.title.toLowerCase().includes(lowerCasedFilter); + } + }); + }, }); diff --git a/plugins/chat/assets/javascripts/discourse/components/chat-channel-settings-view.hbs b/plugins/chat/assets/javascripts/discourse/components/chat-channel-settings-view.hbs index 5597f74a5c9..9ea17a5e4ed 100644 --- a/plugins/chat/assets/javascripts/discourse/components/chat-channel-settings-view.hbs +++ b/plugins/chat/assets/javascripts/discourse/components/chat-channel-settings-view.hbs @@ -3,13 +3,13 @@
- {{#unless this.channel.current_user_membership.muted}} + {{#unless this.channel.currentUserMembership.muted}}
{{i18n "chat.settings.mobile_notification_level"}}
{ - this.channel.current_user_membership.setProperties({ - muted: membership.muted, - desktop_notification_level: membership.desktop_notification_level, - mobile_notification_level: membership.mobile_notification_level, + return this.chatApi + .updateCurrentUserChatChannelNotificationsSettings( + this.channel.id, + settings + ) + .then((result) => { + [ + "muted", + "desktop_notification_level", + "mobile_notification_level", + ].forEach((property) => { + if ( + result.membership[property] !== + this.channel.currentUserMembership[property] + ) { + this.channel.currentUserMembership[property] = + result.membership[property]; + } + }); }); - }); } @action @@ -155,9 +165,10 @@ export default class ChatChannelSettingsView extends Component { const payload = {}; payload[property] = value; - return ChatApi.modifyChatChannel(channel.id, payload) - .then((updatedChannel) => { - channel.set(property, updatedChannel[property]); + return this.chatApi + .updateChannel(channel.id, payload) + .then((result) => { + channel.set(property, result.channel[property]); }) .catch((event) => { if (event.jqXHR?.responseJSON?.errors) { diff --git a/plugins/chat/assets/javascripts/discourse/components/chat-channel-toggle-view.js b/plugins/chat/assets/javascripts/discourse/components/chat-channel-toggle-view.js index 87c0cfd9b21..5ed0b2dbdf5 100644 --- a/plugins/chat/assets/javascripts/discourse/components/chat-channel-toggle-view.js +++ b/plugins/chat/assets/javascripts/discourse/components/chat-channel-toggle-view.js @@ -3,12 +3,12 @@ import { htmlSafe } from "@ember/template"; import { CHANNEL_STATUSES } from "discourse/plugins/chat/discourse/models/chat-channel"; import I18n from "I18n"; import { action, computed } from "@ember/object"; -import { ajax } from "discourse/lib/ajax"; import { inject as service } from "@ember/service"; import { popupAjaxError } from "discourse/lib/ajax-error"; export default class ChatChannelToggleView extends Component { @service chat; + @service chatApi; @service router; tagName = ""; channel = null; @@ -47,16 +47,11 @@ export default class ChatChannelToggleView extends Component { ? CHANNEL_STATUSES.open : CHANNEL_STATUSES.closed; - return ajax(`/chat/chat_channels/${this.channel.id}/change_status.json`, { - method: "PUT", - data: { status }, - }) - .then(() => { - this.channel.set("status", status); - }) - .catch(popupAjaxError) + return this.chatApi + .updateChannelStatus(this.channel.id, status) .finally(() => { this.onStatusChange?.(this.channel); - }); + }) + .catch(popupAjaxError); } } diff --git a/plugins/chat/assets/javascripts/discourse/components/chat-channel-unread-indicator.hbs b/plugins/chat/assets/javascripts/discourse/components/chat-channel-unread-indicator.hbs index 4a53e0296a9..478d225337f 100644 --- a/plugins/chat/assets/javascripts/discourse/components/chat-channel-unread-indicator.hbs +++ b/plugins/chat/assets/javascripts/discourse/components/chat-channel-unread-indicator.hbs @@ -1,5 +1,16 @@ -{{#if this.hasUnread}} -
-
{{this.unreadCount}}
+{{#if (gt @channel.currentUserMembership.unread_count 0)}} +
+
{{@channel.currentUserMembership.unread_count}}
{{/if}} diff --git a/plugins/chat/assets/javascripts/discourse/components/chat-channel-unread-indicator.js b/plugins/chat/assets/javascripts/discourse/components/chat-channel-unread-indicator.js deleted file mode 100644 index f24cd64a31b..00000000000 --- a/plugins/chat/assets/javascripts/discourse/components/chat-channel-unread-indicator.js +++ /dev/null @@ -1,46 +0,0 @@ -import discourseComputed from "discourse-common/utils/decorators"; -import Component from "@ember/component"; -import { equal, gt } from "@ember/object/computed"; -import { CHATABLE_TYPES } from "discourse/plugins/chat/discourse/models/chat-channel"; - -export default Component.extend({ - tagName: "", - channel: null, - - isDirectMessage: equal( - "channel.chatable_type", - CHATABLE_TYPES.directMessageChannel - ), - - hasUnread: gt("unreadCount", 0), - - @discourseComputed( - "currentUser.chat_channel_tracking_state.@each.{unread_count,unread_mentions}", - "channel.id" - ) - channelTrackingState(state, channelId) { - return state?.[channelId]; - }, - - @discourseComputed( - "channelTrackingState.unread_mentions", - "channel", - "isDirectMessage" - ) - isUrgent(unreadMentions, channel, isDirectMessage) { - if (!channel) { - return; - } - - return isDirectMessage || unreadMentions > 0; - }, - - @discourseComputed("channelTrackingState.unread_count", "channel") - unreadCount(unreadCount, channel) { - if (!channel) { - return; - } - - return unreadCount || 0; - }, -}); diff --git a/plugins/chat/assets/javascripts/discourse/components/chat-draft-channel-screen.js b/plugins/chat/assets/javascripts/discourse/components/chat-draft-channel-screen.js index e374564c35b..f8f00d3e920 100644 --- a/plugins/chat/assets/javascripts/discourse/components/chat-draft-channel-screen.js +++ b/plugins/chat/assets/javascripts/discourse/components/chat-draft-channel-screen.js @@ -34,7 +34,7 @@ export default class ChatDraftChannelScreen extends Component { this.set( "previewedChannel", ChatChannel.create( - Object.assign({}, response.chat_channel, { isDraft: true }) + Object.assign({}, response.channel, { isDraft: true }) ) ); }) diff --git a/plugins/chat/assets/javascripts/discourse/components/chat-drawer.js b/plugins/chat/assets/javascripts/discourse/components/chat-drawer.js index 3052f153983..35dab11417d 100644 --- a/plugins/chat/assets/javascripts/discourse/components/chat-drawer.js +++ b/plugins/chat/assets/javascripts/discourse/components/chat-drawer.js @@ -17,6 +17,7 @@ export default Component.extend({ draftChannelView: equal("view", DRAFT_CHANNEL_VIEW), chat: service(), router: service(), + chatChannelsManager: service(), chatStateManager: service(), loading: false, showClose: true, // TODO - false when on same topic @@ -40,7 +41,6 @@ export default Component.extend({ this, "openChannelAtMessage" ); - this.appEvents.on("chat:refresh-channels", this, "refreshChannels"); this.appEvents.on("composer:closed", this, "_checkSize"); this.appEvents.on("composer:opened", this, "_checkSize"); this.appEvents.on("composer:resized", this, "_checkSize"); @@ -68,7 +68,6 @@ export default Component.extend({ this, "openChannelAtMessage" ); - this.appEvents.off("chat:refresh-channels", this, "refreshChannels"); this.appEvents.off("composer:closed", this, "_checkSize"); this.appEvents.off("composer:opened", this, "_checkSize"); this.appEvents.off("composer:resized", this, "_checkSize"); @@ -198,12 +197,9 @@ export default Component.extend({ } }, - @discourseComputed( - "chat.activeChannel", - "currentUser.chat_channel_tracking_state" - ) - unreadCount(activeChannel, trackingState) { - return trackingState[activeChannel.id]?.unread_count || 0; + @discourseComputed("chat.activeChannel.currentUserMembership.unread_count") + unreadCount(count) { + return count || 0; }, @action @@ -218,7 +214,6 @@ export default Component.extend({ switch (route.name) { case "chat": this.set("view", LIST_VIEW); - this.refreshChannels(); this.appEvents.trigger("chat:float-toggled", false); return; case "chat.draft-channel": @@ -226,8 +221,8 @@ export default Component.extend({ this.appEvents.trigger("chat:float-toggled", false); return; case "chat.channel": - return this.chat - .getChannelBy("id", route.params.channelId) + return this.chatChannelsManager + .find(route.params.channelId) .then((channel) => { this.chat.set("messageId", route.queryParams.messageId); this.chat.setActiveChannel(channel); @@ -262,32 +257,6 @@ export default Component.extend({ this.appEvents.trigger("chat:float-toggled", true); }, - @action - refreshChannels() { - if (this.view === LIST_VIEW) { - this.fetchChannels(); - } - }, - - @action - fetchChannels() { - this.set("loading", true); - - this.chat.getChannels().then(() => { - if (this.isDestroying || this.isDestroyed) { - return; - } - - this.setProperties({ - loading: false, - view: LIST_VIEW, - }); - - this.chatStateManager.didExpandDrawer(); - this.chat.setActiveChannel(null); - }); - }, - @action switchChannel(channel) { // we need next here to ensure we correctly let the time for routes transitions diff --git a/plugins/chat/assets/javascripts/discourse/components/chat-header-icon-unread-indicator.hbs b/plugins/chat/assets/javascripts/discourse/components/chat-header-icon-unread-indicator.hbs new file mode 100644 index 00000000000..c8af41c7876 --- /dev/null +++ b/plugins/chat/assets/javascripts/discourse/components/chat-header-icon-unread-indicator.hbs @@ -0,0 +1,9 @@ +{{#if (gt this.chatChannelsManager.unreadUrgentCount 0)}} +
+
+
{{this.chatChannelsManager.unreadUrgentCount}}
+
+
+{{else if (gt this.chatChannelsManager.unreadCount 0)}} +
+{{/if}} diff --git a/plugins/chat/assets/javascripts/discourse/components/chat-header-icon-unread-indicator.js b/plugins/chat/assets/javascripts/discourse/components/chat-header-icon-unread-indicator.js new file mode 100644 index 00000000000..df39429e489 --- /dev/null +++ b/plugins/chat/assets/javascripts/discourse/components/chat-header-icon-unread-indicator.js @@ -0,0 +1,6 @@ +import { inject as service } from "@ember/service"; +import Component from "@glimmer/component"; + +export default class ChatHeaderIconUnreadIndicator extends Component { + @service chatChannelsManager; +} diff --git a/plugins/chat/assets/javascripts/discourse/components/chat-header-icon.hbs b/plugins/chat/assets/javascripts/discourse/components/chat-header-icon.hbs new file mode 100644 index 00000000000..4eb8b8ad9d8 --- /dev/null +++ b/plugins/chat/assets/javascripts/discourse/components/chat-header-icon.hbs @@ -0,0 +1,21 @@ +{{#if (and this.chatStateManager.isFullPageActive this.site.desktopView)}} + + {{d-icon "comment"}} + + {{#unless this.currentUserInDnD}} + + {{/unless}} + +{{else}} + + {{d-icon "comment"}} + + {{#unless this.currentUserInDnD}} + + {{/unless}} + +{{/if}} diff --git a/plugins/chat/assets/javascripts/discourse/components/chat-header-icon.js b/plugins/chat/assets/javascripts/discourse/components/chat-header-icon.js new file mode 100644 index 00000000000..9753f8ab005 --- /dev/null +++ b/plugins/chat/assets/javascripts/discourse/components/chat-header-icon.js @@ -0,0 +1,31 @@ +import { inject as service } from "@ember/service"; +import Component from "@glimmer/component"; + +export default class ChatHeaderIcon extends Component { + @service currentUser; + @service site; + @service chatStateManager; + + get currentUserInDnD() { + return this.currentUser.isInDoNotDisturb(); + } + + get href() { + if (this.chatStateManager.isFullPageActive && this.site.mobileView) { + return "/chat"; + } + + if (this.chatStateManager.isDrawerActive) { + return "/chat"; + } else { + return this.chatStateManager.lastKnownChatURL || "/chat"; + } + } + + get isActive() { + return ( + this.chatStateManager.isFullPageActive || + this.chatStateManager.isDrawerActive + ); + } +} diff --git a/plugins/chat/assets/javascripts/discourse/components/chat-live-pane.hbs b/plugins/chat/assets/javascripts/discourse/components/chat-live-pane.hbs index 2e1696714df..66d3a0e149c 100644 --- a/plugins/chat/assets/javascripts/discourse/components/chat-live-pane.hbs +++ b/plugins/chat/assets/javascripts/discourse/components/chat-live-pane.hbs @@ -21,6 +21,7 @@ {{/if}}
+
{{/if}} diff --git a/plugins/chat/assets/javascripts/discourse/components/chat-live-pane.js b/plugins/chat/assets/javascripts/discourse/components/chat-live-pane.js index 4104ba0ba9f..320017945f5 100644 --- a/plugins/chat/assets/javascripts/discourse/components/chat-live-pane.js +++ b/plugins/chat/assets/javascripts/discourse/components/chat-live-pane.js @@ -1,7 +1,5 @@ import isElementInViewport from "discourse/lib/is-element-in-viewport"; -import ChatApi from "discourse/plugins/chat/discourse/lib/chat-api"; import { cloneJSON } from "discourse-common/lib/object"; -import ChatChannel from "discourse/plugins/chat/discourse/models/chat-channel"; import ChatMessage from "discourse/plugins/chat/discourse/models/chat-message"; import Component from "@ember/component"; import discourseComputed, { @@ -83,10 +81,12 @@ export default Component.extend({ _mentionWarningsSeen: null, // Hash chat: service(), + chatChannelsManager: service(), router: service(), chatEmojiPickerManager: service(), chatComposerPresenceManager: service(), chatStateManager: service(), + chatApi: service(), getCachedChannelDetails: null, clearCachedChannelDetails: null, @@ -546,8 +546,7 @@ export default Component.extend({ }, _getLastReadId() { - return this.currentUser?.chat_channel_tracking_state?.[this.chatChannel.id] - ?.chat_message_id; + return this.chatChannel.currentUserMembership.chat_message_id; }, _markLastReadMessage(opts = { reRender: false }) { @@ -563,7 +562,6 @@ export default Component.extend({ return; } - this.set("lastSendReadMessageId", lastReadId); const indexOfLastReadMessage = this.messages.findIndex((m) => m.id === lastReadId) || 0; let newestUnreadMessage = this.messages[indexOfLastReadMessage + 1]; @@ -1009,7 +1007,8 @@ export default Component.extend({ // Start ajax request but don't return here, we want to stage the message instantly when all messages are loaded. // Otherwise, we'll fetch latest and scroll to the one we just created. // Return a resolved promise below. - const msgCreationPromise = ChatApi.sendMessage(this.chatChannel.id, data) + const msgCreationPromise = this.chatApi + .sendMessage(this.chatChannel.id, data) .catch((error) => { this._onSendError(data.staged_id, error); }) @@ -1047,33 +1046,25 @@ export default Component.extend({ }, async _upsertChannelWithMessage(channel, message, uploads) { - let promise; + let promise = Promise.resolve(channel); if (channel.isDirectMessageChannel || channel.isDraft) { promise = this.chat.upsertDmChannelForUsernames( channel.chatable.users.mapBy("username") ); - } else { - promise = ChatApi.loading(channel.id).then(() => channel); } - return promise - .then((c) => { - c.current_user_membership.set("following", true); - return this.chat.startTrackingChannel(c); + return promise.then((c) => + ajax(`/chat/${c.id}.json`, { + type: "POST", + data: { + message, + upload_ids: (uploads || []).mapBy("id"), + }, + }).then(() => { + this.onSwitchChannel(c); }) - .then((c) => - ajax(`/chat/${c.id}.json`, { - type: "POST", - data: { - message, - upload_ids: (uploads || []).mapBy("id"), - }, - }).then(() => { - this.chat.forceRefreshChannels(); - this.onSwitchChannel(ChatChannel.create(c)); - }) - ); + ); }, _onSendError(stagedId, error) { @@ -1103,7 +1094,8 @@ export default Component.extend({ staged_id: stagedMessage.stagedId, }; - ChatApi.sendMessage(this.chatChannel.id, data) + this.chatApi + .sendMessage(this.chatChannel.id, data) .catch((error) => { this._onSendError(data.staged_id, error); }) diff --git a/plugins/chat/assets/javascripts/discourse/components/chat-message-move-to-channel-modal-inner.hbs b/plugins/chat/assets/javascripts/discourse/components/chat-message-move-to-channel-modal-inner.hbs index 2c7ba4de58e..48c9c87792d 100644 --- a/plugins/chat/assets/javascripts/discourse/components/chat-message-move-to-channel-modal-inner.hbs +++ b/plugins/chat/assets/javascripts/discourse/components/chat-message-move-to-channel-modal-inner.hbs @@ -6,11 +6,23 @@

{{this.instructionsText}}

- + diff --git a/plugins/chat/assets/javascripts/discourse/components/chat-message-move-to-channel-modal-inner.js b/plugins/chat/assets/javascripts/discourse/components/chat-message-move-to-channel-modal-inner.js index 3045b7f4ed7..7d404060d25 100644 --- a/plugins/chat/assets/javascripts/discourse/components/chat-message-move-to-channel-modal-inner.js +++ b/plugins/chat/assets/javascripts/discourse/components/chat-message-move-to-channel-modal-inner.js @@ -3,14 +3,15 @@ import I18n from "I18n"; import { reads } from "@ember/object/computed"; import { isBlank } from "@ember/utils"; import { action, computed } from "@ember/object"; -import { ajax } from "discourse/lib/ajax"; import { inject as service } from "@ember/service"; import { popupAjaxError } from "discourse/lib/ajax-error"; import { htmlSafe } from "@ember/template"; export default class MoveToChannelModalInner extends Component { @service chat; + @service chatApi; @service router; + @service chatChannelsManager; tagName = ""; sourceChannel = null; destinationChannelId = null; @@ -23,31 +24,25 @@ export default class MoveToChannelModalInner extends Component { return isBlank(this.destinationChannelId); } - @computed("chat.publicChannels.[]") + @computed("chatChannelsManager.publicMessageChannels.[]") get availableChannels() { - return this.chat.publicChannels.rejectBy("id", this.sourceChannel.id); + return this.chatChannelsManager.publicMessageChannels.rejectBy( + "id", + this.sourceChannel.id + ); } @action moveMessages() { - return ajax( - `/chat/${this.sourceChannel.id}/move_messages_to_channel.json`, - { - method: "PUT", - data: { - message_ids: this.selectedMessageIds, - destination_channel_id: this.destinationChannelId, - }, - } - ) + return this.chatApi + .moveChannelMessages(this.sourceChannel.id, { + message_ids: this.selectedMessageIds, + destination_channel_id: this.destinationChannelId, + }) .then((response) => { - this.router.transitionTo( - "chat.channel", + return this.chat.openChannelAtMessage( response.destination_channel_id, - response.destination_channel_title, - { - queryParams: { messageId: response.first_moved_message_id }, - } + response.first_moved_message_id ); }) .catch(popupAjaxError); diff --git a/plugins/chat/assets/javascripts/discourse/components/chat-message.hbs b/plugins/chat/assets/javascripts/discourse/components/chat-message.hbs index f93d4213caf..db545f463f6 100644 --- a/plugins/chat/assets/javascripts/discourse/components/chat-message.hbs +++ b/plugins/chat/assets/javascripts/discourse/components/chat-message.hbs @@ -27,6 +27,7 @@ (if this.selectingMessages "selecting-messages") }} data-id={{or this.message.id this.message.stagedId}} + data-staged-id={{if this.message.staged this.message.stagedId}} > {{#if this.show}} {{#if this.selectingMessages}} diff --git a/plugins/chat/assets/javascripts/discourse/components/chat-message.js b/plugins/chat/assets/javascripts/discourse/components/chat-message.js index 867610bdf31..8f82d105015 100644 --- a/plugins/chat/assets/javascripts/discourse/components/chat-message.js +++ b/plugins/chat/assets/javascripts/discourse/components/chat-message.js @@ -43,6 +43,7 @@ export default Component.extend({ onHoverMessage: null, chatEmojiReactionStore: service("chat-emoji-reaction-store"), chatEmojiPickerManager: service("chat-emoji-picker-manager"), + chatChannelsManager: service("chat-channels-manager"), adminTools: optionalService(), _hasSubscribedToAppEvents: false, tagName: "", @@ -589,13 +590,11 @@ export default Component.extend({ // so we will fully refresh if we were not members of the channel // already if (!this.chatChannel.isFollowing || this.chatChannel.isDraft) { - this.chat.forceRefreshChannels().then(() => { - return this.chat - .getChannelBy("id", this.chatChannel.id) - .then((reactedChannel) => { - this.onSwitchChannel(reactedChannel); - }); - }); + return this.chatChannelsManager + .getChannel(this.chatChannel.id) + .then((reactedChannel) => { + this.onSwitchChannel(reactedChannel); + }); } }); }, diff --git a/plugins/chat/assets/javascripts/discourse/components/chat-to-topic-selector.js b/plugins/chat/assets/javascripts/discourse/components/chat-to-topic-selector.js index e7ae0190ffc..eb44133b1f8 100644 --- a/plugins/chat/assets/javascripts/discourse/components/chat-to-topic-selector.js +++ b/plugins/chat/assets/javascripts/discourse/components/chat-to-topic-selector.js @@ -3,9 +3,9 @@ import { htmlSafe } from "@ember/template"; import discourseComputed from "discourse-common/utils/decorators"; import { alias, equal } from "@ember/object/computed"; -export const NEW_TOPIC_SELECTION = "newTopic"; -export const EXISTING_TOPIC_SELECTION = "existingTopic"; -export const NEW_MESSAGE_SELECTION = "newMessage"; +export const NEW_TOPIC_SELECTION = "new_topic"; +export const EXISTING_TOPIC_SELECTION = "existing_topic"; +export const NEW_MESSAGE_SELECTION = "new_message"; export default Component.extend({ newTopicSelection: NEW_TOPIC_SELECTION, diff --git a/plugins/chat/assets/javascripts/discourse/components/full-page-chat.js b/plugins/chat/assets/javascripts/discourse/components/full-page-chat.js index 76edf0f21ee..a736c4e9011 100644 --- a/plugins/chat/assets/javascripts/discourse/components/full-page-chat.js +++ b/plugins/chat/assets/javascripts/discourse/components/full-page-chat.js @@ -10,9 +10,6 @@ export default Component.extend({ init() { this._super(...arguments); - - this.appEvents.on("chat:refresh-channels", this, "refreshModel"); - this.appEvents.on("chat:refresh-channel", this, "_refreshChannel"); }, didInsertElement() { @@ -25,8 +22,6 @@ export default Component.extend({ willDestroyElement() { this._super(...arguments); - this.appEvents.off("chat:refresh-channels", this, "refreshModel"); - this.appEvents.off("chat:refresh-channel", this, "_refreshChannel"); document.removeEventListener("keydown", this._autoFocusChatComposer); }, @@ -77,12 +72,6 @@ export default Component.extend({ } }, - _refreshChannel(channelId) { - if (this.chat.activeChannel?.id === channelId) { - this.refreshModel(true); - } - }, - @action navigateToIndex() { this.router.transitionTo("chat.index"); diff --git a/plugins/chat/assets/javascripts/discourse/components/toggle-channel-membership-button.hbs b/plugins/chat/assets/javascripts/discourse/components/toggle-channel-membership-button.hbs index a8b2d2c538c..00751d14915 100644 --- a/plugins/chat/assets/javascripts/discourse/components/toggle-channel-membership-button.hbs +++ b/plugins/chat/assets/javascripts/discourse/components/toggle-channel-membership-button.hbs @@ -1,4 +1,4 @@ -{{#if this.channel.isFollowing}} +{{#if @channel.currentUserMembership.following}} { this.onToggle?.(); }) @@ -69,16 +60,16 @@ export default class ToggleChannelMembershipButton extends Component { return; } - this.set("isLoading", false); + this.isLoading = false; }); } @action onLeaveChannel() { - this.set("isLoading", true); + this.isLoading = true; return this.chat - .unfollowChannel(this.channel) + .unfollowChannel(this.args.channel) .then(() => { this.onToggle?.(); }) @@ -88,7 +79,7 @@ export default class ToggleChannelMembershipButton extends Component { return; } - this.set("isLoading", false); + this.isLoading = false; }); } } diff --git a/plugins/chat/assets/javascripts/discourse/controllers/chat-channel-edit-description.js b/plugins/chat/assets/javascripts/discourse/controllers/chat-channel-edit-description.js index 0efdb70bbf1..85e834963ae 100644 --- a/plugins/chat/assets/javascripts/discourse/controllers/chat-channel-edit-description.js +++ b/plugins/chat/assets/javascripts/discourse/controllers/chat-channel-edit-description.js @@ -1,11 +1,12 @@ import Controller from "@ember/controller"; import { action, computed } from "@ember/object"; import ModalFunctionality from "discourse/mixins/modal-functionality"; -import ChatApi from "discourse/plugins/chat/discourse/lib/chat-api"; +import { inject as service } from "@ember/service"; export default class ChatChannelEditDescriptionController extends Controller.extend( ModalFunctionality ) { + @service chatApi; editedDescription = ""; @computed("model.description", "editedDescription") @@ -27,11 +28,12 @@ export default class ChatChannelEditDescriptionController extends Controller.ext @action onSaveChatChannelDescription() { - return ChatApi.modifyChatChannel(this.model.id, { - description: this.editedDescription, - }) - .then((chatChannel) => { - this.model.set("description", chatChannel.description); + return this.chatApi + .updateChannel(this.model.id, { + description: this.editedDescription, + }) + .then((result) => { + this.model.set("description", result.channel.description); this.send("closeModal"); }) .catch((event) => { diff --git a/plugins/chat/assets/javascripts/discourse/controllers/chat-channel-edit-title.js b/plugins/chat/assets/javascripts/discourse/controllers/chat-channel-edit-title.js index 9eb3dbde1f5..d57ad3f6ce6 100644 --- a/plugins/chat/assets/javascripts/discourse/controllers/chat-channel-edit-title.js +++ b/plugins/chat/assets/javascripts/discourse/controllers/chat-channel-edit-title.js @@ -1,11 +1,11 @@ import Controller from "@ember/controller"; import { action, computed } from "@ember/object"; import ModalFunctionality from "discourse/mixins/modal-functionality"; -import ChatApi from "discourse/plugins/chat/discourse/lib/chat-api"; - +import { inject as service } from "@ember/service"; export default class ChatChannelEditTitleController extends Controller.extend( ModalFunctionality ) { + @service chatApi; editedTitle = ""; @computed("model.title", "editedTitle") @@ -27,11 +27,12 @@ export default class ChatChannelEditTitleController extends Controller.extend( @action onSaveChatChannelTitle() { - return ChatApi.modifyChatChannel(this.model.id, { - name: this.editedTitle, - }) - .then((chatChannel) => { - this.model.set("title", chatChannel.title); + return this.chatApi + .updateChannel(this.model.id, { + name: this.editedTitle, + }) + .then((result) => { + this.model.set("title", result.channel.title); this.send("closeModal"); }) .catch((event) => { diff --git a/plugins/chat/assets/javascripts/discourse/controllers/chat-channel-info-about.js b/plugins/chat/assets/javascripts/discourse/controllers/chat-channel-info-about.js index c7976a24ff7..d33ec8fd222 100644 --- a/plugins/chat/assets/javascripts/discourse/controllers/chat-channel-info-about.js +++ b/plugins/chat/assets/javascripts/discourse/controllers/chat-channel-info-about.js @@ -8,13 +8,13 @@ export default class ChatChannelInfoAboutController extends Controller.extend( ) { @action onEditChatChannelTitle() { - showModal("chat-channel-edit-title", { model: this.model?.chatChannel }); + showModal("chat-channel-edit-title", { model: this.model }); } @action onEditChatChannelDescription() { showModal("chat-channel-edit-description", { - model: this.model?.chatChannel, + model: this.model, }); } } diff --git a/plugins/chat/assets/javascripts/discourse/controllers/chat-channel-info.js b/plugins/chat/assets/javascripts/discourse/controllers/chat-channel-info.js index 720e6f635f3..65c132080d9 100644 --- a/plugins/chat/assets/javascripts/discourse/controllers/chat-channel-info.js +++ b/plugins/chat/assets/javascripts/discourse/controllers/chat-channel-info.js @@ -1,7 +1,7 @@ import Controller from "@ember/controller"; -import { action, computed } from "@ember/object"; import { inject as service } from "@ember/service"; import { reads } from "@ember/object/computed"; +import { computed } from "@ember/object"; export default class ChatChannelInfoIndexController extends Controller { @service router; @@ -10,28 +10,25 @@ export default class ChatChannelInfoIndexController extends Controller { @reads("router.currentRoute.localName") tab; - @computed("model.chatChannel.{membershipsCount,status}") + @computed("model.{membershipsCount,status,currentUserMembership.following}") get tabs() { const tabs = []; - if (!this.model.chatChannel.isDirectMessageChannel) { + if (!this.model.isDirectMessageChannel) { tabs.push("about"); } - if ( - this.model.chatChannel.isOpen && - this.model.chatChannel.membershipsCount >= 1 - ) { + if (this.model.isOpen && this.model.membershipsCount >= 1) { tabs.push("members"); } - tabs.push("settings"); + if ( + this.currentUser?.staff || + this.model.currentUserMembership?.following + ) { + tabs.push("settings"); + } return tabs; } - - @action - switchChannel(channel) { - return this.chat.openChannel(channel); - } } diff --git a/plugins/chat/assets/javascripts/discourse/controllers/create-channel.js b/plugins/chat/assets/javascripts/discourse/controllers/create-channel.js index 00b9b8e0aef..ae2ed12f483 100644 --- a/plugins/chat/assets/javascripts/discourse/controllers/create-channel.js +++ b/plugins/chat/assets/javascripts/discourse/controllers/create-channel.js @@ -1,10 +1,7 @@ import { escapeExpression } from "discourse/lib/utilities"; import Controller from "@ember/controller"; -import ChatApi from "discourse/plugins/chat/discourse/lib/chat-api"; -import ChatChannel from "discourse/plugins/chat/discourse/models/chat-channel"; import I18n from "I18n"; import ModalFunctionality from "discourse/mixins/modal-functionality"; -import { ajax } from "discourse/lib/ajax"; import { action, computed } from "@ember/object"; import { gt, notEmpty } from "@ember/object/computed"; import { inject as service } from "@ember/service"; @@ -23,6 +20,8 @@ export default class CreateChannelController extends Controller.extend( ) { @service chat; @service dialog; + @service chatChannelsManager; + @service chatApi; category = null; categoryId = null; @@ -57,20 +56,18 @@ export default class CreateChannelController extends Controller.extend( _createChannel() { const data = { - id: this.categoryId, + chatable_id: this.categoryId, name: this.name, description: this.description, auto_join_users: this.autoJoinUsers, }; - return ajax("/chat/chat_channels", { method: "PUT", data }) - .then((response) => { - const chatChannel = ChatChannel.create(response.chat_channel); - - return this.chat.startTrackingChannel(chatChannel).then(() => { - this.send("closeModal"); - this.chat.openChannel(chatChannel); - }); + return this.chatApi + .createChannel(data) + .then((channel) => { + this.send("closeModal"); + this.chatChannelsManager.follow(channel); + this.chat.openChannel(channel); }) .catch((e) => { this.flash(e.jqXHR.responseJSON.errors[0], "error"); @@ -117,24 +114,26 @@ export default class CreateChannelController extends Controller.extend( if (category) { const fullSlug = this._buildCategorySlug(category); - return ChatApi.categoryPermissions(category.id).then((catPermissions) => { - this._updateAutoJoinConfirmWarning(category, catPermissions); - const allowedGroups = catPermissions.allowed_groups; - const translationKey = - allowedGroups.length < 3 ? "hint_groups" : "hint_multiple_groups"; + return this.chatApi + .categoryPermissions(category.id) + .then((catPermissions) => { + this._updateAutoJoinConfirmWarning(category, catPermissions); + const allowedGroups = catPermissions.allowed_groups; + const translationKey = + allowedGroups.length < 3 ? "hint_groups" : "hint_multiple_groups"; - this.set( - "categoryPermissionsHint", - htmlSafe( - I18n.t(`chat.create_channel.choose_category.${translationKey}`, { - link: `/c/${escapeExpression(fullSlug)}/edit/security`, - hint: escapeExpression(allowedGroups[0]), - hint_2: escapeExpression(allowedGroups[1]), - count: allowedGroups.length, - }) - ) - ); - }); + this.set( + "categoryPermissionsHint", + htmlSafe( + I18n.t(`chat.create_channel.choose_category.${translationKey}`, { + link: `/c/${escapeExpression(fullSlug)}/edit/security`, + hint: escapeExpression(allowedGroups[0]), + hint_2: escapeExpression(allowedGroups[1]), + count: allowedGroups.length, + }) + ) + ); + }); } else { this.set("categoryPermissionsHint", DEFAULT_HINT); this.set("autoJoinWarning", ""); diff --git a/plugins/chat/assets/javascripts/discourse/initializers/chat-setup.js b/plugins/chat/assets/javascripts/discourse/initializers/chat-setup.js index ee754a1e75e..c892853691c 100644 --- a/plugins/chat/assets/javascripts/discourse/initializers/chat-setup.js +++ b/plugins/chat/assets/javascripts/discourse/initializers/chat-setup.js @@ -12,15 +12,13 @@ export default { name: "chat-setup", initialize(container) { this.chatService = container.lookup("service:chat"); - - if (!this.chatService.userCanChat) { - return; - } - this.siteSettings = container.lookup("service:site-settings"); this.appEvents = container.lookup("service:appEvents"); this.appEvents.on("discourse:focus-changed", this, "_handleFocusChanged"); + if (!this.chatService.userCanChat) { + return; + } withPluginApi("0.12.1", (api) => { api.registerChatComposerButton({ id: "chat-upload-btn", @@ -99,8 +97,6 @@ export default { const currentUser = api.getCurrentUser(); if (currentUser?.chat_channels) { this.chatService.setupWithPreloadedChannels(currentUser.chat_channels); - } else { - this.chatService.setupWithoutPreloadedChannels(); } const chatNotificationManager = container.lookup( @@ -115,19 +111,7 @@ export default { api.addCardClickListenerSelector(".chat-drawer-outlet"); - api.dispatchWidgetAppEvent( - "site-header", - "header-chat-link", - "chat:rerender-header" - ); - - api.dispatchWidgetAppEvent( - "sidebar-header", - "header-chat-link", - "chat:rerender-header" - ); - - api.addToHeaderIcons("header-chat-link"); + api.addToHeaderIcons("chat-header-icon"); api.decorateChatMessage(function (chatMessage, chatChannel) { if (!this.currentUser) { @@ -155,17 +139,22 @@ export default { }, teardown() { + this.appEvents.off("discourse:focus-changed", this, "_handleFocusChanged"); + if (!this.chatService.userCanChat) { return; } - this.appEvents.off("discourse:focus-changed", this, "_handleFocusChanged"); _lastForcedRefreshAt = null; clearChatComposerButtons(); }, @bind _handleFocusChanged(hasFocus) { + if (!this.chatService.userCanChat) { + return; + } + if (!hasFocus) { _lastForcedRefreshAt = Date.now(); return; @@ -179,6 +168,5 @@ export default { } _lastForcedRefreshAt = Date.now(); - this.chatService.refreshTrackingState(); }, }; diff --git a/plugins/chat/assets/javascripts/discourse/initializers/chat-sidebar.js b/plugins/chat/assets/javascripts/discourse/initializers/chat-sidebar.js index e7c417e8e1e..547632c6c77 100644 --- a/plugins/chat/assets/javascripts/discourse/initializers/chat-sidebar.js +++ b/plugins/chat/assets/javascripts/discourse/initializers/chat-sidebar.js @@ -25,41 +25,12 @@ export default { api.addSidebarSection( (BaseCustomSidebarSection, BaseCustomSidebarSectionLink) => { const SidebarChatChannelsSectionLink = class extends BaseCustomSidebarSectionLink { - @tracked chatChannelTrackingState = - this.chatService.currentUser.chat_channel_tracking_state[ - this.channel.id - ]; - constructor({ channel, chatService }) { super(...arguments); this.channel = channel; this.chatService = chatService; } - @bind - willDestroy() { - this.chatService.appEvents.off( - "chat:user-tracking-state-changed", - this._refreshTrackingState - ); - } - - @bind - didInsert() { - this.chatService.appEvents.on( - "chat:user-tracking-state-changed", - this._refreshTrackingState - ); - } - - @bind - _refreshTrackingState() { - this.chatChannelTrackingState = - this.chatService.currentUser.chat_channel_tracking_state[ - this.channel.id - ]; - } - get name() { return dasherize(slugifyChannel(this.channel)); } @@ -68,7 +39,7 @@ export default { get classNames() { const classes = []; - if (this.channel.current_user_membership.muted) { + if (this.channel.currentUserMembership.muted) { classes.push("sidebar-section-link--muted"); } @@ -76,6 +47,8 @@ export default { classes.push("sidebar-section-link--active"); } + classes.push(`channel-${this.channel.id}`); + return classes.join(" "); } @@ -118,26 +91,19 @@ export default { } get suffixValue() { - return this.chatChannelTrackingState?.unread_count > 0 + return this.channel.currentUserMembership.unread_count > 0 ? "circle" : ""; } get suffixCSSClass() { - return this.chatChannelTrackingState?.unread_mentions > 0 + return this.channel.currentUserMembership.unread_mentions > 0 ? "urgent" : "unread"; } }; const SidebarChatChannelsSection = class extends BaseCustomSidebarSection { - @tracked sectionLinks = []; - - @tracked sectionIndicator = - this.chatService.publicChannels && - this.chatService.publicChannels[0].current_user_membership - .unread_count; - @tracked currentUserCanJoinPublicChannels = this.sidebar.currentUser && (this.sidebar.currentUser.staff || @@ -150,37 +116,20 @@ export default { return; } this.chatService = container.lookup("service:chat"); - this.router = container.lookup("service:router"); - this.appEvents = container.lookup("service:app-events"); - this.appEvents.on("chat:refresh-channels", this._refreshChannels); - this._refreshChannels(); - } - - @bind - willDestroy() { - if (!this.appEvents) { - return; - } - this.appEvents.off( - "chat:refresh-channels", - this._refreshChannels + this.chatChannelsManager = container.lookup( + "service:chat-channels-manager" ); + this.router = container.lookup("service:router"); } - @bind - _refreshChannels() { - const newSectionLinks = []; - this.chatService.getChannels().then((channels) => { - channels.publicChannels.forEach((channel) => { - newSectionLinks.push( - new SidebarChatChannelsSectionLink({ - channel, - chatService: this.chatService, - }) - ); - }); - this.sectionLinks = newSectionLinks; - }); + get sectionLinks() { + return this.chatChannelsManager.publicMessageChannels.map( + (channel) => + new SidebarChatChannelsSectionLink({ + channel, + chatService: this.chatService, + }) + ); } get name() { @@ -228,11 +177,6 @@ export default { api.addSidebarSection( (BaseCustomSidebarSection, BaseCustomSidebarSectionLink) => { const SidebarChatDirectMessagesSectionLink = class extends BaseCustomSidebarSectionLink { - @tracked chatChannelTrackingState = - this.chatService.currentUser.chat_channel_tracking_state[ - this.channel.id - ]; - constructor({ channel, chatService }) { super(...arguments); this.channel = channel; @@ -258,7 +202,7 @@ export default { get classNames() { const classes = []; - if (this.channel.current_user_membership.muted) { + if (this.channel.currentUserMembership.muted) { classes.push("sidebar-section-link--muted"); } @@ -266,6 +210,8 @@ export default { classes.push("sidebar-section-link--active"); } + classes.push(`channel-${this.channel.id}`); + return classes.join(" "); } @@ -340,7 +286,7 @@ export default { } get suffixValue() { - return this.chatChannelTrackingState?.unread_count > 0 + return this.channel.currentUserMembership.unread_count > 0 ? "circle" : ""; } @@ -396,7 +342,6 @@ export default { const SidebarChatDirectMessagesSection = class extends BaseCustomSidebarSection { @service site; @service router; - @tracked sectionLinks = []; @tracked userCanDirectMessage = this.chatService.userCanDirectMessage; @@ -407,40 +352,19 @@ export default { return; } this.chatService = container.lookup("service:chat"); - this.chatService.appEvents.on( - "chat:user-tracking-state-changed", - this._refreshDirectMessageChannels - ); - this._refreshDirectMessageChannels(); - } - - @bind - willDestroy() { - if (container.isDestroyed) { - return; - } - this.chatService.appEvents.off( - "chat:user-tracking-state-changed", - this._refreshDirectMessageChannels + this.chatChannelsManager = container.lookup( + "service:chat-channels-manager" ); } - @bind - _refreshDirectMessageChannels() { - const newSectionLinks = []; - this.chatService.getChannels().then((channels) => { - this.chatService - .truncateDirectMessageChannels(channels.directMessageChannels) - .forEach((channel) => { - newSectionLinks.push( - new SidebarChatDirectMessagesSectionLink({ - channel, - chatService: this.chatService, - }) - ); - }); - this.sectionLinks = newSectionLinks; - }); + get sectionLinks() { + return this.chatChannelsManager.truncatedDirectMessageChannels.map( + (channel) => + new SidebarChatDirectMessagesSectionLink({ + channel, + chatService: this.chatService, + }) + ); } get name() { diff --git a/plugins/chat/assets/javascripts/discourse/lib/chat-api.js b/plugins/chat/assets/javascripts/discourse/lib/chat-api.js deleted file mode 100644 index 3b373570a72..00000000000 --- a/plugins/chat/assets/javascripts/discourse/lib/chat-api.js +++ /dev/null @@ -1,95 +0,0 @@ -import { ajax } from "discourse/lib/ajax"; -import { popupAjaxError } from "discourse/lib/ajax-error"; -import ChatChannel from "discourse/plugins/chat/discourse/models/chat-channel"; -export default class ChatApi { - static async chatChannelMemberships(channelId, data) { - return await ajax(`/chat/api/chat_channels/${channelId}/memberships.json`, { - data, - }).catch(popupAjaxError); - } - - static async updateChatChannelNotificationsSettings(channelId, data = {}) { - return await ajax( - `/chat/api/chat_channels/${channelId}/notifications_settings.json`, - { - method: "PUT", - data, - } - ).catch(popupAjaxError); - } - - static async sendMessage(channelId, data = {}) { - return ajax(`/chat/${channelId}.json`, { - ignoreUnsent: false, - method: "POST", - data, - }); - } - - static async chatChannels(data = {}) { - if (data?.status === "all") { - delete data.status; - } - - return await ajax(`/chat/api/chat_channels.json`, { - method: "GET", - data, - }) - .then((channels) => - channels.map((channel) => ChatChannel.create(channel)) - ) - .catch(popupAjaxError); - } - - static async modifyChatChannel(channelId, data) { - return await this._performRequest( - `/chat/api/chat_channels/${channelId}.json`, - { - method: "PUT", - data, - } - ); - } - - static async unfollowChatChannel(channel) { - return await this._performRequest( - `/chat/chat_channels/${channel.id}/unfollow.json`, - { - method: "POST", - } - ).then((updatedChannel) => { - channel.updateMembership(updatedChannel.current_user_membership); - - // doesn't matter if this is inaccurate, it will be eventually consistent - // via the channel-metadata MessageBus channel - channel.set("memberships_count", channel.memberships_count - 1); - return channel; - }); - } - - static async followChatChannel(channel) { - return await this._performRequest( - `/chat/chat_channels/${channel.id}/follow.json`, - { - method: "POST", - } - ).then((updatedChannel) => { - channel.updateMembership(updatedChannel.current_user_membership); - - // doesn't matter if this is inaccurate, it will be eventually consistent - // via the channel-metadata MessageBus channel - channel.set("memberships_count", channel.memberships_count + 1); - return channel; - }); - } - - static async categoryPermissions(categoryId) { - return await this._performRequest( - `/chat/api/category-chatables/${categoryId}/permissions.json` - ); - } - - static async _performRequest(...args) { - return await ajax(...args).catch(popupAjaxError); - } -} diff --git a/plugins/chat/assets/javascripts/discourse/models/chat-channel.js b/plugins/chat/assets/javascripts/discourse/models/chat-channel.js index 7dca63e974b..182b01a1ec3 100644 --- a/plugins/chat/assets/javascripts/discourse/models/chat-channel.js +++ b/plugins/chat/assets/javascripts/discourse/models/chat-channel.js @@ -1,15 +1,16 @@ import RestModel from "discourse/models/rest"; import I18n from "I18n"; -import { computed } from "@ember/object"; import User from "discourse/models/user"; import UserChatChannelMembership from "discourse/plugins/chat/discourse/models/user-chat-channel-membership"; import { ajax } from "discourse/lib/ajax"; import { escapeExpression } from "discourse/lib/utilities"; +import { tracked } from "@glimmer/tracking"; export const CHATABLE_TYPES = { directMessageChannel: "DirectMessage", categoryChannel: "Category", }; + export const CHANNEL_STATUSES = { open: "open", readOnly: "read_only", @@ -38,13 +39,10 @@ export function channelStatusIcon(channelStatus) { switch (channelStatus) { case CHANNEL_STATUSES.closed: return "lock"; - break; case CHANNEL_STATUSES.readOnly: return "comment-slash"; - break; case CHANNEL_STATUSES.archived: return "archive"; - break; } } @@ -60,62 +58,51 @@ const READONLY_STATUSES = [ ]; export default class ChatChannel extends RestModel { - isDraft = false; - lastSendReadMessageId = null; + @tracked currentUserMembership = null; + @tracked isDraft = false; + @tracked title; + @tracked description; + @tracked chatableType; + @tracked status; - @computed("title") get escapedTitle() { return escapeExpression(this.title); } - @computed("description") get escapedDescription() { return escapeExpression(this.description); } - @computed("chatable_type") get isDirectMessageChannel() { return this.chatable_type === CHATABLE_TYPES.directMessageChannel; } - @computed("chatable_type") get isCategoryChannel() { return this.chatable_type === CHATABLE_TYPES.categoryChannel; } - @computed("status") get isOpen() { return !this.status || this.status === CHANNEL_STATUSES.open; } - @computed("status") get isReadOnly() { return this.status === CHANNEL_STATUSES.readOnly; } - @computed("status") get isClosed() { return this.status === CHANNEL_STATUSES.closed; } - @computed("status") get isArchived() { return this.status === CHANNEL_STATUSES.archived; } - @computed("isArchived", "isOpen") get isJoinable() { return this.isOpen && !this.isArchived; } - @computed("memberships_count") - get membershipsCount() { - return this.memberships_count; - } - - @computed("current_user_membership.following") get isFollowing() { - return this.current_user_membership.following; + return this.currentUserMembership.following; } canModifyMessages(user) { @@ -127,12 +114,12 @@ export default class ChatChannel extends RestModel { } updateMembership(membership) { - this.current_user_membership.setProperties({ - following: membership.following, - muted: membership.muted, - desktop_notification_level: membership.desktop_notification_level, - mobile_notification_level: membership.mobile_notification_level, - }); + this.currentUserMembership.following = membership.following; + this.currentUserMembership.muted = membership.muted; + this.currentUserMembership.desktop_notification_level = + membership.desktop_notification_level; + this.currentUserMembership.mobile_notification_level = + membership.mobile_notification_level; } updateLastReadMessage(messageId) { @@ -143,7 +130,7 @@ export default class ChatChannel extends RestModel { return ajax(`/chat/${this.id}/read/${messageId}.json`, { method: "PUT", }).then(() => { - this.set("lastSendReadMessageId", messageId); + this.currentUserMembership.last_read_message_id = messageId; }); } } @@ -151,11 +138,12 @@ export default class ChatChannel extends RestModel { ChatChannel.reopenClass({ create(args) { args = args || {}; + this._initUserModels(args); this._initUserMembership(args); - args.lastSendReadMessageId = - args.current_user_membership?.last_read_message_id; + args.chatableType = args.chatable_type; + args.membershipsCount = args.memberships_count; return this._super(args); }, @@ -170,11 +158,11 @@ ChatChannel.reopenClass({ }, _initUserMembership(args) { - if (args.current_user_membership instanceof UserChatChannelMembership) { + if (args.currentUserMembership instanceof UserChatChannelMembership) { return; } - args.current_user_membership = UserChatChannelMembership.create( + args.currentUserMembership = UserChatChannelMembership.create( args.current_user_membership || { following: false, muted: false, @@ -182,6 +170,8 @@ ChatChannel.reopenClass({ unread_mentions: 0, } ); + + delete args.current_user_membership; }, }); diff --git a/plugins/chat/assets/javascripts/discourse/models/user-chat-channel-membership.js b/plugins/chat/assets/javascripts/discourse/models/user-chat-channel-membership.js index 8e97e26bfe2..9d732e82fc4 100644 --- a/plugins/chat/assets/javascripts/discourse/models/user-chat-channel-membership.js +++ b/plugins/chat/assets/javascripts/discourse/models/user-chat-channel-membership.js @@ -1,3 +1,30 @@ import RestModel from "discourse/models/rest"; +import { tracked } from "@glimmer/tracking"; +import User from "discourse/models/user"; +export default class UserChatChannelMembership extends RestModel { + @tracked following = false; + @tracked muted = false; + @tracked unread_count = 0; + @tracked unread_mentions = 0; + @tracked chat_message_id = null; + @tracked chat_channel_id = null; + @tracked desktop_notification_level = null; + @tracked mobile_notification_level = null; + @tracked last_read_message_id = null; +} -export default class UserChatChannelMembership extends RestModel {} +UserChatChannelMembership.reopenClass({ + create(args) { + args = args || {}; + this._initUser(args); + return this._super(args); + }, + + _initUser(args) { + if (args.user instanceof User) { + return; + } + + args.user = User.create(args.user); + }, +}); diff --git a/plugins/chat/assets/javascripts/discourse/routes/chat-browse-archived.js b/plugins/chat/assets/javascripts/discourse/routes/chat-browse-archived.js new file mode 100644 index 00000000000..7fc075e2cee --- /dev/null +++ b/plugins/chat/assets/javascripts/discourse/routes/chat-browse-archived.js @@ -0,0 +1,9 @@ +import DiscourseRoute from "discourse/routes/discourse"; + +export default class ChatBrowseIndexRoute extends DiscourseRoute { + afterModel() { + if (!this.siteSettings.chat_allow_archiving_channels) { + this.replaceWith("chat.browse"); + } + } +} diff --git a/plugins/chat/assets/javascripts/discourse/routes/chat-channel-by-name.js b/plugins/chat/assets/javascripts/discourse/routes/chat-channel-by-name.js index 7516fe58bb1..ba6c91c245e 100644 --- a/plugins/chat/assets/javascripts/discourse/routes/chat-channel-by-name.js +++ b/plugins/chat/assets/javascripts/discourse/routes/chat-channel-by-name.js @@ -13,8 +13,8 @@ export default class ChatChannelByNameRoute extends DiscourseRoute { .then((response) => { this.transitionTo( "chat.channel", - response.chat_channel.id, - response.chat_channel.title + response.channel.id, + response.channel.title ); }) .catch(() => this.replaceWith("/404")); diff --git a/plugins/chat/assets/javascripts/discourse/routes/chat-channel-info-about.js b/plugins/chat/assets/javascripts/discourse/routes/chat-channel-info-about.js index 181c9ffb690..a92d5e882f0 100644 --- a/plugins/chat/assets/javascripts/discourse/routes/chat-channel-info-about.js +++ b/plugins/chat/assets/javascripts/discourse/routes/chat-channel-info-about.js @@ -2,7 +2,7 @@ import DiscourseRoute from "discourse/routes/discourse"; export default class ChatChannelInfoAboutRoute extends DiscourseRoute { afterModel(model) { - if (model.chatChannel.isDirectMessageChannel) { + if (model.isDirectMessageChannel) { this.replaceWith("chat.channel.info.index"); } } diff --git a/plugins/chat/assets/javascripts/discourse/routes/chat-channel-info-index.js b/plugins/chat/assets/javascripts/discourse/routes/chat-channel-info-index.js index ffc3bc589b4..c7bfd1c0905 100644 --- a/plugins/chat/assets/javascripts/discourse/routes/chat-channel-info-index.js +++ b/plugins/chat/assets/javascripts/discourse/routes/chat-channel-info-index.js @@ -2,8 +2,8 @@ import DiscourseRoute from "discourse/routes/discourse"; export default class ChatChannelInfoIndexRoute extends DiscourseRoute { afterModel(model) { - if (model.chatChannel.isDirectMessageChannel) { - if (model.chatChannel.isOpen && model.chatChannel.membershipsCount >= 1) { + if (model.isDirectMessageChannel) { + if (model.isOpen && model.membershipsCount >= 1) { this.replaceWith("chat.channel.info.members"); } else { this.replaceWith("chat.channel.info.settings"); diff --git a/plugins/chat/assets/javascripts/discourse/routes/chat-channel-info-members.js b/plugins/chat/assets/javascripts/discourse/routes/chat-channel-info-members.js index 25d2e4eb93a..d3fba6f97de 100644 --- a/plugins/chat/assets/javascripts/discourse/routes/chat-channel-info-members.js +++ b/plugins/chat/assets/javascripts/discourse/routes/chat-channel-info-members.js @@ -2,8 +2,12 @@ import DiscourseRoute from "discourse/routes/discourse"; export default class ChatChannelInfoMembersRoute extends DiscourseRoute { afterModel(model) { - if (!model.chatChannel.isOpen) { - this.replaceWith("chat.channel.info.settings"); + if (!model.isOpen) { + return this.replaceWith("chat.channel.info.settings"); + } + + if (model.membershipsCount < 1) { + return this.replaceWith("chat.channel.info"); } } } diff --git a/plugins/chat/assets/javascripts/discourse/routes/chat-channel-info-settings.js b/plugins/chat/assets/javascripts/discourse/routes/chat-channel-info-settings.js new file mode 100644 index 00000000000..61635238534 --- /dev/null +++ b/plugins/chat/assets/javascripts/discourse/routes/chat-channel-info-settings.js @@ -0,0 +1,9 @@ +import DiscourseRoute from "discourse/routes/discourse"; + +export default class ChatChannelInfoSettingsRoute extends DiscourseRoute { + afterModel(model) { + if (!this.currentUser?.staff && !model.currentUserMembership?.following) { + this.replaceWith("chat.channel.info"); + } + } +} diff --git a/plugins/chat/assets/javascripts/discourse/routes/chat-channel.js b/plugins/chat/assets/javascripts/discourse/routes/chat-channel.js index 17e6a2f7456..7e49d1f764e 100644 --- a/plugins/chat/assets/javascripts/discourse/routes/chat-channel.js +++ b/plugins/chat/assets/javascripts/discourse/routes/chat-channel.js @@ -1,48 +1,23 @@ import DiscourseRoute from "discourse/routes/discourse"; -import Promise from "rsvp"; -import EmberObject, { action } from "@ember/object"; -import { ajax } from "discourse/lib/ajax"; import { inject as service } from "@ember/service"; -import ChatChannel from "discourse/plugins/chat/discourse/models/chat-channel"; import slugifyChannel from "discourse/plugins/chat/discourse/lib/slugify-channel"; export default class ChatChannelRoute extends DiscourseRoute { @service chat; @service router; + @service chatChannelsManager; async model(params) { - let [chatChannel, channels] = await Promise.all([ - this.getChannel(params.channelId), - this.chat.getChannels(), - ]); - - return EmberObject.create({ - chatChannel, - channels, - }); - } - - async getChannel(id) { - let channel = await this.chat.getChannelBy("id", id); - if (!channel || this.forceRefetchChannel) { - channel = await this.getChannelFromServer(id); - } - return channel; - } - - async getChannelFromServer(id) { - return ajax(`/chat/chat_channels/${id}`) - .then((response) => ChatChannel.create(response)) - .catch(() => this.replaceWith("/404")); + return this.chatChannelsManager.find(params.channelId); } afterModel(model) { - this.chat.setActiveChannel(model?.chatChannel); + this.chat.setActiveChannel(model); const queryParams = this.paramsFor(this.routeName); - const slug = slugifyChannel(model.chatChannel); + const slug = slugifyChannel(model); if (queryParams?.channelTitle !== slug) { - this.router.replaceWith("chat.channel.index", model.chatChannel.id, slug); + this.router.replaceWith("chat.channel.index", model.id, slug); } } @@ -54,10 +29,4 @@ export default class ChatChannelRoute extends DiscourseRoute { this.controller.set("messageId", null); } } - - @action - refreshModel(forceRefetchChannel = false) { - this.forceRefetchChannel = forceRefetchChannel; - this.refresh(); - } } diff --git a/plugins/chat/assets/javascripts/discourse/routes/chat-index.js b/plugins/chat/assets/javascripts/discourse/routes/chat-index.js index fbf8cd0a3cb..027f21b4672 100644 --- a/plugins/chat/assets/javascripts/discourse/routes/chat-index.js +++ b/plugins/chat/assets/javascripts/discourse/routes/chat-index.js @@ -3,36 +3,29 @@ import { inject as service } from "@ember/service"; export default class ChatIndexRoute extends DiscourseRoute { @service chat; + @service chatChannelsManager; @service router; redirect() { + // Always want the channel index on mobile. if (this.site.mobileView) { - return; // Always want the channel index on mobile. + return; } - // We are on desktop. Check for a channel to enter and transition if so. - // Otherwise, `setupController` will fetch all available - return this.chat.getIdealFirstChannelIdAndTitle().then((channelInfo) => { - if (channelInfo) { - return this.chat.getChannelBy("id", channelInfo.id).then((c) => { - return this.chat.openChannel(c); - }); - } else { - return this.router.transitionTo("chat.browse"); - } - }); + // We are on desktop. Check for a channel to enter and transition if so + const id = this.chat.getIdealFirstChannelId(); + if (id) { + return this.chatChannelsManager.find(id).then((c) => { + return this.chat.openChannel(c); + }); + } else { + return this.router.transitionTo("chat.browse"); + } } model() { if (this.site.mobileView) { - return this.chat.getChannels().then((channels) => { - if ( - channels.publicChannels.length || - channels.directMessageChannels.length - ) { - return channels; - } - }); + return this.chatChannelsManager.channels; } } } diff --git a/plugins/chat/assets/javascripts/discourse/services/chat-api.js b/plugins/chat/assets/javascripts/discourse/services/chat-api.js new file mode 100644 index 00000000000..4a24d8a9b6e --- /dev/null +++ b/plugins/chat/assets/javascripts/discourse/services/chat-api.js @@ -0,0 +1,242 @@ +import Service, { inject as service } from "@ember/service"; +import { ajax } from "discourse/lib/ajax"; +import UserChatChannelMembership from "discourse/plugins/chat/discourse/models/user-chat-channel-membership"; +import { tracked } from "@glimmer/tracking"; +import { bind } from "discourse-common/utils/decorators"; +import { Promise } from "rsvp"; + +class Collection { + @tracked items = []; + @tracked meta = {}; + @tracked loading = false; + + constructor(resourceURL, handler) { + this._resourceURL = resourceURL; + this._handler = handler; + this._fetchedAll = false; + } + + get loadMoreURL() { + return this.meta.load_more_url; + } + + get totalRows() { + return this.meta.total_rows; + } + + get length() { + return this.items.length; + } + + // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Iteration_protocols + [Symbol.iterator]() { + let index = 0; + + return { + next: () => { + if (index < this.items.length) { + return { value: this.items[index++], done: false }; + } else { + return { done: true }; + } + }, + }; + } + + @bind + load(params = {}) { + this._fetchedAll = false; + + if (this.loading) { + return; + } + + this.loading = true; + + const filteredQueryParams = Object.entries(params).filter( + ([, v]) => v !== undefined + ); + const queryString = new URLSearchParams(filteredQueryParams).toString(); + + const endpoint = this._resourceURL + (queryString ? `?${queryString}` : ""); + return this.#fetch(endpoint) + .then((result) => { + this.items = this._handler(result); + this.meta = result.meta; + }) + .finally(() => { + this.loading = false; + }); + } + + @bind + loadMore() { + if (this.loading) { + return; + } + + if ( + this._fetchedAll || + (this.totalRows && this.items.length >= this.totalRows) + ) { + return; + } + + let promise; + + this.loading = true; + + if (this.loadMoreURL) { + promise = this.#fetch(this.loadMoreURL).then((result) => { + const newItems = this._handler(result); + + if (newItems.length) { + this.items = this.items.concat(newItems); + } else { + this._fetchedAll = true; + } + this.meta = result.meta; + }); + } else { + promise = Promise.resolve(); + } + + return promise.finally(() => { + this.loading = false; + }); + } + + #fetch(url) { + return ajax(url, { type: "GET" }); + } +} + +export default class ChatApi extends Service { + @service chatChannelsManager; + + getChannel(channelId) { + return this.#getRequest(`/channels/${channelId}`).then((result) => + this.chatChannelsManager.store(result.channel) + ); + } + + channels() { + return new Collection(`${this.#basePath}/channels`, (response) => { + return response.channels.map((channel) => + this.chatChannelsManager.store(channel) + ); + }); + } + + moveChannelMessages(channelId, data = {}) { + return this.#postRequest(`/channels/${channelId}/messages/moves`, { + move: data, + }); + } + + destroyChannel(channelId, data = {}) { + return this.#deleteRequest(`/channels/${channelId}`, { channel: data }); + } + + createChannel(data = {}) { + return this.#postRequest("/channels", { channel: data }).then((response) => + this.chatChannelsManager.store(response.channel) + ); + } + + categoryPermissions(categoryId) { + return ajax(`/chat/api/category-chatables/${categoryId}/permissions`); + } + + sendMessage(channelId, data = {}) { + return ajax(`/chat/${channelId}`, { + ignoreUnsent: false, + type: "POST", + data, + }); + } + + createChannelArchive(channelId, data = {}) { + return this.#postRequest(`/channels/${channelId}/archives`, { + archive: data, + }); + } + + updateChannel(channelId, data = {}) { + return this.#putRequest(`/channels/${channelId}`, { channel: data }); + } + + updateChannelStatus(channelId, status) { + return this.#putRequest(`/channels/${channelId}/status`, { status }); + } + + listChannelMemberships(channelId) { + return new Collection( + `${this.#basePath}/channels/${channelId}/memberships`, + (response) => { + return response.memberships.map((membership) => + UserChatChannelMembership.create(membership) + ); + } + ); + } + + listCurrentUserChannels() { + return this.#getRequest(`/channels/me`).then((result) => { + return (result?.channels || []).map((channel) => + this.chatChannelsManager.store(channel) + ); + }); + } + + followChannel(channelId) { + return this.#postRequest(`/channels/${channelId}/memberships/me`).then( + (result) => UserChatChannelMembership.create(result.membership) + ); + } + + unfollowChannel(channelId) { + return this.#deleteRequest(`/channels/${channelId}/memberships/me`).then( + (result) => UserChatChannelMembership.create(result.membership) + ); + } + + updateCurrentUserChatChannelNotificationsSettings(channelId, data = {}) { + return this.#putRequest( + `/channels/${channelId}/notifications-settings/me`, + { notifications_settings: data } + ); + } + + get #basePath() { + return "/chat/api"; + } + + #getRequest(endpoint, data = {}) { + return ajax(`${this.#basePath}/${endpoint}`, { + type: "GET", + data, + }); + } + + #putRequest(endpoint, data = {}) { + return ajax(`${this.#basePath}/${endpoint}`, { + type: "PUT", + data, + }); + } + + #postRequest(endpoint, data = {}) { + return ajax(`${this.#basePath}/${endpoint}`, { + type: "POST", + data, + }); + } + + #deleteRequest(endpoint, data = {}) { + return ajax(`${this.#basePath}/${endpoint}`, { + type: "DELETE", + data, + }); + } +} diff --git a/plugins/chat/assets/javascripts/discourse/services/chat-channels-manager.js b/plugins/chat/assets/javascripts/discourse/services/chat-channels-manager.js new file mode 100644 index 00000000000..4ba6ab2365b --- /dev/null +++ b/plugins/chat/assets/javascripts/discourse/services/chat-channels-manager.js @@ -0,0 +1,136 @@ +import Service, { inject as service } from "@ember/service"; +import Promise from "rsvp"; +import ChatChannel from "discourse/plugins/chat/discourse/models/chat-channel"; +import { tracked } from "@glimmer/tracking"; +import { TrackedObject } from "@ember-compat/tracked-built-ins"; + +const DIRECT_MESSAGE_CHANNELS_LIMIT = 20; + +export default class ChatChannelsManager extends Service { + @service chatSubscriptionsManager; + @service chatApi; + @service currentUser; + @tracked _cached = new TrackedObject(); + + get channels() { + return Object.values(this._cached); + } + + async find(id) { + const existingChannel = this.#findStale(id); + if (existingChannel) { + return Promise.resolve(existingChannel); + } else { + return this.#find(id); + } + } + + store(channelObject) { + let model = this.#findStale(channelObject.id); + + if (!model) { + model = ChatChannel.create(channelObject); + this.#cache(model); + } + + return model; + } + + async follow(model) { + this.chatSubscriptionsManager.startChannelSubscription(model); + + if (!model.currentUserMembership.following) { + return this.chatApi.followChannel(model.id).then((membership) => { + model.currentUserMembership.following = membership.following; + model.currentUserMembership.muted = membership.muted; + model.currentUserMembership.desktop_notification_level = + membership.desktop_notification_level; + model.currentUserMembership.mobile_notification_level = + membership.mobile_notification_level; + + return model; + }); + } else { + return Promise.resolve(model); + } + } + + async unfollow(model) { + this.chatSubscriptionsManager.stopChannelSubscription(model); + + return this.chatApi.unfollowChannel(model.id).then((membership) => { + model.currentUserMembership = membership; + + return model; + }); + } + + get unreadCount() { + let count = 0; + this.publicMessageChannels.forEach((channel) => { + count += channel.currentUserMembership.unread_count || 0; + }); + return count; + } + + get unreadUrgentCount() { + let count = 0; + this.channels.forEach((channel) => { + if (channel.isDirectMessageChannel) { + count += channel.currentUserMembership.unread_count || 0; + } + count += channel.currentUserMembership.unread_mentions || 0; + }); + return count; + } + + get publicMessageChannels() { + return this.channels.filter( + (channel) => + channel.isCategoryChannel && channel.currentUserMembership.following + ); + } + + get directMessageChannels() { + return this.#sortDirectMessageChannels( + this.channels.filter((channel) => { + const membership = channel.currentUserMembership; + return channel.isDirectMessageChannel && membership.following; + }) + ); + } + + get truncatedDirectMessageChannels() { + return this.directMessageChannels.slice(0, DIRECT_MESSAGE_CHANNELS_LIMIT); + } + + async #find(id) { + return this.chatApi.getChannel(id).then((channel) => { + this.#cache(channel); + return channel; + }); + } + + #cache(channel) { + this._cached[channel.id] = channel; + } + + #findStale(id) { + return this._cached[id]; + } + + #sortDirectMessageChannels(channels) { + return channels.sort((a, b) => { + const unreadCountA = a.currentUserMembership.unread_count || 0; + const unreadCountB = b.currentUserMembership.unread_count || 0; + if (unreadCountA === unreadCountB) { + return new Date(a.get("last_message_sent_at")) > + new Date(b.get("last_message_sent_at")) + ? -1 + : 1; + } else { + return unreadCountA > unreadCountB ? -1 : 1; + } + }); + } +} diff --git a/plugins/chat/assets/javascripts/discourse/services/chat-message-visibility-observer.js b/plugins/chat/assets/javascripts/discourse/services/chat-message-visibility-observer.js index b76f8420e44..bc51a26394e 100644 --- a/plugins/chat/assets/javascripts/discourse/services/chat-message-visibility-observer.js +++ b/plugins/chat/assets/javascripts/discourse/services/chat-message-visibility-observer.js @@ -19,7 +19,11 @@ export default class ChatMessageVisibilityObserver extends Service { entries.forEach((entry) => { entry.target.dataset.visible = entry.isIntersecting; - if (entry.isIntersecting && !isTesting()) { + if ( + !entry.target.dataset.stagedId && + entry.isIntersecting && + !isTesting() + ) { this.chat.updateLastReadMessage(); } }); diff --git a/plugins/chat/assets/javascripts/discourse/services/chat-state-manager.js b/plugins/chat/assets/javascripts/discourse/services/chat-state-manager.js index 12df7f6382e..ee79bb20bb3 100644 --- a/plugins/chat/assets/javascripts/discourse/services/chat-state-manager.js +++ b/plugins/chat/assets/javascripts/discourse/services/chat-state-manager.js @@ -86,6 +86,10 @@ export default class ChatStateManager extends Service { return this.router.currentRouteName?.startsWith("chat"); } + get isActive() { + return this.isFullPageActive || this.isDrawerActive; + } + storeAppURL(URL = null) { this._appURL = URL || this.router.currentURL; } diff --git a/plugins/chat/assets/javascripts/discourse/services/chat-subscriptions-manager.js b/plugins/chat/assets/javascripts/discourse/services/chat-subscriptions-manager.js new file mode 100644 index 00000000000..cf627de4d57 --- /dev/null +++ b/plugins/chat/assets/javascripts/discourse/services/chat-subscriptions-manager.js @@ -0,0 +1,265 @@ +import Service, { inject as service } from "@ember/service"; +import { bind } from "discourse-common/utils/decorators"; +import { CHANNEL_STATUSES } from "discourse/plugins/chat/discourse/models/chat-channel"; + +export default class ChatSubscriptionsManager extends Service { + @service store; + @service chatChannelsManager; + @service currentUser; + @service appEvents; + + _channelSubscriptions = new Set(); + + startChannelSubscription(channel) { + if ( + channel.currentUserMembership.muted || + this._channelSubscriptions.has(channel.id) + ) { + return; + } + + this._channelSubscriptions.add(channel.id); + + if (!channel.isDirectMessageChannel) { + this._startChannelMentionsSubscription(channel); + } + + this._startChannelNewMessagesSubscription(channel); + } + + stopChannelSubscription(channel) { + this.messageBus.unsubscribe( + `/chat/${channel.id}/new-messages`, + this._onNewMessages + ); + if (!channel.isDirectMessageChannel) { + this.messageBus.unsubscribe( + `/chat/${channel.id}/new-mentions`, + this._onNewMentions + ); + } + + this._channelSubscriptions.delete(channel.id); + } + + startChannelsSubscriptions(messageBusIds) { + this._startNewChannelSubscription(messageBusIds.new_channel); + this._startUserTrackingStateSubscription(messageBusIds.user_tracking_state); + this._startChannelsEditsSubscription(messageBusIds.channel_edits); + this._startChannelsStatusChangesSubscription(messageBusIds.channel_status); + this._startChannelsMetadataChangesSubscription( + messageBusIds.channel_metadata + ); + } + + stopChannelsSubscriptions() { + this._stopNewChannelSubscription(); + this._stopUserTrackingStateSubscription(); + this._stopChannelsEditsSubscription(); + this._stopChannelsStatusChangesSubscription(); + this._stopChannelsMetadataChangesSubscription(); + + (this.chatChannelsManager.channels || []).forEach((channel) => { + this.stopChannelSubscription(channel); + }); + } + + _startChannelMentionsSubscription(channel) { + this.messageBus.subscribe( + `/chat/${channel.id}/new-mentions`, + this._onNewMentions, + channel.meta.message_bus_last_ids.new_mentions + ); + } + + @bind + _onNewMentions(busData) { + this.chatChannelsManager.find(busData.channel_id).then((channel) => { + const membership = channel.currentUserMembership; + if (membership) { + membership.unread_mentions = (membership.unread_mentions || 0) + 1; + } + }); + } + + _startChannelNewMessagesSubscription(channel) { + this.messageBus.subscribe( + `/chat/${channel.id}/new-messages`, + this._onNewMessages, + channel.meta.message_bus_last_ids.new_messages + ); + } + + @bind + _onNewMessages(busData) { + this.chatChannelsManager.find(busData.channel_id).then((channel) => { + if (busData.user_id === this.currentUser.id) { + // User sent message, update tracking state to no unread + channel.currentUserMembership.chat_message_id = busData.message_id; + } else { + // Ignored user sent message, update tracking state to no unread + if (this.currentUser.ignored_users.includes(busData.username)) { + channel.currentUserMembership.chat_message_id = busData.message_id; + } else { + // Message from other user. Increment trackings state + if ( + busData.message_id > + (channel.currentUserMembership.chat_message_id || 0) + ) { + channel.currentUserMembership.unread_count = + channel.currentUserMembership.unread_count + 1; + } + } + } + + channel.set("last_message_sent_at", new Date()); + }); + } + + _startUserTrackingStateSubscription(lastId) { + if (!this.currentUser) { + return; + } + + this.messageBus.subscribe( + `/chat/user-tracking-state/${this.currentUser.id}`, + this._onUserTrackingStateUpdate, + lastId + ); + } + + _stopUserTrackingStateSubscription() { + if (!this.currentUser) { + return; + } + + this.messageBus.unsubscribe( + `/chat/user-tracking-state/${this.currentUser.id}`, + this._onUserTrackingStateUpdate + ); + } + + @bind + _onUserTrackingStateUpdate(data) { + this.chatChannelsManager.find(data.chat_channel_id).then((channel) => { + if ( + channel?.currentUserMembership?.chat_message_id < data.chat_message_id + ) { + channel.currentUserMembership.chat_message_id = data.chat_message_id; + channel.currentUserMembership.unread_count = 0; + channel.currentUserMembership.unread_mentions = 0; + } + }); + } + + _startNewChannelSubscription(lastId) { + this.messageBus.subscribe( + "/chat/new-channel", + this._onNewChannelSubscription, + lastId + ); + } + + _stopNewChannelSubscription() { + this.messageBus.unsubscribe( + "/chat/new-channel", + this._onNewChannelSubscription + ); + } + + @bind + _onNewChannelSubscription(data) { + this.chatChannelsManager.find(data.channel.id).then((channel) => { + // we need to refrehs here to have correct last message ids + channel.meta = data.channel.meta; + + if ( + channel.isDirectMessageChannel && + !channel.currentUserMembership.following + ) { + channel.currentUserMembership.unread_count = 1; + } + + this.chatChannelsManager.follow(channel); + }); + } + + _startChannelsMetadataChangesSubscription(lastId) { + this.messageBus.subscribe( + "/chat/channel-metadata", + this._onChannelMetadata, + lastId + ); + } + + _startChannelsEditsSubscription(lastId) { + this.messageBus.subscribe( + "/chat/channel-edits", + this._onChannelEdits, + lastId + ); + } + + _startChannelsStatusChangesSubscription(lastId) { + this.messageBus.subscribe( + "/chat/channel-status", + this._onChannelStatus, + lastId + ); + } + + _stopChannelsStatusChangesSubscription() { + this.messageBus.unsubscribe("/chat/channel-status", this._onChannelStatus); + } + + _stopChannelsEditsSubscription() { + this.messageBus.unsubscribe("/chat/channel-edits", this._onChannelEdits); + } + + _stopChannelsMetadataChangesSubscription() { + this.messageBus.unsubscribe( + "/chat/channel-metadata", + this._onChannelMetadata + ); + } + + @bind + _onChannelMetadata(busData) { + this.chatChannelsManager.find(busData.chat_channel_id).then((channel) => { + if (channel) { + channel.setProperties({ + memberships_count: busData.memberships_count, + }); + this.appEvents.trigger("chat:refresh-channel-members"); + } + }); + } + + @bind + _onChannelEdits(busData) { + this.chatChannelsManager.find(busData.chat_channel_id).then((channel) => { + if (channel) { + channel.setProperties({ + title: busData.name, + description: busData.description, + }); + } + }); + } + + @bind + _onChannelStatus(busData) { + this.chatChannelsManager.find(busData.chat_channel_id).then((channel) => { + channel.set("status", busData.status); + + // it is not possible for the user to set their last read message id + // if the channel has been archived, because all the messages have + // been deleted. we don't want them seeing the blue dot anymore so + // just completely reset the unreads + if (busData.status === CHANNEL_STATUSES.archived) { + channel.currentUserMembership.unread_count = 0; + channel.currentUserMembership.unread_mentions = 0; + } + }); + } +} diff --git a/plugins/chat/assets/javascripts/discourse/services/chat.js b/plugins/chat/assets/javascripts/discourse/services/chat.js index 26feb212750..03b9164a058 100644 --- a/plugins/chat/assets/javascripts/discourse/services/chat.js +++ b/plugins/chat/assets/javascripts/discourse/services/chat.js @@ -5,22 +5,15 @@ import { popupAjaxError } from "discourse/lib/ajax-error"; import Service, { inject as service } from "@ember/service"; import Site from "discourse/models/site"; import { ajax } from "discourse/lib/ajax"; -import { A } from "@ember/array"; import { generateCookFunction } from "discourse/lib/text"; import { cancel, next } from "@ember/runloop"; import { and } from "@ember/object/computed"; +import { computed } from "@ember/object"; import { Promise } from "rsvp"; -import ChatChannel, { - CHANNEL_STATUSES, - CHATABLE_TYPES, -} from "discourse/plugins/chat/discourse/models/chat-channel"; import simpleCategoryHashMentionTransform from "discourse/plugins/chat/discourse/lib/simple-category-hash-mention-transform"; import discourseDebounce from "discourse-common/lib/debounce"; -import EmberObject, { computed } from "@ember/object"; -import ChatApi from "discourse/plugins/chat/discourse/lib/chat-api"; import discourseLater from "discourse-common/lib/later"; import userPresent from "discourse/lib/user-presence"; -import { bind } from "discourse-common/utils/decorators"; export const LIST_VIEW = "list_view"; export const CHAT_VIEW = "chat_view"; @@ -36,30 +29,21 @@ const READ_INTERVAL = 1000; export default class Chat extends Service { @service appEvents; @service chatNotificationManager; + @service chatSubscriptionsManager; @service chatStateManager; @service presence; @service router; @service site; + @service chatChannelsManager; activeChannel = null; - allChannels = null; cook = null; - directMessageChannels = null; - hasFetchedChannels = false; - hasUnreadMessages = false; - idToTitleMap = null; - lastUserTrackingMessageId = null; + messageId = null; presenceChannel = null; - publicChannels = null; sidebarActive = false; - unreadUrgentCount = null; - directMessagesLimit = 20; isNetworkUnreliable = false; @and("currentUser.has_chat_enabled", "siteSettings.chat_enabled") userCanChat; - _fetchingChannels = null; - _onNewMentionsCallbacks = new Map(); - _onNewMessagesCallbacks = new Map(); @computed("currentUser.staff", "currentUser.groups.[]") get userCanDirectMessage() { @@ -81,7 +65,6 @@ export default class Chat extends Service { super.init(...arguments); if (this.userCanChat) { - this.set("allChannels", []); this.presenceChannel = this.presence.getChannel("/chat/online"); this.draftStore = {}; @@ -114,38 +97,24 @@ export default class Chat extends Service { } setupWithPreloadedChannels(channels) { - this.currentUser.set("chat_channel_tracking_state", {}); - this._processChannels(channels || {}); - this.subscribeToChannelMessageBus(); - this.userChatChannelTrackingStateChanged(); - this.appEvents.trigger("chat:refresh-channels"); - } + this.chatSubscriptionsManager.startChannelsSubscriptions( + channels.meta.message_bus_last_ids + ); + this.presenceChannel.subscribe(channels.global_presence_channel_state); - setupWithoutPreloadedChannels() { - this.getChannels().then(() => { - this.subscribeToChannelMessageBus(); - }); - } - - subscribeToChannelMessageBus() { - this._subscribeToNewChannelUpdates(); - this._subscribeToUserTrackingChannel(); - this._subscribeToChannelEdits(); - this._subscribeToChannelMetadata(); - this._subscribeToChannelStatusChange(); + [...channels.public_channels, ...channels.direct_message_channels].forEach( + (channelObject) => { + const channel = this.chatChannelsManager.store(channelObject); + return this.chatChannelsManager.follow(channel); + } + ); } willDestroy() { super.willDestroy(...arguments); if (this.userCanChat) { - this.set("allChannels", null); - this._unsubscribeFromNewDmChannelUpdates(); - this._unsubscribeFromUserTrackingChannel(); - this._unsubscribeFromChannelEdits(); - this._unsubscribeFromChannelMetadata(); - this._unsubscribeFromChannelStatusChange(); - this._unsubscribeFromAllChatChannels(); + this.chatSubscriptionsManager.stopChannelsSubscriptions(); } } @@ -186,10 +155,7 @@ export default class Chat extends Service { return; } - if ( - this.chatStateManager.isFullPageActive || - this.chatStateManager.isDrawerActive - ) { + if (this.chatStateManager.isActive) { this.presenceChannel.enter({ activeOptions: CHAT_ONLINE_OPTIONS }); } else { this.presenceChannel.leave(); @@ -199,61 +165,10 @@ export default class Chat extends Service { getDocumentTitleCount() { return this.chatNotificationManager.shouldCountChatInDocTitle() - ? this.unreadUrgentCount + ? this.chatChannelsManager.unreadUrgentCount : 0; } - _channelObject() { - return { - publicChannels: this.publicChannels, - directMessageChannels: this.directMessageChannels, - }; - } - - truncateDirectMessageChannels(channels) { - return channels.slice(0, this.directMessagesLimit); - } - - async getChannelsWithFilter(filter, opts = { excludeActiveChannel: true }) { - let sortedChannels = this.allChannels.sort((a, b) => { - return new Date(a.last_message_sent_at) > new Date(b.last_message_sent_at) - ? -1 - : 1; - }); - - const trimmedFilter = filter.trim(); - const lowerCasedFilter = filter.toLowerCase(); - const { activeChannel } = this; - - return sortedChannels.filter((channel) => { - if ( - opts.excludeActiveChannel && - activeChannel && - activeChannel.id === channel.id - ) { - return false; - } - if (!trimmedFilter.length) { - return true; - } - - if (channel.isDirectMessageChannel) { - let userFound = false; - channel.chatable.users.forEach((user) => { - if ( - user.username.toLowerCase().includes(lowerCasedFilter) || - user.name?.toLowerCase().includes(lowerCasedFilter) - ) { - return (userFound = true); - } - }); - return userFound; - } else { - return channel.title.toLowerCase().includes(lowerCasedFilter); - } - }); - } - switchChannelUpOrDown(direction) { const { activeChannel } = this; if (!activeChannel) { @@ -262,15 +177,11 @@ export default class Chat extends Service { let currentList, otherList; if (activeChannel.isDirectMessageChannel) { - currentList = this.truncateDirectMessageChannels( - this.directMessageChannels - ); - otherList = this.publicChannels; + currentList = this.chatChannelsManager.truncatedDirectMessageChannels; + otherList = this.chatChannelsManager.publicMessageChannels; } else { - currentList = this.publicChannels; - otherList = this.truncateDirectMessageChannels( - this.directMessageChannels - ); + currentList = this.chatChannelsManager.publicMessageChannels; + otherList = this.chatChannelsManager.truncatedDirectMessageChannels; } const directionUp = direction === "up"; @@ -296,109 +207,6 @@ export default class Chat extends Service { } } - getChannels() { - return new Promise((resolve) => { - if (this.hasFetchedChannels) { - return resolve(this._channelObject()); - } - - if (!this._fetchingChannels) { - this._fetchingChannels = this._refreshChannels(); - } - - this._fetchingChannels - .then(() => resolve(this._channelObject())) - .finally(() => (this._fetchingChannels = null)); - }); - } - - forceRefreshChannels() { - this.set("hasFetchedChannels", false); - this._unsubscribeFromAllChatChannels(); - return this.getChannels(); - } - - refreshTrackingState() { - if (!this.currentUser) { - return; - } - - return ajax("/chat/chat_channels.json") - .then((response) => { - this.currentUser.set("chat_channel_tracking_state", {}); - (response.direct_message_channels || []).forEach((channel) => { - this._updateUserTrackingState(channel); - }); - (response.public_channels || []).forEach((channel) => { - this._updateUserTrackingState(channel); - }); - }) - .finally(() => { - this.userChatChannelTrackingStateChanged(); - }); - } - - _refreshChannels() { - return new Promise((resolve) => { - this.setProperties({ - loading: true, - allChannels: [], - }); - this.currentUser.set("chat_channel_tracking_state", {}); - ajax("/chat/chat_channels.json").then((channels) => { - this._processChannels(channels); - this.userChatChannelTrackingStateChanged(); - this.appEvents.trigger("chat:refresh-channels"); - resolve(this._channelObject()); - }); - }); - } - - _processChannels(channels) { - // Must be set first because `processChannels` relies on this data. - this.set("messageBusLastIds", channels.message_bus_last_ids); - this.setProperties({ - publicChannels: A( - this.sortPublicChannels( - (channels.public_channels || []).map((channel) => - this.processChannel(channel) - ) - ) - ), - directMessageChannels: A( - this.sortDirectMessageChannels( - (channels.direct_message_channels || []).map((channel) => - this.processChannel(channel) - ) - ) - ), - hasFetchedChannels: true, - loading: false, - }); - const idToTitleMap = {}; - this.allChannels.forEach((c) => { - idToTitleMap[c.id] = c.title; - }); - this.set("idToTitleMap", idToTitleMap); - this.presenceChannel.subscribe(channels.global_presence_channel_state); - } - - reSortDirectMessageChannels() { - this.set( - "directMessageChannels", - this.sortDirectMessageChannels(this.directMessageChannels) - ); - } - - async getChannelBy(key, value) { - return this.getChannels().then(() => { - if (!isNaN(value)) { - value = parseInt(value, 10); - } - return (this.allChannels || []).findBy(key, value); - }); - } - searchPossibleDirectMessageUsers(options) { // TODO: implement a chat specific user search function return userSearch(options); @@ -414,99 +222,54 @@ export default class Chat extends Service { // if that is present and in the list of channels the user can access. // If none of these options exist, then we get the first public channel, // or failing that the first DM channel. - return this.getChannels().then(() => { - // Defined in order of significance. - let publicChannelWithMention, - dmChannelWithUnread, - publicChannelWithUnread, - publicChannel, - dmChannel, - defaultChannel; + // Defined in order of significance. + let publicChannelWithMention, + dmChannelWithUnread, + publicChannelWithUnread, + publicChannel, + dmChannel, + defaultChannel; - for (const [channel, state] of Object.entries( - this.currentUser.chat_channel_tracking_state - )) { - if (state.chatable_type === CHATABLE_TYPES.directMessageChannel) { - if (!dmChannelWithUnread && state.unread_count > 0) { - dmChannelWithUnread = channel; - } else if (!dmChannel) { - dmChannel = channel; - } - } else { - if (state.unread_mentions > 0) { - publicChannelWithMention = channel; - break; // <- We have a public channel with a mention. Break and return this. - } else if (!publicChannelWithUnread && state.unread_count > 0) { - publicChannelWithUnread = channel; - } else if ( - !defaultChannel && - parseInt(this.siteSettings.chat_default_channel_id || 0, 10) === - parseInt(channel, 10) - ) { - defaultChannel = channel; - } else if (!publicChannel) { - publicChannel = channel; - } + this.chatChannelsManager.channels.forEach((channel) => { + const membership = channel.currentUserMembership; + + if (channel.isDirectMessageChannel) { + if (!dmChannelWithUnread && membership.unread_count > 0) { + dmChannelWithUnread = channel.id; + } else if (!dmChannel) { + dmChannel = channel.id; + } + } else { + if (membership.unread_mentions > 0) { + publicChannelWithMention = channel.id; + return; // <- We have a public channel with a mention. Break and return this. + } else if (!publicChannelWithUnread && membership.unread_count > 0) { + publicChannelWithUnread = channel.id; + } else if ( + !defaultChannel && + parseInt(this.siteSettings.chat_default_channel_id || 0, 10) === + channel.id + ) { + defaultChannel = channel.id; + } else if (!publicChannel) { + publicChannel = channel.id; } } - return ( - publicChannelWithMention || - dmChannelWithUnread || - publicChannelWithUnread || - defaultChannel || - publicChannel || - dmChannel - ); }); - } - sortPublicChannels(channels) { - return channels.sort((a, b) => a.title.localeCompare(b.title)); - } - - sortDirectMessageChannels(channels) { - return channels.sort((a, b) => { - const unreadCountA = - this.currentUser.chat_channel_tracking_state[a.id]?.unread_count || 0; - const unreadCountB = - this.currentUser.chat_channel_tracking_state[b.id]?.unread_count || 0; - if (unreadCountA === unreadCountB) { - return new Date(a.last_message_sent_at) > - new Date(b.last_message_sent_at) - ? -1 - : 1; - } else { - return unreadCountA > unreadCountB ? -1 : 1; - } - }); - } - - getIdealFirstChannelIdAndTitle() { - return this.getIdealFirstChannelId().then((channelId) => { - if (!channelId) { - return; - } - return { - id: channelId, - title: this.idToTitleMap[channelId], - }; - }); + return ( + publicChannelWithMention || + dmChannelWithUnread || + publicChannelWithUnread || + defaultChannel || + publicChannel || + dmChannel + ); } async openChannelAtMessage(channelId, messageId = null) { - let channel = await this.getChannelBy("id", channelId); - if (channel) { + return this.chatChannelsManager.find(channelId).then((channel) => { return this._openFoundChannelAtMessage(channel, messageId); - } - - return ajax(`/chat/chat_channels/${channelId}`).then((response) => { - const queryParams = messageId ? { messageId } : {}; - return this.router.transitionTo( - "chat.channel", - response.id, - slugifyChannel(response), - { queryParams } - ); }); } @@ -559,380 +322,18 @@ export default class Chat extends Service { this.appEvents.trigger("chat-live-pane:highlight-message", messageId); } - async startTrackingChannel(channel) { - if (!channel.current_user_membership.following) { - return; - } - - let existingChannel = await this.getChannelBy("id", channel.id); - if (existingChannel) { - return existingChannel; // User is already tracking this channel. return! - } - - const existingChannels = channel.isDirectMessageChannel - ? this.directMessageChannels - : this.publicChannels; - - // this check shouldn't be needed given the previous check to existingChannel - // this is a safety net, to ensure we never track duplicated channels - existingChannel = existingChannels.findBy("id", channel.id); - if (existingChannel) { - return existingChannel; - } - - const newChannel = this.processChannel(channel); - existingChannels.pushObject(newChannel); - this.currentUser.chat_channel_tracking_state[channel.id] = - EmberObject.create({ - unread_count: 1, - unread_mentions: 0, - chatable_type: channel.chatable_type, - }); - this.userChatChannelTrackingStateChanged(); - if (channel.isDirectMessageChannel) { - this.reSortDirectMessageChannels(); - } - if (channel.isPublicChannel) { - this.set("publicChannels", this.sortPublicChannels(this.publicChannels)); - } - this.appEvents.trigger("chat:refresh-channels"); - return newChannel; - } - - async stopTrackingChannel(channel) { - return this.getChannelBy("id", channel.id).then((existingChannel) => { - if (existingChannel) { - return this.forceRefreshChannels(); - } - }); - } - - _subscribeToChannelMetadata() { - this.messageBus.subscribe( - "/chat/channel-metadata", - this._onChannelMetadata, - this.messageBusLastIds.channel_metadata - ); - } - - _subscribeToChannelEdits() { - this.messageBus.subscribe( - "/chat/channel-edits", - this._onChannelEdits, - this.messageBusLastIds.channel_edits - ); - } - - _subscribeToChannelStatusChange() { - this.messageBus.subscribe("/chat/channel-status", this._onChannelStatus); - } - - _unsubscribeFromChannelStatusChange() { - this.messageBus.unsubscribe("/chat/channel-status", this._onChannelStatus); - } - - _unsubscribeFromChannelEdits() { - this.messageBus.unsubscribe("/chat/channel-edits", this._onChannelEdits); - } - - _unsubscribeFromChannelMetadata() { - this.messageBus.unsubscribe( - "/chat/channel-metadata", - this._onChannelMetadata - ); - } - - _subscribeToNewChannelUpdates() { - this.messageBus.subscribe( - "/chat/new-channel", - this._onNewChannel, - this.messageBusLastIds.new_channel - ); - } - - _unsubscribeFromNewDmChannelUpdates() { - this.messageBus.unsubscribe("/chat/new-channel", this._onNewChannel); - } - - _subscribeToSingleUpdateChannel(channel) { - if (channel.current_user_membership.muted) { - return; - } - - // We do this first so we don't multi-subscribe to mention + messages - // messageBus channels for this chat channel, since _subscribeToSingleUpdateChannel - // is called from multiple places. - this._unsubscribeFromChatChannel(channel); - - if (!channel.isDirectMessageChannel) { - this._subscribeToMentionChannel(channel); - } - - this._subscribeToNewMessagesChannel(channel); - } - - _subscribeToMentionChannel(channel) { - const onNewMentions = () => { - const trackingState = - this.currentUser.chat_channel_tracking_state[channel.id]; - - if (trackingState) { - const count = (trackingState.unread_mentions || 0) + 1; - trackingState.set("unread_mentions", count); - this.userChatChannelTrackingStateChanged(); - } - }; - - this._onNewMentionsCallbacks.set(channel.id, onNewMentions); - - this.messageBus.subscribe( - `/chat/${channel.id}/new-mentions`, - onNewMentions, - channel.message_bus_last_ids.new_mentions - ); - } - - _subscribeToNewMessagesChannel(channel) { - const onNewMessages = (busData) => { - const trackingState = - this.currentUser.chat_channel_tracking_state[channel.id]; - - if (busData.user_id === this.currentUser.id) { - // User sent message, update tracking state to no unread - trackingState.set("chat_message_id", busData.message_id); - } else { - // Ignored user sent message, update tracking state to no unread - if (this.currentUser.ignored_users.includes(busData.username)) { - trackingState.set("chat_message_id", busData.message_id); - } else { - // Message from other user. Increment trackings state - if (busData.message_id > (trackingState.chat_message_id || 0)) { - trackingState.set("unread_count", trackingState.unread_count + 1); - } - } - } - - this.userChatChannelTrackingStateChanged(); - channel.set("last_message_sent_at", new Date()); - - const directMessageChannel = (this.directMessageChannels || []).findBy( - "id", - parseInt(channel.id, 10) - ); - - if (directMessageChannel) { - this.reSortDirectMessageChannels(); - } - }; - - this._onNewMessagesCallbacks.set(channel.id, onNewMessages); - - this.messageBus.subscribe( - `/chat/${channel.id}/new-messages`, - onNewMessages, - channel.message_bus_last_ids.new_messages - ); - } - - @bind - _onChannelMetadata(busData) { - this.getChannelBy("id", busData.chat_channel_id).then((channel) => { - if (channel) { - channel.setProperties({ - memberships_count: busData.memberships_count, - }); - this.appEvents.trigger("chat:refresh-channel-members"); - } - }); - } - - @bind - _onChannelEdits(busData) { - this.getChannelBy("id", busData.chat_channel_id).then((channel) => { - if (channel) { - channel.setProperties({ - title: busData.name, - description: busData.description, - }); - } - }); - } - - @bind - _onChannelStatus(busData) { - this.getChannelBy("id", busData.chat_channel_id).then((channel) => { - if (!channel) { - return; - } - - channel.set("status", busData.status); - - // it is not possible for the user to set their last read message id - // if the channel has been archived, because all the messages have - // been deleted. we don't want them seeing the blue dot anymore so - // just completely reset the unreads - if (busData.status === CHANNEL_STATUSES.archived) { - this.currentUser.chat_channel_tracking_state[channel.id] = { - unread_count: 0, - unread_mentions: 0, - chatable_type: channel.chatable_type, - }; - this.userChatChannelTrackingStateChanged(); - } - - this.appEvents.trigger("chat:refresh-channel", channel.id); - }, this.messageBusLastIds.channel_status); - } - - @bind - _onNewChannel(busData) { - this.startTrackingChannel(ChatChannel.create(busData.chat_channel)); - } - async followChannel(channel) { - return ChatApi.followChatChannel(channel).then(() => { - this.startTrackingChannel(channel); - this._subscribeToSingleUpdateChannel(channel); - }); + return this.chatChannelsManager.follow(channel); } async unfollowChannel(channel) { - return ChatApi.unfollowChatChannel(channel).then(() => { - this._unsubscribeFromChatChannel(channel); - this.stopTrackingChannel(channel); - + return this.chatChannelsManager.unfollow(channel).then(() => { if (channel === this.activeChannel && channel.isDirectMessageChannel) { this.router.transitionTo("chat"); } }); } - _unsubscribeFromAllChatChannels() { - (this.allChannels || []).forEach((channel) => { - this._unsubscribeFromChatChannel(channel); - }); - } - - _unsubscribeFromChatChannel(channel) { - this.messageBus.unsubscribe("/chat/*", this._onNewMessagesCallbacks); - if (!channel.isDirectMessageChannel) { - this.messageBus.unsubscribe("/chat/*", this._onNewMentionsCallbacks); - } - } - - _subscribeToUserTrackingChannel() { - this.messageBus.subscribe( - `/chat/user-tracking-state/${this.currentUser.id}`, - this._onUserTrackingState, - this.messageBusLastIds.user_tracking_state - ); - } - - _unsubscribeFromUserTrackingChannel() { - this.messageBus.unsubscribe( - `/chat/user-tracking-state/${this.currentUser.id}`, - this._onUserTrackingState - ); - } - - @bind - _onUserTrackingState(busData, _, messageId) { - const lastId = this.lastUserTrackingMessageId; - - // we don't want this state to go backwards, only catch - // up if messages from messagebus were missed - if (!lastId || messageId > lastId) { - this.lastUserTrackingMessageId = messageId; - } - - // we are too far out of sync, we should resync everything. - // this will trigger a route transition and blur the chat input - if (lastId && messageId > lastId + 1) { - return this.forceRefreshChannels(); - } - - const trackingState = - this.currentUser.chat_channel_tracking_state[busData.chat_channel_id]; - - if (trackingState) { - trackingState.set("chat_message_id", busData.chat_message_id); - trackingState.set("unread_count", 0); - trackingState.set("unread_mentions", 0); - this.userChatChannelTrackingStateChanged(); - } - } - - resetTrackingStateForChannel(channelId) { - const trackingState = - this.currentUser.chat_channel_tracking_state[channelId]; - if (trackingState) { - trackingState.set("unread_count", 0); - this.userChatChannelTrackingStateChanged(); - } - } - - userChatChannelTrackingStateChanged() { - this._recalculateUnreadMessages(); - this.appEvents.trigger("chat:user-tracking-state-changed"); - } - - _recalculateUnreadMessages() { - let unreadPublicCount = 0; - let unreadUrgentCount = 0; - let headerNeedsRerender = false; - - Object.values(this.currentUser.chat_channel_tracking_state).forEach( - (state) => { - if (state.muted) { - return; - } - - if (state.chatable_type === CHATABLE_TYPES.directMessageChannel) { - unreadUrgentCount += state.unread_count || 0; - } else { - unreadUrgentCount += state.unread_mentions || 0; - unreadPublicCount += state.unread_count || 0; - } - } - ); - - let hasUnreadPublic = unreadPublicCount > 0; - if (hasUnreadPublic !== this.hasUnreadMessages) { - headerNeedsRerender = true; - this.set("hasUnreadMessages", hasUnreadPublic); - } - - if (unreadUrgentCount !== this.unreadUrgentCount) { - headerNeedsRerender = true; - this.set("unreadUrgentCount", unreadUrgentCount); - } - - this.currentUser.notifyPropertyChange("chat_channel_tracking_state"); - if (headerNeedsRerender) { - this.appEvents.trigger("chat:rerender-header"); - this.appEvents.trigger("notifications:changed"); - } - } - - processChannel(channel) { - channel = ChatChannel.create(channel); - this._subscribeToSingleUpdateChannel(channel); - this._updateUserTrackingState(channel); - this.allChannels.push(channel); - return channel; - } - - _updateUserTrackingState(channel) { - this.currentUser.chat_channel_tracking_state[channel.id] = - EmberObject.create({ - chatable_type: channel.chatable_type, - muted: channel.current_user_membership.muted, - unread_count: channel.current_user_membership.unread_count, - unread_mentions: channel.current_user_membership.unread_mentions, - chat_message_id: channel.current_user_membership.last_read_message_id, - }); - } - upsertDmChannelForUser(channel, user) { const usernames = (channel.chatable.users || []) .mapBy("username") @@ -951,9 +352,9 @@ export default class Chat extends Service { data: { usernames: usernames.uniq() }, }) .then((response) => { - const chatChannel = ChatChannel.create(response.chat_channel); - this.startTrackingChannel(chatChannel); - return chatChannel; + const channel = this.chatChannelsManager.store(response.channel); + this.chatChannelsManager.follow(channel); + return channel; }) .catch(popupAjaxError); } @@ -1031,17 +432,8 @@ export default class Chat extends Service { 10 ); - const hasUnreadMessages = latestUnreadMsgId > channel.lastSendReadMessageId; - - if ( - !hasUnreadMessages && - this.currentUser.chat_channel_tracking_state[this.activeChannel.id] - ?.unread_count > 0 - ) { - // Weird state here where the chat_channel_tracking_state is wrong. Need to reset it. - this.resetTrackingStateForChannel(this.activeChannel.id); - } - + const hasUnreadMessages = + latestUnreadMsgId > channel.currentUserMembership.last_read_message_id; if (hasUnreadMessages) { channel.updateLastReadMessage(latestUnreadMsgId); } diff --git a/plugins/chat/assets/javascripts/discourse/templates/chat-channel-index.hbs b/plugins/chat/assets/javascripts/discourse/templates/chat-channel-index.hbs index dc261f00413..89a38629ad6 100644 --- a/plugins/chat/assets/javascripts/discourse/templates/chat-channel-index.hbs +++ b/plugins/chat/assets/javascripts/discourse/templates/chat-channel-index.hbs @@ -1 +1 @@ - + diff --git a/plugins/chat/assets/javascripts/discourse/templates/chat-channel-info-about.hbs b/plugins/chat/assets/javascripts/discourse/templates/chat-channel-info-about.hbs index 5211ae3ff72..a9cb4f67d41 100644 --- a/plugins/chat/assets/javascripts/discourse/templates/chat-channel-info-about.hbs +++ b/plugins/chat/assets/javascripts/discourse/templates/chat-channel-info-about.hbs @@ -1 +1,5 @@ - + diff --git a/plugins/chat/assets/javascripts/discourse/templates/chat-channel-info-members.hbs b/plugins/chat/assets/javascripts/discourse/templates/chat-channel-info-members.hbs index b6e654e73d6..e80afabd3ba 100644 --- a/plugins/chat/assets/javascripts/discourse/templates/chat-channel-info-members.hbs +++ b/plugins/chat/assets/javascripts/discourse/templates/chat-channel-info-members.hbs @@ -1 +1 @@ - + diff --git a/plugins/chat/assets/javascripts/discourse/templates/chat-channel-info-settings.hbs b/plugins/chat/assets/javascripts/discourse/templates/chat-channel-info-settings.hbs index d267701835a..a30950bc79e 100644 --- a/plugins/chat/assets/javascripts/discourse/templates/chat-channel-info-settings.hbs +++ b/plugins/chat/assets/javascripts/discourse/templates/chat-channel-info-settings.hbs @@ -1 +1 @@ - + diff --git a/plugins/chat/assets/javascripts/discourse/templates/chat-channel-info.hbs b/plugins/chat/assets/javascripts/discourse/templates/chat-channel-info.hbs index e7e0ba3aacb..fdb757c23c3 100644 --- a/plugins/chat/assets/javascripts/discourse/templates/chat-channel-info.hbs +++ b/plugins/chat/assets/javascripts/discourse/templates/chat-channel-info.hbs @@ -3,16 +3,17 @@
{{#if this.chatChannelInfoRouteOriginManager.isBrowse}} - + {{d-icon "chevron-left"}} {{else}} @@ -21,7 +22,7 @@ {{/if}}
- +
@@ -35,16 +36,13 @@ > {{i18n (concat "chat.channel_info.tabs." tab)}} {{#if (eq tab "members")}} - ({{this.model.chatChannel.membershipsCount}}) + ({{this.model.membershipsCount}}) {{/if}} diff --git a/plugins/chat/assets/javascripts/discourse/templates/modal/create-channel.hbs b/plugins/chat/assets/javascripts/discourse/templates/modal/create-channel.hbs index e39cc9a9d59..14199193dec 100644 --- a/plugins/chat/assets/javascripts/discourse/templates/modal/create-channel.hbs +++ b/plugins/chat/assets/javascripts/discourse/templates/modal/create-channel.hbs @@ -17,15 +17,15 @@ {{/if}} -