Skip to content
GitLab
Menu
Projects
Groups
Snippets
Loading...
Help
Help
Support
Community forum
Keyboard shortcuts
?
Submit feedback
Contribute to GitLab
Sign in / Register
Toggle navigation
Menu
Open sidebar
elm
element-web
matrix-js-sdk
Commits
e5edf5bd
Commit
e5edf5bd
authored
2 years ago
by
Germain Souquet
Browse files
Options
Download
Email Patches
Plain Diff
Add e2ee support for notifications highlight
parent
66f7cdbf
Changes
3
Hide whitespace changes
Inline
Side-by-side
Showing
3 changed files
with
116 additions
and
194 deletions
+116
-194
src/client.ts
src/client.ts
+64
-31
src/models/room.ts
src/models/room.ts
+0
-117
src/models/timeline-receipts.ts
src/models/timeline-receipts.ts
+52
-46
No files found.
src/client.ts
View file @
e5edf5bd
...
...
@@ -1048,37 +1048,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
// actions for themselves, so we have to kinda help them out when they are encrypted.
// We do this so that push rules are correctly executed on events in their decrypted
// state, such as highlights when the user's name is mentioned.
this
.
on
(
MatrixEventEvent
.
Decrypted
,
(
event
)
=>
{
const
oldActions
=
event
.
getPushActions
();
const
actions
=
this
.
getPushActionsForEvent
(
event
,
true
);
const
room
=
this
.
getRoom
(
event
.
getRoomId
());
if
(
!
room
)
return
;
const
currentCount
=
room
.
getUnreadNotificationCount
(
NotificationCountType
.
Highlight
);
// Ensure the unread counts are kept up to date if the event is encrypted
// We also want to make sure that the notification count goes up if we already
// have encrypted events to avoid other code from resetting 'highlight' to zero.
const
oldHighlight
=
!!
oldActions
?.
tweaks
?.
highlight
;
const
newHighlight
=
!!
actions
?.
tweaks
?.
highlight
;
if
(
oldHighlight
!==
newHighlight
||
currentCount
>
0
)
{
// TODO: Handle mentions received while the client is offline
// See also https://github.com/vector-im/element-web/issues/9069
if
(
!
room
.
hasUserReadEvent
(
this
.
getUserId
(),
event
.
getId
()))
{
let
newCount
=
currentCount
;
if
(
newHighlight
&&
!
oldHighlight
)
newCount
++
;
if
(
!
newHighlight
&&
oldHighlight
)
newCount
--
;
room
.
setUnreadNotificationCount
(
NotificationCountType
.
Highlight
,
newCount
);
// Fix 'Mentions Only' rooms from not having the right badge count
const
totalCount
=
room
.
getUnreadNotificationCount
(
NotificationCountType
.
Total
);
if
(
totalCount
<
newCount
)
{
room
.
setUnreadNotificationCount
(
NotificationCountType
.
Total
,
newCount
);
}
}
}
});
this
.
on
(
MatrixEventEvent
.
Decrypted
,
this
.
recalculateNotifications
);
// Like above, we have to listen for read receipts from ourselves in order to
// correctly handle notification counts on encrypted rooms.
...
...
@@ -1130,6 +1100,69 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
});
}
private
recalculateNotifications
(
event
:
MatrixEvent
):
void
{
const
oldActions
=
event
.
getPushActions
();
const
actions
=
this
.
getPushActionsForEvent
(
event
,
true
);
const
room
=
this
.
getRoom
(
event
.
getRoomId
());
if
(
!
room
)
return
;
const
isThreadEvent
=
!!
event
.
threadRootId
;
let
currentCount
;
if
(
isThreadEvent
)
{
currentCount
=
room
.
getThreadUnreadNotificationCount
(
event
.
threadRootId
,
NotificationCountType
.
Highlight
);
}
else
{
currentCount
=
room
.
getUnreadNotificationCount
(
NotificationCountType
.
Highlight
);
}
// Ensure the unread counts are kept up to date if the event is encrypted
// We also want to make sure that the notification count goes up if we already
// have encrypted events to avoid other code from resetting 'highlight' to zero.
const
oldHighlight
=
!!
oldActions
?.
tweaks
?.
highlight
;
const
newHighlight
=
!!
actions
?.
tweaks
?.
highlight
;
if
(
oldHighlight
!==
newHighlight
||
currentCount
>
0
)
{
// TODO: Handle mentions received while the client is offline
// See also https://github.com/vector-im/element-web/issues/9069
const
hasReadEvent
=
isThreadEvent
?
room
.
getThread
(
event
.
threadRootId
).
hasUserReadEvent
(
this
.
getUserId
(),
event
.
getId
())
:
room
.
hasUserReadEvent
(
this
.
getUserId
(),
event
.
getId
());
if
(
!
hasReadEvent
)
{
let
newCount
=
currentCount
;
if
(
newHighlight
&&
!
oldHighlight
)
newCount
++
;
if
(
!
newHighlight
&&
oldHighlight
)
newCount
--
;
if
(
isThreadEvent
)
{
room
.
setThreadUnreadNotificationCount
(
event
.
threadRootId
,
NotificationCountType
.
Highlight
,
newCount
,
);
}
else
{
room
.
setUnreadNotificationCount
(
NotificationCountType
.
Highlight
,
newCount
);
}
// Fix 'Mentions Only' rooms from not having the right badge count
const
totalCount
=
isThreadEvent
?
room
.
getThreadUnreadNotificationCount
(
event
.
threadRootId
,
NotificationCountType
.
Total
)
:
room
.
getUnreadNotificationCount
(
NotificationCountType
.
Total
);
if
(
totalCount
<
newCount
)
{
if
(
isThreadEvent
)
{
room
.
setThreadUnreadNotificationCount
(
event
.
threadRootId
,
NotificationCountType
.
Total
,
newCount
,
);
}
else
{
room
.
setUnreadNotificationCount
(
NotificationCountType
.
Total
,
newCount
);
}
}
}
}
}
/**
* High level helper method to begin syncing and poll for new events. To listen for these
* events, add a listener for {@link module:client~MatrixClient#event:"event"}
...
...
This diff is collapsed.
Click to expand it.
src/models/room.ts
View file @
e5edf5bd
...
...
@@ -2483,123 +2483,6 @@ export class Room extends TimelineReceipts<EmittedEvents, RoomEventHandlerMap> {
}
}
/**
* Get a list of user IDs who have <b>read up to</b> the given event.
* @param {MatrixEvent} event the event to get read receipts for.
* @return {String[]} A list of user IDs.
*/
public
getUsersReadUpTo
(
event
:
MatrixEvent
):
string
[]
{
return
this
.
getReceiptsForEvent
(
event
).
filter
(
function
(
receipt
)
{
return
utils
.
isSupportedReceiptType
(
receipt
.
type
);
}).
map
(
function
(
receipt
)
{
return
receipt
.
userId
;
});
}
/**
* Get the ID of the event that a given user has read up to, or null if we
* have received no read receipts from them.
* @param {String} userId The user ID to get read receipt event ID for
* @param {Boolean} ignoreSynthesized If true, return only receipts that have been
* sent by the server, not implicit ones generated
* by the JS SDK.
* @return {String} ID of the latest event that the given user has read, or null.
*/
public
getEventReadUpTo
(
userId
:
string
,
ignoreSynthesized
=
false
):
string
|
null
{
// XXX: This is very very ugly and I hope I won't have to ever add a new
// receipt type here again. IMHO this should be done by the server in
// some more intelligent manner or the client should just use timestamps
const
timelineSet
=
this
.
getUnfilteredTimelineSet
();
const
publicReadReceipt
=
this
.
getReadReceiptForUserId
(
userId
,
ignoreSynthesized
,
ReceiptType
.
Read
,
);
const
privateReadReceipt
=
this
.
getReadReceiptForUserId
(
userId
,
ignoreSynthesized
,
ReceiptType
.
ReadPrivate
,
);
const
unstablePrivateReadReceipt
=
this
.
getReadReceiptForUserId
(
userId
,
ignoreSynthesized
,
ReceiptType
.
UnstableReadPrivate
,
);
// If we have all, compare them
if
(
publicReadReceipt
?.
eventId
&&
privateReadReceipt
?.
eventId
&&
unstablePrivateReadReceipt
?.
eventId
)
{
const
comparison1
=
timelineSet
.
compareEventOrdering
(
publicReadReceipt
.
eventId
,
privateReadReceipt
.
eventId
,
);
const
comparison2
=
timelineSet
.
compareEventOrdering
(
publicReadReceipt
.
eventId
,
unstablePrivateReadReceipt
.
eventId
,
);
const
comparison3
=
timelineSet
.
compareEventOrdering
(
privateReadReceipt
.
eventId
,
unstablePrivateReadReceipt
.
eventId
,
);
if
(
comparison1
&&
comparison2
&&
comparison3
)
{
return
(
comparison1
>
0
)
?
((
comparison2
>
0
)
?
publicReadReceipt
.
eventId
:
unstablePrivateReadReceipt
.
eventId
)
:
((
comparison3
>
0
)
?
privateReadReceipt
.
eventId
:
unstablePrivateReadReceipt
.
eventId
);
}
}
let
latest
=
privateReadReceipt
;
[
unstablePrivateReadReceipt
,
publicReadReceipt
].
forEach
((
receipt
)
=>
{
if
(
receipt
?.
data
?.
ts
>
latest
?.
data
?.
ts
)
{
latest
=
receipt
;
}
});
if
(
latest
?.
eventId
)
return
latest
?.
eventId
;
// The more less likely it is for a read receipt to drift out of date
// the bigger is its precedence
return
(
privateReadReceipt
?.
eventId
??
unstablePrivateReadReceipt
?.
eventId
??
publicReadReceipt
?.
eventId
??
null
);
}
/**
* Determines if the given user has read a particular event ID with the known
* history of the room. This is not a definitive check as it relies only on
* what is available to the room at the time of execution.
* @param {String} userId The user ID to check the read state of.
* @param {String} eventId The event ID to check if the user read.
* @returns {Boolean} True if the user has read the event, false otherwise.
*/
public
hasUserReadEvent
(
userId
:
string
,
eventId
:
string
):
boolean
{
const
readUpToId
=
this
.
getEventReadUpTo
(
userId
,
false
);
if
(
readUpToId
===
eventId
)
return
true
;
if
(
this
.
timeline
.
length
&&
this
.
timeline
[
this
.
timeline
.
length
-
1
].
getSender
()
&&
this
.
timeline
[
this
.
timeline
.
length
-
1
].
getSender
()
===
userId
)
{
// It doesn't matter where the event is in the timeline, the user has read
// it because they've sent the latest event.
return
true
;
}
for
(
let
i
=
this
.
timeline
.
length
-
1
;
i
>=
0
;
--
i
)
{
const
ev
=
this
.
timeline
[
i
];
// If we encounter the target event first, the user hasn't read it
// however if we encounter the readUpToId first then the user has read
// it. These rules apply because we're iterating bottom-up.
if
(
ev
.
getId
()
===
eventId
)
return
false
;
if
(
ev
.
getId
()
===
readUpToId
)
return
true
;
}
// We don't know if the user has read it, so assume not.
return
false
;
}
/**
* Add a receipt event to the room.
* @param {MatrixEvent} event The m.receipt event.
...
...
This diff is collapsed.
Click to expand it.
src/models/timeline-receipts.ts
View file @
e5edf5bd
...
...
@@ -101,7 +101,19 @@ export abstract class TimelineReceipts<
private
receiptCacheByEventId
:
ReceiptCache
=
{};
// { event_id: ICachedReceipt[] }
public
abstract
getUnfilteredTimelineSet
():
EventTimelineSet
;
public
abstract
timeline
:
MatrixEvent
[];
/**
* Get a list of user IDs who have <b>read up to</b> the given event.
* @param {MatrixEvent} event the event to get read receipts for.
* @return {String[]} A list of user IDs.
*/
public
getUsersReadUpTo
(
event
:
MatrixEvent
):
string
[]
{
return
this
.
getReceiptsForEvent
(
event
).
filter
(
function
(
receipt
)
{
return
utils
.
isSupportedReceiptType
(
receipt
.
type
);
}).
map
(
function
(
receipt
)
{
return
receipt
.
userId
;
});
}
/**
* Gets the latest receipt for a given user in the room
...
...
@@ -191,6 +203,40 @@ export abstract class TimelineReceipts<
);
}
/**
* Determines if the given user has read a particular event ID with the known
* history of the room. This is not a definitive check as it relies only on
* what is available to the room at the time of execution.
* @param {String} userId The user ID to check the read state of.
* @param {String} eventId The event ID to check if the user read.
* @returns {Boolean} True if the user has read the event, false otherwise.
*/
public
hasUserReadEvent
(
userId
:
string
,
eventId
:
string
):
boolean
{
const
readUpToId
=
this
.
getEventReadUpTo
(
userId
,
false
);
if
(
readUpToId
===
eventId
)
return
true
;
if
(
this
.
timeline
.
length
&&
this
.
timeline
[
this
.
timeline
.
length
-
1
].
getSender
()
&&
this
.
timeline
[
this
.
timeline
.
length
-
1
].
getSender
()
===
userId
)
{
// It doesn't matter where the event is in the timeline, the user has read
// it because they've sent the latest event.
return
true
;
}
for
(
let
i
=
this
.
timeline
.
length
-
1
;
i
>=
0
;
--
i
)
{
const
ev
=
this
.
timeline
[
i
];
// If we encounter the target event first, the user hasn't read it
// however if we encounter the readUpToId first then the user has read
// it. These rules apply because we're iterating bottom-up.
if
(
ev
.
getId
()
===
eventId
)
return
false
;
if
(
ev
.
getId
()
===
readUpToId
)
return
true
;
}
// We don't know if the user has read it, so assume not.
return
false
;
}
public
addReceiptToStructure
(
eventId
:
string
,
receiptType
:
ReceiptType
,
...
...
@@ -308,50 +354,10 @@ export abstract class TimelineReceipts<
this
.
addReceipt
(
synthesizeReceipt
(
userId
,
e
,
receiptType
),
true
);
}
/**
* Get a list of user IDs who have <b>read up to</b> the given event.
* @param {MatrixEvent} event the event to get read receipts for.
* @return {String[]} A list of user IDs.
*/
public
getUsersReadUpTo
(
event
:
MatrixEvent
):
string
[]
{
return
this
.
getReceiptsForEvent
(
event
).
filter
(
function
(
receipt
)
{
return
utils
.
isSupportedReceiptType
(
receipt
.
type
);
}).
map
(
function
(
receipt
)
{
return
receipt
.
userId
;
});
}
/**
* Determines if the given user has read a particular event ID with the known
* history of the room. This is not a definitive check as it relies only on
* what is available to the room at the time of execution.
* @param {String} userId The user ID to check the read state of.
* @param {String} eventId The event ID to check if the user read.
* @returns {Boolean} True if the user has read the event, false otherwise.
*/
public
hasUserReadEvent
(
userId
:
string
,
eventId
:
string
):
boolean
{
const
readUpToId
=
this
.
getEventReadUpTo
(
userId
,
false
);
if
(
readUpToId
===
eventId
)
return
true
;
if
(
this
.
timeline
.
length
&&
this
.
timeline
[
this
.
timeline
.
length
-
1
].
getSender
()
&&
this
.
timeline
[
this
.
timeline
.
length
-
1
].
getSender
()
===
userId
)
{
// It doesn't matter where the event is in the timeline, the user has read
// it because they've sent the latest event.
return
true
;
}
for
(
let
i
=
this
.
timeline
.
length
-
1
;
i
>=
0
;
--
i
)
{
const
ev
=
this
.
timeline
[
i
];
// If we encounter the target event first, the user hasn't read it
// however if we encounter the readUpToId first then the user has read
// it. These rules apply because we're iterating bottom-up.
if
(
ev
.
getId
()
===
eventId
)
return
false
;
if
(
ev
.
getId
()
===
readUpToId
)
return
true
;
}
// We don't know if the user has read it, so assume not.
return
false
;
public
get
timeline
():
MatrixEvent
[]
{
return
this
.
getUnfilteredTimelineSet
()
.
getLiveTimeline
()
.
getEvents
();
}
}
This diff is collapsed.
Click to expand it.
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
.
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment