FEATURE: Add ability to watch chat threads (#28639)

This change introduces a new thread notification level allowing users to get notified when someone replies to the thread.

Users who watch a thread will get a green notification on the chat icon and a user notification (blue). User notifications are consolidated based on thread id to prevent cluttering the original users notification area.

---------

Co-authored-by: Régis Hanol <regis@hanol.fr>
This commit is contained in:
David Battersby 2024-09-02 16:45:55 +04:00 committed by GitHub
parent cef1dcfc7d
commit 997fbc9757
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
40 changed files with 929 additions and 78 deletions

View File

@ -164,6 +164,7 @@ class Notification < ActiveRecord::Base
new_features: 37, new_features: 37,
admin_problems: 38, admin_problems: 38,
linked_consolidated: 39, linked_consolidated: 39,
chat_watched_thread: 40,
following: 800, # Used by https://github.com/discourse/discourse-follow following: 800, # Used by https://github.com/discourse/discourse-follow
following_created_topic: 801, # Used by https://github.com/discourse/discourse-follow following_created_topic: 801, # Used by https://github.com/discourse/discourse-follow
following_replied: 802, # Used by https://github.com/discourse/discourse-follow following_replied: 802, # Used by https://github.com/discourse/discourse-follow

View File

@ -15,7 +15,6 @@ module Jobs
@is_direct_message_channel = @chat_channel.direct_message_channel? @is_direct_message_channel = @chat_channel.direct_message_channel?
always_notification_level = ::Chat::UserChatChannelMembership::NOTIFICATION_LEVELS[:always] always_notification_level = ::Chat::UserChatChannelMembership::NOTIFICATION_LEVELS[:always]
members = members =
::Chat::UserChatChannelMembership ::Chat::UserChatChannelMembership
.includes(user: :groups) .includes(user: :groups)
@ -25,9 +24,10 @@ module Jobs
.where(chat_channel_id: @chat_channel.id) .where(chat_channel_id: @chat_channel.id)
.where(following: true) .where(following: true)
.where( .where(
"desktop_notification_level = ? OR mobile_notification_level = ?", "desktop_notification_level = :always OR mobile_notification_level = :always OR users.id IN (SELECT user_id FROM user_chat_thread_memberships WHERE thread_id = :thread_id AND notification_level = :watching)",
always_notification_level, always: always_notification_level,
always_notification_level, thread_id: @chat_message.thread_id,
watching: ::Chat::NotificationLevels.all[:watching],
) )
.merge(User.not_suspended) .merge(User.not_suspended)
@ -83,6 +83,17 @@ module Jobs
channel_id: @chat_channel.id, channel_id: @chat_channel.id,
} }
if @chat_message.in_thread? && !membership.muted?
thread_membership =
::Chat::UserChatThreadMembership.find_by(
user_id: user.id,
thread_id: @chat_message.thread_id,
notification_level: "watching",
)
thread_membership && create_watched_thread_notification(thread_membership)
end
if membership.desktop_notifications_always? && !membership.muted? if membership.desktop_notifications_always? && !membership.muted?
send_notification = send_notification =
DiscoursePluginRegistry.push_notification_filters.all? do |filter| DiscoursePluginRegistry.push_notification_filters.all? do |filter|
@ -101,6 +112,27 @@ module Jobs
::PostAlerter.push_notification(user, payload) ::PostAlerter.push_notification(user, payload)
end end
end end
def create_watched_thread_notification(thread_membership)
thread = @chat_message.thread
description = thread.title.presence || thread.original_message.message
data = {
username: @creator.username,
chat_message_id: @chat_message.id,
chat_channel_id: @chat_channel.id,
chat_thread_id: @chat_message.thread_id,
last_read_message_id: thread_membership&.last_read_message_id,
description: description,
user_ids: [@chat_message.user_id],
}
Notification.consolidate_or_create!(
notification_type: ::Notification.types[:chat_watched_thread],
user_id: thread_membership.user_id,
data: data.to_json,
)
end
end end
end end
end end

View File

@ -43,7 +43,10 @@ module Chat
before_create { self.last_message_id = self.original_message_id } before_create { self.last_message_id = self.original_message_id }
def add(user, notification_level: Chat::NotificationLevels.all[:tracking]) def add(user, notification_level: Chat::NotificationLevels.all[:tracking])
Chat::UserChatThreadMembership.find_or_create_by!( membership = Chat::UserChatThreadMembership.find_by(user: user, thread: self)
return membership if membership
Chat::UserChatThreadMembership.create!(
user: user, user: user,
thread: self, thread: self,
notification_level: notification_level, notification_level: notification_level,

View File

@ -8,11 +8,15 @@ module Chat
attr_accessor :channel_tracking, :thread_tracking attr_accessor :channel_tracking, :thread_tracking
class TrackingStateInfo class TrackingStateInfo
attr_accessor :unread_count, :mention_count, :last_reply_created_at attr_accessor :unread_count,
:mention_count,
:watched_threads_unread_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
@watched_threads_unread_count = info.present? ? info[:watched_threads_unread_count] : 0
@last_reply_created_at = info.present? ? info[:last_reply_created_at] : nil @last_reply_created_at = info.present? ? info[:last_reply_created_at] : nil
end end
@ -24,6 +28,7 @@ module Chat
{ {
unread_count: unread_count, unread_count: unread_count,
mention_count: mention_count, mention_count: mention_count,
watched_threads_unread_count: watched_threads_unread_count,
last_reply_created_at: last_reply_created_at, last_reply_created_at: last_reply_created_at,
} }
end end

View File

@ -43,11 +43,28 @@ module Chat
WHERE NOT read WHERE NOT read
AND user_chat_channel_memberships.chat_channel_id = memberships.chat_channel_id AND user_chat_channel_memberships.chat_channel_id = memberships.chat_channel_id
AND notifications.user_id = :user_id AND notifications.user_id = :user_id
AND notifications.notification_type = :notification_type AND notifications.notification_type = :notification_type_mention
AND (data::json->>'chat_message_id')::bigint > COALESCE(user_chat_channel_memberships.last_read_message_id, 0) AND (data::json->>'chat_message_id')::bigint > COALESCE(user_chat_channel_memberships.last_read_message_id, 0)
AND (data::json->>'chat_channel_id')::bigint = memberships.chat_channel_id AND (data::json->>'chat_channel_id')::bigint = memberships.chat_channel_id
AND (chat_messages.thread_id IS NULL OR chat_messages.id = chat_threads.original_message_id) AND (chat_messages.thread_id IS NULL OR chat_messages.id = chat_threads.original_message_id)
) AS mention_count, ) AS mention_count,
(
SELECT COUNT(*) AS watched_threads_unread_count
FROM chat_messages
INNER JOIN chat_channels ON chat_channels.id = chat_messages.chat_channel_id
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
WHERE chat_messages.chat_channel_id = memberships.chat_channel_id
AND chat_messages.thread_id = user_chat_thread_memberships.thread_id
AND chat_messages.user_id != :user_id
AND chat_messages.deleted_at IS NULL
AND chat_messages.thread_id IS NOT NULL
AND chat_messages.id != chat_threads.original_message_id
AND chat_messages.id > COALESCE(user_chat_thread_memberships.last_read_message_id, 0)
AND user_chat_thread_memberships.user_id = :user_id
AND user_chat_thread_memberships.notification_level = :watching_level
AND (chat_channels.threading_enabled OR chat_threads.force = true)
) AS watched_threads_unread_count,
memberships.chat_channel_id AS channel_id memberships.chat_channel_id AS channel_id
FROM user_chat_channel_memberships AS memberships FROM user_chat_channel_memberships AS memberships
WHERE memberships.user_id = :user_id AND memberships.chat_channel_id IN (:channel_ids) WHERE memberships.user_id = :user_id AND memberships.chat_channel_id IN (:channel_ids)
@ -59,12 +76,12 @@ module Chat
SELECT * FROM ( SELECT * FROM (
#{sql} #{sql}
) AS channel_tracking ) AS channel_tracking
WHERE (unread_count > 0 OR mention_count > 0) WHERE (unread_count > 0 OR mention_count > 0 OR watched_threads_unread_count > 0)
SQL SQL
sql += <<~SQL if include_missing_memberships && include_read sql += <<~SQL if include_missing_memberships && include_read
UNION ALL UNION ALL
SELECT 0 AS unread_count, 0 AS mention_count, chat_channels.id AS channel_id SELECT 0 AS unread_count, 0 AS mention_count, 0 AS watched_threads_unread_count, chat_channels.id AS channel_id
FROM chat_channels FROM chat_channels
LEFT JOIN user_chat_channel_memberships ON user_chat_channel_memberships.chat_channel_id = chat_channels.id LEFT JOIN user_chat_channel_memberships ON user_chat_channel_memberships.chat_channel_id = chat_channels.id
AND user_chat_channel_memberships.user_id = :user_id AND user_chat_channel_memberships.user_id = :user_id
@ -77,7 +94,8 @@ module Chat
sql, sql,
channel_ids: channel_ids, channel_ids: channel_ids,
user_id: user_id, user_id: user_id,
notification_type: Notification.types[:chat_mention], notification_type_mention: ::Notification.types[:chat_mention],
watching_level: ::Chat::UserChatThreadMembership.notification_levels[:watching],
limit: MAX_CHANNELS, limit: MAX_CHANNELS,
) )
end end

View File

