Merge pull request #7360 from hayaksi1/fix/7253-skip-other-account-notifications

Do not notify about an event sent by another account on the same device
This commit is contained in:
Benoit Marty
2026-08-11 14:14:36 +02:00
committed by GitHub
2 changed files with 132 additions and 1 deletions
@@ -56,6 +56,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
@@ -94,6 +95,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,
@@ -117,10 +119,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)
}
}
@@ -136,10 +143,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)
@@ -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
@@ -836,6 +840,111 @@ class DefaultNotifiableEventResolverTest : RobolectricTest() {
assertThat(result.getEvent(request)?.getOrNull()).isNull()
}
@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
fun `resolve null cases`() {
testNoResults(NotificationContent.MessageLike.CallAnswer)
@@ -893,6 +1002,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 +1027,7 @@ class DefaultNotifiableEventResolverTest : RobolectricTest() {
clock = FakeSystemClock(),
),
featureFlagService = FakeFeatureFlagService(),
sessionStore = sessionStore,
)
}
}