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

Commit 8dff4a1

Browse files
authored
Re-implement unread counts (#7736)
1 parent 2184f61 commit 8dff4a1

File tree

11 files changed

+339
-18
lines changed

11 files changed

+339
-18
lines changed

changelog.d/7736.feature

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Add unread messages count to sync responses.

scripts/synapse_port_db

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@ logger = logging.getLogger("synapse_port_db")
6969

7070

7171
BOOLEAN_COLUMNS = {
72-
"events": ["processed", "outlier", "contains_url"],
72+
"events": ["processed", "outlier", "contains_url", "count_as_unread"],
7373
"rooms": ["is_public"],
7474
"event_edges": ["is_state"],
7575
"presence_list": ["accepted"],

synapse/handlers/sync.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,7 @@ class JoinedSyncResult:
103103
account_data = attr.ib(type=List[JsonDict])
104104
unread_notifications = attr.ib(type=JsonDict)
105105
summary = attr.ib(type=Optional[JsonDict])
106+
unread_count = attr.ib(type=int)
106107

107108
def __nonzero__(self) -> bool:
108109
"""Make the result appear empty if there are no updates. This is used
@@ -1886,6 +1887,10 @@ async def _generate_room_entry(
18861887

18871888
if room_builder.rtype == "joined":
18881889
unread_notifications = {} # type: Dict[str, str]
1890+
1891+
unread_count = await self.store.get_unread_message_count_for_user(
1892+
room_id, sync_config.user.to_string(),
1893+
)
18891894
room_sync = JoinedSyncResult(
18901895
room_id=room_id,
18911896
timeline=batch,
@@ -1894,6 +1899,7 @@ async def _generate_room_entry(
18941899
account_data=account_data_events,
18951900
unread_notifications=unread_notifications,
18961901
summary=summary,
1902+
unread_count=unread_count,
18971903
)
18981904

18991905
if room_sync or always_include:

synapse/push/push_tools.py

Lines changed: 4 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -21,22 +21,13 @@ async def get_badge_count(store, user_id):
2121
invites = await store.get_invited_rooms_for_local_user(user_id)
2222
joins = await store.get_rooms_for_user(user_id)
2323

24-
my_receipts_by_room = await store.get_receipts_for_user(user_id, "m.read")
25-
2624
badge = len(invites)
2725

2826
for room_id in joins:
29-
if room_id in my_receipts_by_room:
30-
last_unread_event_id = my_receipts_by_room[room_id]
31-
32-
notifs = await (
33-
store.get_unread_event_push_actions_by_room_for_user(
34-
room_id, user_id, last_unread_event_id
35-
)
36-
)
37-
# return one badge count per conversation, as count per
38-
# message is so noisy as to be almost useless
39-
badge += 1 if notifs["notify_count"] else 0
27+
unread_count = await store.get_unread_message_count_for_user(room_id, user_id)
28+
# return one badge count per conversation, as count per
29+
# message is so noisy as to be almost useless
30+
badge += 1 if unread_count else 0
4031
return badge
4132

4233

synapse/rest/client/v2_alpha/sync.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -426,6 +426,7 @@ def serialize(events):
426426
result["ephemeral"] = {"events": ephemeral_events}
427427
result["unread_notifications"] = room.unread_notifications
428428
result["summary"] = room.summary
429+
result["org.matrix.msc2654.unread_count"] = room.unread_count
429430

430431
return result
431432

synapse/storage/data_stores/main/cache.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -172,6 +172,7 @@ def _invalidate_caches_for_event(
172172

173173
self.get_latest_event_ids_in_room.invalidate((room_id,))
174174

175+
self.get_unread_message_count_for_user.invalidate_many((room_id,))
175176
self.get_unread_event_push_actions_by_room_for_user.invalidate_many((room_id,))
176177

177178
if not backfilled:

synapse/storage/data_stores/main/events.py

Lines changed: 47 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,47 @@
5353
["type", "origin_type", "origin_entity"],
5454
)
5555

56+
STATE_EVENT_TYPES_TO_MARK_UNREAD = {
57+
EventTypes.Topic,
58+
EventTypes.Name,
59+
EventTypes.RoomAvatar,
60+
EventTypes.Tombstone,
61+
}
62+
63+
64+
def should_count_as_unread(event: EventBase, context: EventContext) -> bool:
65+
# Exclude rejected and soft-failed events.
66+
if context.rejected or event.internal_metadata.is_soft_failed():
67+
return False
68+
69+
# Exclude notices.
70+
if (
71+
not event.is_state()
72+
and event.type == EventTypes.Message
73+
and event.content.get("msgtype") == "m.notice"
74+
):
75+
return False
76+
77+
# Exclude edits.
78+
relates_to = event.content.get("m.relates_to", {})
79+
if relates_to.get("rel_type") == RelationTypes.REPLACE:
80+
return False
81+
82+
# Mark events that have a non-empty string body as unread.
83+
body = event.content.get("body")
84+
if isinstance(body, str) and body:
85+
return True
86+
87+
# Mark some state events as unread.
88+
if event.is_state() and event.type in STATE_EVENT_TYPES_TO_MARK_UNREAD:
89+
return True
90+
91+
# Mark encrypted events as unread.
92+
if not event.is_state() and event.type == EventTypes.Encrypted:
93+
return True
94+
95+
return False
96+
5697

5798
def encode_json(json_object):
5899
"""
@@ -196,6 +237,10 @@ def _persist_events_and_state_updates(
196237

197238
event_counter.labels(event.type, origin_type, origin_entity).inc()
198239

240+
self.store.get_unread_message_count_for_user.invalidate_many(
241+
(event.room_id,),
242+
)
243+
199244
for room_id, new_state in current_state_for_room.items():
200245
self.store.get_current_state_ids.prefill((room_id,), new_state)
201246

@@ -817,8 +862,9 @@ def event_dict(event):
817862
"contains_url": (
818863
"url" in event.content and isinstance(event.content["url"], str)
819864
),
865+
"count_as_unread": should_count_as_unread(event, context),
820866
}
821-
for event, _ in events_and_contexts
867+
for event, context in events_and_contexts
822868
],
823869
)
824870

