mirror of
https://github.com/Novaloop-AG/element-x-ios.git
synced 2026-11-04 10:33:45 +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_invitation_action_reject" = "Reject";
|
||||||
"notification_invite_body" = "Invited you to chat";
|
"notification_invite_body" = "Invited you to chat";
|
||||||
"notification_invite_body_with_sender" = "%1$@ 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_mentioned_you_body" = "Mentioned you: %1$@";
|
||||||
"notification_new_messages" = "New Messages";
|
"notification_new_messages" = "New Messages";
|
||||||
"notification_reaction_body" = "Reacted with %1$@";
|
"notification_reaction_body" = "Reacted with %1$@";
|
||||||
|
|||||||
@@ -519,6 +519,7 @@
|
|||||||
"notification_invitation_action_reject" = "Reject";
|
"notification_invitation_action_reject" = "Reject";
|
||||||
"notification_invite_body" = "Invited you to chat";
|
"notification_invite_body" = "Invited you to chat";
|
||||||
"notification_invite_body_with_sender" = "%1$@ 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_mentioned_you_body" = "Mentioned you: %1$@";
|
||||||
"notification_new_messages" = "New Messages";
|
"notification_new_messages" = "New Messages";
|
||||||
"notification_reaction_body" = "Reacted with %1$@";
|
"notification_reaction_body" = "Reacted with %1$@";
|
||||||
|
|||||||
@@ -1192,6 +1192,8 @@ internal nonisolated enum L10n {
|
|||||||
internal static func notificationInviteBodyWithSender(_ p1: Any) -> String {
|
internal static func notificationInviteBodyWithSender(_ p1: Any) -> String {
|
||||||
return L10n.tr("Localizable", "notification_invite_body_with_sender", String(describing: p1))
|
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$@
|
/// Mentioned you: %1$@
|
||||||
internal static func notificationMentionedYouBody(_ p1: Any) -> String {
|
internal static func notificationMentionedYouBody(_ p1: Any) -> String {
|
||||||
return L10n.tr("Localizable", "notification_mentioned_you_body", String(describing: p1))
|
return L10n.tr("Localizable", "notification_mentioned_you_body", String(describing: p1))
|
||||||
|
|||||||
@@ -59,28 +59,38 @@ nonisolated struct NotificationContentBuilder {
|
|||||||
notificationItem: notificationItem,
|
notificationItem: notificationItem,
|
||||||
mediaProvider: mediaProvider)
|
mediaProvider: mediaProvider)
|
||||||
case .timeline(let event):
|
case .timeline(let event):
|
||||||
guard case let .messageLike(messageContent) = try? event.content() else {
|
switch try? event.content() {
|
||||||
processEmpty(¬ificationContent)
|
case .messageLike(let messageContent):
|
||||||
return
|
await processAsMessage(notificationContent: ¬ificationContent,
|
||||||
}
|
notificationItem: notificationItem,
|
||||||
|
mediaProvider: mediaProvider)
|
||||||
await processMessageLike(notificationContent: ¬ificationContent,
|
|
||||||
notificationItem: notificationItem,
|
switch messageContent {
|
||||||
mediaProvider: mediaProvider)
|
case .roomMessage(let messageType, _):
|
||||||
|
await processRoomMessage(notificationContent: ¬ificationContent,
|
||||||
switch messageContent {
|
notificationItem: notificationItem,
|
||||||
case .roomMessage(let messageType, _):
|
messageType: messageType,
|
||||||
await processRoomMessage(notificationContent: ¬ificationContent,
|
mediaProvider: mediaProvider)
|
||||||
notificationItem: notificationItem,
|
case .poll(let question):
|
||||||
messageType: messageType,
|
notificationContent.body = L10n.commonPollSummary(question)
|
||||||
mediaProvider: mediaProvider)
|
case .callInvite:
|
||||||
case .poll(let question):
|
notificationContent.body = L10n.commonUnsupportedCall
|
||||||
notificationContent.body = L10n.commonPollSummary(question)
|
case .rtcNotification:
|
||||||
case .callInvite:
|
notificationContent.body = L10n.notificationIncomingCall
|
||||||
notificationContent.body = L10n.commonUnsupportedCall
|
default:
|
||||||
case .rtcNotification:
|
processEmpty(¬ificationContent)
|
||||||
notificationContent.body = L10n.notificationIncomingCall
|
}
|
||||||
default:
|
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)
|
processEmpty(¬ificationContent)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -117,9 +127,9 @@ nonisolated struct NotificationContentBuilder {
|
|||||||
mediaProvider: mediaProvider)
|
mediaProvider: mediaProvider)
|
||||||
}
|
}
|
||||||
|
|
||||||
private func processMessageLike(notificationContent: inout UNMutableNotificationContent,
|
private func processAsMessage(notificationContent: inout UNMutableNotificationContent,
|
||||||
notificationItem: NotificationItemProxyProtocol,
|
notificationItem: NotificationItemProxyProtocol,
|
||||||
mediaProvider: MediaProviderProtocol) async {
|
mediaProvider: MediaProviderProtocol) async {
|
||||||
notificationContent.title = notificationItem.senderDisplayName ?? notificationItem.roomDisplayName
|
notificationContent.title = notificationItem.senderDisplayName ?? notificationItem.roomDisplayName
|
||||||
if notificationContent.title != notificationItem.roomDisplayName {
|
if notificationContent.title != notificationItem.roomDisplayName {
|
||||||
notificationContent.subtitle = notificationItem.roomDisplayName
|
notificationContent.subtitle = notificationItem.roomDisplayName
|
||||||
|
|||||||
@@ -148,8 +148,13 @@ nonisolated class NotificationHandler {
|
|||||||
.beacon:
|
.beacon:
|
||||||
return .unsupportedShouldDiscard
|
return .unsupportedShouldDiscard
|
||||||
}
|
}
|
||||||
case .state:
|
case .state(let stateContent):
|
||||||
return .unsupportedShouldDiscard
|
switch stateContent {
|
||||||
|
case .beaconInfo:
|
||||||
|
return .shouldDisplay
|
||||||
|
default:
|
||||||
|
return .unsupportedShouldDiscard
|
||||||
|
}
|
||||||
case .none:
|
case .none:
|
||||||
return .unsupportedShouldDiscard
|
return .unsupportedShouldDiscard
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,7 @@
|
|||||||
import Dynamic
|
import Dynamic
|
||||||
@testable import ElementX
|
@testable import ElementX
|
||||||
import MatrixRustSDK
|
import MatrixRustSDK
|
||||||
|
import MatrixRustSDKMocks
|
||||||
import Testing
|
import Testing
|
||||||
import UserNotifications
|
import UserNotifications
|
||||||
|
|
||||||
@@ -221,6 +222,56 @@ nonisolated struct NotificationContentBuilderTests {
|
|||||||
#expect(notificationContent.attachments == [])
|
#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
|
@Test
|
||||||
mutating func roomMessageNotificationWithThreadAndMention() async {
|
mutating func roomMessageNotificationWithThreadAndMention() async {
|
||||||
let notificationItem = NotificationItemProxyMock(.init(roomID: "!testroom:matrix.org",
|
let notificationItem = NotificationItemProxyMock(.init(roomID: "!testroom:matrix.org",
|
||||||
|
|||||||
Reference in New Issue
Block a user