diff --git a/api_docs/changelog.md b/api_docs/changelog.md
index 50efdf400b..2f5d63d08b 100644
--- a/api_docs/changelog.md
+++ b/api_docs/changelog.md
@@ -20,6 +20,13 @@ format used by the Zulip server that they are interacting with.
## Changes in Zulip 8.0
+**Feature level 235**
+
+* [`PATCH /realm/user_settings_defaults`](/api/update-realm-user-settings-defaults),
+ [`POST /register`](/api/register-queue), [`PATCH /settings`](/api/update-settings):
+ Added a new user setting,`automatically_follow_topics_where_mentioned`
+ that allows user to automatically follow topics where the user is mentioned.
+
**Feature level 234**
* Mobile push notifications now include a `realm_name` field.
diff --git a/version.py b/version.py
index 0e3e912d2d..d4288a70c2 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 = 234
+API_FEATURE_LEVEL = 235
# 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/realm_user_settings_defaults.ts b/web/src/realm_user_settings_defaults.ts
index ffe74a04a5..aacdd77918 100644
--- a/web/src/realm_user_settings_defaults.ts
+++ b/web/src/realm_user_settings_defaults.ts
@@ -46,6 +46,7 @@ export type RealmDefaultSettings = {
wildcard_mentions_notify: boolean;
automatically_follow_topics_policy: number;
automatically_unmute_topics_in_muted_streams_policy: number;
+ automatically_follow_topics_where_mentioned: boolean;
};
export let realm_user_settings_defaults: RealmDefaultSettings;
diff --git a/web/src/settings_config.ts b/web/src/settings_config.ts
index d32622ec33..7beb24a5ad 100644
--- a/web/src/settings_config.ts
+++ b/web/src/settings_config.ts
@@ -586,6 +586,9 @@ export const notification_settings_labels = {
automatically_unmute_topics_in_muted_streams_policy: $t({
defaultMessage: "Automatically unmute topics in muted streams",
}),
+ automatically_follow_topics_where_mentioned: $t({
+ defaultMessage: "Automatically follow topics where I'm mentioned",
+ }),
};
export const realm_user_settings_defaults_labels = {
@@ -756,6 +759,7 @@ const other_notification_settings = [
"notification_sound",
"automatically_follow_topics_policy",
"automatically_unmute_topics_in_muted_streams_policy",
+ "automatically_follow_topics_where_mentioned",
];
export const all_notification_settings = [
diff --git a/web/src/user_settings.ts b/web/src/user_settings.ts
index d311d193f5..b02f7c63c5 100644
--- a/web/src/user_settings.ts
+++ b/web/src/user_settings.ts
@@ -58,6 +58,7 @@ export type UserSettings = (StreamNotificationSettings &
send_read_receipts: boolean;
automatically_follow_topics_policy: number;
automatically_unmute_topics_in_muted_streams_policy: number;
+ automatically_follow_topics_where_mentioned: boolean;
timezone: string;
};
diff --git a/web/templates/settings/notification_settings.hbs b/web/templates/settings/notification_settings.hbs
index 79e8a27e44..ce06bb99c0 100644
--- a/web/templates/settings/notification_settings.hbs
+++ b/web/templates/settings/notification_settings.hbs
@@ -79,6 +79,12 @@
{{> dropdown_options_widget option_values=automatically_unmute_topics_in_muted_streams_policy_values}}
+
+ {{> settings_checkbox
+ setting_name="automatically_follow_topics_where_mentioned"
+ is_checked=(lookup settings_object "automatically_follow_topics_where_mentioned")
+ label=(lookup settings_label "automatically_follow_topics_where_mentioned")
+ prefix=notification_settings.prefix}}
diff --git a/web/tests/i18n.test.js b/web/tests/i18n.test.js
index eee7eb78d3..dc395a0b06 100644
--- a/web/tests/i18n.test.js
+++ b/web/tests/i18n.test.js
@@ -107,6 +107,8 @@ run_test("tr_tag", ({mock_template}) => {
automatically_follow_topics_policy: "Automatically follow topics",
automatically_unmute_topics_in_muted_streams_policy:
"Automatically unmute topics in muted streams",
+ automatically_follow_topics_where_mentioned:
+ "Automatically follow topics where I'm mentioned",
},
show_push_notifications_tooltip: false,
user_role_text: "Member",
diff --git a/zerver/actions/message_send.py b/zerver/actions/message_send.py
index a607b25d9b..f4aab56406 100644
--- a/zerver/actions/message_send.py
+++ b/zerver/actions/message_send.py
@@ -30,7 +30,10 @@ from django.utils.translation import override as override_language
from django_stubs_ext import ValuesQuerySet
from zerver.actions.uploads import do_claim_attachments
-from zerver.actions.user_topics import do_set_user_topic_visibility_policy
+from zerver.actions.user_topics import (
+ bulk_do_set_user_topic_visibility_policy,
+ do_set_user_topic_visibility_policy,
+)
from zerver.lib.addressee import Addressee
from zerver.lib.alert_words import get_alert_word_automaton
from zerver.lib.cache import cache_with_key, user_profile_delivery_email_cache_key
@@ -988,6 +991,47 @@ def do_send_messages(
)
send_request.automatic_new_visibility_policy = new_visibility_policy
+ # Set the visibility_policy of the users mentioned in the message
+ # to "FOLLOWED" if "automatically_follow_topics_where_mentioned" is "True".
+ human_user_personal_mentions = send_request.rendering_result.mentions_user_ids & (
+ send_request.active_user_ids - send_request.all_bot_user_ids
+ )
+ expect_follow_user_profiles: Set[UserProfile] = set()
+
+ if len(human_user_personal_mentions) > 0:
+ expect_follow_user_profiles = set(
+ UserProfile.objects.filter(
+ realm_id=realm_id,
+ id__in=human_user_personal_mentions,
+ automatically_follow_topics_where_mentioned=True,
+ )
+ )
+ if len(expect_follow_user_profiles) > 0:
+ user_topics_query_set = UserTopic.objects.filter(
+ user_profile__in=expect_follow_user_profiles,
+ stream_id=send_request.stream.id,
+ topic_name__iexact=send_request.message.topic_name(),
+ visibility_policy__in=[
+ # Explicitly muted takes precedence over this setting.
+ UserTopic.VisibilityPolicy.MUTED,
+ # Already followed
+ UserTopic.VisibilityPolicy.FOLLOWED,
+ ],
+ )
+ skip_follow_users = {
+ user_topic.user_profile for user_topic in user_topics_query_set
+ }
+
+ to_follow_users = list(expect_follow_user_profiles - skip_follow_users)
+
+ if to_follow_users:
+ bulk_do_set_user_topic_visibility_policy(
+ user_profiles=to_follow_users,
+ stream=send_request.stream,
+ topic=send_request.message.topic_name(),
+ visibility_policy=UserTopic.VisibilityPolicy.FOLLOWED,
+ )
+
# Deliver events to the real-time push system, as well as
# enqueuing any additional processing triggered by the message.
wide_message_dict = MessageDict.wide_dict(send_request.message, realm_id)
diff --git a/zerver/migrations/0494_realmuserdefault_automatically_follow_topics_where_mentioned_and_more.py b/zerver/migrations/0494_realmuserdefault_automatically_follow_topics_where_mentioned_and_more.py
new file mode 100644
index 0000000000..e976359e6c
--- /dev/null
+++ b/zerver/migrations/0494_realmuserdefault_automatically_follow_topics_where_mentioned_and_more.py
@@ -0,0 +1,22 @@
+# Generated by Django 4.2.7 on 2023-12-10 13:18
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+ dependencies = [
+ ("zerver", "0493_rename_userhotspot_to_onboardingstep"),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name="realmuserdefault",
+ name="automatically_follow_topics_where_mentioned",
+ field=models.BooleanField(default=True),
+ ),
+ migrations.AddField(
+ model_name="userprofile",
+ name="automatically_follow_topics_where_mentioned",
+ field=models.BooleanField(default=True),
+ ),
+ ]
diff --git a/zerver/models.py b/zerver/models.py
index 529965cba8..ad506b5153 100644
--- a/zerver/models.py
+++ b/zerver/models.py
@@ -1768,6 +1768,7 @@ class UserBaseSettings(models.Model):
automatically_unmute_topics_in_muted_streams_policy = models.PositiveSmallIntegerField(
default=AUTOMATICALLY_CHANGE_VISIBILITY_POLICY_ON_SEND,
)
+ automatically_follow_topics_where_mentioned = models.BooleanField(default=True)
# Whether or not the user wants to sync their drafts.
enable_drafts_synchronization = models.BooleanField(default=True)
@@ -1866,6 +1867,7 @@ class UserBaseSettings(models.Model):
enable_followed_topic_wildcard_mentions_notify=bool,
automatically_follow_topics_policy=int,
automatically_unmute_topics_in_muted_streams_policy=int,
+ automatically_follow_topics_where_mentioned=bool,
)
notification_setting_types = {
diff --git a/zerver/openapi/zulip.yaml b/zerver/openapi/zulip.yaml
index 7beaf8609b..c926766603 100644
--- a/zerver/openapi/zulip.yaml
+++ b/zerver/openapi/zulip.yaml
@@ -10673,6 +10673,16 @@ paths:
- 3
- 4
example: 1
+ - name: automatically_follow_topics_where_mentioned
+ in: query
+ description: |
+ Whether the server will automatically mark the user as following
+ topics where the user is mentioned.
+
+ **Changes**: New in Zulip 8.0 (feature level 235).
+ schema:
+ type: boolean
+ example: true
- name: presence_enabled
in: query
description: |
@@ -12936,6 +12946,13 @@ paths:
- 4 - Never
**Changes**: New in Zulip 8.0 (feature level 214).
+ automatically_follow_topics_where_mentioned:
+ type: boolean
+ description: |
+ Whether the server will automatically mark the user as following
+ topics where the user is mentioned.
+
+ **Changes**: New in Zulip 8.0 (feature level 235).
presence_enabled:
type: boolean
description: |
@@ -15151,6 +15168,13 @@ paths:
- 4 - Never
**Changes**: New in Zulip 8.0 (feature level 214).
+ automatically_follow_topics_where_mentioned:
+ type: boolean
+ description: |
+ Whether the server will automatically mark the user as following
+ topics where the user is mentioned.
+
+ **Changes**: New in Zulip 8.0 (feature level 235).
presence_enabled:
type: boolean
description: |
@@ -16492,6 +16516,16 @@ paths:
- 3
- 4
example: 1
+ - name: automatically_follow_topics_where_mentioned
+ in: query
+ description: |
+ Whether the server will automatically mark the user as following
+ topics where the user is mentioned.
+
+ **Changes**: New in Zulip 8.0 (feature level 235).
+ schema:
+ type: boolean
+ example: true
- name: presence_enabled
in: query
description: |
diff --git a/zerver/tests/test_events.py b/zerver/tests/test_events.py
index 02e8bac37c..231ca97a26 100644
--- a/zerver/tests/test_events.py
+++ b/zerver/tests/test_events.py
@@ -476,6 +476,36 @@ class NormalActionsTest(BaseAction):
partial(self.send_stream_message, self.example_user("cordelia"), "Verona", content),
)
+ def test_automatically_follow_topic_where_mentioned(self) -> None:
+ user = self.example_user("hamlet")
+
+ do_change_user_setting(
+ user_profile=user,
+ setting_name="automatically_follow_topics_where_mentioned",
+ setting_value=True,
+ acting_user=None,
+ )
+
+ def get_num_events() -> int: # nocoverage
+ try:
+ user_topic = UserTopic.objects.get(
+ user_profile=user,
+ stream_id=get_stream("Verona", user.realm).id,
+ topic_name__iexact="test",
+ )
+ if user_topic.visibility_policy != UserTopic.VisibilityPolicy.FOLLOWED:
+ return 3
+ except UserTopic.DoesNotExist:
+ return 3
+ return 1
+
+ for i in range(3):
+ content = "mentioning... @**" + user.full_name + "** hello " + str(i)
+ self.verify_action(
+ partial(self.send_stream_message, self.example_user("cordelia"), "Verona", content),
+ num_events=get_num_events(),
+ )
+
def test_topic_wildcard_mentioned_send_message_events(self) -> None:
for i in range(3):
content = "mentioning... @**topic** hello " + str(i)
diff --git a/zerver/tests/test_message_send.py b/zerver/tests/test_message_send.py
index 7b7d5babbf..8992a087d8 100644
--- a/zerver/tests/test_message_send.py
+++ b/zerver/tests/test_message_send.py
@@ -1560,6 +1560,7 @@ class StreamMessagesTest(ZulipTestCase):
self.subscribe(user_profile, "Denmark")
sender = self.example_user("hamlet")
+ user = self.example_user("othello")
sending_client = make_client(name="test suite")
stream_name = "Denmark"
topic_name = "foo"
@@ -1643,6 +1644,54 @@ class StreamMessagesTest(ZulipTestCase):
body=content,
)
+ realm = get_realm("zulip")
+ subscribers = self.users_subscribed_to_stream(stream_name, realm)
+
+ for user in subscribers:
+ do_change_user_setting(
+ user_profile=user,
+ setting_name="automatically_follow_topics_where_mentioned",
+ setting_value=True,
+ acting_user=None,
+ )
+ # There will be an increase in the query count of 5 while sending
+ # a message with a mention to a topic if visibility policy for the
+ # mentioned user is other than FOLLOWED.
+ # 1 to get the user_id of the mentioned user + 1 to check if the topic
+ # is already followed + 3 queries to follow the topic.
+ flush_per_request_caches()
+ with self.assert_database_query_count(22):
+ check_send_stream_message(
+ sender=sender,
+ client=sending_client,
+ stream_name=stream_name,
+ topic="topic 2",
+ body="@**" + user.full_name + "**",
+ )
+ # If the topic is already FOLLOWED, there will be an increase in the query
+ # count of 2.
+ # 1 to get the user_id of the mentioned user + 1 to check if the topic is
+ # already followed.
+ flush_per_request_caches()
+ with self.assert_database_query_count(19):
+ check_send_stream_message(
+ sender=sender,
+ client=sending_client,
+ stream_name=stream_name,
+ topic="topic 2",
+ body="@**" + user.full_name + "**",
+ )
+
+ flush_per_request_caches()
+ with self.assert_database_query_count(16):
+ check_send_stream_message(
+ sender=sender,
+ client=sending_client,
+ stream_name=stream_name,
+ topic="topic 2",
+ body="@**all**",
+ )
+
def test_stream_message_dict(self) -> None:
user_profile = self.example_user("iago")
self.subscribe(user_profile, "Denmark")
diff --git a/zerver/tests/test_realm.py b/zerver/tests/test_realm.py
index e4f3aa77bd..bdd6c59dca 100644
--- a/zerver/tests/test_realm.py
+++ b/zerver/tests/test_realm.py
@@ -1437,6 +1437,7 @@ class RealmAPITest(ZulipTestCase):
realm_name_in_email_notifications_policy=UserProfile.REALM_NAME_IN_EMAIL_NOTIFICATIONS_POLICY_CHOICES,
automatically_follow_topics_policy=UserProfile.AUTOMATICALLY_CHANGE_VISIBILITY_POLICY_CHOICES,
automatically_unmute_topics_in_muted_streams_policy=UserProfile.AUTOMATICALLY_CHANGE_VISIBILITY_POLICY_CHOICES,
+ automatically_follow_topics_where_mentioned=[True, False],
)
vals = test_values.get(name)
diff --git a/zerver/tests/test_user_topics.py b/zerver/tests/test_user_topics.py
index f0f3a09760..b3f4af476b 100644
--- a/zerver/tests/test_user_topics.py
+++ b/zerver/tests/test_user_topics.py
@@ -763,6 +763,45 @@ class AutomaticallyFollowTopicsTests(ZulipTestCase):
)
self.assertEqual(user_ids, {hamlet.id})
+ def test_automatically_follow_topic_on_mention(self) -> None:
+ hamlet = self.example_user("hamlet")
+ aaron = self.example_user("aaron")
+ stream = get_stream("Verona", hamlet.realm)
+ topic_name = "teST topic"
+
+ do_change_user_setting(
+ hamlet,
+ "automatically_follow_topics_where_mentioned",
+ True,
+ acting_user=None,
+ )
+
+ content = "silently mentioning... @_**" + hamlet.full_name + "**"
+ self.send_stream_message(aaron, stream.name, content, topic_name)
+
+ stream_topic_target = StreamTopicTarget(
+ stream_id=stream.id,
+ topic_name=topic_name,
+ )
+ user_ids = stream_topic_target.user_ids_with_visibility_policy(
+ UserTopic.VisibilityPolicy.FOLLOWED
+ )
+ self.assertEqual(user_ids, set())
+
+ content = "quoting... \n```quote\n@**" + hamlet.full_name + "**\n```"
+ self.send_stream_message(aaron, stream.name, content, topic_name)
+ user_ids = stream_topic_target.user_ids_with_visibility_policy(
+ UserTopic.VisibilityPolicy.FOLLOWED
+ )
+ self.assertEqual(user_ids, set())
+
+ content = "mentioning... @**" + hamlet.full_name + "**"
+ self.send_stream_message(aaron, stream.name, content, topic_name)
+ user_ids = stream_topic_target.user_ids_with_visibility_policy(
+ UserTopic.VisibilityPolicy.FOLLOWED
+ )
+ self.assertEqual(user_ids, {hamlet.id})
+
def test_automatically_follow_topic_on_participation_send_message(self) -> None:
hamlet = self.example_user("hamlet")
aaron = self.example_user("aaron")
diff --git a/zerver/views/realm.py b/zerver/views/realm.py
index 2b140caff7..fce8ddbdd8 100644
--- a/zerver/views/realm.py
+++ b/zerver/views/realm.py
@@ -571,6 +571,9 @@ def update_realm_user_settings_defaults(
json_validator=check_int_in(UserProfile.AUTOMATICALLY_CHANGE_VISIBILITY_POLICY_CHOICES),
default=None,
),
+ automatically_follow_topics_where_mentioned: Optional[bool] = REQ(
+ json_validator=check_bool, default=None
+ ),
presence_enabled: Optional[bool] = REQ(json_validator=check_bool, default=None),
enter_sends: Optional[bool] = REQ(json_validator=check_bool, default=None),
enable_drafts_synchronization: Optional[bool] = REQ(json_validator=check_bool, default=None),
diff --git a/zerver/views/user_settings.py b/zerver/views/user_settings.py
index 861b5eae0a..c073f35cbc 100644
--- a/zerver/views/user_settings.py
+++ b/zerver/views/user_settings.py
@@ -300,6 +300,9 @@ def json_change_settings(
json_validator=check_int_in(UserProfile.AUTOMATICALLY_CHANGE_VISIBILITY_POLICY_CHOICES),
default=None,
),
+ automatically_follow_topics_where_mentioned: Optional[bool] = REQ(
+ json_validator=check_bool, default=None
+ ),
presence_enabled: Optional[bool] = REQ(json_validator=check_bool, default=None),
enter_sends: Optional[bool] = REQ(json_validator=check_bool, default=None),
send_private_typing_notifications: Optional[bool] = REQ(
diff --git a/zilencer/management/commands/populate_db.py b/zilencer/management/commands/populate_db.py
index 58383dcb7c..0c31bd7a98 100644
--- a/zilencer/management/commands/populate_db.py
+++ b/zilencer/management/commands/populate_db.py
@@ -860,6 +860,9 @@ class Command(BaseCommand):
#
# We have separate tests to verify events generated, database query counts,
# and other important details related to the above-mentioned settings.
+ #
+ # We set the value of 'automatically_follow_topics_where_mentioned' to 'False' so that it
+ # does not increase the number of events and db queries while running tests.
for user in user_profiles:
do_change_user_setting(
user,
@@ -873,6 +876,12 @@ class Command(BaseCommand):
UserProfile.AUTOMATICALLY_CHANGE_VISIBILITY_POLICY_NEVER,
acting_user=None,
)
+ do_change_user_setting(
+ user,
+ "automatically_follow_topics_where_mentioned",
+ False,
+ acting_user=None,
+ )
# Create a test realm emoji.
IMAGE_FILE_PATH = static_path("images/test-images/checkbox.png")