synapse/storage/data_stores/main/events_worker.py

Lines changed: 85 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,9 +41,15 @@
4141
from synapse.replication.tcp.streams.events import EventsStream
4242
from synapse.storage._base import SQLBaseStore, db_to_json, make_in_list_sql_clause
4343
from synapse.storage.database import Database
44+
from synapse.storage.types import Cursor
4445
from synapse.storage.util.id_generators import StreamIdGenerator
4546
from synapse.types import get_domain_from_id
46-
from synapse.util.caches.descriptors import Cache, cached, cachedInlineCallbacks
47+
from synapse.util.caches.descriptors import (
48+
Cache,
49+
_CacheContext,
50+
cached,
51+
cachedInlineCallbacks,
52+
)
4753
from synapse.util.iterutils import batch_iter
4854
from synapse.util.metrics import Measure
4955

@@ -1358,6 +1364,84 @@ def get_next_event_to_expire_txn(txn):
13581364
desc="get_next_event_to_expire", func=get_next_event_to_expire_txn
13591365
)
13601366

1367+
@cached(tree=True, cache_context=True)
1368+
async def get_unread_message_count_for_user(
1369+
self, room_id: str, user_id: str, cache_context: _CacheContext,
1370+
) -> int:
1371+
"""Retrieve the count of unread messages for the given room and user.
1372+
1373+
Args:
1374+
room_id: The ID of the room to count unread messages in.
1375+
user_id: The ID of the user to count unread messages for.
1376+
1377+
Returns:
1378+
The number of unread messages for the given user in the given room.
1379+
"""
1380+
with Measure(self._clock, "get_unread_message_count_for_user"):
1381+
last_read_event_id = await self.get_last_receipt_event_id_for_user(
1382+
user_id=user_id,
1383+
room_id=room_id,
1384+
receipt_type="m.read",
1385+
on_invalidate=cache_context.invalidate,
1386+
)
1387+
1388+
return await self.db.runInteraction(
1389+
"get_unread_message_count_for_user",
1390+
self._get_unread_message_count_for_user_txn,
1391+
user_id,
1392+
room_id,
1393+
last_read_event_id,
1394+
)
1395+
1396+
def _get_unread_message_count_for_user_txn(
1397+
self,
1398+
txn: Cursor,
1399+
user_id: str,
1400+
room_id: str,
1401+
last_read_event_id: Optional[str],
1402+
) -> int:
1403+
if last_read_event_id:
1404+
# Get the stream ordering for the last read event.
1405+
stream_ordering = self.db.simple_select_one_onecol_txn(
1406+
txn=txn,
1407+
table="events",
1408+
keyvalues={"room_id": room_id, "event_id": last_read_event_id},
1409+
retcol="stream_ordering",
1410+
)
1411+
else:
1412+
# If there's no read receipt for that room, it probably means the user hasn't
1413+
# opened it yet, in which case use the stream ID of their join event.
1414+
# We can't just set it to 0 otherwise messages from other local users from
1415+
# before this user joined will be counted as well.
1416+
txn.execute(
1417+
"""
1418+
SELECT stream_ordering FROM local_current_membership
1419+
LEFT JOIN events USING (event_id, room_id)
1420+
WHERE membership = 'join'
1421+
AND user_id = ?
1422+
AND room_id = ?
1423+
""",
1424+
(user_id, room_id),
1425+
)
1426+
row = txn.fetchone()
1427+
1428+
if row is None:
1429+
return 0
1430+
1431+
stream_ordering = row[0]
1432+
1433+
# Count the messages that qualify as unread after the stream ordering we've just
1434+
# retrieved.
1435+
sql = """
1436+
SELECT COUNT(*) FROM events
1437+
WHERE sender != ? AND room_id = ? AND stream_ordering > ? AND count_as_unread
1438+
"""
1439+
1440+
txn.execute(sql, (user_id, room_id, stream_ordering))
1441+
row = txn.fetchone()
1442+
1443+
return row[0] if row else 0
1444+
13611445

13621446
AllNewEventsResult = namedtuple(
13631447
"AllNewEventsResult",
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
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 a boolean value in the events table for whether the event should be counted in
17+
-- the unread_count property of sync responses.
18+
ALTER TABLE events ADD COLUMN count_as_unread BOOLEAN;

tests/rest/client/v1/utils.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,26 @@ def send_event(
143143

144144
return channel.json_body
145145

146+
def redact(self, room_id, event_id, txn_id=None, tok=None, expect_code=200):
147+
if txn_id is None:
148+
txn_id = "m%s" % (str(time.time()))
149+
150+
path = "/_matrix/client/r0/rooms/%s/redact/%s/%s" % (room_id, event_id, txn_id)
151+
if tok:
152+
path = path + "?access_token=%s" % tok
153+
154+
request, channel = make_request(
155+
self.hs.get_reactor(), "PUT", path, json.dumps({}).encode("utf8")
156+
)
157+
render(request, self.resource, self.hs.get_reactor())
158+
159+
assert int(channel.result["code"]) == expect_code, (
160+
"Expected: %d, got: %d, resp: %r"
161+
% (expect_code, int(channel.result["code"]), channel.result["body"])
162+
)
163+
164+
return channel.json_body
165+
146166
def _read_write_state(
147167
self,
148168
room_id: str,

0 commit comments

Comments
 (0)