@ -54,12 +54,33 @@ module Chat
AND chat_messages.thread_id IS NOT NULL AND chat_messages.thread_id IS NOT NULL
AND chat_messages.id != chat_threads.original_message_id AND chat_messages.id != chat_threads.original_message_id
AND (chat_channels.threading_enabled OR chat_threads.force = true) AND (chat_channels.threading_enabled OR chat_threads.force = true)
AND user_chat_thread_memberships.notification_level NOT IN (:quiet_notification_levels) AND user_chat_thread_memberships.notification_level = :tracking_level
AND original_message.deleted_at IS NULL AND original_message.deleted_at IS NULL
AND user_chat_channel_memberships.muted = false AND user_chat_channel_memberships.muted = false
AND user_chat_channel_memberships.user_id = :user_id AND user_chat_channel_memberships.user_id = :user_id
) AS unread_count, ) AS unread_count,
0 AS mention_count, 0 as mention_count,
(
SELECT COUNT(*) AS watched_threads_unread_count
FROM chat_messages
INNER JOIN chat_channels ON chat_channels.id = chat_messages.chat_channel_id
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
INNER JOIN user_chat_channel_memberships ON user_chat_channel_memberships.chat_channel_id = chat_messages.chat_channel_id
INNER JOIN chat_messages AS original_message ON original_message.id = chat_threads.original_message_id
WHERE 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
AND chat_messages.thread_id IS NOT NULL
AND chat_messages.id != chat_threads.original_message_id
AND (chat_channels.threading_enabled OR chat_threads.force = true)
AND user_chat_thread_memberships.notification_level = :watching_level
AND original_message.deleted_at IS NULL
AND user_chat_channel_memberships.user_id = :user_id
AND NOT user_chat_channel_memberships.muted
) AS watched_threads_unread_count,
chat_threads.channel_id, chat_threads.channel_id,
memberships.thread_id memberships.thread_id
FROM user_chat_thread_memberships AS memberships FROM user_chat_thread_memberships AS memberships
@ -75,12 +96,12 @@ module Chat
SELECT * FROM ( SELECT * FROM (
#{sql} #{sql}
) AS thread_tracking ) AS thread_tracking
WHERE (unread_count > 0 OR mention_count > 0) WHERE (unread_count > 0 OR mention_count > 0 OR watched_threads_unread_count > 0)
SQL SQL
sql += <<~SQL if include_missing_memberships && include_read sql += <<~SQL if include_missing_memberships && include_read
UNION ALL UNION ALL
SELECT 0 AS unread_count, 0 AS mention_count, chat_threads.channel_id, chat_threads.id AS thread_id SELECT 0 AS unread_count, 0 AS mention_count, 0 AS watched_threads_unread_count, chat_threads.channel_id, chat_threads.id AS thread_id
FROM chat_channels FROM chat_channels
INNER JOIN chat_threads ON chat_threads.channel_id = chat_channels.id INNER JOIN chat_threads ON chat_threads.channel_id = chat_channels.id
LEFT JOIN user_chat_thread_memberships ON user_chat_thread_memberships.thread_id = chat_threads.id LEFT JOIN user_chat_thread_memberships ON user_chat_thread_memberships.thread_id = chat_threads.id
@ -99,10 +120,8 @@ module Chat
user_id: user_id, user_id: user_id,
notification_type: ::Notification.types[:chat_mention], notification_type: ::Notification.types[:chat_mention],
limit: MAX_THREADS, limit: MAX_THREADS,
quiet_notification_levels: [ tracking_level: ::Chat::UserChatThreadMembership.notification_levels[:tracking],
::Chat::UserChatThreadMembership.notification_levels[:muted], watching_level: ::Chat::UserChatThreadMembership.notification_levels[:watching],
::Chat::UserChatThreadMembership.notification_levels[:normal],
],
) )
end end
end end

View File

@ -52,7 +52,14 @@ module Chat
include_read: include_read, include_read: include_read,
) )
.map do |ct| .map do |ct|
[ct.channel_id, { mention_count: ct.mention_count, unread_count: ct.unread_count }] [
ct.channel_id,
{
mention_count: ct.mention_count,
unread_count: ct.unread_count,
watched_threads_unread_count: ct.watched_threads_unread_count,
},
]
end end
.to_h .to_h
end end
@ -85,6 +92,7 @@ module Chat
channel_id: tt.channel_id, channel_id: tt.channel_id,
mention_count: tt.mention_count, mention_count: tt.mention_count,
unread_count: tt.unread_count, unread_count: tt.unread_count,
watched_threads_unread_count: tt.watched_threads_unread_count,
} }
if include_last_reply_details if include_last_reply_details

View File

@ -103,6 +103,7 @@ module Chat
"user_chat_thread_memberships.notification_level IN (?)", "user_chat_thread_memberships.notification_level IN (?)",
[ [
::Chat::UserChatThreadMembership.notification_levels[:normal], ::Chat::UserChatThreadMembership.notification_levels[:normal],
::Chat::UserChatThreadMembership.notification_levels[:watching],
::Chat::UserChatThreadMembership.notification_levels[:tracking], ::Chat::UserChatThreadMembership.notification_levels[:tracking],
], ],
) )

View File

@ -22,6 +22,9 @@ export default class ChatChannelUnreadIndicator extends Component {
if (this.#hasChannelMentions()) { if (this.#hasChannelMentions()) {
return this.args.channel.tracking.mentionCount; return this.args.channel.tracking.mentionCount;
} }
if (this.#hasWatchedThreads()) {
return this.args.channel.tracking.watchedThreadsUnreadCount;
}
return this.args.channel.tracking.unreadCount; return this.args.channel.tracking.unreadCount;
} }
@ -30,7 +33,9 @@ export default class ChatChannelUnreadIndicator extends Component {
return this.#hasChannelMentions(); return this.#hasChannelMentions();
} }
return ( return (
this.args.channel.isDirectMessageChannel || this.#hasChannelMentions() this.args.channel.isDirectMessageChannel ||
this.#hasChannelMentions() ||
this.#hasWatchedThreads()
); );
} }
@ -38,6 +43,10 @@ export default class ChatChannelUnreadIndicator extends Component {
return this.args.channel.tracking.mentionCount > 0; return this.args.channel.tracking.mentionCount > 0;
} }
#hasWatchedThreads() {
return this.args.channel.tracking.watchedThreadsUnreadCount > 0;
}
#onlyMentions() { #onlyMentions() {
return hasChatIndicator(this.currentUser).ONLY_MENTIONS; return hasChatIndicator(this.currentUser).ONLY_MENTIONS;
} }

View File

@ -680,6 +680,8 @@ export default class ChatChannel extends Component {
thread.tracking.unreadCount = threadTracking[thread.id].unread_count; thread.tracking.unreadCount = threadTracking[thread.id].unread_count;
thread.tracking.mentionCount = threadTracking[thread.id].mention_count; thread.tracking.mentionCount = threadTracking[thread.id].mention_count;
thread.tracking.watchedThreadsUnreadCount =
threadTracking[thread.id].watched_threads_unread_count;
} }
#flushIgnoreNextScroll() { #flushIgnoreNextScroll() {

View File

