UX: remove last reply from My Threads preview + restyle (#25568)

On mobile, when viewing the My Threads area, each thread will show:

- The avatar of the last responder in the thread, overlaid with the chat thread symbol to visually distinguish this area from DMs.
- Either the thread title, where applicable, or the first message of the thread, truncated to fit on one line.
- The channel where the thread originated.
- The last message sent in the thread, truncated to fit on one line.
- When the last message was sent in the thread.

---------

Co-authored-by: David Battersby <info@davidbattersby.com>
This commit is contained in:
chapoi 2024-03-11 11:59:40 +02:00 committed by GitHub
parent 0e3a8b15f5
commit cfd72fa65c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 155 additions and 19 deletions

View File

@ -19,6 +19,10 @@ export default class ChatChannelIcon extends Component {
return htmlSafe(`color: #${this.args.channel.chatable.color}`);
}
get isThreadsList() {
return this.args.thread ?? false;
}
<template>
{{#if @channel.isDirectMessageChannel}}
<div class="chat-channel-icon">
@ -44,6 +48,19 @@ export default class ChatChannelIcon extends Component {
{{/if}}
</span>
</div>
{{else if this.isThreadsList}}
<div class="chat-channel-icon">
<div class="chat-channel-icon --avatar">
<ChatUserAvatar
@user={{@thread.originalMessage.user}}
@interactive={{true}}
@showPresence={{false}}
/>
<div class="avatar-flair --threads">
{{icon "discourse-threads"}}
</div>
</div>
</div>
{{/if}}
</template>
}

View File

@ -24,14 +24,14 @@ export default class ChatThreadHeader extends Component {
route = "chat.channel.threads";
title = I18n.t("chat.return_to_threads_list");
models = this.channel?.routeModels;
} else if (!this.currentUser.isInDoNotDisturb() && this.unreadCount > 0) {
route = "chat.channel.threads";
title = I18n.t("chat.return_to_threads_list");
models = this.channel?.routeModels;
} else if (prevPage === "chat.threads") {
route = "chat.threads";
title = I18n.t("chat.my_threads.title");
models = [];
} else if (!this.currentUser.isInDoNotDisturb() && this.unreadCount > 0) {
route = "chat.channel.threads";
title = I18n.t("chat.return_to_threads_list");
models = this.channel?.routeModels;
} else {
route = "chat.channel.index";
title = I18n.t("chat.return_to_channel");
@ -53,6 +53,12 @@ export default class ChatThreadHeader extends Component {
return this.channel?.threadsManager?.unreadThreadCount;
}
get showThreadUnreadIndicator() {
return (
this.backLink.route === "chat.channel.threads" && this.unreadCount > 0
);
}
<template>
<Navbar @showFullTitle={{@showFullTitle}} as |navbar|>
{{#if (and this.channel.threadingEnabled @thread)}}
@ -61,7 +67,9 @@ export default class ChatThreadHeader extends Component {
@routeModels={{this.backLink.models}}
@title={{this.backLink.title}}
>
<ChatThreadHeaderUnreadIndicator @channel={{this.channel}} />
{{#if this.showThreadUnreadIndicator}}
<ChatThreadHeaderUnreadIndicator @channel={{this.channel}} />
{{/if}}
{{icon "chevron-left"}}
</navbar.BackButton>
{{/if}}

View File

@ -3,15 +3,18 @@ import { cached } from "@glimmer/tracking";
import { service } from "@ember/service";
import i18n from "discourse-common/helpers/i18n";
import { bind } from "discourse-common/utils/decorators";
import ChannelIcon from "discourse/plugins/chat/discourse/components/channel-icon";
import ChannelTitle from "discourse/plugins/chat/discourse/components/channel-title";
import List from "discourse/plugins/chat/discourse/components/chat/list";
import ThreadIndicator from "discourse/plugins/chat/discourse/components/chat-message-thread-indicator";
import ThreadTitle from "discourse/plugins/chat/discourse/components/thread-title";
import ThreadPreview from "discourse/plugins/chat/discourse/components/user-threads/preview";
export default class UserThreads extends Component {
@service chat;
@service chatApi;
@service chatChannelsManager;
@service site;
@cached
get threadsCollection() {
@ -41,14 +44,23 @@ export default class UserThreads extends Component {
>
<list.Item as |thread|>
<div class="c-user-thread" data-id={{thread.id}}>
<ThreadTitle @thread={{thread}} />
{{#if this.site.mobileView}}
<ChannelIcon @thread={{thread}} />
{{/if}}
<ChannelTitle @channel={{thread.channel}} />
<ThreadIndicator
@message={{thread.originalMessage}}
@interactiveUser={{false}}
@interactiveThread={{false}}
tabindex="-1"
/>
<ThreadTitle @thread={{thread}} />
{{#if this.site.mobileView}}
<ThreadPreview @preview={{thread.preview}} />
{{else}}
<ThreadIndicator
@message={{thread.originalMessage}}
@interactiveUser={{false}}
@interactiveThread={{false}}
tabindex="-1"
/>
{{/if}}
</div>
</list.Item>
<list.EmptyState>

View File

@ -0,0 +1,23 @@
import Component from "@glimmer/component";
import formatDate from "discourse/helpers/format-date";
export default class ThreadPreview extends Component {
get lastReplyDate() {
return formatDate(this.args.preview.lastReplyCreatedAt, { leaveAgo: true });
}
<template>
<span class="chat-message-thread-indicator__last-reply-timestamp">
{{this.lastReplyDate}}
</span>
<div class="c-user-thread__excerpt">
<div class="c-user-thread__excerpt-poster">
{{@preview.lastReplyUser.username}}
</div>
<span>:</span>
<span class="c-user-thread__excerpt-text">
{{@preview.lastReplyExcerpt}}
</span>
</div>
</template>
}

View File

@ -1,9 +1,67 @@
.chat-user-threads {
.chat__thread-title {
justify-content: space-between;
.c-user-thread {
display: grid;
grid-template-areas:
"avatar category timestamp"
"avatar title indicator"
"avatar excerpt excerpt";
grid-template-columns: auto 1fr auto;
grid-column-gap: 0.75em;
margin-inline: 0;
padding: 0.5rem 1.5rem;
.chat-channel-icon {
grid-area: avatar;
position: relative;
.avatar {
margin-top: 4px;
width: var(--channel-list-avatar-size);
height: var(--channel-list-avatar-size);
}
}
.avatar-flair.--threads {
position: absolute;
top: -4px;
right: -3px;
background: var(--primary-low);
border-radius: 50%;
padding: 0.2em;
line-height: var(--line-height-small);
border: 2px solid var(--secondary-very-high);
color: var(--primary-high);
}
.chat__thread-title-container {
@include ellipsis;
grid-area: title;
.chat__thread-title {
&__name {
@include ellipsis;
}
}
}
.chat-channel-title {
grid-area: category;
}
.chat-message-thread-indicator__last-reply-timestamp {
grid-area: timestamp;
font-size: var(--font-down-2-rem);
align-self: center;
}
.c-user-thread__excerpt {
@include ellipsis;
grid-area: excerpt;
display: flex;
color: var(--primary-high);
}
.c-user-thread__excerpt-text {
margin-left: 0.25em;
overflow: hidden;
text-overflow: ellipsis;
}
}
.c-user-thread {
margin-inline: 1.5rem;
}

View File

@ -244,4 +244,22 @@ RSpec.describe "User threads", type: :system do
expect(user_threads_page).to have_threads
end
end
context "when in mobile", mobile: true do
before do
chat_thread_chain_bootstrap(channel: channel_1, users: [current_user, Fabricate(:user)])
end
it "has the expected UI elements" do
chat_page.visit_user_threads
expect(user_threads_page).to have_threads(count: 1)
expect(user_threads_page).to have_css(".chat-user-avatar")
expect(user_threads_page).to have_css(".chat__thread-title__name")
expect(user_threads_page).to have_css(".chat-channel-name")
expect(user_threads_page).to have_css(".c-user-thread__excerpt")
expect(user_threads_page).to have_css(".c-user-thread__excerpt-poster")
expect(user_threads_page).to have_css(".c-user-thread .relative-date")
end
end
end