settings: Add option for followed topics to unread count badge setting.

This commit adds a new option 'DMs, mentions, and followed topics'
to 'desktop_icon_count_display' setting.

The total unread count of DMs, mentions, and followed topics appears
in desktop sidebar and browser tab when this option is configured.

Some existing options are relabeled and renumbered. We finally have:
* All unread messages
* DMs, mentions, and followed topics
* DMs and mentions
* None

Fixes #27503.
This commit is contained in:
Prakhar Pratyush 2023-11-16 14:15:19 +05:30 committed by Tim Abbott
parent 34f4622091
commit cc934429fe
8 changed files with 173 additions and 27 deletions

View File

@ -20,6 +20,15 @@ format used by the Zulip server that they are interacting with.
## Changes in Zulip 8.0 ## Changes in Zulip 8.0
**Feature level 227**
* [`PATCH /realm/user_settings_defaults`](/api/update-realm-user-settings-defaults),
[`POST /register`](/api/register-queue), [`PATCH /settings`](/api/update-settings):
Added `DMs, mentions, and followed topics` option for `desktop_icon_count_display`
setting, and renumbered the options.
The total unread count of DMs, mentions, and followed topics appears in
desktop sidebar and browser tab when this option is configured.
**Feature level 226** **Feature level 226**
* [`POST /register`](/api/register-queue), [`GET /events`](/api/get-events), * [`POST /register`](/api/register-queue), [`GET /events`](/api/get-events),

View File

@ -33,7 +33,7 @@ DESKTOP_WARNING_VERSION = "5.9.3"
# Changes should be accompanied by documentation explaining what the # Changes should be accompanied by documentation explaining what the
# new level means in api_docs/changelog.md, as well as "**Changes**" # new level means in api_docs/changelog.md, as well as "**Changes**"
# entries in the endpoint's documentation in `zulip.yaml`. # entries in the endpoint's documentation in `zulip.yaml`.
API_FEATURE_LEVEL = 226 API_FEATURE_LEVEL = 227
# Bump the minor PROVISION_VERSION to indicate that folks should provision # Bump the minor PROVISION_VERSION to indicate that folks should provision
# only when going from an old version of the code to a newer version. Bump # only when going from an old version of the code to a newer version. Bump

View File

