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:
parent
23738541da
commit
6b3a68e562
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue