mirror of
https://github.com/Novaloop-AG/element-x-ios.git
synced 2026-11-03 16:10:20 +00:00
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:
@@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user