Render push notifications for knocks

The homeserver's new MSCxxxx knock push rule notifies members who can
act on a knock; the push resolver previously filtered every
m.room.member state event out, so the push produced no notification.
Split RoomMemberContent out of the filtered-out catch-all and render
membership=knock as "Requested to join" from the knocking user
(other membership changes stay filtered out).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Matthew Hodgson
2026-07-13 19:54:40 +01:00
parent 3bb7fe97e6
commit c86307dead
3 changed files with 57 additions and 1 deletions
@@ -36,6 +36,7 @@ import io.element.android.libraries.matrix.api.media.isPreviewEnabled
import io.element.android.libraries.matrix.api.notification.NotificationContent
import io.element.android.libraries.matrix.api.notification.NotificationData
import io.element.android.libraries.matrix.api.permalink.PermalinkParser
import io.element.android.libraries.matrix.api.room.RoomMembershipState
import io.element.android.libraries.matrix.api.room.join.JoinRule
import io.element.android.libraries.matrix.api.timeline.item.event.AudioMessageType
import io.element.android.libraries.matrix.api.timeline.item.event.EmoteMessageType
@@ -299,7 +300,29 @@ class DefaultNotifiableEventResolver(
Timber.tag(loggerTag.value).d("Ignoring notification for sticker")
throw NotificationResolverException.EventFilteredOut
}
is NotificationContent.StateEvent.RoomMemberContent,
is NotificationContent.StateEvent.RoomMemberContent -> {
// MSCxxxx: the homeserver pushes knocks to users who can act on them.
if (content.membershipState == RoomMembershipState.KNOCK) {
val notifiableMessageEvent = buildNotifiableMessageEvent(
sessionId = userId,
senderId = content.userId,
roomId = roomId,
eventId = eventId,
noisy = isNoisy,
timestamp = this.timestamp,
senderDisambiguatedDisplayName = getDisambiguatedDisplayName(content.userId),
body = stringProvider.getString(R.string.notification_knock_request_body),
roomName = roomDisplayName,
roomIsDm = isDm,
roomAvatarPath = roomAvatarUrl,
senderAvatarPath = senderAvatarUrl,
)
ResolvedPushEvent.Event(notifiableMessageEvent)
} else {
Timber.tag(loggerTag.value).d("Ignoring notification for membership ${content.membershipState}")
throw NotificationResolverException.EventFilteredOut
}
}
NotificationContent.StateEvent.PolicyRuleRoom,
NotificationContent.StateEvent.PolicyRuleServer,
NotificationContent.StateEvent.PolicyRuleUser,
@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?><!--
~ Copyright (c) 2026 Element Creations Ltd.
~
~ SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
~ Please see LICENSE files in the repository root for full details.
-->
<resources>
<!-- Body of the notification shown to users who can act on a knock (MSCxxxx knock push rule). -->
<string name="notification_knock_request_body">"Requested to join"</string>
</resources>
@@ -627,6 +627,28 @@ class DefaultNotifiableEventResolverTest : RobolectricTest() {
assertThat(result.getEvent(request)).isEqualTo(Result.success(expectedResult))
}
@Test
fun `resolve RoomMemberContent knock`() = runTest {
val sut = createDefaultNotifiableEventResolver(
notificationResult = Result.success(
mapOf(
AN_EVENT_ID to Result.success(aNotificationData(
content = NotificationContent.StateEvent.RoomMemberContent(
userId = A_USER_ID_2,
membershipState = RoomMembershipState.KNOCK
)
))
)
)
)
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 = "Requested to join")
)
assertThat(result.getEvent(request)).isEqualTo(Result.success(expectedResult))
}
@Test
fun `resolve RoomMemberContent other`() = runTest {
val sut = createDefaultNotifiableEventResolver(