@ -84,6 +84,30 @@ export default class ChatThreadList extends Component {
thread.originalMessage?.id !== thread.lastMessageId thread.originalMessage?.id !== thread.lastMessageId
) )
.sort((threadA, threadB) => { .sort((threadA, threadB) => {
// if both threads have watched unread count, then show latest first
if (
threadA.tracking.watchedThreadsUnreadCount &&
threadB.tracking.watchedThreadsUnreadCount
) {
if (
threadA.preview.lastReplyCreatedAt >
threadB.preview.lastReplyCreatedAt
) {
return -1;
} else {
return 1;
}
}
// sort threads by watched unread count
if (threadA.tracking.watchedThreadsUnreadCount) {
return -1;
}
if (threadB.tracking.watchedThreadsUnreadCount) {
return 1;
}
// If both are unread we just want to sort by last reply date + time descending. // If both are unread we just want to sort by last reply date + time descending.
if (threadA.tracking.unreadCount && threadB.tracking.unreadCount) { if (threadA.tracking.unreadCount && threadB.tracking.unreadCount) {
if ( if (

View File

@ -28,6 +28,8 @@ export default class FooterUnreadIndicator extends Component {
return this.chatTrackingStateManager.publicChannelMentionCount; return this.chatTrackingStateManager.publicChannelMentionCount;
} else if (this.badgeType === DMS_TAB) { } else if (this.badgeType === DMS_TAB) {
return this.chatTrackingStateManager.directMessageUnreadCount; return this.chatTrackingStateManager.directMessageUnreadCount;
} else if (this.badgeType === THREADS_TAB) {
return this.chatTrackingStateManager.watchedThreadsUnreadCount;
} else { } else {
return 0; return 0;
} }

View File

@ -11,7 +11,9 @@ export default class Channel extends Component {
return ( return (
this.args.item.model.isDirectMessageChannel || this.args.item.model.isDirectMessageChannel ||
(this.args.item.model.isCategoryChannel && (this.args.item.model.isCategoryChannel &&
this.args.item.model.tracking.mentionCount > 0) this.args.item.model.tracking.mentionCount > 0) ||
(this.args.item.model.isCategoryChannel &&
this.args.item.model.tracking.watchedThreadsUnreadCount > 0)
); );
} }

View File

@ -25,6 +25,7 @@ export default class ChatThreadListItem extends Component {
class={{concatClass class={{concatClass
"chat-thread-list-item" "chat-thread-list-item"
(if (gt @thread.tracking.unreadCount 0) "-is-unread") (if (gt @thread.tracking.unreadCount 0) "-is-unread")
(if (gt @thread.tracking.watchedThreadsUnreadCount 0) "-is-urgent")
}} }}
data-thread-id={{@thread.id}} data-thread-id={{@thread.id}}
...attributes ...attributes

View File

@ -1,21 +1,32 @@
import Component from "@glimmer/component"; import Component from "@glimmer/component";
const MAX_UNREAD_COUNT = 99;
export default class ChatThreadUnreadIndicator extends Component { export default class ChatThreadUnreadIndicator extends Component {
get unreadCount() { get unreadCount() {
return this.args.thread.tracking.unreadCount; return this.args.thread.tracking.unreadCount;
} }
get urgentCount() {
return this.args.thread.tracking.watchedThreadsUnreadCount;
}
get showUnreadIndicator() { get showUnreadIndicator() {
return this.unreadCount > 0; return this.unreadCount > 0 || this.urgentCount > 0;
} }
get unreadCountLabel() { get unreadCountLabel() {
return this.unreadCount > 99 ? "99+" : this.unreadCount; const count = this.urgentCount > 0 ? this.urgentCount : this.unreadCount;
return count > MAX_UNREAD_COUNT ? `${MAX_UNREAD_COUNT}+` : count;
}
get isUrgent() {
return this.urgentCount > 0 ? "-urgent" : "";
} }
<template> <template>
{{#if this.showUnreadIndicator}} {{#if this.showUnreadIndicator}}
<span class="chat-thread-list-item-unread-indicator"> <span class="chat-thread-list-item-unread-indicator {{this.isUrgent}}">
<span class="chat-thread-list-item-unread-indicator__number"> <span class="chat-thread-list-item-unread-indicator__number">
{{this.unreadCountLabel}} {{this.unreadCountLabel}}
</span> </span>

View File

@ -46,6 +46,8 @@ export default class UserThreads extends Component {
if (tracking) { if (tracking) {
thread.tracking.mentionCount = tracking.mention_count; thread.tracking.mentionCount = tracking.mention_count;
thread.tracking.unreadCount = tracking.unread_count; thread.tracking.unreadCount = tracking.unread_count;
thread.tracking.watchedThreadsUnreadCount =
tracking.watched_threads_unread_count;
} }
this.trackChannel(thread.channel); this.trackChannel(thread.channel);

View File

@ -103,6 +103,43 @@ export default {
}; };
} }
); );
api.registerNotificationTypeRenderer(
"chat_watched_thread",
(NotificationItemBase) => {
return class extends NotificationItemBase {
icon = "discourse-threads";
linkTitle = I18n.t("notifications.titles.chat_watched_thread");
description = this.notification.data.description;
get label() {
const data = this.notification.data;
if (data.user_ids.length > 2) {
return I18n.t("notifications.chat_watched_thread_label", {
username: formatUsername(data.username2),
count: data.user_ids.length - 1,
});
} else if (data.user_ids.length === 2) {
return I18n.t("notifications.chat_watched_thread_label", {
username: formatUsername(data.username2),
username2: formatUsername(data.username),
count: 1,
});
} else {
return formatUsername(data.username);
}
}
get linkHref() {
const data = this.notification.data;
return getURL(
`/chat/c/-/${data.chat_channel_id}/t/${data.chat_thread_id}/${data.chat_message_id}`
);
}
};
}
);
} }
if (api.registerUserMenuTab) { if (api.registerUserMenuTab) {
@ -123,7 +160,8 @@ export default {
get count() { get count() {
return ( return (
this.getUnreadCountForType("chat_mention") + this.getUnreadCountForType("chat_mention") +
this.getUnreadCountForType("chat_invitation") this.getUnreadCountForType("chat_invitation") +
this.getUnreadCountForType("chat_watched_thread")
); );
} }
@ -133,6 +171,7 @@ export default {
"chat_mention", "chat_mention",
"chat_message", "chat_message",
"chat_quoted", "chat_quoted",
"chat_watched_thread",
]; ];
} }
}; };

View File

@ -4,6 +4,7 @@ import {
} from "discourse/lib/notification-levels"; } from "discourse/lib/notification-levels";
export const threadNotificationButtonLevels = [ export const threadNotificationButtonLevels = [
NotificationLevels.WATCHING,
NotificationLevels.TRACKING, NotificationLevels.TRACKING,
NotificationLevels.REGULAR, NotificationLevels.REGULAR,
].map(buttonDetails); ].map(buttonDetails);

View File

@ -114,6 +114,12 @@ export default class ChatChannel {
return Array.from(this.threadsManager.unreadThreadOverview.values()).length; return Array.from(this.threadsManager.unreadThreadOverview.values()).length;
} }
get watchedThreadsUnreadCount() {
return this.threadsManager.threads.reduce((unreadCount, thread) => {
return unreadCount + thread.tracking.watchedThreadsUnreadCount;
}, 0);
}
updateLastViewedAt() { updateLastViewedAt() {
this.currentUserMembership.lastViewedAt = new Date(); this.currentUserMembership.lastViewedAt = new Date();
} }

View File

@ -7,16 +7,19 @@ export default class ChatTrackingState {
@tracked _unreadCount; @tracked _unreadCount;
@tracked _mentionCount; @tracked _mentionCount;
@tracked _watchedThreadsUnreadCount;
constructor(owner, params = {}) { constructor(owner, params = {}) {
setOwner(this, owner); setOwner(this, owner);
this._unreadCount = params.unreadCount ?? 0; this._unreadCount = params.unreadCount ?? 0;
this._mentionCount = params.mentionCount ?? 0; this._mentionCount = params.mentionCount ?? 0;
this._watchedThreadsUnreadCount = params.watchedThreadsUnreadCount ?? 0;
} }
reset() { reset() {
this._unreadCount = 0; this._unreadCount = 0;
this._mentionCount = 0; this._mentionCount = 0;
this._watchedThreadsUnreadCount = 0;
} }
get unreadCount() { get unreadCount() {
@ -42,4 +45,16 @@ export default class ChatTrackingState {
this.chatTrackingStateManager.triggerNotificationsChanged(); this.chatTrackingStateManager.triggerNotificationsChanged();
} }
} }
get watchedThreadsUnreadCount() {
return this._watchedThreadsUnreadCount;
}
set watchedThreadsUnreadCount(value) {
const valueChanged = this._watchedThreadsUnreadCount !== value;
if (valueChanged) {
this._watchedThreadsUnreadCount = value;
this.chatTrackingStateManager.triggerNotificationsChanged();
}
}
} }

View File

@ -1,4 +1,5 @@
import Service, { service } from "@ember/service"; import Service, { service } from "@ember/service";
import { NotificationLevels } from "discourse/lib/notification-levels";
import { bind } from "discourse-common/utils/decorators"; import { bind } from "discourse-common/utils/decorators";
import I18n from "discourse-i18n"; import I18n from "discourse-i18n";
import { CHANNEL_STATUSES } from "discourse/plugins/chat/discourse/models/chat-channel"; import { CHANNEL_STATUSES } from "discourse/plugins/chat/discourse/models/chat-channel";
@ -267,7 +268,17 @@ export default class ChatSubscriptionsManager extends Service {
busData.thread_id, busData.thread_id,
busData.message.created_at busData.message.created_at
); );
thread.tracking.unreadCount++;
if (
thread.currentUserMembership.notificationLevel ===
NotificationLevels.WATCHING
) {
thread.tracking.watchedThreadsUnreadCount++;
channel.tracking.watchedThreadsUnreadCount++;
} else {
thread.tracking.unreadCount++;
}
this._updateActiveLastViewedAt(channel); this._updateActiveLastViewedAt(channel);
} }
} }
@ -339,6 +350,8 @@ 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;
channel.tracking.watchedThreadsUnreadCount =
busData.watched_threads_unread_count;
if ( if (
busData.hasOwnProperty("unread_thread_overview") && busData.hasOwnProperty("unread_thread_overview") &&
@ -366,6 +379,8 @@ export default class ChatSubscriptionsManager extends Service {
busData.thread_tracking.unread_count; busData.thread_tracking.unread_count;
thread.tracking.mentionCount = thread.tracking.mentionCount =
busData.thread_tracking.mention_count; busData.thread_tracking.mention_count;
thread.tracking.watchedThreadsUnreadCount =
busData.thread_tracking.watched_threads_unread_count;
} }
}); });
} }

View File

@ -70,7 +70,11 @@ export default class ChatTrackingStateManager extends Service {
} }
get allChannelUrgentCount() { get allChannelUrgentCount() {
return this.publicChannelMentionCount + this.directMessageUnreadCount; return (
this.publicChannelMentionCount +
this.directMessageUnreadCount +
this.watchedThreadsUnreadCount
);
} }
get hasUnreadThreads() { get hasUnreadThreads() {
@ -79,6 +83,12 @@ export default class ChatTrackingStateManager extends Service {
); );
} }
get watchedThreadsUnreadCount() {
return this.#publicChannels.reduce((unreadCount, channel) => {
return unreadCount + channel.tracking.watchedThreadsUnreadCount;
}, 0);
}
willDestroy() { willDestroy() {
super.willDestroy(...arguments); super.willDestroy(...arguments);
cancel(this._onTriggerNotificationDebounceHandler); cancel(this._onTriggerNotificationDebounceHandler);
@ -108,6 +118,8 @@ export default class ChatTrackingStateManager extends Service {
} }
model.tracking.unreadCount = state.unread_count; model.tracking.unreadCount = state.unread_count;
model.tracking.mentionCount = state.mention_count; model.tracking.mentionCount = state.mention_count;
model.tracking.watchedThreadsUnreadCount =
state.watched_threads_unread_count;
} }
get #publicChannels() { get #publicChannels() {

