diff --git a/api_docs/changelog.md b/api_docs/changelog.md index c01622b33b7ae0..668750e8249e39 100644 --- a/api_docs/changelog.md +++ b/api_docs/changelog.md @@ -20,6 +20,16 @@ format used by the Zulip server that they are interacting with. ## Changes in Zulip 9.0 +**Feature level 240** + +* [`POST /register`](/api/register-queue): + Added a new property - `alerts` to the `unread_msgs`, which is an + array that includes message ids of all the messages which contain alert + words set by the user. +* [`GET /events`](/api/get-events): + Added an `has_alert_words` flag which indicates whether a message includes + any of the alert words set by the user or not. + Feature levels 238-239 are reserved for future use in 8.x maintenance releases. diff --git a/web/src/left_sidebar_navigation_area.js b/web/src/left_sidebar_navigation_area.js index 56a79e41776f08..8368a3c6a3d91b 100644 --- a/web/src/left_sidebar_navigation_area.js +++ b/web/src/left_sidebar_navigation_area.js @@ -66,11 +66,13 @@ export function update_dom_with_unread_counts(counts, skip_animations) { // mentioned/home views have simple integer counts const $mentioned_li = $(".top_left_mentions"); + const $alerted_li = $(".top_left_alerts"); const $home_view_li = $(".selected-home-view"); const $streams_header = $("#streams_header"); const $back_to_streams = $("#topics_header"); ui_util.update_unread_count_in_dom($mentioned_li, counts.mentioned_message_count); + ui_util.update_unread_count_in_dom($alerted_li, counts.alert_message_count); ui_util.update_unread_count_in_dom($home_view_li, counts.home_unread_messages); ui_util.update_unread_count_in_dom($streams_header, counts.stream_unread_messages); ui_util.update_unread_count_in_dom($back_to_streams, counts.stream_unread_messages); diff --git a/web/src/left_sidebar_navigation_area_popovers.js b/web/src/left_sidebar_navigation_area_popovers.js index 4aba8ba0b277df..b3298e87b7715e 100644 --- a/web/src/left_sidebar_navigation_area_popovers.js +++ b/web/src/left_sidebar_navigation_area_popovers.js @@ -18,6 +18,7 @@ import * as settings_config from "./settings_config"; import * as starred_messages from "./starred_messages"; import * as starred_messages_ui from "./starred_messages_ui"; import * as ui_util from "./ui_util"; +import {get_counts} from "./unread"; import * as unread_ops from "./unread_ops"; import {user_settings} from "./user_settings"; @@ -257,6 +258,10 @@ export function initialize() { ); }, onMount() { + ui_util.update_unread_count_in_dom( + $(".condensed-views-popover-menu-alerts"), + get_counts().alert_message_count, + ); ui_util.update_unread_count_in_dom( $(".condensed-views-popover-menu-drafts"), drafts.draft_model.getDraftCount(), diff --git a/web/src/unread.ts b/web/src/unread.ts index 8b76414460020c..a5070465f2b06f 100644 --- a/web/src/unread.ts +++ b/web/src/unread.ts @@ -41,6 +41,8 @@ export const unread_mentions_counter = new Set(); export const direct_message_with_mention_count = new Set(); const unread_messages = new Set(); +export const unread_alerts_counter = new Set(); + // Map with keys of the form "{stream_id}:{topic.toLowerCase()}" and // values being Sets of message IDs for unread messages mentioning the // user within that topic. Use `recent_view_util.get_topic_key` to @@ -676,6 +678,7 @@ export function process_loaded_messages( if (message.type === "private") { process_unread_message({ id: message.id, + alerted: message.alerted, mentioned: message.mentioned, mentioned_me_directly: message.mentioned_me_directly, type: message.type, @@ -685,6 +688,7 @@ export function process_loaded_messages( } else { process_unread_message({ id: message.id, + alerted: message.alerted, mentioned: message.mentioned, mentioned_me_directly: message.mentioned_me_directly, type: message.type, @@ -702,6 +706,7 @@ export function process_loaded_messages( type UnreadMessageData = { id: number; + alerted: boolean; mentioned: boolean; mentioned_me_directly: boolean; unread: boolean; @@ -740,6 +745,7 @@ export function process_unread_message(message: UnreadMessageData): void { } update_message_for_mention(message); + update_message_for_alert(message); } export function update_message_for_mention( @@ -787,6 +793,27 @@ export function update_message_for_mention( return false; } +export function update_message_for_alert( + message: UnreadMessageData, + content_edited = false, +): boolean { + // Returns true if this is a stream message whose content was + // changed, and thus the caller might need to trigger a rerender + // of UI elements displaying whether the message's topic contains + // an unread mention of the user. + if (!message.unread) { + unread_alerts_counter.delete(message.id); + return false; + } + if (message.alerted) { + unread_alerts_counter.add(message.id); + } + if (content_edited && message.type === "stream") { + return true; + } + return false; +} + export function mark_as_read(message_id: number): void { // We don't need to check anything about the message, since all // the following methods are cheap and work fine even if message_id @@ -799,6 +826,7 @@ export function mark_as_read(message_id: number): void { remove_message_from_unread_mention_topics(message_id); unread_topic_counter.delete(message_id); unread_mentions_counter.delete(message_id); + unread_alerts_counter.delete(message_id); direct_message_with_mention_count.delete(message_id); unread_messages.delete(message_id); @@ -813,6 +841,7 @@ export function declare_bankruptcy(): void { unread_direct_message_counter.clear(); unread_topic_counter.clear(); unread_mentions_counter.clear(); + unread_alerts_counter.clear(); direct_message_with_mention_count.clear(); unread_messages.clear(); unread_mention_topics.clear(); @@ -833,6 +862,7 @@ export function get_unread_topics(include_per_topic_latest_msg_id = false): Unre export type FullUnreadCountsData = { direct_message_count: number; mentioned_message_count: number; + alert_message_count: number; direct_message_with_mention_count: number; stream_unread_messages: number; followed_topic_unread_messages_count: number; @@ -854,6 +884,7 @@ export function get_counts(): FullUnreadCountsData { return { direct_message_count: pm_res.total_count, mentioned_message_count: unread_mentions_counter.size, + alert_message_count: unread_alerts_counter.size, direct_message_with_mention_count: direct_message_with_mention_count.size, stream_unread_messages: topic_res.stream_unread_messages, followed_topic_unread_messages_count: topic_res.followed_topic_unread_messages, @@ -1023,6 +1054,7 @@ type UnreadMessagesParams = { streams: UnreadStreamInfo[]; huddles: UnreadHuddleInfo[]; mentions: number[]; + alerts: number[]; count: number; old_unreads_missing: boolean; }; @@ -1043,6 +1075,10 @@ export function initialize(params: UnreadMessagesParams): void { } clear_and_populate_unread_mention_topics(); + for (const message_id of unread_msgs.alerts) { + unread_alerts_counter.add(message_id); + } + for (const obj of unread_msgs.huddles) { for (const message_id of obj.unread_message_ids) { unread_messages.add(message_id); diff --git a/web/src/unread_ops.js b/web/src/unread_ops.js index cbb9dd1a55f1c2..005eb37a0ee28a 100644 --- a/web/src/unread_ops.js +++ b/web/src/unread_ops.js @@ -383,6 +383,7 @@ export function process_unread_messages_event({message_ids, message_details}) { unread.process_unread_message({ id: message_id, mentioned: message_info.mentioned, + alerted: message_info.alert, mentioned_me_directly, stream_id: message_info.stream_id, topic: message_info.topic, diff --git a/web/tests/left_sidebar_navigation_area.test.js b/web/tests/left_sidebar_navigation_area.test.js index d1d2916ea16de9..e9b1bb7c7d57e6 100644 --- a/web/tests/left_sidebar_navigation_area.test.js +++ b/web/tests/left_sidebar_navigation_area.test.js @@ -82,10 +82,13 @@ run_test("update_count_in_dom", () => { mentioned_message_count: 222, home_unread_messages: 333, stream_unread_messages: 666, + alert_message_count: 999, }; make_elem($(".top_left_mentions"), ""); + make_elem($(".top_left_alerts"), ""); + make_elem($(".top_left_inbox"), ""); make_elem($(".selected-home-view"), ""); @@ -104,6 +107,7 @@ run_test("update_count_in_dom", () => { left_sidebar_navigation_area.initialize(); assert.equal($("").text(), "222"); + assert.equal($("").text(), "999"); assert.equal($("").text(), "333"); assert.equal($("").text(), "444"); assert.equal($("").text(), "555"); diff --git a/web/tests/unread.test.js b/web/tests/unread.test.js index 9013799e2e4417..f2166d0dc07ae3 100644 --- a/web/tests/unread.test.js +++ b/web/tests/unread.test.js @@ -768,6 +768,7 @@ test("server_counts", () => { }, ], mentions: [31, 34, 40, 41], + alerts: [23, 74, 10, 99], }, }; diff --git a/zerver/lib/event_schema.py b/zerver/lib/event_schema.py index b54a00835360fd..9ee5fea9ca9bf5 100644 --- a/zerver/lib/event_schema.py +++ b/zerver/lib/event_schema.py @@ -1753,6 +1753,7 @@ def check_update_message( ], optional_keys=[ ("mentioned", bool), + ("has_alert_words", bool), ("user_ids", ListType(int)), ("stream_id", int), ("topic", str), diff --git a/zerver/lib/message.py b/zerver/lib/message.py index f9872fbb679906..1d48f37ab5916c 100644 --- a/zerver/lib/message.py +++ b/zerver/lib/message.py @@ -86,6 +86,7 @@ class MessageDetailsDict(TypedDict, total=False): type: str mentioned: bool + has_alert_words: bool user_ids: List[int] stream_id: int topic: str @@ -120,6 +121,7 @@ class RawUnreadMessagesResult(TypedDict): stream_dict: Dict[int, RawUnreadStreamDict] huddle_dict: Dict[int, RawUnreadHuddleDict] mentions: Set[int] + alerts: Set[int] muted_stream_ids: Set[int] unmuted_stream_msgs: Set[int] old_unreads_missing: bool @@ -148,6 +150,7 @@ class UnreadMessagesResult(TypedDict): streams: List[UnreadStreamInfo] huddles: List[UnreadHuddleInfo] mentions: List[int] + alerts: List[int] count: int old_unreads_missing: bool @@ -1155,6 +1158,7 @@ def extract_unread_data_from_um_rows( unmuted_stream_msgs: Set[int] = set() huddle_dict: Dict[int, RawUnreadHuddleDict] = {} mentions: Set[int] = set() + alerts: Set[int] = set() total_unreads = 0 raw_unread_messages: RawUnreadMessagesResult = dict( @@ -1164,6 +1168,7 @@ def extract_unread_data_from_um_rows( unmuted_stream_msgs=unmuted_stream_msgs, huddle_dict=huddle_dict, mentions=mentions, + alerts=alerts, old_unreads_missing=False, ) @@ -1240,7 +1245,7 @@ def get_huddle_users(recipient_id: int) -> str: user_ids_string=user_ids_string, ) - # TODO: Add support for alert words here as well. + is_alert = row["flags"] & UserMessage.flags.has_alert_word is_mentioned = (row["flags"] & UserMessage.flags.mentioned) != 0 is_stream_wildcard_mentioned = ( row["flags"] & UserMessage.flags.stream_wildcard_mentioned @@ -1250,6 +1255,8 @@ def get_huddle_users(recipient_id: int) -> str: ) != 0 if is_mentioned: mentions.add(message_id) + if is_alert: + alerts.add(message_id) if is_stream_wildcard_mentioned or is_topic_wildcard_mentioned: if msg_type == Recipient.STREAM: stream_id = row["message__recipient__type_id"] @@ -1348,6 +1355,7 @@ def aggregate_unread_data(raw_data: RawUnreadMessagesResult) -> UnreadMessagesRe unmuted_stream_msgs = raw_data["unmuted_stream_msgs"] huddle_dict = raw_data["huddle_dict"] mentions = list(raw_data["mentions"]) + alerts = list(raw_data["alerts"]) count = len(pm_dict) + len(unmuted_stream_msgs) + len(huddle_dict) @@ -1360,6 +1368,7 @@ def aggregate_unread_data(raw_data: RawUnreadMessagesResult) -> UnreadMessagesRe streams=stream_objects, huddles=huddle_objects, mentions=mentions, + alerts=alerts, count=count, old_unreads_missing=raw_data["old_unreads_missing"], ) @@ -1427,6 +1436,8 @@ def apply_unread_message_event( if "mentioned" in flags: state["mentions"].add(message_id) + if "has_alert_word" in flags: + state["alerts"].add(message_id) if ( "stream_wildcard_mentioned" in flags or "topic_wildcard_mentioned" in flags ) and message_id in state["unmuted_stream_msgs"]: @@ -1441,6 +1452,7 @@ def remove_message_id_from_unread_mgs(state: RawUnreadMessagesResult, message_id state["huddle_dict"].pop(message_id, None) state["unmuted_stream_msgs"].discard(message_id) state["mentions"].discard(message_id) + state["alerts"].discard(message_id) def format_unread_message_details( @@ -1464,6 +1476,8 @@ def format_unread_message_details( ) if message_id in raw_unread_data["mentions"]: message_details["mentioned"] = True + if message_id in raw_unread_data["alerts"]: + message_details["has_alert_words"] = True unread_data[str(message_id)] = message_details for message_id, stream_message_details in raw_unread_data["stream_dict"].items(): @@ -1478,6 +1492,8 @@ def format_unread_message_details( ) if message_id in raw_unread_data["mentions"]: message_details["mentioned"] = True + if message_id in raw_unread_data["alerts"]: + message_details["has_alert_words"] = True unread_data[str(message_id)] = message_details for message_id, huddle_message_details in raw_unread_data["huddle_dict"].items(): @@ -1494,6 +1510,8 @@ def format_unread_message_details( ) if message_id in raw_unread_data["mentions"]: message_details["mentioned"] = True + if message_id in raw_unread_data["alerts"]: + message_details["has_alert_words"] = True unread_data[str(message_id)] = message_details return unread_data @@ -1508,6 +1526,9 @@ def add_message_to_unread_msgs( if message_details.get("mentioned"): state["mentions"].add(message_id) + if message_details.get("has_alert_words"): + state["alerts"].add(message_id) + if message_details["type"] == "private": user_ids: List[int] = message_details["user_ids"] user_ids = [user_id for user_id in user_ids if user_id != my_user_id] diff --git a/zerver/openapi/zulip.yaml b/zerver/openapi/zulip.yaml index 397a38572eb427..ed3c9a65383daa 100644 --- a/zerver/openapi/zulip.yaml +++ b/zerver/openapi/zulip.yaml @@ -3041,6 +3041,15 @@ paths: of the user. Present only if the message mentions the current user. + has_alert_words: + type: boolean + description: | + A flag which indicates whether the message contains an alert + word set by the user. + + Present only if the message contains the alert word. + + **Changes**: New in Zulip 9.0 (feature level 240). user_ids: type: array items: @@ -12723,6 +12732,15 @@ paths: topic features were not handled correctly in calculating this field. items: type: integer + alerts: + type: array + description: | + Array containing the IDs of all unread messages which contain the alert + words set by the user. + + **Changes**: New in Zulip 9.0 (feature level 240). + items: + type: integer old_unreads_missing: type: boolean description: | diff --git a/zerver/tests/test_events.py b/zerver/tests/test_events.py index 0a29b29e26c45d..2ddc2c76e76e4a 100644 --- a/zerver/tests/test_events.py +++ b/zerver/tests/test_events.py @@ -476,6 +476,16 @@ def test_mentioned_send_message_events(self) -> None: partial(self.send_stream_message, self.example_user("cordelia"), "Verona", content), ) + def test_alert_send_message_events(self) -> None: + alert_words = ["hello", "hi", "bye"] + do_add_alert_words(self.user_profile, alert_words) + + for alert_word in alert_words: + content = alert_word + self.verify_action( + 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") @@ -1025,6 +1035,7 @@ def test_update_message_flags(self) -> None: def test_update_read_flag_removes_unread_msg_ids(self) -> None: user_profile = self.example_user("hamlet") mention = "@**" + user_profile.full_name + "**" + do_add_alert_words(self.user_profile, ["hello"]) for content in ["hello", mention]: message = self.send_stream_message( diff --git a/zerver/tests/test_message_flags.py b/zerver/tests/test_message_flags.py index bc906e2c07ec42..29cde441ccb9ed 100644 --- a/zerver/tests/test_message_flags.py +++ b/zerver/tests/test_message_flags.py @@ -1066,11 +1066,12 @@ def get_unread_data() -> UnreadMessagesResult: um.save() result = get_unread_data() self.assertEqual(result["mentions"], [stream_message_id]) + self.assertEqual(result["alerts"], []) um.flags = UserMessage.flags.has_alert_word um.save() result = get_unread_data() - # TODO: This should change when we make alert words work better. + self.assertEqual(result["alerts"], [stream_message_id]) self.assertEqual(result["mentions"], []) um.flags = UserMessage.flags.stream_wildcard_mentioned @@ -1103,6 +1104,7 @@ def get_unread_data() -> UnreadMessagesResult: um.save() result = get_unread_data() self.assertEqual(result["mentions"], []) + self.assertEqual(result["alerts"], [muted_stream_message_id]) # wildcard mentions don't take precedence over mutedness in # a normal or muted topic within a muted stream @@ -1158,6 +1160,7 @@ def get_unread_data() -> UnreadMessagesResult: um.save() result = get_unread_data() self.assertEqual(result["mentions"], []) + self.assertEqual(result["alerts"], [muted_topic_message_id]) # wildcard mentions don't take precedence over mutedness in a muted topic. um.flags = UserMessage.flags.stream_wildcard_mentioned @@ -1685,6 +1688,7 @@ def test_format_unread_message_details(self) -> None: stream_dict={}, huddle_dict={}, mentions=set(), + alerts=set(), muted_stream_ids=set(), unmuted_stream_msgs=set(), old_unreads_missing=False, @@ -1707,6 +1711,7 @@ def test_add_message_to_unread_msgs(self) -> None: stream_dict={}, huddle_dict={}, mentions=set(), + alerts=set(), muted_stream_ids=set(), unmuted_stream_msgs=set(), old_unreads_missing=False, @@ -1930,6 +1935,69 @@ def test_stream_messages_unread_mention(self) -> None: ) self.assertTrue(um.flags.read) + def test_stream_messages_unread_alert(self) -> None: + sender = self.example_user("cordelia") + receiver = self.example_user("hamlet") + stream_name = "Denmark" + self.subscribe(sender, stream_name) + topic_name = "test" + self.login("hamlet") + params = { + "alert_words": orjson.dumps(["milk", "cookies"]).decode(), + } + result = self.client_post("/json/users/me/alert_words", params) + self.assert_json_success(result) + message_ids = [ + self.send_stream_message( + sender=sender, stream_name=stream_name, topic_name=topic_name, content="milk" + ) + for i in range(4) + ] + result = self.client_post( + "/json/messages/flags", + {"messages": orjson.dumps(message_ids).decode(), "op": "add", "flag": "read"}, + ) + self.assert_json_success(result) + for message_id in message_ids: + um = UserMessage.objects.get( + user_profile_id=receiver.id, + message_id=message_id, + ) + self.assertTrue(um.flags.read) + messages_to_unread = message_ids[2:] + messages_still_read = message_ids[:2] + + params = { + "messages": orjson.dumps(messages_to_unread).decode(), + "op": "remove", + "flag": "read", + } + + # Use the capture_send_event_calls context manager to capture events. + with self.capture_send_event_calls(expected_num_events=1) as events: + result = self.api_post(receiver, "/api/v1/messages/flags", params) + + self.assert_json_success(result) + event = events[0]["event"] + self.assertEqual(event["messages"], messages_to_unread) + unread_message_ids = {str(message_id) for message_id in messages_to_unread} + self.assertSetEqual(set(event["message_details"].keys()), unread_message_ids) + for message_id in event["message_details"]: + self.assertEqual(event["message_details"][message_id]["has_alert_words"], True) + + for message_id in messages_to_unread: + um = UserMessage.objects.get( + user_profile_id=receiver.id, + message_id=message_id, + ) + self.assertFalse(um.flags.read) + for message_id in messages_still_read: + um = UserMessage.objects.get( + user_profile_id=receiver.id, + message_id=message_id, + ) + self.assertTrue(um.flags.read) + def test_unsubscribed_stream_messages_unread(self) -> None: """An extended test verifying that the `update_message_flags` endpoint correctly preserves the invariant that messages cannot be @@ -2253,6 +2321,78 @@ def test_pm_messages_unread_mention(self) -> None: ) self.assertTrue(um.flags.read) + def test_pm_messages_unread_alert(self) -> None: + sender = self.example_user("cordelia") + receiver = self.example_user("hamlet") + stream_name = "Denmark" + self.login("hamlet") + self.subscribe(receiver, stream_name) + params = { + "alert_words": orjson.dumps(["milk", "cookies"]).decode(), + } + result = self.client_post("/json/users/me/alert_words", params) + self.assert_json_success(result) + message_ids = [ + self.send_personal_message(sender, receiver, content="cookies") for i in range(4) + ] + for message_id in message_ids: + um = UserMessage.objects.get( + user_profile_id=receiver.id, + message_id=message_id, + ) + self.assertFalse(um.flags.read) + result = self.client_post( + "/json/messages/flags", + {"messages": orjson.dumps(message_ids).decode(), "op": "add", "flag": "read"}, + ) + self.assert_json_success(result) + for message_id in message_ids: + um = UserMessage.objects.get( + user_profile_id=receiver.id, + message_id=message_id, + ) + self.assertTrue(um.flags.read) + messages_to_unread = message_ids[2:] + messages_still_read = message_ids[:2] + + params = { + "messages": orjson.dumps(messages_to_unread).decode(), + "op": "remove", + "flag": "read", + } + + # Use the capture_send_event_calls context manager to capture events. + with self.capture_send_event_calls(expected_num_events=1) as events: + result = self.api_post(receiver, "/api/v1/messages/flags", params) + + self.assert_json_success(result) + event = events[0]["event"] + self.assertEqual(event["messages"], messages_to_unread) + unread_message_ids = {str(message_id) for message_id in messages_to_unread} + self.assertSetEqual(set(event["message_details"].keys()), unread_message_ids) + for message_id in event["message_details"]: + self.assertEqual( + event["message_details"][message_id], + dict( + type="private", + user_ids=[sender.id], + has_alert_words=True, + ), + ) + + for message_id in messages_to_unread: + um = UserMessage.objects.get( + user_profile_id=receiver.id, + message_id=message_id, + ) + self.assertFalse(um.flags.read) + for message_id in messages_still_read: + um = UserMessage.objects.get( + user_profile_id=receiver.id, + message_id=message_id, + ) + self.assertTrue(um.flags.read) + def test_huddle_messages_unread(self) -> None: sender = self.example_user("cordelia") receiver = self.example_user("hamlet") @@ -2314,6 +2454,72 @@ def test_huddle_messages_unread(self) -> None: ) self.assertTrue(um.flags.read) + def test_huddle_messages_unread_alerts(self) -> None: + sender = self.example_user("cordelia") + receiver = self.example_user("hamlet") + user1 = self.example_user("othello") + self.login("hamlet") + params = { + "alert_words": orjson.dumps(["milk", "cookies"]).decode(), + } + result = self.client_post("/json/users/me/alert_words", params) + self.assert_json_success(result) + message_ids = [ + # self.send_huddle_message(sender, receiver, content="Hello") for i in range(4) + self.send_huddle_message(sender, [receiver, user1], content="cookies") + for i in range(4) + ] + for message_id in message_ids: + um = UserMessage.objects.get( + user_profile_id=receiver.id, + message_id=message_id, + ) + self.assertFalse(um.flags.read) + result = self.client_post( + "/json/messages/flags", + {"messages": orjson.dumps(message_ids).decode(), "op": "add", "flag": "read"}, + ) + self.assert_json_success(result) + for message_id in message_ids: + um = UserMessage.objects.get( + user_profile_id=receiver.id, + message_id=message_id, + ) + self.assertTrue(um.flags.read) + messages_to_unread = message_ids[2:] + messages_still_read = message_ids[:2] + + params = { + "messages": orjson.dumps(messages_to_unread).decode(), + "op": "remove", + "flag": "read", + } + + # Use the capture_send_event_calls context manager to capture events. + with self.capture_send_event_calls(expected_num_events=1) as events: + result = self.api_post(receiver, "/api/v1/messages/flags", params) + + self.assert_json_success(result) + event = events[0]["event"] + self.assertEqual(event["messages"], messages_to_unread) + unread_message_ids = {str(message_id) for message_id in messages_to_unread} + self.assertSetEqual(set(event["message_details"].keys()), unread_message_ids) + for message_id in event["message_details"]: + self.assertEqual(event["message_details"][message_id]["has_alert_words"], True) + + for message_id in messages_to_unread: + um = UserMessage.objects.get( + user_profile_id=receiver.id, + message_id=message_id, + ) + self.assertFalse(um.flags.read) + for message_id in messages_still_read: + um = UserMessage.objects.get( + user_profile_id=receiver.id, + message_id=message_id, + ) + self.assertTrue(um.flags.read) + def test_huddle_messages_unread_mention(self) -> None: sender = self.example_user("cordelia") receiver = self.example_user("hamlet")