FEATURE: Improving thread list item and header (#21749)

* Moved the settings cog from thread list to thread and
  put it in a new header component
* Remove thread original message component, no longer needed
  and the list item and thread indicator styles/content
  will be quite different
* Start adding content (unread indicator etc.) to the thread
  list item and changing structure to be more like designs
* Serialize the last thread reply when opening the thread index,
  show in list and update with message bus
This commit is contained in:
Martin Brennan 2023-05-29 09:11:55 +02:00 committed by GitHub
parent 8a9d3b3eed
commit 7a9514922b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
45 changed files with 394 additions and 224 deletions

View File

@ -146,6 +146,12 @@ module Chat
PrettyText.excerpt(cooked, max_length)
end
def censored_excerpt(rich: false, max_length: 50)
WordWatcher.censor(
rich ? rich_excerpt(max_length: max_length) : excerpt(max_length: max_length),
)
end
def cooked_for_excerpt
(cooked.blank? && uploads.present?) ? "<p>#{uploads.first.original_filename}</p>" : cooked
end

View File

@ -23,6 +23,7 @@ module Chat
primary_key: :id,
class_name: "Chat::Message"
has_many :user_chat_thread_memberships
has_one :last_reply, -> { order("created_at DESC, id DESC") }, class_name: "Chat::Message"
enum :status, { open: 0, read_only: 1, closed: 2, archived: 3 }, scopes: false

View File

@ -8,7 +8,7 @@ module Chat
attributes :id, :cooked, :excerpt
def excerpt
WordWatcher.censor(object.excerpt)
object.censored_excerpt
end
def user

View File

@ -44,7 +44,7 @@ module Chat
end
def excerpt
WordWatcher.censor(object.excerpt)
object.censored_excerpt
end
def reactions

View File

@ -10,6 +10,7 @@ module Chat
thread,
scope: scope,
membership: object.memberships.find { |m| m.thread_id == thread.id },
include_preview: true,
root: nil,
)
end

View File

@ -3,7 +3,11 @@
module Chat
class ThreadOriginalMessageSerializer < Chat::MessageSerializer
def excerpt
WordWatcher.censor(object.rich_excerpt(max_length: Chat::Thread::EXCERPT_LENGTH))
object.censored_excerpt(rich: true, max_length: Chat::Thread::EXCERPT_LENGTH)
end
def include_available_flags?
false
end
def include_reactions?

View File

@ -0,0 +1,19 @@
# frozen_string_literal: true
module Chat
class ThreadPreviewSerializer < ApplicationSerializer
attributes :last_reply_created_at, :last_reply_excerpt, :last_reply_id
def last_reply_created_at
object.last_reply.created_at
end
def last_reply_id
object.last_reply.id
end
def last_reply_excerpt
object.last_reply.censored_excerpt
end
end
end

View File

@ -5,7 +5,14 @@ module Chat
has_one :original_message_user, serializer: BasicUserWithStatusSerializer, embed: :objects
has_one :original_message, serializer: Chat::ThreadOriginalMessageSerializer, embed: :objects
attributes :id, :title, :status, :channel_id, :meta, :reply_count, :current_user_membership
attributes :id,
:title,
:status,
:channel_id,
:meta,
:reply_count,
:current_user_membership,
:preview
def initialize(object, opts)
super(object, opts)
@ -24,6 +31,14 @@ module Chat
object.replies_count_cache || 0
end
def include_preview?
@opts[:include_preview]
end
def preview
Chat::ThreadPreviewSerializer.new(object, scope: scope, root: false).as_json
end
def include_current_user_membership?
@current_user_membership.present?
end

View File

@ -54,6 +54,7 @@ module Chat
Chat::Thread
.includes(
:channel,
:last_reply,
original_message_user: :user_status,
original_message: :chat_webhook_event,
)

View File

@ -72,6 +72,9 @@ module Chat
user_id: chat_message.user.id,
username: chat_message.user.username,
thread_id: chat_message.thread_id,
created_at: chat_message.created_at,
excerpt:
chat_message.censored_excerpt(rich: true, max_length: Chat::Thread::EXCERPT_LENGTH),
},
permissions(chat_channel),
)

View File

