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
|
attr_accessor :channel_tracking, :thread_tracking
|
||||||
|
|
||||||
class TrackingStateInfo
|
class TrackingStateInfo
|
||||||
attr_accessor :unread_count, :mention_count
|
attr_accessor :unread_count, :mention_count, :last_reply_created_at
|
||||||
|
|
||||||
def initialize(info)
|
def initialize(info)
|
||||||
@unread_count = info.present? ? info[:unread_count] : 0
|
@unread_count = info.present? ? info[:unread_count] : 0
|
||||||
@mention_count = info.present? ? info[:mention_count] : 0
|
@mention_count = info.present? ? info[:mention_count] : 0
|
||||||
|
@last_reply_created_at = info.present? ? info[:last_reply_created_at] : nil
|
||||||
end
|
end
|
||||||
|
|
||||||
def to_hash
|
def to_hash
|
||||||
|
@ -20,7 +21,11 @@ module Chat
|
||||||
end
|
end
|
||||||
|
|
||||||
def to_h
|
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
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -38,10 +43,34 @@ module Chat
|
||||||
end
|
end
|
||||||
|
|
||||||
def find_channel_threads(channel_id)
|
def find_channel_threads(channel_id)
|
||||||
thread_tracking
|
thread_tracking.inject({}) do |result, (thread_id, thread)|
|
||||||
.select { |_, thread| thread[:channel_id] == channel_id }
|
if thread[:channel_id] == channel_id
|
||||||
.map { |thread_id, thread| [thread_id, TrackingStateInfo.new(thread)] }
|
result.merge(thread_id => TrackingStateInfo.new(thread))
|
||||||
.to_h
|
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
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -7,7 +7,7 @@ module Chat
|
||||||
:chat_messages,
|
:chat_messages,
|
||||||
:can_load_more_past,
|
:can_load_more_past,
|
||||||
:can_load_more_future,
|
:can_load_more_future,
|
||||||
:unread_thread_ids,
|
:unread_thread_overview,
|
||||||
:threads,
|
:threads,
|
||||||
:tracking,
|
:tracking,
|
||||||
:thread_memberships,
|
:thread_memberships,
|
||||||
|
@ -19,7 +19,7 @@ module Chat
|
||||||
user:,
|
user:,
|
||||||
can_load_more_past: nil,
|
can_load_more_past: nil,
|
||||||
can_load_more_future: nil,
|
can_load_more_future: nil,
|
||||||
unread_thread_ids: nil,
|
unread_thread_overview: nil,
|
||||||
threads: nil,
|
threads: nil,
|
||||||
tracking: nil,
|
tracking: nil,
|
||||||
thread_memberships: nil,
|
thread_memberships: nil,
|
||||||
|
@ -30,7 +30,7 @@ module Chat
|
||||||
@user = user
|
@user = user
|
||||||
@can_load_more_past = can_load_more_past
|
@can_load_more_past = can_load_more_past
|
||||||
@can_load_more_future = can_load_more_future
|
@can_load_more_future = can_load_more_future
|
||||||
@unread_thread_ids = unread_thread_ids
|
@unread_thread_overview = unread_thread_overview
|
||||||
@threads = threads
|
@threads = threads
|
||||||
@tracking = tracking
|
@tracking = tracking
|
||||||
@thread_memberships = thread_memberships
|
@thread_memberships = thread_memberships
|
||||||
|
|
|
@ -35,7 +35,8 @@ module Chat
|
||||||
thread_ids: nil,
|
thread_ids: nil,
|
||||||
include_missing_memberships: false,
|
include_missing_memberships: false,
|
||||||
include_threads: false,
|
include_threads: false,
|
||||||
include_read: true
|
include_read: true,
|
||||||
|
include_last_reply_details: false
|
||||||
)
|
)
|
||||||
report = ::Chat::TrackingStateReport.new
|
report = ::Chat::TrackingStateReport.new
|
||||||
|
|
||||||
|
@ -59,24 +60,40 @@ module Chat
|
||||||
if !include_threads || (thread_ids.blank? && channel_ids.blank?)
|
if !include_threads || (thread_ids.blank? && channel_ids.blank?)
|
||||||
report.thread_tracking = {}
|
report.thread_tracking = {}
|
||||||
else
|
else
|
||||||
|
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 =
|
report.thread_tracking =
|
||||||
::Chat::ThreadUnreadsQuery
|
tracking
|
||||||
.call(
|
|
||||||
channel_ids: channel_ids,
|
|
||||||
thread_ids: thread_ids,
|
|
||||||
user_id: guardian.user.id,
|
|
||||||
include_missing_memberships: include_missing_memberships,
|
|
||||||
include_read: include_read,
|
|
||||||
)
|
|
||||||
.map do |tt|
|
.map do |tt|
|
||||||
[
|
data = {
|
||||||
tt.thread_id,
|
channel_id: tt.channel_id,
|
||||||
{
|
mention_count: tt.mention_count,
|
||||||
channel_id: tt.channel_id,
|
unread_count: tt.unread_count,
|
||||||
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
|
end
|
||||||
.to_h
|
.to_h
|
||||||
end
|
end
|
||||||
|
|
|
@ -2,12 +2,20 @@
|
||||||
|
|
||||||
module Chat
|
module Chat
|
||||||
class StructuredChannelSerializer < ApplicationSerializer
|
class StructuredChannelSerializer < ApplicationSerializer
|
||||||
attributes :public_channels, :direct_message_channels, :tracking, :meta
|
attributes :public_channels, :direct_message_channels, :tracking, :meta, :unread_thread_overview
|
||||||
|
|
||||||
def tracking
|
def tracking
|
||||||
object[:tracking]
|
object[:tracking]
|
||||||
end
|
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
|
def public_channels
|
||||||
object[:public_channels].map do |channel|
|
object[:public_channels].map do |channel|
|
||||||
Chat::ChannelSerializer.new(
|
Chat::ChannelSerializer.new(
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
|
|
||||||
module Chat
|
module Chat
|
||||||
class ViewSerializer < ApplicationSerializer
|
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
|
def threads
|
||||||
return [] if !object.threads
|
return [] if !object.threads
|
||||||
|
@ -23,15 +23,15 @@ module Chat
|
||||||
object.tracking || {}
|
object.tracking || {}
|
||||||
end
|
end
|
||||||
|
|
||||||
def unread_thread_ids
|
def unread_thread_overview
|
||||||
object.unread_thread_ids || []
|
object.unread_thread_overview || {}
|
||||||
end
|
end
|
||||||
|
|
||||||
def include_threads?
|
def include_threads?
|
||||||
include_thread_data?
|
include_thread_data?
|
||||||
end
|
end
|
||||||
|
|
||||||
def include_unread_thread_ids?
|
def include_unread_thread_overview?
|
||||||
include_thread_data?
|
include_thread_data?
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -34,7 +34,7 @@ module Chat
|
||||||
step :determine_threads_enabled
|
step :determine_threads_enabled
|
||||||
step :determine_include_thread_messages
|
step :determine_include_thread_messages
|
||||||
step :fetch_messages
|
step :fetch_messages
|
||||||
step :fetch_unread_thread_ids
|
step :fetch_unread_thread_overview
|
||||||
step :fetch_threads_for_messages
|
step :fetch_threads_for_messages
|
||||||
step :fetch_tracking
|
step :fetch_tracking
|
||||||
step :fetch_thread_memberships
|
step :fetch_thread_memberships
|
||||||
|
@ -155,24 +155,25 @@ module Chat
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
# The thread tracking overview is a simple array of thread IDs
|
# The thread tracking overview is a simple array of hashes consisting
|
||||||
# that have unread messages, only threads with unread messages
|
# of thread IDs that have unread messages as well as the datetime of the
|
||||||
# will be included in this array. This is a low-cost way to know
|
# last reply in the thread.
|
||||||
# how many threads the user has unread across the entire channel.
|
#
|
||||||
def fetch_unread_thread_ids(guardian:, channel:, threads_enabled:, **)
|
# 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
|
if !threads_enabled
|
||||||
context.unread_thread_ids = []
|
context.unread_thread_overview = {}
|
||||||
else
|
else
|
||||||
context.unread_thread_ids =
|
context.unread_thread_overview =
|
||||||
::Chat::TrackingStateReportQuery
|
::Chat::TrackingStateReportQuery.call(
|
||||||
.call(
|
guardian: guardian,
|
||||||
guardian: guardian,
|
channel_ids: [channel.id],
|
||||||
channel_ids: [channel.id],
|
include_threads: true,
|
||||||
include_threads: true,
|
include_read: false,
|
||||||
include_read: false,
|
include_last_reply_details: true,
|
||||||
)
|
).find_channel_thread_overviews(channel.id)
|
||||||
.find_channel_threads(channel.id)
|
|
||||||
.keys
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -238,7 +239,7 @@ module Chat
|
||||||
messages:,
|
messages:,
|
||||||
threads:,
|
threads:,
|
||||||
tracking:,
|
tracking:,
|
||||||
unread_thread_ids:,
|
unread_thread_overview:,
|
||||||
can_load_more_past:,
|
can_load_more_past:,
|
||||||
can_load_more_future:,
|
can_load_more_future:,
|
||||||
thread_memberships:,
|
thread_memberships:,
|
||||||
|
@ -252,7 +253,7 @@ module Chat
|
||||||
user: guardian.user,
|
user: guardian.user,
|
||||||
can_load_more_past: can_load_more_past,
|
can_load_more_past: can_load_more_past,
|
||||||
can_load_more_future: can_load_more_future,
|
can_load_more_future: can_load_more_future,
|
||||||
unread_thread_ids: unread_thread_ids,
|
unread_thread_overview: unread_thread_overview,
|
||||||
threads: threads,
|
threads: threads,
|
||||||
tracking: tracking,
|
tracking: tracking,
|
||||||
thread_memberships: thread_memberships,
|
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
|
# and a message is sent in the thread. We also need to pass the actual
|
||||||
# thread tracking state.
|
# thread tracking state.
|
||||||
if channel.threading_enabled && message.thread_reply?
|
if channel.threading_enabled && message.thread_reply?
|
||||||
data[:unread_thread_ids] = ::Chat::TrackingStateReportQuery
|
data[:unread_thread_overview] = ::Chat::TrackingStateReportQuery.call(
|
||||||
.call(
|
guardian: user.guardian,
|
||||||
guardian: user.guardian,
|
channel_ids: [channel.id],
|
||||||
channel_ids: [channel.id],
|
include_threads: true,
|
||||||
include_threads: true,
|
include_read: false,
|
||||||
include_read: false,
|
include_last_reply_details: true,
|
||||||
)
|
).find_channel_thread_overviews(channel.id)
|
||||||
.find_channel_threads(channel.id)
|
|
||||||
.keys
|
|
||||||
|
|
||||||
data[:thread_tracking] = ::Chat::TrackingStateReportQuery.call(
|
data[:thread_tracking] = ::Chat::TrackingStateReportQuery.call(
|
||||||
guardian: user.guardian,
|
guardian: user.guardian,
|
||||||
|
|
|
@ -5,6 +5,7 @@
|
||||||
</div>
|
</div>
|
||||||
{{/if}}
|
{{/if}}
|
||||||
|
|
||||||
|
{{! TODO (martin) We need to have the thread unread count in the mobile channel row too }}
|
||||||
{{#if this.unreadIndicator}}
|
{{#if this.unreadIndicator}}
|
||||||
<ChatChannelUnreadIndicator @channel={{@channel}} />
|
<ChatChannelUnreadIndicator @channel={{@channel}} />
|
||||||
{{/if}}
|
{{/if}}
|
||||||
|
|
|
@ -1,17 +1,12 @@
|
||||||
{{#if (gt @channel.tracking.unreadCount 0)}}
|
{{#if this.showUnreadIndicator}}
|
||||||
<div
|
<div
|
||||||
class={{concat-class
|
class={{concat-class
|
||||||
"chat-channel-unread-indicator"
|
"chat-channel-unread-indicator"
|
||||||
(if
|
(if this.isUrgent "-urgent")
|
||||||
(or
|
|
||||||
@channel.isDirectMessageChannel (gt @channel.tracking.mentionCount 0)
|
|
||||||
)
|
|
||||||
"-urgent"
|
|
||||||
)
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div
|
<div class="chat-channel-unread-indicator__number">{{#if
|
||||||
class="chat-channel-unread-indicator__number"
|
this.showUnreadCount
|
||||||
>{{@channel.tracking.unreadCount}}</div>
|
}}{{this.unreadCount}}{{else}} {{/if}}</div>
|
||||||
</div>
|
</div>
|
||||||
{{/if}}
|
{{/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.addMessages(messages);
|
||||||
this.args.channel.details = meta;
|
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) {
|
if (result.threads) {
|
||||||
result.threads.forEach((thread) => {
|
result.threads.forEach((thread) => {
|
||||||
const storedThread = this.args.channel.threadsManager.add(
|
const storedThread = this.args.channel.threadsManager.add(
|
||||||
|
@ -237,8 +241,9 @@ export default class ChatLivePane extends Component {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (result.unread_thread_ids) {
|
if (result.unread_thread_overview) {
|
||||||
this.args.channel.unreadThreadIds = result.unread_thread_ids;
|
this.args.channel.threadsManager.unreadThreadOverview =
|
||||||
|
result.unread_thread_overview;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.requestedTargetMessageId) {
|
if (this.requestedTargetMessageId) {
|
||||||
|
@ -353,12 +358,9 @@ export default class ChatLivePane extends Component {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (result.thread_tracking_overview) {
|
if (result.unread_thread_overview) {
|
||||||
result.thread_tracking_overview.forEach((threadId) => {
|
this.args.channel.threadsManager.unreadThreadOverview =
|
||||||
if (!this.args.channel.threadTrackingOverview.includes(threadId)) {
|
result.unread_thread_overview;
|
||||||
this.args.channel.threadTrackingOverview.push(threadId);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
this.args.channel.details = meta;
|
this.args.channel.details = meta;
|
||||||
|
|
|
@ -9,7 +9,7 @@ export default class ChatThreadHeaderUnreadIndicator extends Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
get unreadCount() {
|
get unreadCount() {
|
||||||
return this.args.channel.unreadThreadCount;
|
return this.args.channel.threadsManager.unreadThreadCount;
|
||||||
}
|
}
|
||||||
|
|
||||||
get showUnreadIndicator() {
|
get showUnreadIndicator() {
|
||||||
|
|
|
@ -4,7 +4,7 @@
|
||||||
title={{i18n "chat.threads.list"}}
|
title={{i18n "chat.threads.list"}}
|
||||||
class={{concat-class
|
class={{concat-class
|
||||||
"chat-threads-list-button btn btn-flat"
|
"chat-threads-list-button btn btn-flat"
|
||||||
(if @channel.unreadThreadCount "has-unreads")
|
(if @channel.threadsManager.unreadThreadCount "has-unreads")
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{{d-icon "discourse-threads"}}
|
{{d-icon "discourse-threads"}}
|
||||||
|
|
|
@ -93,7 +93,13 @@ export default {
|
||||||
}
|
}
|
||||||
|
|
||||||
get suffixValue() {
|
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() {
|
get suffixCSSClass() {
|
||||||
|
|
|
@ -3,7 +3,7 @@ import { setOwner } from "@ember/application";
|
||||||
import Promise from "rsvp";
|
import Promise from "rsvp";
|
||||||
import ChatThread from "discourse/plugins/chat/discourse/models/chat-thread";
|
import ChatThread from "discourse/plugins/chat/discourse/models/chat-thread";
|
||||||
import { cached, tracked } from "@glimmer/tracking";
|
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
|
The ChatThreadsManager is responsible for managing the loaded chat threads
|
||||||
|
@ -19,11 +19,37 @@ export default class ChatThreadsManager {
|
||||||
@service chatApi;
|
@service chatApi;
|
||||||
|
|
||||||
@tracked _cached = new TrackedObject();
|
@tracked _cached = new TrackedObject();
|
||||||
|
@tracked _unreadThreadOverview = new TrackedMap();
|
||||||
|
|
||||||
constructor(owner) {
|
constructor(owner) {
|
||||||
setOwner(this, 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
|
@cached
|
||||||
get threads() {
|
get threads() {
|
||||||
return Object.values(this._cached);
|
return Object.values(this._cached);
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
import UserChatChannelMembership from "discourse/plugins/chat/discourse/models/user-chat-channel-membership";
|
import UserChatChannelMembership from "discourse/plugins/chat/discourse/models/user-chat-channel-membership";
|
||||||
import ChatMessage from "discourse/plugins/chat/discourse/models/chat-message";
|
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 { escapeExpression } from "discourse/lib/utilities";
|
||||||
import { tracked } from "@glimmer/tracking";
|
import { tracked } from "@glimmer/tracking";
|
||||||
import slugifyChannel from "discourse/plugins/chat/discourse/lib/slugify-channel";
|
import slugifyChannel from "discourse/plugins/chat/discourse/lib/slugify-channel";
|
||||||
|
@ -80,7 +79,6 @@ export default class ChatChannel {
|
||||||
threadsManager = new ChatThreadsManager(getOwner(this));
|
threadsManager = new ChatThreadsManager(getOwner(this));
|
||||||
messagesManager = new ChatMessagesManager(getOwner(this));
|
messagesManager = new ChatMessagesManager(getOwner(this));
|
||||||
|
|
||||||
@tracked _unreadThreadIds = new TrackedSet();
|
|
||||||
@tracked _currentUserMembership;
|
@tracked _currentUserMembership;
|
||||||
@tracked _lastMessage;
|
@tracked _lastMessage;
|
||||||
|
|
||||||
|
@ -119,16 +117,15 @@ export default class ChatChannel {
|
||||||
this.lastMessage = args.last_message;
|
this.lastMessage = args.last_message;
|
||||||
}
|
}
|
||||||
|
|
||||||
get unreadThreadCount() {
|
get unreadThreadsCountSinceLastViewed() {
|
||||||
return this.unreadThreadIds.size;
|
return Array.from(this.threadsManager.unreadThreadOverview.values()).filter(
|
||||||
|
(lastReplyCreatedAt) =>
|
||||||
|
lastReplyCreatedAt >= this.currentUserMembership.lastViewedAt
|
||||||
|
).length;
|
||||||
}
|
}
|
||||||
|
|
||||||
get unreadThreadIds() {
|
updateLastViewedAt() {
|
||||||
return this._unreadThreadIds;
|
this.currentUserMembership.lastViewedAt = new Date();
|
||||||
}
|
|
||||||
|
|
||||||
set unreadThreadIds(unreadThreadIds) {
|
|
||||||
this._unreadThreadIds = new TrackedSet(unreadThreadIds);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
findIndexOfMessage(id) {
|
findIndexOfMessage(id) {
|
||||||
|
|
|
@ -11,8 +11,8 @@ export default class UserChatChannelMembership {
|
||||||
@tracked desktopNotificationLevel = null;
|
@tracked desktopNotificationLevel = null;
|
||||||
@tracked mobileNotificationLevel = null;
|
@tracked mobileNotificationLevel = null;
|
||||||
@tracked lastReadMessageId = null;
|
@tracked lastReadMessageId = null;
|
||||||
@tracked user = null;
|
|
||||||
@tracked lastViewedAt = null;
|
@tracked lastViewedAt = null;
|
||||||
|
@tracked user = null;
|
||||||
|
|
||||||
constructor(args = {}) {
|
constructor(args = {}) {
|
||||||
this.following = args.following;
|
this.following = args.following;
|
||||||
|
@ -20,7 +20,7 @@ export default class UserChatChannelMembership {
|
||||||
this.desktopNotificationLevel = args.desktop_notification_level;
|
this.desktopNotificationLevel = args.desktop_notification_level;
|
||||||
this.mobileNotificationLevel = args.mobile_notification_level;
|
this.mobileNotificationLevel = args.mobile_notification_level;
|
||||||
this.lastReadMessageId = args.last_read_message_id;
|
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);
|
this.user = this.#initUserModel(args.user);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -6,6 +6,10 @@ export default class ChatIndexRoute extends DiscourseRoute {
|
||||||
@service chatChannelsManager;
|
@service chatChannelsManager;
|
||||||
@service router;
|
@service router;
|
||||||
|
|
||||||
|
activate() {
|
||||||
|
this.chat.activeChannel = null;
|
||||||
|
}
|
||||||
|
|
||||||
redirect() {
|
redirect() {
|
||||||
// Always want the channel index on mobile.
|
// Always want the channel index on mobile.
|
||||||
if (this.site.mobileView) {
|
if (this.site.mobileView) {
|
||||||
|
|
|
@ -211,7 +211,11 @@ export default class ChatSubscriptionsManager extends Service {
|
||||||
.find(channel.id, busData.thread_id)
|
.find(channel.id, busData.thread_id)
|
||||||
.then((thread) => {
|
.then((thread) => {
|
||||||
if (thread.currentUserMembership) {
|
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) {
|
if (busData.message.user.id === this.currentUser.id) {
|
||||||
// Thread should no longer be considered unread.
|
// Thread should no longer be considered unread.
|
||||||
if (thread.currentUserMembership) {
|
if (thread.currentUserMembership) {
|
||||||
channel.unreadThreadIds.delete(busData.thread_id);
|
channel.threadsManager.unreadThreadOverview.delete(
|
||||||
|
parseInt(busData.thread_id, 10)
|
||||||
|
);
|
||||||
thread.currentUserMembership.lastReadMessageId =
|
thread.currentUserMembership.lastReadMessageId =
|
||||||
busData.message.id;
|
busData.message.id;
|
||||||
}
|
}
|
||||||
|
@ -251,8 +257,12 @@ export default class ChatSubscriptionsManager extends Service {
|
||||||
(thread.currentUserMembership.lastReadMessageId || 0) &&
|
(thread.currentUserMembership.lastReadMessageId || 0) &&
|
||||||
!thread.currentUserMembership.isQuiet
|
!thread.currentUserMembership.isQuiet
|
||||||
) {
|
) {
|
||||||
channel.unreadThreadIds.add(busData.thread_id);
|
channel.threadsManager.markThreadUnread(
|
||||||
|
busData.thread_id,
|
||||||
|
busData.message.created_at
|
||||||
|
);
|
||||||
thread.tracking.unreadCount++;
|
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) {
|
_startUserTrackingStateSubscription(lastId) {
|
||||||
if (!this.currentUser) {
|
if (!this.currentUser) {
|
||||||
return;
|
return;
|
||||||
|
@ -316,8 +334,9 @@ export default class ChatSubscriptionsManager extends Service {
|
||||||
channel.tracking.unreadCount = busData.unread_count;
|
channel.tracking.unreadCount = busData.unread_count;
|
||||||
channel.tracking.mentionCount = busData.mention_count;
|
channel.tracking.mentionCount = busData.mention_count;
|
||||||
|
|
||||||
if (busData.hasOwnProperty("unread_thread_ids")) {
|
if (busData.hasOwnProperty("unread_thread_overview")) {
|
||||||
channel.unreadThreadIds = busData.unread_thread_ids;
|
channel.threadsManager.unreadThreadOverview =
|
||||||
|
busData.unread_thread_overview;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (busData.thread_id && busData.hasOwnProperty("thread_tracking")) {
|
if (busData.thread_id && busData.hasOwnProperty("thread_tracking")) {
|
||||||
|
|
|
@ -173,36 +173,44 @@ export default class Chat extends Service {
|
||||||
this.set("isNetworkUnreliable", false);
|
this.set("isNetworkUnreliable", false);
|
||||||
}
|
}
|
||||||
|
|
||||||
setupWithPreloadedChannels(channels) {
|
setupWithPreloadedChannels(channelsView) {
|
||||||
this.chatSubscriptionsManager.startChannelsSubscriptions(
|
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) => {
|
...channelsView.public_channels,
|
||||||
const channel = this.chatChannelsManager.store(channelObject);
|
...channelsView.direct_message_channels,
|
||||||
const storedDraft = (this.currentUser?.chat_drafts || []).find(
|
].forEach((channelObject) => {
|
||||||
(draft) => draft.channel_id === channel.id
|
const storedChannel = this.chatChannelsManager.store(channelObject);
|
||||||
);
|
const storedDraft = (this.currentUser?.chat_drafts || []).find(
|
||||||
|
(draft) => draft.channel_id === storedChannel.id
|
||||||
|
);
|
||||||
|
|
||||||
if (storedDraft) {
|
if (storedDraft) {
|
||||||
this.chatDraftsManager.add(
|
this.chatDraftsManager.add(
|
||||||
ChatMessage.createDraftMessage(
|
ChatMessage.createDraftMessage(
|
||||||
channel,
|
storedChannel,
|
||||||
Object.assign(
|
Object.assign(
|
||||||
{ user: this.currentUser },
|
{ user: this.currentUser },
|
||||||
JSON.parse(storedDraft.data)
|
JSON.parse(storedDraft.data)
|
||||||
)
|
|
||||||
)
|
)
|
||||||
);
|
)
|
||||||
}
|
);
|
||||||
|
|
||||||
return this.chatChannelsManager.follow(channel);
|
|
||||||
}
|
}
|
||||||
);
|
|
||||||
|
|
||||||
this.chatTrackingStateManager.setupWithPreloadedState(channels.tracking);
|
if (channelsView.unread_thread_overview?.[storedChannel.id]) {
|
||||||
|
storedChannel.threadsManager.unreadThreadOverview =
|
||||||
|
channelsView.unread_thread_overview[storedChannel.id];
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.chatChannelsManager.follow(storedChannel);
|
||||||
|
});
|
||||||
|
|
||||||
|
this.chatTrackingStateManager.setupWithPreloadedState(
|
||||||
|
channelsView.tracking
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
willDestroy() {
|
willDestroy() {
|
||||||
|
|
|
@ -237,6 +237,17 @@ after_initialize do
|
||||||
|
|
||||||
add_to_serializer(:current_user, :chat_channels) do
|
add_to_serializer(:current_user, :chat_channels) do
|
||||||
structured = Chat::ChannelFetcher.structured(self.scope)
|
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
|
Chat::ChannelIndexSerializer.new(structured, scope: self.scope, root: false).as_json
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -9,6 +9,7 @@ RSpec.describe Chat::TrackingStateReportQuery do
|
||||||
include_missing_memberships: include_missing_memberships,
|
include_missing_memberships: include_missing_memberships,
|
||||||
include_threads: include_threads,
|
include_threads: include_threads,
|
||||||
include_read: include_read,
|
include_read: include_read,
|
||||||
|
include_last_reply_details: include_last_reply_details,
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -20,6 +21,7 @@ RSpec.describe Chat::TrackingStateReportQuery do
|
||||||
let(:include_missing_memberships) { false }
|
let(:include_missing_memberships) { false }
|
||||||
let(:include_threads) { false }
|
let(:include_threads) { false }
|
||||||
let(:include_read) { true }
|
let(:include_read) { true }
|
||||||
|
let(:include_last_reply_details) { false }
|
||||||
context "when channel_ids empty" do
|
context "when channel_ids empty" do
|
||||||
it "returns empty object for channel_tracking" do
|
it "returns empty object for channel_tracking" do
|
||||||
expect(query.channel_tracking).to eq({})
|
expect(query.channel_tracking).to eq({})
|
||||||
|
@ -129,6 +131,56 @@ RSpec.describe Chat::TrackingStateReportQuery do
|
||||||
)
|
)
|
||||||
end
|
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
|
context "when thread_ids and channel_ids is empty" do
|
||||||
let(:thread_ids) { [] }
|
let(:thread_ids) { [] }
|
||||||
let(:channel_ids) { [] }
|
let(:channel_ids) { [] }
|
||||||
|
|
|
@ -173,6 +173,7 @@ RSpec.describe Chat::ChannelViewBuilder do
|
||||||
channel_ids: [channel.id],
|
channel_ids: [channel.id],
|
||||||
include_threads: true,
|
include_threads: true,
|
||||||
include_read: false,
|
include_read: false,
|
||||||
|
include_last_reply_details: true,
|
||||||
)
|
)
|
||||||
.returns(Chat::TrackingStateReport.new)
|
.returns(Chat::TrackingStateReport.new)
|
||||||
.once
|
.once
|
||||||
|
@ -188,7 +189,7 @@ RSpec.describe Chat::ChannelViewBuilder do
|
||||||
thread = Fabricate(:chat_thread, channel: channel)
|
thread = Fabricate(:chat_thread, channel: channel)
|
||||||
thread.add(current_user)
|
thread.add(current_user)
|
||||||
message_1 = Fabricate(:chat_message, chat_channel: channel, thread: thread)
|
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
|
end
|
||||||
|
|
||||||
it "fetches the tracking state of threads in the channel" do
|
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(
|
expect(message.data).to eq(
|
||||||
channel_1.id.to_s => {
|
channel_1.id.to_s => {
|
||||||
"last_read_message_id" => message_2.id,
|
"last_read_message_id" => message_2.id,
|
||||||
|
"last_reply_created_at" => nil,
|
||||||
"membership_id" => membership_1.id,
|
"membership_id" => membership_1.id,
|
||||||
"mention_count" => 0,
|
"mention_count" => 0,
|
||||||
"unread_count" => 0,
|
"unread_count" => 0,
|
||||||
},
|
},
|
||||||
channel_2.id.to_s => {
|
channel_2.id.to_s => {
|
||||||
"last_read_message_id" => message_4.id,
|
"last_read_message_id" => message_4.id,
|
||||||
|
"last_reply_created_at" => nil,
|
||||||
"membership_id" => membership_2.id,
|
"membership_id" => membership_2.id,
|
||||||
"mention_count" => 0,
|
"mention_count" => 0,
|
||||||
"unread_count" => 0,
|
"unread_count" => 0,
|
||||||
},
|
},
|
||||||
channel_3.id.to_s => {
|
channel_3.id.to_s => {
|
||||||
"last_read_message_id" => message_6.id,
|
"last_read_message_id" => message_6.id,
|
||||||
|
"last_reply_created_at" => nil,
|
||||||
"membership_id" => membership_3.id,
|
"membership_id" => membership_3.id,
|
||||||
"mention_count" => 0,
|
"mention_count" => 0,
|
||||||
"unread_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
|
context "when the channel has threading enabled and the message is a thread reply" do
|
||||||
fab!(:thread) { Fabricate(:chat_thread, channel: channel) }
|
fab!(:thread) { Fabricate(:chat_thread, channel: channel) }
|
||||||
|
|
||||||
before do
|
before do
|
||||||
message_1.update!(thread: thread)
|
message_1.update!(thread: thread)
|
||||||
|
thread.update_last_message_id!
|
||||||
channel.update!(threading_enabled: true)
|
channel.update!(threading_enabled: true)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -106,16 +108,22 @@ describe Chat::Publisher do
|
||||||
|
|
||||||
it "publishes the tracking state with correct counts" do
|
it "publishes the tracking state with correct counts" do
|
||||||
expect(data["thread_id"]).to eq(thread.id)
|
expect(data["thread_id"]).to eq(thread.id)
|
||||||
expect(data["unread_thread_ids"]).to eq([thread.id])
|
expect(data["unread_thread_overview"]).to eq(
|
||||||
expect(data["thread_tracking"]).to eq({ "unread_count" => 1, "mention_count" => 0 })
|
{ 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
|
||||||
end
|
end
|
||||||
|
|
||||||
context "when the user has no thread membership" do
|
context "when the user has no thread membership" do
|
||||||
it "publishes the tracking state with zeroed out counts" do
|
it "publishes the tracking state with zeroed out counts" do
|
||||||
expect(data["thread_id"]).to eq(thread.id)
|
expect(data["thread_id"]).to eq(thread.id)
|
||||||
expect(data["unread_thread_ids"]).to eq([])
|
expect(data["unread_thread_overview"]).to eq({})
|
||||||
expect(data["thread_tracking"]).to eq({ "unread_count" => 0, "mention_count" => 0 })
|
expect(data["thread_tracking"]).to eq(
|
||||||
|
{ "unread_count" => 0, "mention_count" => 0, "last_reply_created_at" => nil },
|
||||||
|
)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -26,6 +26,7 @@
|
||||||
"avatar_template": { "type": "string" },
|
"avatar_template": { "type": "string" },
|
||||||
"username": { "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-header-icon .chat-channel-unread-indicator", text: "")
|
||||||
expect(page).to have_css(
|
expect(page).to have_css(
|
||||||
".chat-channel-row[data-chat-channel-id=\"#{channel_1.id}\"] .chat-channel-unread-indicator",
|
".chat-channel-row[data-chat-channel-id=\"#{channel_1.id}\"] .chat-channel-unread-indicator",
|
||||||
text: 1,
|
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
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-header-icon .chat-channel-unread-indicator")
|
||||||
expect(page).to have_css(
|
expect(page).to have_css(
|
||||||
".chat-channel-row[data-chat-channel-id=\"#{channel_1.id}\"] .chat-channel-unread-indicator",
|
".chat-channel-row[data-chat-channel-id=\"#{channel_1.id}\"] .chat-channel-unread-indicator",
|
||||||
text: 1,
|
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -20,12 +20,22 @@ module PageObjects
|
||||||
end
|
end
|
||||||
|
|
||||||
def open_channel(channel)
|
def open_channel(channel)
|
||||||
find(
|
find("#{VISIBLE_DRAWER} .channels-list #{channel_row_selector(channel)}").click
|
||||||
"#{VISIBLE_DRAWER} .channels-list .chat-channel-row[data-chat-channel-id='#{channel.id}']",
|
|
||||||
).click
|
|
||||||
has_no_css?(".chat-skeleton")
|
has_no_css?(".chat-skeleton")
|
||||||
end
|
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
|
def maximize
|
||||||
mouseout
|
mouseout
|
||||||
find("#{VISIBLE_DRAWER} .chat-drawer-header__full-screen-btn").click
|
find("#{VISIBLE_DRAWER} .chat-drawer-header__full-screen-btn").click
|
||||||
|
|
|
@ -40,6 +40,16 @@ module PageObjects
|
||||||
find(".sidebar-section-link.channel-#{channel.id}")
|
find(".sidebar-section-link.channel-#{channel.id}")
|
||||||
self
|
self
|
||||||
end
|
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
|
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(drawer_page).to have_unread_thread_indicator(count: 1)
|
||||||
expect(thread_list_page).to have_unread_item(thread.id)
|
expect(thread_list_page).to have_unread_item(thread.id)
|
||||||
end
|
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
|
||||||
end
|
end
|
||||||
|
|
|
@ -10,6 +10,7 @@ describe "Thread tracking state | full page", type: :system do
|
||||||
let(:channel_page) { PageObjects::Pages::ChatChannel.new }
|
let(:channel_page) { PageObjects::Pages::ChatChannel.new }
|
||||||
let(:thread_page) { PageObjects::Pages::ChatThread.new }
|
let(:thread_page) { PageObjects::Pages::ChatThread.new }
|
||||||
let(:thread_list_page) { PageObjects::Components::Chat::ThreadList.new }
|
let(:thread_list_page) { PageObjects::Components::Chat::ThreadList.new }
|
||||||
|
let(:sidebar_page) { PageObjects::Pages::Sidebar.new }
|
||||||
|
|
||||||
before do
|
before do
|
||||||
SiteSetting.enable_experimental_chat_threaded_discussions = true
|
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)
|
expect(thread_list_page).to have_thread(new_thread)
|
||||||
end
|
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
|
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) }
|
before { thread.membership_for(current_user).update!(notification_level: :normal) }
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue