Threaded notifications (#4644)
* implemented grouping * implemented presenting the thread * improved the implementation by reusing the presentation action * add Thread group to DMs to differentiate from non threaded messages of the same DM * name for a threaded notification in group room * focus event when tapping on a notification * pr suggestions * document
This commit is contained in:
@@ -420,6 +420,7 @@
|
||||
"leave_room_alert_select_new_owner_title" = "Transfer ownership";
|
||||
"leave_room_alert_subtitle" = "Are you sure that you want to leave the room?";
|
||||
"login_initial_device_name_ios" = "%1$@ iOS";
|
||||
"notification_thread_in_room" = "Thread in %1$@";
|
||||
"notification_channel_call" = "Call";
|
||||
"notification_channel_listening_for_events" = "Listening for events";
|
||||
"notification_channel_noisy" = "Noisy notifications";
|
||||
|
||||
@@ -359,9 +359,14 @@ class AppCoordinator: AppCoordinatorProtocol, AuthenticationFlowCoordinatorDeleg
|
||||
} else {
|
||||
storedRoomsToAwait = [roomID]
|
||||
}
|
||||
}
|
||||
|
||||
handleAppRoute(.room(roomID: roomID, via: []))
|
||||
} else if appSettings.threadsEnabled, let threadRootEventID = content.threadRootEventID {
|
||||
handleAppRoute(.thread(roomID: roomID, threadRootEventID: threadRootEventID, focusEventID: content.eventID))
|
||||
} else if let eventID = content.eventID {
|
||||
handleAppRoute(.event(eventID: eventID, roomID: roomID, via: []))
|
||||
} else {
|
||||
handleAppRoute(.room(roomID: roomID, via: []))
|
||||
}
|
||||
}
|
||||
|
||||
func handleInlineReply(_ service: NotificationManagerProtocol, content: UNNotificationContent, replyText: String) async {
|
||||
|
||||
@@ -51,6 +51,8 @@ enum AppRoute: Hashable {
|
||||
case share(ShareExtensionPayload)
|
||||
/// The change roles screen of a room with the transfer ownership setting
|
||||
case transferOwnership(roomID: String)
|
||||
/// A thread within a room, only to be used to handle tap on notification for threaded events.
|
||||
case thread(roomID: String, threadRootEventID: String, focusEventID: String?)
|
||||
|
||||
/// Whether or not the route should be handled by the authentication flow.
|
||||
var isAuthenticationRoute: Bool {
|
||||
|
||||
@@ -130,6 +130,12 @@ class ChatsFlowCoordinator: FlowCoordinatorProtocol {
|
||||
roomFlowCoordinator?.clearRoute(animated: animated)
|
||||
case .roomMemberDetails:
|
||||
roomFlowCoordinator?.handleAppRoute(appRoute, animated: animated)
|
||||
case .thread(let roomID, let threadRootEventID, let focusEventID):
|
||||
stateMachine.processEvent(.selectRoom(roomID: roomID,
|
||||
via: [],
|
||||
entryPoint: .thread(rootEventID: threadRootEventID,
|
||||
focusEventID: focusEventID)),
|
||||
userInfo: .init(animated: animated))
|
||||
case .event(let eventID, let roomID, let via):
|
||||
stateMachine.processEvent(.selectRoom(roomID: roomID, via: via, entryPoint: .eventID(eventID)), userInfo: .init(animated: animated))
|
||||
case .eventOnRoomAlias(let eventID, let alias):
|
||||
@@ -190,24 +196,7 @@ class ChatsFlowCoordinator: FlowCoordinatorProtocol {
|
||||
case (.initial, .start, .roomList):
|
||||
presentHomeScreen()
|
||||
case(.roomList(let detailState), .selectRoom(let roomID, let via, let entryPoint), .roomList):
|
||||
if case .room(roomID) = detailState,
|
||||
!entryPoint.isEventID, // Don't reuse the existing room so the live timeline is hidden while the detached timeline is loading.
|
||||
let roomFlowCoordinator {
|
||||
let route: AppRoute = switch entryPoint {
|
||||
case .room: .room(roomID: roomID, via: via)
|
||||
case .roomDetails: .roomDetails(roomID: roomID)
|
||||
case .eventID(let eventID): .event(eventID: eventID, roomID: roomID, via: via) // ignored.
|
||||
case .share(let payload): .share(payload)
|
||||
case .transferOwnership: .transferOwnership(roomID: roomID)
|
||||
}
|
||||
roomFlowCoordinator.handleAppRoute(route, animated: animated)
|
||||
} else {
|
||||
if case .space = detailState {
|
||||
dismissRoomFlow(animated: animated)
|
||||
}
|
||||
startRoomFlow(roomID: roomID, via: via, entryPoint: entryPoint, animated: animated)
|
||||
}
|
||||
actionsSubject.send(.hideCallScreenOverlay) // Turn any active call into a PiP so that navigation from a notification is visible to the user.
|
||||
handleSelectRoomTransition(roomID: roomID, via: via, entryPoint: entryPoint, detailState: detailState, animated: animated)
|
||||
case(.roomList, .deselectRoom, .roomList):
|
||||
dismissRoomFlow(animated: animated)
|
||||
|
||||
@@ -298,6 +287,28 @@ class ChatsFlowCoordinator: FlowCoordinatorProtocol {
|
||||
}
|
||||
}
|
||||
|
||||
private func handleSelectRoomTransition(roomID: String, via: [String], entryPoint: RoomFlowCoordinatorEntryPoint, detailState: ChatsFlowCoordinatorStateMachine.DetailState?, animated: Bool) {
|
||||
if case .room(roomID) = detailState,
|
||||
!entryPoint.isEventID, // Don't reuse the existing room so the live timeline is hidden while the detached timeline is loading.
|
||||
let roomFlowCoordinator {
|
||||
let route: AppRoute = switch entryPoint {
|
||||
case .room: .room(roomID: roomID, via: via)
|
||||
case .roomDetails: .roomDetails(roomID: roomID)
|
||||
case .eventID(let eventID): .event(eventID: eventID, roomID: roomID, via: via) // ignored.
|
||||
case .share(let payload): .share(payload)
|
||||
case .transferOwnership: .transferOwnership(roomID: roomID)
|
||||
case .thread(let rootEventID, let focusEventID): .thread(roomID: roomID, threadRootEventID: rootEventID, focusEventID: focusEventID)
|
||||
}
|
||||
roomFlowCoordinator.handleAppRoute(route, animated: animated)
|
||||
} else {
|
||||
if case .space = detailState {
|
||||
dismissRoomFlow(animated: animated)
|
||||
}
|
||||
startRoomFlow(roomID: roomID, via: via, entryPoint: entryPoint, animated: animated)
|
||||
}
|
||||
actionsSubject.send(.hideCallScreenOverlay) // Turn any active call into a PiP so that navigation from a notification is visible to the user.
|
||||
}
|
||||
|
||||
private func setupObservers() {
|
||||
userSession.clientProxy.actionsPublisher
|
||||
.receive(on: DispatchQueue.main)
|
||||
@@ -498,6 +509,8 @@ class ChatsFlowCoordinator: FlowCoordinatorProtocol {
|
||||
coordinator.handleAppRoute(.share(payload), animated: animated)
|
||||
case .transferOwnership:
|
||||
coordinator.handleAppRoute(.transferOwnership(roomID: roomID), animated: animated)
|
||||
case .thread(let rootEventID, let focusEventID):
|
||||
coordinator.handleAppRoute(.thread(roomID: roomID, threadRootEventID: rootEventID, focusEventID: focusEventID), animated: animated)
|
||||
}
|
||||
|
||||
Task {
|
||||
|
||||
@@ -81,7 +81,7 @@ class EncryptionSettingsFlowCoordinator: FlowCoordinatorProtocol {
|
||||
case .accountProvisioningLink:
|
||||
break // We always ignore this flow when logged in.
|
||||
case .roomList, .room, .roomAlias, .childRoom, .childRoomAlias,
|
||||
.roomDetails, .roomMemberDetails, .userProfile,
|
||||
.roomDetails, .roomMemberDetails, .userProfile, .thread,
|
||||
.event, .eventOnRoomAlias, .childEvent, .childEventOnRoomAlias,
|
||||
.call, .genericCallLink, .settings, .share, .transferOwnership:
|
||||
// These routes aren't in this flow so clear the entire stack.
|
||||
|
||||
@@ -37,6 +37,9 @@ enum RoomFlowCoordinatorEntryPoint: Hashable {
|
||||
case room
|
||||
/// The flow will start by showing the room, focussing on the supplied event ID.
|
||||
case eventID(String)
|
||||
/// The flow will start by showing a thread timeline, can only be triggered by notification taps,
|
||||
/// which means it can never be a used for child flows.
|
||||
case thread(rootEventID: String, focusEventID: String?)
|
||||
/// The flow will start by showing the room's details.
|
||||
case roomDetails
|
||||
/// An external media share request
|
||||
@@ -154,6 +157,14 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol {
|
||||
} else {
|
||||
stateMachine.tryEvent(.presentRoomMemberDetails(userID: userID), userInfo: EventUserInfo(animated: animated))
|
||||
}
|
||||
case .thread(let roomID, let threadRootEventID, let focusEventID):
|
||||
Task {
|
||||
await handleRoomRoute(roomID: roomID,
|
||||
via: [],
|
||||
presentationAction: .thread(rootEventID: threadRootEventID,
|
||||
focusEventID: focusEventID),
|
||||
animated: animated)
|
||||
}
|
||||
case .event(let eventID, let roomID, let via):
|
||||
Task {
|
||||
await handleRoomRoute(roomID: roomID,
|
||||
@@ -162,38 +173,7 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol {
|
||||
animated: animated)
|
||||
}
|
||||
case .childEvent(let eventID, let roomID, let via):
|
||||
if case .presentingChild = stateMachine.state, let childRoomFlowCoordinator {
|
||||
childRoomFlowCoordinator.handleAppRoute(appRoute, animated: animated)
|
||||
} else if roomID != roomProxy.id {
|
||||
stateMachine.tryEvent(.startChildFlow(roomID: roomID, via: via, entryPoint: .eventID(eventID)), userInfo: EventUserInfo(animated: animated))
|
||||
} else {
|
||||
showLoadingIndicator(delay: .seconds(0.5))
|
||||
Task {
|
||||
defer { hideLoadingIndicator() }
|
||||
switch await roomProxy.loadOrFetchEventDetails(for: eventID) {
|
||||
case .success(let event):
|
||||
if flowParameters.appSettings.threadsEnabled, let threadRootEventID = event.threadRootEventId() {
|
||||
if case .thread(threadRootEventID: threadRootEventID, _) = stateMachine.state, let threadCoordinator = childThreadScreenCoordinators.last {
|
||||
threadCoordinator.focusOnEvent(eventID: eventID)
|
||||
} else {
|
||||
// If we are showing the room timeline, we want to focus the thread root.
|
||||
if childThreadScreenCoordinators.isEmpty {
|
||||
roomScreenCoordinator?.focusOnEvent(.init(eventID: threadRootEventID, shouldSetPin: false))
|
||||
}
|
||||
stateMachine.tryEvent(.presentThread(threadRootEventID: threadRootEventID, focusEventID: eventID))
|
||||
}
|
||||
} else if !childThreadScreenCoordinators.isEmpty {
|
||||
// If we are showing a child thread and we are navigating to a non threaded event
|
||||
// of the same room, we want to push the room on top of the thread.
|
||||
stateMachine.tryEvent(.startChildFlow(roomID: roomID, via: via, entryPoint: .eventID(eventID)), userInfo: EventUserInfo(animated: animated))
|
||||
} else {
|
||||
roomScreenCoordinator?.focusOnEvent(.init(eventID: eventID, shouldSetPin: false))
|
||||
}
|
||||
case .failure:
|
||||
showErrorIndicator()
|
||||
}
|
||||
}
|
||||
}
|
||||
handleChildEventRoute(eventID: eventID, roomID: roomID, via: via, animated: animated)
|
||||
case .share(let payload):
|
||||
guard let roomID = payload.roomID, roomID == self.roomID else {
|
||||
fatalError("Navigation route doesn't belong to this room flow.")
|
||||
@@ -226,6 +206,41 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol {
|
||||
}
|
||||
}
|
||||
|
||||
private func handleChildEventRoute(eventID: String, roomID: String, via: [String], animated: Bool) {
|
||||
if case .presentingChild = stateMachine.state, let childRoomFlowCoordinator {
|
||||
childRoomFlowCoordinator.handleAppRoute(.childEvent(eventID: eventID, roomID: roomID, via: via), animated: animated)
|
||||
} else if roomID != roomProxy.id {
|
||||
stateMachine.tryEvent(.startChildFlow(roomID: roomID, via: via, entryPoint: .eventID(eventID)), userInfo: EventUserInfo(animated: animated))
|
||||
} else {
|
||||
showLoadingIndicator(delay: .seconds(0.5))
|
||||
Task {
|
||||
defer { hideLoadingIndicator() }
|
||||
switch await roomProxy.loadOrFetchEventDetails(for: eventID) {
|
||||
case .success(let event):
|
||||
if flowParameters.appSettings.threadsEnabled, let threadRootEventID = event.threadRootEventId() {
|
||||
if case .thread(threadRootEventID: threadRootEventID, _) = stateMachine.state, let threadCoordinator = childThreadScreenCoordinators.last {
|
||||
threadCoordinator.focusOnEvent(eventID: eventID)
|
||||
} else {
|
||||
// If we are showing the room timeline, we want to focus the thread root.
|
||||
if childThreadScreenCoordinators.isEmpty {
|
||||
roomScreenCoordinator?.focusOnEvent(.init(eventID: threadRootEventID, shouldSetPin: false))
|
||||
}
|
||||
stateMachine.tryEvent(.presentThread(threadRootEventID: threadRootEventID, focusEventID: eventID))
|
||||
}
|
||||
} else if !childThreadScreenCoordinators.isEmpty {
|
||||
// If we are showing a child thread and we are navigating to a non threaded event
|
||||
// of the same room, we want to push the room on top of the thread.
|
||||
stateMachine.tryEvent(.startChildFlow(roomID: roomID, via: via, entryPoint: .eventID(eventID)), userInfo: EventUserInfo(animated: animated))
|
||||
} else {
|
||||
roomScreenCoordinator?.focusOnEvent(.init(eventID: eventID, shouldSetPin: false))
|
||||
}
|
||||
case .failure:
|
||||
showErrorIndicator()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func presentTransferOwnershipScreen() {
|
||||
let parameters = RoomChangeRolesScreenCoordinatorParameters(mode: .owner,
|
||||
roomProxy: roomProxy,
|
||||
@@ -281,8 +296,7 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol {
|
||||
switch await roomProxy.loadOrFetchEventDetails(for: focusEvent.eventID) {
|
||||
case .success(let event):
|
||||
if flowParameters.appSettings.threadsEnabled, let threadRootEventID = event.threadRootEventId() {
|
||||
stateMachine.tryEvent(.presentRoom(presentationAction: .eventFocus(.init(eventID: threadRootEventID, shouldSetPin: false))), userInfo: EventUserInfo(animated: animated))
|
||||
stateMachine.tryEvent(.presentThread(threadRootEventID: threadRootEventID, focusEventID: focusEvent.eventID), userInfo: EventUserInfo(animated: false))
|
||||
stateMachine.tryEvent(.presentRoom(presentationAction: .thread(rootEventID: threadRootEventID, focusEventID: focusEvent.eventID)), userInfo: EventUserInfo(animated: animated))
|
||||
} else {
|
||||
stateMachine.tryEvent(.presentRoom(presentationAction: presentationAction), userInfo: EventUserInfo(animated: animated))
|
||||
}
|
||||
@@ -552,6 +566,9 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol {
|
||||
userInfo: EventUserInfo(animated: animated, timelineController: timelineController))
|
||||
case .share(.text(_, let text)):
|
||||
roomScreenCoordinator?.shareText(text)
|
||||
case .thread(let rootEventID, let focusEventID):
|
||||
roomScreenCoordinator?.focusOnEvent(.init(eventID: rootEventID, shouldSetPin: false))
|
||||
stateMachine.tryEvent(.presentThread(threadRootEventID: rootEventID, focusEventID: focusEventID))
|
||||
case .none:
|
||||
break
|
||||
}
|
||||
@@ -586,6 +603,8 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol {
|
||||
case .share(.mediaFiles(_, let mediaFiles)):
|
||||
stateMachine.tryEvent(.presentMediaUploadPreview(mediaURLs: mediaFiles.map(\.url)),
|
||||
userInfo: EventUserInfo(animated: animated, timelineController: timelineController))
|
||||
case .thread(let rootEventID, let focusEventID):
|
||||
stateMachine.tryEvent(.presentThread(threadRootEventID: rootEventID, focusEventID: focusEventID))
|
||||
case .share(.text), .eventFocus:
|
||||
break // These are both handled in the coordinator's init.
|
||||
case .none:
|
||||
@@ -1564,6 +1583,8 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol {
|
||||
coordinator.handleAppRoute(.share(payload), animated: true)
|
||||
case .transferOwnership:
|
||||
coordinator.handleAppRoute(.transferOwnership(roomID: roomID), animated: true)
|
||||
case .thread:
|
||||
fatalError("This entry point is not allowed for child flows")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -25,11 +25,21 @@ extension RoomFlowCoordinator {
|
||||
enum PresentationAction: Hashable {
|
||||
case eventFocus(FocusEvent)
|
||||
case share(ShareExtensionPayload)
|
||||
case thread(rootEventID: String, focusEventID: String?)
|
||||
|
||||
var focusedEvent: FocusEvent? {
|
||||
switch self {
|
||||
case .eventFocus(let focusEvent):
|
||||
focusEvent
|
||||
case .thread(let rootEventID, let focusEventID):
|
||||
// Since this enum is for the room and not the threaded timeline,
|
||||
// we will focus the thread root event id, and not the event id itself
|
||||
// which will be done at the thread presentation level
|
||||
if focusEventID != nil {
|
||||
.init(eventID: rootEventID, shouldSetPin: false)
|
||||
} else {
|
||||
nil
|
||||
}
|
||||
default:
|
||||
nil
|
||||
}
|
||||
|
||||
@@ -132,7 +132,7 @@ class UserSessionFlowCoordinator: FlowCoordinatorProtocol {
|
||||
case .roomList, .room, .roomAlias, .childRoom, .childRoomAlias,
|
||||
.roomDetails, .roomMemberDetails, .userProfile,
|
||||
.event, .eventOnRoomAlias, .childEvent, .childEventOnRoomAlias,
|
||||
.share, .transferOwnership:
|
||||
.share, .transferOwnership, .thread:
|
||||
clearPresentedSheets(animated: animated) // Make sure the presented route is visible.
|
||||
chatsFlowCoordinator.handleAppRoute(appRoute, animated: animated)
|
||||
if navigationTabCoordinator.selectedTab != .chats {
|
||||
|
||||
@@ -1026,6 +1026,10 @@ internal enum L10n {
|
||||
}
|
||||
/// You are viewing the notification! Click me!
|
||||
internal static var notificationTestPushNotificationContent: String { return L10n.tr("Localizable", "notification_test_push_notification_content") }
|
||||
/// Thread in %1$@
|
||||
internal static func notificationThreadInRoom(_ p1: Any) -> String {
|
||||
return L10n.tr("Localizable", "notification_thread_in_room", String(describing: p1))
|
||||
}
|
||||
/// %1$@: %2$@
|
||||
internal static func notificationTickerTextDm(_ p1: Any, _ p2: Any) -> String {
|
||||
return L10n.tr("Localizable", "notification_ticker_text_dm", String(describing: p1), String(describing: p2))
|
||||
|
||||
@@ -25,6 +25,10 @@ extension UNNotificationContent {
|
||||
@objc var pusherNotificationClientIdentifier: String? {
|
||||
userInfo[NotificationConstants.UserInfoKey.pusherNotificationClientIdentifier] as? String
|
||||
}
|
||||
|
||||
@objc var threadRootEventID: String? {
|
||||
userInfo[NotificationConstants.UserInfoKey.threadRootEventIdentifier] as? String
|
||||
}
|
||||
}
|
||||
|
||||
extension UNMutableNotificationContent {
|
||||
@@ -55,6 +59,15 @@ extension UNMutableNotificationContent {
|
||||
}
|
||||
}
|
||||
|
||||
override var threadRootEventID: String? {
|
||||
get {
|
||||
userInfo[NotificationConstants.UserInfoKey.threadRootEventIdentifier] as? String
|
||||
}
|
||||
set {
|
||||
userInfo[NotificationConstants.UserInfoKey.threadRootEventIdentifier] = newValue
|
||||
}
|
||||
}
|
||||
|
||||
var unreadCount: Int? {
|
||||
userInfo[NotificationConstants.UserInfoKey.unreadCount] as? Int
|
||||
}
|
||||
|
||||
@@ -395,7 +395,10 @@ class TimelineTableViewController: UIViewController {
|
||||
guard let self else { return }
|
||||
if let kvPair = timelineItemsDictionary.first(where: { $0.value.identifier.eventID == eventID }),
|
||||
let indexPath = dataSource?.indexPath(for: kvPair.key) {
|
||||
tableView.scrollToRow(at: indexPath, at: .middle, animated: animated)
|
||||
// Scrolling to the middle created a small bump in the timeline
|
||||
// Using top, which is bottom in the reversed timeline helps with rendering
|
||||
// in full long messages and images
|
||||
tableView.scrollToRow(at: indexPath, at: .top, animated: animated)
|
||||
coordinator.send(viewAction: .scrolledToFocussedItem)
|
||||
// Ensure VoiceOver focus happens after the scroll animation (if any)
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + (animated ? 0.5 : 0.0)) {
|
||||
|
||||
@@ -12,6 +12,7 @@ enum NotificationConstants {
|
||||
enum UserInfoKey {
|
||||
static let roomIdentifier = "room_id"
|
||||
static let eventIdentifier = "event_id"
|
||||
static let threadRootEventIdentifier = "thread_root_event_id"
|
||||
static let unreadCount = "unread_count"
|
||||
static let pusherNotificationClientIdentifier = "pusher_notification_client_identifier"
|
||||
static let receiverIdentifier = "receiver_id"
|
||||
|
||||
@@ -77,6 +77,10 @@ struct NotificationItemProxy: NotificationItemProxyProtocol {
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
var threadRootEventID: String? {
|
||||
notificationItem.threadId
|
||||
}
|
||||
}
|
||||
|
||||
struct EmptyNotificationItemProxy: NotificationItemProxyProtocol {
|
||||
@@ -109,4 +113,6 @@ struct EmptyNotificationItemProxy: NotificationItemProxyProtocol {
|
||||
var roomJoinedMembers: Int { 0 }
|
||||
|
||||
var hasMention: Bool { false }
|
||||
|
||||
var threadRootEventID: String? { nil }
|
||||
}
|
||||
|
||||
@@ -36,6 +36,8 @@ protocol NotificationItemProxyProtocol {
|
||||
var isNoisy: Bool { get }
|
||||
|
||||
var hasMention: Bool { get }
|
||||
|
||||
var threadRootEventID: String? { get }
|
||||
}
|
||||
|
||||
extension NotificationItemProxyProtocol {
|
||||
|
||||
@@ -11,6 +11,7 @@ import MatrixRustSDK
|
||||
|
||||
final class NSEUserSession {
|
||||
let sessionDirectories: SessionDirectories
|
||||
let appSettings: CommonSettingsProtocol
|
||||
|
||||
private let baseClient: Client
|
||||
private let notificationClient: NotificationClient
|
||||
@@ -49,6 +50,7 @@ final class NSEUserSession {
|
||||
appSettings: CommonSettingsProtocol) async throws {
|
||||
sessionDirectories = credentials.restorationToken.sessionDirectories
|
||||
userID = credentials.userID
|
||||
self.appSettings = appSettings
|
||||
|
||||
let homeserverURL = credentials.restorationToken.session.homeserverUrl
|
||||
let clientBuilder = ClientBuilder
|
||||
|
||||
@@ -28,6 +28,7 @@ struct NotificationContentBuilder {
|
||||
mediaProvider: MediaProviderProtocol) async {
|
||||
notificationContent.receiverID = notificationItem.receiverID
|
||||
notificationContent.roomID = notificationItem.roomID
|
||||
notificationContent.threadRootEventID = notificationItem.threadRootEventID
|
||||
|
||||
switch notificationItem.event {
|
||||
case .timeline(let event):
|
||||
@@ -36,9 +37,16 @@ struct NotificationContentBuilder {
|
||||
notificationContent.eventID = nil
|
||||
}
|
||||
|
||||
// So that the UI groups notification that are received for the same room but also for the same user
|
||||
// So that the UI groups notification that are received for the same room/thread but also for the same user
|
||||
let threadIdentifier = if userSession.appSettings.threadsEnabled, let threadRootEventID = notificationItem.threadRootEventID {
|
||||
// If a threaded message we group notifications also by thread root id
|
||||
"\(notificationItem.receiverID)\(notificationItem.roomID)\(threadRootEventID)"
|
||||
} else {
|
||||
// otherwise only by room and receiver id
|
||||
"\(notificationItem.receiverID)\(notificationItem.roomID)"
|
||||
}
|
||||
// Removing the @ fixes an iOS bug where the notification crashes if the mute button is tapped
|
||||
notificationContent.threadIdentifier = "\(notificationItem.receiverID)\(notificationItem.roomID)".replacingOccurrences(of: "@", with: "")
|
||||
notificationContent.threadIdentifier = threadIdentifier.replacingOccurrences(of: "@", with: "")
|
||||
|
||||
MXLog.info("isNoisy: \(notificationItem.isNoisy)")
|
||||
notificationContent.sound = notificationItem.isNoisy ? UNNotificationSound(named: UNNotificationSoundName(rawValue: "message.caf")) : nil
|
||||
@@ -133,10 +141,23 @@ struct NotificationContentBuilder {
|
||||
|
||||
private func icon(for notificationItem: NotificationItemProxyProtocol) -> NotificationIcon {
|
||||
if notificationItem.isDM {
|
||||
return NotificationIcon(mediaSource: notificationItem.senderAvatarMediaSource, groupInfo: nil)
|
||||
if userSession.appSettings.threadsEnabled, let threadRootEventID = notificationItem.threadRootEventID {
|
||||
.init(mediaSource: notificationItem.senderAvatarMediaSource,
|
||||
groupInfo: .init(name: L10n.commonThread,
|
||||
id: "\(notificationItem.roomID)\(threadRootEventID)"))
|
||||
} else {
|
||||
return NotificationIcon(mediaSource: notificationItem.roomAvatarMediaSource,
|
||||
groupInfo: .init(name: notificationItem.roomDisplayName, id: notificationItem.roomID))
|
||||
.init(mediaSource: notificationItem.senderAvatarMediaSource, groupInfo: nil)
|
||||
}
|
||||
} else {
|
||||
if userSession.appSettings.threadsEnabled, let threadRootEventID = notificationItem.threadRootEventID {
|
||||
.init(mediaSource: notificationItem.roomAvatarMediaSource,
|
||||
groupInfo: .init(name: L10n.notificationThreadInRoom(notificationItem.roomDisplayName),
|
||||
id: "\(notificationItem.roomID)\(threadRootEventID)"))
|
||||
} else {
|
||||
.init(mediaSource: notificationItem.roomAvatarMediaSource,
|
||||
groupInfo: .init(name: notificationItem.roomDisplayName,
|
||||
id: notificationItem.roomID))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user