mirror of
https://github.com/discourse/discourse.git
synced 2025-03-05 11:17:25 +08:00
FEATURE: Initial chat thread unread indicators (#21694)
This commit adds the thread index and individual thread in the index list unread indicators, and wires up the message bus events to mark the threads as read/unread when: 1. People send a new message in the thread 2. The user marks a thread as read There are several hacky parts and TODOs to cover before this is more functional: 1. We need to flesh out the thread scrolling and message visibility behaviour. Currently if you scroll to the end of the thread it will just mark the whole thread read unconditionally. 2. We need to send down the thread current user membership along with the last read message ID to the client and update that with read state. 3. We need to handle the sidebar unread dot for when threads are unread in the channel and clear it based on when the channel was last viewed. 4. We need to show some indicator of thread unreads on the thread indicators on original messages. 5. UI improvements to make the experience nicer and more like the actual design rather than just placeholders. But, the basic premise around incrementing/decrementing the thread overview count and showing which thread is unread in the list is working as intended.
This commit is contained in:
parent
eae47d82e2
commit
b6c5a2da08
@ -9,6 +9,7 @@ class Chat::Api::ChannelThreadsController < Chat::ApiController
|
||||
user: guardian.user,
|
||||
threads: result.threads,
|
||||
channel: result.channel,
|
||||
tracking: result.tracking,
|
||||
),
|
||||
::Chat::ThreadListSerializer,
|
||||
root: false,
|
||||
|
@ -8,7 +8,7 @@ class Chat::Api::ReadsController < Chat::ApiController
|
||||
on_failed_policy(:ensure_message_id_recency) do
|
||||
raise Discourse::InvalidParameters.new(:message_id)
|
||||
end
|
||||
on_failed_policy(:ensure_message_exists) { raise Discourse::NotFound }
|
||||
on_model_not_found(:message) { raise Discourse::NotFound }
|
||||
on_model_not_found(:active_membership) { raise Discourse::NotFound }
|
||||
on_model_not_found(:channel) { raise Discourse::NotFound }
|
||||
end
|
||||
|
@ -141,17 +141,16 @@ module Chat
|
||||
end
|
||||
end
|
||||
|
||||
Chat::Publisher.publish_user_tracking_state(
|
||||
current_user,
|
||||
@chat_channel.id,
|
||||
message =
|
||||
(
|
||||
if chat_message_creator.chat_message.thread_id.present?
|
||||
@user_chat_channel_membership.last_read_message_id
|
||||
if chat_message_creator.chat_message.in_thread?
|
||||
Chat::Message.find(@user_chat_channel_membership.last_read_message_id)
|
||||
else
|
||||
chat_message_creator.chat_message.id
|
||||
chat_message_creator.chat_message
|
||||
end
|
||||
),
|
||||
)
|
||||
)
|
||||
|
||||
Chat::Publisher.publish_user_tracking_state!(current_user, @chat_channel, message)
|
||||
render json: success_json
|
||||
end
|
||||
|
||||
|
@ -2,12 +2,13 @@
|
||||
|
||||
module Chat
|
||||
class ThreadsView
|
||||
attr_reader :user, :channel, :threads
|
||||
attr_reader :user, :channel, :threads, :tracking
|
||||
|
||||
def initialize(channel:, threads:, user:)
|
||||
def initialize(channel:, threads:, user:, tracking:)
|
||||
@channel = channel
|
||||
@threads = threads
|
||||
@user = user
|
||||
@tracking = tracking
|
||||
end
|
||||
end
|
||||
end
|
||||
|
@ -7,7 +7,7 @@ module Chat
|
||||
:chat_messages,
|
||||
:can_load_more_past,
|
||||
:can_load_more_future,
|
||||
:thread_tracking_overview,
|
||||
:unread_thread_ids,
|
||||
:threads,
|
||||
:tracking
|
||||
|
||||
@ -17,7 +17,7 @@ module Chat
|
||||
user:,
|
||||
can_load_more_past: nil,
|
||||
can_load_more_future: nil,
|
||||
thread_tracking_overview: nil,
|
||||
unread_thread_ids: nil,
|
||||
threads: nil,
|
||||
tracking: nil
|
||||
)
|
||||
@ -26,7 +26,7 @@ module Chat
|
||||
@user = user
|
||||
@can_load_more_past = can_load_more_past
|
||||
@can_load_more_future = can_load_more_future
|
||||
@thread_tracking_overview = thread_tracking_overview
|
||||
@unread_thread_ids = unread_thread_ids
|
||||
@threads = threads
|
||||
@tracking = tracking
|
||||
end
|
||||
|
@ -45,6 +45,7 @@ module Chat
|
||||
INNER JOIN chat_threads ON chat_threads.id = chat_messages.thread_id AND chat_threads.channel_id = chat_messages.chat_channel_id
|
||||
INNER JOIN user_chat_thread_memberships ON user_chat_thread_memberships.thread_id = chat_threads.id
|
||||
AND chat_messages.thread_id = memberships.thread_id
|
||||
AND chat_messages.user_id != :user_id
|
||||
AND user_chat_thread_memberships.user_id = :user_id
|
||||
AND chat_messages.id > COALESCE(user_chat_thread_memberships.last_read_message_id, 0)
|
||||
AND chat_messages.deleted_at IS NULL
|
||||
|
@ -2,7 +2,7 @@
|
||||
|
||||
module Chat
|
||||
class ThreadListSerializer < ApplicationSerializer
|
||||
attributes :meta, :threads
|
||||
attributes :meta, :threads, :tracking
|
||||
|
||||
def threads
|
||||
ActiveModel::ArraySerializer.new(
|
||||
@ -12,6 +12,10 @@ module Chat
|
||||
)
|
||||
end
|
||||
|
||||
def tracking
|
||||
object.tracking
|
||||
end
|
||||
|
||||
def meta
|
||||
{ channel_id: object.channel.id }
|
||||
end
|
||||
|
@ -2,7 +2,7 @@
|
||||
|
||||
module Chat
|
||||
class ViewSerializer < ApplicationSerializer
|
||||
attributes :meta, :chat_messages, :threads, :tracking, :thread_tracking_overview, :channel
|
||||
attributes :meta, :chat_messages, :threads, :tracking, :unread_thread_ids, :channel
|
||||
|
||||
def threads
|
||||
return [] if !object.threads
|
||||
@ -18,15 +18,15 @@ module Chat
|
||||
object.tracking || {}
|
||||
end
|
||||
|
||||
def thread_tracking_overview
|
||||
object.thread_tracking_overview || []
|
||||
def unread_thread_ids
|
||||
object.unread_thread_ids || []
|
||||
end
|
||||
|
||||
def include_threads?
|
||||
include_thread_data?
|
||||
end
|
||||
|
||||
def include_thread_tracking_overview?
|
||||
def include_unread_thread_ids?
|
||||
include_thread_data?
|
||||
end
|
||||
|
||||
|
@ -32,7 +32,7 @@ module Chat
|
||||
step :determine_threads_enabled
|
||||
step :determine_include_thread_messages
|
||||
step :fetch_messages
|
||||
step :fetch_thread_tracking_overview
|
||||
step :fetch_unread_thread_ids
|
||||
step :fetch_threads_for_messages
|
||||
step :fetch_tracking
|
||||
step :build_view
|
||||
@ -120,11 +120,11 @@ module Chat
|
||||
# that have unread messages, only threads with unread messages
|
||||
# will be included in this array. This is a low-cost way to know
|
||||
# how many threads the user has unread across the entire channel.
|
||||
def fetch_thread_tracking_overview(guardian:, channel:, threads_enabled:, **)
|
||||
def fetch_unread_thread_ids(guardian:, channel:, threads_enabled:, **)
|
||||
if !threads_enabled
|
||||
context.thread_tracking_overview = []
|
||||
context.unread_thread_ids = []
|
||||
else
|
||||
context.thread_tracking_overview =
|
||||
context.unread_thread_ids =
|
||||
::Chat::TrackingStateReportQuery
|
||||
.call(
|
||||
guardian: guardian,
|
||||
@ -177,7 +177,7 @@ module Chat
|
||||
messages:,
|
||||
threads:,
|
||||
tracking:,
|
||||
thread_tracking_overview:,
|
||||
unread_thread_ids:,
|
||||
can_load_more_past:,
|
||||
can_load_more_future:,
|
||||
**
|
||||
@ -189,7 +189,7 @@ module Chat
|
||||
user: guardian.user,
|
||||
can_load_more_past: can_load_more_past,
|
||||
can_load_more_future: can_load_more_future,
|
||||
thread_tracking_overview: thread_tracking_overview,
|
||||
unread_thread_ids: unread_thread_ids,
|
||||
threads: threads,
|
||||
tracking: tracking,
|
||||
)
|
||||
|
@ -23,6 +23,7 @@ module Chat
|
||||
policy :threading_enabled_for_channel
|
||||
policy :can_view_channel
|
||||
model :threads
|
||||
step :fetch_tracking
|
||||
|
||||
# @!visibility private
|
||||
class Contract
|
||||
@ -71,5 +72,14 @@ module Chat
|
||||
.order("last_posted_at DESC NULLS LAST")
|
||||
.limit(50)
|
||||
end
|
||||
|
||||
def fetch_tracking(guardian:, threads:, **)
|
||||
context.tracking =
|
||||
::Chat::TrackingStateReportQuery.call(
|
||||
guardian: guardian,
|
||||
thread_ids: threads.map(&:id),
|
||||
include_threads: true,
|
||||
).thread_tracking
|
||||
end
|
||||
end
|
||||
end
|
||||
|
@ -63,7 +63,7 @@ module Chat
|
||||
membership_id: membership.membership_id,
|
||||
}
|
||||
end
|
||||
Chat::Publisher.publish_bulk_user_tracking_state(guardian.user, data)
|
||||
Chat::Publisher.publish_bulk_user_tracking_state!(guardian.user, data)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
@ -47,13 +47,26 @@ module Chat
|
||||
),
|
||||
)
|
||||
|
||||
# NOTE: This means that the read count is only updated in the client
|
||||
# for new messages in the main channel stream, maybe in future we want to
|
||||
# do this for thread messages as well?
|
||||
if !chat_message.thread_reply? || !allow_publish_to_thread?(chat_channel)
|
||||
MessageBus.publish(
|
||||
self.new_messages_message_bus_channel(chat_channel.id),
|
||||
{
|
||||
type: "channel",
|
||||
channel_id: chat_channel.id,
|
||||
message_id: chat_message.id,
|
||||
user_id: chat_message.user.id,
|
||||
username: chat_message.user.username,
|
||||
thread_id: chat_message.thread_id,
|
||||
},
|
||||
permissions(chat_channel),
|
||||
)
|
||||
end
|
||||
|
||||
if chat_message.thread_reply? && allow_publish_to_thread?(chat_channel)
|
||||
MessageBus.publish(
|
||||
self.new_messages_message_bus_channel(chat_channel.id),
|
||||
{
|
||||
type: "thread",
|
||||
channel_id: chat_channel.id,
|
||||
message_id: chat_message.id,
|
||||
user_id: chat_message.user.id,
|
||||
@ -253,23 +266,46 @@ module Chat
|
||||
"/chat/user-tracking-state/#{user_id}"
|
||||
end
|
||||
|
||||
def self.publish_user_tracking_state(user, chat_channel_id, chat_message_id)
|
||||
tracking_data =
|
||||
Chat::TrackingState.call(
|
||||
guardian: Guardian.new(user),
|
||||
channel_ids: [chat_channel_id],
|
||||
def self.publish_user_tracking_state!(user, channel, message)
|
||||
data = {
|
||||
channel_id: channel.id,
|
||||
last_read_message_id: message.id,
|
||||
thread_id: message.thread_id,
|
||||
}
|
||||
|
||||
channel_tracking_data =
|
||||
Chat::TrackingStateReportQuery.call(
|
||||
guardian: user.guardian,
|
||||
channel_ids: [channel.id],
|
||||
include_missing_memberships: true,
|
||||
)
|
||||
if tracking_data.failure?
|
||||
raise StandardError,
|
||||
"Tracking service failed when trying to publish user tracking state:\n\n#{tracking_data.inspect_steps}"
|
||||
).find_channel(channel.id)
|
||||
|
||||
data.merge!(channel_tracking_data)
|
||||
|
||||
# Need the thread unread overview if channel has threading enabled
|
||||
# and a message is sent in the thread. We also need to pass the actual
|
||||
# thread tracking state.
|
||||
if channel.threading_enabled && message.thread_reply?
|
||||
data[:unread_thread_ids] = ::Chat::TrackingStateReportQuery
|
||||
.call(
|
||||
guardian: user.guardian,
|
||||
channel_ids: [channel.id],
|
||||
include_threads: true,
|
||||
include_read: false,
|
||||
)
|
||||
.find_channel_threads(channel.id)
|
||||
.keys
|
||||
|
||||
data[:thread_tracking] = ::Chat::TrackingStateReportQuery.call(
|
||||
guardian: user.guardian,
|
||||
thread_ids: [message.thread_id],
|
||||
include_threads: true,
|
||||
).find_thread(message.thread_id)
|
||||
end
|
||||
|
||||
MessageBus.publish(
|
||||
self.user_tracking_state_message_bus_channel(user.id),
|
||||
{ channel_id: chat_channel_id, last_read_message_id: chat_message_id }.merge(
|
||||
tracking_data.report.find_channel(chat_channel_id),
|
||||
).as_json,
|
||||
data.as_json,
|
||||
user_ids: [user.id],
|
||||
)
|
||||
end
|
||||
@ -278,7 +314,7 @@ module Chat
|
||||
"/chat/bulk-user-tracking-state/#{user_id}"
|
||||
end
|
||||
|
||||
def self.publish_bulk_user_tracking_state(user, channel_last_read_map)
|
||||
def self.publish_bulk_user_tracking_state!(user, channel_last_read_map)
|
||||
tracking_data =
|
||||
Chat::TrackingState.call(
|
||||
guardian: Guardian.new(user),
|
||||
|
@ -19,7 +19,7 @@ module Chat
|
||||
model :channel
|
||||
model :active_membership
|
||||
policy :invalid_access
|
||||
policy :ensure_message_exists
|
||||
model :message
|
||||
policy :ensure_message_id_recency
|
||||
step :update_last_read_message_id
|
||||
step :mark_associated_mentions_as_read
|
||||
@ -47,29 +47,29 @@ module Chat
|
||||
guardian.can_join_chat_channel?(active_membership.chat_channel)
|
||||
end
|
||||
|
||||
def ensure_message_exists(channel:, contract:, **)
|
||||
::Chat::Message.with_deleted.exists?(chat_channel_id: channel.id, id: contract.message_id)
|
||||
def fetch_message(channel:, contract:, **)
|
||||
::Chat::Message.with_deleted.find_by(chat_channel_id: channel.id, id: contract.message_id)
|
||||
end
|
||||
|
||||
def ensure_message_id_recency(contract:, active_membership:, **)
|
||||
def ensure_message_id_recency(message:, active_membership:, **)
|
||||
!active_membership.last_read_message_id ||
|
||||
contract.message_id >= active_membership.last_read_message_id
|
||||
message.id >= active_membership.last_read_message_id
|
||||
end
|
||||
|
||||
def update_last_read_message_id(contract:, active_membership:, **)
|
||||
active_membership.update!(last_read_message_id: contract.message_id)
|
||||
def update_last_read_message_id(message:, active_membership:, **)
|
||||
active_membership.update!(last_read_message_id: message.id)
|
||||
end
|
||||
|
||||
def mark_associated_mentions_as_read(active_membership:, contract:, **)
|
||||
def mark_associated_mentions_as_read(active_membership:, message:, **)
|
||||
::Chat::Action::MarkMentionsRead.call(
|
||||
active_membership.user,
|
||||
channel_ids: [active_membership.chat_channel.id],
|
||||
message_id: contract.message_id,
|
||||
message_id: message.id,
|
||||
)
|
||||
end
|
||||
|
||||
def publish_new_last_read_to_clients(guardian:, channel:, contract:, **)
|
||||
::Chat::Publisher.publish_user_tracking_state(guardian.user, channel.id, contract.message_id)
|
||||
def publish_new_last_read_to_clients(guardian:, channel:, message:, **)
|
||||
::Chat::Publisher.publish_user_tracking_state!(guardian.user, channel, message)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
@ -70,10 +70,10 @@ module Chat
|
||||
end
|
||||
|
||||
def publish_new_last_read_to_clients(guardian:, thread:, **)
|
||||
::Chat::Publisher.publish_user_tracking_state(
|
||||
::Chat::Publisher.publish_user_tracking_state!(
|
||||
guardian.user,
|
||||
thread.channel_id,
|
||||
thread.replies.last.id,
|
||||
thread.channel,
|
||||
thread.replies.last,
|
||||
)
|
||||
end
|
||||
end
|
||||
|
@ -188,6 +188,16 @@ export default class ChatLivePane extends Component {
|
||||
this.args.channel.addMessages(messages);
|
||||
this.args.channel.details = meta;
|
||||
|
||||
if (result.threads) {
|
||||
result.threads.forEach((thread) => {
|
||||
this.args.channel.threadsManager.store(this.args.channel, thread);
|
||||
});
|
||||
}
|
||||
|
||||
if (result.unread_thread_ids) {
|
||||
this.args.channel.unreadThreadIds = result.unread_thread_ids;
|
||||
}
|
||||
|
||||
if (this.requestedTargetMessageId) {
|
||||
this.scrollToMessage(findArgs["targetMessageId"], {
|
||||
highlight: true,
|
||||
@ -205,18 +215,6 @@ export default class ChatLivePane extends Component {
|
||||
}
|
||||
|
||||
this.scrollToBottom();
|
||||
|
||||
if (result.threads) {
|
||||
result.threads.forEach((thread) => {
|
||||
this.args.channel.threadsManager.store(this.args.channel, thread);
|
||||
});
|
||||
}
|
||||
|
||||
if (result.thread_tracking_overview) {
|
||||
this.args.channel.threadTrackingOverview.push(
|
||||
...result.thread_tracking_overview
|
||||
);
|
||||
}
|
||||
})
|
||||
.catch(this._handleErrors)
|
||||
.finally(() => {
|
||||
|
@ -1,7 +1,7 @@
|
||||
<div class="chat-drawer-header__right-actions">
|
||||
<div class="chat-drawer-header__top-line">
|
||||
{{#if this.chat.activeChannel.threadingEnabled}}
|
||||
<ChatDrawer::Header::ThreadListButton />
|
||||
<Chat::Thread::ThreadsListButton @channel={{this.chat.activeChannel}} />
|
||||
{{/if}}
|
||||
|
||||
<ChatDrawer::Header::ToggleExpandButton
|
||||
|
@ -1,9 +0,0 @@
|
||||
<LinkTo
|
||||
@route="chat.channel.threads"
|
||||
@models={{this.chat.activeChannel.routeModels}}
|
||||
title={{i18n "chat.threads.list"}}
|
||||
class="open-thread-list-btn btn btn-link btn-flat chat-drawer-header__thread-list-btn"
|
||||
{{on "click" this.stopPropagation}}
|
||||
>
|
||||
{{d-icon "discourse-threads"}}
|
||||
</LinkTo>
|
@ -1,11 +0,0 @@
|
||||
import Component from "@glimmer/component";
|
||||
import { inject as service } from "@ember/service";
|
||||
import { action } from "@ember/object";
|
||||
export default class ChatDrawerThreadListButton extends Component {
|
||||
@service chat;
|
||||
|
||||
@action
|
||||
stopPropagation(event) {
|
||||
event.stopPropagation();
|
||||
}
|
||||
}
|
@ -14,7 +14,7 @@ export default class ChatDrawerThread extends Component {
|
||||
models: this.chat.activeChannel.routeModels,
|
||||
};
|
||||
|
||||
if (this.chatDrawerRouter.previousRouteName === "chat.channel.threads") {
|
||||
if (this.chatDrawerRouter.previousRoute?.name === "chat.channel.threads") {
|
||||
link.title = "chat.return_to_threads_list";
|
||||
link.route = "chat.channel.threads";
|
||||
} else {
|
||||
|
@ -41,14 +41,7 @@
|
||||
{{/if}}
|
||||
|
||||
{{#if @channel.threadingEnabled}}
|
||||
<LinkTo
|
||||
@route="chat.channel.threads"
|
||||
@models={{@channel.routeModels}}
|
||||
title={{i18n "chat.threads.list"}}
|
||||
class="open-thread-list-btn btn btn-flat"
|
||||
>
|
||||
{{d-icon "discourse-threads"}}
|
||||
</LinkTo>
|
||||
<Chat::Thread::ThreadsListButton @channel={{@channel}} />
|
||||
{{/if}}
|
||||
</div>
|
||||
{{/if}}
|
||||
|
@ -25,7 +25,7 @@
|
||||
<div
|
||||
class="chat-thread__body popper-viewport"
|
||||
{{did-insert this.setScrollable}}
|
||||
{{on "scroll" this.resetActiveMessage passive=true}}
|
||||
{{on "scroll" this.computeScrollState passive=true}}
|
||||
>
|
||||
<div
|
||||
class="chat-thread__messages chat-messages-container"
|
||||
|
@ -6,11 +6,12 @@ import ChatMessage from "discourse/plugins/chat/discourse/models/chat-message";
|
||||
import { popupAjaxError } from "discourse/lib/ajax-error";
|
||||
import { bind, debounce } from "discourse-common/utils/decorators";
|
||||
import { inject as service } from "@ember/service";
|
||||
import { next, schedule } from "@ember/runloop";
|
||||
import { cancel, next, schedule } from "@ember/runloop";
|
||||
import discourseLater from "discourse-common/lib/later";
|
||||
import { resetIdle } from "discourse/lib/desktop-notifications";
|
||||
|
||||
const PAGE_SIZE = 100;
|
||||
const READ_INTERVAL_MS = 1000;
|
||||
|
||||
export default class ChatThreadPanel extends Component {
|
||||
@service siteSettings;
|
||||
@ -53,6 +54,66 @@ export default class ChatThreadPanel extends Component {
|
||||
this.chatChannelThreadPaneSubscriptionsManager.unsubscribe();
|
||||
}
|
||||
|
||||
// TODO (martin) This needs to have the extended scroll/message visibility/
|
||||
// mark read behaviour the same as the channel.
|
||||
@action
|
||||
computeScrollState() {
|
||||
cancel(this.onScrollEndedHandler);
|
||||
|
||||
if (!this.scrollable) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.resetActiveMessage();
|
||||
|
||||
if (this.#isAtBottom()) {
|
||||
this.updateLastReadMessage();
|
||||
this.onScrollEnded();
|
||||
} else {
|
||||
this.isScrolling = true;
|
||||
this.onScrollEndedHandler = discourseLater(this, this.onScrollEnded, 150);
|
||||
}
|
||||
}
|
||||
|
||||
#isAtBottom() {
|
||||
if (!this.scrollable) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// This is different from the channel scrolling because the scrolling here
|
||||
// is inverted -- in the channel's case scrollTop is 0 when scrolled to the
|
||||
// bottom of the channel, but in the negatives when scrolling up to past messages.
|
||||
//
|
||||
// c.f. https://developer.mozilla.org/en-US/docs/Web/API/Element/scrollHeight#determine_if_an_element_has_been_totally_scrolled
|
||||
return (
|
||||
Math.abs(
|
||||
this.scrollable.scrollHeight -
|
||||
this.scrollable.clientHeight -
|
||||
this.scrollable.scrollTop
|
||||
) <= 2
|
||||
);
|
||||
}
|
||||
|
||||
@bind
|
||||
onScrollEnded() {
|
||||
this.isScrolling = false;
|
||||
}
|
||||
|
||||
@debounce(READ_INTERVAL_MS)
|
||||
updateLastReadMessage() {
|
||||
schedule("afterRender", () => {
|
||||
if (this._selfDeleted) {
|
||||
return;
|
||||
}
|
||||
|
||||
// TODO (martin) HACK: We don't have proper scroll visibility over
|
||||
// what message we are looking at, don't have the lastReadMessageId
|
||||
// for the thread, and this updateLastReadMessage function is only
|
||||
// called when scrolling all the way to the bottom.
|
||||
this.markThreadAsRead();
|
||||
});
|
||||
}
|
||||
|
||||
@action
|
||||
setScrollable(element) {
|
||||
this.scrollable = element;
|
||||
|
@ -0,0 +1,9 @@
|
||||
{{#if this.showUnreadIndicator}}
|
||||
<div class="chat-thread-header-unread-indicator">
|
||||
<div class="chat-thread-header-unread-indicator__number-wrap">
|
||||
<div
|
||||
class="chat-thread-header-unread-indicator__number"
|
||||
>{{this.unreadCountLabel}}</div>
|
||||
</div>
|
||||
</div>
|
||||
{{/if}}
|
@ -0,0 +1,18 @@
|
||||
import Component from "@glimmer/component";
|
||||
import { inject as service } from "@ember/service";
|
||||
|
||||
export default class ChatThreadHeaderUnreadIndicator extends Component {
|
||||
@service chatTrackingStateManager;
|
||||
|
||||
get showUnreadIndicator() {
|
||||
return this.args.channel.unreadThreadCount > 0;
|
||||
}
|
||||
|
||||
get unreadCountLabel() {
|
||||
if (this.args.channel.unreadThreadCount > 99) {
|
||||
return "99+";
|
||||
}
|
||||
|
||||
return this.args.channel.unreadThreadCount;
|
||||
}
|
||||
}
|
@ -1,4 +1,10 @@
|
||||
<div class="chat-thread-list-item" data-thread-id={{@thread.id}}>
|
||||
<div
|
||||
class={{concat-class
|
||||
"chat-thread-list-item"
|
||||
(if (gt @thread.tracking.unreadCount 0) "-unread")
|
||||
}}
|
||||
data-thread-id={{@thread.id}}
|
||||
>
|
||||
<div class="chat-thread-list-item__main">
|
||||
<div
|
||||
title={{i18n "chat.thread.view_thread"}}
|
||||
|
@ -0,0 +1,15 @@
|
||||
<LinkTo
|
||||
@route="chat.channel.threads"
|
||||
@models={{@channel.routeModels}}
|
||||
title={{i18n "chat.threads.list"}}
|
||||
class={{concat-class
|
||||
"chat-threads-list-button btn btn-flat"
|
||||
(if @channel.unreadThreadCount "-has-unreads")
|
||||
}}
|
||||
>
|
||||
{{d-icon "discourse-threads"}}
|
||||
|
||||
{{#unless this.currentUserInDnD}}
|
||||
<Chat::Thread::HeaderUnreadIndicator @channel={{@channel}} />
|
||||
{{/unless}}
|
||||
</LinkTo>
|
@ -0,0 +1,10 @@
|
||||
import { inject as service } from "@ember/service";
|
||||
import Component from "@glimmer/component";
|
||||
|
||||
export default class ChatThreadsListButton extends Component {
|
||||
@service currentUser;
|
||||
|
||||
get currentUserInDnD() {
|
||||
return this.currentUser.isInDoNotDisturb();
|
||||
}
|
||||
}
|
@ -15,6 +15,7 @@ import { TrackedObject } from "@ember-compat/tracked-built-ins";
|
||||
|
||||
export default class ChatThreadsManager {
|
||||
@service chatSubscriptionsManager;
|
||||
@service chatTrackingStateManager;
|
||||
@service chatApi;
|
||||
@service chat;
|
||||
@service currentUser;
|
||||
@ -43,6 +44,12 @@ export default class ChatThreadsManager {
|
||||
thread
|
||||
);
|
||||
});
|
||||
|
||||
this.chatTrackingStateManager.setupChannelThreadState(
|
||||
this.chat.activeChannel,
|
||||
result.tracking
|
||||
);
|
||||
|
||||
return { threads, meta: result.meta };
|
||||
});
|
||||
}
|
||||
|
@ -1,5 +1,5 @@
|
||||
import UserChatChannelMembership from "discourse/plugins/chat/discourse/models/user-chat-channel-membership";
|
||||
import { TrackedArray } from "@ember-compat/tracked-built-ins";
|
||||
import { TrackedSet } from "@ember-compat/tracked-built-ins";
|
||||
import { ajax } from "discourse/lib/ajax";
|
||||
import { escapeExpression } from "discourse/lib/utilities";
|
||||
import { tracked } from "@glimmer/tracking";
|
||||
@ -90,11 +90,12 @@ export default class ChatChannel {
|
||||
@tracked archive;
|
||||
@tracked tracking;
|
||||
@tracked threadingEnabled = false;
|
||||
@tracked threadTrackingOverview = new TrackedArray();
|
||||
|
||||
threadsManager = new ChatThreadsManager(getOwner(this));
|
||||
messagesManager = new ChatMessagesManager(getOwner(this));
|
||||
|
||||
@tracked _unreadThreadIds = new TrackedSet();
|
||||
|
||||
constructor(args = {}) {
|
||||
this.id = args.id;
|
||||
this.chatableId = args.chatable_id;
|
||||
@ -132,6 +133,18 @@ export default class ChatChannel {
|
||||
this.tracking = new ChatTrackingState(getOwner(this));
|
||||
}
|
||||
|
||||
get unreadThreadCount() {
|
||||
return this.unreadThreadIds.size;
|
||||
}
|
||||
|
||||
get unreadThreadIds() {
|
||||
return this._unreadThreadIds;
|
||||
}
|
||||
|
||||
set unreadThreadIds(unreadThreadIds) {
|
||||
this._unreadThreadIds = new TrackedSet(unreadThreadIds);
|
||||
}
|
||||
|
||||
findIndexOfMessage(id) {
|
||||
return this.messagesManager.findIndexOfMessage(id);
|
||||
}
|
||||
|
@ -4,6 +4,7 @@ import { escapeExpression } from "discourse/lib/utilities";
|
||||
import { tracked } from "@glimmer/tracking";
|
||||
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";
|
||||
|
||||
export const THREAD_STATUSES = {
|
||||
open: "open",
|
||||
@ -26,6 +27,7 @@ export default class ChatThread {
|
||||
@tracked originalMessage;
|
||||
@tracked threadMessageBusLastId;
|
||||
@tracked replyCount;
|
||||
@tracked tracking;
|
||||
|
||||
messagesManager = new ChatMessagesManager(getOwner(this));
|
||||
|
||||
@ -38,6 +40,8 @@ export default class ChatThread {
|
||||
this.staged = args.staged;
|
||||
this.replyCount = args.reply_count;
|
||||
this.originalMessage = ChatMessage.create(channel, args.original_message);
|
||||
|
||||
this.tracking = new ChatTrackingState(getOwner(this));
|
||||
}
|
||||
|
||||
stageMessage(message) {
|
||||
|
@ -6,7 +6,7 @@ import ChatDrawerThread from "discourse/plugins/chat/discourse/components/chat-d
|
||||
import ChatDrawerThreads from "discourse/plugins/chat/discourse/components/chat-drawer/threads";
|
||||
import ChatDrawerIndex from "discourse/plugins/chat/discourse/components/chat-drawer/index";
|
||||
|
||||
const COMPONENTS_MAP = {
|
||||
const ROUTES = {
|
||||
"chat.draft-channel": { name: ChatDrawerDraftChannel },
|
||||
"chat.channel": { name: ChatDrawerChannel },
|
||||
"chat.channel.thread": {
|
||||
@ -50,23 +50,40 @@ const COMPONENTS_MAP = {
|
||||
export default class ChatDrawerRouter extends Service {
|
||||
@service router;
|
||||
@tracked component = null;
|
||||
@tracked drawerRoute = null;
|
||||
@tracked params = null;
|
||||
@tracked history = [];
|
||||
|
||||
get previousRouteName() {
|
||||
get previousRoute() {
|
||||
if (this.history.length > 1) {
|
||||
return this.history[this.history.length - 2];
|
||||
}
|
||||
}
|
||||
|
||||
get currentRoute() {
|
||||
if (this.history.length > 0) {
|
||||
return this.history[this.history.length - 1];
|
||||
}
|
||||
}
|
||||
|
||||
stateFor(route) {
|
||||
this.history.push(route.name);
|
||||
this.drawerRoute?.deactivate?.(this.currentRoute);
|
||||
|
||||
this.history.push(route);
|
||||
if (this.history.length > 10) {
|
||||
this.history.shift();
|
||||
}
|
||||
|
||||
const component = COMPONENTS_MAP[route.name];
|
||||
this.params = component?.extractParams?.(route) || route.params;
|
||||
this.component = component?.name || ChatDrawerIndex;
|
||||
this.drawerRoute = ROUTES[route.name];
|
||||
|
||||
if (!this.drawerRoute) {
|
||||
// TODO (joffrey) maybe we should implement the equivalent of a 404 here?
|
||||
return;
|
||||
}
|
||||
|
||||
this.params = this.drawerRoute?.extractParams?.(route) || route.params;
|
||||
this.component = this.drawerRoute?.name || ChatDrawerIndex;
|
||||
|
||||
this.drawerRoute.activate?.(route);
|
||||
}
|
||||
}
|
||||
|
@ -179,6 +179,17 @@ export default class ChatSubscriptionsManager extends Service {
|
||||
|
||||
@bind
|
||||
_onNewMessages(busData) {
|
||||
switch (busData.type) {
|
||||
case "channel":
|
||||
this._onNewChannelMessage(busData);
|
||||
break;
|
||||
case "thread":
|
||||
this._onNewThreadMessage(busData);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
_onNewChannelMessage(busData) {
|
||||
this.chatChannelsManager.find(busData.channel_id).then((channel) => {
|
||||
if (busData.user_id === this.currentUser.id) {
|
||||
// User sent message, update tracking state to no unread
|
||||
@ -188,13 +199,18 @@ export default class ChatSubscriptionsManager extends Service {
|
||||
if (this.currentUser.ignored_users.includes(busData.username)) {
|
||||
channel.currentUserMembership.lastReadMessageId = busData.message_id;
|
||||
} else {
|
||||
// Message from other user. Increment trackings state
|
||||
// Message from other user. Increment unread for channel tracking state.
|
||||
if (
|
||||
busData.message_id >
|
||||
(channel.currentUserMembership.lastReadMessageId || 0)
|
||||
) {
|
||||
channel.tracking.unreadCount++;
|
||||
}
|
||||
|
||||
// Thread should be considered unread if not already.
|
||||
if (busData.thread_id) {
|
||||
channel.unreadThreadIds.add(busData.thread_id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -202,6 +218,35 @@ export default class ChatSubscriptionsManager extends Service {
|
||||
});
|
||||
}
|
||||
|
||||
_onNewThreadMessage(busData) {
|
||||
this.chatChannelsManager.find(busData.channel_id).then((channel) => {
|
||||
channel.threadsManager
|
||||
.find(busData.channel_id, busData.thread_id)
|
||||
.then((thread) => {
|
||||
if (busData.user_id === this.currentUser.id) {
|
||||
// Thread should no longer be considered unread.
|
||||
channel.unreadThreadIds.remove(busData.thread_id);
|
||||
// TODO (martin) Update lastReadMessageId for thread membership on client.
|
||||
} else {
|
||||
if (this.currentUser.ignored_users.includes(busData.username)) {
|
||||
// TODO (martin) Update lastReadMessageId for thread membership on client.
|
||||
} else {
|
||||
if (
|
||||
this.chat.activeChannel?.activeThread?.id === busData.thread_id
|
||||
) {
|
||||
// TODO (martin) HACK: We don't yet have the lastReadMessageId on the client,
|
||||
// so if the user is looking at the thread don't do anything to mark it unread.
|
||||
} else {
|
||||
// Message from other user. Thread should be considered unread if not already.
|
||||
channel.unreadThreadIds.add(busData.thread_id);
|
||||
thread.tracking.unreadCount += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
_startUserTrackingStateSubscription(lastId) {
|
||||
if (!this.currentUser) {
|
||||
return;
|
||||
@ -248,13 +293,31 @@ export default class ChatSubscriptionsManager extends Service {
|
||||
}
|
||||
|
||||
@bind
|
||||
_updateChannelTrackingData(channelId, trackingData) {
|
||||
_updateChannelTrackingData(channelId, busData) {
|
||||
this.chatChannelsManager.find(channelId).then((channel) => {
|
||||
channel.currentUserMembership.lastReadMessageId =
|
||||
trackingData.last_read_message_id;
|
||||
if (busData.thread_id) {
|
||||
// TODO (martin) Update thread membership last read message ID on client.
|
||||
} else {
|
||||
channel.currentUserMembership.lastReadMessageId =
|
||||
busData.last_read_message_id;
|
||||
}
|
||||
|
||||
channel.tracking.unreadCount = trackingData.unread_count;
|
||||
channel.tracking.mentionCount = trackingData.mention_count;
|
||||
channel.tracking.unreadCount = busData.unread_count;
|
||||
channel.tracking.mentionCount = busData.mention_count;
|
||||
|
||||
if (busData.hasOwnProperty("unread_thread_ids")) {
|
||||
channel.unreadThreadIds = busData.unread_thread_ids;
|
||||
}
|
||||
|
||||
if (busData.thread_id && busData.hasOwnProperty("thread_tracking")) {
|
||||
channel.threadsManager
|
||||
.find(channelId, busData.thread_id)
|
||||
.then((thread) => {
|
||||
thread.tracking.unreadCount = busData.thread_tracking.unread_count;
|
||||
thread.tracking.mentionCount =
|
||||
busData.thread_tracking.mention_count;
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -32,6 +32,18 @@ export default class ChatTrackingStateManager extends Service {
|
||||
});
|
||||
}
|
||||
|
||||
setupChannelThreadState(channel, threadTracking) {
|
||||
channel.threadsManager.threads.forEach((thread) => {
|
||||
// TODO (martin) Since we didn't backfill data for thread membership,
|
||||
// there are cases where we are getting threads the user "participated"
|
||||
// in but don't have tracking state for them. We need a migration to
|
||||
// backfill this data.
|
||||
if (threadTracking[thread.id.toString()]) {
|
||||
this.#setState(thread, threadTracking[thread.id.toString()]);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
get publicChannelUnreadCount() {
|
||||
return this.#publicChannels().reduce((unreadCount, channel) => {
|
||||
return unreadCount + channel.tracking.unreadCount;
|
||||
|
@ -56,6 +56,10 @@ export default class Chat extends Service {
|
||||
this._activeMessage = null;
|
||||
}
|
||||
|
||||
if (this._activeChannel) {
|
||||
this._activeChannel.activeThread = null;
|
||||
}
|
||||
|
||||
this._activeChannel = channel;
|
||||
}
|
||||
|
||||
@ -123,6 +127,13 @@ export default class Chat extends Service {
|
||||
this.chatChannelsManager
|
||||
.find(channelObject.id, { fetchIfNotFound: false })
|
||||
.then((channel) => {
|
||||
// TODO (martin) We need to do something here for thread tracking
|
||||
// state as well on presence change, otherwise we will be back in
|
||||
// the same place as the channels were.
|
||||
//
|
||||
// At some point it would likely be better to just fetch an
|
||||
// endpoint that gives you all channel tracking state and the
|
||||
// thread tracking state for the current channel.
|
||||
if (channel) {
|
||||
channel.updateMembership(channelObject.current_user_membership);
|
||||
|
||||
|
@ -1,3 +1,19 @@
|
||||
@mixin chat-channel-header-button {
|
||||
color: var(--primary-low-mid);
|
||||
|
||||
&:visited {
|
||||
color: var(--primary-low-mid);
|
||||
}
|
||||
|
||||
&:hover {
|
||||
color: var(--primary);
|
||||
}
|
||||
|
||||
> * {
|
||||
pointer-events: none;
|
||||
}
|
||||
}
|
||||
|
||||
.chat-channel {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@ -9,39 +25,8 @@
|
||||
min-width: 250px;
|
||||
@include chat-height;
|
||||
|
||||
.open-drawer-btn,
|
||||
.open-thread-list-btn {
|
||||
color: var(--primary-low-mid);
|
||||
|
||||
&:visited {
|
||||
color: var(--primary-low-mid);
|
||||
}
|
||||
|
||||
&:hover {
|
||||
color: var(--primary);
|
||||
}
|
||||
|
||||
> * {
|
||||
pointer-events: none;
|
||||
}
|
||||
}
|
||||
|
||||
.open-thread-list-btn {
|
||||
.d-icon {
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
.discourse-touch & {
|
||||
background: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
&:active {
|
||||
.discourse-touch & {
|
||||
background: var(--secondary-very-high) !important;
|
||||
}
|
||||
}
|
||||
.open-drawer-btn {
|
||||
@include chat-channel-header-button;
|
||||
}
|
||||
|
||||
.chat-messages-scroll {
|
||||
|
@ -248,6 +248,10 @@ a.chat-drawer-header__title {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&__thread-list-btn.-has-unreads {
|
||||
margin-right: 0.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
.chat-drawer-content {
|
||||
|
@ -0,0 +1,53 @@
|
||||
.chat-threads-list-button {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
border: 1px solid var(--blend-primary-secondary-5);
|
||||
border-radius: 5px;
|
||||
|
||||
@include chat-channel-header-button;
|
||||
|
||||
&.-has-unreads {
|
||||
.d-icon {
|
||||
color: var(--tertiary-med-or-tertiary);
|
||||
}
|
||||
}
|
||||
|
||||
.d-icon {
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
&:active {
|
||||
.discourse-touch & {
|
||||
background: var(--secondary-very-high) !important;
|
||||
}
|
||||
}
|
||||
}
|
@ -16,6 +16,10 @@
|
||||
.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;
|
||||
}
|
||||
|
@ -52,3 +52,4 @@
|
||||
@import "chat-threads-list";
|
||||
@import "chat-thread-original-message";
|
||||
@import "chat-composer-separator";
|
||||
@import "chat-thread-header-button";
|
||||
|
@ -148,7 +148,7 @@ RSpec.describe Chat::ChannelViewBuilder do
|
||||
thread = Fabricate(:chat_thread, channel: channel)
|
||||
thread.add(current_user)
|
||||
message_1 = Fabricate(:chat_message, chat_channel: channel, thread: thread)
|
||||
expect(subject.view.thread_tracking_overview).to eq([message_1.thread.id])
|
||||
expect(subject.view.unread_thread_ids).to eq([message_1.thread.id])
|
||||
end
|
||||
|
||||
it "fetches the tracking state of threads in the channel" do
|
||||
|
@ -219,6 +219,7 @@ describe Chat::Publisher do
|
||||
end
|
||||
expect(messages.first.data).to eq(
|
||||
{
|
||||
type: "channel",
|
||||
channel_id: channel.id,
|
||||
message_id: message_1.id,
|
||||
user_id: message_1.user_id,
|
||||
@ -270,12 +271,21 @@ describe Chat::Publisher do
|
||||
context "if threading_enabled is true for the channel" do
|
||||
before { channel.update!(threading_enabled: true) }
|
||||
|
||||
it "does not publish to the new_messages_message_bus_channel" do
|
||||
it "does publish to the new_messages_message_bus_channel" do
|
||||
messages =
|
||||
MessageBus.track_publish(
|
||||
described_class.new_messages_message_bus_channel(channel.id),
|
||||
) { described_class.publish_new!(channel, message_1, staged_id) }
|
||||
expect(messages).to be_empty
|
||||
expect(messages.first.data).to eq(
|
||||
{
|
||||
type: "thread",
|
||||
channel_id: channel.id,
|
||||
message_id: message_1.id,
|
||||
user_id: message_1.user_id,
|
||||
username: message_1.user.username,
|
||||
thread_id: thread.id,
|
||||
},
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
@ -65,7 +65,7 @@ RSpec.describe Chat::UpdateUserLastRead do
|
||||
membership.update!(last_read_message_id: 1)
|
||||
end
|
||||
|
||||
it { is_expected.to fail_a_policy(:ensure_message_exists) }
|
||||
it { is_expected.to fail_to_find_a_model(:message) }
|
||||
end
|
||||
|
||||
context "when everything is fine" do
|
||||
|
@ -52,10 +52,6 @@ module PageObjects
|
||||
find(".open-drawer-btn").click
|
||||
end
|
||||
|
||||
def open_thread_list
|
||||
find(".open-thread-list-btn").click
|
||||
end
|
||||
|
||||
def has_message?(message)
|
||||
container = find(".chat-message-container[data-id=\"#{message.id}\"")
|
||||
container.has_content?(message.message)
|
||||
|
@ -214,6 +214,26 @@ module PageObjects
|
||||
find(message_thread_indicator_selector(message))
|
||||
end
|
||||
|
||||
def open_thread_list
|
||||
find(thread_list_button_selector).click
|
||||
end
|
||||
|
||||
def has_unread_thread_indicator?(count:)
|
||||
has_css?("#{thread_list_button_selector}.-has-unreads") &&
|
||||
has_css?(
|
||||
".chat-thread-header-unread-indicator .chat-thread-header-unread-indicator__number-wrap",
|
||||
text: count.to_s,
|
||||
)
|
||||
end
|
||||
|
||||
def has_no_unread_thread_indicator?
|
||||
has_no_css?("#{thread_list_button_selector}.-has-unreads")
|
||||
end
|
||||
|
||||
def thread_list_button_selector
|
||||
".chat-threads-list-button"
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def message_thread_indicator_selector(message)
|
||||
|
@ -7,6 +7,14 @@ module PageObjects
|
||||
find(item_by_id_selector(id))
|
||||
end
|
||||
|
||||
def has_unread_item?(id)
|
||||
has_css?(item_by_id_selector(id) + ".-unread")
|
||||
end
|
||||
|
||||
def has_no_unread_item?(id)
|
||||
has_no_css?(item_by_id_selector(id) + ".-unread")
|
||||
end
|
||||
|
||||
def item_by_id_selector(id)
|
||||
".chat-thread-list__items .chat-thread-list-item[data-thread-id=\"#{id}\"]"
|
||||
end
|
||||
|
@ -46,6 +46,26 @@ module PageObjects
|
||||
def has_open_thread_list?
|
||||
has_css?("#{VISIBLE_DRAWER} .chat-thread-list")
|
||||
end
|
||||
|
||||
def open_thread_list
|
||||
find(thread_list_button_selector).click
|
||||
end
|
||||
|
||||
def thread_list_button_selector
|
||||
".chat-threads-list-button"
|
||||
end
|
||||
|
||||
def has_unread_thread_indicator?(count:)
|
||||
has_css?("#{thread_list_button_selector}.-has-unreads") &&
|
||||
has_css?(
|
||||
".chat-thread-header-unread-indicator .chat-thread-header-unread-indicator__number-wrap",
|
||||
text: count.to_s,
|
||||
)
|
||||
end
|
||||
|
||||
def has_no_unread_thread_indicator?
|
||||
has_no_css?("#{thread_list_button_selector}.-has-unreads")
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
@ -25,7 +25,9 @@ describe "Thread list in side panel | drawer", type: :system, js: true do
|
||||
visit("/")
|
||||
chat_page.open_from_header
|
||||
drawer_page.open_channel(channel)
|
||||
expect(find(".chat-drawer-header__right-actions")).not_to have_css(".open-thread-list-btn")
|
||||
expect(find(".chat-drawer-header__right-actions")).not_to have_css(
|
||||
drawer_page.thread_list_button_selector,
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
@ -55,7 +57,7 @@ describe "Thread list in side panel | drawer", type: :system, js: true do
|
||||
visit("/")
|
||||
chat_page.open_from_header
|
||||
drawer_page.open_channel(channel)
|
||||
find(".open-thread-list-btn").click
|
||||
drawer_page.open_thread_list
|
||||
expect(drawer_page).to have_open_thread_list
|
||||
end
|
||||
|
||||
@ -63,7 +65,7 @@ describe "Thread list in side panel | drawer", type: :system, js: true do
|
||||
visit("/")
|
||||
chat_page.open_from_header
|
||||
drawer_page.open_channel(channel)
|
||||
find(".open-thread-list-btn").click
|
||||
drawer_page.open_thread_list
|
||||
expect(drawer_page).to have_open_thread_list
|
||||
expect(thread_list_page).to have_content(thread_1.title)
|
||||
expect(thread_list_page).to have_content(thread_2.title)
|
||||
@ -73,7 +75,7 @@ describe "Thread list in side panel | drawer", type: :system, js: true do
|
||||
visit("/")
|
||||
chat_page.open_from_header
|
||||
drawer_page.open_channel(channel)
|
||||
find(".open-thread-list-btn").click
|
||||
drawer_page.open_thread_list
|
||||
expect(drawer_page).to have_open_thread_list
|
||||
thread_list_page.item_by_id(thread_1.id).click
|
||||
expect(drawer_page).to have_open_thread(thread_1)
|
||||
|
@ -20,7 +20,7 @@ describe "Thread list in side panel | full page", type: :system, js: true do
|
||||
context "when there are no threads that the user is participating in" do
|
||||
it "shows a message" do
|
||||
chat_page.visit_channel(channel)
|
||||
chat_page.open_thread_list
|
||||
channel_page.open_thread_list
|
||||
expect(page).to have_content(I18n.t("js.chat.threads.none"))
|
||||
end
|
||||
end
|
||||
@ -37,14 +37,14 @@ describe "Thread list in side panel | full page", type: :system, js: true do
|
||||
|
||||
it "shows a default title for threads without a title" do
|
||||
chat_page.visit_channel(channel)
|
||||
chat_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))
|
||||
end
|
||||
|
||||
it "shows the thread title with emoji" do
|
||||
thread_1.update!(title: "What is for dinner? :hamburger:")
|
||||
chat_page.visit_channel(channel)
|
||||
chat_page.open_thread_list
|
||||
channel_page.open_thread_list
|
||||
expect(thread_list_page.item_by_id(thread_1.id)).to have_content("What is for dinner?")
|
||||
expect(thread_list_page.item_by_id(thread_1.id)).to have_css("img.emoji[alt='hamburger']")
|
||||
end
|
||||
@ -53,7 +53,7 @@ describe "Thread list in side panel | full page", type: :system, js: true do
|
||||
thread_1.original_message.update!(message: "This is a long message for the excerpt")
|
||||
thread_1.original_message.rebake!
|
||||
chat_page.visit_channel(channel)
|
||||
chat_page.open_thread_list
|
||||
channel_page.open_thread_list
|
||||
expect(thread_list_page.item_by_id(thread_1.id)).to have_content(
|
||||
"This is a long message for the excerpt",
|
||||
)
|
||||
@ -61,7 +61,7 @@ describe "Thread list in side panel | full page", type: :system, js: true do
|
||||
|
||||
it "shows the thread original message user username and avatar" do
|
||||
chat_page.visit_channel(channel)
|
||||
chat_page.open_thread_list
|
||||
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",
|
||||
)
|
||||
@ -72,7 +72,7 @@ describe "Thread list in side panel | full page", type: :system, js: true do
|
||||
|
||||
it "opens a thread" do
|
||||
chat_page.visit_channel(channel)
|
||||
chat_page.open_thread_list
|
||||
channel_page.open_thread_list
|
||||
thread_list_page.item_by_id(thread_1.id).click
|
||||
expect(side_panel).to have_open_thread(thread_1)
|
||||
end
|
||||
@ -82,7 +82,7 @@ describe "Thread list in side panel | full page", type: :system, js: true do
|
||||
|
||||
def open_thread_list
|
||||
chat_page.visit_channel(channel)
|
||||
chat_page.open_thread_list
|
||||
channel_page.open_thread_list
|
||||
expect(side_panel).to have_open_thread_list
|
||||
end
|
||||
|
||||
|
68
plugins/chat/spec/system/thread_tracking/drawer_spec.rb
Normal file
68
plugins/chat/spec/system/thread_tracking/drawer_spec.rb
Normal file
@ -0,0 +1,68 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
describe "Thread tracking state | drawer", type: :system, js: true do
|
||||
fab!(:current_user) { Fabricate(:admin) }
|
||||
fab!(:channel) { Fabricate(:chat_channel, threading_enabled: true) }
|
||||
fab!(:other_user) { Fabricate(:user) }
|
||||
fab!(:thread) { Fabricate(:chat_thread, channel: channel) }
|
||||
|
||||
let(:chat_page) { PageObjects::Pages::Chat.new }
|
||||
let(:channel_page) { PageObjects::Pages::ChatChannel.new }
|
||||
let(:thread_page) { PageObjects::Pages::ChatThread.new }
|
||||
let(:thread_list_page) { PageObjects::Pages::ChatThreadList.new }
|
||||
let(:drawer_page) { PageObjects::Pages::ChatDrawer.new }
|
||||
|
||||
before do
|
||||
SiteSetting.enable_experimental_chat_threaded_discussions = true
|
||||
chat_system_bootstrap(current_user, [channel])
|
||||
sign_in(current_user)
|
||||
thread.add(current_user)
|
||||
end
|
||||
|
||||
context "when the user has unread messages for a thread" do
|
||||
fab!(:message_1) { Fabricate(:chat_message, chat_channel: channel, thread: thread) }
|
||||
fab!(:message_2) do
|
||||
Fabricate(:chat_message, chat_channel: channel, thread: thread, user: current_user)
|
||||
end
|
||||
|
||||
it "shows the count of threads with unread messages on the thread list button" do
|
||||
visit("/")
|
||||
chat_page.open_from_header
|
||||
drawer_page.open_channel(channel)
|
||||
expect(drawer_page).to have_unread_thread_indicator(count: 1)
|
||||
end
|
||||
|
||||
it "shows an indicator on the unread thread in the list" do
|
||||
visit("/")
|
||||
chat_page.open_from_header
|
||||
drawer_page.open_channel(channel)
|
||||
drawer_page.open_thread_list
|
||||
expect(drawer_page).to have_open_thread_list
|
||||
expect(thread_list_page).to have_unread_item(thread.id)
|
||||
end
|
||||
|
||||
it "marks the thread as read and removes both indicators when the user opens it" do
|
||||
visit("/")
|
||||
chat_page.open_from_header
|
||||
drawer_page.open_channel(channel)
|
||||
drawer_page.open_thread_list
|
||||
thread_list_page.item_by_id(thread.id).click
|
||||
expect(drawer_page).to have_no_unread_thread_indicator
|
||||
drawer_page.open_thread_list
|
||||
expect(thread_list_page).to have_no_unread_item(thread.id)
|
||||
end
|
||||
|
||||
it "shows unread indicators for the header icon and the list when a new unread arrives" do
|
||||
message_1.trash!
|
||||
visit("/")
|
||||
chat_page.open_from_header
|
||||
drawer_page.open_channel(channel)
|
||||
drawer_page.open_thread_list
|
||||
expect(drawer_page).to have_no_unread_thread_indicator
|
||||
expect(thread_list_page).to have_no_unread_item(thread.id)
|
||||
Fabricate(:chat_message, chat_channel: channel, thread: thread)
|
||||
expect(drawer_page).to have_unread_thread_indicator(count: 1)
|
||||
expect(thread_list_page).to have_unread_item(thread.id)
|
||||
end
|
||||
end
|
||||
end
|
58
plugins/chat/spec/system/thread_tracking/full_page_spec.rb
Normal file
58
plugins/chat/spec/system/thread_tracking/full_page_spec.rb
Normal file
@ -0,0 +1,58 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
describe "Thread tracking state | full page", type: :system, js: true do
|
||||
fab!(:current_user) { Fabricate(:user) }
|
||||
fab!(:channel) { Fabricate(:chat_channel, threading_enabled: true) }
|
||||
fab!(:other_user) { Fabricate(:user) }
|
||||
fab!(:thread) { Fabricate(:chat_thread, channel: channel) }
|
||||
|
||||
let(:chat_page) { PageObjects::Pages::Chat.new }
|
||||
let(:channel_page) { PageObjects::Pages::ChatChannel.new }
|
||||
let(:thread_page) { PageObjects::Pages::ChatThread.new }
|
||||
let(:thread_list_page) { PageObjects::Pages::ChatThreadList.new }
|
||||
|
||||
before do
|
||||
SiteSetting.enable_experimental_chat_threaded_discussions = true
|
||||
chat_system_bootstrap(current_user, [channel])
|
||||
sign_in(current_user)
|
||||
thread.add(current_user)
|
||||
end
|
||||
|
||||
context "when the user has unread messages for a thread" do
|
||||
fab!(:message_1) { Fabricate(:chat_message, chat_channel: channel, thread: thread) }
|
||||
fab!(:message_2) do
|
||||
Fabricate(:chat_message, chat_channel: channel, thread: thread, user: current_user)
|
||||
end
|
||||
|
||||
it "shows the count of threads with unread messages on the thread list button" do
|
||||
chat_page.visit_channel(channel)
|
||||
expect(channel_page).to have_unread_thread_indicator(count: 1)
|
||||
end
|
||||
|
||||
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)
|
||||
end
|
||||
|
||||
it "marks the thread as read and removes both indicators when the user opens it" do
|
||||
chat_page.visit_channel(channel)
|
||||
channel_page.open_thread_list
|
||||
thread_list_page.item_by_id(thread.id).click
|
||||
expect(channel_page).to have_no_unread_thread_indicator
|
||||
channel_page.open_thread_list
|
||||
expect(thread_list_page).to have_no_unread_item(thread.id)
|
||||
end
|
||||
|
||||
it "shows unread indicators for the header icon and the list when a new unread arrives" do
|
||||
message_1.trash!
|
||||
chat_page.visit_channel(channel)
|
||||
channel_page.open_thread_list
|
||||
expect(channel_page).to have_no_unread_thread_indicator
|
||||
expect(thread_list_page).to have_no_unread_item(thread.id)
|
||||
Fabricate(:chat_message, chat_channel: channel, thread: thread)
|
||||
expect(channel_page).to have_unread_thread_indicator(count: 1)
|
||||
expect(thread_list_page).to have_unread_item(thread.id)
|
||||
end
|
||||
end
|
||||
end
|
Loading…
x
Reference in New Issue
Block a user