View File

@ -144,6 +144,8 @@ export default class Chat extends Service {
const state = channelsView.tracking.channel_tracking[channel.id]; const state = channelsView.tracking.channel_tracking[channel.id];
channel.tracking.unreadCount = state.unread_count; channel.tracking.unreadCount = state.unread_count;
channel.tracking.mentionCount = state.mention_count; channel.tracking.mentionCount = state.mention_count;
channel.tracking.watchedThreadsUnreadCount =
state.watched_threads_unread_count;
channel.currentUserMembership = channel.currentUserMembership =
channelObject.current_user_membership; channelObject.current_user_membership;

View File

@ -132,7 +132,7 @@ en:
header_indicator_preference: header_indicator_preference:
title: "Show activity indicator in header" title: "Show activity indicator in header"
all_new: "All New Messages" all_new: "All New Messages"
dm_and_mentions: "Direct Messages and Mentions" dm_and_mentions: "Direct Messages, Mentions and Watched Threads"
only_mentions: "Only Mentions" only_mentions: "Only Mentions"
never: "Never" never: "Never"
separate_sidebar_mode: separate_sidebar_mode:
@ -634,10 +634,13 @@ en:
notifications: notifications:
regular: regular:
title: "Normal" title: "Normal"
description: "You will be notified if someone mentions your @name in this thread." description: "Get notified when someone mentions you in this thread."
tracking: tracking:
title: "Tracking" title: "Tracking"
description: "A count of new replies for this thread will be shown in the thread list and the channel. You will be notified if someone mentions your @name in this thread." description: "Get notified when someone mentions you in this thread and see a count of new replies in the thread list."
watching:
title: "Watching"
description: "Get notified about all replies in this thread and see a count of new replies in the thread list."
participants_other_count: participants_other_count:
one: "+%{count}" one: "+%{count}"
other: "+%{count}" other: "+%{count}"
@ -664,6 +667,9 @@ en:
chat_invitation: "invited you to join a chat channel" chat_invitation: "invited you to join a chat channel"
chat_invitation_html: "<span>%{username}</span> <span>invited you to join a chat channel</span>" chat_invitation_html: "<span>%{username}</span> <span>invited you to join a chat channel</span>"
chat_quoted: "<span>%{username}</span> %{description}" chat_quoted: "<span>%{username}</span> %{description}"
chat_watched_thread_label:
one: "%{username} and %{username2}"
other: "%{username} and %{count} others"
popup: popup:
chat_mention: chat_mention:
@ -685,6 +691,7 @@ en:
chat_mention: "Chat mention" chat_mention: "Chat mention"
chat_invitation: "Chat invitation" chat_invitation: "Chat invitation"
chat_quoted: "Chat quoted" chat_quoted: "Chat quoted"
chat_watched_thread: "Chat watched thread"
action_codes: action_codes:
chat: chat:
enabled: '%{who} enabled <button class="btn-link open-chat">chat</button> %{when}' enabled: '%{who} enabled <button class="btn-link open-chat">chat</button> %{when}'

View File

@ -0,0 +1,63 @@
# frozen_string_literal: true
module Chat
module NotificationConsolidationExtension
CONSOLIDATION_THRESHOLD = 1
def self.watched_thread_message_plan
Notifications::ConsolidateNotifications.new(
from: Notification.types[:chat_watched_thread],
to: Notification.types[:chat_watched_thread],
threshold: CONSOLIDATION_THRESHOLD,
unconsolidated_query_blk:
Proc.new do |notifications, data|
notifications.where("data::json ->> 'consolidated' IS NULL").where(
"data::json ->> 'chat_thread_id' = ?",
data[:chat_thread_id].to_s,
)
end,
consolidated_query_blk:
Proc.new do |notifications, data|
notifications.where("(data::json ->> 'consolidated')::bool").where(
"data::json ->> 'chat_thread_id' = ?",
data[:chat_thread_id].to_s,
)
end,
).set_mutations(
set_data_blk:
lambda do |notification|
data = notification.data_hash
last_watched_thread_notification =
Notification
.where(user_id: notification.user_id)
.order("notifications.id DESC")
.where("data::json ->> 'chat_thread_id' = ?", data[:chat_thread_id].to_s)
.where(notification_type: Notification.types[:chat_watched_thread])
.first
return data if !last_watched_thread_notification
consolidated_data = last_watched_thread_notification.data_hash
if data[:last_read_message_id].to_i <= consolidated_data[:chat_message_id].to_i
data[:chat_message_id] = consolidated_data[:chat_message_id]
end
if !consolidated_data[:username2] && data[:username] != consolidated_data[:username]
data.merge(
username2: consolidated_data[:username],
user_ids: consolidated_data[:user_ids].concat(data[:user_ids]),
)
else
data.merge(
username: consolidated_data[:username],
username2: consolidated_data[:username2],
user_ids: (consolidated_data[:user_ids].concat(data[:user_ids])).uniq,
)
end
end,
)
end
end
end

View File

@ -68,6 +68,7 @@ after_initialize do
Guardian.prepend Chat::GuardianExtensions Guardian.prepend Chat::GuardianExtensions
UserNotifications.prepend Chat::UserNotificationsExtension UserNotifications.prepend Chat::UserNotificationsExtension
Notifications::ConsolidationPlan.prepend Chat::NotificationConsolidationExtension
UserOption.prepend Chat::UserOptionExtension UserOption.prepend Chat::UserOptionExtension
Category.prepend Chat::CategoryExtension Category.prepend Chat::CategoryExtension
Reviewable.prepend Chat::ReviewableExtension Reviewable.prepend Chat::ReviewableExtension
@ -533,6 +534,10 @@ after_initialize do
Proc.new { |user| Jobs.enqueue(Jobs::Chat::DeleteUserMessages, user_id: user.id) }, Proc.new { |user| Jobs.enqueue(Jobs::Chat::DeleteUserMessages, user_id: user.id) },
) )
register_notification_consolidation_plan(
Chat::NotificationConsolidationExtension.watched_thread_message_plan,
)
register_bookmarkable(Chat::MessageBookmarkable) register_bookmarkable(Chat::MessageBookmarkable)
end end

View File

@ -12,16 +12,21 @@ RSpec.describe Jobs::Chat::NotifyWatching do
SiteSetting.chat_allowed_groups = Group::AUTO_GROUPS[:everyone] SiteSetting.chat_allowed_groups = Group::AUTO_GROUPS[:everyone]
end end
def run_job def run_job(message)
described_class.new.execute(chat_message_id: message.id, except_user_ids: except_user_ids) described_class.new.execute(chat_message_id: message.id, except_user_ids: except_user_ids)
end end
def notification_messages_for(user) def notification_messages_for(user, chat_message: message)
MessageBus MessageBus
.track_publish { run_job } .track_publish { run_job(chat_message) }
.filter { |m| m.channel == "/chat/notification-alert/#{user.id}" } .filter { |m| m.channel == "/chat/notification-alert/#{user.id}" }
end end
def track_core_notification(user:, message:, type: ::Notification.types[:chat_watched_thread])
described_class.new.execute(chat_message_id: message.id)
Notification.where(user: user, notification_type: type).last
end
context "for a category channel" do context "for a category channel" do
fab!(:channel) { Fabricate(:category_channel) } fab!(:channel) { Fabricate(:category_channel) }
fab!(:membership1) do fab!(:membership1) do
@ -62,6 +67,77 @@ RSpec.describe Jobs::Chat::NotifyWatching do
) )
end end
context "with watched threads" do
fab!(:chat_message) { Fabricate(:chat_message, chat_channel: channel, user: user1) }
fab!(:thread) { Fabricate(:chat_thread, channel: channel, original_message: chat_message) }
fab!(:thread_message) do
Fabricate(:chat_message, chat_channel: channel, thread: thread, user: user2)
end
before { channel.update!(threading_enabled: true) }
context "with channel notification_level is always" do
before do
always = Chat::UserChatChannelMembership::NOTIFICATION_LEVELS[:always]
membership1.update!(desktop_notification_level: always, mobile_notification_level: always)
end
it "creates a core notification when watching the thread" do
thread.membership_for(user1).update!(
notification_level: Chat::NotificationLevels.all[:watching],
)
notification = track_core_notification(user: user1, message: thread_message)
expect(notification).to be_present
expect(notification.notification_type).to eq(Notification.types[:chat_watched_thread])
end
it "does not create a core notification when not watching the thread" do
notification = track_core_notification(user: user1, message: thread_message)
expect(notification).to be_nil
end
it "does not create a core notification when the channel is muted" do
thread.membership_for(user1).update!(
notification_level: Chat::NotificationLevels.all[:watching],
)
membership1.update!(muted: true)
notification = track_core_notification(user: user1, message: thread_message)
expect(notification).to be_nil
end
end
context "without channel notifications" do
before do
thread.membership_for(user1).update!(
notification_level: Chat::NotificationLevels.all[:watching],
)
end
it "creates a core notification for watched threads" do
expect { run_job(thread_message) }.to change { Notification.count }
end
it "does not create a core notification if channel is muted" do
membership1.update!(muted: true)
expect { run_job(thread_message) }.not_to change { Notification.count }
end
it "does not create a desktop notification" do
messages = notification_messages_for(user1)
expect(messages).to be_empty
end
it "does not create a mobile notification" do
PostAlerter.expects(:push_notification).never
run_job(thread_message)
end
end
end
context "with chat_notification_translation_args plugin_modifier" do context "with chat_notification_translation_args plugin_modifier" do
let(:modifier_block) do let(:modifier_block) do
Proc.new do |args| Proc.new do |args|