@ -872,14 +872,18 @@ export const realm_name_in_email_notifications_policy_values = {
export const desktop_icon_count_display_values = { export const desktop_icon_count_display_values = {
messages: { messages: {
code: 1, code: 1,
description: $t({defaultMessage: "All unreads"}), description: $t({defaultMessage: "All unread messages"}),
}, },
notifiable: { dm_mention_followed_topic: {
code: 2, code: 2,
description: $t({defaultMessage: "Direct messages and mentions"}), description: $t({defaultMessage: "DMs, mentions, and followed topics"}),
},
dm_mention: {
code: 3,
description: $t({defaultMessage: "DMs and mentions"}),
}, },
none: { none: {
code: 3, code: 4,
description: $t({defaultMessage: "None"}), description: $t({defaultMessage: "None"}),
}, },
}; };

View File

@ -264,6 +264,7 @@ class UnreadTopicCounter {
get_counts(include_per_topic_count = false, include_per_topic_latest_msg_id = false) { get_counts(include_per_topic_count = false, include_per_topic_latest_msg_id = false) {
const res = {}; const res = {};
res.stream_unread_messages = 0; res.stream_unread_messages = 0;
res.followed_topic_unread_messages = 0;
res.stream_count = new Map(); // hash by stream_id -> count res.stream_count = new Map(); // hash by stream_id -> count
for (const [stream_id, per_stream_bucketer] of this.bucketer) { for (const [stream_id, per_stream_bucketer] of this.bucketer) {
// We track unread counts for streams that may be currently // We track unread counts for streams that may be currently
@ -308,6 +309,8 @@ class UnreadTopicCounter {
// unreads. // unreads.
res.stream_count.set(stream_id, this.get_stream_count(stream_id)); res.stream_count.set(stream_id, this.get_stream_count(stream_id));
res.stream_unread_messages += res.stream_count.get(stream_id).unmuted_count; res.stream_unread_messages += res.stream_count.get(stream_id).unmuted_count;
res.followed_topic_unread_messages +=
res.stream_count.get(stream_id).followed_count;
} }
} }
@ -365,9 +368,14 @@ class UnreadTopicCounter {
const sub = sub_store.get(stream_id); const sub = sub_store.get(stream_id);
let unmuted_count = 0; let unmuted_count = 0;
let muted_count = 0; let muted_count = 0;
let followed_count = 0;
for (const [topic, msgs] of per_stream_bucketer) { for (const [topic, msgs] of per_stream_bucketer) {
const topic_count = msgs.size; const topic_count = msgs.size;
if (user_topics.is_topic_followed(stream_id, topic)) {
followed_count += topic_count;
}
if (user_topics.is_topic_unmuted_or_followed(stream_id, topic)) { if (user_topics.is_topic_unmuted_or_followed(stream_id, topic)) {
unmuted_count += topic_count; unmuted_count += topic_count;
} else if (user_topics.is_topic_muted(stream_id, topic)) { } else if (user_topics.is_topic_muted(stream_id, topic)) {
@ -381,6 +389,7 @@ class UnreadTopicCounter {
const stream_count = { const stream_count = {
unmuted_count, unmuted_count,
muted_count, muted_count,
followed_count,
stream_is_muted: sub.is_muted, stream_is_muted: sub.is_muted,
}; };
return stream_count; return stream_count;
@ -484,6 +493,24 @@ class UnreadTopicCounter {
return streams_with_unmuted_mentions; return streams_with_unmuted_mentions;
} }
get_followed_topic_unread_mentions() {
let followed_topic_unread_mentions = 0;
for (const message_id of unread_mentions_counter) {
const stream_id = this.bucketer.reverse_lookup.get(message_id);
if (stream_id === undefined) {
// This is a direct message containing a mention.
continue;
}
const stream_bucketer = this.bucketer.get_bucket(stream_id);
const topic = stream_bucketer.reverse_lookup.get(message_id);
if (user_topics.is_topic_followed(stream_id, topic)) {
followed_topic_unread_mentions += 1;
}
}
return followed_topic_unread_mentions;
}
topic_has_any_unread(stream_id, topic) { topic_has_any_unread(stream_id, topic) {
const per_stream_bucketer = this.bucketer.get_bucket(stream_id); const per_stream_bucketer = this.bucketer.get_bucket(stream_id);
@ -808,6 +835,9 @@ export function get_counts() {
const streams_with_unmuted_mentions = unread_topic_counter.get_streams_with_unmuted_mentions(); const streams_with_unmuted_mentions = unread_topic_counter.get_streams_with_unmuted_mentions();
res.home_unread_messages = topic_res.stream_unread_messages; res.home_unread_messages = topic_res.stream_unread_messages;
res.stream_unread_messages = topic_res.stream_unread_messages; res.stream_unread_messages = topic_res.stream_unread_messages;
res.followed_topic_unread_messages_count = topic_res.followed_topic_unread_messages;
res.followed_topic_unread_messages_with_mention_count =
unread_topic_counter.get_followed_topic_unread_mentions();
res.stream_count = topic_res.stream_count; res.stream_count = topic_res.stream_count;
res.streams_with_mentions = [...streams_with_mentions]; res.streams_with_mentions = [...streams_with_mentions];
res.streams_with_unmuted_mentions = [...streams_with_unmuted_mentions]; res.streams_with_unmuted_mentions = [...streams_with_unmuted_mentions];
@ -824,19 +854,33 @@ export function get_counts() {
export function calculate_notifiable_count(res) { export function calculate_notifiable_count(res) {
let new_message_count = 0; let new_message_count = 0;
const only_show_notifiable = const only_show_dm_mention =
user_settings.desktop_icon_count_display === user_settings.desktop_icon_count_display ===
settings_config.desktop_icon_count_display_values.notifiable.code; settings_config.desktop_icon_count_display_values.dm_mention.code;
const only_show_dm_mention_followed_topic =
user_settings.desktop_icon_count_display ===
settings_config.desktop_icon_count_display_values.dm_mention_followed_topic.code;
const no_notifications = const no_notifications =
user_settings.desktop_icon_count_display === user_settings.desktop_icon_count_display ===
settings_config.desktop_icon_count_display_values.none.code; settings_config.desktop_icon_count_display_values.none.code;
if (only_show_notifiable) {
// DESKTOP_ICON_COUNT_DISPLAY_NOTIFIABLE if (only_show_dm_mention || only_show_dm_mention_followed_topic) {
new_message_count = // DESKTOP_ICON_COUNT_DISPLAY_DM_MENTION
const dm_mention_count =
res.mentioned_message_count + res.mentioned_message_count +
res.direct_message_count - res.direct_message_count -
// Avoid double-counting direct messages containing mentions // Avoid double-counting direct messages containing mentions
res.direct_message_with_mention_count; res.direct_message_with_mention_count;
if (only_show_dm_mention_followed_topic) {
// DESKTOP_ICON_COUNT_DISPLAY_DM_MENTION_FOLLOWED_TOPIC
// Avoid double-counting followed topic messages containing mentions
new_message_count =
dm_mention_count +
res.followed_topic_unread_messages_count -
res.followed_topic_unread_messages_with_mention_count;
} else {
new_message_count = dm_mention_count;
}
} else if (no_notifications) { } else if (no_notifications) {
// DESKTOP_ICON_COUNT_DISPLAY_NONE // DESKTOP_ICON_COUNT_DISPLAY_NONE
new_message_count = 0; new_message_count = 0;

View File

@ -59,6 +59,9 @@ function test_notifiable_count(home_unread_messages, expected_notifiable_count)
assert.deepEqual(notifiable_counts, expected_notifiable_count); assert.deepEqual(notifiable_counts, expected_notifiable_count);
user_settings.desktop_icon_count_display = 3; user_settings.desktop_icon_count_display = 3;
notifiable_counts = unread.get_notifiable_count(); notifiable_counts = unread.get_notifiable_count();
assert.deepEqual(notifiable_counts, expected_notifiable_count);
user_settings.desktop_icon_count_display = 4;
notifiable_counts = unread.get_notifiable_count();
assert.deepEqual(notifiable_counts, 0); assert.deepEqual(notifiable_counts, 0);
} }

View File

@ -0,0 +1,64 @@
# Generated by Django 4.2.7 on 2023-11-17 04:55
from django.db import migrations
from django.db.backends.base.schema import BaseDatabaseSchemaEditor
from django.db.migrations.state import StateApps
OLD_DESKTOP_ICON_COUNT_DISPLAY_DM_MENTION = 2
NEW_DESKTOP_ICON_COUNT_DISPLAY_DM_MENTION = 3
OLD_DESKTOP_ICON_COUNT_DISPLAY_NONE = 3
NEW_DESKTOP_ICON_COUNT_DISPLAY_NONE = 4
def renumber_options(apps: StateApps, schema_editor: BaseDatabaseSchemaEditor) -> None:
# We added a new option 'DESKTOP_ICON_COUNT_DISPLAY_DM_MENTION_FOLLOWED_TOPIC'
# for 'desktop_icon_count_display' setting. It has the value 2.
# The following options are renumbered:
# * 'DESKTOP_ICON_COUNT_DISPLAY_DM_MENTION' from 2 to 3
# * 'DESKTOP_ICON_COUNT_DISPLAY_NONE' from 3 to 4
# The migration is to update these values.
RealmUserDefault = apps.get_model("zerver", "RealmUserDefault")
UserProfile = apps.get_model("zerver", "UserProfile")
UserProfile.objects.filter(
desktop_icon_count_display=OLD_DESKTOP_ICON_COUNT_DISPLAY_NONE
).update(desktop_icon_count_display=NEW_DESKTOP_ICON_COUNT_DISPLAY_NONE)
RealmUserDefault.objects.filter(
desktop_icon_count_display=OLD_DESKTOP_ICON_COUNT_DISPLAY_NONE
).update(desktop_icon_count_display=NEW_DESKTOP_ICON_COUNT_DISPLAY_NONE)
UserProfile.objects.filter(
desktop_icon_count_display=OLD_DESKTOP_ICON_COUNT_DISPLAY_DM_MENTION
).update(desktop_icon_count_display=NEW_DESKTOP_ICON_COUNT_DISPLAY_DM_MENTION)
RealmUserDefault.objects.filter(
desktop_icon_count_display=OLD_DESKTOP_ICON_COUNT_DISPLAY_DM_MENTION
).update(desktop_icon_count_display=NEW_DESKTOP_ICON_COUNT_DISPLAY_DM_MENTION)
def reverse_code(apps: StateApps, schema_editor: BaseDatabaseSchemaEditor) -> None:
RealmUserDefault = apps.get_model("zerver", "RealmUserDefault")
UserProfile = apps.get_model("zerver", "UserProfile")
UserProfile.objects.filter(
desktop_icon_count_display=NEW_DESKTOP_ICON_COUNT_DISPLAY_NONE
).update(desktop_icon_count_display=OLD_DESKTOP_ICON_COUNT_DISPLAY_NONE)
RealmUserDefault.objects.filter(
desktop_icon_count_display=NEW_DESKTOP_ICON_COUNT_DISPLAY_NONE
).update(desktop_icon_count_display=OLD_DESKTOP_ICON_COUNT_DISPLAY_NONE)
UserProfile.objects.filter(
desktop_icon_count_display=NEW_DESKTOP_ICON_COUNT_DISPLAY_DM_MENTION
).update(desktop_icon_count_display=OLD_DESKTOP_ICON_COUNT_DISPLAY_DM_MENTION)
RealmUserDefault.objects.filter(
desktop_icon_count_display=NEW_DESKTOP_ICON_COUNT_DISPLAY_DM_MENTION
).update(desktop_icon_count_display=OLD_DESKTOP_ICON_COUNT_DISPLAY_DM_MENTION)
class Migration(migrations.Migration):
dependencies = [
("zerver", "0489_alter_realm_can_access_all_users_group"),
]
operations = [
migrations.RunPython(renumber_options, reverse_code=reverse_code, elidable=True),
]

View File

@ -1680,11 +1680,13 @@ class UserBaseSettings(models.Model):
enable_online_push_notifications = models.BooleanField(default=True) enable_online_push_notifications = models.BooleanField(default=True)
DESKTOP_ICON_COUNT_DISPLAY_MESSAGES = 1 DESKTOP_ICON_COUNT_DISPLAY_MESSAGES = 1
DESKTOP_ICON_COUNT_DISPLAY_NOTIFIABLE = 2 DESKTOP_ICON_COUNT_DISPLAY_DM_MENTION_FOLLOWED_TOPIC = 2
DESKTOP_ICON_COUNT_DISPLAY_NONE = 3 DESKTOP_ICON_COUNT_DISPLAY_DM_MENTION = 3
DESKTOP_ICON_COUNT_DISPLAY_NONE = 4
DESKTOP_ICON_COUNT_DISPLAY_CHOICES = [ DESKTOP_ICON_COUNT_DISPLAY_CHOICES = [
DESKTOP_ICON_COUNT_DISPLAY_MESSAGES, DESKTOP_ICON_COUNT_DISPLAY_MESSAGES,
DESKTOP_ICON_COUNT_DISPLAY_NOTIFIABLE, DESKTOP_ICON_COUNT_DISPLAY_DM_MENTION,
DESKTOP_ICON_COUNT_DISPLAY_DM_MENTION_FOLLOWED_TOPIC,
DESKTOP_ICON_COUNT_DISPLAY_NONE, DESKTOP_ICON_COUNT_DISPLAY_NONE,
] ]
desktop_icon_count_display = models.PositiveSmallIntegerField( desktop_icon_count_display = models.PositiveSmallIntegerField(

View File

@ -10461,15 +10461,20 @@ paths:
description: | description: |
Unread count badge (appears in desktop sidebar and browser tab) Unread count badge (appears in desktop sidebar and browser tab)
- 1 - All unreads - 1 - All unread messages
- 2 - Direct messages and mentions - 2 - DMs, mentions, and followed topics
- 3 - None - 3 - DMs and mentions
- 4 - None
**Changes**: In Zulip 8.0 (feature level 227), added `DMs, mentions, and followed
topics` option, renumbering the options to insert it in order.
schema: schema:
type: integer type: integer
enum: enum:
- 1 - 1
- 2 - 2
- 3 - 3
- 4
example: 1 example: 1
- name: realm_name_in_email_notifications_policy - name: realm_name_in_email_notifications_policy
in: query in: query
@ -12742,9 +12747,14 @@ paths:
description: | description: |
Unread count badge (appears in desktop sidebar and browser tab) Unread count badge (appears in desktop sidebar and browser tab)
- 1 - All unreads - 1 - All unread messages
- 2 - Direct messages and mentions - 2 - DMs, mentions, and followed topics
- 3 - None - 3 - DMs and mentions
- 4 - None
**Changes**: In Zulip 8.0 (feature level 227), added `DMs, mentions,
and followed topics` option, renumbering the options to insert it in
order.
realm_name_in_email_notifications_policy: realm_name_in_email_notifications_policy:
type: integer type: integer
description: | description: |
@ -14934,9 +14944,14 @@ paths:
description: | description: |
Unread count badge (appears in desktop sidebar and browser tab) Unread count badge (appears in desktop sidebar and browser tab)
- 1 - All unreads - 1 - All unread messages
- 2 - Direct messages and mentions - 2 - DMs, mentions, and followed topics
- 3 - None - 3 - DMs and mentions
- 4 - None
**Changes**: In Zulip 8.0 (feature level 227), added `DMs, mentions,
and followed topics` option, renumbering the options to insert it in
order.
realm_name_in_email_notifications_policy: realm_name_in_email_notifications_policy:
type: integer type: integer
description: | description: |
@ -16219,18 +16234,23 @@ paths:
description: | description: |
Unread count badge (appears in desktop sidebar and browser tab) Unread count badge (appears in desktop sidebar and browser tab)
- 1 - All unreads - 1 - All unread messages
- 2 - Direct messages and mentions - 2 - DMs, mentions, and followed topics
- 3 - None - 3 - DMs and mentions
- 4 - None
**Changes**: Before Zulip 5.0 (feature level 80), this setting was managed by **Changes**: In Zulip 8.0 (feature level 227), added `DMs, mentions, and followed
the `PATCH /settings/notifications` endpoint. topics` option, renumbering the options to insert it in order.
Before Zulip 5.0 (feature level 80), this setting was managed by the
`PATCH /settings/notifications` endpoint.
schema: schema:
type: integer type: integer
enum: enum:
- 1 - 1
- 2 - 2
- 3 - 3
- 4
example: 1 example: 1
- name: realm_name_in_email_notifications_policy - name: realm_name_in_email_notifications_policy
in: query in: query