diff --git a/Documentation/Acknowledgments.md b/Documentation/Acknowledgments.md index a9389bb9f..91e2d463d 100644 --- a/Documentation/Acknowledgments.md +++ b/Documentation/Acknowledgments.md @@ -17,7 +17,6 @@ - [Nuke-FLAnimatedImage-Plugin](https://github.com/kean/Nuke-FLAnimatedImage-Plugin) - [Nuke](https://github.com/kean/Nuke) - [Pageboy](https://github.com/uias/Pageboy#the-basics) -- [PanModal](https://github.com/slackhq/PanModal.git) - [SDWebImage](https://github.com/SDWebImage/SDWebImage) - [swift-collections](https://github.com/apple/swift-collections) - [swift-nio](https://github.com/apple/swift-nio) diff --git a/Localization/StringsConvertor/input/Base.lproj/app.json b/Localization/StringsConvertor/input/Base.lproj/app.json index 261d40e95..c54b4f821 100644 --- a/Localization/StringsConvertor/input/Base.lproj/app.json +++ b/Localization/StringsConvertor/input/Base.lproj/app.json @@ -877,7 +877,9 @@ "account_list": { "tab_bar_hint": "Current selected profile: %s. Double tap then hold to show account switcher", "dismiss_account_switcher": "Dismiss Account Switcher", - "add_account": "Add Account" + "add_account": "Add Account", + "logout_all_accounts": "Log Out Of All Accounts", + "logout": "Logout" }, "bookmark": { "title": "Bookmarks" diff --git a/Localization/app.json b/Localization/app.json index 261d40e95..c54b4f821 100644 --- a/Localization/app.json +++ b/Localization/app.json @@ -877,7 +877,9 @@ "account_list": { "tab_bar_hint": "Current selected profile: %s. Double tap then hold to show account switcher", "dismiss_account_switcher": "Dismiss Account Switcher", - "add_account": "Add Account" + "add_account": "Add Account", + "logout_all_accounts": "Log Out Of All Accounts", + "logout": "Logout" }, "bookmark": { "title": "Bookmarks" diff --git a/Mastodon.xcodeproj/project.pbxproj b/Mastodon.xcodeproj/project.pbxproj index 5c4fdf59b..1c7cbf890 100644 --- a/Mastodon.xcodeproj/project.pbxproj +++ b/Mastodon.xcodeproj/project.pbxproj @@ -173,6 +173,7 @@ D8D688F92AB8B970000F651A /* SearchResultOverviewCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8D688F82AB8B970000F651A /* SearchResultOverviewCoordinator.swift */; }; D8E5C346296DAB84007E76A7 /* DataSourceFacade+Status+History.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8E5C345296DAB84007E76A7 /* DataSourceFacade+Status+History.swift */; }; D8E5C349296DB8A3007E76A7 /* StatusEditHistoryViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8E5C348296DB8A3007E76A7 /* StatusEditHistoryViewController.swift */; }; + D8E64F412BA84F80003A4539 /* LogoutOfAllAccountsCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8E64F402BA84F80003A4539 /* LogoutOfAllAccountsCell.swift */; }; D8ECC8102AC31EA400AE0818 /* NotificationSettingsDisabledTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8ECC80F2AC31EA400AE0818 /* NotificationSettingsDisabledTableViewCell.swift */; }; D8F0372C29D232730027DE2E /* HashtagIntentHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8F0372B29D232730027DE2E /* HashtagIntentHandler.swift */; }; D8F8A03A29CA5C15000195DD /* HashtagWidgetView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8F8A03929CA5C15000195DD /* HashtagWidgetView.swift */; }; @@ -812,6 +813,7 @@ D8D688F82AB8B970000F651A /* SearchResultOverviewCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchResultOverviewCoordinator.swift; sourceTree = ""; }; D8E5C345296DAB84007E76A7 /* DataSourceFacade+Status+History.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "DataSourceFacade+Status+History.swift"; sourceTree = ""; }; D8E5C348296DB8A3007E76A7 /* StatusEditHistoryViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusEditHistoryViewController.swift; sourceTree = ""; }; + D8E64F402BA84F80003A4539 /* LogoutOfAllAccountsCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LogoutOfAllAccountsCell.swift; sourceTree = ""; }; D8ECC80F2AC31EA400AE0818 /* NotificationSettingsDisabledTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationSettingsDisabledTableViewCell.swift; sourceTree = ""; }; D8F0372B29D232730027DE2E /* HashtagIntentHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HashtagIntentHandler.swift; sourceTree = ""; }; D8F8A03929CA5C15000195DD /* HashtagWidgetView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HashtagWidgetView.swift; sourceTree = ""; }; @@ -2709,6 +2711,7 @@ children = ( DB9F58F026EF512300E7BBE9 /* AccountListTableViewCell.swift */, DBA5A53426F0A36A00CACBAA /* AddAccountTableViewCell.swift */, + D8E64F402BA84F80003A4539 /* LogoutOfAllAccountsCell.swift */, ); path = Cell; sourceTree = ""; @@ -3799,6 +3802,7 @@ 85BC11B32932414900E191CD /* AltTextViewController.swift in Sources */, DB63F775279A997D00455B82 /* NotificationTableViewCell+ViewModel.swift in Sources */, DB98EB5927B109890082E365 /* ReportSupplementaryViewController.swift in Sources */, + D8E64F412BA84F80003A4539 /* LogoutOfAllAccountsCell.swift in Sources */, DB0617EB277EF3820030EE79 /* GradientBorderView.swift in Sources */, DB789A1225F9F2CC0071ACA0 /* ComposeViewModel.swift in Sources */, DBEFCD7D282A2A3B00C0ABEA /* ReportServerRulesViewController.swift in Sources */, diff --git a/Mastodon/Coordinator/SceneCoordinator.swift b/Mastodon/Coordinator/SceneCoordinator.swift index 01942a5ce..3bd441c03 100644 --- a/Mastodon/Coordinator/SceneCoordinator.swift +++ b/Mastodon/Coordinator/SceneCoordinator.swift @@ -8,7 +8,6 @@ import UIKit import Combine import SafariServices import CoreDataStack -import PanModal import MastodonSDK import MastodonCore import MastodonAsset @@ -155,12 +154,12 @@ extension SceneCoordinator { case showDetail // replace case modal(animated: Bool, completion: (() -> Void)? = nil) case popover(sourceView: UIView) - case panModal case custom(transitioningDelegate: UIViewControllerTransitioningDelegate) case customPush(animated: Bool) case safariPresent(animated: Bool, completion: (() -> Void)? = nil) case alertController(animated: Bool, completion: (() -> Void)? = nil) case activityViewControllerPresent(animated: Bool, completion: (() -> Void)? = nil) + case formSheet case none } @@ -307,76 +306,70 @@ extension SceneCoordinator { let topViewController = navigationController.topViewController { presentingViewController = topViewController } - + switch transition { - case .none: - // do nothing - break - case .show: - presentingViewController.show(viewController, sender: sender) - case .showDetail: - secondaryStackHashValues.insert(viewController.hashValue) - let navigationController = AdaptiveStatusBarStyleNavigationController(rootViewController: viewController) - presentingViewController.showDetailViewController(navigationController, sender: sender) + case .none: + // do nothing + break + case .show: + presentingViewController.show(viewController, sender: sender) + case .showDetail: + secondaryStackHashValues.insert(viewController.hashValue) + let navigationController = AdaptiveStatusBarStyleNavigationController(rootViewController: viewController) + presentingViewController.showDetailViewController(navigationController, sender: sender) - case .modal(let animated, let completion): - let modalNavigationController: UINavigationController = { - if scene.isOnboarding { - return OnboardingNavigationController(rootViewController: viewController) - } else { - return AdaptiveStatusBarStyleNavigationController(rootViewController: viewController) - } - }() - modalNavigationController.modalPresentationCapturesStatusBarAppearance = true - if let adaptivePresentationControllerDelegate = viewController as? UIAdaptivePresentationControllerDelegate { - modalNavigationController.presentationController?.delegate = adaptivePresentationControllerDelegate - } - presentingViewController.present(modalNavigationController, animated: animated, completion: completion) - - case .panModal: - guard let panModalPresentable = viewController as? PanModalPresentable & UIViewController else { - assertionFailure() - return nil - } - - // https://github.com/slackhq/PanModal/issues/74#issuecomment-572426441 - panModalPresentable.modalPresentationStyle = .custom - panModalPresentable.modalPresentationCapturesStatusBarAppearance = true - panModalPresentable.transitioningDelegate = PanModalPresentationDelegate.default - presentingViewController.present(panModalPresentable, animated: true, completion: nil) - //presentingViewController.presentPanModal(panModalPresentable) - case .popover(let sourceView): - viewController.modalPresentationStyle = .popover - viewController.popoverPresentationController?.sourceView = sourceView - (splitViewController ?? presentingViewController)?.present(viewController, animated: true, completion: nil) - case .custom(let transitioningDelegate): - viewController.modalPresentationStyle = .custom - viewController.transitioningDelegate = transitioningDelegate - viewController.modalPresentationCapturesStatusBarAppearance = true - (splitViewController ?? presentingViewController)?.present(viewController, animated: true, completion: nil) - - case .customPush(let animated): - // set delegate in view controller - assert(sender?.navigationController?.delegate != nil) - sender?.navigationController?.pushViewController(viewController, animated: animated) - - case .safariPresent(let animated, let completion): - if UserDefaults.shared.preferredUsingDefaultBrowser, case let .safari(url) = scene { - UIApplication.shared.open(url, options: [:], completionHandler: nil) + case .modal(let animated, let completion): + let modalNavigationController: UINavigationController = { + if scene.isOnboarding { + return OnboardingNavigationController(rootViewController: viewController) } else { - viewController.modalPresentationCapturesStatusBarAppearance = true - presentingViewController.present(viewController, animated: animated, completion: completion) + return AdaptiveStatusBarStyleNavigationController(rootViewController: viewController) } + }() + modalNavigationController.modalPresentationCapturesStatusBarAppearance = true + if let adaptivePresentationControllerDelegate = viewController as? UIAdaptivePresentationControllerDelegate { + modalNavigationController.presentationController?.delegate = adaptivePresentationControllerDelegate + } + presentingViewController.present(modalNavigationController, animated: animated, completion: completion) + case .popover(let sourceView): + viewController.modalPresentationStyle = .popover + viewController.popoverPresentationController?.sourceView = sourceView + (splitViewController ?? presentingViewController)?.present(viewController, animated: true, completion: nil) + case .custom(let transitioningDelegate): + viewController.modalPresentationStyle = .custom + viewController.transitioningDelegate = transitioningDelegate + viewController.modalPresentationCapturesStatusBarAppearance = true + (splitViewController ?? presentingViewController)?.present(viewController, animated: true, completion: nil) - case .alertController(let animated, let completion): + case .customPush(let animated): + // set delegate in view controller + assert(sender?.navigationController?.delegate != nil) + sender?.navigationController?.pushViewController(viewController, animated: animated) + + case .safariPresent(let animated, let completion): + if UserDefaults.shared.preferredUsingDefaultBrowser, case let .safari(url) = scene { + UIApplication.shared.open(url, options: [:], completionHandler: nil) + } else { viewController.modalPresentationCapturesStatusBarAppearance = true presentingViewController.present(viewController, animated: animated, completion: completion) + } - case .activityViewControllerPresent(let animated, let completion): - viewController.modalPresentationCapturesStatusBarAppearance = true - presentingViewController.present(viewController, animated: animated, completion: completion) + case .alertController(let animated, let completion): + viewController.modalPresentationCapturesStatusBarAppearance = true + presentingViewController.present(viewController, animated: animated, completion: completion) + + case .activityViewControllerPresent(let animated, let completion): + viewController.modalPresentationCapturesStatusBarAppearance = true + presentingViewController.present(viewController, animated: animated, completion: completion) + + case .formSheet: + viewController.modalPresentationStyle = .formSheet + if let sheetPresentation = viewController.sheetPresentationController { + sheetPresentation.detents = [.large(), .medium()] + } + presentingViewController.present(viewController, animated: true) } - + return viewController } @@ -397,175 +390,175 @@ private extension SceneCoordinator { let viewController: UIViewController? switch scene { - case .welcome: - let _viewController = WelcomeViewController() - viewController = _viewController - case .mastodonPickServer(let viewModel): - let _viewController = MastodonPickServerViewController() - _viewController.viewModel = viewModel - viewController = _viewController - case .mastodonRegister(let viewModel): - let _viewController = MastodonRegisterViewController() - _viewController.viewModel = viewModel - viewController = _viewController - case .mastodonServerRules(let viewModel): - let _viewController = MastodonServerRulesViewController(viewModel: viewModel) - viewController = _viewController - case .mastodonConfirmEmail(let viewModel): - let _viewController = MastodonConfirmEmailViewController() - _viewController.viewModel = viewModel - viewController = _viewController - case .mastodonLogin: - let loginViewController = MastodonLoginViewController(appContext: appContext, - authenticationViewModel: AuthenticationViewModel(context: appContext, coordinator: self, isAuthenticationExist: false), - sceneCoordinator: self) - loginViewController.delegate = self + case .welcome: + let _viewController = WelcomeViewController() + viewController = _viewController + case .mastodonPickServer(let viewModel): + let _viewController = MastodonPickServerViewController() + _viewController.viewModel = viewModel + viewController = _viewController + case .mastodonRegister(let viewModel): + let _viewController = MastodonRegisterViewController() + _viewController.viewModel = viewModel + viewController = _viewController + case .mastodonServerRules(let viewModel): + let _viewController = MastodonServerRulesViewController(viewModel: viewModel) + viewController = _viewController + case .mastodonConfirmEmail(let viewModel): + let _viewController = MastodonConfirmEmailViewController() + _viewController.viewModel = viewModel + viewController = _viewController + case .mastodonLogin: + let loginViewController = MastodonLoginViewController(appContext: appContext, + authenticationViewModel: AuthenticationViewModel(context: appContext, coordinator: self, isAuthenticationExist: false), + sceneCoordinator: self) + loginViewController.delegate = self - viewController = loginViewController - case .mastodonPrivacyPolicies(let viewModel): - let privacyViewController = PrivacyTableViewController(context: appContext, coordinator: self, viewModel: viewModel) - viewController = privacyViewController - case .mastodonResendEmail(let viewModel): - let _viewController = MastodonResendEmailViewController() - _viewController.viewModel = viewModel - viewController = _viewController - case .mastodonWebView(let viewModel): - let _viewController = WebViewController() - _viewController.viewModel = viewModel - viewController = _viewController - case .searchDetail(let viewModel): - let _viewController = SearchDetailViewController(appContext: appContext, sceneCoordinator: self, authContext: viewModel.authContext) - _viewController.viewModel = viewModel - viewController = _viewController - case .searchResult(let viewModel): - let searchResultViewController = SearchResultViewController() - searchResultViewController.context = appContext - searchResultViewController.coordinator = self - searchResultViewController.viewModel = viewModel - viewController = searchResultViewController - case .compose(let viewModel): - let _viewController = ComposeViewController(viewModel: viewModel) - viewController = _viewController - case .thread(let viewModel): - let _viewController = ThreadViewController() - _viewController.viewModel = viewModel - viewController = _viewController - case .editHistory(let viewModel): - let editHistoryViewController = StatusEditHistoryViewController(viewModel: viewModel) - viewController = editHistoryViewController - case .hashtagTimeline(let viewModel): - let _viewController = HashtagTimelineViewController() - _viewController.viewModel = viewModel - viewController = _viewController - case .accountList(let viewModel): - let _viewController = AccountListViewController() - _viewController.viewModel = viewModel - viewController = _viewController - case .profile(let viewModel): - let _viewController = ProfileViewController() - _viewController.viewModel = viewModel - viewController = _viewController - case .bookmark(let viewModel): - let _viewController = BookmarkViewController() - _viewController.viewModel = viewModel - viewController = _viewController - case .followedTags(let viewModel): - guard let authContext else { return nil } - - viewController = FollowedTagsViewController(appContext: appContext, sceneCoordinator: self, authContext: authContext, viewModel: viewModel) - case .favorite(let viewModel): - let _viewController = FavoriteViewController() - _viewController.viewModel = viewModel - viewController = _viewController - case .follower(let viewModel): - let followerListViewController = FollowerListViewController(viewModel: viewModel, coordinator: self, context: appContext) - viewController = followerListViewController - case .following(let viewModel): - let followingListViewController = FollowingListViewController(viewModel: viewModel, coordinator: self, context: appContext) - viewController = followingListViewController - case .familiarFollowers(let viewModel): - viewController = FamiliarFollowersViewController(viewModel: viewModel, context: appContext, coordinator: self) - case .rebloggedBy(let viewModel): - let _viewController = RebloggedByViewController() - _viewController.viewModel = viewModel - viewController = _viewController - case .favoritedBy(let viewModel): - let _viewController = FavoritedByViewController() - _viewController.viewModel = viewModel - viewController = _viewController - case .report(let viewModel): - viewController = ReportViewController(viewModel: viewModel) - case .reportServerRules(let viewModel): - let _viewController = ReportServerRulesViewController() - _viewController.viewModel = viewModel - viewController = _viewController - case .reportStatus(let viewModel): - let _viewController = ReportStatusViewController() - _viewController.viewModel = viewModel - viewController = _viewController - case .reportSupplementary(let viewModel): - let _viewController = ReportSupplementaryViewController() - _viewController.viewModel = viewModel - viewController = _viewController - case .reportResult(let viewModel): - let _viewController = ReportResultViewController() - _viewController.viewModel = viewModel - viewController = _viewController - case .suggestionAccount(let viewModel): - let _viewController = SuggestionAccountViewController() - _viewController.viewModel = viewModel - viewController = _viewController - case .mediaPreview(let viewModel): - let _viewController = MediaPreviewViewController() - _viewController.viewModel = viewModel - viewController = _viewController - case .safari(let url): - guard let scheme = url.scheme?.lowercased(), - scheme == "http" || scheme == "https" else { - return nil - } - let _viewController = SFSafariViewController(url: url) - _viewController.preferredBarTintColor = SystemTheme.navigationBarBackgroundColor - _viewController.preferredControlTintColor = Asset.Colors.Brand.blurple.color - viewController = _viewController - - case .alertController(let alertController): - if let popoverPresentationController = alertController.popoverPresentationController { - assert( - popoverPresentationController.sourceView != nil || - popoverPresentationController.sourceRect != .zero || - popoverPresentationController.barButtonItem != nil - ) - } - viewController = alertController - case .activityViewController(let activityViewController, let sourceView, let barButtonItem): - activityViewController.popoverPresentationController?.sourceView = sourceView - activityViewController.popoverPresentationController?.barButtonItem = barButtonItem - viewController = activityViewController - case .settings(let setting): - guard let presentedOn = sender, - let accountName = authContext?.mastodonAuthenticationBox.authentication.username, - let authContext - else { return nil } - - let settingsCoordinator = SettingsCoordinator(presentedOn: presentedOn, - accountName: accountName, - setting: setting, - appContext: appContext, - authContext: authContext, - sceneCoordinator: self + viewController = loginViewController + case .mastodonPrivacyPolicies(let viewModel): + let privacyViewController = PrivacyTableViewController(context: appContext, coordinator: self, viewModel: viewModel) + viewController = privacyViewController + case .mastodonResendEmail(let viewModel): + let _viewController = MastodonResendEmailViewController() + _viewController.viewModel = viewModel + viewController = _viewController + case .mastodonWebView(let viewModel): + let _viewController = WebViewController() + _viewController.viewModel = viewModel + viewController = _viewController + case .searchDetail(let viewModel): + let _viewController = SearchDetailViewController(appContext: appContext, sceneCoordinator: self, authContext: viewModel.authContext) + _viewController.viewModel = viewModel + viewController = _viewController + case .searchResult(let viewModel): + let searchResultViewController = SearchResultViewController() + searchResultViewController.context = appContext + searchResultViewController.coordinator = self + searchResultViewController.viewModel = viewModel + viewController = searchResultViewController + case .compose(let viewModel): + let _viewController = ComposeViewController(viewModel: viewModel) + viewController = _viewController + case .thread(let viewModel): + let _viewController = ThreadViewController() + _viewController.viewModel = viewModel + viewController = _viewController + case .editHistory(let viewModel): + let editHistoryViewController = StatusEditHistoryViewController(viewModel: viewModel) + viewController = editHistoryViewController + case .hashtagTimeline(let viewModel): + let _viewController = HashtagTimelineViewController() + _viewController.viewModel = viewModel + viewController = _viewController + case .accountList(let viewModel): + let accountListViewController = AccountListViewController() + accountListViewController.viewModel = viewModel + viewController = accountListViewController + case .profile(let viewModel): + let _viewController = ProfileViewController() + _viewController.viewModel = viewModel + viewController = _viewController + case .bookmark(let viewModel): + let _viewController = BookmarkViewController() + _viewController.viewModel = viewModel + viewController = _viewController + case .followedTags(let viewModel): + guard let authContext else { return nil } + + viewController = FollowedTagsViewController(appContext: appContext, sceneCoordinator: self, authContext: authContext, viewModel: viewModel) + case .favorite(let viewModel): + let _viewController = FavoriteViewController() + _viewController.viewModel = viewModel + viewController = _viewController + case .follower(let viewModel): + let followerListViewController = FollowerListViewController(viewModel: viewModel, coordinator: self, context: appContext) + viewController = followerListViewController + case .following(let viewModel): + let followingListViewController = FollowingListViewController(viewModel: viewModel, coordinator: self, context: appContext) + viewController = followingListViewController + case .familiarFollowers(let viewModel): + viewController = FamiliarFollowersViewController(viewModel: viewModel, context: appContext, coordinator: self) + case .rebloggedBy(let viewModel): + let _viewController = RebloggedByViewController() + _viewController.viewModel = viewModel + viewController = _viewController + case .favoritedBy(let viewModel): + let _viewController = FavoritedByViewController() + _viewController.viewModel = viewModel + viewController = _viewController + case .report(let viewModel): + viewController = ReportViewController(viewModel: viewModel) + case .reportServerRules(let viewModel): + let _viewController = ReportServerRulesViewController() + _viewController.viewModel = viewModel + viewController = _viewController + case .reportStatus(let viewModel): + let _viewController = ReportStatusViewController() + _viewController.viewModel = viewModel + viewController = _viewController + case .reportSupplementary(let viewModel): + let _viewController = ReportSupplementaryViewController() + _viewController.viewModel = viewModel + viewController = _viewController + case .reportResult(let viewModel): + let _viewController = ReportResultViewController() + _viewController.viewModel = viewModel + viewController = _viewController + case .suggestionAccount(let viewModel): + let _viewController = SuggestionAccountViewController() + _viewController.viewModel = viewModel + viewController = _viewController + case .mediaPreview(let viewModel): + let _viewController = MediaPreviewViewController() + _viewController.viewModel = viewModel + viewController = _viewController + case .safari(let url): + guard let scheme = url.scheme?.lowercased(), + scheme == "http" || scheme == "https" else { + return nil + } + let _viewController = SFSafariViewController(url: url) + _viewController.preferredBarTintColor = SystemTheme.navigationBarBackgroundColor + _viewController.preferredControlTintColor = Asset.Colors.Brand.blurple.color + viewController = _viewController + + case .alertController(let alertController): + if let popoverPresentationController = alertController.popoverPresentationController { + assert( + popoverPresentationController.sourceView != nil || + popoverPresentationController.sourceRect != .zero || + popoverPresentationController.barButtonItem != nil ) - settingsCoordinator.delegate = self - settingsCoordinator.start() - - viewController = settingsCoordinator.navigationController - childCoordinator = settingsCoordinator - - case .editStatus(let viewModel): - let composeViewController = ComposeViewController(viewModel: viewModel) - viewController = composeViewController + } + viewController = alertController + case .activityViewController(let activityViewController, let sourceView, let barButtonItem): + activityViewController.popoverPresentationController?.sourceView = sourceView + activityViewController.popoverPresentationController?.barButtonItem = barButtonItem + viewController = activityViewController + case .settings(let setting): + guard let presentedOn = sender, + let accountName = authContext?.mastodonAuthenticationBox.authentication.username, + let authContext + else { return nil } + + let settingsCoordinator = SettingsCoordinator(presentedOn: presentedOn, + accountName: accountName, + setting: setting, + appContext: appContext, + authContext: authContext, + sceneCoordinator: self + ) + settingsCoordinator.delegate = self + settingsCoordinator.start() + + viewController = settingsCoordinator.navigationController + childCoordinator = settingsCoordinator + + case .editStatus(let viewModel): + let composeViewController = ComposeViewController(viewModel: viewModel) + viewController = composeViewController } - + setupDependency(for: viewController as? NeedsDependency) return viewController diff --git a/Mastodon/Scene/Account/AccountListViewModel.swift b/Mastodon/Scene/Account/AccountListViewModel.swift index ea2206703..e081f432c 100644 --- a/Mastodon/Scene/Account/AccountListViewModel.swift +++ b/Mastodon/Scene/Account/AccountListViewModel.swift @@ -25,7 +25,6 @@ final class AccountListViewModel: NSObject { // output @Published var items: [Item] = [] - let dataSourceDidUpdate = PassthroughSubject() var diffableDataSource: UITableViewDiffableDataSource! init(context: AppContext, authContext: AuthContext) { @@ -49,9 +48,11 @@ final class AccountListViewModel: NSObject { snapshot.appendItems(authenticationItems, toSection: .main) snapshot.appendItems([.addAccount], toSection: .main) - diffableDataSource.apply(snapshot) { - self.dataSourceDidUpdate.send() + if authentications.count > 1 { + snapshot.appendItems([.logoutOfAllAccounts], toSection: .main) } + + diffableDataSource.apply(snapshot, animatingDifferences: false) } .store(in: &disposeBag) } @@ -66,12 +67,10 @@ extension AccountListViewModel { enum Item: Hashable { case authentication(record: MastodonAuthentication) case addAccount + case logoutOfAllAccounts } - func setupDiffableDataSource( - tableView: UITableView, - managedObjectContext: NSManagedObjectContext - ) { + func setupDiffableDataSource(tableView: UITableView) { diffableDataSource = UITableViewDiffableDataSource(tableView: tableView) { tableView, indexPath, item in switch item { case .authentication(let record): @@ -79,7 +78,6 @@ extension AccountListViewModel { if let activeAuthentication = AuthenticationServiceProvider.shared.authenticationSortedByActivation().first { AccountListViewModel.configure( - in: managedObjectContext, cell: cell, authentication: record, activeAuthentication: activeAuthentication @@ -89,6 +87,9 @@ extension AccountListViewModel { case .addAccount: let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: AddAccountTableViewCell.self), for: indexPath) as! AddAccountTableViewCell return cell + case .logoutOfAllAccounts: + let cell = tableView.dequeueReusableCell(withIdentifier: LogoutOfAllAccountsCell.reuseIdentifier, for: indexPath) as! LogoutOfAllAccountsCell + return cell } } @@ -98,7 +99,6 @@ extension AccountListViewModel { } static func configure( - in context: NSManagedObjectContext, cell: AccountListTableViewCell, authentication: MastodonAuthentication, activeAuthentication: MastodonAuthentication diff --git a/Mastodon/Scene/Account/AccountViewController.swift b/Mastodon/Scene/Account/AccountViewController.swift index cb64e364b..f618b4c3c 100644 --- a/Mastodon/Scene/Account/AccountViewController.swift +++ b/Mastodon/Scene/Account/AccountViewController.swift @@ -8,7 +8,6 @@ import UIKit import Combine import CoreDataStack -import PanModal import MastodonAsset import MastodonLocalization import MastodonCore @@ -35,14 +34,14 @@ final class AccountListViewController: UIViewController, NeedsDependency { self?.dismiss(animated: true, completion: nil) } - var hasLoaded = false private(set) lazy var tableView: UITableView = { let tableView = UITableView() tableView.register(AccountListTableViewCell.self, forCellReuseIdentifier: String(describing: AccountListTableViewCell.self)) tableView.register(AddAccountTableViewCell.self, forCellReuseIdentifier: String(describing: AddAccountTableViewCell.self)) + tableView.register(LogoutOfAllAccountsCell.self, forCellReuseIdentifier: LogoutOfAllAccountsCell.reuseIdentifier) tableView.backgroundColor = .clear - tableView.separatorStyle = .none tableView.tableFooterView = UIView() + tableView.separatorInset = UIEdgeInsets(top: 0, left: 16, bottom: 0, right: 0) return tableView }() @@ -51,38 +50,11 @@ final class AccountListViewController: UIViewController, NeedsDependency { } } -// MARK: - PanModalPresentable -extension AccountListViewController: PanModalPresentable { - var panScrollable: UIScrollView? { tableView } - var showDragIndicator: Bool { false } - - var shortFormHeight: PanModalHeight { - func calculateHeight(of numberOfItems: Int) -> CGFloat { - return CGFloat(numberOfItems * 60 + 64) - } - - if hasLoaded { - let height = calculateHeight(of: viewModel.diffableDataSource.snapshot().numberOfItems) - return .contentHeight(CGFloat(height)) - } - - let authenticationCount = AuthenticationServiceProvider.shared.authentications.count - - let count = authenticationCount + 1 - let height = calculateHeight(of: count) - return .contentHeight(height) - } - - var longFormHeight: PanModalHeight { - return .maxHeightWithTopInset(0) - } -} - extension AccountListViewController { override func viewDidLoad() { super.viewDidLoad() - setupBackgroundColor() + view.backgroundColor = .secondarySystemGroupedBackground navigationItem.rightBarButtonItem = addBarButtonItem dragIndicatorView.translatesAutoresizingMaskIntoConstraints = false @@ -104,40 +76,8 @@ extension AccountListViewController { ]) tableView.delegate = self - viewModel.setupDiffableDataSource( - tableView: tableView, - managedObjectContext: context.managedObjectContext - ) - - viewModel.dataSourceDidUpdate - .receive(on: DispatchQueue.main) - .sink { [weak self, weak presentingViewController] in - guard let self = self else { return } - - // the presentingViewController may deinit. - // Hold it and check the window to prevent PanModel crash - guard let _ = presentingViewController else { return } - guard self.view.window != nil else { return } - - self.hasLoaded = true - self.panModalSetNeedsLayoutUpdate() // <<< may crash the app - self.panModalTransition(to: .shortForm) - } - .store(in: &disposeBag) + viewModel.setupDiffableDataSource(tableView: tableView) } - - private func setupBackgroundColor() { - let backgroundColor = UIColor { traitCollection in - switch traitCollection.userInterfaceLevel { - case .elevated where traitCollection.userInterfaceStyle == .dark: - return SystemTheme.systemElevatedBackgroundColor - default: - return .systemBackground.withAlphaComponent(0.9) - } - } - view.backgroundColor = backgroundColor - } - } extension AccountListViewController { @@ -155,11 +95,50 @@ extension AccountListViewController { // MARK: - UITableViewDelegate extension AccountListViewController: UITableViewDelegate { + + func tableView(_ tableView: UITableView, trailingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? { + guard let diffableDataSource = viewModel.diffableDataSource, + let item = diffableDataSource.itemIdentifier(for: indexPath) else { return nil } + + switch item { + case .authentication(let record): + let logoutAction = UIContextualAction(style: .destructive, title: L10n.Scene.AccountList.logout, handler: { [weak self] action, view, completion in + guard let self else { return } + + UserDefaults.shared.setNotificationCountWithAccessToken(accessToken: record.userAccessToken, value: 0) + + Task { @MainActor in + do { + try await self.viewModel.context.authenticationService.signOutMastodonUser(authentication: record) + + let userIdentifier = record + FileManager.default.invalidateHomeTimelineCache(for: userIdentifier) + FileManager.default.invalidateNotificationsAll(for: userIdentifier) + FileManager.default.invalidateNotificationsMentions(for: userIdentifier) + self.coordinator.setup() + + } catch { + assertionFailure("Failed to delete Authentication: \(error)") + } + + } + }) + logoutAction.image = UIImage(systemName: "rectangle.portrait.and.arrow.forward") + + let swipeConfiguration = UISwipeActionsConfiguration(actions: [logoutAction]) + swipeConfiguration.performsFirstActionWithFullSwipe = false + return swipeConfiguration + case .addAccount, .logoutOfAllAccounts: + return nil + } + + } + func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { tableView.deselectRow(at: indexPath, animated: true) - guard let diffableDataSource = viewModel.diffableDataSource else { return } - guard let item = diffableDataSource.itemIdentifier(for: indexPath) else { return } + guard let diffableDataSource = viewModel.diffableDataSource, + let item = diffableDataSource.itemIdentifier(for: indexPath) else { return } switch item { case .authentication(let record): @@ -172,6 +151,27 @@ extension AccountListViewController: UITableViewDelegate { case .addAccount: // TODO: add dismiss entry for welcome scene _ = coordinator.present(scene: .welcome, from: self, transition: .modal(animated: true, completion: nil)) + case .logoutOfAllAccounts: + let alert = UIAlertController(title: nil, message: nil, preferredStyle: .actionSheet) + + let logoutAction = UIAlertAction(title: L10n.Scene.AccountList.logoutAllAccounts, style: .destructive) { _ in + Task { @MainActor in + self.coordinator.showLoading() + for authenticationBox in self.context.authenticationService.mastodonAuthenticationBoxes { + try? await self.context.authenticationService.signOutMastodonUser(authenticationBox: authenticationBox) + } + self.coordinator.hideLoading() + + self.coordinator.setup() + } + } + + alert.addAction(logoutAction) + alert.popoverPresentationController?.sourceView = tableView.cellForRow(at: indexPath) + + let cancelAction = UIAlertAction(title: L10n.Common.Controls.Actions.cancel, style: .default) + alert.addAction(cancelAction) + present(alert, animated: true) } } } diff --git a/Mastodon/Scene/Account/Cell/AccountListTableViewCell.swift b/Mastodon/Scene/Account/Cell/AccountListTableViewCell.swift index 11569cdd3..53bed876a 100644 --- a/Mastodon/Scene/Account/Cell/AccountListTableViewCell.swift +++ b/Mastodon/Scene/Account/Cell/AccountListTableViewCell.swift @@ -1,9 +1,4 @@ -// -// AccountListTableViewCell.swift -// Mastodon -// -// Created by Cirno MainasuK on 2021-9-13. -// +// Copyright © 2023 Mastodon gGmbH. All rights reserved. import UIKit import Combine @@ -26,8 +21,7 @@ final class AccountListTableViewCell: UITableViewCell { let imageView = UIImageView(image: image) return imageView }() - let separatorLine = UIView.separatorLine - + override func prepareForReuse() { super.prepareForReuse() @@ -107,15 +101,6 @@ extension AccountListTableViewCell { usernameLabel.isUserInteractionEnabled = false badgeButton.isUserInteractionEnabled = false - separatorLine.translatesAutoresizingMaskIntoConstraints = false - contentView.addSubview(separatorLine) - NSLayoutConstraint.activate([ - separatorLine.leadingAnchor.constraint(equalTo: contentView.readableContentGuide.leadingAnchor), - separatorLine.trailingAnchor.constraint(equalTo: trailingAnchor), // needs align to edge - separatorLine.bottomAnchor.constraint(equalTo: contentView.bottomAnchor), - separatorLine.heightAnchor.constraint(equalToConstant: UIView.separatorLineHeight(of: contentView)), - ]) - badgeButton.setBadge(number: 0) checkmarkImageView.isHidden = true diff --git a/Mastodon/Scene/Account/Cell/AddAccountTableViewCell.swift b/Mastodon/Scene/Account/Cell/AddAccountTableViewCell.swift index c598ea96f..15c08be9f 100644 --- a/Mastodon/Scene/Account/Cell/AddAccountTableViewCell.swift +++ b/Mastodon/Scene/Account/Cell/AddAccountTableViewCell.swift @@ -1,108 +1,28 @@ -// -// AddAccountTableViewCell.swift -// Mastodon -// -// Created by Cirno MainasuK on 2021-9-14. -// +// Copyright © 2023 Mastodon gGmbH. All rights reserved. import UIKit -import Combine -import MetaTextKit import MastodonAsset import MastodonLocalization -import MastodonCore -import MastodonUI final class AddAccountTableViewCell: UITableViewCell { - - private var _disposeBag = Set() - - let iconImageView: UIImageView = { - let image = UIImage(systemName: "plus.circle.fill")! - let imageView = UIImageView(image: image) - imageView.tintColor = Asset.Colors.Label.primary.color - return imageView - }() - let titleLabel: UILabel = { - let label = UILabel() - label.font = UIFontMetrics(forTextStyle: .headline).scaledFont(for: .systemFont(ofSize: 17, weight: .regular), maximumPointSize: 22) - label.textColor = Asset.Colors.Label.primary.color - label.text = L10n.Scene.AccountList.addAccount - return label - }() - let usernameLabel = MetaLabel(style: .accountListUsername) - let separatorLine = UIView.separatorLine override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { super.init(style: style, reuseIdentifier: reuseIdentifier) - _init() - } - required init?(coder: NSCoder) { - super.init(coder: coder) - _init() - } + var configuration = defaultContentConfiguration() + configuration.image = UIImage(systemName: "plus") + configuration.imageProperties.tintColor = Asset.Colors.Label.primary.color -} - -extension AddAccountTableViewCell { - - private func _init() { + configuration.text = L10n.Scene.AccountList.addAccount + configuration.textProperties.color = Asset.Colors.Label.primary.color + configuration.textProperties.font = UIFontMetrics(forTextStyle: .headline).scaledFont(for: .systemFont(ofSize: 17, weight: .regular), maximumPointSize: 22) + configuration.directionalLayoutMargins = NSDirectionalEdgeInsets(top: 16, leading: 0, bottom: 16, trailing: 0) backgroundColor = .secondarySystemGroupedBackground - - iconImageView.translatesAutoresizingMaskIntoConstraints = false - contentView.addSubview(iconImageView) - NSLayoutConstraint.activate([ - iconImageView.leadingAnchor.constraint(equalTo: contentView.readableContentGuide.leadingAnchor), - iconImageView.centerYAnchor.constraint(equalTo: contentView.centerYAnchor), - iconImageView.heightAnchor.constraint(equalTo: iconImageView.widthAnchor, multiplier: 1.0).priority(.required - 1), - iconImageView.heightAnchor.constraint(greaterThanOrEqualToConstant: 30).priority(.required - 1), - ]) - iconImageView.setContentHuggingPriority(.defaultLow, for: .horizontal) - iconImageView.setContentHuggingPriority(.defaultLow, for: .vertical) - - // layout the same placeholder UI from `AccountListTableViewCell` - let placeholderLabelContainerStackView = UIStackView() - placeholderLabelContainerStackView.axis = .vertical - placeholderLabelContainerStackView.distribution = .equalCentering - placeholderLabelContainerStackView.spacing = 2 - placeholderLabelContainerStackView.distribution = .fillProportionally - placeholderLabelContainerStackView.translatesAutoresizingMaskIntoConstraints = false - contentView.addSubview(placeholderLabelContainerStackView) - NSLayoutConstraint.activate([ - placeholderLabelContainerStackView.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 8), - placeholderLabelContainerStackView.leadingAnchor.constraint(equalTo: iconImageView.trailingAnchor, constant: 10), - contentView.bottomAnchor.constraint(equalTo: placeholderLabelContainerStackView.bottomAnchor, constant: 10), - iconImageView.heightAnchor.constraint(equalTo: placeholderLabelContainerStackView.heightAnchor, multiplier: 0.8).priority(.required - 10), - ]) - let _nameLabel = MetaLabel(style: .accountListName) - _nameLabel.configure(content: PlaintextMetaContent(string: " ")) - let _usernameLabel = MetaLabel(style: .accountListUsername) - _usernameLabel.configure(content: PlaintextMetaContent(string: " ")) - placeholderLabelContainerStackView.addArrangedSubview(_nameLabel) - placeholderLabelContainerStackView.addArrangedSubview(_usernameLabel) - placeholderLabelContainerStackView.isHidden = true - - titleLabel.translatesAutoresizingMaskIntoConstraints = false - contentView.addSubview(titleLabel) - NSLayoutConstraint.activate([ - titleLabel.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 15), - titleLabel.leadingAnchor.constraint(equalTo: iconImageView.trailingAnchor, constant: 10), - contentView.bottomAnchor.constraint(equalTo: titleLabel.bottomAnchor, constant: 15), - // iconImageView.heightAnchor.constraint(equalTo: titleLabel.heightAnchor, multiplier: 1.0).priority(.required - 10), - titleLabel.trailingAnchor.constraint(equalTo: contentView.trailingAnchor), - ]) - - separatorLine.translatesAutoresizingMaskIntoConstraints = false - contentView.addSubview(separatorLine) - NSLayoutConstraint.activate([ - separatorLine.leadingAnchor.constraint(equalTo: contentView.leadingAnchor), - separatorLine.trailingAnchor.constraint(equalTo: contentView.trailingAnchor), - separatorLine.bottomAnchor.constraint(equalTo: contentView.bottomAnchor), - separatorLine.heightAnchor.constraint(equalToConstant: UIView.separatorLineHeight(of: contentView)), - ]) + self.contentConfiguration = configuration accessibilityTraits.insert(.button) } + required init?(coder: NSCoder) { fatalError() } + } diff --git a/Mastodon/Scene/Account/Cell/LogoutOfAllAccountsCell.swift b/Mastodon/Scene/Account/Cell/LogoutOfAllAccountsCell.swift new file mode 100644 index 000000000..296e8196d --- /dev/null +++ b/Mastodon/Scene/Account/Cell/LogoutOfAllAccountsCell.swift @@ -0,0 +1,30 @@ +// Copyright © 2023 Mastodon gGmbH. All rights reserved. + +import UIKit +import MastodonLocalization + +final class LogoutOfAllAccountsCell: UITableViewCell { + + static let reuseIdentifier = "LogoutOfAllAccountsCell" + + override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { + super.init(style: style, reuseIdentifier: reuseIdentifier) + + var configuration = defaultContentConfiguration() + configuration.image = UIImage(systemName: "rectangle.portrait.and.arrow.forward") + configuration.imageProperties.tintColor = .systemRed + + configuration.text = L10n.Scene.AccountList.logoutAllAccounts + configuration.textProperties.color = .systemRed + configuration.textProperties.font = UIFontMetrics(forTextStyle: .headline).scaledFont(for: .systemFont(ofSize: 17, weight: .regular), maximumPointSize: 22) + configuration.directionalLayoutMargins = NSDirectionalEdgeInsets(top: 16, leading: 0, bottom: 16, trailing: 0) + backgroundColor = .secondarySystemGroupedBackground + + self.contentConfiguration = configuration + accessibilityTraits.insert(.button) + + } + + required init?(coder: NSCoder) { fatalError() } +} + diff --git a/Mastodon/Scene/Root/MainTab/MainTabBarController.swift b/Mastodon/Scene/Root/MainTab/MainTabBarController.swift index f83c60608..627f59b11 100644 --- a/Mastodon/Scene/Root/MainTab/MainTabBarController.swift +++ b/Mastodon/Scene/Root/MainTab/MainTabBarController.swift @@ -297,7 +297,7 @@ extension MainTabBarController { case .me: guard let authContext = self.authContext else { return } let accountListViewModel = AccountListViewModel(context: context, authContext: authContext) - _ = coordinator.present(scene: .accountList(viewModel: accountListViewModel), from: self, transition: .panModal) + _ = coordinator.present(scene: .accountList(viewModel: accountListViewModel), from: self, transition: .formSheet) default: break } diff --git a/MastodonSDK/Package.swift b/MastodonSDK/Package.swift index 00773dfe9..9f743eff2 100644 --- a/MastodonSDK/Package.swift +++ b/MastodonSDK/Package.swift @@ -44,7 +44,6 @@ let package = Package( .package(url: "https://github.com/Flipboard/FLAnimatedImage.git", from: "1.0.0"), .package(url: "https://github.com/kean/Nuke.git", from: "10.3.1"), .package(url: "https://github.com/kishikawakatsumi/KeychainAccess.git", from: "4.2.2"), - .package(url: "https://github.com/slackhq/PanModal.git", from: "1.2.7"), .package(url: "https://github.com/TimOliver/TOCropViewController.git", from: "2.6.1"), .package(url: "https://github.com/TwidereProject/MetaTextKit.git", exact: "2.2.5"), .package(url: "https://github.com/TwidereProject/TabBarPager.git", from: "0.1.0"), @@ -124,7 +123,6 @@ let package = Package( .product(name: "Tabman", package: "Tabman"), .product(name: "MetaTextKit", package: "MetaTextKit"), .product(name: "CropViewController", package: "TOCropViewController"), - .product(name: "PanModal", package: "PanModal"), .product(name: "Stripes", package: "Stripes"), .product(name: "NextLevelSessionExporter", package: "NextLevelSessionExporter"), .product(name: "SDWebImage", package: "SDWebImage"), diff --git a/MastodonSDK/Sources/MastodonCore/MastodonAuthentication.swift b/MastodonSDK/Sources/MastodonCore/MastodonAuthentication.swift index 2ed7542df..f8c4410b7 100644 --- a/MastodonSDK/Sources/MastodonCore/MastodonAuthentication.swift +++ b/MastodonSDK/Sources/MastodonCore/MastodonAuthentication.swift @@ -4,7 +4,7 @@ import Foundation import CoreDataStack import MastodonSDK -public struct MastodonAuthentication: Codable, Hashable { +public struct MastodonAuthentication: Codable, Hashable, UserIdentifier { public typealias ID = UUID public private(set) var identifier: ID @@ -23,7 +23,7 @@ public struct MastodonAuthentication: Codable, Hashable { public private(set) var userID: String public private(set) var instanceObjectIdURI: URL? - internal var persistenceIdentifier: String { + public var persistenceIdentifier: String { "\(username)@\(domain)" } @@ -120,4 +120,8 @@ public struct MastodonAuthentication: Codable, Hashable { func updating(activatedAt: Date) -> Self { copy(activedAt: activatedAt) } + + var authorization: Mastodon.API.OAuth.Authorization { + .init(accessToken: userAccessToken) + } } diff --git a/MastodonSDK/Sources/MastodonCore/Service/AuthenticationService.swift b/MastodonSDK/Sources/MastodonCore/Service/AuthenticationService.swift index dde021705..34ecf54b2 100644 --- a/MastodonSDK/Sources/MastodonCore/Service/AuthenticationService.swift +++ b/MastodonSDK/Sources/MastodonCore/Service/AuthenticationService.swift @@ -135,24 +135,13 @@ extension AuthenticationService { return isActive } - + + public func signOutMastodonUser(authentication: MastodonAuthentication) async throws { + try AuthenticationServiceProvider.shared.delete(authentication: authentication) + _ = try await apiService?.cancelSubscription(domain: authentication.domain, authorization: authentication.authorization) + } + public func signOutMastodonUser(authenticationBox: MastodonAuthenticationBox) async throws { - let managedObjectContext = backgroundManagedObjectContext - try await managedObjectContext.performChanges { - // remove Feed - let request = Feed.sortedFetchRequest - request.predicate = Feed.predicate( - acct: .mastodon( - domain: authenticationBox.domain, - userID: authenticationBox.userID - ) - ) - let feeds = managedObjectContext.safeFetch(request) - for feed in feeds { - managedObjectContext.delete(feed) - } - } - do { try AuthenticationServiceProvider.shared.delete(authentication: authenticationBox.authentication) } catch { diff --git a/MastodonSDK/Sources/MastodonLocalization/Generated/Strings.swift b/MastodonSDK/Sources/MastodonLocalization/Generated/Strings.swift index fcd63dc9f..877174a02 100644 --- a/MastodonSDK/Sources/MastodonLocalization/Generated/Strings.swift +++ b/MastodonSDK/Sources/MastodonLocalization/Generated/Strings.swift @@ -572,6 +572,10 @@ public enum L10n { public static let addAccount = L10n.tr("Localizable", "Scene.AccountList.AddAccount", fallback: "Add Account") /// Dismiss Account Switcher public static let dismissAccountSwitcher = L10n.tr("Localizable", "Scene.AccountList.DismissAccountSwitcher", fallback: "Dismiss Account Switcher") + /// Logout + public static let logout = L10n.tr("Localizable", "Scene.AccountList.Logout", fallback: "Logout") + /// Log Out Of All Accounts + public static let logoutAllAccounts = L10n.tr("Localizable", "Scene.AccountList.LogoutAllAccounts", fallback: "Log Out Of All Accounts") /// Current selected profile: %@. Double tap then hold to show account switcher public static func tabBarHint(_ p1: Any) -> String { return L10n.tr("Localizable", "Scene.AccountList.TabBarHint", String(describing: p1), fallback: "Current selected profile: %@. Double tap then hold to show account switcher") diff --git a/MastodonSDK/Sources/MastodonLocalization/Resources/Base.lproj/Localizable.strings b/MastodonSDK/Sources/MastodonLocalization/Resources/Base.lproj/Localizable.strings index 9c6b106d2..cbc90e3fd 100644 --- a/MastodonSDK/Sources/MastodonLocalization/Resources/Base.lproj/Localizable.strings +++ b/MastodonSDK/Sources/MastodonLocalization/Resources/Base.lproj/Localizable.strings @@ -202,6 +202,8 @@ Your profile looks like this to them."; "Extension.OpenIn.InvalidLinkError" = "This doesn't seem to be a valid Mastodon link."; "Scene.AccountList.AddAccount" = "Add Account"; "Scene.AccountList.DismissAccountSwitcher" = "Dismiss Account Switcher"; +"Scene.AccountList.Logout" = "Logout"; +"Scene.AccountList.LogoutAllAccounts" = "Log Out Of All Accounts"; "Scene.AccountList.TabBarHint" = "Current selected profile: %@. Double tap then hold to show account switcher"; "Scene.Bookmark.Title" = "Bookmarks"; "Scene.Compose.Accessibility.AppendAttachment" = "Add Attachment";