View File

@ -26,7 +26,14 @@ describe Chat::ChannelUnreadsQuery do
before { Fabricate(:chat_message, chat_channel: channel_1) } before { Fabricate(:chat_message, chat_channel: channel_1) }
it "returns a correct unread count" do it "returns a correct unread count" do
expect(query.first).to eq({ mention_count: 0, unread_count: 1, channel_id: channel_1.id }) expect(query.first).to eq(
{
mention_count: 0,
unread_count: 1,
channel_id: channel_1.id,
watched_threads_unread_count: 0,
},
)
end end
context "when the membership has been muted" do context "when the membership has been muted" do
@ -38,7 +45,14 @@ describe Chat::ChannelUnreadsQuery do
end end
it "returns a zeroed unread count" do it "returns a zeroed unread count" do
expect(query.first).to eq({ mention_count: 0, unread_count: 0, channel_id: channel_1.id }) expect(query.first).to eq(
{
mention_count: 0,
unread_count: 0,
channel_id: channel_1.id,
watched_threads_unread_count: 0,
},
)
end end
end end
@ -47,13 +61,27 @@ describe Chat::ChannelUnreadsQuery do
fab!(:thread) { Fabricate(:chat_thread, channel: channel_1, original_message: thread_om) } fab!(:thread) { Fabricate(:chat_thread, channel: channel_1, original_message: thread_om) }
it "does include the original message in the unread count" do it "does include the original message in the unread count" do
expect(query.first).to eq({ mention_count: 0, unread_count: 2, channel_id: channel_1.id }) expect(query.first).to eq(
{
mention_count: 0,
unread_count: 2,
watched_threads_unread_count: 0,
channel_id: channel_1.id,
},
)
end end
it "does not include other thread messages in the unread count" do it "does not include other thread messages in the unread count" do
Fabricate(:chat_message, chat_channel: channel_1, thread: thread) Fabricate(:chat_message, chat_channel: channel_1, thread: thread)
Fabricate(:chat_message, chat_channel: channel_1, thread: thread) Fabricate(:chat_message, chat_channel: channel_1, thread: thread)
expect(query.first).to eq({ mention_count: 0, unread_count: 2, channel_id: channel_1.id }) expect(query.first).to eq(
{
mention_count: 0,
unread_count: 2,
watched_threads_unread_count: 0,
channel_id: channel_1.id,
},
)
end end
end end
@ -70,8 +98,18 @@ describe Chat::ChannelUnreadsQuery do
it "returns accurate counts" do it "returns accurate counts" do
expect(query).to match_array( expect(query).to match_array(
[ [
{ mention_count: 0, unread_count: 1, channel_id: channel_1.id }, {
{ mention_count: 0, unread_count: 2, channel_id: channel_2.id }, mention_count: 0,
unread_count: 1,
watched_threads_unread_count: 0,
channel_id: channel_1.id,
},
{
mention_count: 0,
unread_count: 2,
watched_threads_unread_count: 0,
channel_id: channel_2.id,
},
], ],
) )
end end
@ -86,7 +124,14 @@ describe Chat::ChannelUnreadsQuery do
it "does not return counts for the channels" do it "does not return counts for the channels" do
expect(query).to match_array( expect(query).to match_array(
[{ mention_count: 0, unread_count: 1, channel_id: channel_1.id }], [
{
mention_count: 0,
unread_count: 1,
watched_threads_unread_count: 0,
channel_id: channel_1.id,
},
],
) )
end end
@ -96,8 +141,18 @@ describe Chat::ChannelUnreadsQuery do
it "does return zeroed counts for the channels" do it "does return zeroed counts for the channels" do
expect(query).to match_array( expect(query).to match_array(
[ [
{ mention_count: 0, unread_count: 1, channel_id: channel_1.id }, {
{ mention_count: 0, unread_count: 0, channel_id: channel_2.id }, mention_count: 0,
unread_count: 1,
watched_threads_unread_count: 0,
channel_id: channel_1.id,
},
{
mention_count: 0,
unread_count: 0,
watched_threads_unread_count: 0,
channel_id: channel_2.id,
},
], ],
) )
end end
@ -107,7 +162,14 @@ describe Chat::ChannelUnreadsQuery do
it "does not return counts for the channels" do it "does not return counts for the channels" do
expect(query).to match_array( expect(query).to match_array(
[{ mention_count: 0, unread_count: 1, channel_id: channel_1.id }], [
{
mention_count: 0,
unread_count: 1,
watched_threads_unread_count: 0,
channel_id: channel_1.id,
},
],
) )
end end
end end
@ -137,7 +199,14 @@ describe Chat::ChannelUnreadsQuery do
message = Fabricate(:chat_message, chat_channel: channel_1) message = Fabricate(:chat_message, chat_channel: channel_1)
create_mention(message, channel_1) create_mention(message, channel_1)
expect(query.first).to eq({ mention_count: 1, unread_count: 1, channel_id: channel_1.id }) expect(query.first).to eq(
{
mention_count: 1,
unread_count: 1,
watched_threads_unread_count: 0,
channel_id: channel_1.id,
},
)
end end
context "for unread mentions in a thread" do context "for unread mentions in a thread" do
@ -146,7 +215,14 @@ describe Chat::ChannelUnreadsQuery do
it "does include the original message in the mention count" do it "does include the original message in the mention count" do
create_mention(thread_om, channel_1) create_mention(thread_om, channel_1)
expect(query.first).to eq({ mention_count: 1, unread_count: 1, channel_id: channel_1.id }) expect(query.first).to eq(
{
mention_count: 1,
unread_count: 1,
watched_threads_unread_count: 0,
channel_id: channel_1.id,
},
)
end end
it "does not include other thread messages in the mention count" do it "does not include other thread messages in the mention count" do
@ -154,7 +230,14 @@ describe Chat::ChannelUnreadsQuery do
thread_message_2 = Fabricate(:chat_message, chat_channel: channel_1, thread: thread) thread_message_2 = Fabricate(:chat_message, chat_channel: channel_1, thread: thread)
create_mention(thread_message_1, channel_1) create_mention(thread_message_1, channel_1)
create_mention(thread_message_2, channel_1) create_mention(thread_message_2, channel_1)
expect(query.first).to eq({ mention_count: 0, unread_count: 1, channel_id: channel_1.id }) expect(query.first).to eq(
{
mention_count: 0,
unread_count: 1,
watched_threads_unread_count: 0,
channel_id: channel_1.id,
},
)
end end
end end
@ -173,8 +256,93 @@ describe Chat::ChannelUnreadsQuery do
expect(query).to match_array( expect(query).to match_array(
[ [
{ mention_count: 1, unread_count: 1, channel_id: channel_1.id }, {
{ mention_count: 1, unread_count: 2, channel_id: channel_2.id }, mention_count: 1,
unread_count: 1,
watched_threads_unread_count: 0,
channel_id: channel_1.id,
},
{
mention_count: 1,
unread_count: 2,
watched_threads_unread_count: 0,
channel_id: channel_2.id,
},
],
)
end
end
end
context "with watched threads" do
fab!(:message) { Fabricate(:chat_message, chat_channel: channel_1, user: current_user) }
fab!(:thread) { Fabricate(:chat_thread, channel: channel_1, original_message: message) }
fab!(:thread_reply) { Fabricate(:chat_message, chat_channel: channel_1, thread: thread) }
before do
channel_1.update(threading_enabled: true)
channel_1.membership_for(current_user).mark_read!(message.id)
thread.membership_for(current_user).update!(
notification_level: ::Chat::NotificationLevels.all[:watching],
)
end
it "returns correct watched thread unread count" do
expect(query.first).to eq(
{
mention_count: 0,
unread_count: 0,
watched_threads_unread_count: 1,
channel_id: channel_1.id,
},
)
end
it "returns unread and watched thread unread counts" do
Fabricate(:chat_message, chat_channel: channel_1)
expect(query.first).to eq(
{
mention_count: 0,
unread_count: 1,
watched_threads_unread_count: 1,
channel_id: channel_1.id,
},
)
end
context "for multiple channels" do
fab!(:channel_2) { Fabricate(:category_channel) }
fab!(:message_2) { Fabricate(:chat_message, chat_channel: channel_2, user: current_user) }
fab!(:thread_2) { Fabricate(:chat_thread, channel: channel_2, original_message: message_2) }
let(:channel_ids) { [channel_1.id, channel_2.id] }
before do
channel_2.add(current_user)
channel_2.update(threading_enabled: true)
channel_2.membership_for(current_user).mark_read!(message_2.id)
thread_2.membership_for(current_user).update!(
notification_level: ::Chat::NotificationLevels.all[:watching],
)
Fabricate(:chat_message, chat_channel: channel_2, thread: thread_2)
Fabricate(:chat_message, chat_channel: channel_2, thread: thread_2)
end
it "returns accurate counts" do
expect(query).to match_array(
[
{
mention_count: 0,
unread_count: 0,
watched_threads_unread_count: 1,
channel_id: channel_1.id,
},
{
mention_count: 0,
unread_count: 0,
watched_threads_unread_count: 2,
channel_id: channel_2.id,
},
], ],
) )
end end
@ -183,7 +351,14 @@ describe Chat::ChannelUnreadsQuery do
context "with nothing unread" do context "with nothing unread" do
it "returns a correct state" do it "returns a correct state" do
expect(query.first).to eq({ mention_count: 0, unread_count: 0, channel_id: channel_1.id }) expect(query.first).to eq(
{
mention_count: 0,
unread_count: 0,
watched_threads_unread_count: 0,
channel_id: channel_1.id,
},
)
end end
context "when include_read is false" do context "when include_read is false" do

