diff --git a/api_docs/changelog.md b/api_docs/changelog.md index 10b9919ef7..20b9a70b51 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 189** + +* [`PATCH /realm/user_settings_defaults`](/api/update-realm-user-settings-defaults), + [`POST /register`](/api/register-queue), [`PATCH /settings`](/api/update-settings): + Added `enable_followed_topic_email_notifications` field to control + email notifications for messages sent to followed topics. + **Feature level 188** * [`POST /users/me/muted_users/{muted_user_id}`](/api/mute-user), diff --git a/templates/zerver/emails/missed_message.html b/templates/zerver/emails/missed_message.html index 53dbfe45fb..37adba30e1 100644 --- a/templates/zerver/emails/missed_message.html +++ b/templates/zerver/emails/missed_message.html @@ -33,6 +33,8 @@ {% trans %}You are receiving this because everyone was mentioned in #{{ stream_name }}.{% endtrans %}
{% elif stream_email_notify %} {% trans %}You are receiving this because you have email notifications enabled for #{{ stream_name }}.{% endtrans %}
+ {% elif followed_topic_email_notify %} + {% trans %}You are receiving this because you have email notifications enabled for topics you follow.{% endtrans %}
{% endif %} {% if reply_to_zulip %} {% trans notif_url=realm_uri + "/#settings/notifications" %}Reply to this email directly, view it in {{ realm_name }} Zulip, or manage email preferences.{% endtrans %} diff --git a/templates/zerver/emails/missed_message.subject.txt b/templates/zerver/emails/missed_message.subject.txt index ac2326559c..6ad8e8c3db 100644 --- a/templates/zerver/emails/missed_message.subject.txt +++ b/templates/zerver/emails/missed_message.subject.txt @@ -1,7 +1,7 @@ {% if show_message_content %} {% if group_pm %} {% trans %}Group DMs with {{ huddle_display_name }}{% endtrans %} {% elif private_message %} {% trans %}DMs with {{ sender_str }}{% endtrans %} - {% elif stream_email_notify or mention %} + {% elif stream_email_notify or mention or followed_topic_email_notify %} {# Some email clients, like Gmail Web (as of 2022), will auto-thread emails that share a subject and recipients, but will disregard diff --git a/templates/zerver/emails/missed_message.txt b/templates/zerver/emails/missed_message.txt index 56bc427c5c..b0319647e9 100644 --- a/templates/zerver/emails/missed_message.txt +++ b/templates/zerver/emails/missed_message.txt @@ -29,6 +29,8 @@ See {{ alert_notif_url }} for more details. {% trans %}You are receiving this because everyone was mentioned in #{{ stream_name }}.{% endtrans %} {% elif stream_email_notify %} {% trans %}You are receiving this because you have email notifications enabled for #{{ stream_name }}.{% endtrans %} +{% elif followed_topic_email_notify %} +{% trans %}You are receiving this because you have email notifications enabled for topics you follow.{% endtrans %} {% endif %} {% if reply_to_zulip %} diff --git a/zerver/actions/message_edit.py b/zerver/actions/message_edit.py index 1a7d19eb4a..fc74427a25 100644 --- a/zerver/actions/message_edit.py +++ b/zerver/actions/message_edit.py @@ -473,6 +473,7 @@ def do_update_message( event["pm_mention_email_disabled_user_ids"] = list(info.pm_mention_email_disabled_user_ids) event["stream_push_user_ids"] = list(info.stream_push_user_ids) event["stream_email_user_ids"] = list(info.stream_email_user_ids) + event["followed_topic_email_user_ids"] = list(info.followed_topic_email_user_ids) event["muted_sender_user_ids"] = list(info.muted_sender_user_ids) event["prior_mention_user_ids"] = list(prior_mention_user_ids) event["presence_idle_user_ids"] = filter_presence_idle_user_ids(info.active_user_ids) diff --git a/zerver/actions/message_send.py b/zerver/actions/message_send.py index a7be30d2d8..01fbcd3b0a 100644 --- a/zerver/actions/message_send.py +++ b/zerver/actions/message_send.py @@ -166,6 +166,7 @@ class RecipientInfoResult: stream_email_user_ids: Set[int] stream_push_user_ids: Set[int] wildcard_mention_user_ids: Set[int] + followed_topic_email_user_ids: Set[int] muted_sender_user_ids: Set[int] um_eligible_user_ids: Set[int] long_term_idle_user_ids: Set[int] @@ -196,6 +197,7 @@ def get_recipient_info( stream_push_user_ids: Set[int] = set() stream_email_user_ids: Set[int] = set() wildcard_mention_user_ids: Set[int] = set() + followed_topic_email_user_ids: Set[int] = set() muted_sender_user_ids: Set[int] = get_muting_users(sender_id) if recipient.type == Recipient.PERSONAL: @@ -214,6 +216,7 @@ def get_recipient_info( get_subscriptions_for_send_message( realm_id=realm_id, stream_id=stream_topic.stream_id, + topic_name=stream_topic.topic_name, possible_wildcard_mention=possible_wildcard_mention, possibly_mentioned_user_ids=possibly_mentioned_user_ids, ) @@ -223,12 +226,16 @@ def get_recipient_info( ), user_profile_push_notifications=F("user_profile__enable_stream_push_notifications"), user_profile_wildcard_mentions_notify=F("user_profile__wildcard_mentions_notify"), + followed_topic_email_notifications=F( + "user_profile__enable_followed_topic_email_notifications" + ), ) .values( "user_profile_id", "push_notifications", "email_notifications", "wildcard_mentions_notify", + "followed_topic_email_notifications", "user_profile_email_notifications", "user_profile_push_notifications", "user_profile_wildcard_mentions_notify", @@ -257,6 +264,21 @@ def get_recipient_info( stream_push_user_ids = notification_recipients("push_notifications") stream_email_user_ids = notification_recipients("email_notifications") + def followed_topic_notification_recipients(setting: str) -> Set[int]: + return { + row["user_profile_id"] + for row in subscription_rows + if user_id_to_visibility_policy.get( + row["user_profile_id"], UserTopic.VisibilityPolicy.INHERIT + ) + == UserTopic.VisibilityPolicy.FOLLOWED + and row["followed_topic_" + setting] + } + + followed_topic_email_user_ids = followed_topic_notification_recipients( + "email_notifications" + ) + if possible_wildcard_mention: # We calculate `wildcard_mention_user_ids` only if there's a possible # wildcard mention in the message. This is important so as to avoid @@ -381,6 +403,7 @@ def get_recipient_info( stream_push_user_ids=stream_push_user_ids, stream_email_user_ids=stream_email_user_ids, wildcard_mention_user_ids=wildcard_mention_user_ids, + followed_topic_email_user_ids=followed_topic_email_user_ids, muted_sender_user_ids=muted_sender_user_ids, um_eligible_user_ids=um_eligible_user_ids, long_term_idle_user_ids=long_term_idle_user_ids, @@ -564,6 +587,7 @@ def build_message_send_dict( pm_mention_push_disabled_user_ids=info.pm_mention_push_disabled_user_ids, stream_push_user_ids=info.stream_push_user_ids, stream_email_user_ids=info.stream_email_user_ids, + followed_topic_email_user_ids=info.followed_topic_email_user_ids, muted_sender_user_ids=info.muted_sender_user_ids, um_eligible_user_ids=info.um_eligible_user_ids, long_term_idle_user_ids=info.long_term_idle_user_ids, @@ -588,6 +612,7 @@ def create_user_messages( stream_push_user_ids: AbstractSet[int], stream_email_user_ids: AbstractSet[int], mentioned_user_ids: AbstractSet[int], + followed_topic_email_user_ids: AbstractSet[int], mark_as_read_user_ids: Set[int], limit_unread_user_ids: Optional[Set[int]], scheduled_message_to_self: bool, @@ -648,6 +673,7 @@ def create_user_messages( user_profile_id in long_term_idle_user_ids and user_profile_id not in stream_push_user_ids and user_profile_id not in stream_email_user_ids + and user_profile_id not in followed_topic_email_user_ids and is_stream_message and int(flags) == 0 ): @@ -761,6 +787,7 @@ def do_send_messages( stream_push_user_ids=send_request.stream_push_user_ids, stream_email_user_ids=send_request.stream_email_user_ids, mentioned_user_ids=mentioned_user_ids, + followed_topic_email_user_ids=send_request.followed_topic_email_user_ids, mark_as_read_user_ids=mark_as_read_user_ids, limit_unread_user_ids=send_request.limit_unread_user_ids, scheduled_message_to_self=scheduled_message_to_self, @@ -861,6 +888,7 @@ def do_send_messages( stream_push_user_ids=send_request.stream_push_user_ids, stream_email_user_ids=send_request.stream_email_user_ids, wildcard_mention_user_ids=send_request.wildcard_mention_user_ids, + followed_topic_email_user_ids=send_request.followed_topic_email_user_ids, muted_sender_user_ids=send_request.muted_sender_user_ids, all_bot_user_ids=send_request.all_bot_user_ids, ) @@ -886,6 +914,7 @@ def do_send_messages( stream_push_user_ids=list(send_request.stream_push_user_ids), stream_email_user_ids=list(send_request.stream_email_user_ids), wildcard_mention_user_ids=list(send_request.wildcard_mention_user_ids), + followed_topic_email_user_ids=list(send_request.followed_topic_email_user_ids), muted_sender_user_ids=list(send_request.muted_sender_user_ids), all_bot_user_ids=list(send_request.all_bot_user_ids), disable_external_notifications=send_request.disable_external_notifications, diff --git a/zerver/lib/email_notifications.py b/zerver/lib/email_notifications.py index b6d186912f..4fb9e55f37 100644 --- a/zerver/lib/email_notifications.py +++ b/zerver/lib/email_notifications.py @@ -457,6 +457,7 @@ def do_send_missedmessage_events_reply_in_zulip( personal_mentioned=personal_mentioned, wildcard_mentioned="wildcard_mentioned" in unique_triggers, stream_email_notify="stream_email_notify" in unique_triggers, + followed_topic_email_notify="followed_topic_email_notify" in unique_triggers, mention_count=triggers.count("mentioned") + triggers.count("wildcard_mentioned"), mentioned_user_group_name=mentioned_user_group_name, ) @@ -508,7 +509,11 @@ def do_send_missedmessage_events_reply_in_zulip( context.update(huddle_display_name=huddle_display_name) elif missed_messages[0]["message"].recipient.type == Recipient.PERSONAL: context.update(private_message=True) - elif context["mention"] or context["stream_email_notify"]: + elif ( + context["mention"] + or context["stream_email_notify"] + or context["followed_topic_email_notify"] + ): # Keep only the senders who actually mentioned the user if context["mention"]: senders = list( diff --git a/zerver/lib/message.py b/zerver/lib/message.py index f7bc7b521a..e53d1c8303 100644 --- a/zerver/lib/message.py +++ b/zerver/lib/message.py @@ -156,6 +156,8 @@ class SendMessageRequest: pm_mention_email_disabled_user_ids: Set[int] stream_push_user_ids: Set[int] stream_email_user_ids: Set[int] + # IDs of users who have followed the topic the message is being sent to, and have the followed topic email notifications setting ON. + followed_topic_email_user_ids: Set[int] muted_sender_user_ids: Set[int] um_eligible_user_ids: Set[int] long_term_idle_user_ids: Set[int] diff --git a/zerver/lib/notification_data.py b/zerver/lib/notification_data.py index 3946cbf939..901d2e0447 100644 --- a/zerver/lib/notification_data.py +++ b/zerver/lib/notification_data.py @@ -19,15 +19,20 @@ class UserMessageNotificationsData: wildcard_mention_push_notify: bool stream_push_notify: bool stream_email_notify: bool + followed_topic_email_notify: bool sender_is_muted: bool disable_external_notifications: bool def __post_init__(self) -> None: # Check that there's no dubious data. if self.pm_email_notify or self.pm_push_notify: - assert not (self.stream_email_notify or self.stream_push_notify) + assert not ( + self.stream_email_notify + or self.stream_push_notify + or self.followed_topic_email_notify + ) - if self.stream_email_notify or self.stream_push_notify: + if self.stream_email_notify or self.stream_push_notify or self.followed_topic_email_notify: assert not (self.pm_email_notify or self.pm_push_notify) @classmethod @@ -44,6 +49,7 @@ class UserMessageNotificationsData: stream_push_user_ids: Set[int], stream_email_user_ids: Set[int], wildcard_mention_user_ids: Set[int], + followed_topic_email_user_ids: Set[int], muted_sender_user_ids: Set[int], all_bot_user_ids: Set[int], ) -> "UserMessageNotificationsData": @@ -60,6 +66,7 @@ class UserMessageNotificationsData: online_push_enabled=False, stream_push_notify=False, stream_email_notify=False, + followed_topic_email_notify=False, sender_is_muted=False, disable_external_notifications=False, ) @@ -97,6 +104,7 @@ class UserMessageNotificationsData: online_push_enabled=(user_id in online_push_user_ids), stream_push_notify=(user_id in stream_push_user_ids), stream_email_notify=(user_id in stream_email_user_ids), + followed_topic_email_notify=(user_id in followed_topic_email_user_ids), sender_is_muted=(user_id in muted_sender_user_ids), disable_external_notifications=disable_external_notifications, ) @@ -168,6 +176,8 @@ class UserMessageNotificationsData: return NotificationTriggers.MENTION elif self.wildcard_mention_email_notify: return NotificationTriggers.WILDCARD_MENTION + elif self.followed_topic_email_notify: + return NotificationTriggers.FOLLOWED_TOPIC_EMAIL elif self.stream_email_notify: return NotificationTriggers.STREAM_EMAIL else: diff --git a/zerver/lib/stream_subscription.py b/zerver/lib/stream_subscription.py index a54dbd406d..86a725687a 100644 --- a/zerver/lib/stream_subscription.py +++ b/zerver/lib/stream_subscription.py @@ -7,7 +7,7 @@ from typing import AbstractSet, Any, Collection, Dict, List, Optional, Set from django.db.models import Q, QuerySet from django_stubs_ext import ValuesQuerySet -from zerver.models import AlertWord, Realm, Recipient, Stream, Subscription, UserProfile +from zerver.models import AlertWord, Realm, Recipient, Stream, Subscription, UserProfile, UserTopic @dataclass @@ -305,6 +305,7 @@ def get_subscriptions_for_send_message( *, realm_id: int, stream_id: int, + topic_name: str, possible_wildcard_mention: bool, possibly_mentioned_user_ids: AbstractSet[int], ) -> QuerySet[Subscription]: @@ -356,5 +357,12 @@ def get_subscriptions_for_send_message( "user_profile_id" ) ) + | Q( + user_profile_id__in=UserTopic.objects.filter( + stream_id=stream_id, + topic_name__iexact=topic_name, + visibility_policy=UserTopic.VisibilityPolicy.FOLLOWED, + ).values_list("user_profile_id") + ) ) return query diff --git a/zerver/lib/test_classes.py b/zerver/lib/test_classes.py index a68ea5f9d8..e43b8a21c9 100644 --- a/zerver/lib/test_classes.py +++ b/zerver/lib/test_classes.py @@ -1729,6 +1729,7 @@ Output: wildcard_mention_push_notify=kwargs.get("wildcard_mention_push_notify", False), stream_email_notify=kwargs.get("stream_email_notify", False), stream_push_notify=kwargs.get("stream_push_notify", False), + followed_topic_email_notify=kwargs.get("followed_topic_email_notify", False), sender_is_muted=kwargs.get("sender_is_muted", False), disable_external_notifications=kwargs.get("disable_external_notifications", False), ) diff --git a/zerver/migrations/0331_scheduledmessagenotificationemail.py b/zerver/migrations/0331_scheduledmessagenotificationemail.py index d75fe9703c..1051f54d75 100644 --- a/zerver/migrations/0331_scheduledmessagenotificationemail.py +++ b/zerver/migrations/0331_scheduledmessagenotificationemail.py @@ -28,6 +28,7 @@ class Migration(migrations.Migration): ("mentioned", "Mention"), ("wildcard_mentioned", "Wildcard mention"), ("stream_email_notify", "Stream notifications enabled"), + ("followed_topic_email_notify", "Followed topic notifications enabled"), ] ), ), diff --git a/zerver/migrations/0453_followed_topic_notifications.py b/zerver/migrations/0453_followed_topic_notifications.py new file mode 100644 index 0000000000..38fe3ea731 --- /dev/null +++ b/zerver/migrations/0453_followed_topic_notifications.py @@ -0,0 +1,62 @@ +# Generated by Django 4.2.1 on 2023-05-24 11:09 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("zerver", "0452_realmauditlog_extra_data_json"), + ] + + operations = [ + migrations.AddField( + model_name="realmuserdefault", + name="enable_followed_topic_audible_notifications", + field=models.BooleanField(default=True), + ), + migrations.AddField( + model_name="realmuserdefault", + name="enable_followed_topic_desktop_notifications", + field=models.BooleanField(default=True), + ), + migrations.AddField( + model_name="realmuserdefault", + name="enable_followed_topic_email_notifications", + field=models.BooleanField(default=True), + ), + migrations.AddField( + model_name="realmuserdefault", + name="enable_followed_topic_push_notifications", + field=models.BooleanField(default=True), + ), + migrations.AddField( + model_name="realmuserdefault", + name="enable_followed_topic_wildcard_mentions_notify", + field=models.BooleanField(default=True), + ), + migrations.AddField( + model_name="userprofile", + name="enable_followed_topic_audible_notifications", + field=models.BooleanField(default=True), + ), + migrations.AddField( + model_name="userprofile", + name="enable_followed_topic_desktop_notifications", + field=models.BooleanField(default=True), + ), + migrations.AddField( + model_name="userprofile", + name="enable_followed_topic_email_notifications", + field=models.BooleanField(default=True), + ), + migrations.AddField( + model_name="userprofile", + name="enable_followed_topic_push_notifications", + field=models.BooleanField(default=True), + ), + migrations.AddField( + model_name="userprofile", + name="enable_followed_topic_wildcard_mentions_notify", + field=models.BooleanField(default=True), + ), + ] diff --git a/zerver/models.py b/zerver/models.py index aaefe73326..12e6a1aa8e 100644 --- a/zerver/models.py +++ b/zerver/models.py @@ -1589,6 +1589,13 @@ class UserBaseSettings(models.Model): notification_sound = models.CharField(max_length=20, default="zulip") wildcard_mentions_notify = models.BooleanField(default=True) + # Followed Topics notifications. + enable_followed_topic_desktop_notifications = models.BooleanField(default=True) + enable_followed_topic_email_notifications = models.BooleanField(default=True) + enable_followed_topic_push_notifications = models.BooleanField(default=True) + enable_followed_topic_audible_notifications = models.BooleanField(default=True) + enable_followed_topic_wildcard_mentions_notify = models.BooleanField(default=True) + # PM + @-mention notifications. enable_desktop_notifications = models.BooleanField(default=True) pm_content_in_desktop_notifications = models.BooleanField(default=True) @@ -1716,6 +1723,7 @@ class UserBaseSettings(models.Model): modern_notification_settings: Dict[str, Any] = dict( # Add new notification settings here. + enable_followed_topic_email_notifications=bool, ) notification_setting_types = { @@ -4276,6 +4284,7 @@ class NotificationTriggers: WILDCARD_MENTION = "wildcard_mentioned" STREAM_PUSH = "stream_push_notify" STREAM_EMAIL = "stream_email_notify" + FOLLOWED_TOPIC_EMAIL = "followed_topic_email_notify" class ScheduledMessageNotificationEmail(models.Model): @@ -4293,6 +4302,7 @@ class ScheduledMessageNotificationEmail(models.Model): (NotificationTriggers.MENTION, "Mention"), (NotificationTriggers.WILDCARD_MENTION, "Wildcard mention"), (NotificationTriggers.STREAM_EMAIL, "Stream notifications enabled"), + (NotificationTriggers.FOLLOWED_TOPIC_EMAIL, "Followed topic notifications enabled"), ] trigger = models.TextField(choices=EMAIL_NOTIFICATION_TRIGGER_CHOICES) diff --git a/zerver/openapi/zulip.yaml b/zerver/openapi/zulip.yaml index 0a691246fa..31c8777f30 100644 --- a/zerver/openapi/zulip.yaml +++ b/zerver/openapi/zulip.yaml @@ -9748,6 +9748,15 @@ paths: schema: type: boolean example: true + - name: enable_followed_topic_email_notifications + in: query + description: | + Enable email notifications for messages sent to followed topics. + + **Changes**: New in Zulip 8.0 (feature level 189). + schema: + type: boolean + example: true - name: email_notifications_batching_period_seconds in: query description: | @@ -11840,6 +11849,12 @@ paths: description: | Enable audible desktop notifications for private messages and @-mentions. + enable_followed_topic_email_notifications: + type: boolean + description: | + Enable email notifications for messages sent to followed topics. + + **Changes**: New in Zulip 8.0 (feature level 189). email_notifications_batching_period_seconds: type: integer description: | @@ -13900,6 +13915,12 @@ paths: description: | Enable mobile notification for private messages and @-mentions received when the user is online. + enable_followed_topic_email_notifications: + type: boolean + description: | + Enable email notifications for messages sent to followed topics. + + **Changes**: New in Zulip 8.0 (feature level 189). enable_digest_emails: type: boolean description: | @@ -15033,6 +15054,15 @@ paths: schema: type: boolean example: true + - name: enable_followed_topic_email_notifications + in: query + description: | + Enable email notifications for messages sent to followed topics. + + **Changes**: New in Zulip 8.0 (feature level 189). + schema: + type: boolean + example: true - name: enable_digest_emails in: query description: | diff --git a/zerver/tests/test_email_notifications.py b/zerver/tests/test_email_notifications.py index 452f410c08..9701e5e08c 100644 --- a/zerver/tests/test_email_notifications.py +++ b/zerver/tests/test_email_notifications.py @@ -1851,6 +1851,26 @@ class TestMissedMessages(ZulipTestCase): ], ) + def test_followed_topic_missed_message(self) -> None: + hamlet = self.example_user("hamlet") + othello = self.example_user("othello") + msg_id = self.send_stream_message(othello, "Denmark") + + handle_missedmessage_emails( + hamlet.id, + [ + {"message_id": msg_id, "trigger": "followed_topic_email_notify"}, + ], + ) + self.assert_length(mail.outbox, 1) + email_subject = mail.outbox[0].subject + email_body = mail.outbox[0].body + self.assertEqual("#Denmark > test", email_subject) + self.assertIn( + "You are receiving this because you have email notifications enabled for topics you follow.", + email_body, + ) + class TestFollowupEmailDelay(ZulipTestCase): def test_get_onboarding_email_schedule(self) -> None: diff --git a/zerver/tests/test_event_queue.py b/zerver/tests/test_event_queue.py index ae62e95081..24a4aed921 100644 --- a/zerver/tests/test_event_queue.py +++ b/zerver/tests/test_event_queue.py @@ -760,6 +760,56 @@ class MissedMessageHookTest(ZulipTestCase): already_notified={"email_notified": False, "push_notified": False}, ) + def test_followed_topic_email_notify(self) -> None: + # By default, messages sent in followed topics should send email notifications. + do_set_user_topic_visibility_policy( + self.user_profile, + get_stream("Denmark", self.user_profile.realm), + "followed_topic_test", + visibility_policy=UserTopic.VisibilityPolicy.FOLLOWED, + ) + msg_id = self.send_stream_message( + self.iago, "Denmark", content="what's up everyone?", topic_name="followed_topic_test" + ) + with mock.patch("zerver.tornado.event_queue.maybe_enqueue_notifications") as mock_enqueue: + missedmessage_hook(self.user_profile.id, self.client_descriptor, True) + mock_enqueue.assert_called_once() + args_dict = mock_enqueue.call_args_list[0][1] + + self.assert_maybe_enqueue_notifications_call_args( + args_dict=args_dict, + message_id=msg_id, + user_id=self.user_profile.id, + followed_topic_email_notify=True, + already_notified={"email_notified": True, "push_notified": False}, + ) + + def test_followed_topic_email_notify_global_setting(self) -> None: + do_change_user_setting( + self.user_profile, "enable_followed_topic_email_notifications", False, acting_user=None + ) + do_set_user_topic_visibility_policy( + self.user_profile, + get_stream("Denmark", self.user_profile.realm), + "followed_topic_test", + visibility_policy=UserTopic.VisibilityPolicy.FOLLOWED, + ) + msg_id = self.send_stream_message( + self.iago, "Denmark", content="what's up everyone?", topic_name="followed_topic_test" + ) + with mock.patch("zerver.tornado.event_queue.maybe_enqueue_notifications") as mock_enqueue: + missedmessage_hook(self.user_profile.id, self.client_descriptor, True) + mock_enqueue.assert_called_once() + args_dict = mock_enqueue.call_args_list[0][1] + + self.assert_maybe_enqueue_notifications_call_args( + args_dict=args_dict, + message_id=msg_id, + user_id=self.user_profile.id, + followed_topic_email_notify=False, + already_notified={"email_notified": False, "push_notified": False}, + ) + def test_muted_sender(self) -> None: do_mute_user(self.user_profile, self.iago) msg_id = self.send_personal_message(self.iago, self.user_profile) diff --git a/zerver/tests/test_events.py b/zerver/tests/test_events.py index 712a4c4568..dfb3dbe973 100644 --- a/zerver/tests/test_events.py +++ b/zerver/tests/test_events.py @@ -1946,6 +1946,17 @@ class NormalActionsTest(BaseAction): self.user_profile, notification_setting, False, acting_user=self.user_profile ) + num_events = 2 + is_modern_notification_setting = ( + notification_setting in self.user_profile.modern_notification_settings + ) + if is_modern_notification_setting: + # The legacy event format is not sent for modern_notification_settings + # as it exists only for backwards-compatibility with + # clients that don't support the new user_settings event type. + # We only send the legacy event for settings added before Feature level 89. + num_events = 1 + for setting_value in [True, False]: events = self.verify_action( lambda: do_change_user_setting( @@ -1954,10 +1965,11 @@ class NormalActionsTest(BaseAction): setting_value, acting_user=self.user_profile, ), - num_events=2, + num_events=num_events, ) check_user_settings_update("events[0]", events[0]) - check_update_global_notifications("events[1]", events[1], setting_value) + if not is_modern_notification_setting: + check_update_global_notifications("events[1]", events[1], setting_value) # Also test with notification_settings_null=True events = self.verify_action( @@ -1969,10 +1981,11 @@ class NormalActionsTest(BaseAction): ), notification_settings_null=True, state_change_expected=False, - num_events=2, + num_events=num_events, ) check_user_settings_update("events[0]", events[0]) - check_update_global_notifications("events[1]", events[1], setting_value) + if not is_modern_notification_setting: + check_update_global_notifications("events[1]", events[1], setting_value) def test_change_presence_enabled(self) -> None: presence_enabled_setting = "presence_enabled" diff --git a/zerver/tests/test_notification_data.py b/zerver/tests/test_notification_data.py index f4f7dfa59a..4d52326bd0 100644 --- a/zerver/tests/test_notification_data.py +++ b/zerver/tests/test_notification_data.py @@ -182,6 +182,16 @@ class TestNotificationData(ZulipTestCase): ) self.assertTrue(user_data.is_email_notifiable(acting_user_id=acting_user_id, idle=True)) + # Followed Topic notification + user_data = self.create_user_notifications_data_object( + user_id=user_id, followed_topic_email_notify=True + ) + self.assertEqual( + user_data.get_email_notification_trigger(acting_user_id=acting_user_id, idle=True), + "followed_topic_email_notify", + ) + self.assertTrue(user_data.is_email_notifiable(acting_user_id=acting_user_id, idle=True)) + # Test no notifications when not idle user_data = self.create_user_notifications_data_object( user_id=user_id, pm_email_notify=True @@ -267,6 +277,7 @@ class TestNotificationData(ZulipTestCase): stream_email_user_ids=set(), stream_push_user_ids=set(), wildcard_mention_user_ids=set(), + followed_topic_email_user_ids=set(), ) self.assertEqual(user_data.is_notifiable(acting_user_id=1000, idle=True), notifiable) diff --git a/zerver/tests/test_soft_deactivation.py b/zerver/tests/test_soft_deactivation.py index 12be027883..90684267fe 100644 --- a/zerver/tests/test_soft_deactivation.py +++ b/zerver/tests/test_soft_deactivation.py @@ -631,6 +631,7 @@ class SoftDeactivationMessageTest(ZulipTestCase): get_subscriptions_for_send_message( realm_id=realm_id, stream_id=stream_id, + topic_name=topic_name, possible_wildcard_mention=possible_wildcard_mention, possibly_mentioned_user_ids=possibly_mentioned_user_ids, ) diff --git a/zerver/tests/test_users.py b/zerver/tests/test_users.py index acc31ed048..6acc335fb1 100644 --- a/zerver/tests/test_users.py +++ b/zerver/tests/test_users.py @@ -1763,6 +1763,7 @@ class RecipientInfoTest(ZulipTestCase): stream_push_user_ids=set(), stream_email_user_ids=set(), wildcard_mention_user_ids=set(), + followed_topic_email_user_ids=set(), muted_sender_user_ids=set(), um_eligible_user_ids=all_user_ids, long_term_idle_user_ids=set(), @@ -1979,6 +1980,39 @@ class RecipientInfoTest(ZulipTestCase): self.assertEqual(info.default_bot_user_ids, {normal_bot.id}) self.assertEqual(info.all_bot_user_ids, {normal_bot.id, service_bot.id}) + # Now Hamlet follows the topic with the 'followed_topic_email_notifications' + # global setting enabled by default. + do_set_user_topic_visibility_policy( + hamlet, + stream, + topic_name, + visibility_policy=UserTopic.VisibilityPolicy.FOLLOWED, + ) + + info = get_recipient_info( + realm_id=realm.id, + recipient=recipient, + sender_id=hamlet.id, + stream_topic=stream_topic, + ) + self.assertEqual(info.followed_topic_email_user_ids, {hamlet.id}) + + # Omit Hamlet from followed_topic_email_user_ids + do_change_user_setting( + hamlet, + "enable_followed_topic_email_notifications", + False, + acting_user=None, + ) + + info = get_recipient_info( + realm_id=realm.id, + recipient=recipient, + sender_id=hamlet.id, + stream_topic=stream_topic, + ) + self.assertEqual(info.followed_topic_email_user_ids, set()) + def test_get_recipient_info_invalid_recipient_type(self) -> None: hamlet = self.example_user("hamlet") realm = hamlet.realm diff --git a/zerver/tornado/event_queue.py b/zerver/tornado/event_queue.py index ef666c89e6..cfbefd08c6 100644 --- a/zerver/tornado/event_queue.py +++ b/zerver/tornado/event_queue.py @@ -780,6 +780,7 @@ def missedmessage_hook( wildcard_mention_email_notify=internal_data.get("wildcard_mention_email_notify", False), stream_push_notify=internal_data.get("stream_push_notify", False), stream_email_notify=internal_data.get("stream_email_notify", False), + followed_topic_email_notify=internal_data.get("followed_topic_email_notify", False), # Since one is by definition idle, we don't need to check online_push_enabled online_push_enabled=False, disable_external_notifications=internal_data.get( @@ -933,6 +934,7 @@ def process_message_event( stream_push_user_ids = set(event_template.get("stream_push_user_ids", [])) stream_email_user_ids = set(event_template.get("stream_email_user_ids", [])) wildcard_mention_user_ids = set(event_template.get("wildcard_mention_user_ids", [])) + followed_topic_email_user_ids = set(event_template.get("followed_topic_email_user_ids", [])) muted_sender_user_ids = set(event_template.get("muted_sender_user_ids", [])) all_bot_user_ids = set(event_template.get("all_bot_user_ids", [])) disable_external_notifications = event_template.get("disable_external_notifications", False) @@ -983,6 +985,7 @@ def process_message_event( stream_push_user_ids=stream_push_user_ids, stream_email_user_ids=stream_email_user_ids, wildcard_mention_user_ids=wildcard_mention_user_ids, + followed_topic_email_user_ids=followed_topic_email_user_ids, muted_sender_user_ids=muted_sender_user_ids, all_bot_user_ids=all_bot_user_ids, ) @@ -1134,6 +1137,7 @@ def process_message_update_event( stream_push_user_ids = set(event_template.pop("stream_push_user_ids", [])) stream_email_user_ids = set(event_template.pop("stream_email_user_ids", [])) wildcard_mention_user_ids = set(event_template.pop("wildcard_mention_user_ids", [])) + followed_topic_email_user_ids = set(event_template.pop("followed_topic_email_user_ids", [])) muted_sender_user_ids = set(event_template.pop("muted_sender_user_ids", [])) all_bot_user_ids = set(event_template.pop("all_bot_user_ids", [])) disable_external_notifications = event_template.pop("disable_external_notifications", False) @@ -1194,6 +1198,7 @@ def process_message_update_event( stream_push_user_ids=stream_push_user_ids, stream_email_user_ids=stream_email_user_ids, wildcard_mention_user_ids=wildcard_mention_user_ids, + followed_topic_email_user_ids=followed_topic_email_user_ids, muted_sender_user_ids=muted_sender_user_ids, all_bot_user_ids=all_bot_user_ids, ) @@ -1266,7 +1271,11 @@ def maybe_enqueue_notifications_for_message_update( # without extending the UserMessage data model. return - if user_notifications_data.stream_push_notify or user_notifications_data.stream_email_notify: + if ( + user_notifications_data.stream_push_notify + or user_notifications_data.stream_email_notify + or user_notifications_data.followed_topic_email_notify + ): # Currently we assume that if this flag is set to True, then # the user already was notified about the earlier message, # so we short circuit. We may handle this more rigorously diff --git a/zerver/views/realm.py b/zerver/views/realm.py index 80fc40dae7..619b172a3c 100644 --- a/zerver/views/realm.py +++ b/zerver/views/realm.py @@ -426,6 +426,9 @@ def update_realm_user_settings_defaults( json_validator=check_bool, default=None ), wildcard_mentions_notify: Optional[bool] = REQ(json_validator=check_bool, default=None), + enable_followed_topic_email_notifications: Optional[bool] = REQ( + json_validator=check_bool, default=None + ), notification_sound: Optional[str] = REQ(default=None), enable_desktop_notifications: Optional[bool] = REQ(json_validator=check_bool, default=None), enable_sounds: Optional[bool] = REQ(json_validator=check_bool, default=None), diff --git a/zerver/views/user_settings.py b/zerver/views/user_settings.py index 371b33b193..2c96603f39 100644 --- a/zerver/views/user_settings.py +++ b/zerver/views/user_settings.py @@ -193,6 +193,9 @@ def json_change_settings( json_validator=check_bool, default=None ), wildcard_mentions_notify: Optional[bool] = REQ(json_validator=check_bool, default=None), + enable_followed_topic_email_notifications: Optional[bool] = REQ( + json_validator=check_bool, default=None + ), notification_sound: Optional[str] = REQ(default=None), enable_desktop_notifications: Optional[bool] = REQ(json_validator=check_bool, default=None), enable_sounds: Optional[bool] = REQ(json_validator=check_bool, default=None),