mirror of
https://github.com/discourse/discourse.git
synced 2025-01-18 13:52:50 +08:00
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:
parent
8a9d3b3eed
commit
7a9514922b
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -8,7 +8,7 @@ module Chat
|
|||
attributes :id, :cooked, :excerpt
|
||||
|
||||
def excerpt
|
||||
WordWatcher.censor(object.excerpt)
|
||||
object.censored_excerpt
|
||||
end
|
||||
|
||||
def user
|
||||
|
|
|
@ -44,7 +44,7 @@ module Chat
|
|||
end
|
||||
|
||||
def excerpt
|
||||
WordWatcher.censor(object.excerpt)
|
||||
object.censored_excerpt
|
||||
end
|
||||
|
||||
def reactions
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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?
|
||||
|
|
|
@ -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
|
|
@ -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
|
||||
|
|
|
@ -54,6 +54,7 @@ module Chat
|
|||
Chat::Thread
|
||||
.includes(
|
||||
:channel,
|
||||
:last_reply,
|
||||
original_message_user: :user_status,
|
||||
original_message: :chat_webhook_event,
|
||||
)
|
||||
|
|
|
@ -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),
|
||||
)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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>
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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}}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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>
|
|
@ -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
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -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>
|
|
@ -1,3 +0,0 @@
|
|||
import Component from "@glimmer/component";
|
||||
|
||||
export default class ChatThreadOriginalMessage extends Component {}
|
|
@ -1,5 +0,0 @@
|
|||
<StyleguideExample @title="<ChatThreadOriginalMessage>">
|
||||
<Styleguide::Component>
|
||||
<Chat::Thread::OriginalMessage @message={{this.message}} />
|
||||
</Styleguide::Component>
|
||||
</StyleguideExample>
|
|
@ -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 });
|
||||
}
|
|
@ -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);
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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) {
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -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();
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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";
|
||||
|
|
|
@ -553,6 +553,7 @@ en:
|
|||
original_message:
|
||||
started_by: "Started by"
|
||||
settings: "Settings"
|
||||
last_reply: "last reply"
|
||||
threads:
|
||||
open: "Open Thread"
|
||||
list: "Ongoing discussions"
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
)
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue
Block a user