View File

@ -47,10 +47,34 @@ describe Chat::ThreadUnreadsQuery do
it "gets a count of all the thread unreads across the channels" do it "gets a count of all the thread unreads across the channels" do
expect(query.map(&:to_h)).to match_array( expect(query.map(&:to_h)).to match_array(
[ [
{ channel_id: channel_1.id, mention_count: 0, thread_id: thread_1.id, unread_count: 1 }, {
{ channel_id: channel_1.id, mention_count: 0, thread_id: thread_2.id, unread_count: 0 }, channel_id: channel_1.id,
{ channel_id: channel_2.id, mention_count: 0, thread_id: thread_3.id, unread_count: 1 }, mention_count: 0,
{ channel_id: channel_2.id, mention_count: 0, thread_id: thread_4.id, unread_count: 1 }, thread_id: thread_1.id,
unread_count: 1,
watched_threads_unread_count: 0,
},
{
channel_id: channel_1.id,
mention_count: 0,
thread_id: thread_2.id,
unread_count: 0,
watched_threads_unread_count: 0,
},
{
channel_id: channel_2.id,
mention_count: 0,
thread_id: thread_3.id,
unread_count: 1,
watched_threads_unread_count: 0,
},
{
channel_id: channel_2.id,
mention_count: 0,
thread_id: thread_4.id,
unread_count: 1,
watched_threads_unread_count: 0,
},
], ],
) )
end end
@ -58,7 +82,13 @@ describe Chat::ThreadUnreadsQuery do
it "does not count deleted messages" do it "does not count deleted messages" do
message_1.trash! message_1.trash!
expect(query.map(&:to_h).find { |tracking| tracking[:thread_id] == thread_1.id }).to eq( expect(query.map(&:to_h).find { |tracking| tracking[:thread_id] == thread_1.id }).to eq(
{ channel_id: channel_1.id, mention_count: 0, thread_id: thread_1.id, unread_count: 0 }, {
channel_id: channel_1.id,
mention_count: 0,
thread_id: thread_1.id,
unread_count: 0,
watched_threads_unread_count: 0,
},
) )
end end
@ -66,17 +96,35 @@ describe Chat::ThreadUnreadsQuery do
channel_1.membership_for(current_user).update!(muted: true) channel_1.membership_for(current_user).update!(muted: true)
expect(query.map(&:to_h).find { |tracking| tracking[:thread_id] == thread_1.id }).to eq( expect(query.map(&:to_h).find { |tracking| tracking[:thread_id] == thread_1.id }).to eq(
{ channel_id: channel_1.id, mention_count: 0, thread_id: thread_1.id, unread_count: 0 }, {
channel_id: channel_1.id,
mention_count: 0,
thread_id: thread_1.id,
unread_count: 0,
watched_threads_unread_count: 0,
},
) )
end end
it "does not messages in threads where threading_enabled is false on the channel" do it "does not messages in threads where threading_enabled is false on the channel" do
channel_1.update!(threading_enabled: false) channel_1.update!(threading_enabled: false)
expect(query.map(&:to_h).find { |tracking| tracking[:thread_id] == thread_1.id }).to eq( expect(query.map(&:to_h).find { |tracking| tracking[:thread_id] == thread_1.id }).to eq(
{ channel_id: channel_1.id, mention_count: 0, thread_id: thread_1.id, unread_count: 0 }, {
channel_id: channel_1.id,
mention_count: 0,
thread_id: thread_1.id,
unread_count: 0,
watched_threads_unread_count: 0,
},
) )
expect(query.map(&:to_h).find { |tracking| tracking[:thread_id] == thread_2.id }).to eq( expect(query.map(&:to_h).find { |tracking| tracking[:thread_id] == thread_2.id }).to eq(
{ channel_id: channel_1.id, mention_count: 0, thread_id: thread_2.id, unread_count: 0 }, {
channel_id: channel_1.id,
mention_count: 0,
thread_id: thread_2.id,
unread_count: 0,
watched_threads_unread_count: 0,
},
) )
end end
@ -86,7 +134,13 @@ describe Chat::ThreadUnreadsQuery do
.find_by(user: current_user) .find_by(user: current_user)
.update!(last_read_message_id: message_1.id) .update!(last_read_message_id: message_1.id)
expect(query.map(&:to_h).find { |tracking| tracking[:thread_id] == thread_1.id }).to eq( expect(query.map(&:to_h).find { |tracking| tracking[:thread_id] == thread_1.id }).to eq(
{ channel_id: channel_1.id, mention_count: 0, thread_id: thread_1.id, unread_count: 0 }, {
channel_id: channel_1.id,
mention_count: 0,
thread_id: thread_1.id,
unread_count: 0,
watched_threads_unread_count: 0,
},
) )
end end
@ -94,14 +148,26 @@ describe Chat::ThreadUnreadsQuery do
thread_1.original_message.destroy thread_1.original_message.destroy
thread_1.update!(original_message: message_1) thread_1.update!(original_message: message_1)
expect(query.map(&:to_h).find { |tracking| tracking[:thread_id] == thread_1.id }).to eq( expect(query.map(&:to_h).find { |tracking| tracking[:thread_id] == thread_1.id }).to eq(
{ channel_id: channel_1.id, mention_count: 0, thread_id: thread_1.id, unread_count: 0 }, {
channel_id: channel_1.id,
mention_count: 0,
thread_id: thread_1.id,
unread_count: 0,
watched_threads_unread_count: 0,
},
) )
end end
it "does not count the thread as unread if the original message is deleted" do it "does not count the thread as unread if the original message is deleted" do
thread_1.original_message.destroy thread_1.original_message.destroy
expect(query.map(&:to_h).find { |tracking| tracking[:thread_id] == thread_1.id }).to eq( expect(query.map(&:to_h).find { |tracking| tracking[:thread_id] == thread_1.id }).to eq(
{ channel_id: channel_1.id, mention_count: 0, thread_id: thread_1.id, unread_count: 0 }, {
channel_id: channel_1.id,
mention_count: 0,
thread_id: thread_1.id,
unread_count: 0,
watched_threads_unread_count: 0,
},
) )
end end
@ -116,6 +182,7 @@ describe Chat::ThreadUnreadsQuery do
mention_count: 0, mention_count: 0,
thread_id: thread_2.id, thread_id: thread_2.id,
unread_count: 0, unread_count: 0,
watched_threads_unread_count: 0,
}, },
], ],
) )
@ -129,8 +196,20 @@ describe Chat::ThreadUnreadsQuery do
it "gets a count of all the thread unreads for the specified threads" do it "gets a count of all the thread unreads for the specified threads" do
expect(query.map(&:to_h)).to match_array( expect(query.map(&:to_h)).to match_array(
[ [
{ channel_id: channel_1.id, mention_count: 0, thread_id: thread_1.id, unread_count: 1 }, {
{ channel_id: channel_2.id, mention_count: 0, thread_id: thread_3.id, unread_count: 1 }, channel_id: channel_1.id,
mention_count: 0,
thread_id: thread_1.id,
unread_count: 1,
watched_threads_unread_count: 0,
},
{
channel_id: channel_2.id,
mention_count: 0,
thread_id: thread_3.id,
unread_count: 1,
watched_threads_unread_count: 0,
},
], ],
) )
end end
@ -145,7 +224,13 @@ describe Chat::ThreadUnreadsQuery do
it "gets a zeroed out count for the thread" do it "gets a zeroed out count for the thread" do
expect(query.map(&:to_h)).to include( expect(query.map(&:to_h)).to include(
{ channel_id: channel_1.id, mention_count: 0, thread_id: thread_1.id, unread_count: 0 }, {
channel_id: channel_1.id,
mention_count: 0,
thread_id: thread_1.id,
unread_count: 0,
watched_threads_unread_count: 0,
},
) )
end end
end end
@ -160,7 +245,13 @@ describe Chat::ThreadUnreadsQuery do
it "gets a zeroed out count for the thread" do it "gets a zeroed out count for the thread" do
expect(query.map(&:to_h)).to include( expect(query.map(&:to_h)).to include(
{ channel_id: channel_1.id, mention_count: 0, thread_id: thread_1.id, unread_count: 0 }, {
channel_id: channel_1.id,
mention_count: 0,
thread_id: thread_1.id,
unread_count: 0,
watched_threads_unread_count: 0,
},
) )
end end
end end
@ -176,6 +267,7 @@ describe Chat::ThreadUnreadsQuery do
mention_count: 0, mention_count: 0,
thread_id: thread_3.id, thread_id: thread_3.id,
unread_count: 1, unread_count: 1,
watched_threads_unread_count: 0,
}, },
], ],
) )
@ -192,12 +284,14 @@ describe Chat::ThreadUnreadsQuery do
mention_count: 0, mention_count: 0,
thread_id: thread_1.id, thread_id: thread_1.id,
unread_count: 0, unread_count: 0,
watched_threads_unread_count: 0,
}, },
{ {
channel_id: channel_2.id, channel_id: channel_2.id,
mention_count: 0, mention_count: 0,
thread_id: thread_3.id, thread_id: thread_3.id,
unread_count: 1, unread_count: 1,
watched_threads_unread_count: 0,
}, },
], ],
) )
@ -214,6 +308,7 @@ describe Chat::ThreadUnreadsQuery do
mention_count: 0, mention_count: 0,
thread_id: thread_3.id, thread_id: thread_3.id,
unread_count: 1, unread_count: 1,
watched_threads_unread_count: 0,
}, },
], ],
) )
@ -230,11 +325,113 @@ describe Chat::ThreadUnreadsQuery do
it "gets a count of all the thread unreads across the channels filtered by thread id" do it "gets a count of all the thread unreads across the channels filtered by thread id" do
expect(query.map(&:to_h)).to match_array( expect(query.map(&:to_h)).to match_array(
[ [
{ channel_id: channel_1.id, mention_count: 0, thread_id: thread_1.id, unread_count: 1 }, {
{ channel_id: channel_2.id, mention_count: 0, thread_id: thread_3.id, unread_count: 1 }, channel_id: channel_1.id,
mention_count: 0,
thread_id: thread_1.id,
unread_count: 1,
watched_threads_unread_count: 0,
},
{
channel_id: channel_2.id,
mention_count: 0,
thread_id: thread_3.id,
unread_count: 1,
watched_threads_unread_count: 0,
},
], ],
) )
end end
end end
end end
context "with watched threads" do
let(:channel_ids) { [channel_1.id] }
before do
[thread_1, thread_3].each do |thread|
thread.membership_for(current_user).update!(
notification_level: Chat::NotificationLevels.all[:watching],
)
end
3.times { Fabricate(:chat_message, chat_channel: channel_1, thread: thread_1) }
2.times { Fabricate(:chat_message, chat_channel: channel_1, thread: thread_2) }
end
it "returns correct count for channel" do
expect(query.map(&:to_h)).to match_array(
[
{
channel_id: channel_1.id,
thread_id: thread_1.id,
mention_count: 0,
unread_count: 0,
watched_threads_unread_count: 3,
},
{
channel_id: channel_1.id,
thread_id: thread_2.id,
mention_count: 0,
unread_count: 2,
watched_threads_unread_count: 0,
},
],
)
end
it "returns correct count across multiple channels" do
channel_ids.push(channel_2.id)
Fabricate(:chat_message, chat_channel: channel_2, thread: thread_3)
expect(query.map(&:to_h)).to match_array(
[
{
channel_id: channel_1.id,
thread_id: thread_1.id,
mention_count: 0,
unread_count: 0,
watched_threads_unread_count: 3,
},
{
channel_id: channel_1.id,
thread_id: thread_2.id,
mention_count: 0,
unread_count: 2,
watched_threads_unread_count: 0,
},
{
channel_id: channel_2.id,
thread_id: thread_3.id,
mention_count: 0,
unread_count: 0,
watched_threads_unread_count: 1,
},
{
channel_id: channel_2.id,
thread_id: thread_4.id,
mention_count: 0,
unread_count: 0,
watched_threads_unread_count: 0,
},
],
)
end
context "when include_read is false" do
let(:include_read) { false }
it "does not get threads with no unread messages" do
expect(query.map(&:to_h)).to include(
{
channel_id: channel_1.id,
thread_id: thread_1.id,
mention_count: 0,
unread_count: 0,
watched_threads_unread_count: 3,
},
)
end
end
end
end end

