UI: refines thread list item (#23207)

It will now replies count and participants list. Also the title will be OM excerpt or user defined title, no more default "Thread" title. Lastly, the author of the last reply is also shown as prefix of it.
This commit is contained in:
Joffrey JAFFEUX 2023-08-24 18:45:20 +02:00 committed by GitHub
parent 46c7e47f50
commit 39b598f304
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 110 additions and 35 deletions

View File

@ -12,6 +12,7 @@ class Chat::Api::ChannelThreadsController < Chat::ApiController
tracking: result.tracking, tracking: result.tracking,
memberships: result.memberships, memberships: result.memberships,
load_more_url: result.load_more_url, load_more_url: result.load_more_url,
threads_participants: result.participants,
), ),
::Chat::ThreadListSerializer, ::Chat::ThreadListSerializer,
root: false, root: false,

View File

@ -2,15 +2,30 @@
module Chat module Chat
class ThreadsView class ThreadsView
attr_reader :user, :channel, :threads, :tracking, :memberships, :load_more_url attr_reader :user,
:channel,
:threads,
:tracking,
:memberships,
:load_more_url,
:threads_participants
def initialize(channel:, threads:, user:, tracking:, memberships:, load_more_url:) def initialize(
channel:,
threads:,
user:,
tracking:,
memberships:,
load_more_url:,
threads_participants:
)
@channel = channel @channel = channel
@threads = threads @threads = threads
@user = user @user = user
@tracking = tracking @tracking = tracking
@memberships = memberships @memberships = memberships
@load_more_url = load_more_url @load_more_url = load_more_url
@threads_participants = threads_participants
end end
end end
end end

View File

@ -12,6 +12,7 @@ module Chat
membership: object.memberships.find { |m| m.thread_id == thread.id }, membership: object.memberships.find { |m| m.thread_id == thread.id },
include_thread_preview: true, include_thread_preview: true,
include_thread_original_message: true, include_thread_original_message: true,
participants: object.threads_participants[thread.id],
root: nil, root: nil,
) )
end end

View File

@ -33,6 +33,7 @@ module Chat
model :threads model :threads
step :fetch_tracking step :fetch_tracking
step :fetch_memberships step :fetch_memberships
step :fetch_participants
step :build_load_more_url step :build_load_more_url
# @!visibility private # @!visibility private
@ -133,6 +134,10 @@ module Chat
) )
end end
def fetch_participants(threads:, **)
context.participants = ::Chat::ThreadParticipantQuery.call(thread_ids: threads.map(&:id))
end
def build_load_more_url(contract:, **) def build_load_more_url(contract:, **)
load_more_params = { offset: context.offset + context.limit }.to_query load_more_params = { offset: context.offset + context.limit }.to_query
context.load_more_url = context.load_more_url =

View File

