mirror of
https://github.com/mastodon/mastodon-ios.git
synced 2025-02-02 18:36:44 +01:00
Improve Multi-User Account Sheet (IOS-245) (#1273)
* Copynpaste cell to logout all accounts (IOS-245) * Show cell (IOS-245) * Logout of all accounts (IOS-245) * Use iOS-formsheet to present account-list (IOS-245) * Remove dead code (IOS-245) * Don't animate account-switches (IOS-245) * Remove panModal (IOS-245) * UI-fixes (IOS-245) * Add swipe-to-logout-action (IOS-245) * Localize (IOS-245) * Add a little bit of margin (IOS-245) * Fix separator-insets (IOS-245) * Don't crash on iPad when logging out of all accounts (IOS-245)
This commit is contained in:
parent
ddb3211641
commit
c2d0701062
@ -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)
|
||||
|
@ -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"
|
||||
|
@ -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"
|
||||
|
@ -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 = "<group>"; };
|
||||
D8E5C345296DAB84007E76A7 /* DataSourceFacade+Status+History.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "DataSourceFacade+Status+History.swift"; sourceTree = "<group>"; };
|
||||
D8E5C348296DB8A3007E76A7 /* StatusEditHistoryViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusEditHistoryViewController.swift; sourceTree = "<group>"; };
|
||||
D8E64F402BA84F80003A4539 /* LogoutOfAllAccountsCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LogoutOfAllAccountsCell.swift; sourceTree = "<group>"; };
|
||||
D8ECC80F2AC31EA400AE0818 /* NotificationSettingsDisabledTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationSettingsDisabledTableViewCell.swift; sourceTree = "<group>"; };
|
||||
D8F0372B29D232730027DE2E /* HashtagIntentHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HashtagIntentHandler.swift; sourceTree = "<group>"; };
|
||||
D8F8A03929CA5C15000195DD /* HashtagWidgetView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HashtagWidgetView.swift; sourceTree = "<group>"; };
|
||||
@ -2709,6 +2711,7 @@
|
||||
children = (
|
||||
DB9F58F026EF512300E7BBE9 /* AccountListTableViewCell.swift */,
|
||||
DBA5A53426F0A36A00CACBAA /* AddAccountTableViewCell.swift */,
|
||||
D8E64F402BA84F80003A4539 /* LogoutOfAllAccountsCell.swift */,
|
||||
);
|
||||
path = Cell;
|
||||
sourceTree = "<group>";
|
||||
@ -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 */,
|
||||
|
@ -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
|
||||
|
@ -25,7 +25,6 @@ final class AccountListViewModel: NSObject {
|
||||
// output
|
||||
@Published var items: [Item] = []
|
||||
|
||||
let dataSourceDidUpdate = PassthroughSubject<Void, Never>()
|
||||
var diffableDataSource: UITableViewDiffableDataSource<Section, Item>!
|
||||
|
||||
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
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
|
||||
|
@ -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<AnyCancellable>()
|
||||
|
||||
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() }
|
||||
|
||||
}
|
||||
|
30
Mastodon/Scene/Account/Cell/LogoutOfAllAccountsCell.swift
Normal file
30
Mastodon/Scene/Account/Cell/LogoutOfAllAccountsCell.swift
Normal file
@ -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() }
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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"),
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
@ -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 {
|
||||
|
@ -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")
|
||||
|
@ -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";
|
||||
|
Loading…
x
Reference in New Issue
Block a user