FEATURE: Mobile Chat Notification Badges ()

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>
This commit is contained in:
David Battersby 2024-01-29 10:38:14 +08:00 committed by GitHub
parent 23738541da
commit 6b3a68e562
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 208 additions and 59 deletions
plugins/chat
assets
spec/system

@ -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")
}}
/>
>
<UnreadChannelsIndicator />
</DButton>
{{#if this.directMessagesEnabled}}
<DButton
@ -51,7 +58,9 @@ export default class ChatFooter extends Component {
"--active"
)
}}
/>
>
<UnreadDirectMessagesIndicator />
</DButton>
{{/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")
}}
/>
>
<UnreadThreadsIndicator />
</DButton>
{{/if}}
</nav>
{{/if}}

@ -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 = <template>
<FooterUnreadIndicator @badgeType={{CHANNELS_TAB}} />
</template>;
export const UnreadDirectMessagesIndicator = <template>
<FooterUnreadIndicator @badgeType={{DMS_TAB}} />
</template>;
export const UnreadThreadsIndicator = <template>
<FooterUnreadIndicator @badgeType={{THREADS_TAB}} />
</template>;
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;
}
<template>
{{#if this.showUrgent}}
<div class="chat-channel-unread-indicator -urgent">
<div class="chat-channel-unread-indicator__number">
{{this.urgentBadgeCount}}
</div>
</div>
{{else if this.showUnread}}
<div class="chat-channel-unread-indicator"></div>
{{/if}}
</template>
}

@ -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() {

@ -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;
}
}

@ -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);
}
}

@ -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;
}
}
}
}

@ -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