@ -9,17 +9,7 @@
{{will-destroy this.unsubscribeFromUpdates}}
>
{{#if @includeHeader}}
<div class="chat-thread__header">
<span class="chat-thread__label">{{i18n "chat.thread.label"}}</span>
<LinkTo
class="chat-thread__close btn-flat btn btn-icon no-text"
@route="chat.channel"
@models={{this.channel.routeModels}}
title={{i18n "chat.thread.close"}}
>
{{d-icon "times"}}
</LinkTo>
</div>
<Chat::Thread::Header @thread={{this.thread}} @channel={{this.channel}} />
{{/if}}
<div

View File

@ -1,18 +1,15 @@
import Component from "@glimmer/component";
import { inject as service } from "@ember/service";
export default class ChatThreadHeaderUnreadIndicator extends Component {
@service chatTrackingStateManager;
get unreadCount() {
return this.args.channel.unreadThreadCount;
}
get showUnreadIndicator() {
return this.args.channel.unreadThreadCount > 0;
return this.unreadCount > 0;
}
get unreadCountLabel() {
if (this.args.channel.unreadThreadCount > 99) {
return "99+";
}
return this.args.channel.unreadThreadCount;
return this.unreadCount > 99 ? "99+" : this.unreadCount;
}
}

View File

@ -0,0 +1,25 @@
<div class="chat-thread-header">
<span class="chat-thread-header__label overflow-ellipsis">
{{replace-emoji this.label}}
</span>
<div class="chat-thread-header__buttons">
{{#if this.canChangeThreadSettings}}
<DButton
@action={{action this.openThreadSettings}}
@class="btn-flat chat-thread-header__settings"
@icon="cog"
@title="chat.thread.settings"
/>
{{/if}}
<LinkTo
class="chat-thread__close btn-flat btn btn-icon no-text"
@route="chat.channel"
@models={{@channel.routeModels}}
title={{i18n "chat.thread.close"}}
>
{{d-icon "times"}}
</LinkTo>
</div>
</div>

View File

@ -0,0 +1,35 @@
import Component from "@glimmer/component";
import I18n from "I18n";
import showModal from "discourse/lib/show-modal";
import { inject as service } from "@ember/service";
import { action } from "@ember/object";
export default class ChatThreadHeader extends Component {
@service currentUser;
@service router;
get label() {
if (this.args.thread) {
return this.args.thread.escapedTitle;
} else {
return I18n.t("chat.threads.list");
}
}
get canChangeThreadSettings() {
if (!this.args.thread) {
return false;
}
return (
this.currentUser.staff ||
this.currentUser.id === this.args.thread.originalMessage.user.id
);
}
@action
openThreadSettings() {
const controller = showModal("chat-thread-settings-modal");
controller.set("thread", this.args.thread);
}
}

View File

@ -0,0 +1,9 @@
{{#if this.showUnreadIndicator}}
<div class="chat-thread-list-item-unread-indicator">
<div class="chat-thread-list-item-unread-indicator__number-wrap">
<div
class="chat-thread-list-item-unread-indicator__number"
>{{this.unreadCountLabel}}</div>
</div>
</div>
{{/if}}

View File

@ -0,0 +1,15 @@
import Component from "@glimmer/component";
export default class ChatThreadListItemUnreadIndicator extends Component {
get unreadCount() {
return this.args.thread.tracking.unreadCount;
}
get showUnreadIndicator() {
return this.unreadCount > 0;
}
get unreadCountLabel() {
return this.unreadCount > 99 ? "99+" : this.unreadCount;
}
}

View File

@ -1,7 +1,7 @@
<div
class={{concat-class
"chat-thread-list-item"
(if (gt @thread.tracking.unreadCount 0) "-unread")
(if (gt @thread.tracking.unreadCount 0) "-is-unread")
}}
data-thread-id={{@thread.id}}
>
@ -13,20 +13,30 @@
{{on "click" (fn this.openThread @thread) passive=true}}
>
<div class="chat-thread-list-item__header">
<div class="chat-thread-list-item__title">
<div class="chat-thread-list-item__om-user-avatar">
<ChatUserAvatar @user={{@thread.originalMessage.user}} />
</div>
<div class="chat-thread-list-item__title overflow-ellipsis">
{{replace-emoji this.title}}
</div>
<div class="chat-thread-list-item__buttons">
<DButton
@action={{action this.openThreadSettings}}
@class="btn-flat chat-thread-list-item__settings"
@icon="cog"
@title="chat.thread.settings"
@disabled={{not this.canChangeThreadSettings}}
/>
<div class="chat-thread-list-item__unread-indicator">
<Chat::Thread::ListItemUnreadIndicator @thread={{@thread}} />
</div>
</div>
<div class="chat-thread-list-item__body">
{{replace-emoji (html-safe @thread.originalMessage.excerpt)}}
</div>
<div class="chat-thread-list-item__metadata">
<div class="chat-thread-list-item__participants"></div>
<div class="chat-thread-list-item__last-reply">
{{#if @thread.preview.lastReplyCreatedAt}}
{{i18n "chat.thread.last_reply"}}
{{format-date @thread.preview.lastReplyCreatedAt leaveAgo="true"}}
{{/if}}
</div>
</div>
<Chat::Thread::OriginalMessage @message={{@thread.originalMessage}} />
</div>
</div>
</div>

View File

@ -1,7 +1,5 @@
import Component from "@glimmer/component";
import showModal from "discourse/lib/show-modal";
import { inject as service } from "@ember/service";
import I18n from "I18n";
import { action } from "@ember/object";
export default class ChatThreadListItem extends Component {
@ -9,25 +7,7 @@ export default class ChatThreadListItem extends Component {
@service router;
get title() {
return (
this.args.thread.escapedTitle ||
`${I18n.t("chat.thread.default_title", {
thread_id: this.args.thread.id,
})}`
);
}
get canChangeThreadSettings() {
return (
this.currentUser.staff ||
this.currentUser.id === this.args.thread.originalMessage.user.id
);
}
@action
openThreadSettings() {
const controller = showModal("chat-thread-settings-modal");
controller.set("thread", this.args.thread);
return this.args.thread.escapedTitle;
}
@action

View File

@ -5,17 +5,7 @@
{{will-destroy this.teardown}}
>
{{#if @includeHeader}}
<div class="chat-thread__header">
<span class="chat-thread__label">{{i18n "chat.threads.list"}}</span>
<LinkTo
class="chat-thread__close btn-flat btn btn-icon no-text"
@route="chat.channel"
@models={{@channel.routeModels}}
title={{i18n "chat.thread.close"}}
>
{{d-icon "times"}}
</LinkTo>
</div>
<Chat::Thread::Header @channel={{@channel}} />
{{/if}}
<div class="chat-thread-list__items">

View File

@ -1,15 +0,0 @@
<div class="chat-thread-original-message">
<div class="chat-thread-original-message__inner-container">
<div class="chat-thread-original-message__excerpt">
{{replace-emoji (html-safe @message.excerpt)}}
</div>
<div class="chat-thread-original-message__author">
<span class="chat-thread-original-message__avatar">
<ChatUserAvatar @user={{@message.user}} />
</span>
<span class="chat-thread-original-message__username">
{{@message.user.username}}
</span>
</div>
</div>
</div>

View File

@ -1,3 +0,0 @@
import Component from "@glimmer/component";
export default class ChatThreadOriginalMessage extends Component {}

View File

@ -1,5 +0,0 @@
<StyleguideExample @title="<ChatThreadOriginalMessage>">
<Styleguide::Component>
<Chat::Thread::OriginalMessage @message={{this.message}} />
</Styleguide::Component>
</StyleguideExample>

View File

@ -1,9 +0,0 @@
import Component from "@glimmer/component";
import fabricators from "discourse/plugins/chat/discourse/lib/fabricators";
import { inject as service } from "@ember/service";
export default class ChatStyleguideChatThreadOriginalMessage extends Component {
@service currentUser;
message = fabricators.message({ user: this.currentUser });
}

View File

@ -42,7 +42,8 @@ export default class ChatThreadsManager {
const threads = result.threads.map((thread) => {
return this.chat.activeChannel.threadsManager.store(
this.chat.activeChannel,
thread
thread,
{ replace: true }
);
});
@ -59,14 +60,18 @@ export default class ChatThreadsManager {
return Object.values(this._cached);
}
store(channel, threadObject) {
let model = this.#findStale(threadObject.id);
store(channel, threadObject, options = {}) {
let model;
if (!options.replace) {
model = this.#findStale(threadObject.id);
}
if (!model) {
if (threadObject instanceof ChatThread) {
model = threadObject;
} else {
model = new ChatThread(channel, threadObject);
model = ChatThread.create(channel, threadObject);
}
this.#cache(model);

View File

@ -0,0 +1,18 @@
import { tracked } from "@glimmer/tracking";
export default class ChatThreadPreview {
static create(channel, args = {}) {
return new ChatThreadPreview(channel, args);
}
@tracked lastReplyId;
@tracked lastReplyCreatedAt;
@tracked lastReplyExcerpt;
constructor(args = {}) {
this.lastReplyId = args.last_reply_id || args.lastReplyId;
this.lastReplyCreatedAt =
args.last_reply_created_at || args.lastReplyCreatedAt;
this.lastReplyExcerpt = args.last_reply_excerpt || args.lastReplyExcerpt;
}
}

View File

@ -1,4 +1,5 @@
import { getOwner } from "discourse-common/lib/get-owner";
import I18n from "I18n";
import ChatMessagesManager from "discourse/plugins/chat/discourse/lib/chat-messages-manager";
import { escapeExpression } from "discourse/lib/utilities";
import { tracked } from "@glimmer/tracking";
@ -6,6 +7,7 @@ import guid from "pretty-text/guid";
import ChatMessage from "discourse/plugins/chat/discourse/models/chat-message";
import ChatTrackingState from "discourse/plugins/chat/discourse/models/chat-tracking-state";
import UserChatThreadMembership from "discourse/plugins/chat/discourse/models/user-chat-thread-membership";
import ChatThreadPreview from "discourse/plugins/chat/discourse/models/chat-thread-preview";
export const THREAD_STATUSES = {
open: "open",
@ -30,11 +32,11 @@ export default class ChatThread {
@tracked replyCount;
@tracked tracking;
@tracked currentUserMembership = null;
@tracked preview = null;
messagesManager = new ChatMessagesManager(getOwner(this));
constructor(channel, args = {}) {
this.title = args.title;
this.id = args.id;
this.channel = channel;
this.status = args.status;
@ -43,6 +45,12 @@ export default class ChatThread {
this.replyCount = args.reply_count;
this.originalMessage = ChatMessage.create(channel, args.original_message);
this.title =
args.title ||
`${I18n.t("chat.thread.default_title", {
thread_id: this.id,
})}`;
if (args.current_user_membership) {
this.currentUserMembership = UserChatThreadMembership.create(
args.current_user_membership
@ -50,6 +58,9 @@ export default class ChatThread {
}
this.tracking = new ChatTrackingState(getOwner(this));
if (args.preview) {
this.preview = ChatThreadPreview.create(args.preview);
}
}
stageMessage(message) {

View File

@ -35,11 +35,19 @@ export default class ChatChannelsManager extends Service {
return Object.values(this._cached);
}
store(channelObject) {
let model = this.#findStale(channelObject.id);
store(channelObject, options = {}) {
let model;
if (!options.replace) {
model = this.#findStale(channelObject.id);
}
if (!model) {
model = ChatChannel.create(channelObject);
if (channelObject instanceof ChatChannel) {
model = channelObject;
} else {
model = ChatChannel.create(channelObject);
}
this.#cache(model);
}

View File

@ -3,6 +3,7 @@ import I18n from "I18n";
import { bind } from "discourse-common/utils/decorators";
import { CHANNEL_STATUSES } from "discourse/plugins/chat/discourse/models/chat-channel";
import ChatChannelArchive from "../models/chat-channel-archive";
import ChatThreadPreview from "../models/chat-thread-preview";
export default class ChatSubscriptionsManager extends Service {
@service store;
@ -228,6 +229,12 @@ export default class ChatSubscriptionsManager extends Service {
channel.threadsManager
.find(busData.channel_id, busData.thread_id)
.then((thread) => {
thread.preview = ChatThreadPreview.create({
lastReplyId: busData.message_id,
lastReplyExcerpt: busData.excerpt,
lastReplyCreatedAt: busData.created_at,
});
if (busData.user_id === this.currentUser.id) {
// Thread should no longer be considered unread.
if (thread.currentUserMembership) {

View File

@ -17,28 +17,6 @@
margin-right: 0;
}
.chat-thread-header-unread-indicator {
color: var(--tertiary);
padding-left: 0.25rem;
&__number-wrap {
background-color: var(--tertiary-med-or-tertiary);
display: flex;
padding: 0.25rem 0.5rem;
border-radius: 20px;
width: 35px;
box-sizing: border-box;
flex-direction: column;
align-items: center;
}
&__number {
color: var(--secondary);
font-size: var(--font-down-3);
font-weight: bold;
}
}
&:hover {
.discourse-touch & {
background: none !important;

View File

@ -0,0 +1,15 @@
.chat-thread-header {
height: var(--chat-header-offset);
min-height: var(--chat-header-offset);
border-bottom: 1px solid var(--primary-low);
border-top: 1px solid var(--primary-low);
box-sizing: border-box;
display: flex;
align-items: center;
justify-content: space-between;
padding-inline: 1rem;
&__buttons {
display: flex;
}
}

View File

@ -10,6 +10,11 @@
.chat-thread-list-item {
@include thread-list-item;
cursor: pointer;
margin: 0.5rem 0.25rem 0.25rem;
& + .chat-thread-list-item {
margin-top: 0;
}
.touch & {
&:active {
@ -28,21 +33,43 @@
&__main {
flex: 1 1 100%;
width: 100%;
}
&__body {
padding-bottom: 0.25rem;
word-break: break-word;
margin: 0.5rem 0rem;
> * {
pointer-events: none;
}
}
&__metadata {
display: flex;
justify-content: flex-end;
}
&__last-reply {
color: var(--secondary-low);
font-size: var(--font-down-1);
}
&__header {
display: flex;
flex-direction: row;
align-items: center;
margin-bottom: 0.25rem;
}
&__title {
flex: 1;
flex: 1 1 auto;
font-weight: bold;
}
&__settings[disabled] {
display: none;
&__unread-indicator {
flex: 0 0 auto;
}
&__open-button {
@ -57,4 +84,9 @@
color: var(--primary);
}
}
&__om-user-avatar {
margin-right: 0.5rem;
flex: 0 0 auto;
}
}

View File

@ -1,26 +0,0 @@
.chat-thread-original-message {
display: flex;
margin: 0.5rem 0rem;
&__inner-container {
width: 100%;
}
&__excerpt {
padding-bottom: 0.25rem;
word-break: break-word;
> * {
pointer-events: none;
}
}
&__author {
display: flex;
align-items: center;
}
&__avatar {
padding: 0.25rem 0.25rem 0.25rem 0;
}
}

View File

@ -0,0 +1,26 @@
@mixin chat-thread-unread-indicator {
color: var(--tertiary);
padding-left: 0.25rem;
&__number-wrap {
background-color: var(--tertiary-med-or-tertiary);
display: flex;
padding: 0.25rem 0.5rem;
border-radius: 20px;
width: 35px;
box-sizing: border-box;
flex-direction: column;
align-items: center;
}
&__number {
color: var(--secondary);
font-size: var(--font-down-3);
font-weight: bold;
}
}
.chat-thread-header-unread-indicator,
.chat-thread-list-item-unread-indicator {
@include chat-thread-unread-indicator;
}

View File

@ -4,18 +4,6 @@
position: relative;
@include chat-height;
&__header {
height: var(--chat-header-offset);
min-height: var(--chat-header-offset);
border-bottom: 1px solid var(--primary-low);
border-top: 1px solid var(--primary-low);
box-sizing: border-box;
display: flex;
align-items: center;
justify-content: space-between;
padding-inline: 1rem;
}
&__body {
overflow-y: scroll;
@include chat-scrollbar();

View File

@ -12,18 +12,6 @@
overscroll-behavior: contain;
display: flex;
flex-direction: column;
.chat-thread-list-item {
margin: 0.75rem 0.25rem 0.75rem 0.5rem;
&.-unread {
border-left: 2px solid var(--tertiary-medium);
}
& + .chat-thread-list-item {
margin-top: 0;
}
}
}
&__no-threads {

View File

@ -50,6 +50,7 @@
@import "reviewable-chat-message";
@import "chat-thread-list-item";
@import "chat-threads-list";
@import "chat-thread-original-message";
@import "chat-composer-separator";
@import "chat-thread-header-button";
@import "chat-thread-header";
@import "chat-thread-unread-indicator";

View File

@ -553,6 +553,7 @@ en:
original_message:
started_by: "Started by"
settings: "Settings"
last_reply: "last reply"
threads:
open: "Open Thread"
list: "Ongoing discussions"

View File

@ -343,6 +343,9 @@ describe Chat::Publisher do
message_id: message_1.id,
user_id: message_1.user_id,
username: message_1.user.username,
excerpt:
message_1.censored_excerpt(rich: true, max_length: Chat::Thread::EXCERPT_LENGTH),
created_at: message_1.created_at,
thread_id: thread.id,
},
)

View File

@ -155,7 +155,11 @@ RSpec.describe "Message notifications - mobile", type: :system, js: true, mobile
session.quit
end
expect(page).to have_css(".chat-header-icon .chat-channel-unread-indicator", text: "1")
expect(page).to have_css(
".chat-header-icon .chat-channel-unread-indicator",
text: "1",
wait: 25,
)
expect(page).to have_css(
".chat-channel-row[data-chat-channel-id=\"#{dm_channel_1.id}\"] .chat-channel-unread-indicator",
wait: 25,

View File

@ -17,21 +17,13 @@ module PageObjects
end
def header
find(".chat-thread__header")
end
def omu
header.find(".chat-thread__omu")
@header ||= PageObjects::Components::Chat::ThreadHeader.new(".chat-thread")
end
def close
header.find(".chat-thread__close").click
end
def has_header_content?(content)
header.has_content?(content)
end
def has_no_loading_skeleton?
has_no_css?(".chat-thread__messages .chat-skeleton")
end

View File

@ -7,12 +7,27 @@ module PageObjects
find(item_by_id_selector(id))
end
def has_unread_item?(id)
has_css?(item_by_id_selector(id) + ".-unread")
def avatar_selector(user)
".chat-thread-list-item__om-user-avatar .chat-user-avatar .chat-user-avatar-container[data-user-card=\"#{user.username}\"] img"
end
def last_reply_datetime_selector(last_reply)
".chat-thread-list-item__last-reply .relative-date[data-time='#{(last_reply.created_at.to_f * 1000).to_i}']"
end
def has_no_unread_item?(id)
has_no_css?(item_by_id_selector(id) + ".-unread")
has_no_css?(item_by_id_selector(id) + ".-is-unread")
end
def has_unread_item?(id, count: nil)
if count.nil?
has_css?(item_by_id_selector(id) + ".-is-unread")
else
has_css?(
item_by_id_selector(id) + ".-is-unread .chat-thread-list-item-unread-indicator__number",
text: count.to_s,
)
end
end
def item_by_id_selector(id)

View File

@ -0,0 +1,37 @@
# frozen_string_literal: true
module PageObjects
module Components
module Chat
class ThreadHeader < PageObjects::Components::Base
attr_reader :context
SELECTOR = ".chat-thread-header"
def initialize(context)
@context = context
end
def component
find(context)
end
def has_content?(content)
component.find(SELECTOR).has_content?(content)
end
def has_title_content?(content)
component.find(SELECTOR + " .chat-thread-header__label").has_content?(content)
end
def open_settings
component.find(SELECTOR + " .chat-thread-header__settings").click
end
def has_no_settings_button?
component.has_no_css?(SELECTOR + " .chat-thread-header__settings")
end
end
end
end
end

View File

@ -105,19 +105,6 @@ describe "Single thread in side panel", type: :system, js: true do
expect(side_panel).to have_open_thread(thread)
end
xit "shows the excerpt of the thread original message" do
chat_page.visit_channel(channel)
channel_page.message_thread_indicator(thread.original_message).click
expect(thread_page).to have_header_content(thread.excerpt)
end
xit "shows the avatar and username of the original message user" do
chat_page.visit_channel(channel)
channel_page.message_thread_indicator(thread.original_message).click
expect(thread_page.omu).to have_css(".chat-user-avatar img.avatar")
expect(thread_page.omu).to have_content(thread.original_message_user.username)
end
describe "sending a message" do
it "shows the message in the thread pane and links it to the correct channel" do
chat_page.visit_channel(channel)

View File

@ -26,8 +26,6 @@ describe "Thread list in side panel | full page", type: :system, js: true do
end
context "when there are threads that the user is participating in" do
before { chat_system_user_bootstrap(user: other_user, channel: channel) }
fab!(:thread_1) do
chat_thread_chain_bootstrap(channel: channel, users: [current_user, other_user])
end
@ -35,6 +33,12 @@ describe "Thread list in side panel | full page", type: :system, js: true do
chat_thread_chain_bootstrap(channel: channel, users: [current_user, other_user])
end
before do
chat_system_user_bootstrap(user: other_user, channel: channel)
thread_1.add(current_user)
thread_2.add(current_user)
end
it "shows a default title for threads without a title" do
chat_page.visit_channel(channel)
channel_page.open_thread_list
@ -59,15 +63,22 @@ describe "Thread list in side panel | full page", type: :system, js: true do
)
end
it "shows the thread original message user username and avatar" do
it "shows the thread original message user avatar" 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-original-message__avatar .chat-user-avatar .chat-user-avatar-container img",
thread_list_page.avatar_selector(thread_1.original_message.user),
)
end
it "shows the last reply date of the thread" do
freeze_time
last_reply = Fabricate(:chat_message, chat_channel: thread_1.channel, thread: thread_1)
chat_page.visit_channel(channel)
channel_page.open_thread_list
expect(thread_list_page.item_by_id(thread_1.id)).to have_css(
thread_list_page.last_reply_datetime_selector(last_reply),
)
expect(
thread_list_page.item_by_id(thread_1.id).find(".chat-thread-original-message__username"),
).to have_content(thread_1.original_message.user.username)
end
it "opens a thread" do
@ -89,20 +100,22 @@ describe "Thread list in side panel | full page", type: :system, js: true do
it "allows updating when user is admin" do
current_user.update!(admin: true)
open_thread_list
thread_list_page.item_by_id(thread_1.id).find(".chat-thread-list-item__settings").click
thread_list_page.item_by_id(thread_1.id).click
thread_page.header.open_settings
find(".thread-title-input").fill_in(with: new_title)
find(".modal-footer .btn-primary").click
expect(thread_list_page.item_by_id(thread_1.id)).to have_content(new_title)
expect(thread_page.header).to have_title_content(new_title)
end
it "allows updating when user is same as the chat original message user" do
thread_1.update!(original_message_user: current_user)
thread_1.original_message.update!(user: current_user)
open_thread_list
thread_list_page.item_by_id(thread_1.id).find(".chat-thread-list-item__settings").click
thread_list_page.item_by_id(thread_1.id).click
thread_page.header.open_settings
find(".thread-title-input").fill_in(with: new_title)
find(".modal-footer .btn-primary").click
expect(thread_list_page.item_by_id(thread_1.id)).to have_content(new_title)
expect(thread_page.header).to have_title_content(new_title)
end
it "does not allow updating if user is neither admin nor original message user" do
@ -110,10 +123,8 @@ describe "Thread list in side panel | full page", type: :system, js: true do
thread_1.original_message.update!(user: other_user)
open_thread_list
expect(thread_list_page.item_by_id(thread_1.id)).to have_no_css(
".chat-thread-list-item__settings",
)
thread_list_page.item_by_id(thread_1.id).click
expect(thread_page.header).to have_no_settings_button
end
end
end

View File

@ -32,7 +32,7 @@ describe "Thread tracking state | full page", type: :system, js: true do
it "shows an indicator on the unread thread in the list" do
chat_page.visit_channel(channel)
channel_page.open_thread_list
expect(thread_list_page).to have_unread_item(thread.id)
expect(thread_list_page).to have_unread_item(thread.id, count: 1)
end
it "marks the thread as read and removes both indicators when the user opens it" do