From 6b3a68e5625d7c871d545f1a3c75cca19e8ff563 Mon Sep 17 00:00:00 2001 From: David Battersby Date: Mon, 29 Jan 2024 10:38:14 +0800 Subject: [PATCH] FEATURE: Mobile Chat Notification Badges (#25438) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This change adds notification badges to the new footer tabs on mobile chat, to help users easily find areas where there’s new activity to review. When on mobile chat: - Show a badge on the DMs footer when there is unread activity in DMs. - Show a badge on the Channels footer tab when there is unread channel activity. - Show a badge on the Threads footer tab when there is unread activity in a followed thread. - Notification badges should be removed once the unread activity is viewed. Additionally this change will: - Show green notification badges for channel mentions or DMs - Show blue notification badges for unread messages in channels or threads Co-authored-by: chapoi <101828855+chapoi@users.noreply.github.com> --- .../discourse/components/chat-footer.gjs | 17 ++++- .../chat/footer/unread-indicator.gjs | 70 +++++++++++++++++++ .../services/chat-tracking-state-manager.js | 52 +++++++------- .../stylesheets/common/base-common.scss | 37 +++++----- .../stylesheets/mobile/base-mobile.scss | 15 ++-- .../stylesheets/mobile/chat-footer.scss | 14 ++++ plugins/chat/spec/system/chat_footer_spec.rb | 62 +++++++++++++++- 7 files changed, 208 insertions(+), 59 deletions(-) create mode 100644 plugins/chat/assets/javascripts/discourse/components/chat/footer/unread-indicator.gjs diff --git a/plugins/chat/assets/javascripts/discourse/components/chat-footer.gjs b/plugins/chat/assets/javascripts/discourse/components/chat-footer.gjs index fdd98e4b6a4..fb43022b311 100644 --- a/plugins/chat/assets/javascripts/discourse/components/chat-footer.gjs +++ b/plugins/chat/assets/javascripts/discourse/components/chat-footer.gjs @@ -4,6 +4,11 @@ import DButton from "discourse/components/d-button"; import concatClass from "discourse/helpers/concat-class"; import i18n from "discourse-common/helpers/i18n"; import eq from "truth-helpers/helpers/eq"; +import { + UnreadChannelsIndicator, + UnreadDirectMessagesIndicator, + UnreadThreadsIndicator, +} from "discourse/plugins/chat/discourse/components/chat/footer/unread-indicator"; export default class ChatFooter extends Component { @service router; @@ -34,7 +39,9 @@ export default class ChatFooter extends Component { "c-footer__item" (if (eq this.router.currentRouteName "chat.channels") "--active") }} - /> + > + + {{#if this.directMessagesEnabled}} + > + + {{/if}} {{#if this.threadsEnabled}} @@ -66,7 +75,9 @@ export default class ChatFooter extends Component { "c-footer__item" (if (eq this.router.currentRouteName "chat.threads") "--active") }} - /> + > + + {{/if}} {{/if}} diff --git a/plugins/chat/assets/javascripts/discourse/components/chat/footer/unread-indicator.gjs b/plugins/chat/assets/javascripts/discourse/components/chat/footer/unread-indicator.gjs new file mode 100644 index 00000000000..beba2e1602e --- /dev/null +++ b/plugins/chat/assets/javascripts/discourse/components/chat/footer/unread-indicator.gjs @@ -0,0 +1,70 @@ +import Component from "@glimmer/component"; +import { inject as service } from "@ember/service"; + +const CHANNELS_TAB = "channels"; +const DMS_TAB = "dms"; +const THREADS_TAB = "threads"; +const MAX_UNREAD_COUNT = 99; + +export const UnreadChannelsIndicator = ; + +export const UnreadDirectMessagesIndicator = ; + +export const UnreadThreadsIndicator = ; + +export default class FooterUnreadIndicator extends Component { + @service chatTrackingStateManager; + + badgeType = this.args.badgeType; + + get urgentCount() { + if (this.badgeType === CHANNELS_TAB) { + return this.chatTrackingStateManager.publicChannelMentionCount; + } else if (this.badgeType === DMS_TAB) { + return this.chatTrackingStateManager.directMessageUnreadCount; + } else { + return 0; + } + } + + get unreadCount() { + if (this.badgeType === CHANNELS_TAB) { + return this.chatTrackingStateManager.publicChannelUnreadCount; + } else if (this.badgeType === THREADS_TAB) { + return this.chatTrackingStateManager.hasUnreadThreads ? 1 : 0; + } else { + return 0; + } + } + + get showUrgent() { + return this.urgentCount > 0; + } + + get showUnread() { + return this.unreadCount > 0; + } + + get urgentBadgeCount() { + let totalCount = this.urgentCount; + return totalCount > MAX_UNREAD_COUNT ? `${MAX_UNREAD_COUNT}+` : totalCount; + } + + +} diff --git a/plugins/chat/assets/javascripts/discourse/services/chat-tracking-state-manager.js b/plugins/chat/assets/javascripts/discourse/services/chat-tracking-state-manager.js index af885a8b06d..6dac021cc89 100644 --- a/plugins/chat/assets/javascripts/discourse/services/chat-tracking-state-manager.js +++ b/plugins/chat/assets/javascripts/discourse/services/chat-tracking-state-manager.js @@ -47,40 +47,36 @@ export default class ChatTrackingStateManager extends Service { }, 0); } + get directMessageUnreadCount() { + return this.#directMessageChannels.reduce((unreadCount, channel) => { + return unreadCount + channel.tracking.unreadCount; + }, 0); + } + + get publicChannelMentionCount() { + return this.#publicChannels.reduce((mentionCount, channel) => { + return mentionCount + channel.tracking.mentionCount; + }, 0); + } + + get directMessageMentionCount() { + return this.#directMessageChannels.reduce((dmMentionCount, channel) => { + return dmMentionCount + channel.tracking.mentionCount; + }, 0); + } + get allChannelMentionCount() { - let totalPublicMentions = this.#publicChannels.reduce( - (channelMentionCount, channel) => { - return channelMentionCount + channel.tracking.mentionCount; - }, - 0 - ); - - let totalPrivateMentions = this.#directMessageChannels.reduce( - (dmMentionCount, channel) => { - return dmMentionCount + channel.tracking.mentionCount; - }, - 0 - ); - - return totalPublicMentions + totalPrivateMentions; + return this.publicChannelMentionCount + this.directMessageMentionCount; } get allChannelUrgentCount() { - let publicChannelMentionCount = this.#publicChannels.reduce( - (mentionCount, channel) => { - return mentionCount + channel.tracking.mentionCount; - }, - 0 - ); + return this.publicChannelMentionCount + this.directMessageUnreadCount; + } - let dmChannelUnreadCount = this.#directMessageChannels.reduce( - (unreadCount, channel) => { - return unreadCount + channel.tracking.unreadCount; - }, - 0 + get hasUnreadThreads() { + return this.#publicChannels.some( + (channel) => channel.unreadThreadsCount > 0 ); - - return publicChannelMentionCount + dmChannelUnreadCount; } willDestroy() { diff --git a/plugins/chat/assets/stylesheets/common/base-common.scss b/plugins/chat/assets/stylesheets/common/base-common.scss index 681d9c608c4..8373aa4fe0b 100644 --- a/plugins/chat/assets/stylesheets/common/base-common.scss +++ b/plugins/chat/assets/stylesheets/common/base-common.scss @@ -64,26 +64,25 @@ html.ios-device.keyboard-visible body #main-outlet .full-page-chat { } } -.header-dropdown-toggle.chat-header-icon { - .icon { - .chat-channel-unread-indicator { - @include chat-unread-indicator; - border: 2px solid var(--header_background); - position: absolute; - top: 0; - right: 2px; +.header-dropdown-toggle.chat-header-icon .icon, +.c-footer .c-footer__item { + .chat-channel-unread-indicator { + @include chat-unread-indicator; + border: 2px solid var(--header_background); + position: absolute; + top: 0; + right: 2px; - &.-urgent { - display: flex; - align-items: center; - justify-content: center; - width: auto; - height: 1em; - min-width: 0.6em; - padding: 0.21em 0.42em; - top: -1px; - right: 0; - } + &.-urgent { + display: flex; + align-items: center; + justify-content: center; + width: auto; + height: 1em; + min-width: 0.6em; + padding: 0.21em 0.42em; + top: -1px; + right: 0; } } diff --git a/plugins/chat/assets/stylesheets/mobile/base-mobile.scss b/plugins/chat/assets/stylesheets/mobile/base-mobile.scss index c87ef20739c..2750bcffc85 100644 --- a/plugins/chat/assets/stylesheets/mobile/base-mobile.scss +++ b/plugins/chat/assets/stylesheets/mobile/base-mobile.scss @@ -72,13 +72,12 @@ html.has-full-page-chat { margin-left: 0; } -.header-dropdown-toggle.chat-header-icon { - .icon { - &.active .d-icon { - color: var(--primary-medium); - } - .chat-channel-unread-indicator { - border-color: var(--primary-very-low); - } +.header-dropdown-toggle.chat-header-icon .icon, +.c-footer .c-footer__item { + &.active .d-icon { + color: var(--primary-medium); + } + .chat-channel-unread-indicator { + border-color: var(--primary-very-low); } } diff --git a/plugins/chat/assets/stylesheets/mobile/chat-footer.scss b/plugins/chat/assets/stylesheets/mobile/chat-footer.scss index 91e319d3142..f95e342bd29 100644 --- a/plugins/chat/assets/stylesheets/mobile/chat-footer.scss +++ b/plugins/chat/assets/stylesheets/mobile/chat-footer.scss @@ -30,6 +30,7 @@ flex-shrink: 0; padding-block: 0.75rem; height: 100%; + position: relative; &.--active { .d-icon, @@ -52,6 +53,19 @@ font-size: var(--font-down-1-rem); color: var(--primary-medium); } + + .chat-channel-unread-indicator, + .chat-channel-unread-indicator.-urgent { + top: 0.25rem; + right: unset; + left: 50%; + margin-left: 0.75rem; + } + + .chat-channel-unread-indicator:not(.-urgent) { + width: 11px; + height: 11px; + } } } } diff --git a/plugins/chat/spec/system/chat_footer_spec.rb b/plugins/chat/spec/system/chat_footer_spec.rb index b693071f03c..c2ff04fe284 100644 --- a/plugins/chat/spec/system/chat_footer_spec.rb +++ b/plugins/chat/spec/system/chat_footer_spec.rb @@ -1,7 +1,8 @@ # frozen_string_literal: true -RSpec.describe "Chat footer on mobile", type: :system, mobile: true do +RSpec.describe "Mobile Chat footer", type: :system, mobile: true do fab!(:user) + fab!(:user_2) { Fabricate(:user) } fab!(:channel) { Fabricate(:chat_channel, threading_enabled: true) } fab!(:message) { Fabricate(:chat_message, chat_channel: channel, user: user) } let(:chat_page) { PageObjects::Pages::Chat.new } @@ -10,6 +11,7 @@ RSpec.describe "Chat footer on mobile", type: :system, mobile: true do chat_system_bootstrap sign_in(user) channel.add(user) + channel.add(user_2) end context "with multiple tabs" do @@ -69,4 +71,62 @@ RSpec.describe "Chat footer on mobile", type: :system, mobile: true do expect(page).to have_current_path("/chat/channels") end end + + describe "badges" do + context "for channels" do + it "is unread for messages" do + Fabricate(:chat_message, chat_channel: channel) + + visit("/") + chat_page.open_from_header + + expect(page).to have_css("#c-footer-channels .chat-channel-unread-indicator") + end + + it "is urgent for mentions" do + Jobs.run_immediately! + + visit("/") + chat_page.open_from_header + + Fabricate( + :chat_message_with_service, + chat_channel: channel, + message: "hello @#{user.username}", + user: user_2, + ) + + expect(page).to have_css( + "#c-footer-channels .chat-channel-unread-indicator.-urgent", + text: "1", + ) + end + end + + context "for direct messages" do + fab!(:dm_channel) { Fabricate(:direct_message_channel, users: [user]) } + fab!(:dm_message) { Fabricate(:chat_message, chat_channel: dm_channel) } + + it "is urgent" do + visit("/") + chat_page.open_from_header + + expect(page).to have_css("#c-footer-direct-messages .chat-channel-unread-indicator.-urgent") + end + end + + context "for threads" do + fab!(:thread) { Fabricate(:chat_thread, channel: channel, original_message: message) } + fab!(:thread_message) { Fabricate(:chat_message, chat_channel: channel, thread: thread) } + + it "is unread" do + SiteSetting.chat_threads_enabled = true + + visit("/") + chat_page.open_from_header + + expect(page).to have_css("#c-footer-threads .chat-channel-unread-indicator") + end + end + end end