UX: chat drawer increase unread channel visibility (#28731)

This change increases the visibility of unread channels to make them stand out more in drawer mode (desktop).

When a channel is unread:

- it floats to the top;
- when multiple channels are unread, they are sorted alphabetically (equal to how it’s done on mobile)
- the unread indicator blue dot moves to directly right of the channel name
This commit is contained in:
David Battersby
2024-09-05 13:36:50 +04:00
committed by GitHub
parent 67ce50c141
commit e991574389
12 changed files with 95 additions and 90 deletions

View File

@ -5,10 +5,15 @@ import { htmlSafe } from "@ember/template";
import PluginOutlet from "discourse/components/plugin-outlet"; import PluginOutlet from "discourse/components/plugin-outlet";
import UserStatusMessage from "discourse/components/user-status-message"; import UserStatusMessage from "discourse/components/user-status-message";
import replaceEmoji from "discourse/helpers/replace-emoji"; import replaceEmoji from "discourse/helpers/replace-emoji";
import ChatChannelUnreadIndicator from "../chat-channel-unread-indicator";
export default class ChatChannelName extends Component { export default class ChatChannelName extends Component {
@service currentUser; @service currentUser;
get unreadIndicator() {
return this.args.unreadIndicator ?? false;
}
get firstUser() { get firstUser() {
return this.args.channel.chatable.users[0]; return this.args.channel.chatable.users[0];
} }
@ -37,27 +42,43 @@ export default class ChatChannelName extends Component {
} }
get showUserStatus() { get showUserStatus() {
if (!this.args.channel.isDirectMessageChannel) {
return false;
}
return !!(this.users.length === 1 && this.users[0].status); return !!(this.users.length === 1 && this.users[0].status);
} }
get channelTitle() {
if (this.args.channel.isDirectMessageChannel) {
return this.groupDirectMessage
? this.groupsDirectMessageTitle
: this.firstUser.username;
}
return this.args.channel.title;
}
get showPluginOutlet() {
return this.args.channel.isDirectMessageChannel && !this.groupDirectMessage;
}
<template> <template>
<div class="chat-channel-name"> <div class="chat-channel-name">
{{#if @channel.isDirectMessageChannel}} <div class="chat-channel-name__label">
{{#if this.groupDirectMessage}} {{replaceEmoji this.channelTitle}}
<span class="chat-channel-name__label">
{{this.groupsDirectMessageTitle}} {{#if this.unreadIndicator}}
</span> <ChatChannelUnreadIndicator @channel={{@channel}} />
{{else}} {{/if}}
<span class="chat-channel-name__label">
{{this.firstUser.username}} {{#if this.showUserStatus}}
</span> <UserStatusMessage
{{#if this.showUserStatus}} @status={{get this.users "0.status"}}
<UserStatusMessage @showDescription={{if this.site.mobileView "true"}}
@status={{get this.users "0.status"}} class="chat-channel__user-status-message"
@showDescription={{if this.site.mobileView "true"}} />
class="chat-channel__user-status-message" {{/if}}
/>
{{/if}} {{#if this.showPluginOutlet}}
<PluginOutlet <PluginOutlet
@name="after-chat-channel-username" @name="after-chat-channel-username"
@outletArgs={{hash user=@user}} @outletArgs={{hash user=@user}}
@ -65,15 +86,11 @@ export default class ChatChannelName extends Component {
@connectorTagName="" @connectorTagName=""
/> />
{{/if}} {{/if}}
{{else if @channel.isCategoryChannel}}
<span class="chat-channel-name__label">
{{replaceEmoji @channel.title}}
</span>
{{#if (has-block)}} {{#if (has-block)}}
{{yield}} {{yield}}
{{/if}} {{/if}}
{{/if}} </div>
</div> </div>
</template> </template>
} }

View File

@ -3,7 +3,7 @@ import ChannelIcon from "discourse/plugins/chat/discourse/components/channel-ico
import ChannelName from "discourse/plugins/chat/discourse/components/channel-name"; import ChannelName from "discourse/plugins/chat/discourse/components/channel-name";
const ChatChannelTitle = <template> const ChatChannelTitle = <template>
<span <div
class={{concatClass class={{concatClass
"chat-channel-title" "chat-channel-title"
(if @channel.isDirectMessageChannel "is-dm" "is-category") (if @channel.isDirectMessageChannel "is-dm" "is-category")
@ -12,10 +12,14 @@ const ChatChannelTitle = <template>
<ChannelIcon @channel={{@channel}} /> <ChannelIcon @channel={{@channel}} />
<ChannelName @channel={{@channel}} /> <ChannelName @channel={{@channel}} />
{{#if @isUnread}}
<div class="unread-indicator {{if @isUrgent '-urgent'}}"></div>
{{/if}}
{{#if (has-block)}} {{#if (has-block)}}
{{yield}} {{yield}}
{{/if}} {{/if}}
</span> </div>
</template>; </template>;
export default ChatChannelTitle; export default ChatChannelTitle;

View File

@ -1,12 +1,7 @@
import Component from "@glimmer/component"; import Component from "@glimmer/component";
import I18n from "discourse-i18n"; import I18n from "discourse-i18n";
import ChatChannelUnreadIndicator from "./chat-channel-unread-indicator";
export default class ChatChannelMetadata extends Component { export default class ChatChannelMetadata extends Component {
get unreadIndicator() {
return this.args.unreadIndicator ?? false;
}
get lastMessageFormattedDate() { get lastMessageFormattedDate() {
return moment(this.args.channel.lastMessage.createdAt).calendar(null, { return moment(this.args.channel.lastMessage.createdAt).calendar(null, {
sameDay: "LT", sameDay: "LT",
@ -23,10 +18,6 @@ export default class ChatChannelMetadata extends Component {
{{this.lastMessageFormattedDate}} {{this.lastMessageFormattedDate}}
</div> </div>
{{/if}} {{/if}}
{{#if this.unreadIndicator}}
<ChatChannelUnreadIndicator @channel={{@channel}} />
{{/if}}
</div> </div>
</template> </template>
} }

View File

@ -196,11 +196,8 @@ export default class ChatChannelRow extends Component {
> >
<ChannelIcon @channel={{@channel}} /> <ChannelIcon @channel={{@channel}} />
<div class="chat-channel-row__info"> <div class="chat-channel-row__info">
<ChannelName @channel={{@channel}} /> <ChannelName @channel={{@channel}} @unreadIndicator={{true}} />
<ChatChannelMetadata <ChatChannelMetadata @channel={{@channel}} />
@channel={{@channel}}
@unreadIndicator={{true}}
/>
{{#if this.shouldRenderLastMessage}} {{#if this.shouldRenderLastMessage}}
<div class="chat-channel__last-message"> <div class="chat-channel__last-message">
{{replaceEmoji (htmlSafe @channel.lastMessage.excerpt)}} {{replaceEmoji (htmlSafe @channel.lastMessage.excerpt)}}

View File

@ -1,7 +1,6 @@
import Component from "@glimmer/component"; import Component from "@glimmer/component";
import { service } from "@ember/service"; import { service } from "@ember/service";
import { gt, not } from "truth-helpers"; import { gt, not } from "truth-helpers";
import concatClass from "discourse/helpers/concat-class";
import ChannelTitle from "discourse/plugins/chat/discourse/components/channel-title"; import ChannelTitle from "discourse/plugins/chat/discourse/components/channel-title";
export default class Channel extends Component { export default class Channel extends Component {
@ -22,14 +21,11 @@ export default class Channel extends Component {
class="chat-message-creator__chatable -category-channel" class="chat-message-creator__chatable -category-channel"
data-disabled={{not @item.enabled}} data-disabled={{not @item.enabled}}
> >
<ChannelTitle @channel={{@item.model}} /> <ChannelTitle
@channel={{@item.model}}
{{#if (gt @item.tracking.unreadCount 0)}} @isUnread={{gt @item.tracking.unreadCount 0}}
@isUrgent={{this.isUrgent}}
<div />
class={{concatClass "unread-indicator" (if this.isUrgent "-urgent")}}
></div>
{{/if}}
</div> </div>
</template> </template>
} }

View File

@ -20,7 +20,6 @@ export default class ChatChannelsManager extends Service {
@service chatStateManager; @service chatStateManager;
@service currentUser; @service currentUser;
@service router; @service router;
@service site;
@service siteSettings; @service siteSettings;
@tracked _cached = new TrackedObject(); @tracked _cached = new TrackedObject();
@ -130,11 +129,7 @@ export default class ChatChannelsManager extends Service {
channel.isCategoryChannel && channel.currentUserMembership.following channel.isCategoryChannel && channel.currentUserMembership.following
); );
if (this.site.mobileView) { return this.#sortChannelsByActivity(channels);
return this.#sortChannelsByActivity(channels);
} else {
return channels.sort((a, b) => a?.slug?.localeCompare?.(b?.slug));
}
} }
@cached @cached

View File

@ -7,6 +7,8 @@
} }
&__label { &__label {
display: flex;
align-items: center;
white-space: nowrap; white-space: nowrap;
} }
@ -15,4 +17,21 @@
width: 1em; width: 1em;
vertical-align: baseline; vertical-align: baseline;
} }
.chat-channel-unread-indicator {
@include chat-unread-indicator;
display: flex;
align-items: center;
justify-content: center;
width: 8px;
height: 8px;
margin-left: 0.75em;
&.-urgent {
width: auto;
height: auto;
min-width: 0.6em;
padding: 0.3em 0.5em;
}
}
} }

View File

@ -90,22 +90,6 @@
align-items: flex-end; align-items: flex-end;
flex-direction: column; flex-direction: column;
margin-left: 0.5em; margin-left: 0.5em;
.chat-channel-unread-indicator {
@include chat-unread-indicator;
display: flex;
align-items: center;
justify-content: center;
width: 8px;
height: 8px;
&.-urgent {
width: auto;
height: auto;
min-width: 0.6em;
padding: 0.3em 0.5em;
}
}
} }
&__metadata-date { &__metadata-date {

View File

@ -267,11 +267,14 @@
} }
.unread-indicator { .unread-indicator {
margin-left: 0.5rem;
width: 8px; width: 8px;
height: 8px; height: 8px;
background: var(--tertiary); background: var(--tertiary);
border-radius: 100%; border-radius: 100%;
&.-urgent {
background: var(--success);
}
} }
} }

View File

@ -100,7 +100,7 @@ RSpec.describe "Channel - Info - Settings page", type: :system do
expect(page.find(".c-channel-settings__name")["innerHTML"].strip).to eq( expect(page.find(".c-channel-settings__name")["innerHTML"].strip).to eq(
"&lt;script&gt;alert('hello')&lt;/script&gt;", "&lt;script&gt;alert('hello')&lt;/script&gt;",
) )
expect(page.find(".chat-channel-name__label")["innerHTML"].strip).to eq( expect(page.find(".chat-channel-name__label")["innerHTML"].strip).to include(
"&lt;script&gt;alert('hello')&lt;/script&gt;", "&lt;script&gt;alert('hello')&lt;/script&gt;",
) )
end end

View File

@ -66,4 +66,23 @@ module("Discourse Chat | Component | <ChannelName />", function (hooks) {
users.mapBy("username").join(", ") users.mapBy("username").join(", ")
); );
}); });
test("unreadIndicator", async function (assert) {
const channel = new ChatFabricators(getOwner(this)).directMessageChannel();
channel.tracking.unreadCount = 1;
let unreadIndicator = true;
await render(
<template><ChannelName @channel={{channel}} @unreadIndicator={{unreadIndicator}}/></template>
);
assert.true(exists(".chat-channel-unread-indicator"));
unreadIndicator = false;
await render(
<template><ChannelName @channel={{channel}} @unreadIndicator={{unreadIndicator}}/></template>
);
assert.false(exists(".chat-channel-unread-indicator"));
});
}); });

View File

@ -3,7 +3,6 @@ import { render } from "@ember/test-helpers";
import hbs from "htmlbars-inline-precompile"; import hbs from "htmlbars-inline-precompile";
import { module, test } from "qunit"; import { module, test } from "qunit";
import { setupRenderingTest } from "discourse/tests/helpers/component-test"; import { setupRenderingTest } from "discourse/tests/helpers/component-test";
import { exists } from "discourse/tests/helpers/qunit-helpers";
import ChatFabricators from "discourse/plugins/chat/discourse/lib/fabricators"; import ChatFabricators from "discourse/plugins/chat/discourse/lib/fabricators";
module("Discourse Chat | Component | chat-channel-metadata", function (hooks) { module("Discourse Chat | Component | chat-channel-metadata", function (hooks) {
@ -29,23 +28,4 @@ module("Discourse Chat | Component | chat-channel-metadata", function (hooks) {
.dom(".chat-channel__metadata-date") .dom(".chat-channel__metadata-date")
.hasText(lastMessageSentAt.format("LT")); .hasText(lastMessageSentAt.format("LT"));
}); });
test("unreadIndicator", async function (assert) {
this.channel = new ChatFabricators(getOwner(this)).directMessageChannel();
this.channel.tracking.unreadCount = 1;
this.unreadIndicator = true;
await render(
hbs`<ChatChannelMetadata @channel={{this.channel}} @unreadIndicator={{this.unreadIndicator}}/>`
);
assert.true(exists(".chat-channel-unread-indicator"));
this.unreadIndicator = false;
await render(
hbs`<ChatChannelMetadata @channel={{this.channel}} @unreadIndicator={{this.unreadIndicator}}/>`
);
assert.false(exists(".chat-channel-unread-indicator"));
});
}); });