Implements push notifications for LLS via MSC4505 (#5872)

* Render live location share start push notifications in the NSE

Live location sharing starts with a cleartext org.matrix.msc3672.beacon_info
STATE event, which the NSE previously rendered as the generic fallback
notification because NotificationContentBuilder only handled message-like
event content.

- Handle .state(.beaconInfo) in NotificationContentBuilder: sender display
  name as the title (same as other messages) with a new localized body,
  "Started sharing their live location".
- Let beaconInfo state events through NotificationHandler's preprocessing
  (all other state events are still discarded).
- Add notification_live_location_started_body, imported from Localazy.
- Unit tests for the new state-event notification paths.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* rename processMessageLike in processAsMessage

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: Mauro Romito <mauro.romito@element.io>
This commit is contained in:
Matthew Hodgson
2026-07-27 15:31:47 +01:00
committed by GitHub
parent 5262350b40
commit a8c79fdd46
6 changed files with 97 additions and 27 deletions
+35 -25
View File
@@ -59,28 +59,38 @@ nonisolated struct NotificationContentBuilder {
notificationItem: notificationItem,
mediaProvider: mediaProvider)
case .timeline(let event):
guard case let .messageLike(messageContent) = try? event.content() else {
processEmpty(&notificationContent)
return
}
await processMessageLike(notificationContent: &notificationContent,
notificationItem: notificationItem,
mediaProvider: mediaProvider)
switch messageContent {
case .roomMessage(let messageType, _):
await processRoomMessage(notificationContent: &notificationContent,
notificationItem: notificationItem,
messageType: messageType,
mediaProvider: mediaProvider)
case .poll(let question):
notificationContent.body = L10n.commonPollSummary(question)
case .callInvite:
notificationContent.body = L10n.commonUnsupportedCall
case .rtcNotification:
notificationContent.body = L10n.notificationIncomingCall
default:
switch try? event.content() {
case .messageLike(let messageContent):
await processAsMessage(notificationContent: &notificationContent,
notificationItem: notificationItem,
mediaProvider: mediaProvider)
switch messageContent {
case .roomMessage(let messageType, _):
await processRoomMessage(notificationContent: &notificationContent,
notificationItem: notificationItem,
messageType: messageType,
mediaProvider: mediaProvider)
case .poll(let question):
notificationContent.body = L10n.commonPollSummary(question)
case .callInvite:
notificationContent.body = L10n.commonUnsupportedCall
case .rtcNotification:
notificationContent.body = L10n.notificationIncomingCall
default:
processEmpty(&notificationContent)
}
case .state(let stateContent):
switch stateContent {
case .beaconInfo:
await processAsMessage(notificationContent: &notificationContent,
notificationItem: notificationItem,
mediaProvider: mediaProvider)
notificationContent.body = L10n.notificationLiveLocationStartedBody
default:
processEmpty(&notificationContent)
}
case .none:
processEmpty(&notificationContent)
}
}
@@ -117,9 +127,9 @@ nonisolated struct NotificationContentBuilder {
mediaProvider: mediaProvider)
}
private func processMessageLike(notificationContent: inout UNMutableNotificationContent,
notificationItem: NotificationItemProxyProtocol,
mediaProvider: MediaProviderProtocol) async {
private func processAsMessage(notificationContent: inout UNMutableNotificationContent,
notificationItem: NotificationItemProxyProtocol,
mediaProvider: MediaProviderProtocol) async {
notificationContent.title = notificationItem.senderDisplayName ?? notificationItem.roomDisplayName
if notificationContent.title != notificationItem.roomDisplayName {
notificationContent.subtitle = notificationItem.roomDisplayName
+7 -2
View File
@@ -148,8 +148,13 @@ nonisolated class NotificationHandler {
.beacon:
return .unsupportedShouldDiscard
}
case .state:
return .unsupportedShouldDiscard
case .state(let stateContent):
switch stateContent {
case .beaconInfo:
return .shouldDisplay
default:
return .unsupportedShouldDiscard
}
case .none:
return .unsupportedShouldDiscard
}