Merge branch 'develop' into matthew/knock-push-notifications-pr

This commit is contained in:
Benoit Marty
2026-08-11 17:21:34 +02:00
committed by GitHub
2069 changed files with 16894 additions and 7537 deletions
@@ -57,6 +57,7 @@ import io.element.android.libraries.push.impl.db.PushRequest
import io.element.android.libraries.push.impl.notifications.model.InviteNotifiableEvent
import io.element.android.libraries.push.impl.notifications.model.NotifiableMessageEvent
import io.element.android.libraries.push.impl.notifications.model.ResolvedPushEvent
import io.element.android.libraries.sessionstorage.api.SessionStore
import io.element.android.libraries.ui.strings.CommonStrings
import io.element.android.services.toolbox.api.strings.StringProvider
import timber.log.Timber
@@ -95,6 +96,7 @@ class DefaultNotifiableEventResolver(
private val callNotificationEventResolver: CallNotificationEventResolver,
private val fallbackNotificationFactory: FallbackNotificationFactory,
private val featureFlagService: FeatureFlagService,
private val sessionStore: SessionStore,
) : NotifiableEventResolver {
override suspend fun resolveEvents(
sessionId: SessionId,
@@ -118,10 +120,15 @@ class DefaultNotifiableEventResolver(
return Result.failure(exception ?: NotificationResolverException.UnknownError("Unknown error while fetching notifications"))
}
val otherSessionUserIds = sessionStore.getAllSessions()
.map { UserId(it.userId) }
.minus(sessionId)
.toSet()
// The null check is done above
val notificationDataMap = notificationsResult.getOrNull()!!.mapValues { (_, notificationData) ->
notificationData.flatMap { data ->
data.asNotifiableEvent(client, sessionId)
data.asNotifiableEvent(client, sessionId, otherSessionUserIds)
}
}
@@ -137,10 +144,23 @@ class DefaultNotifiableEventResolver(
)
}
private fun NotificationContent.senderIdOrNull(): UserId? = when (this) {
is NotificationContent.MessageLike.RoomMessage -> senderId
is NotificationContent.MessageLike.Poll -> senderId
is NotificationContent.MessageLike.CallInvite -> senderId
is NotificationContent.MessageLike.RtcNotification -> senderId
is NotificationContent.Invite -> senderId
else -> null
}
private suspend fun NotificationData.asNotifiableEvent(
client: MatrixClient,
userId: SessionId,
otherSessionUserIds: Set<UserId>,
): Result<ResolvedPushEvent> = runCatchingExceptions {
if (content.senderIdOrNull() in otherSessionUserIds) {
throw NotificationResolverException.EventFilteredOut
}
when (val content = this.content) {
is NotificationContent.MessageLike.RoomMessage -> {
val showMediaPreview = client.mediaPreviewService.getMediaPreviewValue().isPreviewEnabled(roomJoinRule)
@@ -285,8 +305,8 @@ class DefaultNotifiableEventResolver(
// Note: this case will be handled below
val redactedEventId = content.redactedEventId
if (redactedEventId == null) {
Timber.tag(loggerTag.value).d("redactedEventId is null.")
throw NotificationResolverException.UnknownError("redactedEventId is null")
Timber.tag(loggerTag.value).w("Ignoring redaction notification with no redactedEventId.")
throw NotificationResolverException.EventFilteredOut
} else {
ResolvedPushEvent.Redaction(
sessionId = userId,
@@ -323,6 +343,28 @@ class DefaultNotifiableEventResolver(
throw NotificationResolverException.EventFilteredOut
}
}
NotificationContent.MessageLike.Beacon -> {
Timber.tag(loggerTag.value).d("Ignoring notification for beacon")
throw NotificationResolverException.EventFilteredOut
}
is NotificationContent.StateEvent.BeaconInfo -> {
val notifiableEventMessage = buildNotifiableMessageEvent(
sessionId = userId,
senderId = content.senderId,
roomId = roomId,
eventId = eventId,
noisy = isNoisy,
timestamp = this.timestamp,
senderDisambiguatedDisplayName = getDisambiguatedDisplayName(content.senderId),
body = stringProvider.getString(R.string.notification_live_location_started_body),
imageUriString = null,
roomName = roomDisplayName,
roomIsDm = isDm,
roomAvatarPath = roomAvatarUrl,
senderAvatarPath = senderAvatarUrl,
)
ResolvedPushEvent.Event(notifiableEventMessage)
}
NotificationContent.StateEvent.PolicyRuleRoom,
NotificationContent.StateEvent.PolicyRuleServer,
NotificationContent.StateEvent.PolicyRuleUser,
@@ -105,13 +105,21 @@ class NotificationBroadcastReceiverHandler(
@Suppress("unused")
private fun handleMarkAsRead(sessionId: SessionId, roomId: RoomId, threadId: ThreadId?) = appCoroutineScope.launch {
val client = matrixClientProvider.getOrRestore(sessionId).getOrNull() ?: return@launch
val isSendPublicReadReceiptsEnabled = sessionPreferencesStore.get(sessionId, this).isSendPublicReadReceiptsEnabled().first()
val isSendPublicReadReceiptsEnabled = sessionPreferencesStore.get(
sessionId = sessionId,
// Make sure `sessionCoroutineScope` is used here, otherwise we'll have several instances using different coroutines,
// and it will not work as expected.
sessionCoroutineScope = client.sessionCoroutineScope,
).isSendPublicReadReceiptsEnabled().first()
val receiptType = if (isSendPublicReadReceiptsEnabled) {
ReceiptType.READ
} else {
ReceiptType.READ_PRIVATE
}
val room = client.getJoinedRoom(roomId) ?: return@launch
val (room, needsDestroy) = activeRoomsHolder.getActiveRoomMatching(sessionId, roomId)?.let { it to false }
?: client.getJoinedRoom(roomId)?.let { it to true }
?: return@launch
val timeline = if (threadId != null) {
room.createTimeline(CreateTimelineParams.Threaded(threadId)).getOrNull()
} else {
@@ -131,6 +139,10 @@ class NotificationBroadcastReceiverHandler(
if (timeline?.mode != Timeline.Mode.Live) {
timeline?.close()
}
if (needsDestroy) {
room.destroy()
}
}
private fun handleSmartReply(
@@ -30,6 +30,7 @@
</plurals>
<string name="notification_invite_body">"Kutse osalema vestluses"</string>
<string name="notification_invite_body_with_sender">"%1$s saatus sulle vestluskutse"</string>
<string name="notification_live_location_started_body">"Hakkas jagama oma asukohta reaalajas"</string>
<string name="notification_mentioned_you_body">"Mainis sind: %1$s"</string>
<string name="notification_new_messages">"Uued sõnumid"</string>
<plurals name="notification_new_messages_for_room">
@@ -21,6 +21,10 @@
<item quantity="one">"Imate%d novu poruku."</item>
<item quantity="other">"Imate %d novi poruka."</item>
</plurals>
<plurals name="notification_gallery_body">
<item quantity="one">"Dijeljeni privitak %1$d"</item>
<item quantity="other">"Dijeljeni prilozi %1$d"</item>
</plurals>
<string name="notification_incoming_audio_call">"📞 Dolazni poziv"</string>
<string name="notification_incoming_call">"📹 Dolazni poziv"</string>
<string name="notification_inline_reply_failed">"** Slanje nije uspjelo – otvorite sobu"</string>
@@ -33,6 +37,7 @@
</plurals>
<string name="notification_invite_body">"Pozvao/la te na razgovor"</string>
<string name="notification_invite_body_with_sender">"%1$s pozvao/la te na razgovor"</string>
<string name="notification_live_location_started_body">"Počeli su dijeliti svoju lokaciju uživo"</string>
<string name="notification_mentioned_you_body">"Korisnik vas je spomenuo: %1$s"</string>
<string name="notification_new_messages">"Nove poruke"</string>
<plurals name="notification_new_messages_for_room">
@@ -30,6 +30,7 @@
</plurals>
<string name="notification_invite_body">"Meghívta, hogy csevegjen"</string>
<string name="notification_invite_body_with_sender">"%1$s meghívta egy csevegésre"</string>
<string name="notification_live_location_started_body">"Elkezdték megosztani az élő helyzetüket"</string>
<string name="notification_mentioned_you_body">"Megemlítette Önt: %1$s"</string>
<string name="notification_new_messages">"Új üzenetek"</string>
<plurals name="notification_new_messages_for_room">
@@ -22,6 +22,11 @@
<item quantity="few">"Masz %d nowe wiadomości."</item>
<item quantity="many">"Masz %d nowych wiadomości."</item>
</plurals>
<plurals name="notification_gallery_body">
<item quantity="one">"Udostępnił %1$d załącznik"</item>
<item quantity="few">"Udostępnił %1$d załączniki"</item>
<item quantity="many">"Udostępnił %1$d załączników"</item>
</plurals>
<string name="notification_incoming_audio_call">"📞 Połączenie przychodzące"</string>
<string name="notification_incoming_call">"📹 Połączenie przychodzące"</string>
<string name="notification_inline_reply_failed">"** Nie udało się wysłać - proszę otworzyć pokój"</string>
@@ -34,6 +39,7 @@
</plurals>
<string name="notification_invite_body">"Zaprosił Cię do czatu"</string>
<string name="notification_invite_body_with_sender">"%1$s zaprosił Cię do czatu"</string>
<string name="notification_live_location_started_body">"Rozpoczął udostępnianie swojej lokalizacji"</string>
<string name="notification_mentioned_you_body">"Wspomniano o Tobie: %1$s"</string>
<string name="notification_new_messages">"Nowe wiadomości"</string>
<plurals name="notification_new_messages_for_room">
@@ -20,6 +20,11 @@
<item quantity="few">"Aveți %d mesaje noi."</item>
<item quantity="other">"Aveți %d mesaje noi."</item>
</plurals>
<plurals name="notification_gallery_body">
<item quantity="one">"%1$d atașament partajat"</item>
<item quantity="few">"%1$d atașamente partajate"</item>
<item quantity="other">"%1$d atașamente partajate"</item>
</plurals>
<string name="notification_incoming_audio_call">"📞 Apel primit"</string>
<string name="notification_incoming_call">"Apel primit"</string>
<string name="notification_inline_reply_failed">"** Trimiterea eșuată - vă rugăm să deschideți camera"</string>
@@ -31,6 +36,7 @@
</plurals>
<string name="notification_invite_body">"V-a invitat la o discuție"</string>
<string name="notification_invite_body_with_sender">"%1$s v-a invitat să discutați"</string>
<string name="notification_live_location_started_body">"A început să își partajeze locația live"</string>
<string name="notification_mentioned_you_body">"%1$s v-a menționat"</string>
<string name="notification_new_messages">"Mesaje noi"</string>
<plurals name="notification_new_messages_for_room">
@@ -22,6 +22,11 @@
<item quantity="few">"У вас %d нових повідомлення."</item>
<item quantity="many">"У вас %d нових повідомлень."</item>
</plurals>
<plurals name="notification_gallery_body">
<item quantity="one">"Поширено %1$d вкладення"</item>
<item quantity="few">"Поширено %1$d вкладення"</item>
<item quantity="many">"Поширено %1$d вкладень"</item>
</plurals>
<string name="notification_incoming_audio_call">"📞 Вхідний виклик"</string>
<string name="notification_incoming_call">"📹 Вхідний виклик"</string>
<string name="notification_inline_reply_failed">"** Не вдалося надіслати - відкрийте кімнату"</string>
@@ -34,6 +39,7 @@
</plurals>
<string name="notification_invite_body">"Запрошує вас до бесіди"</string>
<string name="notification_invite_body_with_sender">"%1$s запрошує вас до бесіди"</string>
<string name="notification_live_location_started_body">"Починає ділитися своїм поточним місцем розташування"</string>
<string name="notification_mentioned_you_body">"Вас згадує: %1$s"</string>
<string name="notification_new_messages">"Нові повідомлення"</string>
<plurals name="notification_new_messages_for_room">
@@ -16,6 +16,9 @@
<plurals name="notification_fallback_n_content">
<item quantity="other">"您有 %d 則新訊息。"</item>
</plurals>
<plurals name="notification_gallery_body">
<item quantity="other">"已分享 %1$d 個附件"</item>
</plurals>
<string name="notification_incoming_audio_call">"📞 來電"</string>
<string name="notification_incoming_call">"📹 來電"</string>
<string name="notification_inline_reply_failed">"** 無法傳送,請開啟聊天室"</string>
@@ -26,6 +29,7 @@
</plurals>
<string name="notification_invite_body">"邀請您聊天"</string>
<string name="notification_invite_body_with_sender">"%1$s 邀請您加入聊天"</string>
<string name="notification_live_location_started_body">"已開始分享他們的即時位置"</string>
<string name="notification_mentioned_you_body">"提及您:%1$s"</string>
<string name="notification_new_messages">"新訊息"</string>
<plurals name="notification_new_messages_for_room">
@@ -16,6 +16,9 @@
<plurals name="notification_fallback_n_content">
<item quantity="other">"你有 %d 个新消息。"</item>
</plurals>
<plurals name="notification_gallery_body">
<item quantity="other">"已共享 %1$d 个附件"</item>
</plurals>
<string name="notification_incoming_audio_call">"📞 来电"</string>
<string name="notification_incoming_call">"📹 来电"</string>
<string name="notification_inline_reply_failed">"** 无法发送——请打开房间"</string>
@@ -26,6 +29,7 @@
</plurals>
<string name="notification_invite_body">"已邀请你聊天"</string>
<string name="notification_invite_body_with_sender">"%1$s 已邀请你聊天"</string>
<string name="notification_live_location_started_body">"已开始共享其实时位置"</string>
<string name="notification_mentioned_you_body">"提到了你:%1$s"</string>
<string name="notification_new_messages">"新消息"</string>
<plurals name="notification_new_messages_for_room">
@@ -19,6 +19,10 @@
<item quantity="one">"You have %d new message."</item>
<item quantity="other">"You have %d new messages."</item>
</plurals>
<plurals name="notification_gallery_body">
<item quantity="one">"Shared %1$d attachment"</item>
<item quantity="other">"Shared %1$d attachments"</item>
</plurals>
<string name="notification_incoming_audio_call">"📞 Incoming call"</string>
<string name="notification_incoming_call">"📹 Incoming call"</string>
<string name="notification_inline_reply_failed">"** Failed to send - please open room"</string>
@@ -30,6 +34,7 @@
</plurals>
<string name="notification_invite_body">"Invited you to chat"</string>
<string name="notification_invite_body_with_sender">"%1$s invited you to chat"</string>
<string name="notification_live_location_started_body">"Started sharing their live location"</string>
<string name="notification_mentioned_you_body">"Mentioned you: %1$s"</string>
<string name="notification_new_messages">"New Messages"</string>
<plurals name="notification_new_messages_for_room">
@@ -39,6 +39,7 @@ import io.element.android.libraries.matrix.test.A_REDACTION_REASON
import io.element.android.libraries.matrix.test.A_ROOM_ID
import io.element.android.libraries.matrix.test.A_ROOM_NAME
import io.element.android.libraries.matrix.test.A_SESSION_ID
import io.element.android.libraries.matrix.test.A_SESSION_ID_2
import io.element.android.libraries.matrix.test.A_SPACE_NAME
import io.element.android.libraries.matrix.test.A_TIMESTAMP
import io.element.android.libraries.matrix.test.A_USER_ID_2
@@ -56,6 +57,9 @@ import io.element.android.libraries.push.impl.notifications.model.FallbackNotifi
import io.element.android.libraries.push.impl.notifications.model.InviteNotifiableEvent
import io.element.android.libraries.push.impl.notifications.model.NotifiableMessageEvent
import io.element.android.libraries.push.impl.notifications.model.ResolvedPushEvent
import io.element.android.libraries.sessionstorage.api.SessionStore
import io.element.android.libraries.sessionstorage.test.InMemorySessionStore
import io.element.android.libraries.sessionstorage.test.aSessionData
import io.element.android.services.toolbox.impl.strings.AndroidStringProvider
import io.element.android.services.toolbox.test.systemclock.A_FAKE_TIMESTAMP
import io.element.android.services.toolbox.test.systemclock.FakeSystemClock
@@ -416,6 +420,27 @@ class DefaultNotifiableEventResolverTest : RobolectricTest() {
assertThat(result.getEvent(request)).isEqualTo(Result.success(expectedResult))
}
@Test
fun `resolve live location share start`() = runTest {
val sut = createDefaultNotifiableEventResolver(
notificationResult = Result.success(
mapOf(
AN_EVENT_ID to Result.success(aNotificationData(
content = NotificationContent.StateEvent.BeaconInfo(
senderId = A_USER_ID_2,
),
))
)
)
)
val request = aPushRequest(A_SESSION_ID, A_ROOM_ID, AN_EVENT_ID, "firebase")
val result = sut.resolveEvents(A_SESSION_ID, listOf(request))
val expectedResult = ResolvedPushEvent.Event(
aNotifiableMessageEvent(body = "Started sharing their live location")
)
assertThat(result.getEvent(request)).isEqualTo(Result.success(expectedResult))
}
@Test
fun `resolve RoomMemberContent invite room`() = runTest {
val sut = createDefaultNotifiableEventResolver(
@@ -819,7 +844,7 @@ class DefaultNotifiableEventResolverTest : RobolectricTest() {
}
@Test
fun `resolve RoomRedaction with null redactedEventId should return null`() = runTest {
fun `resolve RoomRedaction with null redactedEventId is filtered out`() = runTest {
val sut = createDefaultNotifiableEventResolver(
notificationResult = Result.success(
mapOf(
@@ -835,6 +860,112 @@ class DefaultNotifiableEventResolverTest : RobolectricTest() {
val request = aPushRequest(A_SESSION_ID, A_ROOM_ID, AN_EVENT_ID, "firebase")
val result = sut.resolveEvents(A_SESSION_ID, listOf(request))
assertThat(result.getEvent(request)?.getOrNull()).isNull()
assertThat(result.getEvent(request)?.exceptionOrNull()).isEqualTo(NotificationResolverException.EventFilteredOut)
}
@Test
fun `resolve RoomMessage sent by another session on the same device is filtered out`() = runTest {
val sut = createDefaultNotifiableEventResolver(
notificationResult = Result.success(
mapOf(
AN_EVENT_ID to Result.success(
aNotificationData(
content = NotificationContent.MessageLike.RoomMessage(
senderId = A_SESSION_ID_2,
messageType = TextMessageType("Hello world", null),
)
)
)
)
),
sessionStore = InMemorySessionStore(
listOf(
aSessionData(sessionId = A_SESSION_ID.value),
aSessionData(sessionId = A_SESSION_ID_2.value),
)
),
)
val request = aPushRequest(A_SESSION_ID, A_ROOM_ID, AN_EVENT_ID, "firebase")
val result = sut.resolveEvents(A_SESSION_ID, listOf(request))
assertThat(result.getEvent(request)?.exceptionOrNull()).isEqualTo(NotificationResolverException.EventFilteredOut)
}
@Test
fun `resolve RtcNotification sent by another session on the same device is filtered out`() = runTest {
val sut = createDefaultNotifiableEventResolver(
notificationResult = Result.success(
mapOf(
AN_EVENT_ID to Result.success(
aNotificationData(
content = NotificationContent.MessageLike.RtcNotification(
senderId = A_SESSION_ID_2,
type = RtcNotificationType.RING,
callIntent = CallIntent.AUDIO,
expirationTimestampMillis = 0,
)
)
)
)
),
sessionStore = InMemorySessionStore(
listOf(
aSessionData(sessionId = A_SESSION_ID.value),
aSessionData(sessionId = A_SESSION_ID_2.value),
)
),
)
val request = aPushRequest(A_SESSION_ID, A_ROOM_ID, AN_EVENT_ID, "firebase")
val result = sut.resolveEvents(A_SESSION_ID, listOf(request))
assertThat(result.getEvent(request)?.exceptionOrNull()).isEqualTo(NotificationResolverException.EventFilteredOut)
}
@Test
fun `resolve RoomMessage sent by a user who is not logged in is not filtered out`() = runTest {
val sut = createDefaultNotifiableEventResolver(
notificationResult = Result.success(
mapOf(
AN_EVENT_ID to Result.success(
aNotificationData(
content = NotificationContent.MessageLike.RoomMessage(
senderId = A_SESSION_ID_2,
messageType = TextMessageType("Hello world", null),
)
)
)
)
),
sessionStore = InMemorySessionStore(listOf(aSessionData(sessionId = A_SESSION_ID.value))),
)
val request = aPushRequest(A_SESSION_ID, A_ROOM_ID, AN_EVENT_ID, "firebase")
val result = sut.resolveEvents(A_SESSION_ID, listOf(request))
assertThat(result.getEvent(request)?.isSuccess).isTrue()
}
@Test
fun `resolve RoomMessage sent by the receiving session itself is not filtered out`() = runTest {
val sut = createDefaultNotifiableEventResolver(
notificationResult = Result.success(
mapOf(
AN_EVENT_ID to Result.success(
aNotificationData(
content = NotificationContent.MessageLike.RoomMessage(
senderId = A_SESSION_ID,
messageType = TextMessageType("Hello world", null),
)
)
)
)
),
sessionStore = InMemorySessionStore(
listOf(
aSessionData(sessionId = A_SESSION_ID.value),
aSessionData(sessionId = A_SESSION_ID_2.value),
)
),
)
val request = aPushRequest(A_SESSION_ID, A_ROOM_ID, AN_EVENT_ID, "firebase")
val result = sut.resolveEvents(A_SESSION_ID, listOf(request))
assertThat(result.getEvent(request)?.isSuccess).isTrue()
}
@Test
@@ -851,6 +982,7 @@ class DefaultNotifiableEventResolverTest : RobolectricTest() {
testNoResults(NotificationContent.MessageLike.KeyVerificationDone)
testNoResults(NotificationContent.MessageLike.ReactionContent(relatedEventId = AN_EVENT_ID_2.value))
testNoResults(NotificationContent.MessageLike.Sticker)
testNoResults(NotificationContent.MessageLike.Beacon)
testNoResults(NotificationContent.StateEvent.PolicyRuleRoom)
testNoResults(NotificationContent.StateEvent.PolicyRuleServer)
testNoResults(NotificationContent.StateEvent.PolicyRuleUser)
@@ -893,6 +1025,7 @@ class DefaultNotifiableEventResolverTest : RobolectricTest() {
notificationService: FakeNotificationService? = FakeNotificationService(),
notificationResult: Result<Map<EventId, Result<NotificationData>>> = Result.success(emptyMap()),
callNotificationEventResolver: FakeCallNotificationEventResolver = FakeCallNotificationEventResolver(),
sessionStore: SessionStore = InMemorySessionStore(),
): DefaultNotifiableEventResolver {
val context = RuntimeEnvironment.getApplication() as Context
notificationService?.givenGetNotificationsResult(notificationResult)
@@ -917,6 +1050,7 @@ class DefaultNotifiableEventResolverTest : RobolectricTest() {
clock = FakeSystemClock(),
),
featureFlagService = FakeFeatureFlagService(),
sessionStore = sessionStore,
)
}
}