settings: Add "Automatically follow topics where I'm mentioned" setting.

Fixes: #26795
This commit is contained in:
Vector73 2023-12-10 19:23:52 +05:30 committed by Tim Abbott
parent c7c0b871c5
commit 2e71ec78e3
18 changed files with 259 additions and 2 deletions

View File

@ -20,6 +20,13 @@ format used by the Zulip server that they are interacting with.
## Changes in Zulip 8.0 ## 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** **Feature level 234**
* Mobile push notifications now include a `realm_name` field. * Mobile push notifications now include a `realm_name` field.

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 = 234 API_FEATURE_LEVEL = 235
# 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

@ -46,6 +46,7 @@ export type RealmDefaultSettings = {
wildcard_mentions_notify: boolean; wildcard_mentions_notify: boolean;
automatically_follow_topics_policy: number; automatically_follow_topics_policy: number;
automatically_unmute_topics_in_muted_streams_policy: number; automatically_unmute_topics_in_muted_streams_policy: number;
automatically_follow_topics_where_mentioned: boolean;
}; };
export let realm_user_settings_defaults: RealmDefaultSettings; export let realm_user_settings_defaults: RealmDefaultSettings;

View File

@ -586,6 +586,9 @@ export const notification_settings_labels = {
automatically_unmute_topics_in_muted_streams_policy: $t({ automatically_unmute_topics_in_muted_streams_policy: $t({
defaultMessage: "Automatically unmute topics in muted streams", 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 = { export const realm_user_settings_defaults_labels = {
@ -756,6 +759,7 @@ const other_notification_settings = [
"notification_sound", "notification_sound",
"automatically_follow_topics_policy", "automatically_follow_topics_policy",
"automatically_unmute_topics_in_muted_streams_policy", "automatically_unmute_topics_in_muted_streams_policy",
"automatically_follow_topics_where_mentioned",
]; ];
export const all_notification_settings = [ export const all_notification_settings = [

View File

@ -58,6 +58,7 @@ export type UserSettings = (StreamNotificationSettings &
send_read_receipts: boolean; send_read_receipts: boolean;
automatically_follow_topics_policy: number; automatically_follow_topics_policy: number;
automatically_unmute_topics_in_muted_streams_policy: number; automatically_unmute_topics_in_muted_streams_policy: number;
automatically_follow_topics_where_mentioned: boolean;
timezone: string; timezone: string;
}; };

View File

@ -79,6 +79,12 @@
{{> dropdown_options_widget option_values=automatically_unmute_topics_in_muted_streams_policy_values}} {{> dropdown_options_widget option_values=automatically_unmute_topics_in_muted_streams_policy_values}}
</select> </select>
</div> </div>
{{> 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}}
</div> </div>
<div class="desktop_notifications m-10 {{#if for_realm_settings}}settings-subsection-parent{{else}}subsection-parent{{/if}}"> <div class="desktop_notifications m-10 {{#if for_realm_settings}}settings-subsection-parent{{else}}subsection-parent{{/if}}">

View File

@ -107,6 +107,8 @@ run_test("tr_tag", ({mock_template}) => {
automatically_follow_topics_policy: "Automatically follow topics", automatically_follow_topics_policy: "Automatically follow topics",
automatically_unmute_topics_in_muted_streams_policy: automatically_unmute_topics_in_muted_streams_policy:
"Automatically unmute topics in muted streams", "Automatically unmute topics in muted streams",
automatically_follow_topics_where_mentioned:
"Automatically follow topics where I'm mentioned",
}, },
show_push_notifications_tooltip: false, show_push_notifications_tooltip: false,
user_role_text: "Member", user_role_text: "Member",

View File

@ -30,7 +30,10 @@ from django.utils.translation import override as override_language
from django_stubs_ext import ValuesQuerySet from django_stubs_ext import ValuesQuerySet
from zerver.actions.uploads import do_claim_attachments 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.addressee import Addressee
from zerver.lib.alert_words import get_alert_word_automaton from zerver.lib.alert_words import get_alert_word_automaton
from zerver.lib.cache import cache_with_key, user_profile_delivery_email_cache_key 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 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 # Deliver events to the real-time push system, as well as
# enqueuing any additional processing triggered by the message. # enqueuing any additional processing triggered by the message.
wide_message_dict = MessageDict.wide_dict(send_request.message, realm_id) wide_message_dict = MessageDict.wide_dict(send_request.message, realm_id)

View File

@ -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),
),
]

View File

@ -1768,6 +1768,7 @@ class UserBaseSettings(models.Model):
automatically_unmute_topics_in_muted_streams_policy = models.PositiveSmallIntegerField( automatically_unmute_topics_in_muted_streams_policy = models.PositiveSmallIntegerField(
default=AUTOMATICALLY_CHANGE_VISIBILITY_POLICY_ON_SEND, 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. # Whether or not the user wants to sync their drafts.
enable_drafts_synchronization = models.BooleanField(default=True) enable_drafts_synchronization = models.BooleanField(default=True)
@ -1866,6 +1867,7 @@ class UserBaseSettings(models.Model):
enable_followed_topic_wildcard_mentions_notify=bool, enable_followed_topic_wildcard_mentions_notify=bool,
automatically_follow_topics_policy=int, automatically_follow_topics_policy=int,
automatically_unmute_topics_in_muted_streams_policy=int, automatically_unmute_topics_in_muted_streams_policy=int,
automatically_follow_topics_where_mentioned=bool,
) )
notification_setting_types = { notification_setting_types = {

View File

@ -10673,6 +10673,16 @@ paths:
- 3 - 3
- 4 - 4
example: 1 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 - name: presence_enabled
in: query in: query
description: | description: |
@ -12936,6 +12946,13 @@ paths:
- 4 - Never - 4 - Never
**Changes**: New in Zulip 8.0 (feature level 214). **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: presence_enabled:
type: boolean type: boolean
description: | description: |
@ -15151,6 +15168,13 @@ paths:
- 4 - Never - 4 - Never
**Changes**: New in Zulip 8.0 (feature level 214). **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: presence_enabled:
type: boolean type: boolean
description: | description: |
@ -16492,6 +16516,16 @@ paths:
- 3 - 3
- 4 - 4
example: 1 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 - name: presence_enabled
in: query in: query
description: | description: |

View File

@ -476,6 +476,36 @@ class NormalActionsTest(BaseAction):
partial(self.send_stream_message, self.example_user("cordelia"), "Verona", content), 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: def test_topic_wildcard_mentioned_send_message_events(self) -> None:
for i in range(3): for i in range(3):
content = "mentioning... @**topic** hello " + str(i) content = "mentioning... @**topic** hello " + str(i)

View File

@ -1560,6 +1560,7 @@ class StreamMessagesTest(ZulipTestCase):
self.subscribe(user_profile, "Denmark") self.subscribe(user_profile, "Denmark")
sender = self.example_user("hamlet") sender = self.example_user("hamlet")
user = self.example_user("othello")
sending_client = make_client(name="test suite") sending_client = make_client(name="test suite")
stream_name = "Denmark" stream_name = "Denmark"
topic_name = "foo" topic_name = "foo"
@ -1643,6 +1644,54 @@ class StreamMessagesTest(ZulipTestCase):
body=content, 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: def test_stream_message_dict(self) -> None:
user_profile = self.example_user("iago") user_profile = self.example_user("iago")
self.subscribe(user_profile, "Denmark") self.subscribe(user_profile, "Denmark")

View File

@ -1437,6 +1437,7 @@ class RealmAPITest(ZulipTestCase):
realm_name_in_email_notifications_policy=UserProfile.REALM_NAME_IN_EMAIL_NOTIFICATIONS_POLICY_CHOICES, 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_follow_topics_policy=UserProfile.AUTOMATICALLY_CHANGE_VISIBILITY_POLICY_CHOICES,
automatically_unmute_topics_in_muted_streams_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) vals = test_values.get(name)

View File

@ -763,6 +763,45 @@ class AutomaticallyFollowTopicsTests(ZulipTestCase):
) )
self.assertEqual(user_ids, {hamlet.id}) 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: def test_automatically_follow_topic_on_participation_send_message(self) -> None:
hamlet = self.example_user("hamlet") hamlet = self.example_user("hamlet")
aaron = self.example_user("aaron") aaron = self.example_user("aaron")

View File

@ -571,6 +571,9 @@ def update_realm_user_settings_defaults(
json_validator=check_int_in(UserProfile.AUTOMATICALLY_CHANGE_VISIBILITY_POLICY_CHOICES), json_validator=check_int_in(UserProfile.AUTOMATICALLY_CHANGE_VISIBILITY_POLICY_CHOICES),
default=None, 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), presence_enabled: Optional[bool] = REQ(json_validator=check_bool, default=None),
enter_sends: 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), enable_drafts_synchronization: Optional[bool] = REQ(json_validator=check_bool, default=None),

View File

@ -300,6 +300,9 @@ def json_change_settings(
json_validator=check_int_in(UserProfile.AUTOMATICALLY_CHANGE_VISIBILITY_POLICY_CHOICES), json_validator=check_int_in(UserProfile.AUTOMATICALLY_CHANGE_VISIBILITY_POLICY_CHOICES),
default=None, 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), presence_enabled: Optional[bool] = REQ(json_validator=check_bool, default=None),
enter_sends: 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( send_private_typing_notifications: Optional[bool] = REQ(

View File

@ -860,6 +860,9 @@ class Command(BaseCommand):
# #
# We have separate tests to verify events generated, database query counts, # We have separate tests to verify events generated, database query counts,
# and other important details related to the above-mentioned settings. # 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: for user in user_profiles:
do_change_user_setting( do_change_user_setting(
user, user,
@ -873,6 +876,12 @@ class Command(BaseCommand):
UserProfile.AUTOMATICALLY_CHANGE_VISIBILITY_POLICY_NEVER, UserProfile.AUTOMATICALLY_CHANGE_VISIBILITY_POLICY_NEVER,
acting_user=None, acting_user=None,
) )
do_change_user_setting(
user,
"automatically_follow_topics_where_mentioned",
False,
acting_user=None,
)
# Create a test realm emoji. # Create a test realm emoji.
IMAGE_FILE_PATH = static_path("images/test-images/checkbox.png") IMAGE_FILE_PATH = static_path("images/test-images/checkbox.png")