View File

@ -62,10 +62,12 @@ RSpec.describe Chat::TrackingStateReportQuery do
channel_1.id => { channel_1.id => {
unread_count: 1, unread_count: 1,
mention_count: 0, mention_count: 0,
watched_threads_unread_count: 0,
}, },
channel_2.id => { channel_2.id => {
unread_count: 1, unread_count: 1,
mention_count: 0, mention_count: 0,
watched_threads_unread_count: 0,
}, },
}, },
) )
@ -113,10 +115,12 @@ RSpec.describe Chat::TrackingStateReportQuery do
channel_1.id => { channel_1.id => {
unread_count: 1, unread_count: 1,
mention_count: 0, mention_count: 0,
watched_threads_unread_count: 0,
}, },
channel_2.id => { channel_2.id => {
unread_count: 1, unread_count: 1,
mention_count: 0, mention_count: 0,
watched_threads_unread_count: 0,
}, },
}, },
) )
@ -125,11 +129,13 @@ RSpec.describe Chat::TrackingStateReportQuery do
thread_1.id => { thread_1.id => {
unread_count: 1, unread_count: 1,
mention_count: 0, mention_count: 0,
watched_threads_unread_count: 0,
channel_id: channel_1.id, channel_id: channel_1.id,
}, },
thread_2.id => { thread_2.id => {
unread_count: 1, unread_count: 1,
mention_count: 0, mention_count: 0,
watched_threads_unread_count: 0,
channel_id: channel_2.id, channel_id: channel_2.id,
}, },
}, },
@ -152,12 +158,14 @@ RSpec.describe Chat::TrackingStateReportQuery do
thread_1.id => { thread_1.id => {
unread_count: 1, unread_count: 1,
mention_count: 0, mention_count: 0,
watched_threads_unread_count: 0,
channel_id: channel_1.id, channel_id: channel_1.id,
last_reply_created_at: thread_1.reload.last_message.created_at, last_reply_created_at: thread_1.reload.last_message.created_at,
}, },
thread_2.id => { thread_2.id => {
unread_count: 1, unread_count: 1,
mention_count: 0, mention_count: 0,
watched_threads_unread_count: 0,
channel_id: channel_2.id, channel_id: channel_2.id,
last_reply_created_at: thread_2.reload.last_message.created_at, last_reply_created_at: thread_2.reload.last_message.created_at,
}, },
@ -172,12 +180,14 @@ RSpec.describe Chat::TrackingStateReportQuery do
thread_1.id => { thread_1.id => {
unread_count: 0, unread_count: 0,
mention_count: 0, mention_count: 0,
watched_threads_unread_count: 0,
channel_id: channel_1.id, channel_id: channel_1.id,
last_reply_created_at: nil, last_reply_created_at: nil,
}, },
thread_2.id => { thread_2.id => {
unread_count: 1, unread_count: 1,
mention_count: 0, mention_count: 0,
watched_threads_unread_count: 0,
channel_id: channel_2.id, channel_id: channel_2.id,
last_reply_created_at: thread_2.reload.last_message.created_at, last_reply_created_at: thread_2.reload.last_message.created_at,
}, },

View File

@ -180,7 +180,14 @@ RSpec.describe Chat::ListChannelMessages do
Fabricate(:chat_message, chat_channel: channel, thread: thread_1) Fabricate(:chat_message, chat_channel: channel, thread: thread_1)
expect(result.tracking.thread_tracking).to eq( expect(result.tracking.thread_tracking).to eq(
{ thread_1.id => { channel_id: channel.id, mention_count: 0, unread_count: 0 } }, {
thread_1.id => {
channel_id: channel.id,
mention_count: 0,
unread_count: 0,
watched_threads_unread_count: 0,
},
},
) )
end end
@ -193,7 +200,14 @@ RSpec.describe Chat::ListChannelMessages do
Fabricate(:chat_message, chat_channel: channel, thread: thread_1) Fabricate(:chat_message, chat_channel: channel, thread: thread_1)
expect(result.tracking.thread_tracking).to eq( expect(result.tracking.thread_tracking).to eq(
{ thread_1.id => { channel_id: channel.id, mention_count: 0, unread_count: 1 } }, {
thread_1.id => {
channel_id: channel.id,
mention_count: 0,
unread_count: 1,
watched_threads_unread_count: 0,
},
},
) )
end end
end end
@ -214,7 +228,14 @@ RSpec.describe Chat::ListChannelMessages do
expect(result.tracking.channel_tracking).to eq({}) expect(result.tracking.channel_tracking).to eq({})
expect(result.tracking.thread_tracking).to eq( expect(result.tracking.thread_tracking).to eq(
{ thread_1.id => { channel_id: channel.id, mention_count: 0, unread_count: 1 } }, {
thread_1.id => {
channel_id: channel.id,
mention_count: 0,
unread_count: 1,
watched_threads_unread_count: 0,
},
},
) )
end end
end end

View File

@ -20,7 +20,7 @@ RSpec.describe Chat::ListUserChannels do
expect(result.structured[:public_channels]).to eq([channel_1]) expect(result.structured[:public_channels]).to eq([channel_1])
expect(result.structured[:direct_message_channels]).to eq([]) expect(result.structured[:direct_message_channels]).to eq([])
expect(result.structured[:tracking].channel_tracking[channel_1.id]).to eq( expect(result.structured[:tracking].channel_tracking[channel_1.id]).to eq(
{ mention_count: 0, unread_count: 0 }, { mention_count: 0, unread_count: 0, watched_threads_unread_count: 0 },
) )
end end

View File

