diff --git a/Riot/Modules/Home/AllChats/AllChatsCoordinator.swift b/Riot/Modules/Home/AllChats/AllChatsCoordinator.swift index a4561437a..932a52333 100644 --- a/Riot/Modules/Home/AllChats/AllChatsCoordinator.swift +++ b/Riot/Modules/Home/AllChats/AllChatsCoordinator.swift @@ -74,7 +74,7 @@ class AllChatsCoordinator: NSObject, SplitViewMasterCoordinatorProtocol { } private var indicators = [UserIndicator]() - private var signOutAlertPresenter = SignOutAlertPresenter() + private var signOutFlowPresenter: SignOutFlowPresenter? // MARK: Public @@ -107,8 +107,6 @@ class AllChatsCoordinator: NSObject, SplitViewMasterCoordinatorProtocol { // If start has been done once do not setup view controllers again if self.hasStartedOnce == false { - signOutAlertPresenter.delegate = self - let allChatsViewController = AllChatsViewController.instantiate() allChatsViewController.allChatsDelegate = self allChatsViewController.userIndicatorStore = UserIndicatorStore(presenter: indicatorPresenter) @@ -580,87 +578,16 @@ class AllChatsCoordinator: NSObject, SplitViewMasterCoordinatorProtocol { // MARK: Sign out process private func signOut() { - guard let keyBackup = currentMatrixSession?.crypto.backup else { - return - } - - signOutAlertPresenter.present(for: keyBackup.state, - areThereKeysToBackup: keyBackup.hasKeysToBackup, - from: self.allChatsViewController, - sourceView: avatarMenuButton, - animated: true) - } - - // MARK: - SecureBackupSetupCoordinatorBridgePresenter - - private var secureBackupSetupCoordinatorBridgePresenter: SecureBackupSetupCoordinatorBridgePresenter? - private var crossSigningSetupCoordinatorBridgePresenter: CrossSigningSetupCoordinatorBridgePresenter? - - private func showSecureBackupSetupFromSignOutFlow() { - if canSetupSecureBackup { - setupSecureBackup2() - } else { - // Set up cross-signing first - setupCrossSigning(title: VectorL10n.secureKeyBackupSetupIntroTitle, - message: VectorL10n.securitySettingsUserPasswordDescription) { [weak self] result in - switch result { - case .success(let isCompleted): - if isCompleted { - self?.setupSecureBackup2() - } - case .failure: - break - } - } - } - } - - private var canSetupSecureBackup: Bool { - return currentMatrixSession?.vc_canSetupSecureBackup() ?? false - } - - private func setupSecureBackup2() { guard let session = currentMatrixSession else { + MXLog.warning("[AllChatsCoordinator] Unable to sign out due to missing current session.") return } - let secureBackupSetupCoordinatorBridgePresenter = SecureBackupSetupCoordinatorBridgePresenter(session: session, allowOverwrite: true) - secureBackupSetupCoordinatorBridgePresenter.delegate = self - secureBackupSetupCoordinatorBridgePresenter.present(from: allChatsViewController, animated: true) - self.secureBackupSetupCoordinatorBridgePresenter = secureBackupSetupCoordinatorBridgePresenter - } - - private func setupCrossSigning(title: String, message: String, completion: @escaping (Result) -> Void) { - guard let session = currentMatrixSession else { - return - } - - allChatsViewController.startActivityIndicator() - allChatsViewController.view.isUserInteractionEnabled = false + let flowPresenter = SignOutFlowPresenter(session: session, presentingViewController: toPresentable()) + flowPresenter.delegate = self - let dismissAnimation = { [weak self] in - guard let self = self else { return } - - self.allChatsViewController.stopActivityIndicator() - self.allChatsViewController.view.isUserInteractionEnabled = true - self.crossSigningSetupCoordinatorBridgePresenter?.dismiss(animated: true, completion: { - self.crossSigningSetupCoordinatorBridgePresenter = nil - }) - } - - let crossSigningSetupCoordinatorBridgePresenter = CrossSigningSetupCoordinatorBridgePresenter(session: session) - crossSigningSetupCoordinatorBridgePresenter.present(with: title, message: message, from: allChatsViewController, animated: true) { - dismissAnimation() - completion(.success(true)) - } cancel: { - dismissAnimation() - completion(.success(false)) - } failure: { error in - dismissAnimation() - completion(.failure(error)) - } - - self.crossSigningSetupCoordinatorBridgePresenter = crossSigningSetupCoordinatorBridgePresenter + flowPresenter.start(sourceView: avatarMenuButton) + self.signOutFlowPresenter = flowPresenter } // MARK: - Private methods @@ -720,42 +647,21 @@ class AllChatsCoordinator: NSObject, SplitViewMasterCoordinatorProtocol { viewController.loadViewIfNeeded() return viewController } - } -// MARK: - SignOutAlertPresenterDelegate -extension AllChatsCoordinator: SignOutAlertPresenterDelegate { - - func signOutAlertPresenterDidTapSignOutAction(_ presenter: SignOutAlertPresenter) { - // Prevent user to perform user interaction in settings when sign out - // TODO: Prevent user interaction in all application (navigation controller and split view controller included) +extension AllChatsCoordinator: SignOutFlowPresenterDelegate { + func signOutFlowPresenterDidStartLoading(_ presenter: SignOutFlowPresenter) { allChatsViewController.view.isUserInteractionEnabled = false allChatsViewController.startActivityIndicator() - - AppDelegate.theDelegate().logout(withConfirmation: false) { [weak self] isLoggedOut in - self?.allChatsViewController.stopActivityIndicator() - self?.allChatsViewController.view.isUserInteractionEnabled = true - } } - func signOutAlertPresenterDidTapBackupAction(_ presenter: SignOutAlertPresenter) { - showSecureBackupSetupFromSignOutFlow() + func signOutFlowPresenterDidStopLoading(_ presenter: SignOutFlowPresenter) { + allChatsViewController.view.isUserInteractionEnabled = true + allChatsViewController.stopActivityIndicator() } -} - -// MARK: - SecureBackupSetupCoordinatorBridgePresenterDelegate -extension AllChatsCoordinator: SecureBackupSetupCoordinatorBridgePresenterDelegate { - func secureBackupSetupCoordinatorBridgePresenterDelegateDidCancel(_ coordinatorBridgePresenter: SecureBackupSetupCoordinatorBridgePresenter) { - coordinatorBridgePresenter.dismiss(animated: true) { - self.secureBackupSetupCoordinatorBridgePresenter = nil - } - } - - func secureBackupSetupCoordinatorBridgePresenterDelegateDidComplete(_ coordinatorBridgePresenter: SecureBackupSetupCoordinatorBridgePresenter) { - coordinatorBridgePresenter.dismiss(animated: true) { - self.secureBackupSetupCoordinatorBridgePresenter = nil - } + func signOutFlowPresenter(_ presenter: SignOutFlowPresenter, didFailWith error: Error) { + AppDelegate.theDelegate().showError(asAlert: error) } } diff --git a/Riot/Modules/Settings/SettingsViewController.m b/Riot/Modules/Settings/SettingsViewController.m index 893fc57e0..e11c720fb 100644 --- a/Riot/Modules/Settings/SettingsViewController.m +++ b/Riot/Modules/Settings/SettingsViewController.m @@ -189,8 +189,7 @@ typedef void (^blockSettingsViewController_onReadyToDestroy)(void); @interface SettingsViewController () @property (nonatomic, strong) NotificationSettingsCoordinatorBridgePresenter *notificationSettingsBridgePresenter; -@property (nonatomic, strong) SignOutAlertPresenter *signOutAlertPresenter; +@property (nonatomic, strong) SignOutFlowPresenter *signOutFlowPresenter; @property (nonatomic, weak) UIButton *signOutButton; @property (nonatomic, strong) SingleImagePickerPresenter *imagePickerPresenter; @@ -275,12 +274,8 @@ ChangePasswordCoordinatorBridgePresenterDelegate> @property (nonatomic, strong) SettingsDiscoveryTableViewSection *settingsDiscoveryTableViewSection; @property (nonatomic, strong) SettingsDiscoveryThreePidDetailsCoordinatorBridgePresenter *discoveryThreePidDetailsPresenter; -@property (nonatomic, strong) SecureBackupSetupCoordinatorBridgePresenter *secureBackupSetupCoordinatorBridgePresenter; - @property (nonatomic, strong) TableViewSections *tableViewSections; -@property (nonatomic, strong) CrossSigningSetupCoordinatorBridgePresenter *crossSigningSetupCoordinatorBridgePresenter; - @property (nonatomic, strong) ReauthenticationCoordinatorBridgePresenter *reauthenticationCoordinatorBridgePresenter; @property (nonatomic, strong) UserInteractiveAuthenticationService *userInteractiveAuthenticationService; @@ -701,9 +696,6 @@ ChangePasswordCoordinatorBridgePresenterDelegate> }]; [self userInterfaceThemeDidChange]; - self.signOutAlertPresenter = [SignOutAlertPresenter new]; - self.signOutAlertPresenter.delegate = self; - _tableViewSections = [TableViewSections new]; _tableViewSections.delegate = self; [self updateSections]; @@ -770,8 +762,7 @@ ChangePasswordCoordinatorBridgePresenterDelegate> [super destroy]; } - - _secureBackupSetupCoordinatorBridgePresenter = nil; + identityServerSettingsCoordinatorBridgePresenter = nil; } @@ -2964,13 +2955,11 @@ ChangePasswordCoordinatorBridgePresenterDelegate> { self.signOutButton = (UIButton*)sender; - MXKeyBackup *keyBackup = self.mainSession.crypto.backup; + SignOutFlowPresenter *flowPresenter = [[SignOutFlowPresenter alloc] initWithSession:self.mainSession presentingViewController:self]; + flowPresenter.delegate = self; - [self.signOutAlertPresenter presentFor:keyBackup.state - areThereKeysToBackup:keyBackup.hasKeysToBackup - from:self - sourceView:self.signOutButton - animated:YES]; + [flowPresenter startWithSourceView:self.signOutButton]; + self.signOutFlowPresenter = flowPresenter; } - (void)onRemove3PID:(NSIndexPath*)indexPath @@ -4181,123 +4170,25 @@ ChangePasswordCoordinatorBridgePresenterDelegate> self.notificationSettingsBridgePresenter = nil; } +#pragma mark - SignOutFlowPresenterDelegate -#pragma mark - SecureBackupSetupCoordinatorBridgePresenter - -- (void)showSecureBackupSetupFromSignOutFlow +- (void)signOutFlowPresenterDidStartLoading:(SignOutFlowPresenter *)presenter { - if (self.canSetupSecureBackup) - { - [self setupSecureBackup2]; - } - else - { - // Set up cross-signing first - [self setupCrossSigningWithTitle:[VectorL10n secureKeyBackupSetupIntroTitle] - message:[VectorL10n securitySettingsUserPasswordDescription] - success:^{ - [self setupSecureBackup2]; - } failure:^(NSError *error) { - }]; - } -} - -- (void)setupSecureBackup2 -{ - SecureBackupSetupCoordinatorBridgePresenter *secureBackupSetupCoordinatorBridgePresenter = [[SecureBackupSetupCoordinatorBridgePresenter alloc] initWithSession:self.mainSession allowOverwrite:YES]; - secureBackupSetupCoordinatorBridgePresenter.delegate = self; - - [secureBackupSetupCoordinatorBridgePresenter presentFrom:self animated:YES]; - - self.secureBackupSetupCoordinatorBridgePresenter = secureBackupSetupCoordinatorBridgePresenter; -} - -- (BOOL)canSetupSecureBackup -{ - return [self.mainSession vc_canSetupSecureBackup]; -} - -#pragma mark - SecureBackupSetupCoordinatorBridgePresenterDelegate - -- (void)secureBackupSetupCoordinatorBridgePresenterDelegateDidComplete:(SecureBackupSetupCoordinatorBridgePresenter *)coordinatorBridgePresenter -{ - [self.secureBackupSetupCoordinatorBridgePresenter dismissWithAnimated:YES completion:nil]; - self.secureBackupSetupCoordinatorBridgePresenter = nil; -} - -- (void)secureBackupSetupCoordinatorBridgePresenterDelegateDidCancel:(SecureBackupSetupCoordinatorBridgePresenter *)coordinatorBridgePresenter -{ - [self.secureBackupSetupCoordinatorBridgePresenter dismissWithAnimated:YES completion:nil]; - self.secureBackupSetupCoordinatorBridgePresenter = nil; -} - -#pragma mark - SignOutAlertPresenterDelegate - -- (void)signOutAlertPresenterDidTapBackupAction:(SignOutAlertPresenter * _Nonnull)presenter -{ - [self showSecureBackupSetupFromSignOutFlow]; -} - -- (void)signOutAlertPresenterDidTapSignOutAction:(SignOutAlertPresenter * _Nonnull)presenter -{ - // Prevent user to perform user interaction in settings when sign out - // TODO: Prevent user interaction in all application (navigation controller and split view controller included) + [self startActivityIndicator]; self.view.userInteractionEnabled = NO; self.signOutButton.enabled = NO; - - [self startActivityIndicator]; - - MXWeakify(self); - - [[AppDelegate theDelegate] logoutWithConfirmation:NO completion:^(BOOL isLoggedOut) { - MXStrongifyAndReturnIfNil(self); - - [self stopActivityIndicator]; - - self.view.userInteractionEnabled = YES; - self.signOutButton.enabled = YES; - }]; } -- (void)setupCrossSigningWithTitle:(NSString*)title - message:(NSString*)message - success:(void (^)(void))success - failure:(void (^)(NSError *error))failure - +- (void)signOutFlowPresenterDidStopLoading:(SignOutFlowPresenter *)presenter { - [self startActivityIndicator]; - self.view.userInteractionEnabled = NO; - - MXWeakify(self); - - void (^animationCompletion)(void) = ^void () { - MXStrongifyAndReturnIfNil(self); - - [self stopActivityIndicator]; - self.view.userInteractionEnabled = YES; - [self.crossSigningSetupCoordinatorBridgePresenter dismissWithAnimated:YES completion:^{}]; - self.crossSigningSetupCoordinatorBridgePresenter = nil; - }; - - CrossSigningSetupCoordinatorBridgePresenter *crossSigningSetupCoordinatorBridgePresenter = [[CrossSigningSetupCoordinatorBridgePresenter alloc] initWithSession:self.mainSession]; - - [crossSigningSetupCoordinatorBridgePresenter presentWith:title - message:message - from:self - animated:YES - success:^{ - animationCompletion(); - success(); - } cancel:^{ - animationCompletion(); - failure(nil); - } failure:^(NSError * _Nonnull error) { - animationCompletion(); - [[AppDelegate theDelegate] showErrorAsAlert:error]; - failure(error); - }]; - - self.crossSigningSetupCoordinatorBridgePresenter = crossSigningSetupCoordinatorBridgePresenter; + [self stopActivityIndicator]; + self.view.userInteractionEnabled = YES; + self.signOutButton.enabled = YES; +} + +- (void)signOutFlowPresenter:(SignOutFlowPresenter *)presenter didFailWith:(NSError *)error +{ + [[AppDelegate theDelegate] showErrorAsAlert:error]; } #pragma mark - SingleImagePickerPresenterDelegate diff --git a/Riot/Modules/Settings/SignOut/SignOutFlowPresenter.swift b/Riot/Modules/Settings/SignOut/SignOutFlowPresenter.swift new file mode 100644 index 000000000..e65f6fd5d --- /dev/null +++ b/Riot/Modules/Settings/SignOut/SignOutFlowPresenter.swift @@ -0,0 +1,162 @@ +// +// 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 + +@objc protocol SignOutFlowPresenterDelegate { + /// The presenter is starting an operation that might take while and the UI should indicate this. + func signOutFlowPresenterDidStartLoading(_ presenter: SignOutFlowPresenter) + /// The presenter has finished an operation and the UI should indicate this if necessary. + func signOutFlowPresenterDidStopLoading(_ presenter: SignOutFlowPresenter) + /// The presenter encountered an error and has stopped. + func signOutFlowPresenter(_ presenter: SignOutFlowPresenter, didFailWith error: Error) +} + +/// This class provides a reusable component to present the sign out flow +/// for the current session, including the initial prompt, and any follow-up +/// key-backup setup that is necessary for the user. +@objcMembers class SignOutFlowPresenter: NSObject { + private let session: MXSession + private let presentingViewController: UIViewController + + private var signOutAlertPresenter = SignOutAlertPresenter() + + weak var delegate: SignOutFlowPresenterDelegate? + + init(session: MXSession, presentingViewController: UIViewController) { + self.session = session + self.presentingViewController = presentingViewController + + super.init() + + signOutAlertPresenter.delegate = self + } + + /// Starts the flow without a specific source view. On iPad any popups + /// will show from the presenting view controller itself. + func start() { + start(sourceView: presentingViewController.view) + } + + /// Starts the flow, presenting any popups on iPad from the specified view. + func start(sourceView: UIView?) { + guard let keyBackup = session.crypto?.backup else { return } + + signOutAlertPresenter.present(for: keyBackup.state, + areThereKeysToBackup: keyBackup.hasKeysToBackup, + from: presentingViewController, + sourceView: sourceView ?? presentingViewController.view, + animated: true) + } + + // MARK: - SecureBackupSetupCoordinatorBridgePresenter + + private var secureBackupSetupCoordinatorBridgePresenter: SecureBackupSetupCoordinatorBridgePresenter? + private var crossSigningSetupCoordinatorBridgePresenter: CrossSigningSetupCoordinatorBridgePresenter? + + private func showSecureBackupSetupFromSignOutFlow() { + if canSetupSecureBackup { + setupSecureBackup() + } else { + // Set up cross-signing first + setupCrossSigning(title: VectorL10n.secureKeyBackupSetupIntroTitle, + message: VectorL10n.securitySettingsUserPasswordDescription) { [weak self] result in + guard let self = self else { return } + switch result { + case .success(let isCompleted): + if isCompleted { + self.setupSecureBackup() + } + case .failure(let error): + self.delegate?.signOutFlowPresenter(self, didFailWith: error) + } + } + } + } + + private var canSetupSecureBackup: Bool { + return session.vc_canSetupSecureBackup() + } + + private func setupSecureBackup() { + let secureBackupSetupCoordinatorBridgePresenter = SecureBackupSetupCoordinatorBridgePresenter(session: session, allowOverwrite: true) + secureBackupSetupCoordinatorBridgePresenter.delegate = self + secureBackupSetupCoordinatorBridgePresenter.present(from: presentingViewController, animated: true) + self.secureBackupSetupCoordinatorBridgePresenter = secureBackupSetupCoordinatorBridgePresenter + } + + private func setupCrossSigning(title: String, message: String, completion: @escaping (Result) -> Void) { + delegate?.signOutFlowPresenterDidStartLoading(self) + + let dismissAnimation = { [weak self] in + guard let self = self else { return } + + self.delegate?.signOutFlowPresenterDidStopLoading(self) + self.crossSigningSetupCoordinatorBridgePresenter?.dismiss(animated: true, completion: { + self.crossSigningSetupCoordinatorBridgePresenter = nil + }) + } + + let crossSigningSetupCoordinatorBridgePresenter = CrossSigningSetupCoordinatorBridgePresenter(session: session) + crossSigningSetupCoordinatorBridgePresenter.present(with: title, message: message, from: presentingViewController, animated: true) { + dismissAnimation() + completion(.success(true)) + } cancel: { + dismissAnimation() + completion(.success(false)) + } failure: { error in + dismissAnimation() + completion(.failure(error)) + } + + self.crossSigningSetupCoordinatorBridgePresenter = crossSigningSetupCoordinatorBridgePresenter + } +} + +// MARK: - SignOutAlertPresenterDelegate +extension SignOutFlowPresenter: SignOutAlertPresenterDelegate { + + func signOutAlertPresenterDidTapSignOutAction(_ presenter: SignOutAlertPresenter) { + // Allow presenting screen to black user interaction when signing out + // TODO: Prevent user interaction in all application (navigation controller and split view controller included) + delegate?.signOutFlowPresenterDidStartLoading(self) + + AppDelegate.theDelegate().logout(withConfirmation: false) { [weak self] isLoggedOut in + guard let self = self else { return } + self.delegate?.signOutFlowPresenterDidStopLoading(self) + } + } + + func signOutAlertPresenterDidTapBackupAction(_ presenter: SignOutAlertPresenter) { + showSecureBackupSetupFromSignOutFlow() + } + +} + +// MARK: - SecureBackupSetupCoordinatorBridgePresenterDelegate +extension SignOutFlowPresenter: SecureBackupSetupCoordinatorBridgePresenterDelegate { + func secureBackupSetupCoordinatorBridgePresenterDelegateDidCancel(_ coordinatorBridgePresenter: SecureBackupSetupCoordinatorBridgePresenter) { + coordinatorBridgePresenter.dismiss(animated: true) { + self.secureBackupSetupCoordinatorBridgePresenter = nil + } + } + + func secureBackupSetupCoordinatorBridgePresenterDelegateDidComplete(_ coordinatorBridgePresenter: SecureBackupSetupCoordinatorBridgePresenter) { + coordinatorBridgePresenter.dismiss(animated: true) { + self.secureBackupSetupCoordinatorBridgePresenter = nil + } + } +} diff --git a/RiotSwiftUI/Modules/UserSessions/Coordinator/UserSessionsFlowCoordinator.swift b/RiotSwiftUI/Modules/UserSessions/Coordinator/UserSessionsFlowCoordinator.swift index ac03303bd..80373a84d 100644 --- a/RiotSwiftUI/Modules/UserSessions/Coordinator/UserSessionsFlowCoordinator.swift +++ b/RiotSwiftUI/Modules/UserSessions/Coordinator/UserSessionsFlowCoordinator.swift @@ -28,6 +28,7 @@ final class UserSessionsFlowCoordinator: Coordinator, Presentable { private let navigationRouter: NavigationRouterType private var reauthenticationPresenter: ReauthenticationCoordinatorBridgePresenter? + private var signOutFlowPresenter: SignOutFlowPresenter? private var errorPresenter: MXKErrorPresentation private var indicatorPresenter: UserIndicatorTypePresenterProtocol private var loadingIndicator: UserIndicator? @@ -167,6 +168,11 @@ final class UserSessionsFlowCoordinator: Coordinator, Presentable { /// Shows a confirmation dialog to the user to sign out of a session. private func showLogoutConfirmation(for sessionInfo: UserSessionInfo) { + guard !sessionInfo.isCurrent else { + showLogoutConfirmationForCurrentSession() + return + } + // Use a UIAlertController as we don't have confirmationDialog in SwiftUI on iOS 14. let alert = UIAlertController(title: VectorL10n.signOutConfirmationMessage, message: nil, preferredStyle: .actionSheet) alert.addAction(UIAlertAction(title: VectorL10n.signOut, style: .destructive) { [weak self] _ in @@ -178,6 +184,14 @@ final class UserSessionsFlowCoordinator: Coordinator, Presentable { navigationRouter.present(alert, animated: true) } + private func showLogoutConfirmationForCurrentSession() { + let flowPresenter = SignOutFlowPresenter(session: parameters.session, presentingViewController: toPresentable()) + flowPresenter.delegate = self + + flowPresenter.start() + signOutFlowPresenter = flowPresenter + } + /// Prompts the user to authenticate (if necessary) in order to log out of a specific session. private func showLogoutAuthentication(for sessionInfo: UserSessionInfo) { startLoading() @@ -340,6 +354,22 @@ final class UserSessionsFlowCoordinator: Coordinator, Presentable { } } +// MARK: SignOutFlowPresenter + +extension UserSessionsFlowCoordinator: SignOutFlowPresenterDelegate { + func signOutFlowPresenterDidStartLoading(_ presenter: SignOutFlowPresenter) { + startLoading() + } + + func signOutFlowPresenterDidStopLoading(_ presenter: SignOutFlowPresenter) { + stopLoading() + } + + func signOutFlowPresenter(_ presenter: SignOutFlowPresenter, didFailWith error: Error) { + errorPresenter.presentError(from: toPresentable(), forError: error, animated: true, handler: { }) + } +} + // MARK: CrossSigningSetupCoordinatorDelegate extension UserSessionsFlowCoordinator: CrossSigningSetupCoordinatorDelegate { diff --git a/changelog.d/6854.change b/changelog.d/6854.change new file mode 100644 index 000000000..c00725bc3 --- /dev/null +++ b/changelog.d/6854.change @@ -0,0 +1 @@ +Sign Out: Add a SignOutFlowPresenter and use this in All Chats, Settings and the Device Manager.