mirror of
https://github.com/Novaloop-AG/element-x-ios.git
synced 2026-10-31 08:15:24 +00:00
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:
@@ -519,6 +519,7 @@
|
||||
"notification_invitation_action_reject" = "Reject";
|
||||
"notification_invite_body" = "Invited you to chat";
|
||||
"notification_invite_body_with_sender" = "%1$@ invited you to chat";
|
||||
"notification_live_location_started_body" = "Started sharing their live location";
|
||||
"notification_mentioned_you_body" = "Mentioned you: %1$@";
|
||||
"notification_new_messages" = "New Messages";
|
||||
"notification_reaction_body" = "Reacted with %1$@";
|
||||
|
||||
@@ -519,6 +519,7 @@
|
||||
"notification_invitation_action_reject" = "Reject";
|
||||
"notification_invite_body" = "Invited you to chat";
|
||||
"notification_invite_body_with_sender" = "%1$@ invited you to chat";
|
||||
"notification_live_location_started_body" = "Started sharing their live location";
|
||||
"notification_mentioned_you_body" = "Mentioned you: %1$@";
|
||||
"notification_new_messages" = "New Messages";
|
||||
"notification_reaction_body" = "Reacted with %1$@";
|
||||
|
||||
@@ -1192,6 +1192,8 @@ internal nonisolated enum L10n {
|
||||
internal static func notificationInviteBodyWithSender(_ p1: Any) -> String {
|
||||
return L10n.tr("Localizable", "notification_invite_body_with_sender", String(describing: p1))
|
||||
}
|
||||
/// Started sharing their live location
|
||||
internal static var notificationLiveLocationStartedBody: String { return L10n.tr("Localizable", "notification_live_location_started_body") }
|
||||
/// Mentioned you: %1$@
|
||||
internal static func notificationMentionedYouBody(_ p1: Any) -> String {
|
||||
return L10n.tr("Localizable", "notification_mentioned_you_body", String(describing: p1))
|
||||
|
||||
@@ -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(¬ificationContent)
|
||||
return
|
||||
}
|
||||
|
||||
await processMessageLike(notificationContent: ¬ificationContent,
|
||||
notificationItem: notificationItem,
|
||||
mediaProvider: mediaProvider)
|
||||
|
||||
switch messageContent {
|
||||
case .roomMessage(let messageType, _):
|
||||
await processRoomMessage(notificationContent: ¬ificationContent,
|
||||
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: ¬ificationContent,
|
||||
notificationItem: notificationItem,
|
||||
mediaProvider: mediaProvider)
|
||||
|
||||
switch messageContent {
|
||||
case .roomMessage(let messageType, _):
|
||||
await processRoomMessage(notificationContent: ¬ificationContent,
|
||||
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(¬ificationContent)
|
||||
}
|
||||
case .state(let stateContent):
|
||||
switch stateContent {
|
||||
case .beaconInfo:
|
||||
await processAsMessage(notificationContent: ¬ificationContent,
|
||||
notificationItem: notificationItem,
|
||||
mediaProvider: mediaProvider)
|
||||
notificationContent.body = L10n.notificationLiveLocationStartedBody
|
||||
default:
|
||||
processEmpty(¬ificationContent)
|
||||
}
|
||||
case .none:
|
||||
processEmpty(¬ificationContent)
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
import Dynamic
|
||||
@testable import ElementX
|
||||
import MatrixRustSDK
|
||||
import MatrixRustSDKMocks
|
||||
import Testing
|
||||
import UserNotifications
|
||||
|
||||
@@ -221,6 +222,56 @@ nonisolated struct NotificationContentBuilderTests {
|
||||
#expect(notificationContent.attachments == [])
|
||||
}
|
||||
|
||||
@Test
|
||||
mutating func liveLocationStartNotification() async {
|
||||
let event = TimelineEventSDKMock()
|
||||
event.eventIdReturnValue = UUID().uuidString
|
||||
event.contentReturnValue = .state(content: .beaconInfo)
|
||||
|
||||
let notificationItem = NotificationItemProxyMock(.init(event: .timeline(event: event),
|
||||
roomID: "!test:matrix.org",
|
||||
receiverID: "@bob:matrix.org",
|
||||
senderDisplayName: "Alice",
|
||||
roomDisplayName: "Alice",
|
||||
roomJoinedMembers: 2,
|
||||
isRoomDirect: true,
|
||||
isRoomPrivate: true,
|
||||
isNoisy: true))
|
||||
|
||||
await notificationContentBuilder.process(notificationContent: ¬ificationContent,
|
||||
notificationItem: notificationItem,
|
||||
mediaProvider: mediaProvider)
|
||||
|
||||
let communicationContext = Dynamic(notificationContent, memberName: "communicationContext")
|
||||
#expect(communicationContext.sender.displayName == "Alice")
|
||||
#expect(notificationContent.body == L10n.notificationLiveLocationStartedBody)
|
||||
#expect(notificationContent.categoryIdentifier == NotificationConstants.Category.message)
|
||||
#expect(notificationContent.sound != nil)
|
||||
}
|
||||
|
||||
@Test
|
||||
mutating func otherStateEventNotification() async {
|
||||
let event = TimelineEventSDKMock()
|
||||
event.eventIdReturnValue = UUID().uuidString
|
||||
event.contentReturnValue = .state(content: .roomName)
|
||||
|
||||
let notificationItem = NotificationItemProxyMock(.init(event: .timeline(event: event),
|
||||
roomID: "!test:matrix.org",
|
||||
receiverID: "@bob:matrix.org",
|
||||
senderDisplayName: "Alice",
|
||||
roomDisplayName: "Alice",
|
||||
roomJoinedMembers: 2,
|
||||
isRoomDirect: true,
|
||||
isRoomPrivate: true,
|
||||
isNoisy: true))
|
||||
|
||||
await notificationContentBuilder.process(notificationContent: ¬ificationContent,
|
||||
notificationItem: notificationItem,
|
||||
mediaProvider: mediaProvider)
|
||||
|
||||
#expect(notificationContent.body == L10n.notification)
|
||||
}
|
||||
|
||||
@Test
|
||||
mutating func roomMessageNotificationWithThreadAndMention() async {
|
||||
let notificationItem = NotificationItemProxyMock(.init(roomID: "!testroom:matrix.org",
|
||||
|
||||
Reference in New Issue
Block a user