@ -143,6 +143,7 @@ RSpec.describe Chat::MarkAllUserChannelsRead do
"membership_id" => membership_1.id, "membership_id" => membership_1.id,
"mention_count" => 0, "mention_count" => 0,
"unread_count" => 0, "unread_count" => 0,
"watched_threads_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,
@ -150,6 +151,7 @@ RSpec.describe Chat::MarkAllUserChannelsRead do
"membership_id" => membership_2.id, "membership_id" => membership_2.id,
"mention_count" => 0, "mention_count" => 0,
"unread_count" => 0, "unread_count" => 0,
"watched_threads_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,
@ -157,6 +159,7 @@ RSpec.describe Chat::MarkAllUserChannelsRead do
"membership_id" => membership_3.id, "membership_id" => membership_3.id,
"mention_count" => 0, "mention_count" => 0,
"unread_count" => 0, "unread_count" => 0,
"watched_threads_unread_count" => 0,
}, },
) )
end end

View File

@ -109,7 +109,12 @@ describe Chat::Publisher do
{ thread.id.to_s => thread.reload.last_message.created_at.iso8601(3) }, { thread.id.to_s => thread.reload.last_message.created_at.iso8601(3) },
) )
expect(data["thread_tracking"]).to eq( expect(data["thread_tracking"]).to eq(
{ "unread_count" => 1, "mention_count" => 0, "last_reply_created_at" => nil }, {
"unread_count" => 1,
"mention_count" => 0,
"watched_threads_unread_count" => 0,
"last_reply_created_at" => nil,
},
) )
end end
end end
@ -119,7 +124,12 @@ describe Chat::Publisher do
expect(data["thread_id"]).to eq(thread.id) expect(data["thread_id"]).to eq(thread.id)
expect(data["unread_thread_overview"]).to eq({}) expect(data["unread_thread_overview"]).to eq({})
expect(data["thread_tracking"]).to eq( expect(data["thread_tracking"]).to eq(
{ "unread_count" => 0, "mention_count" => 0, "last_reply_created_at" => nil }, {
"unread_count" => 0,
"mention_count" => 0,
"watched_threads_unread_count" => 0,
"last_reply_created_at" => nil,
},
) )
end end
end end

View File

@ -44,6 +44,7 @@ RSpec.describe ::Chat::TrackingState do
channel_1.id => { channel_1.id => {
unread_count: 4, # 2 messages + 2 thread original messages unread_count: 4, # 2 messages + 2 thread original messages
mention_count: 0, mention_count: 0,
watched_threads_unread_count: 0,
}, },
) )
end end
@ -55,11 +56,13 @@ RSpec.describe ::Chat::TrackingState do
channel_id: channel_1.id, channel_id: channel_1.id,
unread_count: 1, unread_count: 1,
mention_count: 0, mention_count: 0,
watched_threads_unread_count: 0,
}, },
thread_2.id => { thread_2.id => {
channel_id: channel_1.id, channel_id: channel_1.id,
unread_count: 2, unread_count: 2,
mention_count: 0, mention_count: 0,
watched_threads_unread_count: 0,
}, },
) )
end end
@ -74,6 +77,7 @@ RSpec.describe ::Chat::TrackingState do
channel_1.id => { channel_1.id => {
unread_count: 4, # 2 messages + 2 thread original messages unread_count: 4, # 2 messages + 2 thread original messages
mention_count: 0, mention_count: 0,
watched_threads_unread_count: 0,
}, },
) )
end end
@ -89,6 +93,7 @@ RSpec.describe ::Chat::TrackingState do
channel_1.id => { channel_1.id => {
unread_count: 4, # 2 messages + 2 thread original messages unread_count: 4, # 2 messages + 2 thread original messages
mention_count: 0, mention_count: 0,
watched_threads_unread_count: 0,
}, },
) )
end end
@ -100,6 +105,7 @@ RSpec.describe ::Chat::TrackingState do
channel_id: channel_1.id, channel_id: channel_1.id,
unread_count: 2, unread_count: 2,
mention_count: 0, mention_count: 0,
watched_threads_unread_count: 0,
}, },
) )
end end
@ -117,10 +123,12 @@ RSpec.describe ::Chat::TrackingState do
channel_1.id => { channel_1.id => {
unread_count: 4, # 2 messages + 2 thread original messages unread_count: 4, # 2 messages + 2 thread original messages
mention_count: 0, mention_count: 0,
watched_threads_unread_count: 0,
}, },
channel_2.id => { channel_2.id => {
unread_count: 0, unread_count: 0,
mention_count: 0, mention_count: 0,
watched_threads_unread_count: 0,
}, },
) )
end end
@ -132,21 +140,25 @@ RSpec.describe ::Chat::TrackingState do
channel_id: channel_1.id, channel_id: channel_1.id,
unread_count: 1, unread_count: 1,
mention_count: 0, mention_count: 0,
watched_threads_unread_count: 0,
}, },
thread_2.id => { thread_2.id => {
channel_id: channel_1.id, channel_id: channel_1.id,
unread_count: 2, unread_count: 2,
mention_count: 0, mention_count: 0,
watched_threads_unread_count: 0,
}, },
thread_3.id => { thread_3.id => {
channel_id: channel_2.id, channel_id: channel_2.id,
unread_count: 0, unread_count: 0,
mention_count: 0, mention_count: 0,
watched_threads_unread_count: 0,
}, },
thread_4.id => { thread_4.id => {
channel_id: channel_2.id, channel_id: channel_2.id,
unread_count: 0, unread_count: 0,
mention_count: 0, mention_count: 0,
watched_threads_unread_count: 0,
}, },
) )
end end

View File

@ -153,6 +153,17 @@ RSpec.describe "Mobile Chat footer", type: :system, mobile: true do
expect(page).to have_no_css("#c-footer-threads .c-unread-indicator") expect(page).to have_no_css("#c-footer-threads .c-unread-indicator")
end end
it "is urgent for watched thread messages" do
thread.membership_for(current_user).update!(
notification_level: ::Chat::NotificationLevels.all[:watching],
)
visit("/")
chat_page.open_from_header
expect(page).to have_css("#c-footer-threads .c-unread-indicator.-urgent")
end
end end
end end
end end

View File

@ -45,20 +45,23 @@ module PageObjects
".chat-thread-list-item__last-reply-timestamp .relative-date[data-time='#{(last_reply.created_at.iso8601.to_time.to_f * 1000).to_i}']" ".chat-thread-list-item__last-reply-timestamp .relative-date[data-time='#{(last_reply.created_at.iso8601.to_time.to_f * 1000).to_i}']"
end end
def has_unread_item?(id, count: nil) def has_unread_item?(id, count: nil, urgent: false)
selector_class = urgent ? ".-is-urgent" : ".-is-unread"
if count.nil? if count.nil?
component.has_css?(item_by_id_selector(id) + ".-is-unread") component.has_css?(item_by_id_selector(id) + selector_class)
else else
component.has_css?( component.has_css?(
item_by_id_selector(id) + item_by_id_selector(id) + selector_class +
".-is-unread .chat-thread-list-item-unread-indicator__number", " .chat-thread-list-item-unread-indicator__number",
text: count.to_s, text: count.to_s,
) )
end end
end end
def has_no_unread_item?(id) def has_no_unread_item?(id, urgent: false)
component.has_no_css?(item_by_id_selector(id) + ".-is-unread") selector_class = urgent ? ".-is-urgent" : ".-is-unread"
component.has_no_css?(item_by_id_selector(id) + selector_class)
end end
def item_by_id_selector(id) def item_by_id_selector(id)

View File

@ -43,6 +43,19 @@ describe "Thread tracking state | drawer", type: :system do
expect(thread_list_page).to have_unread_item(thread.id) expect(thread_list_page).to have_unread_item(thread.id)
end end
it "shows an urgent indicator on the watched thread in the list" do
thread.membership_for(current_user).update!(
notification_level: ::Chat::NotificationLevels.all[:watching],
)
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, urgent: true)
end
it "marks the thread as read and removes both indicators when the user opens it" do it "marks the thread as read and removes both indicators when the user opens it" do
skip("Flaky on CI") if ENV["CI"] skip("Flaky on CI") if ENV["CI"]

View File

@ -74,6 +74,18 @@ describe "Thread tracking state | full page", type: :system do
expect(thread_list_page).to have_no_unread_item(thread.id) expect(thread_list_page).to have_no_unread_item(thread.id)
end end
it "shows and urgent for the header of the list when a new watched unread arrives" do
thread.membership_for(current_user).update!(last_read_message_id: message_2.id)
thread.membership_for(current_user).update!(notification_level: :watching)
chat_page.visit_channel(channel)
channel_page.open_thread_list
expect(thread_list_page).to have_no_unread_item(thread.id, urgent: true)
Fabricate(:chat_message, thread: thread, use_service: true)
expect(thread_list_page).to have_unread_item(thread.id, urgent: true)
end
it "allows the user to change their tracking level for an existing thread" do it "allows the user to change their tracking level for an existing thread" do
chat_page.visit_thread(thread) chat_page.visit_thread(thread)
thread_page.notification_level = :normal thread_page.notification_level = :normal

View File

@ -119,6 +119,9 @@
"chat_quoted": { "chat_quoted": {
"type": "integer" "type": "integer"
}, },
"chat_watched_thread": {
"type": "integer"
},
"assigned": { "assigned": {
"type": "integer" "type": "integer"
}, },