Invitation Notification design improvements and navigation (#1091)

* invitation notification

* handling navigation to the invite view

* improved invite notifications

# Conflicts:
#	NSE/Sources/NotificationServiceExtension.swift

* improved the design

* improved the navigation handling on iPad vs iPhone

* comment

* code improvement

* better doc
This commit is contained in:
Mauro
2023-06-19 10:03:32 +02:00
committed by GitHub
parent 4de39201ed
commit 1b90060c9e
8 changed files with 89 additions and 39 deletions
@@ -155,8 +155,15 @@ class AppCoordinator: AppCoordinatorProtocol, AuthenticationCoordinatorDelegate,
} }
// Handle here the account switching when available // Handle here the account switching when available
handleAppRoute(.room(roomID: roomID)) switch content.categoryIdentifier {
case NotificationConstants.Category.message:
handleAppRoute(.room(roomID: roomID))
case NotificationConstants.Category.invite:
handleAppRoute(.invites)
default:
break
}
} }
func handleInlineReply(_ service: NotificationManagerProtocol, content: UNNotificationContent, replyText: String) async { func handleInlineReply(_ service: NotificationManagerProtocol, content: UNNotificationContent, replyText: String) async {
@@ -19,4 +19,5 @@ import Foundation
@MainActor @MainActor
protocol FlowCoordinatorProtocol { protocol FlowCoordinatorProtocol {
func handleAppRoute(_ appRoute: AppRoute, animated: Bool) func handleAppRoute(_ appRoute: AppRoute, animated: Bool)
func clearRoute(animated: Bool)
} }
@@ -22,6 +22,7 @@ enum AppRoute {
case roomList case roomList
case room(roomID: String) case room(roomID: String)
case roomDetails(roomID: String) case roomDetails(roomID: String)
case invites
} }
struct AppRouterManager { struct AppRouterManager {
@@ -71,8 +71,17 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol {
stateMachine.tryEvent(.presentRoomDetails(roomID: roomID), userInfo: EventUserInfo(animated: animated)) stateMachine.tryEvent(.presentRoomDetails(roomID: roomID), userInfo: EventUserInfo(animated: animated))
case .roomList: case .roomList:
stateMachine.tryEvent(.dismissRoom, userInfo: EventUserInfo(animated: animated)) stateMachine.tryEvent(.dismissRoom, userInfo: EventUserInfo(animated: animated))
case .invites:
break
} }
} }
func clearRoute(animated: Bool) {
guard stateMachine.state != .initial else {
return
}
stateMachine.tryEvent(.dismissRoom, userInfo: EventUserInfo(animated: animated))
}
// MARK: - Private // MARK: - Private
@@ -290,10 +299,10 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol {
} }
private func dismissRoom(animated: Bool) { private func dismissRoom(animated: Bool) {
// The room isn't in the same navigation stack of the home screen. // Setting the detail coordinator to nil afirst allows the dismiss to work properly
// Animating the popToRoot causes weird animations on when the room is left from room's details // if followed immediately by another navigation on iPhone
navigationStackCoordinator.popToRoot(animated: false)
navigationSplitCoordinator.setDetailCoordinator(nil, animated: animated) navigationSplitCoordinator.setDetailCoordinator(nil, animated: animated)
navigationStackCoordinator.popToRoot(animated: false)
roomProxy = nil roomProxy = nil
timelineController = nil timelineController = nil
@@ -91,13 +91,22 @@ class UserSessionFlowCoordinator: FlowCoordinatorProtocol {
case .roomList, .initial: case .roomList, .initial:
break break
} }
switch appRoute { switch appRoute {
case .room, .roomDetails, .roomList: case .room, .roomDetails, .roomList:
roomFlowCoordinator.handleAppRoute(appRoute, animated: animated) roomFlowCoordinator.handleAppRoute(appRoute, animated: animated)
case .invites:
if UIDevice.current.isPhone {
roomFlowCoordinator.clearRoute(animated: animated)
}
stateMachine.processEvent(.showInvitesScreen, userInfo: .init(animated: animated))
} }
} }
func clearRoute(animated: Bool) {
fatalError("not necessary as of right now")
}
// MARK: - Private // MARK: - Private
// swiftlint:disable:next cyclomatic_complexity // swiftlint:disable:next cyclomatic_complexity
@@ -141,6 +150,8 @@ class UserSessionFlowCoordinator: FlowCoordinatorProtocol {
case (.roomList, .showInvitesScreen, .invitesScreen): case (.roomList, .showInvitesScreen, .invitesScreen):
self.presentInvitesList(animated: animated) self.presentInvitesList(animated: animated)
case (.invitesScreen, .showInvitesScreen, .invitesScreen):
break
case (.invitesScreen, .closedInvitesScreen, .roomList): case (.invitesScreen, .closedInvitesScreen, .roomList):
break break
@@ -131,6 +131,9 @@ class UserSessionFlowCoordinatorStateMachine {
case (.showInvitesScreen, .roomList(let selectedRoomId)): case (.showInvitesScreen, .roomList(let selectedRoomId)):
return .invitesScreen(selectedRoomId: selectedRoomId) return .invitesScreen(selectedRoomId: selectedRoomId)
case (.showInvitesScreen, .invitesScreen(let selectedRoomId)):
return .invitesScreen(selectedRoomId: selectedRoomId)
case (.closedInvitesScreen, .invitesScreen(let selectedRoomId)): case (.closedInvitesScreen, .invitesScreen(let selectedRoomId)):
return .roomList(selectedRoomId: selectedRoomId) return .roomList(selectedRoomId: selectedRoomId)
@@ -18,24 +18,13 @@ import Foundation
import Intents import Intents
import UserNotifications import UserNotifications
enum NotificationIconType { struct NotificationIcon {
case sender(mediaSource: MediaSourceProxy?) let mediaSource: MediaSourceProxy?
case group(mediaSource: MediaSourceProxy?, groupName: String) // Required as the key to set images for groups
let groupName: String?
var mediaSource: MediaSourceProxy? { var shouldDisplayAsGroup: Bool {
switch self { groupName != nil
case .group(let mediaSource, _), .sender(let mediaSource):
return mediaSource
}
}
var isSenderIcon: Bool {
switch self {
case .sender:
return true
case .group:
return false
}
} }
} }
@@ -122,9 +111,9 @@ extension UNMutableNotificationContent {
func addSenderIcon(using mediaProvider: MediaProviderProtocol?, func addSenderIcon(using mediaProvider: MediaProviderProtocol?,
senderID: String, senderID: String,
senderName: String, senderName: String,
iconType: NotificationIconType) async throws -> UNMutableNotificationContent { icon: NotificationIcon) async throws -> UNMutableNotificationContent {
var image = INImage(named: "") var image = INImage(named: "")
if let mediaSource = iconType.mediaSource { if let mediaSource = icon.mediaSource {
switch await mediaProvider?.loadImageDataFromSource(mediaSource) { switch await mediaProvider?.loadImageDataFromSource(mediaSource) {
case .success(let data): case .success(let data):
image = INImage(imageData: data) image = INImage(imageData: data)
@@ -139,14 +128,14 @@ extension UNMutableNotificationContent {
let sender = INPerson(personHandle: senderHandle, let sender = INPerson(personHandle: senderHandle,
nameComponents: nil, nameComponents: nil,
displayName: senderName, displayName: senderName,
image: iconType.isSenderIcon ? image : nil, image: !icon.shouldDisplayAsGroup ? image : nil,
contactIdentifier: nil, contactIdentifier: nil,
customIdentifier: nil) customIdentifier: nil)
// These are required to show the group name as subtitle // These are required to show the group name as subtitle
var speakableGroupName: INSpeakableString? var speakableGroupName: INSpeakableString?
var recipients: [INPerson]? var recipients: [INPerson]?
if case let .group(_, groupName) = iconType { if let groupName = icon.groupName {
let meHandle = INPersonHandle(value: receiverID, type: .unknown) let meHandle = INPersonHandle(value: receiverID, type: .unknown)
let me = INPerson(personHandle: meHandle, nameComponents: nil, displayName: nil, image: nil, contactIdentifier: nil, customIdentifier: nil, isMe: true) let me = INPerson(personHandle: meHandle, nameComponents: nil, displayName: nil, image: nil, contactIdentifier: nil, customIdentifier: nil, isMe: true)
speakableGroupName = INSpeakableString(spokenPhrase: groupName) speakableGroupName = INSpeakableString(spokenPhrase: groupName)
@@ -41,7 +41,11 @@ protocol NotificationItemProxyProtocol {
var isDirect: Bool { get } var isDirect: Bool { get }
var isEncrypted: Bool? { get } /// Returns `true` if the event of the notification belongs to an encrypted room
var isRoomEncrypted: Bool? { get }
/// Returns `true` if was not possible to decrypt the notification content
var isEncrypted: Bool { get }
} }
extension NotificationItemProxyProtocol { extension NotificationItemProxyProtocol {
@@ -87,7 +91,7 @@ struct NotificationItemProxy: NotificationItemProxyProtocol {
notificationItem.isDirect notificationItem.isDirect
} }
var isEncrypted: Bool? { var isRoomEncrypted: Bool? {
notificationItem.isEncrypted notificationItem.isEncrypted
} }
@@ -106,6 +110,20 @@ struct NotificationItemProxy: NotificationItemProxyProtocol {
} }
return nil return nil
} }
var isEncrypted: Bool {
switch event.type {
case .messageLike(let content):
switch content {
case .roomEncrypted:
return true
default:
return false
}
default:
return false
}
}
} }
struct EmptyNotificationItemProxy: NotificationItemProxyProtocol { struct EmptyNotificationItemProxy: NotificationItemProxyProtocol {
@@ -133,13 +151,15 @@ struct EmptyNotificationItemProxy: NotificationItemProxyProtocol {
var isDirect: Bool { false } var isDirect: Bool { false }
var isEncrypted: Bool? { nil } var isRoomEncrypted: Bool? { nil }
var senderAvatarMediaSource: MediaSourceProxy? { nil } var senderAvatarMediaSource: MediaSourceProxy? { nil }
var roomAvatarMediaSource: MediaSourceProxy? { nil } var roomAvatarMediaSource: MediaSourceProxy? { nil }
var notificationIdentifier: String { "" } var notificationIdentifier: String { "" }
var isEncrypted: Bool { false }
} }
extension NotificationItemProxyProtocol { extension NotificationItemProxyProtocol {
@@ -228,13 +248,22 @@ extension NotificationItemProxyProtocol {
notification.categoryIdentifier = NotificationConstants.Category.invite notification.categoryIdentifier = NotificationConstants.Category.invite
// Sadly as of right now we can't get from the NSE context any information for invited rooms, so we will only display the user name and a simple message let icon: NotificationIcon
let iconType = NotificationIconType.sender(mediaSource: senderAvatarMediaSource) let body: String
if !isDirect {
icon = NotificationIcon(mediaSource: roomAvatarMediaSource, groupName: roomDisplayName)
body = L10n.notificationRoomInviteBody
} else {
icon = NotificationIcon(mediaSource: senderAvatarMediaSource, groupName: nil)
body = L10n.notificationInviteBody
}
notification = try await notification.addSenderIcon(using: mediaProvider, notification = try await notification.addSenderIcon(using: mediaProvider,
senderID: event.senderID, senderID: event.senderID,
senderName: senderDisplayName ?? event.senderID, senderName: senderDisplayName ?? roomDisplayName,
iconType: iconType) icon: icon)
notification.body = L10n.notificationInviteBody notification.body = body
return notification return notification
} }
@@ -274,17 +303,17 @@ extension NotificationItemProxyProtocol {
notification.categoryIdentifier = NotificationConstants.Category.message notification.categoryIdentifier = NotificationConstants.Category.message
let senderName = senderDisplayName ?? roomDisplayName let senderName = senderDisplayName ?? roomDisplayName
let iconType: NotificationIconType let icon: NotificationIcon
if !isDirect { if !isDirect {
iconType = .group(mediaSource: roomAvatarMediaSource, groupName: roomDisplayName) icon = NotificationIcon(mediaSource: roomAvatarMediaSource, groupName: roomDisplayName)
} else { } else {
iconType = .sender(mediaSource: senderAvatarMediaSource) icon = NotificationIcon(mediaSource: senderAvatarMediaSource, groupName: nil)
} }
notification = try await notification.addSenderIcon(using: mediaProvider, notification = try await notification.addSenderIcon(using: mediaProvider,
senderID: event.senderID, senderID: event.senderID,
senderName: senderName, senderName: senderName,
iconType: iconType) icon: icon)
return notification return notification
} }