@ -17,8 +17,13 @@
<div class="chat-thread-list-item__om-user-avatar"> <div class="chat-thread-list-item__om-user-avatar">
<Chat::UserAvatar @user={{@thread.originalMessage.user}} /> <Chat::UserAvatar @user={{@thread.originalMessage.user}} />
</div> </div>
<div class="chat-thread-list-item__title overflow-ellipsis"> <div class="chat-thread-list-item__title">
{{replace-emoji this.title}}
{{#if this.title}}
{{replace-emoji this.title}}
{{else}}
{{@thread.originalMessage.excerpt}}
{{/if}}
</div> </div>
<div class="chat-thread-list-item__unread-indicator"> <div class="chat-thread-list-item__unread-indicator">
<Chat::ThreadList::Item::UnreadIndicator @thread={{@thread}} /> <Chat::ThreadList::Item::UnreadIndicator @thread={{@thread}} />
@ -26,14 +31,26 @@
</div> </div>
<div class="chat-thread-list-item__body"> <div class="chat-thread-list-item__body">
{{replace-emoji (html-safe @thread.originalMessage.excerpt)}} <span class="chat-thread-list-item__last-reply-author">
@{{@thread.preview.lastReplyUser.username}}:
</span>
<span class="chat-thread-list-item__last-reply-excerpt">
{{replace-emoji (html-safe @thread.preview.lastReplyExcerpt)}}
</span>
</div> </div>
<div class="chat-thread-list-item__metadata"> <div class="chat-thread-list-item__metadata">
<div class="chat-thread-list-item__participants"></div> <Chat::Thread::Participants
<div class="chat-thread-list-item__last-reply"> @thread={{@thread}}
class="chat-thread-list-item__participants"
/>
<span class="chat-thread-list-item__replies-count">
{{i18n "chat.thread.replies" count=@thread.preview.replyCount}}
</span>
<span class="chat-thread-list-item__metadata__separator">·</span>
<div class="chat-thread-list-item__last-reply-timestamp">
{{#if @thread.preview.lastReplyCreatedAt}} {{#if @thread.preview.lastReplyCreatedAt}}
{{i18n "chat.thread.last_reply"}} <span>{{i18n "chat.thread.last_reply"}}</span>
{{format-date @thread.preview.lastReplyCreatedAt leaveAgo="true"}} {{format-date @thread.preview.lastReplyCreatedAt leaveAgo="true"}}
{{/if}} {{/if}}
</div> </div>

View File

@ -1,5 +1,5 @@
{{#if (gt @thread.preview.participantUsers.length 1)}} {{#if (gt @thread.preview.participantUsers.length 1)}}
<div class="chat-thread-participants"> <div class="chat-thread-participants" ...attributes>
<div class="chat-thread-participants__avatar-group"> <div class="chat-thread-participants__avatar-group">
{{#each @thread.preview.participantUsers as |user|}} {{#each @thread.preview.participantUsers as |user|}}
<Chat::UserAvatar <Chat::UserAvatar

View File

@ -1,5 +1,4 @@
import { getOwner } from "discourse-common/lib/get-owner"; import { getOwner } from "discourse-common/lib/get-owner";
import I18n from "I18n";
import ChatMessagesManager from "discourse/plugins/chat/discourse/lib/chat-messages-manager"; import ChatMessagesManager from "discourse/plugins/chat/discourse/lib/chat-messages-manager";
import { escapeExpression } from "discourse/lib/utilities"; import { escapeExpression } from "discourse/lib/utilities";
import { tracked } from "@glimmer/tracking"; import { tracked } from "@glimmer/tracking";
@ -48,11 +47,7 @@ export default class ChatThread {
? ChatMessage.create(channel, args.original_message) ? ChatMessage.create(channel, args.original_message)
: null; : null;
this.title = this.title = args.title;
args.title ||
`${I18n.t("chat.thread.default_title", {
thread_id: this.id,
})}`;
if (args.current_user_membership) { if (args.current_user_membership) {
this.currentUserMembership = UserChatThreadMembership.create( this.currentUserMembership = UserChatThreadMembership.create(

View File

@ -80,6 +80,7 @@
align-items: center; align-items: center;
justify-content: flex-end; justify-content: flex-end;
gap: 0.25rem; gap: 0.25rem;
margin-left: 0.5rem;
} }
&__replies-count { &__replies-count {

View File

@ -46,14 +46,34 @@
} }
} }
&__metadata { &__last-reply-author {
display: flex; font-weight: 700;
justify-content: flex-end;
} }
&__last-reply { &__metadata {
display: flex;
align-items: center;
justify-content: space-between;
}
&__metadata__separator {
padding-inline: 0.25rem;
font-weight: 700;
}
&__participants {
margin-right: 0.25rem;
}
&__replies-count {
margin-left: auto;
}
&__last-reply-timestamp,
&__replies-count {
color: var(--secondary-low); color: var(--secondary-low);
font-size: var(--font-down-1); font-size: var(--font-down-1);
@include ellipsis;
} }
&__header { &__header {
@ -66,6 +86,7 @@
&__title { &__title {
flex: 1 1 auto; flex: 1 1 auto;
font-weight: bold; font-weight: bold;
@include ellipsis;
} }
&__unread-indicator { &__unread-indicator {
@ -77,16 +98,10 @@
flex-direction: column; flex-direction: column;
box-sizing: border-box; box-sizing: border-box;
justify-content: center; justify-content: center;
color: var(--primary);
&:hover,
&:visited {
color: var(--primary);
}
} }
&__om-user-avatar { &__om-user-avatar {
margin-right: 0.5rem; margin-right: 0.25rem;
flex: 0 0 auto; flex: 0 0 auto;
} }
} }

View File

@ -1,5 +1,4 @@
.chat-thread-participants { .chat-thread-participants {
margin-left: 0.5rem;
&__other-count { &__other-count {
font-size: var(--font-down-2); font-size: var(--font-down-2);
color: var(--primary-high); color: var(--primary-high);
@ -19,8 +18,6 @@
width: auto !important; width: auto !important;
.avatar { .avatar {
width: 24px;
height: 24px;
padding: 0; padding: 0;
} }
} }
@ -38,8 +35,6 @@
margin-right: -10px; margin-right: -10px;
} }
.avatar { .avatar {
width: 22px;
height: 22px;
border: 1px solid var(--primary-very-low); border: 1px solid var(--primary-very-low);
} }
} }

View File

@ -579,7 +579,6 @@ en:
thread: thread:
title: "Title" title: "Title"
view_thread: View thread view_thread: View thread
default_title: "Thread"
replies: replies:
one: "%{count} reply" one: "%{count} reply"
other: "%{count} replies" other: "%{count} replies"

View File

@ -260,6 +260,14 @@ RSpec.describe ::Chat::LookupChannelThreads do
end end
end end
describe "step - fetch_participants" do
it "returns correct participants" do
expect(result.participants).to eq(
::Chat::ThreadParticipantQuery.call(thread_ids: [thread_1, thread_2, thread_3].map(&:id)),
)
end
end
describe "step - build_load_more_url" do describe "step - build_load_more_url" do
it "returns a url with the correct params" do it "returns a url with the correct params" do
expect(result.load_more_url).to eq("/chat/api/channels/#{channel_1.id}/threads?offset=10") expect(result.load_more_url).to eq("/chat/api/channels/#{channel_1.id}/threads?offset=10")

View File

@ -40,7 +40,7 @@ module PageObjects
end end
def last_reply_datetime_selector(last_reply) def last_reply_datetime_selector(last_reply)
".chat-thread-list-item__last-reply .relative-date[data-time='#{(last_reply.created_at.iso8601.to_time.to_f * 1000).to_i}']" ".chat-thread-list-item__last-reply-timestamp .relative-date[data-time='#{(last_reply.created_at.iso8601.to_time.to_f * 1000).to_i}']"
end end
def has_no_unread_item?(id) def has_no_unread_item?(id)

View File

@ -83,10 +83,11 @@ describe "Thread list in side panel | full page", type: :system do
thread_2.add(current_user) thread_2.add(current_user)
end end
it "shows a default title for threads without a title" do it "shows the OM excerpt for threads without a title" do
chat_page.visit_channel(channel) chat_page.visit_channel(channel)
channel_page.open_thread_list channel_page.open_thread_list
expect(page).to have_content(I18n.t("js.chat.thread.default_title", thread_id: thread_1.id))
expect(page).to have_content(thread_1.original_message.excerpt)
end end
it "shows the thread title with emoji" do it "shows the thread title with emoji" do
@ -125,6 +126,28 @@ describe "Thread list in side panel | full page", type: :system do
) )
end end
it "shows replies count" do
chat_page.visit_channel(channel)
channel_page.open_thread_list
expect(thread_list_page.item_by_id(thread_1.id)).to have_css(
".chat-thread-list-item__replies-count",
text: I18n.t("js.chat.thread.replies", count: thread_1.replies_count_cache),
)
end
it "shows participants" do
chat_page.visit_channel(channel)
channel_page.open_thread_list
expect(thread_list_page.item_by_id(thread_1.id)).to have_css(
".avatar[title='#{current_user.username}']",
)
expect(thread_list_page.item_by_id(thread_1.id)).to have_css(
".avatar[title='#{other_user.username}']",
)
end
it "opens a thread" do it "opens a thread" do
chat_page.visit_channel(channel) chat_page.visit_channel(channel)
channel_page.open_thread_list channel_page.open_thread_list