diff --git a/api_docs/changelog.md b/api_docs/changelog.md index 72b5374f66..520090c494 100644 --- a/api_docs/changelog.md +++ b/api_docs/changelog.md @@ -20,6 +20,15 @@ format used by the Zulip server that they are interacting with. ## 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** * [`POST /register`](/api/register-queue), [`GET /events`](/api/get-events), diff --git a/version.py b/version.py index ec7a8faeb2..2b0148d9e4 100644 --- a/version.py +++ b/version.py @@ -33,7 +33,7 @@ DESKTOP_WARNING_VERSION = "5.9.3" # Changes should be accompanied by documentation explaining what the # new level means in api_docs/changelog.md, as well as "**Changes**" # 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 # only when going from an old version of the code to a newer version. Bump diff --git a/web/src/settings_config.ts b/web/src/settings_config.ts index 211534281a..034e34ff12 100644 --- a/web/src/settings_config.ts +++ b/web/src/settings_config.ts @@ -872,14 +872,18 @@ export const realm_name_in_email_notifications_policy_values = { export const desktop_icon_count_display_values = { messages: { code: 1, - description: $t({defaultMessage: "All unreads"}), + description: $t({defaultMessage: "All unread messages"}), }, - notifiable: { + dm_mention_followed_topic: { 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: { - code: 3, + code: 4, description: $t({defaultMessage: "None"}), }, }; diff --git a/web/src/unread.js b/web/src/unread.js index 336fdcf92d..2e44f699ec 100644 --- a/web/src/unread.js +++ b/web/src/unread.js @@ -264,6 +264,7 @@ class UnreadTopicCounter { get_counts(include_per_topic_count = false, include_per_topic_latest_msg_id = false) { const res = {}; res.stream_unread_messages = 0; + res.followed_topic_unread_messages = 0; res.stream_count = new Map(); // hash by stream_id -> count for (const [stream_id, per_stream_bucketer] of this.bucketer) { // We track unread counts for streams that may be currently @@ -308,6 +309,8 @@ class UnreadTopicCounter { // unreads. 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.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); let unmuted_count = 0; let muted_count = 0; + let followed_count = 0; for (const [topic, msgs] of per_stream_bucketer) { 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)) { unmuted_count += topic_count; } else if (user_topics.is_topic_muted(stream_id, topic)) { @@ -381,6 +389,7 @@ class UnreadTopicCounter { const stream_count = { unmuted_count, muted_count, + followed_count, stream_is_muted: sub.is_muted, }; return stream_count; @@ -484,6 +493,24 @@ class UnreadTopicCounter { 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) { 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(); res.home_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.streams_with_mentions = [...streams_with_mentions]; res.streams_with_unmuted_mentions = [...streams_with_unmuted_mentions]; @@ -824,19 +854,33 @@ export function get_counts() { export function calculate_notifiable_count(res) { let new_message_count = 0; - const only_show_notifiable = + const only_show_dm_mention = 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 = user_settings.desktop_icon_count_display === settings_config.desktop_icon_count_display_values.none.code; - if (only_show_notifiable) { - // DESKTOP_ICON_COUNT_DISPLAY_NOTIFIABLE - new_message_count = + + if (only_show_dm_mention || only_show_dm_mention_followed_topic) { + // DESKTOP_ICON_COUNT_DISPLAY_DM_MENTION + const dm_mention_count = res.mentioned_message_count + res.direct_message_count - // Avoid double-counting direct messages containing mentions 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) { // DESKTOP_ICON_COUNT_DISPLAY_NONE new_message_count = 0; diff --git a/web/tests/unread.test.js b/web/tests/unread.test.js index 2a73ae0c15..82db19a280 100644 --- a/web/tests/unread.test.js +++ b/web/tests/unread.test.js @@ -59,6 +59,9 @@ function test_notifiable_count(home_unread_messages, expected_notifiable_count) assert.deepEqual(notifiable_counts, expected_notifiable_count); user_settings.desktop_icon_count_display = 3; 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); } diff --git a/zerver/migrations/0490_renumber_options_desktop_icon_count_display.py b/zerver/migrations/0490_renumber_options_desktop_icon_count_display.py new file mode 100644 index 0000000000..868bde1f17 --- /dev/null +++ b/zerver/migrations/0490_renumber_options_desktop_icon_count_display.py @@ -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), + ] diff --git a/zerver/models.py b/zerver/models.py index dfcff72005..454fd820f3 100644 --- a/zerver/models.py +++ b/zerver/models.py @@ -1680,11 +1680,13 @@ class UserBaseSettings(models.Model): enable_online_push_notifications = models.BooleanField(default=True) DESKTOP_ICON_COUNT_DISPLAY_MESSAGES = 1 - DESKTOP_ICON_COUNT_DISPLAY_NOTIFIABLE = 2 - DESKTOP_ICON_COUNT_DISPLAY_NONE = 3 + DESKTOP_ICON_COUNT_DISPLAY_DM_MENTION_FOLLOWED_TOPIC = 2 + DESKTOP_ICON_COUNT_DISPLAY_DM_MENTION = 3 + DESKTOP_ICON_COUNT_DISPLAY_NONE = 4 DESKTOP_ICON_COUNT_DISPLAY_CHOICES = [ 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 = models.PositiveSmallIntegerField( diff --git a/zerver/openapi/zulip.yaml b/zerver/openapi/zulip.yaml index d480e77f52..f5e17477cd 100644 --- a/zerver/openapi/zulip.yaml +++ b/zerver/openapi/zulip.yaml @@ -10461,15 +10461,20 @@ paths: description: | Unread count badge (appears in desktop sidebar and browser tab) - - 1 - All unreads - - 2 - Direct messages and mentions - - 3 - None + - 1 - All unread messages + - 2 - DMs, mentions, and followed topics + - 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: type: integer enum: - 1 - 2 - 3 + - 4 example: 1 - name: realm_name_in_email_notifications_policy in: query @@ -12742,9 +12747,14 @@ paths: description: | Unread count badge (appears in desktop sidebar and browser tab) - - 1 - All unreads - - 2 - Direct messages and mentions - - 3 - None + - 1 - All unread messages + - 2 - DMs, mentions, and followed topics + - 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: type: integer description: | @@ -14934,9 +14944,14 @@ paths: description: | Unread count badge (appears in desktop sidebar and browser tab) - - 1 - All unreads - - 2 - Direct messages and mentions - - 3 - None + - 1 - All unread messages + - 2 - DMs, mentions, and followed topics + - 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: type: integer description: | @@ -16219,18 +16234,23 @@ paths: description: | Unread count badge (appears in desktop sidebar and browser tab) - - 1 - All unreads - - 2 - Direct messages and mentions - - 3 - None + - 1 - All unread messages + - 2 - DMs, mentions, and followed topics + - 3 - DMs and mentions + - 4 - None - **Changes**: Before Zulip 5.0 (feature level 80), this setting was managed by - the `PATCH /settings/notifications` endpoint. + **Changes**: In Zulip 8.0 (feature level 227), added `DMs, mentions, and followed + 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: type: integer enum: - 1 - 2 - 3 + - 4 example: 1 - name: realm_name_in_email_notifications_policy in: query