Skip to content
This repository was archived by the owner on Apr 26, 2024. It is now read-only.

Commit 46613aa

Browse files
authored
Implement unread counter (MSC2625) (#7673)
Implementation of matrix-org/matrix-spec-proposals#2625
2 parents e452973 + 6efb2b0 commit 46613aa

File tree

9 files changed

+187
-51
lines changed

9 files changed

+187
-51
lines changed

changelog.d/7673.feature

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Add a per-room counter for unread messages in responses to `/sync` requests. Implements [MSC2625](https://github.com/matrix-org/matrix-doc/pull/2625).

synapse/handlers/sync.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1893,6 +1893,9 @@ async def _generate_room_entry(
18931893
if notifs is not None:
18941894
unread_notifications["notification_count"] = notifs["notify_count"]
18951895
unread_notifications["highlight_count"] = notifs["highlight_count"]
1896+
unread_notifications["org.matrix.msc2625.unread_count"] = notifs[
1897+
"unread_count"
1898+
]
18961899

18971900
sync_result_builder.joined.append(room_sync)
18981901

synapse/push/bulk_push_rule_evaluator.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -189,8 +189,11 @@ def action_for_event_by_user(self, event, context):
189189
)
190190
if matches:
191191
actions = [x for x in rule["actions"] if x != "dont_notify"]
192-
if actions and "notify" in actions:
193-
# Push rules say we should notify the user of this event
192+
if (
193+
"notify" in actions
194+
or "org.matrix.msc2625.mark_unread" in actions
195+
):
196+
# Push rules say we should act on this event.
194197
actions_by_user[uid] = actions
195198
break
196199

synapse/push/push_tools.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,10 @@ def get_badge_count(store, user_id):
3939
)
4040
# return one badge count per conversation, as count per
4141
# message is so noisy as to be almost useless
42-
badge += 1 if notifs["notify_count"] else 0
42+
# We're populating this badge using the unread_count (instead of the
43+
# notify_count) as this badge is the number of missed messages, not the
44+
# number of missed notifications.
45+
badge += 1 if notifs["unread_count"] else 0
4346
return badge
4447

4548

synapse/rest/client/v1/push_rule.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
# -*- coding: utf-8 -*-
2-
# Copyright 2014-2016 OpenMarket Ltd
2+
# Copyright 2014-2020 The Matrix.org Foundation C.I.C.
33
#
44
# Licensed under the Apache License, Version 2.0 (the "License");
55
# you may not use this file except in compliance with the License.
@@ -267,7 +267,7 @@ def _check_actions(actions):
267267
raise InvalidRuleException("No actions found")
268268

269269
for a in actions:
270-
if a in ["notify", "dont_notify", "coalesce"]:
270+
if a in ["notify", "dont_notify", "coalesce", "org.matrix.msc2625.mark_unread"]:
271271
pass
272272
elif isinstance(a, dict) and "set_tweak" in a:
273273
pass

synapse/storage/data_stores/main/event_push_actions.py

Lines changed: 104 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
# -*- coding: utf-8 -*-
2-
# Copyright 2015 OpenMarket Ltd
3-
# Copyright 2018 New Vector Ltd
2+
# Copyright 2015-2020 The Matrix.org Foundation C.I.C.
43
#
54
# Licensed under the Apache License, Version 2.0 (the "License");
65
# you may not use this file except in compliance with the License.
@@ -15,7 +14,9 @@
1514
# limitations under the License.
1615

1716
import logging
17+
from typing import Dict, Tuple
1818

19+
import attr
1920
from canonicaljson import json
2021

2122
from twisted.internet import defer
@@ -36,6 +37,16 @@
3637
]
3738

3839

