FEATURE: Mobile Chat Notification Badges (#25438)

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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