mirror of
https://github.com/Novaloop-AG/element-x-ios.git
synced 2026-11-02 12:07:55 +00:00
Require acknowledgement to send to verified users who have unsigned devices or have changed their identity. (#3215)
* Refactor HeroImage style. * Add a screen to resolve (crypto-related) timeline item send failures. * Refactor send failures. * Update the SDK.
This commit is contained in:
@@ -349,6 +349,11 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol {
|
||||
case (.roomDetails, .presentRoomMemberDetails(let userID)):
|
||||
return .roomMemberDetails(userID: userID, previousState: fromState)
|
||||
|
||||
case (.room, .presentResolveSendFailure):
|
||||
return .resolveSendFailure
|
||||
case (.resolveSendFailure, .dismissResolveSendFailure):
|
||||
return .room
|
||||
|
||||
// Child flow
|
||||
|
||||
case (_, .startChildFlow(let roomID, _, _)):
|
||||
@@ -499,6 +504,11 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol {
|
||||
case (.roomMemberDetails, .dismissUserProfile, .roomDetails):
|
||||
break
|
||||
|
||||
case (.room, .presentResolveSendFailure(let failure, let itemID), .resolveSendFailure):
|
||||
presentResolveSendFailure(failure: failure, itemID: itemID)
|
||||
case (.resolveSendFailure, .dismissResolveSendFailure, .room):
|
||||
break
|
||||
|
||||
// Child flow
|
||||
case (_, .startChildFlow(let roomID, let via, let entryPoint), .presentingChild):
|
||||
Task { await self.startChildFlow(for: roomID, via: via, entryPoint: entryPoint) }
|
||||
@@ -623,6 +633,8 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol {
|
||||
actionsSubject.send(.presentCallScreen(roomProxy: roomProxy))
|
||||
case .presentPinnedEventsTimeline:
|
||||
stateMachine.tryEvent(.presentPinnedEventsTimeline)
|
||||
case .presentResolveSendFailure(failure: let failure, itemID: let itemID):
|
||||
stateMachine.tryEvent(.presentResolveSendFailure(failure: failure, itemID: itemID))
|
||||
}
|
||||
}
|
||||
.store(in: &cancellables)
|
||||
@@ -1351,6 +1363,25 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol {
|
||||
coordinator.start()
|
||||
}
|
||||
|
||||
private func presentResolveSendFailure(failure: TimelineItemSendFailure.VerifiedUser, itemID: TimelineItemIdentifier) {
|
||||
let coordinator = ResolveVerifiedUserSendFailureScreenCoordinator(parameters: .init(failure: failure,
|
||||
itemID: itemID,
|
||||
roomProxy: roomProxy))
|
||||
coordinator.actionsPublisher.sink { [weak self] action in
|
||||
guard let self else { return }
|
||||
|
||||
switch action {
|
||||
case .dismiss:
|
||||
navigationStackCoordinator.setSheetCoordinator(nil)
|
||||
}
|
||||
}
|
||||
.store(in: &cancellables)
|
||||
|
||||
navigationStackCoordinator.setSheetCoordinator(coordinator) { [weak self] in
|
||||
self?.stateMachine.tryEvent(.dismissResolveSendFailure)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Child Flow
|
||||
|
||||
private func startChildFlow(for roomID: String, via: [String], entryPoint: RoomFlowCoordinatorEntryPoint) async {
|
||||
@@ -1425,6 +1456,7 @@ private extension RoomFlowCoordinator {
|
||||
case pollsHistoryForm
|
||||
case rolesAndPermissions
|
||||
case pinnedEventsTimeline(previousState: PinnedEventsTimelineSource)
|
||||
case resolveSendFailure
|
||||
|
||||
/// A child flow is in progress.
|
||||
case presentingChild(childRoomID: String, previousState: State)
|
||||
@@ -1497,6 +1529,9 @@ private extension RoomFlowCoordinator {
|
||||
case presentPinnedEventsTimeline
|
||||
case dismissPinnedEventsTimeline
|
||||
|
||||
case presentResolveSendFailure(failure: TimelineItemSendFailure.VerifiedUser, itemID: TimelineItemIdentifier)
|
||||
case dismissResolveSendFailure
|
||||
|
||||
// Child room flow events
|
||||
case startChildFlow(roomID: String, via: [String], entryPoint: RoomFlowCoordinatorEntryPoint)
|
||||
case dismissChildFlow
|
||||
|
||||
@@ -84,6 +84,8 @@ internal enum L10n {
|
||||
internal static var actionCall: String { return L10n.tr("Localizable", "action_call") }
|
||||
/// Cancel
|
||||
internal static var actionCancel: String { return L10n.tr("Localizable", "action_cancel") }
|
||||
/// Cancel for now
|
||||
internal static var actionCancelForNow: String { return L10n.tr("Localizable", "action_cancel_for_now") }
|
||||
/// Choose photo
|
||||
internal static var actionChoosePhoto: String { return L10n.tr("Localizable", "action_choose_photo") }
|
||||
/// Clear
|
||||
@@ -244,6 +246,14 @@ internal enum L10n {
|
||||
internal static var actionViewSource: String { return L10n.tr("Localizable", "action_view_source") }
|
||||
/// Yes
|
||||
internal static var actionYes: String { return L10n.tr("Localizable", "action_yes") }
|
||||
/// Log Out & Upgrade
|
||||
internal static var bannerMigrateToNativeSlidingSyncAction: String { return L10n.tr("Localizable", "banner_migrate_to_native_sliding_sync_action") }
|
||||
/// Your server now supports a new, faster protocol. Log out and log back in to upgrade now. Doing this now will help you avoid a forced logout when the old protocol is removed later.
|
||||
internal static var bannerMigrateToNativeSlidingSyncDescription: String { return L10n.tr("Localizable", "banner_migrate_to_native_sliding_sync_description") }
|
||||
/// Your homeserver no longer supports the old protocol. Please log out and log back in to continue using the app.
|
||||
internal static var bannerMigrateToNativeSlidingSyncForceLogoutTitle: String { return L10n.tr("Localizable", "banner_migrate_to_native_sliding_sync_force_logout_title") }
|
||||
/// Upgrade available
|
||||
internal static var bannerMigrateToNativeSlidingSyncTitle: String { return L10n.tr("Localizable", "banner_migrate_to_native_sliding_sync_title") }
|
||||
/// About
|
||||
internal static var commonAbout: String { return L10n.tr("Localizable", "common_about") }
|
||||
/// Acceptable use policy
|
||||
@@ -1383,8 +1393,6 @@ internal enum L10n {
|
||||
internal static var screenRecoveryKeyConfirmErrorTitle: String { return L10n.tr("Localizable", "screen_recovery_key_confirm_error_title") }
|
||||
/// If you have a security key or security phrase, this will work too.
|
||||
internal static var screenRecoveryKeyConfirmKeyDescription: String { return L10n.tr("Localizable", "screen_recovery_key_confirm_key_description") }
|
||||
/// Recovery key or passcode
|
||||
internal static var screenRecoveryKeyConfirmKeyLabel: String { return L10n.tr("Localizable", "screen_recovery_key_confirm_key_label") }
|
||||
/// Enter…
|
||||
internal static var screenRecoveryKeyConfirmKeyPlaceholder: String { return L10n.tr("Localizable", "screen_recovery_key_confirm_key_placeholder") }
|
||||
/// Lost your recovery key?
|
||||
@@ -1447,6 +1455,26 @@ internal enum L10n {
|
||||
}
|
||||
/// Can't confirm? Go to your account to reset your identity.
|
||||
internal static var screenResetIdentityConfirmationTitle: String { return L10n.tr("Localizable", "screen_reset_identity_confirmation_title") }
|
||||
/// Withdraw verification and send
|
||||
internal static var screenResolveSendFailureChangedIdentityPrimaryButtonTitle: String { return L10n.tr("Localizable", "screen_resolve_send_failure_changed_identity_primary_button_title") }
|
||||
/// You can withdraw your verification and send this message anyway, or you can cancel for now and try again later after reverifying %1$@.
|
||||
internal static func screenResolveSendFailureChangedIdentitySubtitle(_ p1: Any) -> String {
|
||||
return L10n.tr("Localizable", "screen_resolve_send_failure_changed_identity_subtitle", String(describing: p1))
|
||||
}
|
||||
/// Your message was not sent because %1$@’s verified identity has changed
|
||||
internal static func screenResolveSendFailureChangedIdentityTitle(_ p1: Any) -> String {
|
||||
return L10n.tr("Localizable", "screen_resolve_send_failure_changed_identity_title", String(describing: p1))
|
||||
}
|
||||
/// Send message anyway
|
||||
internal static var screenResolveSendFailureUnsignedDevicePrimaryButtonTitle: String { return L10n.tr("Localizable", "screen_resolve_send_failure_unsigned_device_primary_button_title") }
|
||||
/// %1$@ is using one or more unverified devices. You can send the message anyway, or you can cancel for now and try again later after %2$@ has verified all their devices.
|
||||
internal static func screenResolveSendFailureUnsignedDeviceSubtitle(_ p1: Any, _ p2: Any) -> String {
|
||||
return L10n.tr("Localizable", "screen_resolve_send_failure_unsigned_device_subtitle", String(describing: p1), String(describing: p2))
|
||||
}
|
||||
/// Your message was not sent because %1$@ has not verified one or more devices
|
||||
internal static func screenResolveSendFailureUnsignedDeviceTitle(_ p1: Any) -> String {
|
||||
return L10n.tr("Localizable", "screen_resolve_send_failure_unsigned_device_title", String(describing: p1))
|
||||
}
|
||||
/// Failed to resolve room alias.
|
||||
internal static var screenRoomAliasResolverResolveAliasFailure: String { return L10n.tr("Localizable", "screen_room_alias_resolver_resolve_alias_failure") }
|
||||
/// Camera
|
||||
@@ -1945,22 +1973,16 @@ internal enum L10n {
|
||||
internal static var screenSignoutSaveRecoveryKeyTitle: String { return L10n.tr("Localizable", "screen_signout_save_recovery_key_title") }
|
||||
/// An error occurred when trying to start a chat
|
||||
internal static var screenStartChatErrorStartingChat: String { return L10n.tr("Localizable", "screen_start_chat_error_starting_chat") }
|
||||
/// Message not sent because %1$@’s verified identity has changed.
|
||||
internal static func screenTimelineItemMenuSendFailureChangedIdentity(_ p1: Any) -> String {
|
||||
return L10n.tr("Localizable", "screen_timeline_item_menu_send_failure_changed_identity", String(describing: p1))
|
||||
}
|
||||
/// Message not sent because %1$@ has not verified one or more devices.
|
||||
internal static func screenTimelineItemMenuSendFailureUnsignedDevice(_ p1: Any) -> String {
|
||||
return L10n.tr("Localizable", "screen_timeline_item_menu_send_failure_unsigned_device", String(describing: p1))
|
||||
}
|
||||
/// Location
|
||||
internal static var screenViewLocationTitle: String { return L10n.tr("Localizable", "screen_view_location_title") }
|
||||
/// There's a high demand for %1$@ on %2$@ at the moment. Come back to the app in a few days and try again.
|
||||
///
|
||||
/// Thanks for your patience!
|
||||
internal static func screenWaitlistMessage(_ p1: Any, _ p2: Any) -> String {
|
||||
return L10n.tr("Localizable", "screen_waitlist_message", String(describing: p1), String(describing: p2))
|
||||
}
|
||||
/// Welcome to %1$@!
|
||||
internal static func screenWaitlistMessageSuccess(_ p1: Any) -> String {
|
||||
return L10n.tr("Localizable", "screen_waitlist_message_success", String(describing: p1))
|
||||
}
|
||||
/// You’re almost there.
|
||||
internal static var screenWaitlistTitle: String { return L10n.tr("Localizable", "screen_waitlist_title") }
|
||||
/// You're in.
|
||||
internal static var screenWaitlistTitleSuccess: String { return L10n.tr("Localizable", "screen_waitlist_title_success") }
|
||||
/// Calls, polls, search and more will be added later this year.
|
||||
internal static var screenWelcomeBullet1: String { return L10n.tr("Localizable", "screen_welcome_bullet_1") }
|
||||
/// Message history for encrypted rooms isn’t available yet.
|
||||
|
||||
@@ -6563,6 +6563,216 @@ class JoinedRoomProxyMock: JoinedRoomProxyProtocol {
|
||||
return sendTypingNotificationIsTypingReturnValue
|
||||
}
|
||||
}
|
||||
//MARK: - resend
|
||||
|
||||
var resendItemIDUnderlyingCallsCount = 0
|
||||
var resendItemIDCallsCount: Int {
|
||||
get {
|
||||
if Thread.isMainThread {
|
||||
return resendItemIDUnderlyingCallsCount
|
||||
} else {
|
||||
var returnValue: Int? = nil
|
||||
DispatchQueue.main.sync {
|
||||
returnValue = resendItemIDUnderlyingCallsCount
|
||||
}
|
||||
|
||||
return returnValue!
|
||||
}
|
||||
}
|
||||
set {
|
||||
if Thread.isMainThread {
|
||||
resendItemIDUnderlyingCallsCount = newValue
|
||||
} else {
|
||||
DispatchQueue.main.sync {
|
||||
resendItemIDUnderlyingCallsCount = newValue
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
var resendItemIDCalled: Bool {
|
||||
return resendItemIDCallsCount > 0
|
||||
}
|
||||
var resendItemIDReceivedItemID: TimelineItemIdentifier?
|
||||
var resendItemIDReceivedInvocations: [TimelineItemIdentifier] = []
|
||||
|
||||
var resendItemIDUnderlyingReturnValue: Result<Void, RoomProxyError>!
|
||||
var resendItemIDReturnValue: Result<Void, RoomProxyError>! {
|
||||
get {
|
||||
if Thread.isMainThread {
|
||||
return resendItemIDUnderlyingReturnValue
|
||||
} else {
|
||||
var returnValue: Result<Void, RoomProxyError>? = nil
|
||||
DispatchQueue.main.sync {
|
||||
returnValue = resendItemIDUnderlyingReturnValue
|
||||
}
|
||||
|
||||
return returnValue!
|
||||
}
|
||||
}
|
||||
set {
|
||||
if Thread.isMainThread {
|
||||
resendItemIDUnderlyingReturnValue = newValue
|
||||
} else {
|
||||
DispatchQueue.main.sync {
|
||||
resendItemIDUnderlyingReturnValue = newValue
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
var resendItemIDClosure: ((TimelineItemIdentifier) async -> Result<Void, RoomProxyError>)?
|
||||
|
||||
func resend(itemID: TimelineItemIdentifier) async -> Result<Void, RoomProxyError> {
|
||||
resendItemIDCallsCount += 1
|
||||
resendItemIDReceivedItemID = itemID
|
||||
DispatchQueue.main.async {
|
||||
self.resendItemIDReceivedInvocations.append(itemID)
|
||||
}
|
||||
if let resendItemIDClosure = resendItemIDClosure {
|
||||
return await resendItemIDClosure(itemID)
|
||||
} else {
|
||||
return resendItemIDReturnValue
|
||||
}
|
||||
}
|
||||
//MARK: - ignoreDeviceTrustAndResend
|
||||
|
||||
var ignoreDeviceTrustAndResendDevicesItemIDUnderlyingCallsCount = 0
|
||||
var ignoreDeviceTrustAndResendDevicesItemIDCallsCount: Int {
|
||||
get {
|
||||
if Thread.isMainThread {
|
||||
return ignoreDeviceTrustAndResendDevicesItemIDUnderlyingCallsCount
|
||||
} else {
|
||||
var returnValue: Int? = nil
|
||||
DispatchQueue.main.sync {
|
||||
returnValue = ignoreDeviceTrustAndResendDevicesItemIDUnderlyingCallsCount
|
||||
}
|
||||
|
||||
return returnValue!
|
||||
}
|
||||
}
|
||||
set {
|
||||
if Thread.isMainThread {
|
||||
ignoreDeviceTrustAndResendDevicesItemIDUnderlyingCallsCount = newValue
|
||||
} else {
|
||||
DispatchQueue.main.sync {
|
||||
ignoreDeviceTrustAndResendDevicesItemIDUnderlyingCallsCount = newValue
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
var ignoreDeviceTrustAndResendDevicesItemIDCalled: Bool {
|
||||
return ignoreDeviceTrustAndResendDevicesItemIDCallsCount > 0
|
||||
}
|
||||
var ignoreDeviceTrustAndResendDevicesItemIDReceivedArguments: (devices: [String: [String]], itemID: TimelineItemIdentifier)?
|
||||
var ignoreDeviceTrustAndResendDevicesItemIDReceivedInvocations: [(devices: [String: [String]], itemID: TimelineItemIdentifier)] = []
|
||||
|
||||
var ignoreDeviceTrustAndResendDevicesItemIDUnderlyingReturnValue: Result<Void, RoomProxyError>!
|
||||
var ignoreDeviceTrustAndResendDevicesItemIDReturnValue: Result<Void, RoomProxyError>! {
|
||||
get {
|
||||
if Thread.isMainThread {
|
||||
return ignoreDeviceTrustAndResendDevicesItemIDUnderlyingReturnValue
|
||||
} else {
|
||||
var returnValue: Result<Void, RoomProxyError>? = nil
|
||||
DispatchQueue.main.sync {
|
||||
returnValue = ignoreDeviceTrustAndResendDevicesItemIDUnderlyingReturnValue
|
||||
}
|
||||
|
||||
return returnValue!
|
||||
}
|
||||
}
|
||||
set {
|
||||
if Thread.isMainThread {
|
||||
ignoreDeviceTrustAndResendDevicesItemIDUnderlyingReturnValue = newValue
|
||||
} else {
|
||||
DispatchQueue.main.sync {
|
||||
ignoreDeviceTrustAndResendDevicesItemIDUnderlyingReturnValue = newValue
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
var ignoreDeviceTrustAndResendDevicesItemIDClosure: (([String: [String]], TimelineItemIdentifier) async -> Result<Void, RoomProxyError>)?
|
||||
|
||||
func ignoreDeviceTrustAndResend(devices: [String: [String]], itemID: TimelineItemIdentifier) async -> Result<Void, RoomProxyError> {
|
||||
ignoreDeviceTrustAndResendDevicesItemIDCallsCount += 1
|
||||
ignoreDeviceTrustAndResendDevicesItemIDReceivedArguments = (devices: devices, itemID: itemID)
|
||||
DispatchQueue.main.async {
|
||||
self.ignoreDeviceTrustAndResendDevicesItemIDReceivedInvocations.append((devices: devices, itemID: itemID))
|
||||
}
|
||||
if let ignoreDeviceTrustAndResendDevicesItemIDClosure = ignoreDeviceTrustAndResendDevicesItemIDClosure {
|
||||
return await ignoreDeviceTrustAndResendDevicesItemIDClosure(devices, itemID)
|
||||
} else {
|
||||
return ignoreDeviceTrustAndResendDevicesItemIDReturnValue
|
||||
}
|
||||
}
|
||||
//MARK: - withdrawVerificationAndResend
|
||||
|
||||
var withdrawVerificationAndResendUserIDsItemIDUnderlyingCallsCount = 0
|
||||
var withdrawVerificationAndResendUserIDsItemIDCallsCount: Int {
|
||||
get {
|
||||
if Thread.isMainThread {
|
||||
return withdrawVerificationAndResendUserIDsItemIDUnderlyingCallsCount
|
||||
} else {
|
||||
var returnValue: Int? = nil
|
||||
DispatchQueue.main.sync {
|
||||
returnValue = withdrawVerificationAndResendUserIDsItemIDUnderlyingCallsCount
|
||||
}
|
||||
|
||||
return returnValue!
|
||||
}
|
||||
}
|
||||
set {
|
||||
if Thread.isMainThread {
|
||||
withdrawVerificationAndResendUserIDsItemIDUnderlyingCallsCount = newValue
|
||||
} else {
|
||||
DispatchQueue.main.sync {
|
||||
withdrawVerificationAndResendUserIDsItemIDUnderlyingCallsCount = newValue
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
var withdrawVerificationAndResendUserIDsItemIDCalled: Bool {
|
||||
return withdrawVerificationAndResendUserIDsItemIDCallsCount > 0
|
||||
}
|
||||
var withdrawVerificationAndResendUserIDsItemIDReceivedArguments: (userIDs: [String], itemID: TimelineItemIdentifier)?
|
||||
var withdrawVerificationAndResendUserIDsItemIDReceivedInvocations: [(userIDs: [String], itemID: TimelineItemIdentifier)] = []
|
||||
|
||||
var withdrawVerificationAndResendUserIDsItemIDUnderlyingReturnValue: Result<Void, RoomProxyError>!
|
||||
var withdrawVerificationAndResendUserIDsItemIDReturnValue: Result<Void, RoomProxyError>! {
|
||||
get {
|
||||
if Thread.isMainThread {
|
||||
return withdrawVerificationAndResendUserIDsItemIDUnderlyingReturnValue
|
||||
} else {
|
||||
var returnValue: Result<Void, RoomProxyError>? = nil
|
||||
DispatchQueue.main.sync {
|
||||
returnValue = withdrawVerificationAndResendUserIDsItemIDUnderlyingReturnValue
|
||||
}
|
||||
|
||||
return returnValue!
|
||||
}
|
||||
}
|
||||
set {
|
||||
if Thread.isMainThread {
|
||||
withdrawVerificationAndResendUserIDsItemIDUnderlyingReturnValue = newValue
|
||||
} else {
|
||||
DispatchQueue.main.sync {
|
||||
withdrawVerificationAndResendUserIDsItemIDUnderlyingReturnValue = newValue
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
var withdrawVerificationAndResendUserIDsItemIDClosure: (([String], TimelineItemIdentifier) async -> Result<Void, RoomProxyError>)?
|
||||
|
||||
func withdrawVerificationAndResend(userIDs: [String], itemID: TimelineItemIdentifier) async -> Result<Void, RoomProxyError> {
|
||||
withdrawVerificationAndResendUserIDsItemIDCallsCount += 1
|
||||
withdrawVerificationAndResendUserIDsItemIDReceivedArguments = (userIDs: userIDs, itemID: itemID)
|
||||
DispatchQueue.main.async {
|
||||
self.withdrawVerificationAndResendUserIDsItemIDReceivedInvocations.append((userIDs: userIDs, itemID: itemID))
|
||||
}
|
||||
if let withdrawVerificationAndResendUserIDsItemIDClosure = withdrawVerificationAndResendUserIDsItemIDClosure {
|
||||
return await withdrawVerificationAndResendUserIDsItemIDClosure(userIDs, itemID)
|
||||
} else {
|
||||
return withdrawVerificationAndResendUserIDsItemIDReturnValue
|
||||
}
|
||||
}
|
||||
//MARK: - flagAsUnread
|
||||
|
||||
var flagAsUnreadUnderlyingCallsCount = 0
|
||||
|
||||
@@ -9974,6 +9974,82 @@ open class NotificationSettingsSDKMock: MatrixRustSDK.NotificationSettings {
|
||||
try await unmuteRoomRoomIdIsEncryptedIsOneToOneClosure?(roomId, isEncrypted, isOneToOne)
|
||||
}
|
||||
}
|
||||
open class OidcAuthorizationDataSDKMock: MatrixRustSDK.OidcAuthorizationData {
|
||||
init() {
|
||||
super.init(noPointer: .init())
|
||||
}
|
||||
|
||||
public required init(unsafeFromRawPointer pointer: UnsafeMutableRawPointer) {
|
||||
fatalError("init(unsafeFromRawPointer:) has not been implemented")
|
||||
}
|
||||
|
||||
fileprivate var pointer: UnsafeMutableRawPointer!
|
||||
|
||||
//MARK: - loginUrl
|
||||
|
||||
var loginUrlUnderlyingCallsCount = 0
|
||||
open var loginUrlCallsCount: Int {
|
||||
get {
|
||||
if Thread.isMainThread {
|
||||
return loginUrlUnderlyingCallsCount
|
||||
} else {
|
||||
var returnValue: Int? = nil
|
||||
DispatchQueue.main.sync {
|
||||
returnValue = loginUrlUnderlyingCallsCount
|
||||
}
|
||||
|
||||
return returnValue!
|
||||
}
|
||||
}
|
||||
set {
|
||||
if Thread.isMainThread {
|
||||
loginUrlUnderlyingCallsCount = newValue
|
||||
} else {
|
||||
DispatchQueue.main.sync {
|
||||
loginUrlUnderlyingCallsCount = newValue
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
open var loginUrlCalled: Bool {
|
||||
return loginUrlCallsCount > 0
|
||||
}
|
||||
|
||||
var loginUrlUnderlyingReturnValue: String!
|
||||
open var loginUrlReturnValue: String! {
|
||||
get {
|
||||
if Thread.isMainThread {
|
||||
return loginUrlUnderlyingReturnValue
|
||||
} else {
|
||||
var returnValue: String? = nil
|
||||
DispatchQueue.main.sync {
|
||||
returnValue = loginUrlUnderlyingReturnValue
|
||||
}
|
||||
|
||||
return returnValue!
|
||||
}
|
||||
}
|
||||
set {
|
||||
if Thread.isMainThread {
|
||||
loginUrlUnderlyingReturnValue = newValue
|
||||
} else {
|
||||
DispatchQueue.main.sync {
|
||||
loginUrlUnderlyingReturnValue = newValue
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
open var loginUrlClosure: (() -> String)?
|
||||
|
||||
open override func loginUrl() -> String {
|
||||
loginUrlCallsCount += 1
|
||||
if let loginUrlClosure = loginUrlClosure {
|
||||
return loginUrlClosure()
|
||||
} else {
|
||||
return loginUrlReturnValue
|
||||
}
|
||||
}
|
||||
}
|
||||
open class QrCodeDataSDKMock: MatrixRustSDK.QrCodeData {
|
||||
init() {
|
||||
super.init(noPointer: .init())
|
||||
@@ -11588,6 +11664,52 @@ open class RoomSDKMock: MatrixRustSDK.Room {
|
||||
}
|
||||
}
|
||||
|
||||
//MARK: - ignoreDeviceTrustAndResend
|
||||
|
||||
open var ignoreDeviceTrustAndResendDevicesTransactionIdThrowableError: Error?
|
||||
var ignoreDeviceTrustAndResendDevicesTransactionIdUnderlyingCallsCount = 0
|
||||
open var ignoreDeviceTrustAndResendDevicesTransactionIdCallsCount: Int {
|
||||
get {
|
||||
if Thread.isMainThread {
|
||||
return ignoreDeviceTrustAndResendDevicesTransactionIdUnderlyingCallsCount
|
||||
} else {
|
||||
var returnValue: Int? = nil
|
||||
DispatchQueue.main.sync {
|
||||
returnValue = ignoreDeviceTrustAndResendDevicesTransactionIdUnderlyingCallsCount
|
||||
}
|
||||
|
||||
return returnValue!
|
||||
}
|
||||
}
|
||||
set {
|
||||
if Thread.isMainThread {
|
||||
ignoreDeviceTrustAndResendDevicesTransactionIdUnderlyingCallsCount = newValue
|
||||
} else {
|
||||
DispatchQueue.main.sync {
|
||||
ignoreDeviceTrustAndResendDevicesTransactionIdUnderlyingCallsCount = newValue
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
open var ignoreDeviceTrustAndResendDevicesTransactionIdCalled: Bool {
|
||||
return ignoreDeviceTrustAndResendDevicesTransactionIdCallsCount > 0
|
||||
}
|
||||
open var ignoreDeviceTrustAndResendDevicesTransactionIdReceivedArguments: (devices: [String: [String]], transactionId: String)?
|
||||
open var ignoreDeviceTrustAndResendDevicesTransactionIdReceivedInvocations: [(devices: [String: [String]], transactionId: String)] = []
|
||||
open var ignoreDeviceTrustAndResendDevicesTransactionIdClosure: (([String: [String]], String) async throws -> Void)?
|
||||
|
||||
open override func ignoreDeviceTrustAndResend(devices: [String: [String]], transactionId: String) async throws {
|
||||
if let error = ignoreDeviceTrustAndResendDevicesTransactionIdThrowableError {
|
||||
throw error
|
||||
}
|
||||
ignoreDeviceTrustAndResendDevicesTransactionIdCallsCount += 1
|
||||
ignoreDeviceTrustAndResendDevicesTransactionIdReceivedArguments = (devices: devices, transactionId: transactionId)
|
||||
DispatchQueue.main.async {
|
||||
self.ignoreDeviceTrustAndResendDevicesTransactionIdReceivedInvocations.append((devices: devices, transactionId: transactionId))
|
||||
}
|
||||
try await ignoreDeviceTrustAndResendDevicesTransactionIdClosure?(devices, transactionId)
|
||||
}
|
||||
|
||||
//MARK: - ignoreUser
|
||||
|
||||
open var ignoreUserUserIdThrowableError: Error?
|
||||
@@ -13149,16 +13271,16 @@ open class RoomSDKMock: MatrixRustSDK.Room {
|
||||
|
||||
//MARK: - pinnedEventsTimeline
|
||||
|
||||
open var pinnedEventsTimelineInternalIdPrefixMaxEventsToLoadThrowableError: Error?
|
||||
var pinnedEventsTimelineInternalIdPrefixMaxEventsToLoadUnderlyingCallsCount = 0
|
||||
open var pinnedEventsTimelineInternalIdPrefixMaxEventsToLoadCallsCount: Int {
|
||||
open var pinnedEventsTimelineInternalIdPrefixMaxEventsToLoadMaxConcurrentRequestsThrowableError: Error?
|
||||
var pinnedEventsTimelineInternalIdPrefixMaxEventsToLoadMaxConcurrentRequestsUnderlyingCallsCount = 0
|
||||
open var pinnedEventsTimelineInternalIdPrefixMaxEventsToLoadMaxConcurrentRequestsCallsCount: Int {
|
||||
get {
|
||||
if Thread.isMainThread {
|
||||
return pinnedEventsTimelineInternalIdPrefixMaxEventsToLoadUnderlyingCallsCount
|
||||
return pinnedEventsTimelineInternalIdPrefixMaxEventsToLoadMaxConcurrentRequestsUnderlyingCallsCount
|
||||
} else {
|
||||
var returnValue: Int? = nil
|
||||
DispatchQueue.main.sync {
|
||||
returnValue = pinnedEventsTimelineInternalIdPrefixMaxEventsToLoadUnderlyingCallsCount
|
||||
returnValue = pinnedEventsTimelineInternalIdPrefixMaxEventsToLoadMaxConcurrentRequestsUnderlyingCallsCount
|
||||
}
|
||||
|
||||
return returnValue!
|
||||
@@ -13166,29 +13288,29 @@ open class RoomSDKMock: MatrixRustSDK.Room {
|
||||
}
|
||||
set {
|
||||
if Thread.isMainThread {
|
||||
pinnedEventsTimelineInternalIdPrefixMaxEventsToLoadUnderlyingCallsCount = newValue
|
||||
pinnedEventsTimelineInternalIdPrefixMaxEventsToLoadMaxConcurrentRequestsUnderlyingCallsCount = newValue
|
||||
} else {
|
||||
DispatchQueue.main.sync {
|
||||
pinnedEventsTimelineInternalIdPrefixMaxEventsToLoadUnderlyingCallsCount = newValue
|
||||
pinnedEventsTimelineInternalIdPrefixMaxEventsToLoadMaxConcurrentRequestsUnderlyingCallsCount = newValue
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
open var pinnedEventsTimelineInternalIdPrefixMaxEventsToLoadCalled: Bool {
|
||||
return pinnedEventsTimelineInternalIdPrefixMaxEventsToLoadCallsCount > 0
|
||||
open var pinnedEventsTimelineInternalIdPrefixMaxEventsToLoadMaxConcurrentRequestsCalled: Bool {
|
||||
return pinnedEventsTimelineInternalIdPrefixMaxEventsToLoadMaxConcurrentRequestsCallsCount > 0
|
||||
}
|
||||
open var pinnedEventsTimelineInternalIdPrefixMaxEventsToLoadReceivedArguments: (internalIdPrefix: String?, maxEventsToLoad: UInt16)?
|
||||
open var pinnedEventsTimelineInternalIdPrefixMaxEventsToLoadReceivedInvocations: [(internalIdPrefix: String?, maxEventsToLoad: UInt16)] = []
|
||||
open var pinnedEventsTimelineInternalIdPrefixMaxEventsToLoadMaxConcurrentRequestsReceivedArguments: (internalIdPrefix: String?, maxEventsToLoad: UInt16, maxConcurrentRequests: UInt16)?
|
||||
open var pinnedEventsTimelineInternalIdPrefixMaxEventsToLoadMaxConcurrentRequestsReceivedInvocations: [(internalIdPrefix: String?, maxEventsToLoad: UInt16, maxConcurrentRequests: UInt16)] = []
|
||||
|
||||
var pinnedEventsTimelineInternalIdPrefixMaxEventsToLoadUnderlyingReturnValue: Timeline!
|
||||
open var pinnedEventsTimelineInternalIdPrefixMaxEventsToLoadReturnValue: Timeline! {
|
||||
var pinnedEventsTimelineInternalIdPrefixMaxEventsToLoadMaxConcurrentRequestsUnderlyingReturnValue: Timeline!
|
||||
open var pinnedEventsTimelineInternalIdPrefixMaxEventsToLoadMaxConcurrentRequestsReturnValue: Timeline! {
|
||||
get {
|
||||
if Thread.isMainThread {
|
||||
return pinnedEventsTimelineInternalIdPrefixMaxEventsToLoadUnderlyingReturnValue
|
||||
return pinnedEventsTimelineInternalIdPrefixMaxEventsToLoadMaxConcurrentRequestsUnderlyingReturnValue
|
||||
} else {
|
||||
var returnValue: Timeline? = nil
|
||||
DispatchQueue.main.sync {
|
||||
returnValue = pinnedEventsTimelineInternalIdPrefixMaxEventsToLoadUnderlyingReturnValue
|
||||
returnValue = pinnedEventsTimelineInternalIdPrefixMaxEventsToLoadMaxConcurrentRequestsUnderlyingReturnValue
|
||||
}
|
||||
|
||||
return returnValue!
|
||||
@@ -13196,29 +13318,29 @@ open class RoomSDKMock: MatrixRustSDK.Room {
|
||||
}
|
||||
set {
|
||||
if Thread.isMainThread {
|
||||
pinnedEventsTimelineInternalIdPrefixMaxEventsToLoadUnderlyingReturnValue = newValue
|
||||
pinnedEventsTimelineInternalIdPrefixMaxEventsToLoadMaxConcurrentRequestsUnderlyingReturnValue = newValue
|
||||
} else {
|
||||
DispatchQueue.main.sync {
|
||||
pinnedEventsTimelineInternalIdPrefixMaxEventsToLoadUnderlyingReturnValue = newValue
|
||||
pinnedEventsTimelineInternalIdPrefixMaxEventsToLoadMaxConcurrentRequestsUnderlyingReturnValue = newValue
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
open var pinnedEventsTimelineInternalIdPrefixMaxEventsToLoadClosure: ((String?, UInt16) async throws -> Timeline)?
|
||||
open var pinnedEventsTimelineInternalIdPrefixMaxEventsToLoadMaxConcurrentRequestsClosure: ((String?, UInt16, UInt16) async throws -> Timeline)?
|
||||
|
||||
open override func pinnedEventsTimeline(internalIdPrefix: String?, maxEventsToLoad: UInt16) async throws -> Timeline {
|
||||
if let error = pinnedEventsTimelineInternalIdPrefixMaxEventsToLoadThrowableError {
|
||||
open override func pinnedEventsTimeline(internalIdPrefix: String?, maxEventsToLoad: UInt16, maxConcurrentRequests: UInt16) async throws -> Timeline {
|
||||
if let error = pinnedEventsTimelineInternalIdPrefixMaxEventsToLoadMaxConcurrentRequestsThrowableError {
|
||||
throw error
|
||||
}
|
||||
pinnedEventsTimelineInternalIdPrefixMaxEventsToLoadCallsCount += 1
|
||||
pinnedEventsTimelineInternalIdPrefixMaxEventsToLoadReceivedArguments = (internalIdPrefix: internalIdPrefix, maxEventsToLoad: maxEventsToLoad)
|
||||
pinnedEventsTimelineInternalIdPrefixMaxEventsToLoadMaxConcurrentRequestsCallsCount += 1
|
||||
pinnedEventsTimelineInternalIdPrefixMaxEventsToLoadMaxConcurrentRequestsReceivedArguments = (internalIdPrefix: internalIdPrefix, maxEventsToLoad: maxEventsToLoad, maxConcurrentRequests: maxConcurrentRequests)
|
||||
DispatchQueue.main.async {
|
||||
self.pinnedEventsTimelineInternalIdPrefixMaxEventsToLoadReceivedInvocations.append((internalIdPrefix: internalIdPrefix, maxEventsToLoad: maxEventsToLoad))
|
||||
self.pinnedEventsTimelineInternalIdPrefixMaxEventsToLoadMaxConcurrentRequestsReceivedInvocations.append((internalIdPrefix: internalIdPrefix, maxEventsToLoad: maxEventsToLoad, maxConcurrentRequests: maxConcurrentRequests))
|
||||
}
|
||||
if let pinnedEventsTimelineInternalIdPrefixMaxEventsToLoadClosure = pinnedEventsTimelineInternalIdPrefixMaxEventsToLoadClosure {
|
||||
return try await pinnedEventsTimelineInternalIdPrefixMaxEventsToLoadClosure(internalIdPrefix, maxEventsToLoad)
|
||||
if let pinnedEventsTimelineInternalIdPrefixMaxEventsToLoadMaxConcurrentRequestsClosure = pinnedEventsTimelineInternalIdPrefixMaxEventsToLoadMaxConcurrentRequestsClosure {
|
||||
return try await pinnedEventsTimelineInternalIdPrefixMaxEventsToLoadMaxConcurrentRequestsClosure(internalIdPrefix, maxEventsToLoad, maxConcurrentRequests)
|
||||
} else {
|
||||
return pinnedEventsTimelineInternalIdPrefixMaxEventsToLoadReturnValue
|
||||
return pinnedEventsTimelineInternalIdPrefixMaxEventsToLoadMaxConcurrentRequestsReturnValue
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14345,6 +14467,52 @@ open class RoomSDKMock: MatrixRustSDK.Room {
|
||||
}
|
||||
}
|
||||
|
||||
//MARK: - tryResend
|
||||
|
||||
open var tryResendTransactionIdThrowableError: Error?
|
||||
var tryResendTransactionIdUnderlyingCallsCount = 0
|
||||
open var tryResendTransactionIdCallsCount: Int {
|
||||
get {
|
||||
if Thread.isMainThread {
|
||||
return tryResendTransactionIdUnderlyingCallsCount
|
||||
} else {
|
||||
var returnValue: Int? = nil
|
||||
DispatchQueue.main.sync {
|
||||
returnValue = tryResendTransactionIdUnderlyingCallsCount
|
||||
}
|
||||
|
||||
return returnValue!
|
||||
}
|
||||
}
|
||||
set {
|
||||
if Thread.isMainThread {
|
||||
tryResendTransactionIdUnderlyingCallsCount = newValue
|
||||
} else {
|
||||
DispatchQueue.main.sync {
|
||||
tryResendTransactionIdUnderlyingCallsCount = newValue
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
open var tryResendTransactionIdCalled: Bool {
|
||||
return tryResendTransactionIdCallsCount > 0
|
||||
}
|
||||
open var tryResendTransactionIdReceivedTransactionId: String?
|
||||
open var tryResendTransactionIdReceivedInvocations: [String] = []
|
||||
open var tryResendTransactionIdClosure: ((String) async throws -> Void)?
|
||||
|
||||
open override func tryResend(transactionId: String) async throws {
|
||||
if let error = tryResendTransactionIdThrowableError {
|
||||
throw error
|
||||
}
|
||||
tryResendTransactionIdCallsCount += 1
|
||||
tryResendTransactionIdReceivedTransactionId = transactionId
|
||||
DispatchQueue.main.async {
|
||||
self.tryResendTransactionIdReceivedInvocations.append(transactionId)
|
||||
}
|
||||
try await tryResendTransactionIdClosure?(transactionId)
|
||||
}
|
||||
|
||||
//MARK: - typingNotice
|
||||
|
||||
open var typingNoticeIsTypingThrowableError: Error?
|
||||
@@ -14528,6 +14696,52 @@ open class RoomSDKMock: MatrixRustSDK.Room {
|
||||
}
|
||||
try await uploadAvatarMimeTypeDataMediaInfoClosure?(mimeType, data, mediaInfo)
|
||||
}
|
||||
|
||||
//MARK: - withdrawVerificationAndResend
|
||||
|
||||
open var withdrawVerificationAndResendUserIdsTransactionIdThrowableError: Error?
|
||||
var withdrawVerificationAndResendUserIdsTransactionIdUnderlyingCallsCount = 0
|
||||
open var withdrawVerificationAndResendUserIdsTransactionIdCallsCount: Int {
|
||||
get {
|
||||
if Thread.isMainThread {
|
||||
return withdrawVerificationAndResendUserIdsTransactionIdUnderlyingCallsCount
|
||||
} else {
|
||||
var returnValue: Int? = nil
|
||||
DispatchQueue.main.sync {
|
||||
returnValue = withdrawVerificationAndResendUserIdsTransactionIdUnderlyingCallsCount
|
||||
}
|
||||
|
||||
return returnValue!
|
||||
}
|
||||
}
|
||||
set {
|
||||
if Thread.isMainThread {
|
||||
withdrawVerificationAndResendUserIdsTransactionIdUnderlyingCallsCount = newValue
|
||||
} else {
|
||||
DispatchQueue.main.sync {
|
||||
withdrawVerificationAndResendUserIdsTransactionIdUnderlyingCallsCount = newValue
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
open var withdrawVerificationAndResendUserIdsTransactionIdCalled: Bool {
|
||||
return withdrawVerificationAndResendUserIdsTransactionIdCallsCount > 0
|
||||
}
|
||||
open var withdrawVerificationAndResendUserIdsTransactionIdReceivedArguments: (userIds: [String], transactionId: String)?
|
||||
open var withdrawVerificationAndResendUserIdsTransactionIdReceivedInvocations: [(userIds: [String], transactionId: String)] = []
|
||||
open var withdrawVerificationAndResendUserIdsTransactionIdClosure: (([String], String) async throws -> Void)?
|
||||
|
||||
open override func withdrawVerificationAndResend(userIds: [String], transactionId: String) async throws {
|
||||
if let error = withdrawVerificationAndResendUserIdsTransactionIdThrowableError {
|
||||
throw error
|
||||
}
|
||||
withdrawVerificationAndResendUserIdsTransactionIdCallsCount += 1
|
||||
withdrawVerificationAndResendUserIdsTransactionIdReceivedArguments = (userIds: userIds, transactionId: transactionId)
|
||||
DispatchQueue.main.async {
|
||||
self.withdrawVerificationAndResendUserIdsTransactionIdReceivedInvocations.append((userIds: userIds, transactionId: transactionId))
|
||||
}
|
||||
try await withdrawVerificationAndResendUserIdsTransactionIdClosure?(userIds, transactionId)
|
||||
}
|
||||
}
|
||||
open class RoomDirectorySearchSDKMock: MatrixRustSDK.RoomDirectorySearch {
|
||||
init() {
|
||||
|
||||
@@ -94,6 +94,10 @@ extension JoinedRoomProxyMock {
|
||||
}
|
||||
return .success(member)
|
||||
}
|
||||
|
||||
resendItemIDReturnValue = .success(())
|
||||
ignoreDeviceTrustAndResendDevicesItemIDReturnValue = .success(())
|
||||
withdrawVerificationAndResendUserIDsItemIDReturnValue = .success(())
|
||||
|
||||
flagAsUnreadReturnValue = .success(())
|
||||
markAsReadReceiptTypeReturnValue = .success(())
|
||||
|
||||
@@ -19,6 +19,7 @@ extension ClientBuilder {
|
||||
.enableCrossProcessRefreshLock(processId: InfoPlistReader.main.bundleIdentifier, sessionDelegate: sessionDelegate)
|
||||
.userAgent(userAgent: UserAgentBuilder.makeASCIIUserAgent())
|
||||
.requestConfig(config: .init(retryLimit: 0, timeout: 30000, maxConcurrentRequests: nil, retryTimeout: nil))
|
||||
.roomKeyRecipientStrategy(strategy: .deviceBasedStrategy(onlyAllowTrustedDevices: false, errorOnVerifiedUserProblem: true))
|
||||
|
||||
builder = switch slidingSync {
|
||||
case .restored: builder
|
||||
|
||||
@@ -14,33 +14,36 @@ import SwiftUI
|
||||
struct HeroImage: View {
|
||||
enum Style {
|
||||
case normal
|
||||
case positive
|
||||
case subtle
|
||||
case success
|
||||
case critical
|
||||
case criticalOnSecondary
|
||||
|
||||
var foregroundColor: Color {
|
||||
switch self {
|
||||
case .normal:
|
||||
return .compound.iconPrimary
|
||||
case .positive:
|
||||
return .compound.iconSuccessPrimary
|
||||
.compound.iconPrimary
|
||||
case .subtle:
|
||||
return .compound.iconSecondary
|
||||
case .critical:
|
||||
return .compound.iconCriticalPrimary
|
||||
.compound.iconSecondary
|
||||
case .success:
|
||||
.compound.iconSuccessPrimary
|
||||
case .critical, .criticalOnSecondary:
|
||||
.compound.iconCriticalPrimary
|
||||
}
|
||||
}
|
||||
|
||||
var backgroundFillColor: Color {
|
||||
switch self {
|
||||
case .normal:
|
||||
return .compound.bgSubtleSecondary
|
||||
case .positive:
|
||||
return .compound.bgSuccessSubtle
|
||||
.compound.bgSubtleSecondary
|
||||
case .subtle:
|
||||
return .compound.bgSubtlePrimary
|
||||
.compound.bgSubtlePrimary
|
||||
case .success:
|
||||
.compound.bgSuccessSubtle
|
||||
case .critical:
|
||||
return .compound.bgCanvasDefault
|
||||
.compound.bgCriticalSubtle
|
||||
case .criticalOnSecondary:
|
||||
.compound.bgCanvasDefault
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -86,12 +89,21 @@ private struct HeroImageModifier: ViewModifier {
|
||||
|
||||
struct HeroImage_Previews: PreviewProvider, TestablePreview {
|
||||
static var previews: some View {
|
||||
HStack(spacing: 20) {
|
||||
HeroImage(icon: \.lockSolid)
|
||||
Image(systemName: "hourglass")
|
||||
.heroImage()
|
||||
Image(asset: Asset.Images.serverSelectionIcon)
|
||||
.heroImage(insets: 19)
|
||||
VStack(spacing: 20) {
|
||||
HStack(spacing: 20) {
|
||||
HeroImage(icon: \.lockSolid)
|
||||
Image(systemName: "hourglass")
|
||||
.heroImage()
|
||||
Image(asset: Asset.Images.serverSelectionIcon)
|
||||
.heroImage(insets: 19)
|
||||
}
|
||||
|
||||
HStack(spacing: 20) {
|
||||
HeroImage(icon: \.helpSolid, style: .subtle)
|
||||
HeroImage(icon: \.checkCircleSolid, style: .success)
|
||||
HeroImage(icon: \.error, style: .critical)
|
||||
HeroImage(icon: \.error, style: .criticalOnSecondary)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -39,7 +39,7 @@ struct EncryptionResetScreen: View {
|
||||
|
||||
private var header: some View {
|
||||
VStack(spacing: 8) {
|
||||
HeroImage(icon: \.error, style: .critical)
|
||||
HeroImage(icon: \.error, style: .criticalOnSecondary)
|
||||
.padding(.bottom, 8)
|
||||
|
||||
Text(L10n.screenEncryptionResetTitle)
|
||||
|
||||
+1
-1
@@ -29,7 +29,7 @@ struct IdentityConfirmedScreen: View {
|
||||
@ViewBuilder
|
||||
private var screenHeader: some View {
|
||||
VStack(spacing: 0) {
|
||||
HeroImage(icon: \.checkCircle, style: .positive)
|
||||
HeroImage(icon: \.checkCircle, style: .success)
|
||||
.padding(.bottom, 16)
|
||||
|
||||
Text(L10n.screenIdentityConfirmedTitle)
|
||||
|
||||
+3
-1
@@ -78,7 +78,9 @@ final class PinnedEventsTimelineScreenCoordinator: CoordinatorProtocol {
|
||||
case .viewInRoomTimeline(let eventID):
|
||||
actionsSubject.send(.displayRoomScreenWithFocussedPin(eventID: eventID))
|
||||
// These other actions will not be handled in this view
|
||||
case .displayEmojiPicker, .displayReportContent, .displayCameraPicker, .displayMediaPicker, .displayDocumentPicker, .displayLocationPicker, .displayPollForm, .displayMediaUploadPreviewScreen, .composer, .hasScrolled:
|
||||
case .displayEmojiPicker, .displayReportContent, .displayCameraPicker, .displayMediaPicker,
|
||||
.displayDocumentPicker, .displayLocationPicker, .displayPollForm, .displayMediaUploadPreviewScreen,
|
||||
.displayResolveSendFailure, .composer, .hasScrolled:
|
||||
// These actions are not handled in this coordinator
|
||||
break
|
||||
}
|
||||
|
||||
@@ -266,7 +266,7 @@ struct QRCodeLoginScreen: View {
|
||||
case .connectionNotSecure:
|
||||
VStack(spacing: 40) {
|
||||
VStack(spacing: 16) {
|
||||
HeroImage(icon: \.error, style: .critical)
|
||||
HeroImage(icon: \.error, style: .criticalOnSecondary)
|
||||
|
||||
VStack(spacing: 8) {
|
||||
Text(L10n.screenQrCodeLoginConnectionNoteSecureStateTitle)
|
||||
@@ -332,7 +332,7 @@ struct QRCodeLoginScreen: View {
|
||||
}
|
||||
|
||||
VStack(spacing: 16) {
|
||||
HeroImage(icon: \.error, style: .critical)
|
||||
HeroImage(icon: \.error, style: .criticalOnSecondary)
|
||||
|
||||
VStack(spacing: 8) {
|
||||
Text(title)
|
||||
|
||||
+65
@@ -0,0 +1,65 @@
|
||||
//
|
||||
// Copyright 2022 New Vector Ltd
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import Combine
|
||||
import SwiftUI
|
||||
|
||||
struct ResolveVerifiedUserSendFailureScreenCoordinatorParameters {
|
||||
let failure: TimelineItemSendFailure.VerifiedUser
|
||||
let itemID: TimelineItemIdentifier
|
||||
let roomProxy: JoinedRoomProxyProtocol
|
||||
}
|
||||
|
||||
enum ResolveVerifiedUserSendFailureScreenCoordinatorAction {
|
||||
case dismiss
|
||||
}
|
||||
|
||||
final class ResolveVerifiedUserSendFailureScreenCoordinator: CoordinatorProtocol {
|
||||
private let parameters: ResolveVerifiedUserSendFailureScreenCoordinatorParameters
|
||||
private let viewModel: ResolveVerifiedUserSendFailureScreenViewModelProtocol
|
||||
|
||||
private var cancellables = Set<AnyCancellable>()
|
||||
|
||||
private let actionsSubject: PassthroughSubject<ResolveVerifiedUserSendFailureScreenCoordinatorAction, Never> = .init()
|
||||
var actionsPublisher: AnyPublisher<ResolveVerifiedUserSendFailureScreenCoordinatorAction, Never> {
|
||||
actionsSubject.eraseToAnyPublisher()
|
||||
}
|
||||
|
||||
init(parameters: ResolveVerifiedUserSendFailureScreenCoordinatorParameters) {
|
||||
self.parameters = parameters
|
||||
|
||||
viewModel = ResolveVerifiedUserSendFailureScreenViewModel(failure: parameters.failure,
|
||||
itemID: parameters.itemID,
|
||||
roomProxy: parameters.roomProxy)
|
||||
}
|
||||
|
||||
func start() {
|
||||
viewModel.actionsPublisher.sink { [weak self] action in
|
||||
MXLog.info("Coordinator: received view model action: \(action)")
|
||||
|
||||
guard let self else { return }
|
||||
switch action {
|
||||
case .dismiss:
|
||||
actionsSubject.send(.dismiss)
|
||||
}
|
||||
}
|
||||
.store(in: &cancellables)
|
||||
}
|
||||
|
||||
func toPresentable() -> AnyView {
|
||||
AnyView(ResolveVerifiedUserSendFailureScreen(context: viewModel.context))
|
||||
}
|
||||
}
|
||||
+53
@@ -0,0 +1,53 @@
|
||||
//
|
||||
// Copyright 2022 New Vector Ltd
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
enum ResolveVerifiedUserSendFailureScreenViewModelAction {
|
||||
case dismiss
|
||||
}
|
||||
|
||||
struct ResolveVerifiedUserSendFailureScreenViewState: BindableState {
|
||||
var currentFailure: TimelineItemSendFailure.VerifiedUser
|
||||
var currentMemberDisplayName: String
|
||||
|
||||
var title: String {
|
||||
switch currentFailure {
|
||||
case .hasUnsignedDevice: L10n.screenResolveSendFailureUnsignedDeviceTitle(currentMemberDisplayName)
|
||||
case .changedIdentity: L10n.screenResolveSendFailureChangedIdentityTitle(currentMemberDisplayName)
|
||||
}
|
||||
}
|
||||
|
||||
var subtitle: String {
|
||||
switch currentFailure {
|
||||
case .hasUnsignedDevice: L10n.screenResolveSendFailureUnsignedDeviceSubtitle(currentMemberDisplayName, currentMemberDisplayName)
|
||||
case .changedIdentity: L10n.screenResolveSendFailureChangedIdentitySubtitle(currentMemberDisplayName)
|
||||
}
|
||||
}
|
||||
|
||||
var primaryButtonTitle: String {
|
||||
switch currentFailure {
|
||||
case .hasUnsignedDevice: L10n.screenResolveSendFailureUnsignedDevicePrimaryButtonTitle
|
||||
case .changedIdentity: L10n.screenResolveSendFailureChangedIdentityPrimaryButtonTitle
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum ResolveVerifiedUserSendFailureScreenViewAction {
|
||||
case resolveAndResend
|
||||
case resend
|
||||
case cancel
|
||||
}
|
||||
+132
@@ -0,0 +1,132 @@
|
||||
//
|
||||
// Copyright 2022 New Vector Ltd
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import Combine
|
||||
import SwiftUI
|
||||
|
||||
typealias ResolveVerifiedUserSendFailureScreenViewModelType = StateStoreViewModel<ResolveVerifiedUserSendFailureScreenViewState, ResolveVerifiedUserSendFailureScreenViewAction>
|
||||
|
||||
class ResolveVerifiedUserSendFailureScreenViewModel: ResolveVerifiedUserSendFailureScreenViewModelType, ResolveVerifiedUserSendFailureScreenViewModelProtocol {
|
||||
private let iterator: VerifiedUserSendFailureIterator
|
||||
private let failure: TimelineItemSendFailure.VerifiedUser
|
||||
private let itemID: TimelineItemIdentifier
|
||||
private let roomProxy: JoinedRoomProxyProtocol
|
||||
private var members: [String: RoomMemberProxyProtocol]
|
||||
|
||||
private let actionsSubject: PassthroughSubject<ResolveVerifiedUserSendFailureScreenViewModelAction, Never> = .init()
|
||||
var actionsPublisher: AnyPublisher<ResolveVerifiedUserSendFailureScreenViewModelAction, Never> {
|
||||
actionsSubject.eraseToAnyPublisher()
|
||||
}
|
||||
|
||||
init(failure: TimelineItemSendFailure.VerifiedUser, itemID: TimelineItemIdentifier, roomProxy: JoinedRoomProxyProtocol) {
|
||||
iterator = switch failure {
|
||||
case .hasUnsignedDevice(let devices): UnsignedDeviceFailureIterator(devices: devices)
|
||||
case .changedIdentity(let users): ChangedIdentityFailureIterator(users: users)
|
||||
}
|
||||
|
||||
self.failure = failure
|
||||
self.itemID = itemID
|
||||
self.roomProxy = roomProxy
|
||||
|
||||
members = Dictionary(uniqueKeysWithValues: roomProxy.membersPublisher.value.map { ($0.userID, $0) })
|
||||
|
||||
guard let (userID, failure) = iterator.next() else {
|
||||
MXLog.error("There aren't any known users/devices to resolve the failure with.")
|
||||
fatalError("There aren't any known users/devices to resolve the failure with.")
|
||||
}
|
||||
|
||||
super.init(initialViewState: ResolveVerifiedUserSendFailureScreenViewState(currentFailure: failure,
|
||||
currentMemberDisplayName: members[userID]?.displayName ?? userID))
|
||||
}
|
||||
|
||||
// MARK: Public
|
||||
|
||||
override func process(viewAction: ResolveVerifiedUserSendFailureScreenViewAction) {
|
||||
MXLog.info("View model: received view action: \(viewAction)")
|
||||
|
||||
switch viewAction {
|
||||
case .resolveAndResend:
|
||||
Task { await resolveAndResend() }
|
||||
case .resend:
|
||||
Task { await resend() }
|
||||
case .cancel:
|
||||
actionsSubject.send(.dismiss)
|
||||
}
|
||||
}
|
||||
|
||||
private func resolveAndResend() async {
|
||||
let result = switch failure {
|
||||
case .hasUnsignedDevice(let devices):
|
||||
await roomProxy.ignoreDeviceTrustAndResend(devices: devices, itemID: itemID)
|
||||
case .changedIdentity(let users):
|
||||
await roomProxy.withdrawVerificationAndResend(userIDs: users, itemID: itemID)
|
||||
}
|
||||
|
||||
if case let .failure(error) = result {
|
||||
#warning("Show the error?")
|
||||
return
|
||||
}
|
||||
|
||||
if let (userID, failure) = iterator.next() {
|
||||
state.currentMemberDisplayName = members[userID]?.displayName ?? userID
|
||||
state.currentFailure = failure
|
||||
} else {
|
||||
actionsSubject.send(.dismiss)
|
||||
}
|
||||
}
|
||||
|
||||
private func resend() async {
|
||||
switch await roomProxy.resend(itemID: itemID) {
|
||||
case .success:
|
||||
actionsSubject.send(.dismiss)
|
||||
case .failure(let failure):
|
||||
#warning("Show the error?")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Iterators
|
||||
|
||||
@MainActor
|
||||
private protocol VerifiedUserSendFailureIterator {
|
||||
func next() -> (userID: String, failure: TimelineItemSendFailure.VerifiedUser)?
|
||||
}
|
||||
|
||||
private class UnsignedDeviceFailureIterator: VerifiedUserSendFailureIterator {
|
||||
private var iterator: [String: [String]].Iterator
|
||||
|
||||
init(devices: [String: [String]]) {
|
||||
iterator = devices.makeIterator()
|
||||
}
|
||||
|
||||
func next() -> (userID: String, failure: TimelineItemSendFailure.VerifiedUser)? {
|
||||
guard let nextUserDevices = iterator.next() else { return nil }
|
||||
return (nextUserDevices.key, .hasUnsignedDevice(devices: [nextUserDevices.key: nextUserDevices.value]))
|
||||
}
|
||||
}
|
||||
|
||||
private class ChangedIdentityFailureIterator: VerifiedUserSendFailureIterator {
|
||||
private var iterator: [String].Iterator
|
||||
|
||||
init(users: [String]) {
|
||||
iterator = users.makeIterator()
|
||||
}
|
||||
|
||||
func next() -> (userID: String, failure: TimelineItemSendFailure.VerifiedUser)? {
|
||||
guard let nextUserID = iterator.next() else { return nil }
|
||||
return (nextUserID, .changedIdentity(users: [nextUserID]))
|
||||
}
|
||||
}
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
//
|
||||
// Copyright 2022 New Vector Ltd
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import Combine
|
||||
|
||||
@MainActor
|
||||
protocol ResolveVerifiedUserSendFailureScreenViewModelProtocol {
|
||||
var actionsPublisher: AnyPublisher<ResolveVerifiedUserSendFailureScreenViewModelAction, Never> { get }
|
||||
var context: ResolveVerifiedUserSendFailureScreenViewModelType.Context { get }
|
||||
}
|
||||
+110
@@ -0,0 +1,110 @@
|
||||
//
|
||||
// Copyright 2022 New Vector Ltd
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import Compound
|
||||
import SwiftUI
|
||||
|
||||
struct ResolveVerifiedUserSendFailureScreen: View {
|
||||
@ObservedObject var context: ResolveVerifiedUserSendFailureScreenViewModel.Context
|
||||
@State private var sheetFrame: CGRect = .zero
|
||||
|
||||
var body: some View {
|
||||
ScrollView {
|
||||
VStack(spacing: 40) {
|
||||
header
|
||||
buttons
|
||||
}
|
||||
.padding(.top, 24)
|
||||
.padding(.bottom, 16)
|
||||
.padding(.horizontal, 16)
|
||||
.readFrame($sheetFrame)
|
||||
}
|
||||
.scrollBounceBehavior(.basedOnSize)
|
||||
.presentationDetents([.height(sheetFrame.height)])
|
||||
}
|
||||
|
||||
var header: some View {
|
||||
VStack(spacing: 8) {
|
||||
HeroImage(icon: \.error, style: .critical)
|
||||
.padding(.bottom, 8)
|
||||
|
||||
Text(context.viewState.title)
|
||||
.font(.compound.headingMDBold)
|
||||
.multilineTextAlignment(.center)
|
||||
.foregroundColor(.compound.textPrimary)
|
||||
|
||||
Text(context.viewState.subtitle)
|
||||
.font(.compound.bodyMD)
|
||||
.multilineTextAlignment(.center)
|
||||
.foregroundColor(.compound.textSecondary)
|
||||
}
|
||||
}
|
||||
|
||||
var buttons: some View {
|
||||
VStack(spacing: 16) {
|
||||
Button(context.viewState.primaryButtonTitle) {
|
||||
context.send(viewAction: .resolveAndResend)
|
||||
}
|
||||
.buttonStyle(.compound(.primary))
|
||||
|
||||
Button(L10n.actionRetry) {
|
||||
context.send(viewAction: .resend)
|
||||
}
|
||||
.buttonStyle(.compound(.secondary))
|
||||
|
||||
Button { context.send(viewAction: .cancel) } label: {
|
||||
Text(L10n.actionCancelForNow)
|
||||
.padding(.vertical, 14)
|
||||
}
|
||||
.buttonStyle(.compound(.plain))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Previews
|
||||
|
||||
struct ResolveVerifiedUserSendFailureScreen_Previews: PreviewProvider, TestablePreview {
|
||||
static let unsignedDeviceViewModel = makeViewModel(failure: .hasUnsignedDevice(devices: ["@alice:matrix.org": []]))
|
||||
static let changedIdentityViewModel = makeViewModel(failure: .changedIdentity(users: ["@alice:matrix.org"]))
|
||||
|
||||
static var previews: some View {
|
||||
ResolveVerifiedUserSendFailureScreen(context: unsignedDeviceViewModel.context)
|
||||
.previewDisplayName("Unsigned Device")
|
||||
|
||||
ResolveVerifiedUserSendFailureScreen(context: changedIdentityViewModel.context)
|
||||
.previewDisplayName("Identity Changed")
|
||||
}
|
||||
|
||||
static func makeViewModel(failure: TimelineItemSendFailure.VerifiedUser) -> ResolveVerifiedUserSendFailureScreenViewModel {
|
||||
ResolveVerifiedUserSendFailureScreenViewModel(failure: failure,
|
||||
itemID: .random,
|
||||
roomProxy: JoinedRoomProxyMock(.init()))
|
||||
}
|
||||
}
|
||||
|
||||
struct ResolveVerifiedUserSendFailureScreenSheet_Previews: PreviewProvider {
|
||||
static let viewModel = ResolveVerifiedUserSendFailureScreenViewModel(failure: .changedIdentity(users: ["@alice:matrix.org"]),
|
||||
itemID: .random,
|
||||
roomProxy: JoinedRoomProxyMock(.init()))
|
||||
|
||||
static var previews: some View {
|
||||
Text("Hello")
|
||||
.sheet(isPresented: .constant(true)) {
|
||||
ResolveVerifiedUserSendFailureScreen(context: viewModel.context)
|
||||
}
|
||||
.previewDisplayName("Sheet")
|
||||
}
|
||||
}
|
||||
@@ -38,6 +38,7 @@ enum RoomScreenCoordinatorAction {
|
||||
case presentMessageForwarding(forwardingItem: MessageForwardingItem)
|
||||
case presentCallScreen
|
||||
case presentPinnedEventsTimeline
|
||||
case presentResolveSendFailure(failure: TimelineItemSendFailure.VerifiedUser, itemID: TimelineItemIdentifier)
|
||||
}
|
||||
|
||||
final class RoomScreenCoordinator: CoordinatorProtocol {
|
||||
@@ -126,6 +127,8 @@ final class RoomScreenCoordinator: CoordinatorProtocol {
|
||||
actionsSubject.send(.presentMessageForwarding(forwardingItem: forwardingItem))
|
||||
case .displayLocation(let body, let geoURI, let description):
|
||||
actionsSubject.send(.presentLocationViewer(body: body, geoURI: geoURI, description: description))
|
||||
case .displayResolveSendFailure(let failure, let itemID):
|
||||
actionsSubject.send(.presentResolveSendFailure(failure: failure, itemID: itemID))
|
||||
case .composer(let action):
|
||||
composerViewModel.process(timelineAction: action)
|
||||
case .hasScrolled(direction: let direction):
|
||||
|
||||
@@ -75,7 +75,10 @@ struct RoomScreen: View {
|
||||
}
|
||||
}
|
||||
.sheet(item: $timelineContext.reactionSummaryInfo) {
|
||||
ReactionsSummaryView(reactions: $0.reactions, members: timelineContext.viewState.members, mediaProvider: timelineContext.mediaProvider, selectedReactionKey: $0.selectedKey)
|
||||
ReactionsSummaryView(reactions: $0.reactions,
|
||||
members: timelineContext.viewState.members,
|
||||
mediaProvider: timelineContext.mediaProvider,
|
||||
selectedReactionKey: $0.selectedKey)
|
||||
.edgesIgnoringSafeArea([.bottom])
|
||||
}
|
||||
.sheet(item: $timelineContext.readReceiptsSummaryInfo) {
|
||||
|
||||
@@ -21,6 +21,7 @@ enum TimelineViewModelAction {
|
||||
case tappedOnSenderDetails(userID: String)
|
||||
case displayMessageForwarding(forwardingItem: MessageForwardingItem)
|
||||
case displayLocation(body: String, geoURI: GeoURI, description: String?)
|
||||
case displayResolveSendFailure(failure: TimelineItemSendFailure.VerifiedUser, itemID: TimelineItemIdentifier)
|
||||
case composer(action: TimelineComposerAction)
|
||||
case hasScrolled(direction: ScrollDirection)
|
||||
case viewInRoomTimeline(eventID: String)
|
||||
|
||||
@@ -550,9 +550,10 @@ class TimelineViewModel: TimelineViewModelType, TimelineViewModelProtocol {
|
||||
fatalError("Only events can have send info.")
|
||||
}
|
||||
|
||||
if case .sendingFailed = eventTimelineItem.properties.deliveryStatus {
|
||||
// In the future we will show different errors for the various failure reasons.
|
||||
if case .sendingFailed(.unknown) = eventTimelineItem.properties.deliveryStatus {
|
||||
displayAlert(.sendingFailed)
|
||||
} else if case let .sendingFailed(.verifiedUser(failure)) = eventTimelineItem.properties.deliveryStatus {
|
||||
actionsSubject.send(.displayResolveSendFailure(failure: failure, itemID: itemID))
|
||||
} else if let authenticityMessage = eventTimelineItem.properties.encryptionAuthenticity?.message {
|
||||
displayAlert(.encryptionAuthenticity(authenticityMessage))
|
||||
}
|
||||
|
||||
@@ -21,6 +21,9 @@ struct TimelineItemMenu: View {
|
||||
var body: some View {
|
||||
VStack(spacing: 8) {
|
||||
messagePreview
|
||||
.padding(.horizontal, 16)
|
||||
.padding(.top, 32.0)
|
||||
.padding(.bottom, 4.0)
|
||||
.frame(idealWidth: 300.0)
|
||||
|
||||
Divider()
|
||||
@@ -30,7 +33,6 @@ struct TimelineItemMenu: View {
|
||||
VStack(alignment: .leading, spacing: 0.0) {
|
||||
if !actions.reactions.isEmpty {
|
||||
reactionsSection
|
||||
.padding(.top, 4.0)
|
||||
.padding(.bottom, 8.0)
|
||||
|
||||
Divider()
|
||||
@@ -87,15 +89,20 @@ struct TimelineItemMenu: View {
|
||||
}
|
||||
.accessibilityElement(children: .combine)
|
||||
|
||||
if let authenticity = item.properties.encryptionAuthenticity {
|
||||
if case let .sendingFailed(.verifiedUser(failure)) = item.properties.deliveryStatus {
|
||||
Divider()
|
||||
.padding(.horizontal, -16)
|
||||
|
||||
VerifiedUserSendFailureView(failure: failure, members: context.viewState.members) {
|
||||
send(.itemSendInfoTapped(itemID: item.id))
|
||||
}
|
||||
.padding(.bottom, 8)
|
||||
} else if let authenticity = item.properties.encryptionAuthenticity {
|
||||
Label(authenticity.message, icon: authenticity.icon, iconSize: .small, relativeTo: .compound.bodySMSemibold)
|
||||
.font(.compound.bodySMSemibold)
|
||||
.foregroundStyle(authenticity.foregroundStyle)
|
||||
}
|
||||
}
|
||||
.padding(.horizontal)
|
||||
.padding(.top, 32.0)
|
||||
.padding(.bottom, 4.0)
|
||||
}
|
||||
|
||||
private var reactionsSection: some View {
|
||||
@@ -162,10 +169,50 @@ struct TimelineItemMenu: View {
|
||||
}
|
||||
|
||||
private func send(_ action: TimelineItemMenuAction) {
|
||||
send(.handleTimelineItemMenuAction(itemID: item.id, action: action))
|
||||
}
|
||||
|
||||
private func send(_ action: TimelineViewAction) {
|
||||
dismiss()
|
||||
// Otherwise we might get errors that a sheet is already presented
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
|
||||
context.send(viewAction: .handleTimelineItemMenuAction(itemID: item.id, action: action))
|
||||
context.send(viewAction: action)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private struct VerifiedUserSendFailureView: View {
|
||||
let failure: TimelineItemSendFailure.VerifiedUser
|
||||
let action: () -> Void
|
||||
|
||||
private let memberDisplayName: String
|
||||
|
||||
init(failure: TimelineItemSendFailure.VerifiedUser,
|
||||
members: [String: RoomMemberState],
|
||||
action: @escaping () -> Void) {
|
||||
self.failure = failure
|
||||
self.action = action
|
||||
|
||||
let userIDs = failure.affectedUserIDs
|
||||
memberDisplayName = userIDs.first.map { members[$0]?.displayName ?? $0 } ?? ""
|
||||
}
|
||||
|
||||
var title: String {
|
||||
switch failure {
|
||||
case .hasUnsignedDevice: L10n.screenTimelineItemMenuSendFailureUnsignedDevice(memberDisplayName)
|
||||
case .changedIdentity: L10n.screenTimelineItemMenuSendFailureChangedIdentity(memberDisplayName)
|
||||
}
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
Button(action: action) {
|
||||
HStack(spacing: 8) {
|
||||
Label(title, icon: \.error, iconSize: .small, relativeTo: .compound.bodySMSemibold)
|
||||
.font(.compound.bodySMSemibold)
|
||||
.foregroundStyle(.compound.textCriticalPrimary)
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
ListRowAccessory.navigationLink
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -187,20 +234,27 @@ struct TimelineItemMenu_Previews: PreviewProvider, TestablePreview {
|
||||
static let (backupItem, _) = makeItem(authenticity: .notGuaranteed(color: .gray))
|
||||
static let (unsignedItem, _) = makeItem(authenticity: .unsignedDevice(color: .red))
|
||||
static let (unencryptedItem, _) = makeItem(authenticity: .sentInClear(color: .red))
|
||||
static let (unknownFailureItem, _) = makeItem(deliveryStatus: .sendingFailed(.unknown))
|
||||
static let (identityChangedItem, _) = makeItem(deliveryStatus: .sendingFailed(.verifiedUser(.changedIdentity(users: [
|
||||
"@alice:matrix.org"
|
||||
]))))
|
||||
static let (unsignedDevicesItem, _) = makeItem(deliveryStatus: .sendingFailed(.verifiedUser(.hasUnsignedDevice(devices: [
|
||||
"@alice:matrix.org": ["DEVICE1", "DEVICE2"]
|
||||
]))))
|
||||
|
||||
static var previews: some View {
|
||||
TimelineItemMenu(item: item, actions: actions)
|
||||
.environmentObject(viewModel.context)
|
||||
.previewDisplayName("With button shapes off")
|
||||
.previewDisplayName("Normal")
|
||||
|
||||
TimelineItemMenu(item: item, actions: actions)
|
||||
.environmentObject(viewModel.context)
|
||||
.environment(\._accessibilityShowButtonShapes, true)
|
||||
.previewDisplayName("With button shapes on")
|
||||
.previewDisplayName("Button shapes")
|
||||
|
||||
TimelineItemMenu(item: backupItem, actions: actions)
|
||||
.environmentObject(viewModel.context)
|
||||
.previewDisplayName("Authenticity not guaranteed")
|
||||
.previewDisplayName("Authenticity")
|
||||
|
||||
TimelineItemMenu(item: unsignedItem, actions: actions)
|
||||
.environmentObject(viewModel.context)
|
||||
@@ -209,9 +263,22 @@ struct TimelineItemMenu_Previews: PreviewProvider, TestablePreview {
|
||||
TimelineItemMenu(item: unencryptedItem, actions: actions)
|
||||
.environmentObject(viewModel.context)
|
||||
.previewDisplayName("Unencrypted")
|
||||
|
||||
TimelineItemMenu(item: unknownFailureItem, actions: actions)
|
||||
.environmentObject(viewModel.context)
|
||||
.previewDisplayName("Unknown failure")
|
||||
|
||||
TimelineItemMenu(item: unsignedDevicesItem, actions: actions)
|
||||
.environmentObject(viewModel.context)
|
||||
.previewDisplayName("Unsigned Devices")
|
||||
|
||||
TimelineItemMenu(item: identityChangedItem, actions: actions)
|
||||
.environmentObject(viewModel.context)
|
||||
.previewDisplayName("Identity Changed")
|
||||
}
|
||||
|
||||
static func makeItem(authenticity: EncryptionAuthenticity? = nil) -> (TextRoomTimelineItem, TimelineItemMenuActions)! {
|
||||
static func makeItem(authenticity: EncryptionAuthenticity? = nil,
|
||||
deliveryStatus: TimelineItemDeliveryStatus? = nil) -> (TextRoomTimelineItem, TimelineItemMenuActions)! {
|
||||
guard var item = RoomTimelineItemFixtures.singleMessageChunk.first as? TextRoomTimelineItem,
|
||||
let actions = TimelineItemMenuActions(isReactable: true,
|
||||
actions: [.copy, .edit, .reply(isThread: false), .pin, .redact],
|
||||
@@ -223,6 +290,10 @@ struct TimelineItemMenu_Previews: PreviewProvider, TestablePreview {
|
||||
item.properties.encryptionAuthenticity = authenticity
|
||||
}
|
||||
|
||||
if let deliveryStatus {
|
||||
item.properties.deliveryStatus = deliveryStatus
|
||||
}
|
||||
|
||||
return (item, actions)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,7 +20,7 @@ class JoinedRoomProxy: JoinedRoomProxyProtocol {
|
||||
private var innerPinnedEventsTimelineTask: Task<TimelineProxyProtocol?, Never>?
|
||||
var pinnedEventsTimeline: TimelineProxyProtocol? {
|
||||
get async {
|
||||
// Check if is alrrady available.
|
||||
// Check if is already available.
|
||||
if let innerPinnedEventsTimeline {
|
||||
return innerPinnedEventsTimeline
|
||||
// Otherwise check if there is already a task loading it, and wait for it.
|
||||
@@ -35,7 +35,10 @@ class JoinedRoomProxy: JoinedRoomProxyProtocol {
|
||||
}
|
||||
|
||||
do {
|
||||
let timeline = try await TimelineProxy(timeline: room.pinnedEventsTimeline(internalIdPrefix: nil, maxEventsToLoad: 100), kind: .pinned)
|
||||
let timeline = try await TimelineProxy(timeline: room.pinnedEventsTimeline(internalIdPrefix: nil,
|
||||
maxEventsToLoad: 100,
|
||||
maxConcurrentRequests: 10),
|
||||
kind: .pinned)
|
||||
await timeline.subscribeForUpdates()
|
||||
innerPinnedEventsTimeline = timeline
|
||||
return timeline
|
||||
@@ -377,6 +380,51 @@ class JoinedRoomProxy: JoinedRoomProxyProtocol {
|
||||
}
|
||||
}
|
||||
|
||||
func resend(itemID: TimelineItemIdentifier) async -> Result<Void, RoomProxyError> {
|
||||
guard let transactionID = itemID.transactionID else {
|
||||
MXLog.error("Attempting to resend an item that has no transaction ID: \(itemID)")
|
||||
return .failure(.missingTransactionID)
|
||||
}
|
||||
|
||||
do {
|
||||
try await room.tryResend(transactionId: transactionID)
|
||||
return .success(())
|
||||
} catch {
|
||||
MXLog.error("Failed resending \(transactionID) with error: \(error)")
|
||||
return .failure(.sdkError(error))
|
||||
}
|
||||
}
|
||||
|
||||
func ignoreDeviceTrustAndResend(devices: [String: [String]], itemID: TimelineItemIdentifier) async -> Result<Void, RoomProxyError> {
|
||||
guard let transactionID = itemID.transactionID else {
|
||||
MXLog.error("Attempting to resend an item that has no transaction ID: \(itemID)")
|
||||
return .failure(.missingTransactionID)
|
||||
}
|
||||
|
||||
do {
|
||||
try await room.ignoreDeviceTrustAndResend(devices: devices, transactionId: transactionID)
|
||||
return .success(())
|
||||
} catch {
|
||||
MXLog.error("Failed trusting devices \(devices) and resending \(transactionID) with error: \(error)")
|
||||
return .failure(.sdkError(error))
|
||||
}
|
||||
}
|
||||
|
||||
func withdrawVerificationAndResend(userIDs: [String], itemID: TimelineItemIdentifier) async -> Result<Void, RoomProxyError> {
|
||||
guard let transactionID = itemID.transactionID else {
|
||||
MXLog.error("Attempting to resend an item that has no transaction ID: \(itemID)")
|
||||
return .failure(.missingTransactionID)
|
||||
}
|
||||
|
||||
do {
|
||||
try await room.withdrawVerificationAndResend(userIds: userIDs, transactionId: transactionID)
|
||||
return .success(())
|
||||
} catch {
|
||||
MXLog.error("Failed withdrawing verification of \(userIDs) and resending \(transactionID) with error: \(error)")
|
||||
return .failure(.sdkError(error))
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Room flags
|
||||
|
||||
func flagAsUnread(_ isUnread: Bool) async -> Result<Void, RoomProxyError> {
|
||||
|
||||
@@ -15,6 +15,7 @@ enum RoomProxyError: Error {
|
||||
case invalidURL
|
||||
case invalidMedia
|
||||
case eventNotFound
|
||||
case missingTransactionID
|
||||
}
|
||||
|
||||
enum RoomProxyType {
|
||||
@@ -111,6 +112,12 @@ protocol JoinedRoomProxyProtocol: RoomProxyProtocol {
|
||||
/// https://spec.matrix.org/v1.9/client-server-api/#typing-notifications
|
||||
@discardableResult func sendTypingNotification(isTyping: Bool) async -> Result<Void, RoomProxyError>
|
||||
|
||||
func resend(itemID: TimelineItemIdentifier) async -> Result<Void, RoomProxyError>
|
||||
|
||||
func ignoreDeviceTrustAndResend(devices: [String: [String]], itemID: TimelineItemIdentifier) async -> Result<Void, RoomProxyError>
|
||||
|
||||
func withdrawVerificationAndResend(userIDs: [String], itemID: TimelineItemIdentifier) async -> Result<Void, RoomProxyError>
|
||||
|
||||
// MARK: - Room Flags
|
||||
|
||||
func flagAsUnread(_ isUnread: Bool) async -> Result<Void, RoomProxyError>
|
||||
|
||||
@@ -59,13 +59,7 @@ enum TimelineItemProxy {
|
||||
enum TimelineItemDeliveryStatus: Hashable {
|
||||
case sending
|
||||
case sent
|
||||
case sendingFailed(SendFailureReason)
|
||||
|
||||
enum SendFailureReason: Hashable {
|
||||
case verifiedUserHasUnsignedDevice(devices: [String: [String]])
|
||||
case verifiedUserChangedIdentity(users: [String])
|
||||
case unknown
|
||||
}
|
||||
case sendingFailed(TimelineItemSendFailure)
|
||||
|
||||
var isSendingFailed: Bool {
|
||||
switch self {
|
||||
@@ -75,6 +69,24 @@ enum TimelineItemDeliveryStatus: Hashable {
|
||||
}
|
||||
}
|
||||
|
||||
/// The reason a timeline item failed to send.
|
||||
enum TimelineItemSendFailure: Hashable {
|
||||
enum VerifiedUser: Hashable {
|
||||
case hasUnsignedDevice(devices: [String: [String]])
|
||||
case changedIdentity(users: [String])
|
||||
|
||||
var affectedUserIDs: [String] {
|
||||
switch self {
|
||||
case .hasUnsignedDevice(let devices): Array(devices.keys)
|
||||
case .changedIdentity(let users): users
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
case verifiedUser(VerifiedUser)
|
||||
case unknown
|
||||
}
|
||||
|
||||
/// A light wrapper around event timeline items returned from Rust.
|
||||
class EventTimelineItemProxy {
|
||||
let item: MatrixRustSDK.EventTimelineItem
|
||||
@@ -98,9 +110,9 @@ class EventTimelineItemProxy {
|
||||
case .sent:
|
||||
return .sent
|
||||
case .verifiedUserHasUnsignedDevice(devices: let devices):
|
||||
return .sendingFailed(.verifiedUserHasUnsignedDevice(devices: devices))
|
||||
return .sendingFailed(.verifiedUser(.hasUnsignedDevice(devices: devices)))
|
||||
case .verifiedUserChangedIdentity(users: let users):
|
||||
return .sendingFailed(.verifiedUserChangedIdentity(users: users))
|
||||
return .sendingFailed(.verifiedUser(.changedIdentity(users: users)))
|
||||
case .crossSigningNotSetup, .sendingFromUnverifiedDevice:
|
||||
return .sendingFailed(.unknown)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user