FEATURE: Show unread in sidebar for unread channel threads (#22342)
This commit makes it so that when the user has unread threads for a channel we show a blue dot in the sidebar (or channel index for mobile/drawer). This blue dot is slightly different from the channel unread messages: 1. It will only show if the new thread messages were created since the user last viewed the channel 2. It will be cleared when the user views the channel, but the threads are still considered unread because we want the user to click into the thread list to view them This necessitates a change to the current user serializer to also include the unread thread overview, which is all unread threads across all channels and their last reply date + time.
This commit is contained in:
parent
edc837eaf5
commit
07c3782e51
|
@ -8,11 +8,12 @@ module Chat
|
|||
attr_accessor :channel_tracking, :thread_tracking
|
||||
|
||||
class TrackingStateInfo
|
||||
attr_accessor :unread_count, :mention_count
|
||||
attr_accessor :unread_count, :mention_count, :last_reply_created_at
|
||||
|
||||
def initialize(info)
|
||||
@unread_count = info.present? ? info[:unread_count] : 0
|
||||
@mention_count = info.present? ? info[:mention_count] : 0
|
||||
@last_reply_created_at = info.present? ? info[:last_reply_created_at] : nil
|
||||
end
|
||||
|
||||
def to_hash
|
||||
|
@ -20,7 +21,11 @@ module Chat
|
|||
end
|
||||
|
||||
def to_h
|
||||
{ unread_count: unread_count, mention_count: mention_count }
|
||||
{
|
||||
unread_count: unread_count,
|
||||
mention_count: mention_count,
|
||||
last_reply_created_at: last_reply_created_at,
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -38,10 +43,34 @@ module Chat
|
|||
end
|
||||
|
||||
def find_channel_threads(channel_id)
|
||||
thread_tracking
|
||||
.select { |_, thread| thread[:channel_id] == channel_id }
|
||||
.map { |thread_id, thread| [thread_id, TrackingStateInfo.new(thread)] }
|
||||
.to_h
|
||||
thread_tracking.inject({}) do |result, (thread_id, thread)|
|
||||
if thread[:channel_id] == channel_id
|
||||
result.merge(thread_id => TrackingStateInfo.new(thread))
|
||||
else
|
||||
result
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def find_channel_thread_overviews(channel_id)
|
||||
thread_tracking.inject({}) do |result, (thread_id, thread)|
|
||||
if thread[:channel_id] == channel_id
|
||||
result.merge(thread_id => thread[:last_reply_created_at])
|
||||
else
|
||||
result
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def thread_unread_overview_by_channel
|
||||
thread_tracking.inject({}) do |acc, tt|
|
||||
thread_id = tt.first
|
||||
data = tt.second
|
||||
|
||||
acc[data[:channel_id]] = {} if !acc[data[:channel_id]]
|
||||
acc[data[:channel_id]][thread_id] = data[:last_reply_created_at]
|
||||
acc
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -7,7 +7,7 @@ module Chat
|
|||
:chat_messages,
|
||||
:can_load_more_past,
|
||||
:can_load_more_future,
|
||||
:unread_thread_ids,
|
||||
:unread_thread_overview,
|
||||
:threads,
|
||||
:tracking,
|
||||
:thread_memberships,
|
||||
|
@ -19,7 +19,7 @@ module Chat
|
|||
user:,
|
||||
can_load_more_past: nil,
|
||||
can_load_more_future: nil,
|
||||
unread_thread_ids: nil,
|
||||
unread_thread_overview: nil,
|
||||
threads: nil,
|
||||
tracking: nil,
|
||||
thread_memberships: nil,
|
||||
|
@ -30,7 +30,7 @@ module Chat
|
|||
@user = user
|
||||
@can_load_more_past = can_load_more_past
|
||||
@can_load_more_future = can_load_more_future
|
||||
@unread_thread_ids = unread_thread_ids
|
||||
@unread_thread_overview = unread_thread_overview
|
||||
@threads = threads
|
||||
@tracking = tracking
|
||||
@thread_memberships = thread_memberships
|
||||
|
|
|
@ -35,7 +35,8 @@ module Chat
|
|||
thread_ids: nil,
|
||||
include_missing_memberships: false,
|
||||
include_threads: false,
|
||||
include_read: true
|
||||
include_read: true,
|
||||
include_last_reply_details: false
|
||||
)
|
||||
report = ::Chat::TrackingStateReport.new
|
||||
|
||||
|
@ -59,24 +60,40 @@ module Chat
|
|||
if !include_threads || (thread_ids.blank? && channel_ids.blank?)
|
||||
report.thread_tracking = {}
|
||||
else
|
||||
report.thread_tracking =
|
||||
::Chat::ThreadUnreadsQuery
|
||||
.call(
|
||||
tracking =
|
||||
::Chat::ThreadUnreadsQuery.call(
|
||||
channel_ids: channel_ids,
|
||||
thread_ids: thread_ids,
|
||||
user_id: guardian.user.id,
|
||||
include_missing_memberships: include_missing_memberships,
|
||||
include_read: include_read,
|
||||
)
|
||||
|
||||
last_reply_details =
|
||||
DB.query(<<~SQL, tracking.map(&:thread_id)) if include_last_reply_details
|
||||
SELECT chat_threads.id AS thread_id, last_message.created_at
|
||||
FROM chat_threads
|
||||
INNER JOIN chat_messages AS last_message ON last_message.id = chat_threads.last_message_id
|
||||
WHERE chat_threads.id IN (?)
|
||||
AND last_message.deleted_at IS NULL
|
||||
SQL
|
||||
|
||||
report.thread_tracking =
|
||||
tracking
|
||||
.map do |tt|
|
||||
[
|
||||
tt.thread_id,
|
||||
{
|
||||
data = {
|
||||
channel_id: tt.channel_id,
|
||||
mention_count: tt.mention_count,
|
||||
unread_count: tt.unread_count,
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
if include_last_reply_details
|
||||
data[:last_reply_created_at] = last_reply_details
|
||||
.find { |details| details.thread_id == tt.thread_id }
|
||||
&.created_at
|
||||
end
|
||||
|
||||
[tt.thread_id, data]
|
||||
end
|
||||
.to_h
|
||||
end
|
||||
|
|
|
@ -2,12 +2,20 @@
|
|||
|
||||
module Chat
|
||||
class StructuredChannelSerializer < ApplicationSerializer
|
||||
attributes :public_channels, :direct_message_channels, :tracking, :meta
|
||||
attributes :public_channels, :direct_message_channels, :tracking, :meta, :unread_thread_overview
|
||||
|
||||
def tracking
|
||||
object[:tracking]
|
||||
end
|
||||
|
||||
def include_unread_thread_overview?
|
||||
SiteSetting.enable_experimental_chat_threaded_discussions
|
||||
end
|
||||
|
||||
def unread_thread_overview
|
||||
object[:unread_thread_overview]
|
||||
end
|
||||
|
||||
def public_channels
|
||||
object[:public_channels].map do |channel|
|
||||
Chat::ChannelSerializer.new(
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
module Chat
|
||||
class ViewSerializer < ApplicationSerializer
|
||||
attributes :meta, :chat_messages, :threads, :tracking, :unread_thread_ids, :channel
|
||||
attributes :meta, :chat_messages, :threads, :tracking, :unread_thread_overview, :channel
|
||||
|
||||
def threads
|
||||
return [] if !object.threads
|
||||
|
@ -23,15 +23,15 @@ module Chat
|
|||
object.tracking || {}
|
||||
end
|
||||
|
||||
def unread_thread_ids
|
||||
object.unread_thread_ids || []
|
||||
def unread_thread_overview
|
||||
object.unread_thread_overview || {}
|
||||
end
|
||||
|
||||
def include_threads?
|
||||
include_thread_data?
|
||||
end
|
||||
|
||||
def include_unread_thread_ids?
|
||||
def include_unread_thread_overview?
|
||||
include_thread_data?
|
||||
end
|
||||
|
||||
|
|
|
@ -34,7 +34,7 @@ module Chat
|
|||
step :determine_threads_enabled
|
||||
step :determine_include_thread_messages
|
||||
step :fetch_messages
|
||||
step :fetch_unread_thread_ids
|
||||
step :fetch_unread_thread_overview
|
||||
step :fetch_threads_for_messages
|
||||
step :fetch_tracking
|
||||
step :fetch_thread_memberships
|
||||
|
@ -155,24 +155,25 @@ module Chat
|
|||
end
|
||||
end
|
||||
|
||||
# The thread tracking overview is a simple array of thread IDs
|
||||
# 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_unread_thread_ids(guardian:, channel:, threads_enabled:, **)
|
||||
# The thread tracking overview is a simple array of hashes consisting
|
||||
# of thread IDs that have unread messages as well as the datetime of the
|
||||
# last reply in the thread.
|
||||
#
|
||||
# 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_unread_thread_overview(guardian:, channel:, threads_enabled:, **)
|
||||
if !threads_enabled
|
||||
context.unread_thread_ids = []
|
||||
context.unread_thread_overview = {}
|
||||
else
|
||||
context.unread_thread_ids =
|
||||
::Chat::TrackingStateReportQuery
|
||||
.call(
|
||||
context.unread_thread_overview =
|
||||
::Chat::TrackingStateReportQuery.call(
|
||||
guardian: guardian,
|
||||
channel_ids: [channel.id],
|
||||
include_threads: true,
|
||||
include_read: false,
|
||||
)
|
||||
.find_channel_threads(channel.id)
|
||||
.keys
|
||||
include_last_reply_details: true,
|
||||
).find_channel_thread_overviews(channel.id)
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -238,7 +239,7 @@ module Chat
|
|||
messages:,
|
||||
threads:,
|
||||
tracking:,
|
||||
unread_thread_ids:,
|
||||
unread_thread_overview:,
|
||||
can_load_more_past:,
|
||||
can_load_more_future:,
|
||||
thread_memberships:,
|
||||
|
@ -252,7 +253,7 @@ module Chat
|
|||
user: guardian.user,
|
||||
can_load_more_past: can_load_more_past,
|
||||
can_load_more_future: can_load_more_future,
|
||||
unread_thread_ids: unread_thread_ids,
|
||||
unread_thread_overview: unread_thread_overview,
|
||||
threads: threads,
|
||||
tracking: tracking,
|
||||
thread_memberships: thread_memberships,
|
||||
|
|
|
@ -297,15 +297,13 @@ module Chat
|
|||
# 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(
|
||||
data[:unread_thread_overview] = ::Chat::TrackingStateReportQuery.call(
|
||||
guardian: user.guardian,
|
||||
channel_ids: [channel.id],
|
||||
include_threads: true,
|
||||
include_read: false,
|
||||
)
|
||||
.find_channel_threads(channel.id)
|
||||
.keys
|
||||
include_last_reply_details: true,
|
||||
).find_channel_thread_overviews(channel.id)
|
||||
|
||||
data[:thread_tracking] = ::Chat::TrackingStateReportQuery.call(
|
||||
guardian: user.guardian,
|
||||
|
|
|
@ -5,6 +5,7 @@
|
|||
</div>
|
||||
{{/if}}
|
||||
|
||||
{{! TODO (martin) We need to have the thread unread count in the mobile channel row too }}
|
||||
{{#if this.unreadIndicator}}
|
||||
<ChatChannelUnreadIndicator @channel={{@channel}} />
|
||||
{{/if}}
|
||||
|
|
|
@ -1,17 +1,12 @@
|
|||
{{#if (gt @channel.tracking.unreadCount 0)}}
|
||||
{{#if this.showUnreadIndicator}}
|
||||
<div
|
||||
class={{concat-class
|
||||
"chat-channel-unread-indicator"
|
||||
(if
|
||||
(or
|
||||
@channel.isDirectMessageChannel (gt @channel.tracking.mentionCount 0)
|
||||
)
|
||||
"-urgent"
|
||||
)
|
||||
(if this.isUrgent "-urgent")
|
||||
}}
|
||||
>
|
||||
<div
|
||||
class="chat-channel-unread-indicator__number"
|
||||
>{{@channel.tracking.unreadCount}}</div>
|
||||
<div class="chat-channel-unread-indicator__number">{{#if
|
||||
this.showUnreadCount
|
||||
}}{{this.unreadCount}}{{else}} {{/if}}</div>
|
||||
</div>
|
||||
{{/if}}
|
|
@ -0,0 +1,32 @@
|
|||
import Component from "@glimmer/component";
|
||||
import { inject as service } from "@ember/service";
|
||||
|
||||
export default class ChatChannelUnreadIndicator extends Component {
|
||||
@service chat;
|
||||
@service site;
|
||||
|
||||
get showUnreadIndicator() {
|
||||
return (
|
||||
this.args.channel.tracking.unreadCount > 0 ||
|
||||
// We want to do this so we don't show a blue dot if the user is inside
|
||||
// the channel and a new unread thread comes in.
|
||||
(this.chat.activeChannel?.id !== this.args.channel.id &&
|
||||
this.args.channel.unreadThreadsCountSinceLastViewed > 0)
|
||||
);
|
||||
}
|
||||
|
||||
get unreadCount() {
|
||||
return this.args.channel.tracking.unreadCount;
|
||||
}
|
||||
|
||||
get isUrgent() {
|
||||
return (
|
||||
this.args.channel.isDirectMessageChannel ||
|
||||
this.args.channel.tracking.mentionCount > 0
|
||||
);
|
||||
}
|
||||
|
||||
get showUnreadCount() {
|
||||
return this.args.channel.isDirectMessageChannel;
|
||||
}
|
||||
}
|
|
@ -214,6 +214,10 @@ export default class ChatLivePane extends Component {
|
|||
this.args.channel.addMessages(messages);
|
||||
this.args.channel.details = meta;
|
||||
|
||||
// We update this value server-side when we load the Channel
|
||||
// here, so this reflects reality for sidebar unread logic.
|
||||
this.args.channel.updateLastViewedAt();
|
||||
|
||||
if (result.threads) {
|
||||
result.threads.forEach((thread) => {
|
||||
const storedThread = this.args.channel.threadsManager.add(
|
||||
|
@ -237,8 +241,9 @@ export default class ChatLivePane extends Component {
|
|||
});
|
||||
}
|
||||
|
||||
if (result.unread_thread_ids) {
|
||||
this.args.channel.unreadThreadIds = result.unread_thread_ids;
|
||||
if (result.unread_thread_overview) {
|
||||
this.args.channel.threadsManager.unreadThreadOverview =
|
||||
result.unread_thread_overview;
|
||||
}
|
||||
|
||||
if (this.requestedTargetMessageId) {
|
||||
|
@ -353,12 +358,9 @@ export default class ChatLivePane extends Component {
|
|||
});
|
||||
}
|
||||
|
||||
if (result.thread_tracking_overview) {
|
||||
result.thread_tracking_overview.forEach((threadId) => {
|
||||
if (!this.args.channel.threadTrackingOverview.includes(threadId)) {
|
||||
this.args.channel.threadTrackingOverview.push(threadId);
|
||||
}
|
||||
});
|
||||
if (result.unread_thread_overview) {
|
||||
this.args.channel.threadsManager.unreadThreadOverview =
|
||||
result.unread_thread_overview;
|
||||
}
|
||||
|
||||
this.args.channel.details = meta;
|
||||
|
|
|
@ -9,7 +9,7 @@ export default class ChatThreadHeaderUnreadIndicator extends Component {
|
|||
}
|
||||
|
||||
get unreadCount() {
|
||||
return this.args.channel.unreadThreadCount;
|
||||
return this.args.channel.threadsManager.unreadThreadCount;
|
||||
}
|
||||
|
||||
get showUnreadIndicator() {
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
title={{i18n "chat.threads.list"}}
|
||||
class={{concat-class
|
||||
"chat-threads-list-button btn btn-flat"
|
||||
(if @channel.unreadThreadCount "has-unreads")
|
||||
(if @channel.threadsManager.unreadThreadCount "has-unreads")
|
||||
}}
|
||||
>
|
||||
{{d-icon "discourse-threads"}}
|
||||
|
|
|
@ -93,7 +93,13 @@ export default {
|
|||
}
|
||||
|
||||
get suffixValue() {
|
||||
return this.channel.tracking.unreadCount > 0 ? "circle" : "";
|
||||
return this.channel.tracking.unreadCount > 0 ||
|
||||
// We want to do this so we don't show a blue dot if the user is inside
|
||||
// the channel and a new unread thread comes in.
|
||||
(this.chatService.activeChannel?.id !== this.channel.id &&
|
||||
this.channel.unreadThreadsCountSinceLastViewed > 0)
|
||||
? "circle"
|
||||
: "";
|
||||
}
|
||||
|
||||
get suffixCSSClass() {
|
||||
|
|
|
@ -3,7 +3,7 @@ import { setOwner } from "@ember/application";
|
|||
import Promise from "rsvp";
|
||||
import ChatThread from "discourse/plugins/chat/discourse/models/chat-thread";
|
||||
import { cached, tracked } from "@glimmer/tracking";
|
||||
import { TrackedObject } from "@ember-compat/tracked-built-ins";
|
||||
import { TrackedMap, TrackedObject } from "@ember-compat/tracked-built-ins";
|
||||
|
||||
/*
|
||||
The ChatThreadsManager is responsible for managing the loaded chat threads
|
||||
|
@ -19,11 +19,37 @@ export default class ChatThreadsManager {
|
|||
@service chatApi;
|
||||
|
||||
@tracked _cached = new TrackedObject();
|
||||
@tracked _unreadThreadOverview = new TrackedMap();
|
||||
|
||||
constructor(owner) {
|
||||
setOwner(this, owner);
|
||||
}
|
||||
|
||||
get unreadThreadCount() {
|
||||
return this.unreadThreadOverview.size;
|
||||
}
|
||||
|
||||
get unreadThreadOverview() {
|
||||
return this._unreadThreadOverview;
|
||||
}
|
||||
|
||||
set unreadThreadOverview(unreadThreadOverview) {
|
||||
this._unreadThreadOverview.clear();
|
||||
|
||||
for (const [threadId, lastReplyCreatedAt] of Object.entries(
|
||||
unreadThreadOverview
|
||||
)) {
|
||||
this.markThreadUnread(threadId, lastReplyCreatedAt);
|
||||
}
|
||||
}
|
||||
|
||||
markThreadUnread(threadId, lastReplyCreatedAt) {
|
||||
this.unreadThreadOverview.set(
|
||||
parseInt(threadId, 10),
|
||||
new Date(lastReplyCreatedAt)
|
||||
);
|
||||
}
|
||||
|
||||
@cached
|
||||
get threads() {
|
||||
return Object.values(this._cached);
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
import UserChatChannelMembership from "discourse/plugins/chat/discourse/models/user-chat-channel-membership";
|
||||
import ChatMessage from "discourse/plugins/chat/discourse/models/chat-message";
|
||||
import { TrackedSet } from "@ember-compat/tracked-built-ins";
|
||||
import { escapeExpression } from "discourse/lib/utilities";
|
||||
import { tracked } from "@glimmer/tracking";
|
||||
import slugifyChannel from "discourse/plugins/chat/discourse/lib/slugify-channel";
|
||||
|
@ -80,7 +79,6 @@ export default class ChatChannel {
|
|||
threadsManager = new ChatThreadsManager(getOwner(this));
|
||||
messagesManager = new ChatMessagesManager(getOwner(this));
|
||||
|
||||
@tracked _unreadThreadIds = new TrackedSet();
|
||||
@tracked _currentUserMembership;
|
||||
@tracked _lastMessage;
|
||||
|
||||
|
@ -119,16 +117,15 @@ export default class ChatChannel {
|
|||
this.lastMessage = args.last_message;
|
||||
}
|
||||
|
||||
get unreadThreadCount() {
|
||||
return this.unreadThreadIds.size;
|
||||
get unreadThreadsCountSinceLastViewed() {
|
||||
return Array.from(this.threadsManager.unreadThreadOverview.values()).filter(
|
||||
(lastReplyCreatedAt) =>
|
||||
lastReplyCreatedAt >= this.currentUserMembership.lastViewedAt
|
||||
).length;
|
||||
}
|
||||
|
||||
get unreadThreadIds() {
|
||||
return this._unreadThreadIds;
|
||||
}
|
||||
|
||||
set unreadThreadIds(unreadThreadIds) {
|
||||
this._unreadThreadIds = new TrackedSet(unreadThreadIds);
|
||||
updateLastViewedAt() {
|
||||
this.currentUserMembership.lastViewedAt = new Date();
|
||||
}
|
||||
|
||||
findIndexOfMessage(id) {
|
||||
|
|
|
@ -11,8 +11,8 @@ export default class UserChatChannelMembership {
|
|||
@tracked desktopNotificationLevel = null;
|
||||
@tracked mobileNotificationLevel = null;
|
||||
@tracked lastReadMessageId = null;
|
||||
@tracked user = null;
|
||||
@tracked lastViewedAt = null;
|
||||
@tracked user = null;
|
||||
|
||||
constructor(args = {}) {
|
||||
this.following = args.following;
|
||||
|
@ -20,7 +20,7 @@ export default class UserChatChannelMembership {
|
|||
this.desktopNotificationLevel = args.desktop_notification_level;
|
||||
this.mobileNotificationLevel = args.mobile_notification_level;
|
||||
this.lastReadMessageId = args.last_read_message_id;
|
||||
this.lastViewedAt = args.last_viewed_at;
|
||||
this.lastViewedAt = new Date(args.last_viewed_at);
|
||||
this.user = this.#initUserModel(args.user);
|
||||
}
|
||||
|
||||
|
|
|
@ -6,6 +6,10 @@ export default class ChatIndexRoute extends DiscourseRoute {
|
|||
@service chatChannelsManager;
|
||||
@service router;
|
||||
|
||||
activate() {
|
||||
this.chat.activeChannel = null;
|
||||
}
|
||||
|
||||
redirect() {
|
||||
// Always want the channel index on mobile.
|
||||
if (this.site.mobileView) {
|
||||
|
|
|
@ -211,7 +211,11 @@ export default class ChatSubscriptionsManager extends Service {
|
|||
.find(channel.id, busData.thread_id)
|
||||
.then((thread) => {
|
||||
if (thread.currentUserMembership) {
|
||||
channel.unreadThreadIds.add(busData.thread_id);
|
||||
channel.threadsManager.markThreadUnread(
|
||||
busData.thread_id,
|
||||
busData.message.created_at
|
||||
);
|
||||
this._updateActiveLastViewedAt(channel);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
@ -228,7 +232,9 @@ export default class ChatSubscriptionsManager extends Service {
|
|||
if (busData.message.user.id === this.currentUser.id) {
|
||||
// Thread should no longer be considered unread.
|
||||
if (thread.currentUserMembership) {
|
||||
channel.unreadThreadIds.delete(busData.thread_id);
|
||||
channel.threadsManager.unreadThreadOverview.delete(
|
||||
parseInt(busData.thread_id, 10)
|
||||
);
|
||||
thread.currentUserMembership.lastReadMessageId =
|
||||
busData.message.id;
|
||||
}
|
||||
|
@ -251,8 +257,12 @@ export default class ChatSubscriptionsManager extends Service {
|
|||
(thread.currentUserMembership.lastReadMessageId || 0) &&
|
||||
!thread.currentUserMembership.isQuiet
|
||||
) {
|
||||
channel.unreadThreadIds.add(busData.thread_id);
|
||||
channel.threadsManager.markThreadUnread(
|
||||
busData.thread_id,
|
||||
busData.message.created_at
|
||||
);
|
||||
thread.tracking.unreadCount++;
|
||||
this._updateActiveLastViewedAt(channel);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -260,6 +270,14 @@ export default class ChatSubscriptionsManager extends Service {
|
|||
});
|
||||
}
|
||||
|
||||
// If the user is currently looking at this channel via activeChannel, we don't want the unread
|
||||
// indicator to show in the sidebar for unread threads (since that is based on the lastViewedAt).
|
||||
_updateActiveLastViewedAt(channel) {
|
||||
if (this.chat.activeChannel?.id === channel.id) {
|
||||
channel.updateLastViewedAt();
|
||||
}
|
||||
}
|
||||
|
||||
_startUserTrackingStateSubscription(lastId) {
|
||||
if (!this.currentUser) {
|
||||
return;
|
||||
|
@ -316,8 +334,9 @@ export default class ChatSubscriptionsManager extends Service {
|
|||
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.hasOwnProperty("unread_thread_overview")) {
|
||||
channel.threadsManager.unreadThreadOverview =
|
||||
busData.unread_thread_overview;
|
||||
}
|
||||
|
||||
if (busData.thread_id && busData.hasOwnProperty("thread_tracking")) {
|
||||
|
|
|
@ -173,23 +173,25 @@ export default class Chat extends Service {
|
|||
this.set("isNetworkUnreliable", false);
|
||||
}
|
||||
|
||||
setupWithPreloadedChannels(channels) {
|
||||
setupWithPreloadedChannels(channelsView) {
|
||||
this.chatSubscriptionsManager.startChannelsSubscriptions(
|
||||
channels.meta.message_bus_last_ids
|
||||
channelsView.meta.message_bus_last_ids
|
||||
);
|
||||
this.presenceChannel.subscribe(channels.global_presence_channel_state);
|
||||
this.presenceChannel.subscribe(channelsView.global_presence_channel_state);
|
||||
|
||||
[...channels.public_channels, ...channels.direct_message_channels].forEach(
|
||||
(channelObject) => {
|
||||
const channel = this.chatChannelsManager.store(channelObject);
|
||||
[
|
||||
...channelsView.public_channels,
|
||||
...channelsView.direct_message_channels,
|
||||
].forEach((channelObject) => {
|
||||
const storedChannel = this.chatChannelsManager.store(channelObject);
|
||||
const storedDraft = (this.currentUser?.chat_drafts || []).find(
|
||||
(draft) => draft.channel_id === channel.id
|
||||
(draft) => draft.channel_id === storedChannel.id
|
||||
);
|
||||
|
||||
if (storedDraft) {
|
||||
this.chatDraftsManager.add(
|
||||
ChatMessage.createDraftMessage(
|
||||
channel,
|
||||
storedChannel,
|
||||
Object.assign(
|
||||
{ user: this.currentUser },
|
||||
JSON.parse(storedDraft.data)
|
||||
|
@ -198,11 +200,17 @@ export default class Chat extends Service {
|
|||
);
|
||||
}
|
||||
|
||||
return this.chatChannelsManager.follow(channel);
|
||||
if (channelsView.unread_thread_overview?.[storedChannel.id]) {
|
||||
storedChannel.threadsManager.unreadThreadOverview =
|
||||
channelsView.unread_thread_overview[storedChannel.id];
|
||||
}
|
||||
);
|
||||
|
||||
this.chatTrackingStateManager.setupWithPreloadedState(channels.tracking);
|
||||
return this.chatChannelsManager.follow(storedChannel);
|
||||
});
|
||||
|
||||
this.chatTrackingStateManager.setupWithPreloadedState(
|
||||
channelsView.tracking
|
||||
);
|
||||
}
|
||||
|
||||
willDestroy() {
|
||||
|
|
|
@ -237,6 +237,17 @@ after_initialize do
|
|||
|
||||
add_to_serializer(:current_user, :chat_channels) do
|
||||
structured = Chat::ChannelFetcher.structured(self.scope)
|
||||
|
||||
if SiteSetting.enable_experimental_chat_threaded_discussions
|
||||
structured[:unread_thread_overview] = ::Chat::TrackingStateReportQuery.call(
|
||||
guardian: self.scope,
|
||||
channel_ids: structured[:public_channels].map(&:id),
|
||||
include_threads: true,
|
||||
include_read: false,
|
||||
include_last_reply_details: true,
|
||||
).thread_unread_overview_by_channel
|
||||
end
|
||||
|
||||
Chat::ChannelIndexSerializer.new(structured, scope: self.scope, root: false).as_json
|
||||
end
|
||||
|
||||
|
|
|
@ -9,6 +9,7 @@ RSpec.describe Chat::TrackingStateReportQuery do
|
|||
include_missing_memberships: include_missing_memberships,
|
||||
include_threads: include_threads,
|
||||
include_read: include_read,
|
||||
include_last_reply_details: include_last_reply_details,
|
||||
)
|
||||
end
|
||||
|
||||
|
@ -20,6 +21,7 @@ RSpec.describe Chat::TrackingStateReportQuery do
|
|||
let(:include_missing_memberships) { false }
|
||||
let(:include_threads) { false }
|
||||
let(:include_read) { true }
|
||||
let(:include_last_reply_details) { false }
|
||||
context "when channel_ids empty" do
|
||||
it "returns empty object for channel_tracking" do
|
||||
expect(query.channel_tracking).to eq({})
|
||||
|
@ -129,6 +131,56 @@ RSpec.describe Chat::TrackingStateReportQuery do
|
|||
)
|
||||
end
|
||||
|
||||
context "when include_last_reply_details is true" do
|
||||
let(:include_last_reply_details) { true }
|
||||
|
||||
before do
|
||||
thread_1.add(current_user)
|
||||
thread_2.add(current_user)
|
||||
Fabricate(:chat_message, chat_channel: channel_1, thread: thread_1)
|
||||
Fabricate(:chat_message, chat_channel: channel_2, thread: thread_2)
|
||||
end
|
||||
|
||||
it "gets the last_reply_created_at for each thread based on the last_message" do
|
||||
expect(query.thread_tracking).to eq(
|
||||
{
|
||||
thread_1.id => {
|
||||
unread_count: 1,
|
||||
mention_count: 0,
|
||||
channel_id: channel_1.id,
|
||||
last_reply_created_at: thread_1.reload.last_message.created_at,
|
||||
},
|
||||
thread_2.id => {
|
||||
unread_count: 1,
|
||||
mention_count: 0,
|
||||
channel_id: channel_2.id,
|
||||
last_reply_created_at: thread_2.reload.last_message.created_at,
|
||||
},
|
||||
},
|
||||
)
|
||||
end
|
||||
|
||||
it "does not get the last_reply_created_at for threads where the last_message is deleted" do
|
||||
thread_1.reload.last_message.trash!
|
||||
expect(query.thread_tracking).to eq(
|
||||
{
|
||||
thread_1.id => {
|
||||
unread_count: 0,
|
||||
mention_count: 0,
|
||||
channel_id: channel_1.id,
|
||||
last_reply_created_at: nil,
|
||||
},
|
||||
thread_2.id => {
|
||||
unread_count: 1,
|
||||
mention_count: 0,
|
||||
channel_id: channel_2.id,
|
||||
last_reply_created_at: thread_2.reload.last_message.created_at,
|
||||
},
|
||||
},
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
context "when thread_ids and channel_ids is empty" do
|
||||
let(:thread_ids) { [] }
|
||||
let(:channel_ids) { [] }
|
||||
|
|
|
@ -173,6 +173,7 @@ RSpec.describe Chat::ChannelViewBuilder do
|
|||
channel_ids: [channel.id],
|
||||
include_threads: true,
|
||||
include_read: false,
|
||||
include_last_reply_details: true,
|
||||
)
|
||||
.returns(Chat::TrackingStateReport.new)
|
||||
.once
|
||||
|
@ -188,7 +189,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(result.view.unread_thread_ids).to eq([message_1.thread.id])
|
||||
expect(result.view.unread_thread_overview).to eq({ thread.id => message_1.created_at })
|
||||
end
|
||||
|
||||
it "fetches the tracking state of threads in the channel" do
|
||||
|
|
|
@ -137,18 +137,21 @@ RSpec.describe Chat::MarkAllUserChannelsRead do
|
|||
expect(message.data).to eq(
|
||||
channel_1.id.to_s => {
|
||||
"last_read_message_id" => message_2.id,
|
||||
"last_reply_created_at" => nil,
|
||||
"membership_id" => membership_1.id,
|
||||
"mention_count" => 0,
|
||||
"unread_count" => 0,
|
||||
},
|
||||
channel_2.id.to_s => {
|
||||
"last_read_message_id" => message_4.id,
|
||||
"last_reply_created_at" => nil,
|
||||
"membership_id" => membership_2.id,
|
||||
"mention_count" => 0,
|
||||
"unread_count" => 0,
|
||||
},
|
||||
channel_3.id.to_s => {
|
||||
"last_read_message_id" => message_6.id,
|
||||
"last_reply_created_at" => nil,
|
||||
"membership_id" => membership_3.id,
|
||||
"mention_count" => 0,
|
||||
"unread_count" => 0,
|
||||
|
|
|
@ -96,8 +96,10 @@ describe Chat::Publisher do
|
|||
|
||||
context "when the channel has threading enabled and the message is a thread reply" do
|
||||
fab!(:thread) { Fabricate(:chat_thread, channel: channel) }
|
||||
|
||||
before do
|
||||
message_1.update!(thread: thread)
|
||||
thread.update_last_message_id!
|
||||
channel.update!(threading_enabled: true)
|
||||
end
|
||||
|
||||
|
@ -106,16 +108,22 @@ describe Chat::Publisher do
|
|||
|
||||
it "publishes the tracking state with correct counts" do
|
||||
expect(data["thread_id"]).to eq(thread.id)
|
||||
expect(data["unread_thread_ids"]).to eq([thread.id])
|
||||
expect(data["thread_tracking"]).to eq({ "unread_count" => 1, "mention_count" => 0 })
|
||||
expect(data["unread_thread_overview"]).to eq(
|
||||
{ thread.id.to_s => thread.reload.last_message.created_at.iso8601(3) },
|
||||
)
|
||||
expect(data["thread_tracking"]).to eq(
|
||||
{ "unread_count" => 1, "mention_count" => 0, "last_reply_created_at" => nil },
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
context "when the user has no thread membership" do
|
||||
it "publishes the tracking state with zeroed out counts" do
|
||||
expect(data["thread_id"]).to eq(thread.id)
|
||||
expect(data["unread_thread_ids"]).to eq([])
|
||||
expect(data["thread_tracking"]).to eq({ "unread_count" => 0, "mention_count" => 0 })
|
||||
expect(data["unread_thread_overview"]).to eq({})
|
||||
expect(data["thread_tracking"]).to eq(
|
||||
{ "unread_count" => 0, "mention_count" => 0, "last_reply_created_at" => nil },
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -26,6 +26,7 @@
|
|||
"avatar_template": { "type": "string" },
|
||||
"username": { "type": "string" }
|
||||
}
|
||||
}
|
||||
},
|
||||
"last_viewed_at": { "type": "datetime" }
|
||||
}
|
||||
}
|
||||
|
|
|
@ -109,7 +109,6 @@ RSpec.describe "Message notifications - mobile", type: :system, mobile: true do
|
|||
expect(page).to have_css(".chat-header-icon .chat-channel-unread-indicator", text: "")
|
||||
expect(page).to have_css(
|
||||
".chat-channel-row[data-chat-channel-id=\"#{channel_1.id}\"] .chat-channel-unread-indicator",
|
||||
text: 1,
|
||||
)
|
||||
end
|
||||
end
|
||||
|
@ -130,7 +129,6 @@ RSpec.describe "Message notifications - mobile", type: :system, mobile: true do
|
|||
expect(page).to have_css(".chat-header-icon .chat-channel-unread-indicator")
|
||||
expect(page).to have_css(
|
||||
".chat-channel-row[data-chat-channel-id=\"#{channel_1.id}\"] .chat-channel-unread-indicator",
|
||||
text: 1,
|
||||
)
|
||||
end
|
||||
end
|
||||
|
|
|
@ -20,12 +20,22 @@ module PageObjects
|
|||
end
|
||||
|
||||
def open_channel(channel)
|
||||
find(
|
||||
"#{VISIBLE_DRAWER} .channels-list .chat-channel-row[data-chat-channel-id='#{channel.id}']",
|
||||
).click
|
||||
find("#{VISIBLE_DRAWER} .channels-list #{channel_row_selector(channel)}").click
|
||||
has_no_css?(".chat-skeleton")
|
||||
end
|
||||
|
||||
def channel_row_selector(channel)
|
||||
".chat-channel-row[data-chat-channel-id='#{channel.id}']"
|
||||
end
|
||||
|
||||
def has_unread_channel?(channel)
|
||||
has_css?("#{channel_row_selector(channel)} .chat-channel-unread-indicator")
|
||||
end
|
||||
|
||||
def has_no_unread_channel?(channel)
|
||||
has_no_css?("#{channel_row_selector(channel)} .chat-channel-unread-indicator")
|
||||
end
|
||||
|
||||
def maximize
|
||||
mouseout
|
||||
find("#{VISIBLE_DRAWER} .chat-drawer-header__full-screen-btn").click
|
||||
|
|
|
@ -40,6 +40,16 @@ module PageObjects
|
|||
find(".sidebar-section-link.channel-#{channel.id}")
|
||||
self
|
||||
end
|
||||
|
||||
def has_unread_channel?(channel)
|
||||
has_css?(".sidebar-section-link.channel-#{channel.id} .sidebar-section-link-suffix.unread")
|
||||
end
|
||||
|
||||
def has_no_unread_channel?(channel)
|
||||
has_no_css?(
|
||||
".sidebar-section-link.channel-#{channel.id} .sidebar-section-link-suffix.unread",
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -64,5 +64,53 @@ describe "Thread tracking state | drawer", type: :system do
|
|||
expect(drawer_page).to have_unread_thread_indicator(count: 1)
|
||||
expect(thread_list_page).to have_unread_item(thread.id)
|
||||
end
|
||||
|
||||
describe "channel index unread indicators" do
|
||||
fab!(:other_channel) { Fabricate(:chat_channel) }
|
||||
|
||||
before { other_channel.add(current_user) }
|
||||
|
||||
it "shows an unread indicator for the channel with unread threads in the index" do
|
||||
visit("/")
|
||||
chat_page.open_from_header
|
||||
expect(drawer_page).to have_unread_channel(channel)
|
||||
end
|
||||
|
||||
it "does not show an unread indicator for the channel if the user has visited the channel since the unread thread message arrived" do
|
||||
channel.membership_for(current_user).update!(last_viewed_at: Time.zone.now)
|
||||
visit("/")
|
||||
chat_page.open_from_header
|
||||
expect(drawer_page).to have_no_unread_channel(channel)
|
||||
end
|
||||
|
||||
it "clears the index unread indicator for the channel when opening it but keeps the thread list unread indicator" do
|
||||
visit("/")
|
||||
chat_page.open_from_header
|
||||
drawer_page.open_channel(channel)
|
||||
expect(channel_page).to have_unread_thread_indicator(count: 1)
|
||||
drawer_page.back
|
||||
expect(drawer_page).to have_no_unread_channel(channel)
|
||||
end
|
||||
|
||||
it "does not show an unread indicator for the channel index if a new thread message arrives while the user is looking at the channel" do
|
||||
visit("/")
|
||||
chat_page.open_from_header
|
||||
expect(drawer_page).to have_unread_channel(channel)
|
||||
drawer_page.open_channel(channel)
|
||||
Fabricate(:chat_message, thread: thread)
|
||||
drawer_page.back
|
||||
expect(drawer_page).to have_no_unread_channel(channel)
|
||||
end
|
||||
|
||||
it "shows an unread indicator for the channel index if a new thread message arrives while the user is not looking at the channel" do
|
||||
visit("/")
|
||||
chat_page.open_from_header
|
||||
drawer_page.open_channel(channel)
|
||||
drawer_page.back
|
||||
expect(drawer_page).to have_no_unread_channel(channel)
|
||||
Fabricate(:chat_message, thread: thread)
|
||||
expect(drawer_page).to have_unread_channel(channel)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -10,6 +10,7 @@ describe "Thread tracking state | full page", type: :system do
|
|||
let(:channel_page) { PageObjects::Pages::ChatChannel.new }
|
||||
let(:thread_page) { PageObjects::Pages::ChatThread.new }
|
||||
let(:thread_list_page) { PageObjects::Components::Chat::ThreadList.new }
|
||||
let(:sidebar_page) { PageObjects::Pages::Sidebar.new }
|
||||
|
||||
before do
|
||||
SiteSetting.enable_experimental_chat_threaded_discussions = true
|
||||
|
@ -86,6 +87,47 @@ describe "Thread tracking state | full page", type: :system do
|
|||
expect(thread_list_page).to have_thread(new_thread)
|
||||
end
|
||||
|
||||
describe "sidebar unread indicators" do
|
||||
fab!(:other_channel) { Fabricate(:chat_channel) }
|
||||
|
||||
before do
|
||||
other_channel.add(current_user)
|
||||
SiteSetting.navigation_menu = "sidebar"
|
||||
end
|
||||
|
||||
it "shows an unread indicator for the channel with unread threads in the sidebar" do
|
||||
chat_page.visit_channel(other_channel)
|
||||
expect(sidebar_page).to have_unread_channel(channel)
|
||||
end
|
||||
|
||||
it "does not show an unread indicator for the channel if the user has visited the channel since the unread thread message arrived" do
|
||||
channel.membership_for(current_user).update!(last_viewed_at: Time.zone.now)
|
||||
chat_page.visit_channel(other_channel)
|
||||
expect(sidebar_page).to have_no_unread_channel(channel)
|
||||
end
|
||||
|
||||
it "clears the sidebar unread indicator for the channel when opening it but keeps the thread list unread indicator" do
|
||||
chat_page.visit_channel(channel)
|
||||
expect(sidebar_page).to have_no_unread_channel(channel)
|
||||
expect(channel_page).to have_unread_thread_indicator(count: 1)
|
||||
end
|
||||
|
||||
it "does not show an unread indicator for the channel sidebar if a new thread message arrives while the user is looking at the channel" do
|
||||
chat_page.visit_channel(channel)
|
||||
expect(sidebar_page).to have_no_unread_channel(channel)
|
||||
Fabricate(:chat_message, thread: thread)
|
||||
expect(sidebar_page).to have_no_unread_channel(channel)
|
||||
end
|
||||
|
||||
it "shows an unread indicator for the channel sidebar if a new thread message arrives while the user is not looking at the channel" do
|
||||
chat_page.visit_channel(channel)
|
||||
expect(sidebar_page).to have_no_unread_channel(channel)
|
||||
chat_page.visit_channel(other_channel)
|
||||
Fabricate(:chat_message, thread: thread)
|
||||
expect(sidebar_page).to have_unread_channel(channel)
|
||||
end
|
||||
end
|
||||
|
||||
context "when the user's notification level for the thread is set to normal" do
|
||||
before { thread.membership_for(current_user).update!(notification_level: :normal) }
|
||||
|
||||
|
|
Loading…
Reference in New Issue