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")