40+
@attr.s
41+
class EventPushSummary:
42+
"""Summary of pending event push actions for a given user in a given room."""
43+
44+
unread_count = attr.ib(type=int)
45+
stream_ordering = attr.ib(type=int)
46+
old_user_id = attr.ib(type=str)
47+
notif_count = attr.ib(type=int)
48+
49+
3950
def _serialize_action(actions, is_highlight):
4051
"""Custom serializer for actions. This allows us to "compress" common actions.
4152
@@ -122,32 +133,50 @@ def _get_unread_counts_by_receipt_txn(
122133

123134
def _get_unread_counts_by_pos_txn(self, txn, room_id, user_id, stream_ordering):
124135

125-
# First get number of notifications.
126-
# We don't need to put a notif=1 clause as all rows always have
127-
# notif=1
136+
# First get number of actions, grouped on whether the action notifies.
128137
sql = (
129-
"SELECT count(*)"
138+
"SELECT count(*), notif"
130139
" FROM event_push_actions ea"
131140
" WHERE"
132141
" user_id = ?"
133142
" AND room_id = ?"
134143
" AND stream_ordering > ?"
144+
" GROUP BY notif"
135145
)
136-
137146
txn.execute(sql, (user_id, room_id, stream_ordering))
138-
row = txn.fetchone()
139-
notify_count = row[0] if row else 0
147+
rows = txn.fetchall()
148+
149+
# We should get a maximum number of two rows: one for notif = 0, which is the
150+
# number of actions that contribute to the unread_count but not to the
151+
# notify_count, and one for notif = 1, which is the number of actions that
152+
# contribute to both counters. If one or both rows don't appear, then the
153+
# value for the matching counter should be 0.
154+
unread_count = 0
155+
notify_count = 0
156+
for row in rows:
157+
# We always increment unread_count because actions that notify also
158+
# contribute to it.
159+
unread_count += row[0]
160+
if row[1] == 1:
161+
notify_count = row[0]
162+
elif row[1] != 0:
163+
logger.warning(
164+
"Unexpected value %d for column 'notif' in table"
165+
" 'event_push_actions'",
166+
row[1],
167+
)
140168

141169
txn.execute(
142170
"""
143-
SELECT notif_count FROM event_push_summary
171+
SELECT notif_count, unread_count FROM event_push_summary
144172
WHERE room_id = ? AND user_id = ? AND stream_ordering > ?
145173
""",
146174
(room_id, user_id, stream_ordering),
147175
)
148176
rows = txn.fetchall()
149177
if rows:
150178
notify_count += rows[0][0]
179+
unread_count += rows[0][1]
151180

152181
# Now get the number of highlights
153182
sql = (
@@ -164,7 +193,11 @@ def _get_unread_counts_by_pos_txn(self, txn, room_id, user_id, stream_ordering):
164193
row = txn.fetchone()
165194
highlight_count = row[0] if row else 0
166195

167-
return {"notify_count": notify_count, "highlight_count": highlight_count}
196+
return {
197+
"unread_count": unread_count,
198+
"notify_count": notify_count,
199+
"highlight_count": highlight_count,
200+
}
168201

169202
@defer.inlineCallbacks
170203
def get_push_action_users_in_range(self, min_stream_ordering, max_stream_ordering):
@@ -222,6 +255,7 @@ def get_after_receipt(txn):
222255
" AND ep.user_id = ?"
223256
" AND ep.stream_ordering > ?"
224257
" AND ep.stream_ordering <= ?"
258+
" AND ep.notif = 1"
225259
" ORDER BY ep.stream_ordering ASC LIMIT ?"
226260
)
227261
args = [user_id, user_id, min_stream_ordering, max_stream_ordering, limit]
@@ -250,6 +284,7 @@ def get_no_receipt(txn):
250284
" AND ep.user_id = ?"
251285
" AND ep.stream_ordering > ?"
252286
" AND ep.stream_ordering <= ?"
287+
" AND ep.notif = 1"
253288
" ORDER BY ep.stream_ordering ASC LIMIT ?"
254289
)
255290
args = [user_id, user_id, min_stream_ordering, max_stream_ordering, limit]
@@ -322,6 +357,7 @@ def get_after_receipt(txn):
322357
" AND ep.user_id = ?"
323358
" AND ep.stream_ordering > ?"
324359
" AND ep.stream_ordering <= ?"
360+
" AND ep.notif = 1"
325361
" ORDER BY ep.stream_ordering DESC LIMIT ?"
326362
)
327363
args = [user_id, user_id, min_stream_ordering, max_stream_ordering, limit]
@@ -350,6 +386,7 @@ def get_no_receipt(txn):
350386
" AND ep.user_id = ?"
351387
" AND ep.stream_ordering > ?"
352388
" AND ep.stream_ordering <= ?"
389+
" AND ep.notif = 1"
353390
" ORDER BY ep.stream_ordering DESC LIMIT ?"
354391
)
355392
args = [user_id, user_id, min_stream_ordering, max_stream_ordering, limit]
@@ -399,7 +436,7 @@ def get_if_maybe_push_in_range_for_user(self, user_id, min_stream_ordering):
399436
def _get_if_maybe_push_in_range_for_user_txn(txn):
400437
sql = """
401438
SELECT 1 FROM event_push_actions
402-
WHERE user_id = ? AND stream_ordering > ?
439+
WHERE user_id = ? AND stream_ordering > ? AND notif = 1
403440
LIMIT 1
404441
"""
405442

@@ -428,14 +465,15 @@ def add_push_actions_to_staging(self, event_id, user_id_actions):
428465
return
429466

430467
# This is a helper function for generating the necessary tuple that
431-
# can be used to inert into the `event_push_actions_staging` table.
468+
# can be used to insert into the `event_push_actions_staging` table.
432469
def _gen_entry(user_id, actions):
433470
is_highlight = 1 if _action_has_highlight(actions) else 0
471+
notif = 0 if "org.matrix.msc2625.mark_unread" in actions else 1
434472
return (
435473
event_id, # event_id column
436474
user_id, # user_id column
437475
_serialize_action(actions, is_highlight), # actions column
438-
1, # notif column
476+
notif, # notif column
439477
is_highlight, # highlight column
440478
)
441479

@@ -817,24 +855,51 @@ def _rotate_notifs_before_txn(self, txn, rotate_to_stream_ordering):
817855
# Calculate the new counts that should be upserted into event_push_summary
818856
sql = """
819857
SELECT user_id, room_id,
820-
coalesce(old.notif_count, 0) + upd.notif_count,
858+
coalesce(old.%s, 0) + upd.cnt,
821859
upd.stream_ordering,
822860
old.user_id
823861
FROM (
824-
SELECT user_id, room_id, count(*) as notif_count,
862+
SELECT user_id, room_id, count(*) as cnt,
825863
max(stream_ordering) as stream_ordering
826864
FROM event_push_actions
827865
WHERE ? <= stream_ordering AND stream_ordering < ?
828866
AND highlight = 0
867+
%s
829868
GROUP BY user_id, room_id
830869
) AS upd
831870
LEFT JOIN event_push_summary AS old USING (user_id, room_id)
832871
"""
833872

834-
txn.execute(sql, (old_rotate_stream_ordering, rotate_to_stream_ordering))
835-
rows = txn.fetchall()
873+
# First get the count of unread messages.
874+
txn.execute(
875+
sql % ("unread_count", ""),
876+
(old_rotate_stream_ordering, rotate_to_stream_ordering),
877+
)
836878

837-
logger.info("Rotating notifications, handling %d rows", len(rows))
879+
# We need to merge both lists into a single object because we might not have the
880+
# same amount of rows in each of them. In this case we use a dict indexed on the
881+
# user ID and room ID to make it easier to populate.
882+
summaries = {} # type: Dict[Tuple[str, str], EventPushSummary]
883+
for row in txn:
884+
summaries[(row[0], row[1])] = EventPushSummary(
885+
unread_count=row[2],
886+
stream_ordering=row[3],
887+
old_user_id=row[4],
888+
notif_count=0,
889+
)
890+
891+
# Then get the count of notifications.
892+
txn.execute(
893+
sql % ("notif_count", "AND notif = 1"),
894+
(old_rotate_stream_ordering, rotate_to_stream_ordering),
895+
)
896+
897+
# notif_rows is populated based on a subset of the query used to populate
898+
# unread_rows, so we can be sure that there will be no KeyError here.
899+
for row in txn:
900+
summaries[(row[0], row[1])].notif_count = row[2]
901+
902+
logger.info("Rotating notifications, handling %d rows", len(summaries))
838903

839904
# If the `old.user_id` above is NULL then we know there isn't already an
840905
# entry in the table, so we simply insert it. Otherwise we update the
@@ -844,22 +909,34 @@ def _rotate_notifs_before_txn(self, txn, rotate_to_stream_ordering):
844909
table="event_push_summary",
845910
values=[
846911
{
847-
"user_id": row[0],
848-
"room_id": row[1],
849-
"notif_count": row[2],
850-
"stream_ordering": row[3],
912+
"user_id": user_id,
913+
"room_id": room_id,
914+
"notif_count": summary.notif_count,
915+
"unread_count": summary.unread_count,
916+
"stream_ordering": summary.stream_ordering,
851917
}
852-
for row in rows
853-
if row[4] is None
918+
for ((user_id, room_id), summary) in summaries.items()
919+
if summary.old_user_id is None
854920
],
855921
)
856922

857923
txn.executemany(
858924
"""
859-
UPDATE event_push_summary SET notif_count = ?, stream_ordering = ?
925+
UPDATE event_push_summary
926+
SET notif_count = ?, unread_count = ?, stream_ordering = ?
860927
WHERE user_id = ? AND room_id = ?
861928
""",
862-
((row[2], row[3], row[0], row[1]) for row in rows if row[4] is not None),
929+
(
930+
(
931+
summary.notif_count,
932+
summary.unread_count,
933+
summary.stream_ordering,
934+
user_id,
935+
room_id,
936+
)
937+
for ((user_id, room_id), summary) in summaries.items()
938+
if summary.old_user_id is not None
939+
),
863940
)
864941

865942
txn.execute(
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
/* Copyright 2020 The Matrix.org Foundation C.I.C
2+
*
3+
* Licensed under the Apache License, Version 2.0 (the "License");
4+
* you may not use this file except in compliance with the License.
5+
* You may obtain a copy of the License at
6+
*
7+
* http://www.apache.org/licenses/LICENSE-2.0
8+
*
9+
* Unless required by applicable law or agreed to in writing, software
10+
* distributed under the License is distributed on an "AS IS" BASIS,
11+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
* See the License for the specific language governing permissions and
13+
* limitations under the License.
14+
*/
15+
16+
-- Store the number of unread messages, i.e. messages that triggered either a notify
17+
-- action or a mark_unread one.
18+
ALTER TABLE event_push_summary ADD COLUMN unread_count BIGINT NOT NULL DEFAULT 0;
19+
20+
-- Pre-populate the new column with the count of pending notifications.
21+
-- We expect event_push_summary to be relatively small, so we can do this update
22+
-- synchronously without impacting Synapse's startup time too much.
23+
UPDATE event_push_summary SET unread_count = notif_count;

tests/replication/slave/storage/test_events.py

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -160,7 +160,7 @@ def test_push_actions_for_user(self):
160160
self.check(
161161
"get_unread_event_push_actions_by_room_for_user",
162162
[ROOM_ID, USER_ID_2, event1.event_id],
163-
{"highlight_count": 0, "notify_count": 0},
163+
{"highlight_count": 0, "notify_count": 0, "unread_count": 0},
164164
)
165165

166166
self.persist(
@@ -173,7 +173,7 @@ def test_push_actions_for_user(self):
173173
self.check(
174174
"get_unread_event_push_actions_by_room_for_user",
175175
[ROOM_ID, USER_ID_2, event1.event_id],
176-
{"highlight_count": 0, "notify_count": 1},
176+
{"highlight_count": 0, "notify_count": 1, "unread_count": 1},
177177
)
178178

179179
self.persist(
@@ -188,7 +188,20 @@ def test_push_actions_for_user(self):
188188
self.check(
189189
"get_unread_event_push_actions_by_room_for_user",
190190
[ROOM_ID, USER_ID_2, event1.event_id],
191-
{"highlight_count": 1, "notify_count": 2},
191+
{"highlight_count": 1, "notify_count": 2, "unread_count": 2},
192+
)
193+
194+
self.persist(
195+
type="m.room.message",
196+
msgtype="m.text",
197+
body="world",
198+
push_actions=[(USER_ID_2, ["org.matrix.msc2625.mark_unread"])],
199+
)
200+
self.replicate()
201+
self.check(
202+
"get_unread_event_push_actions_by_room_for_user",
203+
[ROOM_ID, USER_ID_2, event1.event_id],
204+
{"highlight_count": 1, "notify_count": 2, "unread_count": 3},
192205
)
193206

194207
def test_get_rooms_for_user_with_stream_ordering(self):

0 commit comments

Comments
 (0)