Compare commits
22 Commits
public-bet
...
develop
Author | SHA1 | Date |
---|---|---|
Shadowfacts | afe47437e4 | |
Shadowfacts | 4dc484c3c2 | |
Shadowfacts | 0f2a85b108 | |
Shadowfacts | 5e55ce75c2 | |
Shadowfacts | eec2adbfd9 | |
Shadowfacts | a848f6e425 | |
Shadowfacts | 44896d305e | |
Shadowfacts | 6c70ed4b4e | |
Shadowfacts | e3c480131a | |
Shadowfacts | 575166f5b4 | |
Shadowfacts | c60aa3e3f3 | |
Shadowfacts | 75f0d12c82 | |
Shadowfacts | 5cf2bc4fbf | |
Shadowfacts | 908b499f8f | |
Shadowfacts | 67c7905acf | |
Shadowfacts | eacafe87b3 | |
Shadowfacts | 2a53b24487 | |
Shadowfacts | 06ba758309 | |
Shadowfacts | 2c56902389 | |
Shadowfacts | 5620b6ab78 | |
Shadowfacts | 3d0de5af04 | |
Shadowfacts | 966a906436 |
|
@ -63,6 +63,7 @@ class NotificationService: UNNotificationServiceExtension {
|
|||
mutableContent.body = notification.body
|
||||
mutableContent.userInfo["notificationID"] = notification.notificationID
|
||||
mutableContent.userInfo["accountID"] = accountID
|
||||
mutableContent.targetContentIdentifier = accountID
|
||||
|
||||
let task = Task {
|
||||
await updateNotificationContent(mutableContent, account: account, push: notification)
|
||||
|
|
|
@ -52,15 +52,22 @@ class GalleryDismissAnimationController: NSObject, UIViewControllerAnimatedTrans
|
|||
appliedSourceToDestTransform = false
|
||||
}
|
||||
|
||||
to.view.frame = container.bounds
|
||||
// Moving `to.view` to the container is necessary when the presenting VC (i.e., `to`)
|
||||
// is in the window's root presentation.
|
||||
// But it breaks when the gallery is presented from a sheet-presented VC--in which case
|
||||
// `to.view` is already in the view hierarchy at this point; and adding it to the
|
||||
// container causees it to be removed when the transition completes.
|
||||
if to.view.superview == nil {
|
||||
to.view.frame = container.bounds
|
||||
container.addSubview(to.view)
|
||||
}
|
||||
|
||||
from.view.frame = container.bounds
|
||||
container.addSubview(from.view)
|
||||
|
||||
let content = itemViewController.takeContent()
|
||||
content.view.translatesAutoresizingMaskIntoConstraints = true
|
||||
content.view.layer.masksToBounds = true
|
||||
|
||||
container.addSubview(to.view)
|
||||
container.addSubview(from.view)
|
||||
container.addSubview(content.view)
|
||||
|
||||
content.view.frame = destFrameInContainer
|
||||
|
|
|
@ -217,6 +217,18 @@ public final class InstanceFeatures: ObservableObject {
|
|||
instanceType.isPleroma
|
||||
}
|
||||
|
||||
public var muteNotifications: Bool {
|
||||
!instanceType.isPixelfed
|
||||
}
|
||||
|
||||
public var blockDomains: Bool {
|
||||
!instanceType.isPixelfed
|
||||
}
|
||||
|
||||
public var hideReblogs: Bool {
|
||||
!instanceType.isPixelfed
|
||||
}
|
||||
|
||||
public init() {
|
||||
}
|
||||
|
||||
|
@ -338,6 +350,14 @@ extension InstanceFeatures {
|
|||
return false
|
||||
}
|
||||
}
|
||||
|
||||
var isPixelfed: Bool {
|
||||
if case .pixelfed = self {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@_spi(InstanceType) public enum MastodonType {
|
||||
|
|
|
@ -40,8 +40,9 @@ public final class Account: AccountProtocol, Decodable, Sendable {
|
|||
self.displayName = try container.decode(String.self, forKey: .displayName)
|
||||
self.locked = try container.decode(Bool.self, forKey: .locked)
|
||||
self.createdAt = try container.decode(Date.self, forKey: .createdAt)
|
||||
self.followersCount = try container.decode(Int.self, forKey: .followersCount)
|
||||
self.followingCount = try container.decode(Int.self, forKey: .followingCount)
|
||||
// some instance types (pixelfed, firefish) seem to sometimes send null for these fields, so just fallback to 0
|
||||
self.followersCount = try container.decodeIfPresent(Int.self, forKey: .followersCount) ?? 0
|
||||
self.followingCount = try container.decodeIfPresent(Int.self, forKey: .followingCount) ?? 0
|
||||
self.statusesCount = try container.decode(Int.self, forKey: .statusesCount)
|
||||
self.note = try container.decode(String.self, forKey: .note)
|
||||
self.url = try container.decode(URL.self, forKey: .url)
|
||||
|
|
|
@ -27,10 +27,13 @@ public struct Relationship: RelationshipProtocol, Decodable, Sendable {
|
|||
self.followedBy = try container.decode(Bool.self, forKey: .followedBy)
|
||||
self.blocking = try container.decode(Bool.self, forKey: .blocking)
|
||||
self.muting = try container.decode(Bool.self, forKey: .muting)
|
||||
self.mutingNotifications = try container.decode(Bool.self, forKey: .mutingNotifications)
|
||||
// not supported on pixelfed
|
||||
self.mutingNotifications = try container.decodeIfPresent(Bool.self, forKey: .mutingNotifications) ?? false
|
||||
self.followRequested = try container.decode(Bool.self, forKey: .followRequested)
|
||||
self.domainBlocking = try container.decode(Bool.self, forKey: .domainBlocking)
|
||||
self.showingReblogs = try container.decode(Bool.self, forKey: .showingReblogs)
|
||||
// not supported on pixelfed
|
||||
self.domainBlocking = try container.decodeIfPresent(Bool.self, forKey: .domainBlocking) ?? false
|
||||
// not supported on pixelfed
|
||||
self.showingReblogs = try container.decodeIfPresent(Bool.self, forKey: .showingReblogs) ?? true
|
||||
self.endorsed = try container.decodeIfPresent(Bool.self, forKey: .endorsed) ?? false
|
||||
}
|
||||
|
||||
|
|
|
@ -75,6 +75,7 @@
|
|||
D620483423D3801D008A63EF /* LinkTextView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D620483323D3801D008A63EF /* LinkTextView.swift */; };
|
||||
D620483623D38075008A63EF /* ContentTextView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D620483523D38075008A63EF /* ContentTextView.swift */; };
|
||||
D620483823D38190008A63EF /* StatusContentTextView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D620483723D38190008A63EF /* StatusContentTextView.swift */; };
|
||||
D6210D762C0B924F009BB569 /* RemoveProfileSuggestionService.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6210D752C0B924F009BB569 /* RemoveProfileSuggestionService.swift */; };
|
||||
D621733328F1D5ED004C7DB1 /* ReblogService.swift in Sources */ = {isa = PBXBuildFile; fileRef = D621733228F1D5ED004C7DB1 /* ReblogService.swift */; };
|
||||
D62275A824F1CA2800B82A16 /* ComposeReplyContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D62275A724F1CA2800B82A16 /* ComposeReplyContentView.swift */; };
|
||||
D623A53D2635F5590095BD04 /* StatusPollView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D623A53C2635F5590095BD04 /* StatusPollView.swift */; };
|
||||
|
@ -506,6 +507,7 @@
|
|||
D620483323D3801D008A63EF /* LinkTextView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LinkTextView.swift; sourceTree = "<group>"; };
|
||||
D620483523D38075008A63EF /* ContentTextView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentTextView.swift; sourceTree = "<group>"; };
|
||||
D620483723D38190008A63EF /* StatusContentTextView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusContentTextView.swift; sourceTree = "<group>"; };
|
||||
D6210D752C0B924F009BB569 /* RemoveProfileSuggestionService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RemoveProfileSuggestionService.swift; sourceTree = "<group>"; };
|
||||
D621733228F1D5ED004C7DB1 /* ReblogService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReblogService.swift; sourceTree = "<group>"; };
|
||||
D62275A724F1CA2800B82A16 /* ComposeReplyContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeReplyContentView.swift; sourceTree = "<group>"; };
|
||||
D623A53C2635F5590095BD04 /* StatusPollView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusPollView.swift; sourceTree = "<group>"; };
|
||||
|
@ -1757,6 +1759,7 @@
|
|||
D6DD8FFC298495A8002AD3FD /* LogoutService.swift */,
|
||||
D6D79F252A0C8D2700AB2315 /* FetchStatusSourceService.swift */,
|
||||
D630C3CB2BC5FD4600208903 /* GetAuthorizationTokenService.swift */,
|
||||
D6210D752C0B924F009BB569 /* RemoveProfileSuggestionService.swift */,
|
||||
);
|
||||
path = API;
|
||||
sourceTree = "<group>";
|
||||
|
@ -2362,6 +2365,7 @@
|
|||
D698F46D2BD0B8310054DB14 /* AnnouncementsCollection.swift in Sources */,
|
||||
D61F75AF293AF50C00C0B37F /* EditedFilter.swift in Sources */,
|
||||
D691771929A7B8820054D7EF /* ProfileHeaderMovedOverlayView.swift in Sources */,
|
||||
D6210D762C0B924F009BB569 /* RemoveProfileSuggestionService.swift in Sources */,
|
||||
D61F75B9293C15A000C0B37F /* ZeroHeightCollectionViewCell.swift in Sources */,
|
||||
D68232F72464F4FD00325FB8 /* ComposeDrawingViewController.swift in Sources */,
|
||||
04586B4122B2FFB10021BD04 /* PreferencesView.swift in Sources */,
|
||||
|
|
|
@ -0,0 +1,39 @@
|
|||
//
|
||||
// RemoveProfileSuggestionService.swift
|
||||
// Tusker
|
||||
//
|
||||
// Created by Shadowfacts on 6/1/24.
|
||||
// Copyright © 2024 Shadowfacts. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import Pachyderm
|
||||
|
||||
@MainActor
|
||||
class RemoveProfileSuggestionService {
|
||||
private let accountID: String
|
||||
private let mastodonController: MastodonController
|
||||
private let presenter: any TuskerNavigationDelegate
|
||||
private let completionHandler: @MainActor () -> Void
|
||||
|
||||
init(accountID: String, mastodonController: MastodonController, presenter: any TuskerNavigationDelegate, completionHandler: @MainActor @escaping () -> Void) {
|
||||
self.accountID = accountID
|
||||
self.mastodonController = mastodonController
|
||||
self.presenter = presenter
|
||||
self.completionHandler = completionHandler
|
||||
}
|
||||
|
||||
func run() async {
|
||||
let req = Suggestion.remove(accountID: accountID)
|
||||
do {
|
||||
_ = try await mastodonController.run(req)
|
||||
completionHandler()
|
||||
} catch {
|
||||
let config = ToastConfiguration(from: error, with: "Error Removing Suggestion", in: presenter) { toast in
|
||||
toast.dismissToast(animated: true)
|
||||
await self.run()
|
||||
}
|
||||
self.presenter.showToast(configuration: config, animated: true)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -290,12 +290,18 @@ extension AppDelegate: UNUserNotificationCenterDelegate {
|
|||
// if the scene is already active, then we animate the account switching if necessary
|
||||
delegate.activateAccount(account, animated: scene.activationState == .foregroundActive)
|
||||
|
||||
rootViewController.select(route: .notifications, animated: false)
|
||||
let vc = NotificationLoadingViewController(notificationID: notificationID, mastodonController: mastodonController)
|
||||
rootViewController.getNavigationController().pushViewController(vc, animated: false)
|
||||
rootViewController.select(route: .notifications, animated: false) {
|
||||
let vc = NotificationLoadingViewController(notificationID: notificationID, mastodonController: mastodonController)
|
||||
rootViewController.getNavigationController().pushViewController(vc, animated: false)
|
||||
}
|
||||
} else {
|
||||
let activity = UserActivityManager.showNotificationActivity(id: notificationID, accountID: accountID)
|
||||
UIApplication.shared.requestSceneSessionActivation(nil, userActivity: activity, options: nil)
|
||||
if #available(iOS 17.0, *) {
|
||||
let request = UISceneSessionActivationRequest(userActivity: activity)
|
||||
UIApplication.shared.activateSceneSession(for: request)
|
||||
} else {
|
||||
UIApplication.shared.requestSceneSessionActivation(nil, userActivity: activity, options: nil)
|
||||
}
|
||||
}
|
||||
completionHandler()
|
||||
}
|
||||
|
|
|
@ -32,8 +32,9 @@ class AuxiliarySceneDelegate: UIResponder, UIWindowSceneDelegate, TuskerSceneDel
|
|||
}
|
||||
launchActivity = activity
|
||||
|
||||
scene.activationConditions.canActivateForTargetContentIdentifierPredicate = NSPredicate(value: false)
|
||||
|
||||
let account: UserAccountInfo
|
||||
|
||||
if let activityAccount = UserActivityManager.getAccount(from: activity) {
|
||||
account = activityAccount
|
||||
} else if let mostRecent = UserAccountsManager.shared.getMostRecentAccount() {
|
||||
|
|
|
@ -29,6 +29,8 @@ class ComposeSceneDelegate: UIResponder, UIWindowSceneDelegate, TuskerSceneDeleg
|
|||
return
|
||||
}
|
||||
|
||||
scene.activationConditions.canActivateForTargetContentIdentifierPredicate = NSPredicate(value: false)
|
||||
|
||||
let account: UserAccountInfo
|
||||
let controller: MastodonController
|
||||
let draft: Draft?
|
||||
|
|
|
@ -83,7 +83,9 @@ class MainSceneDelegate: UIResponder, UIWindowSceneDelegate, TuskerSceneDelegate
|
|||
} else {
|
||||
context = ActiveAccountUserActivityHandlingContext(isHandoff: true, root: rootViewController!)
|
||||
}
|
||||
_ = userActivity.handleResume(manager: UserActivityManager(scene: scene as! UIWindowScene, context: context))
|
||||
Task(priority: .userInitiated) {
|
||||
_ = await userActivity.handleResume(manager: UserActivityManager(scene: scene as! UIWindowScene, context: context))
|
||||
}
|
||||
}
|
||||
|
||||
func windowScene(_ windowScene: UIWindowScene, performActionFor shortcutItem: UIApplicationShortcutItem, completionHandler: @escaping (Bool) -> Void) {
|
||||
|
@ -191,8 +193,10 @@ class MainSceneDelegate: UIResponder, UIWindowSceneDelegate, TuskerSceneDelegate
|
|||
|
||||
if let activity = launchActivity {
|
||||
func doRestoreActivity(context: UserActivityHandlingContext) {
|
||||
_ = activity.handleResume(manager: UserActivityManager(scene: window!.windowScene!, context: context))
|
||||
context.finalize(activity: activity)
|
||||
Task(priority: .userInitiated) {
|
||||
_ = await activity.handleResume(manager: UserActivityManager(scene: window!.windowScene!, context: context))
|
||||
context.finalize(activity: activity)
|
||||
}
|
||||
}
|
||||
if activity.isStateRestorationActivity {
|
||||
doRestoreActivity(context: StateRestorationUserActivityHandlingContext(root: rootViewController!))
|
||||
|
@ -225,7 +229,8 @@ class MainSceneDelegate: UIResponder, UIWindowSceneDelegate, TuskerSceneDelegate
|
|||
window!.windowScene!.title = account.instanceURL.host!
|
||||
}
|
||||
|
||||
let newRoot = createAppUI()
|
||||
window!.windowScene!.activationConditions.prefersToActivateForTargetContentIdentifierPredicate = NSPredicate(format: "self == %@", account.id)
|
||||
|
||||
if let container = window?.rootViewController as? AccountSwitchingContainerViewController {
|
||||
let direction: AccountSwitchingContainerViewController.AnimationDirection
|
||||
if animated,
|
||||
|
@ -235,9 +240,9 @@ class MainSceneDelegate: UIResponder, UIWindowSceneDelegate, TuskerSceneDelegate
|
|||
} else {
|
||||
direction = .none
|
||||
}
|
||||
container.setRoot(newRoot, for: account, animating: direction)
|
||||
container.setRoot(createAppUI, for: account, animating: direction)
|
||||
} else {
|
||||
window!.rootViewController = AccountSwitchingContainerViewController(root: newRoot, for: account)
|
||||
window!.rootViewController = AccountSwitchingContainerViewController(root: createAppUI(), for: account)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -248,6 +253,9 @@ class MainSceneDelegate: UIResponder, UIWindowSceneDelegate, TuskerSceneDelegate
|
|||
LogoutService(accountInfo: account).run()
|
||||
if UserAccountsManager.shared.onboardingComplete {
|
||||
activateAccount(UserAccountsManager.shared.accounts.first!, animated: false)
|
||||
if let container = window?.rootViewController as? AccountSwitchingContainerViewController {
|
||||
container.removeAccount(account)
|
||||
}
|
||||
} else {
|
||||
window!.rootViewController = createOnboardingUI()
|
||||
}
|
||||
|
|
|
@ -184,7 +184,19 @@ extension SuggestedProfilesViewController: UICollectionViewDelegate {
|
|||
return UIContextMenuConfiguration {
|
||||
ProfileViewController(accountID: id, mastodonController: self.mastodonController)
|
||||
} actionProvider: { _ in
|
||||
UIMenu(children: self.actionsForProfile(accountID: id, source: .view(cell)))
|
||||
let dismiss = UIAction(title: "Remove Suggestion", image: UIImage(systemName: "trash"), attributes: .destructive) { [unowned self] _ in
|
||||
let service = RemoveProfileSuggestionService(accountID: id, mastodonController: self.mastodonController, presenter: self) { [weak self] in
|
||||
guard let self else { return }
|
||||
var snapshot = self.dataSource.snapshot()
|
||||
// the source here doesn't matter, since it's ignored by the equatable and hashable impls
|
||||
snapshot.deleteItems([.account(id, .global)])
|
||||
self.dataSource.apply(snapshot, animatingDifferences: true)
|
||||
}
|
||||
Task {
|
||||
await service.run()
|
||||
}
|
||||
}
|
||||
return UIMenu(children: [UIMenu(options: .displayInline, children: [dismiss])] + self.actionsForProfile(accountID: id, source: .view(cell)))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -368,20 +368,14 @@ class TrendsViewController: UIViewController, CollectionViewController {
|
|||
|
||||
@MainActor
|
||||
private func removeProfileSuggestion(accountID: String) async {
|
||||
let req = Suggestion.remove(accountID: accountID)
|
||||
do {
|
||||
_ = try await mastodonController.run(req)
|
||||
var snapshot = dataSource.snapshot()
|
||||
let service = RemoveProfileSuggestionService(accountID: accountID, mastodonController: mastodonController, presenter: self) { [weak self] in
|
||||
guard let self else { return }
|
||||
var snapshot = self.dataSource.snapshot()
|
||||
// the source here doesn't matter, since it's ignored by the equatable and hashable impls
|
||||
snapshot.deleteItems([.account(accountID, .global)])
|
||||
await apply(snapshot: snapshot)
|
||||
} catch {
|
||||
let config = ToastConfiguration(from: error, with: "Error Removing Suggestion", in: self) { [unowned self] toast in
|
||||
toast.dismissToast(animated: true)
|
||||
_ = await self.removeProfileSuggestion(accountID: accountID)
|
||||
}
|
||||
self.showToast(configuration: config, animated: true)
|
||||
self.dataSource.apply(snapshot, animatingDifferences: true)
|
||||
}
|
||||
await service.run()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -23,7 +23,7 @@ class AccountSwitchingContainerViewController: UIViewController {
|
|||
private(set) var currentAccountID: String
|
||||
private(set) var root: AccountSwitchableViewController
|
||||
|
||||
private var userActivities: [String: NSUserActivity] = [:]
|
||||
private var viewControllers: [String: (AccountSwitchableViewController?, NSUserActivity)] = [:]
|
||||
|
||||
init(root: AccountSwitchableViewController, for account: UserAccountInfo) {
|
||||
self.currentAccountID = account.id
|
||||
|
@ -42,27 +42,49 @@ class AccountSwitchingContainerViewController: UIViewController {
|
|||
embedChild(root)
|
||||
}
|
||||
|
||||
func setRoot(_ newRoot: AccountSwitchableViewController, for account: UserAccountInfo, animating direction: AnimationDirection) {
|
||||
override func didReceiveMemoryWarning() {
|
||||
super.didReceiveMemoryWarning()
|
||||
viewControllers = viewControllers.mapValues { (_, activity) in
|
||||
(nil, activity)
|
||||
}
|
||||
}
|
||||
|
||||
func removeAccount(_ account: UserAccountInfo) {
|
||||
viewControllers.removeValue(forKey: account.id)
|
||||
}
|
||||
|
||||
func setRoot(_ newRootProvider: () -> AccountSwitchableViewController, for account: UserAccountInfo, animating direction: AnimationDirection) {
|
||||
let oldRoot = self.root
|
||||
if direction == .none {
|
||||
oldRoot.removeViewAndController()
|
||||
}
|
||||
if let activity = oldRoot.stateRestorationActivity() {
|
||||
stateRestorationLogger.debug("AccountSwitchingContainer: saving \(activity.activityType, privacy: .public) for \(self.currentAccountID, privacy: .public)")
|
||||
userActivities[currentAccountID] = activity
|
||||
viewControllers[currentAccountID] = (oldRoot, activity)
|
||||
}
|
||||
|
||||
let newRoot: AccountSwitchableViewController
|
||||
if let (existingRoot, activity) = viewControllers.removeValue(forKey: account.id) {
|
||||
if let existingRoot {
|
||||
newRoot = existingRoot
|
||||
stateRestorationLogger.debug("AccountSwitchingContainer: reusing existing VC for \(account.id, privacy: .public)")
|
||||
} else {
|
||||
newRoot = newRootProvider()
|
||||
Task(priority: .userInitiated) {
|
||||
stateRestorationLogger.debug("AccountSwitchingContainer: restoring \(activity.activityType, privacy: .public) for \(account.id, privacy: .public)")
|
||||
let context = StateRestorationUserActivityHandlingContext(root: newRoot)
|
||||
_ = await activity.handleResume(manager: UserActivityManager(scene: view.window!.windowScene!, context: context))
|
||||
context.finalize(activity: activity)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
newRoot = newRootProvider()
|
||||
}
|
||||
|
||||
self.currentAccountID = account.id
|
||||
self.root = newRoot
|
||||
embedChild(newRoot)
|
||||
|
||||
if let activity = userActivities.removeValue(forKey: account.id) {
|
||||
stateRestorationLogger.debug("AccountSwitchingContainer: restoring \(activity.activityType, privacy: .public) for \(account.id, privacy: .public)")
|
||||
let context = StateRestorationUserActivityHandlingContext(root: newRoot)
|
||||
_ = activity.handleResume(manager: UserActivityManager(scene: view.window!.windowScene!, context: context))
|
||||
context.finalize(activity: activity)
|
||||
}
|
||||
|
||||
if direction != .none {
|
||||
if UIAccessibility.prefersCrossFadeTransitions {
|
||||
newRoot.view.alpha = 0
|
||||
|
@ -92,6 +114,7 @@ class AccountSwitchingContainerViewController: UIViewController {
|
|||
#endif
|
||||
|
||||
// only one edge is affected in each direction, i have no idea why
|
||||
let origAdditionalSafeAreaInsets = oldRoot.additionalSafeAreaInsets
|
||||
if direction == .upwards {
|
||||
oldRoot.additionalSafeAreaInsets.bottom = view.safeAreaInsets.bottom
|
||||
} else {
|
||||
|
@ -102,6 +125,8 @@ class AccountSwitchingContainerViewController: UIViewController {
|
|||
oldRoot.view.transform = CGAffineTransform(translationX: 0, y: -newInitialOffset).scaledBy(x: scale, y: scale)
|
||||
newRoot.view.transform = .identity
|
||||
} completion: { (_) in
|
||||
oldRoot.view.transform = .identity
|
||||
oldRoot.additionalSafeAreaInsets = origAdditionalSafeAreaInsets
|
||||
oldRoot.removeViewAndController()
|
||||
newRoot.view.layer.masksToBounds = false
|
||||
}
|
||||
|
@ -127,9 +152,9 @@ extension AccountSwitchingContainerViewController: TuskerRootViewController {
|
|||
root.compose(editing: draft, animated: animated, isDucked: isDucked)
|
||||
}
|
||||
|
||||
func select(route: TuskerRoute, animated: Bool) {
|
||||
func select(route: TuskerRoute, animated: Bool, completion: (() -> Void)?) {
|
||||
loadViewIfNeeded()
|
||||
root.select(route: route, animated: animated)
|
||||
root.select(route: route, animated: animated, completion: completion)
|
||||
}
|
||||
|
||||
func getTabController(tab: MainTabBarViewController.Tab) -> UIViewController? {
|
||||
|
|
|
@ -35,8 +35,8 @@ extension DuckableContainerViewController: AccountSwitchableViewController {
|
|||
(child as! TuskerRootViewController).getNavigationController()
|
||||
}
|
||||
|
||||
func select(route: TuskerRoute, animated: Bool) {
|
||||
(child as? TuskerRootViewController)?.select(route: route, animated: animated)
|
||||
func select(route: TuskerRoute, animated: Bool, completion: (() -> Void)?) {
|
||||
(child as? TuskerRootViewController)?.select(route: route, animated: animated, completion: completion)
|
||||
}
|
||||
|
||||
func getTabController(tab: MainTabBarViewController.Tab) -> UIViewController? {
|
||||
|
|
|
@ -13,8 +13,8 @@ import Combine
|
|||
@MainActor
|
||||
protocol MainSidebarViewControllerDelegate: AnyObject {
|
||||
func sidebarRequestPresentCompose(_ sidebarViewController: MainSidebarViewController)
|
||||
func sidebar(_ sidebarViewController: MainSidebarViewController, didSelectItem item: MainSidebarViewController.Item)
|
||||
func sidebar(_ sidebarViewController: MainSidebarViewController, showViewController viewController: UIViewController)
|
||||
func sidebar(_ sidebarViewController: MainSidebarViewController, didSelectItem item: MainSidebarViewController.Item, previousItem: MainSidebarViewController.Item?)
|
||||
func sidebar(_ sidebarViewController: MainSidebarViewController, showViewController viewController: UIViewController, previousItem: MainSidebarViewController.Item?)
|
||||
func sidebar(_ sidebarViewController: MainSidebarViewController, scrollToTopFor item: MainSidebarViewController.Item)
|
||||
}
|
||||
|
||||
|
@ -57,7 +57,7 @@ class MainSidebarViewController: UIViewController {
|
|||
return items
|
||||
}
|
||||
|
||||
private(set) var previouslySelectedItem: Item?
|
||||
private var previouslySelectedItem: Item?
|
||||
var selectedItem: Item? {
|
||||
guard let indexPath = collectionView?.indexPathsForSelectedItems?.first else {
|
||||
return nil
|
||||
|
@ -261,19 +261,21 @@ class MainSidebarViewController: UIViewController {
|
|||
}
|
||||
|
||||
private func returnToPreviousItem() {
|
||||
let item = previouslySelectedItem ?? .tab(.timelines)
|
||||
let oldItem = selectedItem
|
||||
let newItem = previouslySelectedItem ?? .tab(.timelines)
|
||||
previouslySelectedItem = nil
|
||||
select(item: item, animated: true)
|
||||
sidebarDelegate?.sidebar(self, didSelectItem: item)
|
||||
select(item: newItem, animated: true)
|
||||
sidebarDelegate?.sidebar(self, didSelectItem: newItem, previousItem: oldItem)
|
||||
}
|
||||
|
||||
private func showAddList() {
|
||||
let service = CreateListService(mastodonController: mastodonController, present: { self.present($0, animated: true
|
||||
) }) { list in
|
||||
let oldItem = self.selectedItem
|
||||
self.select(item: .list(list), animated: false)
|
||||
let list = ListTimelineViewController(for: list, mastodonController: self.mastodonController)
|
||||
list.presentEditOnAppear = true
|
||||
self.sidebarDelegate?.sidebar(self, showViewController: list)
|
||||
self.sidebarDelegate?.sidebar(self, showViewController: list, previousItem: oldItem)
|
||||
}
|
||||
service.run()
|
||||
}
|
||||
|
@ -471,7 +473,7 @@ extension MainSidebarViewController: UICollectionViewDelegate {
|
|||
fatalError("unreachable")
|
||||
}
|
||||
} else {
|
||||
sidebarDelegate?.sidebar(self, didSelectItem: item)
|
||||
sidebarDelegate?.sidebar(self, didSelectItem: item, previousItem: previouslySelectedItem)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -540,8 +542,9 @@ extension MainSidebarViewController: UICollectionViewDragDelegate {
|
|||
extension MainSidebarViewController: InstanceTimelineViewControllerDelegate {
|
||||
func didSaveInstance(url: URL) {
|
||||
dismiss(animated: true) {
|
||||
let oldItem = self.selectedItem
|
||||
self.select(item: .savedInstance(url), animated: true)
|
||||
self.sidebarDelegate?.sidebar(self, didSelectItem: .savedInstance(url))
|
||||
self.sidebarDelegate?.sidebar(self, didSelectItem: .savedInstance(url), previousItem: oldItem)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -86,7 +86,7 @@ class MainSplitViewController: UISplitViewController {
|
|||
// don't unnecesarily construct a content VC unless the we're in actually split mode
|
||||
// when we change from compact -> split for the first time, the VC will be transferred anyways
|
||||
if traitCollection.horizontalSizeClass != .compact {
|
||||
select(item: .tab(.timelines))
|
||||
doSelect(item: .tab(.timelines))
|
||||
}
|
||||
|
||||
if UIDevice.current.userInterfaceIdiom != .mac {
|
||||
|
@ -149,7 +149,15 @@ class MainSplitViewController: UISplitViewController {
|
|||
self.setViewController(newNav, for: .secondary)
|
||||
}
|
||||
|
||||
func select(item: MainSidebarViewController.Item) {
|
||||
private func select(newItem: MainSidebarViewController.Item, oldItem: MainSidebarViewController.Item?) {
|
||||
if let oldItem,
|
||||
newItem != oldItem {
|
||||
navigationStacks[oldItem] = secondaryNavController.viewControllers
|
||||
}
|
||||
doSelect(item: newItem)
|
||||
}
|
||||
|
||||
private func doSelect(item: MainSidebarViewController.Item) {
|
||||
secondaryNavController.viewControllers = getOrCreateNavigationStack(item: item)
|
||||
}
|
||||
|
||||
|
@ -181,27 +189,27 @@ class MainSplitViewController: UISplitViewController {
|
|||
|
||||
@objc func handleSidebarCommandTimelines() {
|
||||
sidebar.select(item: .tab(.timelines), animated: false)
|
||||
select(item: .tab(.timelines))
|
||||
select(newItem: .tab(.timelines), oldItem: sidebar.selectedItem)
|
||||
}
|
||||
|
||||
@objc func handleSidebarCommandNotifications() {
|
||||
sidebar.select(item: .tab(.notifications), animated: false)
|
||||
select(item: .tab(.notifications))
|
||||
select(newItem: .tab(.notifications), oldItem: sidebar.selectedItem)
|
||||
}
|
||||
|
||||
@objc func handleSidebarCommandExplore() {
|
||||
sidebar.select(item: .tab(.explore), animated: false)
|
||||
select(item: .tab(.explore))
|
||||
select(newItem: .tab(.explore), oldItem: sidebar.selectedItem)
|
||||
}
|
||||
|
||||
@objc func handleSidebarCommandBookmarks() {
|
||||
sidebar.select(item: .bookmarks, animated: false)
|
||||
select(item: .bookmarks)
|
||||
select(newItem: .bookmarks, oldItem: sidebar.selectedItem)
|
||||
}
|
||||
|
||||
@objc func handleSidebarCommandMyProfile() {
|
||||
sidebar.select(item: .tab(.myProfile), animated: false)
|
||||
select(item: .tab(.myProfile))
|
||||
select(newItem: .tab(.myProfile), oldItem: sidebar.selectedItem)
|
||||
}
|
||||
|
||||
@objc private func sidebarTapped() {
|
||||
|
@ -444,12 +452,12 @@ extension MainSplitViewController: UISplitViewControllerDelegate {
|
|||
// These tabs map 1 <-> 1 with sidebar items
|
||||
let item = MainSidebarViewController.Item.tab(tabBarViewController.selectedTab)
|
||||
sidebar.select(item: item, animated: false)
|
||||
select(item: item)
|
||||
doSelect(item: item)
|
||||
|
||||
case .explore:
|
||||
// If the explore tab is active, the sidebar item is determined above when transferring the explore VC's nav stack
|
||||
sidebar.select(item: exploreItem!, animated: false)
|
||||
select(item: exploreItem!)
|
||||
doSelect(item: exploreItem!)
|
||||
|
||||
default:
|
||||
return
|
||||
|
@ -474,16 +482,13 @@ extension MainSplitViewController: MainSidebarViewControllerDelegate {
|
|||
compose(editing: nil)
|
||||
}
|
||||
|
||||
func sidebar(_ sidebarViewController: MainSidebarViewController, didSelectItem item: MainSidebarViewController.Item) {
|
||||
if let previous = sidebar.previouslySelectedItem {
|
||||
navigationStacks[previous] = secondaryNavController.viewControllers
|
||||
}
|
||||
select(item: item)
|
||||
func sidebar(_ sidebarViewController: MainSidebarViewController, didSelectItem item: MainSidebarViewController.Item, previousItem: MainSidebarViewController.Item?) {
|
||||
select(newItem: item, oldItem: previousItem)
|
||||
}
|
||||
|
||||
func sidebar(_ sidebarViewController: MainSidebarViewController, showViewController viewController: UIViewController) {
|
||||
if let previous = sidebar.previouslySelectedItem {
|
||||
navigationStacks[previous] = secondaryNavController.viewControllers
|
||||
func sidebar(_ sidebarViewController: MainSidebarViewController, showViewController viewController: UIViewController, previousItem: MainSidebarViewController.Item?) {
|
||||
if let previousItem {
|
||||
navigationStacks[previousItem] = secondaryNavController.viewControllers
|
||||
}
|
||||
secondaryNavController.viewControllers = [viewController]
|
||||
}
|
||||
|
@ -537,14 +542,14 @@ extension MainSplitViewController: StateRestorableViewController {
|
|||
}
|
||||
|
||||
extension MainSplitViewController: TuskerRootViewController {
|
||||
func select(route: TuskerRoute, animated: Bool) {
|
||||
func select(route: TuskerRoute, animated: Bool, completion: (() -> Void)?) {
|
||||
guard traitCollection.horizontalSizeClass != .compact else {
|
||||
tabBarViewController?.select(route: route, animated: animated)
|
||||
tabBarViewController?.select(route: route, animated: animated, completion: completion)
|
||||
return
|
||||
}
|
||||
guard presentedViewController == nil else {
|
||||
dismiss(animated: animated) {
|
||||
self.select(route: route, animated: animated)
|
||||
self.select(route: route, animated: animated, completion: completion)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
@ -567,8 +572,10 @@ extension MainSplitViewController: TuskerRootViewController {
|
|||
return
|
||||
}
|
||||
}
|
||||
let oldItem = sidebar.selectedItem
|
||||
sidebar.select(item: item, animated: false)
|
||||
select(item: item)
|
||||
select(newItem: item, oldItem: oldItem)
|
||||
completion?()
|
||||
}
|
||||
|
||||
func getTabController(tab: MainTabBarViewController.Tab) -> UIViewController? {
|
||||
|
@ -610,7 +617,7 @@ extension MainSplitViewController: TuskerRootViewController {
|
|||
}
|
||||
|
||||
if sidebar.selectedItem != .explore {
|
||||
select(item: .explore)
|
||||
select(newItem: .explore, oldItem: sidebar.selectedItem)
|
||||
}
|
||||
|
||||
guard let searchViewController = secondaryNavController.viewControllers.first as? InlineTrendsViewController else {
|
||||
|
|
|
@ -289,7 +289,7 @@ extension MainTabBarViewController: StateRestorableViewController {
|
|||
}
|
||||
|
||||
extension MainTabBarViewController: TuskerRootViewController {
|
||||
func select(route: TuskerRoute, animated: Bool) {
|
||||
func select(route: TuskerRoute, animated: Bool, completion: (() -> Void)?) {
|
||||
switch route {
|
||||
case .timelines:
|
||||
select(tab: .timelines, dismissPresented: true)
|
||||
|
@ -310,6 +310,7 @@ extension MainTabBarViewController: TuskerRootViewController {
|
|||
nav.pushViewController(ListTimelineViewController(for: list, mastodonController: mastodonController), animated: animated)
|
||||
}
|
||||
}
|
||||
completion?()
|
||||
}
|
||||
|
||||
func getNavigationDelegate() -> TuskerNavigationDelegate? {
|
||||
|
|
|
@ -12,7 +12,7 @@ import ComposeUI
|
|||
@MainActor
|
||||
protocol TuskerRootViewController: UIViewController, StateRestorableViewController, StatusBarTappableViewController {
|
||||
func compose(editing draft: Draft?, animated: Bool, isDucked: Bool)
|
||||
func select(route: TuskerRoute, animated: Bool)
|
||||
func select(route: TuskerRoute, animated: Bool, completion: (() -> Void)?)
|
||||
func getTabController(tab: MainTabBarViewController.Tab) -> UIViewController?
|
||||
func getNavigationDelegate() -> TuskerNavigationDelegate?
|
||||
func getNavigationController() -> NavigationControllerProtocol
|
||||
|
@ -21,33 +21,6 @@ protocol TuskerRootViewController: UIViewController, StateRestorableViewControll
|
|||
func presentPreferences(completion: (() -> Void)?) -> PreferencesNavigationController?
|
||||
}
|
||||
|
||||
//extension TuskerRootViewController {
|
||||
// func select(route: NewRoute, animated: Bool) {
|
||||
// doApply(components: route.components, animated: animated)
|
||||
// }
|
||||
//
|
||||
// private func doApply(components: ArraySlice<RouteComponent>, animated: Bool) {
|
||||
// guard let first = components.first else {
|
||||
// return
|
||||
// }
|
||||
// doApply(component: first, animated: animated) {
|
||||
// self.doApply(components: components.dropFirst(), animated: animated)
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// private func doApply(component: RouteComponent, animated: Bool, completion: @escaping () -> Void) {
|
||||
// switch component {
|
||||
// case .topLevelItem(let rootRoute):
|
||||
// select(route: rootRoute)
|
||||
// completion()
|
||||
// case .popToRoot:
|
||||
// _ = getNavigationController().popToRootViewController(animated: animated)
|
||||
// completion()
|
||||
// case .push(<#T##(MastodonController) -> UIViewController#>)
|
||||
// }
|
||||
// }
|
||||
//}
|
||||
|
||||
enum TuskerRoute {
|
||||
case timelines
|
||||
case notifications
|
||||
|
@ -57,33 +30,6 @@ enum TuskerRoute {
|
|||
case list(id: String)
|
||||
}
|
||||
|
||||
//struct NewRoute: ExpressibleByArrayLiteral {
|
||||
// let components: [RouteComponent]
|
||||
//
|
||||
// init(arrayLiteral elements: RouteComponent...) {
|
||||
// self.components = elements
|
||||
// }
|
||||
//
|
||||
// static var timelines: Self { [.topLevelItem(.timelines)] }
|
||||
// static var explore: Self { [.topLevelItem(.explore)] }
|
||||
// static var myProfile: Self { [.topLevelItem(.myProfile)] }
|
||||
// static var bookmarks: Self { [.topLevelItem(.explore), .push({ BookmarksViewController(mastodonController: $0) })] }
|
||||
// static func profile(accountID: String) -> Self { [.topLevelItem(.timelines), .push({ ProfileViewController(accountID: accountID, mastodonController: $0) })] }
|
||||
//}
|
||||
//
|
||||
//enum RouteComponent {
|
||||
// case topLevelItem(RootRoute)
|
||||
// case popToRoot
|
||||
// case push((MastodonController) -> UIViewController)
|
||||
// case present(UIViewController)
|
||||
//}
|
||||
//
|
||||
//enum RootRoute {
|
||||
// case timelines
|
||||
// case explore
|
||||
// case myProfile
|
||||
//}
|
||||
//
|
||||
@MainActor
|
||||
protocol NavigationControllerProtocol: UIViewController {
|
||||
var viewControllers: [UIViewController] { get set }
|
||||
|
|
|
@ -78,18 +78,20 @@ struct MuteAccountView: View {
|
|||
}
|
||||
.accessibilityHidden(true)
|
||||
|
||||
Section {
|
||||
Toggle(isOn: $muteNotifications) {
|
||||
Text("Hide notifications from this person")
|
||||
}
|
||||
} footer: {
|
||||
if muteNotifications {
|
||||
Text("This user's posts and notifications will be hidden.")
|
||||
} else {
|
||||
Text("This user's posts will be hidden from your timeline. You can still receive notifications from them.")
|
||||
if mastodonController.instanceFeatures.muteNotifications {
|
||||
Section {
|
||||
Toggle(isOn: $muteNotifications) {
|
||||
Text("Hide notifications from this person")
|
||||
}
|
||||
} footer: {
|
||||
if muteNotifications {
|
||||
Text("This user's posts and notifications will be hidden.")
|
||||
} else {
|
||||
Text("This user's posts will be hidden from your timeline. You can still receive notifications from them.")
|
||||
}
|
||||
}
|
||||
.appGroupedListRowBackground()
|
||||
}
|
||||
.appGroupedListRowBackground()
|
||||
|
||||
Section {
|
||||
Picker(selection: $duration) {
|
||||
|
|
|
@ -9,6 +9,12 @@
|
|||
import UIKit
|
||||
import Pachyderm
|
||||
import Combine
|
||||
import OSLog
|
||||
#if canImport(Sentry)
|
||||
import Sentry
|
||||
#endif
|
||||
|
||||
private let logger = Logger(subsystem: Bundle.main.bundleIdentifier!, category: "ProfileStatusesViewController")
|
||||
|
||||
class ProfileStatusesViewController: UIViewController, TimelineLikeCollectionViewController, CollectionViewController {
|
||||
|
||||
|
@ -250,9 +256,16 @@ class ProfileStatusesViewController: UIViewController, TimelineLikeCollectionVie
|
|||
state = .setupInitialSnapshot
|
||||
|
||||
Task {
|
||||
if let (all, _) = try? await mastodonController.run(Client.getRelationships(accounts: [accountID])),
|
||||
let relationship = all.first {
|
||||
self.mastodonController.persistentContainer.addOrUpdate(relationship: relationship)
|
||||
do {
|
||||
let (all, _) = try await mastodonController.run(Client.getRelationships(accounts: [accountID]))
|
||||
if let relationship = all.first {
|
||||
self.mastodonController.persistentContainer.addOrUpdate(relationship: relationship)
|
||||
}
|
||||
} catch {
|
||||
logger.error("Error fetching relationship: \(String(describing: error))")
|
||||
#if canImport(Sentry)
|
||||
SentrySDK.capture(error: error)
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -142,8 +142,8 @@ class MultiColumnNavigationController: UIViewController {
|
|||
}
|
||||
|
||||
fileprivate func closeColumn(_ vc: UIViewController) {
|
||||
let index = viewControllers.firstIndex(of: vc)!
|
||||
guard index > 0 else {
|
||||
guard let index = viewControllers.firstIndex(of: vc),
|
||||
index > 0 else {
|
||||
// Can't close the last column
|
||||
return
|
||||
}
|
||||
|
@ -273,12 +273,17 @@ private class ColumnView: UIView {
|
|||
}
|
||||
|
||||
private func installCloseBarButton(navigationItem: UINavigationItem) {
|
||||
let item = UIBarButtonItem(image: UIImage(systemName: "xmark"), style: .done, target: self, action: #selector(closeNavigationColumn))
|
||||
item.accessibilityLabel = "Close Column"
|
||||
if navigationItem.leftBarButtonItems != nil {
|
||||
navigationItem.leftBarButtonItems!.insert(item, at: 0)
|
||||
func makeItem() -> UIBarButtonItem {
|
||||
let item = UIBarButtonItem(image: UIImage(systemName: "xmark"), style: .done, target: self, action: #selector(closeNavigationColumn))
|
||||
item.accessibilityLabel = "Close Column"
|
||||
return item
|
||||
}
|
||||
if let leftItems = navigationItem.leftBarButtonItems {
|
||||
if !leftItems.contains(where: { $0.action == #selector(closeNavigationColumn) }) {
|
||||
navigationItem.leftBarButtonItems!.insert(makeItem(), at: 0)
|
||||
}
|
||||
} else {
|
||||
navigationItem.leftBarButtonItems = [item]
|
||||
navigationItem.leftBarButtonItems = [makeItem()]
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -557,11 +557,15 @@ extension MenuActionProvider {
|
|||
return createAction(identifier: "block", title: "Unblock \(displayName)", systemImageName: "circle.slash", handler: handler(false))
|
||||
} else {
|
||||
let image = UIImage(systemName: "circle.slash")
|
||||
return UIMenu(title: "Block", image: image, children: [
|
||||
var children = [
|
||||
UIAction(title: "Cancel", handler: { _ in }),
|
||||
UIAction(title: "Block \(displayName)", image: image, attributes: .destructive, handler: handler(true)),
|
||||
UIAction(title: "Block \(host)", image: image, attributes: .destructive, handler: domainHandler(true))
|
||||
])
|
||||
]
|
||||
if mastodonController.instanceFeatures.blockDomains,
|
||||
host != mastodonController.account?.url.host {
|
||||
children.append(UIAction(title: "Block \(host)", image: image, attributes: .destructive, handler: domainHandler(true)))
|
||||
}
|
||||
return UIMenu(title: "Block", image: image, children: children)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -592,7 +596,8 @@ extension MenuActionProvider {
|
|||
@MainActor
|
||||
private func hideReblogsAction(for relationship: RelationshipMO, mastodonController: MastodonController) -> UIMenuElement? {
|
||||
// don't show action for people that the user isn't following and isn't already hiding reblogs for
|
||||
guard relationship.following || relationship.showingReblogs else {
|
||||
guard relationship.following || relationship.showingReblogs,
|
||||
mastodonController.instanceFeatures.hideReblogs else {
|
||||
return nil
|
||||
}
|
||||
let title = relationship.showingReblogs ? "Hide Reblogs" : "Show Reblogs"
|
||||
|
|
|
@ -50,8 +50,11 @@ extension UIViewController {
|
|||
}
|
||||
|
||||
func removeViewAndController() {
|
||||
beginAppearanceTransition(false, animated: false)
|
||||
view.removeFromSuperview()
|
||||
willMove(toParent: nil)
|
||||
removeFromParent()
|
||||
endAppearanceTransition()
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -68,7 +71,7 @@ extension UIView {
|
|||
|
||||
if layout {
|
||||
subview.frame = bounds
|
||||
|
||||
subview.translatesAutoresizingMaskIntoConstraints = false
|
||||
NSLayoutConstraint.activate([
|
||||
subview.leadingAnchor.constraint(equalTo: leadingAnchor),
|
||||
subview.trailingAnchor.constraint(equalTo: trailingAnchor),
|
||||
|
|
|
@ -44,9 +44,9 @@ enum AppShortcutItem: String, CaseIterable {
|
|||
}
|
||||
switch self {
|
||||
case .showHomeTimeline:
|
||||
root.select(route: .timelines, animated: false)
|
||||
root.select(route: .timelines, animated: false, completion: nil)
|
||||
case .showNotifications:
|
||||
root.select(route: .notifications, animated: false)
|
||||
root.select(route: .notifications, animated: false, completion: nil)
|
||||
case .composePost:
|
||||
root.compose(editing: nil, animated: false, isDucked: false)
|
||||
}
|
||||
|
|
|
@ -39,12 +39,13 @@ extension NSUserActivity {
|
|||
self.userInfo = [
|
||||
"accountID": accountID
|
||||
]
|
||||
self.targetContentIdentifier = accountID
|
||||
}
|
||||
|
||||
@MainActor
|
||||
func handleResume(manager: UserActivityManager) -> Bool {
|
||||
func handleResume(manager: UserActivityManager) async -> Bool {
|
||||
guard let type = UserActivityType(rawValue: activityType) else { return false }
|
||||
type.handle(manager)(self)
|
||||
await type.handle(manager)(self)
|
||||
return true
|
||||
}
|
||||
|
||||
|
|
|
@ -16,7 +16,8 @@ import ComposeUI
|
|||
protocol UserActivityHandlingContext {
|
||||
var isHandoff: Bool { get }
|
||||
|
||||
func select(route: TuskerRoute)
|
||||
func select(route: TuskerRoute) async
|
||||
func select(route: TuskerRoute, completion: (() -> Void)?)
|
||||
func present(_ vc: UIViewController)
|
||||
|
||||
var topViewController: UIViewController? { get }
|
||||
|
@ -28,6 +29,16 @@ protocol UserActivityHandlingContext {
|
|||
func finalize(activity: NSUserActivity)
|
||||
}
|
||||
|
||||
extension UserActivityHandlingContext {
|
||||
func select(route: TuskerRoute) async {
|
||||
await withCheckedContinuation { continuation in
|
||||
select(route: route) {
|
||||
continuation.resume()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct ActiveAccountUserActivityHandlingContext: UserActivityHandlingContext {
|
||||
let isHandoff: Bool
|
||||
let root: TuskerRootViewController
|
||||
|
@ -35,8 +46,8 @@ struct ActiveAccountUserActivityHandlingContext: UserActivityHandlingContext {
|
|||
root.getNavigationDelegate()!
|
||||
}
|
||||
|
||||
func select(route: TuskerRoute) {
|
||||
root.select(route: route, animated: true)
|
||||
func select(route: TuskerRoute, completion: (() -> Void)?) {
|
||||
root.select(route: route, animated: true, completion: completion)
|
||||
}
|
||||
|
||||
func present(_ vc: UIViewController) {
|
||||
|
@ -71,9 +82,11 @@ class StateRestorationUserActivityHandlingContext: UserActivityHandlingContext {
|
|||
|
||||
var isHandoff: Bool { false }
|
||||
|
||||
func select(route: TuskerRoute) {
|
||||
root.select(route: route, animated: false)
|
||||
state = .selectedRoute
|
||||
func select(route: TuskerRoute, completion: (() -> Void)?) {
|
||||
root.select(route: route, animated: false) {
|
||||
self.state = .selectedRoute
|
||||
completion?()
|
||||
}
|
||||
}
|
||||
|
||||
var topViewController: UIViewController? { root.getNavigationController().topViewController }
|
||||
|
|
|
@ -133,8 +133,8 @@ class UserActivityManager {
|
|||
return activity
|
||||
}
|
||||
|
||||
func handleCheckNotifications(activity: NSUserActivity) {
|
||||
context.select(route: .notifications)
|
||||
func handleCheckNotifications(activity: NSUserActivity) async {
|
||||
await context.select(route: .notifications)
|
||||
context.popToRoot()
|
||||
if let notificationsPageController = context.topViewController as? NotificationsPageViewController {
|
||||
notificationsPageController.loadViewIfNeeded()
|
||||
|
@ -204,22 +204,22 @@ class UserActivityManager {
|
|||
return (timeline, positionInfo)
|
||||
}
|
||||
|
||||
func handleShowTimeline(activity: NSUserActivity) {
|
||||
func handleShowTimeline(activity: NSUserActivity) async {
|
||||
guard let (timeline, positionInfo) = Self.getTimeline(from: activity) else { return }
|
||||
|
||||
var timelineVC: TimelineViewController?
|
||||
if let pinned = PinnedTimeline(timeline: timeline),
|
||||
mastodonController.accountPreferences.pinnedTimelines.contains(pinned) {
|
||||
context.select(route: .timelines)
|
||||
await context.select(route: .timelines)
|
||||
context.popToRoot()
|
||||
let pageController = context.topViewController as! TimelinesPageViewController
|
||||
pageController.selectTimeline(pinned, animated: false)
|
||||
timelineVC = pageController.currentViewController as? TimelineViewController
|
||||
} else if case .list(let id) = timeline {
|
||||
context.select(route: .list(id: id))
|
||||
await context.select(route: .list(id: id))
|
||||
timelineVC = context.topViewController as? TimelineViewController
|
||||
} else {
|
||||
context.select(route: .explore)
|
||||
await context.select(route: .explore)
|
||||
context.popToRoot()
|
||||
timelineVC = TimelineViewController(for: timeline, mastodonController: mastodonController)
|
||||
context.push(timelineVC!)
|
||||
|
@ -249,11 +249,11 @@ class UserActivityManager {
|
|||
return activity.userInfo?["mainStatusID"] as? String
|
||||
}
|
||||
|
||||
func handleShowConversation(activity: NSUserActivity) {
|
||||
func handleShowConversation(activity: NSUserActivity) async {
|
||||
guard let mainStatusID = Self.getConversationStatus(from: activity) else {
|
||||
return
|
||||
}
|
||||
context.select(route: .timelines)
|
||||
await context.select(route: .timelines)
|
||||
context.push(ConversationViewController(for: mainStatusID, state: .unknown, mastodonController: mastodonController))
|
||||
}
|
||||
|
||||
|
@ -274,8 +274,8 @@ class UserActivityManager {
|
|||
return activity.userInfo?["query"] as? String
|
||||
}
|
||||
|
||||
func handleSearch(activity: NSUserActivity) {
|
||||
context.select(route: .explore)
|
||||
func handleSearch(activity: NSUserActivity) async {
|
||||
await context.select(route: .explore)
|
||||
context.popToRoot()
|
||||
|
||||
let searchController: UISearchController
|
||||
|
@ -311,8 +311,8 @@ class UserActivityManager {
|
|||
return activity
|
||||
}
|
||||
|
||||
func handleBookmarks(activity: NSUserActivity) {
|
||||
context.select(route: .bookmarks)
|
||||
func handleBookmarks(activity: NSUserActivity) async {
|
||||
await context.select(route: .bookmarks)
|
||||
}
|
||||
|
||||
// MARK: - My Profile
|
||||
|
@ -325,8 +325,8 @@ class UserActivityManager {
|
|||
return activity
|
||||
}
|
||||
|
||||
func handleMyProfile(activity: NSUserActivity) {
|
||||
context.select(route: .myProfile)
|
||||
func handleMyProfile(activity: NSUserActivity) async {
|
||||
await context.select(route: .myProfile)
|
||||
}
|
||||
|
||||
// MARK: - Show Profile
|
||||
|
@ -344,11 +344,11 @@ class UserActivityManager {
|
|||
return activity.userInfo?["profileID"] as? String
|
||||
}
|
||||
|
||||
func handleShowProfile(activity: NSUserActivity) {
|
||||
func handleShowProfile(activity: NSUserActivity) async {
|
||||
guard let accountID = Self.getProfile(from: activity) else {
|
||||
return
|
||||
}
|
||||
context.select(route: .timelines)
|
||||
await context.select(route: .timelines)
|
||||
context.push(ProfileViewController(accountID: accountID, mastodonController: mastodonController))
|
||||
}
|
||||
|
||||
|
@ -361,11 +361,11 @@ class UserActivityManager {
|
|||
return activity
|
||||
}
|
||||
|
||||
func handleShowNotification(activity: NSUserActivity) {
|
||||
func handleShowNotification(activity: NSUserActivity) async {
|
||||
guard let notificationID = activity.userInfo?["notificationID"] as? String else {
|
||||
return
|
||||
}
|
||||
context.select(route: .notifications)
|
||||
await context.select(route: .notifications)
|
||||
context.push(NotificationLoadingViewController(notificationID: notificationID, mastodonController: mastodonController))
|
||||
}
|
||||
|
||||
|
|
|
@ -23,7 +23,7 @@ enum UserActivityType: String {
|
|||
|
||||
extension UserActivityType {
|
||||
@MainActor
|
||||
var handle: (UserActivityManager) -> @MainActor (NSUserActivity) -> Void {
|
||||
var handle: (UserActivityManager) -> @MainActor (NSUserActivity) async -> Void {
|
||||
switch self {
|
||||
case .mainScene:
|
||||
fatalError("cannot handle main scene activity")
|
||||
|
|
|
@ -33,6 +33,11 @@ class CachedImageView: UIImageView {
|
|||
commonInit()
|
||||
}
|
||||
|
||||
deinit {
|
||||
fetchTask?.cancel()
|
||||
blurHashTask?.cancel()
|
||||
}
|
||||
|
||||
private func commonInit() {
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(preferencesChanged), name: .preferencesChanged, object: nil)
|
||||
}
|
||||
|
|
|
@ -25,9 +25,9 @@ class ProfileHeaderView: UIView {
|
|||
weak var delegate: ProfileHeaderViewDelegate?
|
||||
var mastodonController: MastodonController! { delegate?.apiController }
|
||||
|
||||
@IBOutlet weak var headerImageView: UIImageView!
|
||||
@IBOutlet weak var headerImageView: CachedImageView!
|
||||
@IBOutlet weak var avatarContainerView: UIView!
|
||||
@IBOutlet weak var avatarImageView: UIImageView!
|
||||
@IBOutlet weak var avatarImageView: CachedImageView!
|
||||
@IBOutlet weak var moreButton: ProfileHeaderButton!
|
||||
@IBOutlet weak var followButton: ProfileHeaderButton!
|
||||
@IBOutlet weak var displayNameLabel: AccountDisplayNameLabel!
|
||||
|
@ -44,8 +44,6 @@ class ProfileHeaderView: UIView {
|
|||
|
||||
var accountID: String!
|
||||
|
||||
private var imagesTask: Task<Void, Never>?
|
||||
|
||||
private var isGrayscale = false
|
||||
private var followButtonMode = FollowButtonMode.follow {
|
||||
didSet {
|
||||
|
@ -56,10 +54,6 @@ class ProfileHeaderView: UIView {
|
|||
}
|
||||
}
|
||||
|
||||
deinit {
|
||||
imagesTask?.cancel()
|
||||
}
|
||||
|
||||
override func awakeFromNib() {
|
||||
super.awakeFromNib()
|
||||
|
||||
|
@ -69,11 +63,13 @@ class ProfileHeaderView: UIView {
|
|||
avatarContainerView.layer.cornerCurve = .continuous
|
||||
// Set zPositions so the gallery presentation/dismissal animation looks correct.
|
||||
avatarContainerView.layer.zPosition = 2
|
||||
avatarImageView.cache = .avatars
|
||||
avatarImageView.layer.masksToBounds = true
|
||||
avatarImageView.layer.cornerCurve = .continuous
|
||||
avatarImageView.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(avatarPressed)))
|
||||
avatarImageView.isUserInteractionEnabled = true
|
||||
|
||||
headerImageView.cache = .headers
|
||||
headerImageView.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(headerPressed)))
|
||||
headerImageView.isUserInteractionEnabled = true
|
||||
headerImageView.layer.zPosition = 1
|
||||
|
@ -138,11 +134,11 @@ class ProfileHeaderView: UIView {
|
|||
usernameLabel.text = "@\(account.acct)"
|
||||
lockImageView.isHidden = !account.locked
|
||||
|
||||
imagesTask?.cancel()
|
||||
let avatar = account.avatar
|
||||
let header = account.header
|
||||
imagesTask = Task {
|
||||
await updateImages(avatar: avatar, header: header)
|
||||
if let avatar = account.avatar {
|
||||
avatarImageView.update(for: avatar)
|
||||
}
|
||||
if let header = account.header {
|
||||
headerImageView.update(for: header)
|
||||
}
|
||||
|
||||
moreButton.menu = UIMenu(title: "", image: nil, identifier: nil, options: [], children: delegate?.actionsForProfile(accountID: accountID, source: .view(moreButton), fetchRelationship: false) ?? [])
|
||||
|
@ -294,44 +290,6 @@ class ProfileHeaderView: UIView {
|
|||
avatarContainerView.layer.cornerRadius = Preferences.shared.avatarStyle.cornerRadius(for: avatarContainerView)
|
||||
avatarImageView.layer.cornerRadius = Preferences.shared.avatarStyle.cornerRadius(for: avatarImageView)
|
||||
displayNameLabel.updateForAccountDisplayName(account: account)
|
||||
|
||||
if isGrayscale != Preferences.shared.grayscaleImages {
|
||||
isGrayscale = Preferences.shared.grayscaleImages
|
||||
imagesTask?.cancel()
|
||||
let avatar = account.avatar
|
||||
let header = account.header
|
||||
imagesTask = Task {
|
||||
await updateImages(avatar: avatar, header: header)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private nonisolated func updateImages(avatar: URL?, header: URL?) async {
|
||||
await withTaskGroup(of: Void.self) { group in
|
||||
group.addTask {
|
||||
guard let avatar,
|
||||
let image = await ImageCache.avatars.get(avatar, loadOriginal: true).1,
|
||||
let transformedImage = await ImageGrayscalifier.convertIfNecessary(url: avatar, image: image),
|
||||
!Task.isCancelled else {
|
||||
return
|
||||
}
|
||||
await MainActor.run {
|
||||
self.avatarImageView.image = transformedImage
|
||||
}
|
||||
}
|
||||
group.addTask {
|
||||
guard let header,
|
||||
let image = await ImageCache.avatars.get(header, loadOriginal: true).1,
|
||||
let transformedImage = await ImageGrayscalifier.convertIfNecessary(url: header, image: image),
|
||||
!Task.isCancelled else {
|
||||
return
|
||||
}
|
||||
await MainActor.run {
|
||||
self.headerImageView.image = transformedImage
|
||||
}
|
||||
}
|
||||
await group.waitForAll()
|
||||
}
|
||||
}
|
||||
|
||||
private func formatBigNumber(_ value: Int) -> (String, String) {
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
<device id="retina6_1" orientation="portrait" appearance="light"/>
|
||||
<dependencies>
|
||||
<deployment identifier="iOS"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="22684"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="22685"/>
|
||||
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
|
||||
<capability name="System colors in document resources" minToolsVersion="11.0"/>
|
||||
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
||||
|
@ -14,7 +14,7 @@
|
|||
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="iN0-l3-epB" customClass="ProfileHeaderView" customModule="Tusker" customModuleProvider="target">
|
||||
<rect key="frame" x="0.0" y="0.0" width="414" height="896"/>
|
||||
<subviews>
|
||||
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFill" horizontalHuggingPriority="251" verticalHuggingPriority="251" translatesAutoresizingMaskIntoConstraints="NO" id="dgG-dR-lSv">
|
||||
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFill" horizontalHuggingPriority="251" verticalHuggingPriority="251" translatesAutoresizingMaskIntoConstraints="NO" id="dgG-dR-lSv" customClass="CachedImageView" customModule="Tusker" customModuleProvider="target">
|
||||
<rect key="frame" x="0.0" y="48" width="414" height="150"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="height" constant="150" id="aCE-CA-XWm"/>
|
||||
|
@ -23,7 +23,7 @@
|
|||
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="wT9-2J-uSY">
|
||||
<rect key="frame" x="16" y="138" width="120" height="120"/>
|
||||
<subviews>
|
||||
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFill" horizontalHuggingPriority="251" verticalHuggingPriority="251" translatesAutoresizingMaskIntoConstraints="NO" id="TkY-oK-if4">
|
||||
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFill" horizontalHuggingPriority="251" verticalHuggingPriority="251" translatesAutoresizingMaskIntoConstraints="NO" id="TkY-oK-if4" customClass="CachedImageView" customModule="Tusker" customModuleProvider="target">
|
||||
<rect key="frame" x="2" y="2" width="116" height="116"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="height" constant="116" id="eDg-Vc-o8R"/>
|
||||
|
@ -93,7 +93,7 @@
|
|||
<stackView opaque="NO" contentMode="scaleToFill" distribution="fillProportionally" spacing="8" translatesAutoresizingMaskIntoConstraints="NO" id="udp-EN-wtc">
|
||||
<rect key="frame" x="0.0" y="575.5" width="218.5" height="20.5"/>
|
||||
<subviews>
|
||||
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="system" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="5w9-LA-8kc">
|
||||
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="system" lineBreakMode="middleTruncation" pointerInteraction="YES" translatesAutoresizingMaskIntoConstraints="NO" id="5w9-LA-8kc">
|
||||
<rect key="frame" x="0.0" y="0.0" width="104" height="20.5"/>
|
||||
<state key="normal" title="Button"/>
|
||||
<buttonConfiguration key="configuration" style="plain" title="123 Following">
|
||||
|
@ -104,7 +104,7 @@
|
|||
<action selector="followingCountButtonPressed:" destination="iN0-l3-epB" eventType="touchUpInside" id="QOO-zK-pfu"/>
|
||||
</connections>
|
||||
</button>
|
||||
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="system" lineBreakMode="middleTruncation" id="XCX-Y3-cG5">
|
||||
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="system" lineBreakMode="middleTruncation" pointerInteraction="YES" id="XCX-Y3-cG5">
|
||||
<rect key="frame" x="112" y="0.0" width="106.5" height="20.5"/>
|
||||
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
|
||||
<state key="normal" title="Button"/>
|
||||
|
|
|
@ -640,7 +640,7 @@ extension ConversationMainStatusCollectionViewCell: UIPointerInteractionDelegate
|
|||
return defaultRegion
|
||||
} else if let button = interaction.view as? UIButton,
|
||||
actionButtons.contains(button) {
|
||||
var rect = button.convert(button.imageView!.bounds, to: button.imageView!)
|
||||
var rect = button.convert(button.imageView!.bounds, from: button.imageView!)
|
||||
rect = rect.insetBy(dx: -24, dy: -24)
|
||||
return UIPointerRegion(rect: rect)
|
||||
}
|
||||
|
@ -654,8 +654,8 @@ extension ConversationMainStatusCollectionViewCell: UIPointerInteractionDelegate
|
|||
} else if let button = interaction.view as? UIButton,
|
||||
actionButtons.contains(button) {
|
||||
let preview = UITargetedPreview(view: button.imageView!)
|
||||
var rect = button.convert(button.imageView!.bounds, to: button.imageView!)
|
||||
rect = rect.insetBy(dx: -24, dy: -24)
|
||||
var rect = button.convert(button.imageView!.bounds, from: button.imageView!)
|
||||
rect = rect.insetBy(dx: -8, dy: -8)
|
||||
return UIPointerStyle(effect: .highlight(preview), shape: .roundedRect(rect))
|
||||
}
|
||||
return nil
|
||||
|
|
Loading…
Reference in New Issue