mirror of
https://github.com/Novaloop-AG/element-x-android.git
synced 2026-10-31 16:22:49 +00:00
Do not notify about an event sent by another account on the same device
With several accounts logged in, sending a message from one of them raises a notification on every other account that shares the room. The device notifies its owner about something the owner has just done, and the more accounts share a room the more copies arrive. Resolve the set of logged-in user ids once per push batch and drop an event whose sender is one of the other accounts, using the filtered-out result the notification pipeline already understands, so it is recorded in push history rather than turned into a fallback notification. The receiving session is removed from that set, which keeps the change a strict no-op for a single account and protects the local echo that the reply action pushes back through the same path. Filtering happens at the one point that sees every kind of push, so messages, polls, call invites, ringing calls and room invites are all covered. Filtering is unconditional and has no setting; multiple accounts sit behind an experimental flag that is off by default, so nothing changes for a stock build.
This commit is contained in:
+21
-1
@@ -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)
|
||||
|
||||
+111
@@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user