Compare commits

...

7 Commits

Author SHA1 Message Date
Shadowfacts cc0da2ec54 Fix user activities not continuing when passed at launch
Fix crash when continuing user activities on iPad
2022-05-13 17:10:18 -04:00
Shadowfacts a2868739c2 Fix crash when poll voting fails 2022-05-13 10:00:11 -04:00
Shadowfacts 2f75510889 Disable transparent nav bar in conversation vc 2022-05-11 19:15:56 -04:00
Shadowfacts 46332cd1b9 Jump to statuses below parent when expanding subthread in conversation 2022-05-11 19:12:28 -04:00
Shadowfacts 21e9ca990d Use async/await for conversation loading 2022-05-11 19:10:38 -04:00
Shadowfacts 1a02319894 Fix using old style for show all statuses bar button item when showing a
conversation that initially expands all statuses
2022-05-11 11:33:18 -04:00
Shadowfacts 4a95ccccdb Show expand thread indicator when there are additional replies to an
intermediate post in thread authored by a single person
2022-05-11 11:20:01 -04:00
24 changed files with 263 additions and 105 deletions

View File

@ -9,6 +9,9 @@
import UIKit
import CrashReporter
import CoreData
import OSLog
let stateRestorationLogger = Logger(subsystem: Bundle.main.bundleIdentifier!, category: "StateRestoration")
@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
@ -85,7 +88,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
switch type {
case .mainScene:
return "main-scene"
case .showConversation,
.showTimeline,
.checkNotifications,
@ -93,7 +96,12 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
.bookmarks,
.myProfile,
.showProfile:
return "auxiliary"
if activity.displaysAuxiliaryScene {
stateRestorationLogger.info("Using auxiliary scene for \(type.rawValue, privacy: .public)")
return "auxiliary"
} else {
return "main-scene"
}
case .newPost:
return "compose"

View File

@ -87,6 +87,16 @@ class MastodonCachePersistentStore: NSPersistentContainer {
self.statusSubject.send(status.id)
}
}
@MainActor
func addOrUpdateOnViewContext(status: Status) -> StatusMO {
let statusMO = self.upsert(status: status, context: viewContext)
if viewContext.hasChanges {
try! viewContext.save()
}
statusSubject.send(status.id)
return statusMO
}
func addAll(statuses: [Status], completion: (() -> Void)? = nil) {
backgroundContext.perform {
@ -98,6 +108,14 @@ class MastodonCachePersistentStore: NSPersistentContainer {
completion?()
}
}
func addAll(statuses: [Status]) async {
return await withCheckedContinuation { continuation in
addAll(statuses: statuses) {
continuation.resume()
}
}
}
func account(for id: String, in context: NSManagedObjectContext? = nil) -> AccountMO? {
let context = context ?? viewContext

View File

@ -28,6 +28,7 @@ class MainSceneDelegate: UIResponder, UIWindowSceneDelegate {
if let activity = connectionOptions.userActivities.first ?? session.stateRestorationActivity {
launchActivity = activity
}
stateRestorationLogger.info("MainSceneDelegate.launchActivity = \(self.launchActivity?.activityType ?? "nil", privacy: .public)")
window = UIWindow(windowScene: windowScene)
@ -69,7 +70,8 @@ class MainSceneDelegate: UIResponder, UIWindowSceneDelegate {
}
func scene(_ scene: UIScene, continue userActivity: NSUserActivity) {
_ = userActivity.handleResume()
stateRestorationLogger.info("MainSceneDelegate.scene(_:continue:) called with \(userActivity.activityType, privacy: .public)")
_ = userActivity.handleResume(manager: UserActivityManager(scene: scene as! UIWindowScene))
}
func windowScene(_ windowScene: UIWindowScene, performActionFor shortcutItem: UIApplicationShortcutItem, completionHandler: @escaping (Bool) -> Void) {
@ -169,6 +171,11 @@ class MainSceneDelegate: UIResponder, UIWindowSceneDelegate {
}
activateAccount(account, animated: false)
if let activity = launchActivity,
activity.activityType != UserActivityType.mainScene.rawValue {
_ = activity.handleResume(manager: UserActivityManager(scene: window!.windowScene!))
}
} else {
window!.rootViewController = createOnboardingUI()
}

View File

@ -31,6 +31,7 @@ class ConversationTableViewController: EnhancedTableViewController {
let mainStatusID: String
let mainStatusState: StatusState
var statusIDToScrollToOnLoad: String
private(set) var dataSource: UITableViewDiffableDataSource<Section, Item>!
var showStatusesAutomatically = false
@ -41,6 +42,7 @@ class ConversationTableViewController: EnhancedTableViewController {
init(for mainStatusID: String, state: StatusState = .unknown, mastodonController: MastodonController) {
self.mainStatusID = mainStatusID
self.mainStatusState = state
self.statusIDToScrollToOnLoad = mainStatusID
self.mastodonController = mastodonController
super.init(style: .plain)
@ -115,52 +117,59 @@ class ConversationTableViewController: EnhancedTableViewController {
return cell
case let .expandThread(childThreads: childThreads):
case let .expandThread(childThreads: childThreads, inline: inline):
let cell = tableView.dequeueReusableCell(withIdentifier: "expandThreadCell", for: indexPath) as! ExpandThreadTableViewCell
cell.updateUI(childThreads: childThreads)
cell.updateUI(childThreads: childThreads, inline: inline)
return cell
}
})
let initialImage = showStatusesAutomatically ? ConversationTableViewController.hidePostsImage : ConversationTableViewController.showPostsImage
visibilityBarButtonItem = UIBarButtonItem(image: initialImage, style: .plain, target: self, action: #selector(toggleVisibilityButtonPressed))
if #available(iOS 15.0, *) {
visibilityBarButtonItem = UIBarButtonItem(image: ConversationTableViewController.showPostsImage, style: .plain, target: self, action: #selector(toggleVisibilityButtonPressed))
visibilityBarButtonItem.isSelected = showStatusesAutomatically
} else {
let initialImage = showStatusesAutomatically ? ConversationTableViewController.hidePostsImage : ConversationTableViewController.showPostsImage
visibilityBarButtonItem = UIBarButtonItem(image: initialImage, style: .plain, target: self, action: #selector(toggleVisibilityButtonPressed))
}
navigationItem.rightBarButtonItem = visibilityBarButtonItem
// disable transparent background when scroll to top because it looks weird when items earlier in the thread load in
// (it remains transparent slightly too long, resulting in a flash of the content under the transparent bar)
let appearance = UINavigationBarAppearance()
appearance.configureWithDefaultBackground()
navigationItem.scrollEdgeAppearance = appearance
loadMainStatus()
Task {
await loadMainStatus()
}
}
private func loadMainStatus() {
@MainActor
private func loadMainStatus() async {
guard loadingState == .unloaded else { return }
if let mainStatus = mastodonController.persistentContainer.status(for: mainStatusID) {
self.mainStatusLoaded(mainStatus)
await mainStatusLoaded(mainStatus)
} else {
loadingState = .loadingMain
let request = Client.getStatus(id: mainStatusID)
mastodonController.run(request) { (response) in
switch response {
case let .success(status, _):
let viewContext = self.mastodonController.persistentContainer.viewContext
self.mastodonController.persistentContainer.addOrUpdate(status: status, context: viewContext) { (statusMO) in
self.mainStatusLoaded(statusMO)
}
case let .failure(error):
DispatchQueue.main.async {
self.loadingState = .unloaded
let config = ToastConfiguration(from: error, with: "Error Loading Status", in: self) { [weak self] (toast) in
toast.dismissToast(animated: true)
self?.loadMainStatus()
}
self.showToast(configuration: config, animated: true)
}
let req = Client.getStatus(id: mainStatusID)
do {
let (status, _) = try await mastodonController.run(req)
let statusMO = mastodonController.persistentContainer.addOrUpdateOnViewContext(status: status)
await mainStatusLoaded(statusMO)
} catch {
let error = error as! Client.Error
loadingState = .unloaded
let config = ToastConfiguration(from: error, with: "Error Loading Status", in: self) { [weak self] toast in
toast.dismissToast(animated: true)
await self?.loadMainStatus()
}
showToast(configuration: config, animated: true)
return
}
}
}
private func mainStatusLoaded(_ mainStatus: StatusMO) {
private func mainStatusLoaded(_ mainStatus: StatusMO) async {
let mainStatusItem = Item.status(id: mainStatusID, state: mainStatusState)
var snapshot = NSDiffableDataSourceSnapshot<Section, Item>()
@ -170,10 +179,11 @@ class ConversationTableViewController: EnhancedTableViewController {
loadingState = .loadedMain
loadContext(for: mainStatus)
await loadContext(for: mainStatus)
}
private func loadContext(for mainStatus: StatusMO) {
@MainActor
private func loadContext(for mainStatus: StatusMO) async {
guard loadingState == .loadedMain else { return }
loadingState = .loadingContext
@ -183,30 +193,24 @@ class ConversationTableViewController: EnhancedTableViewController {
// todo: it would be nice to cache these contexts
let request = Status.getContext(mainStatusID)
mastodonController.run(request) { response in
switch response {
case let .success(context, _):
let parentIDs = self.getDirectParents(inReplyTo: mainStatusInReplyToID, from: context.ancestors)
let parentStatuses = context.ancestors.filter { parentIDs.contains($0.id) }
do {
let (context, _) = try await mastodonController.run(request)
let parentIDs = self.getDirectParents(inReplyTo: mainStatusInReplyToID, from: context.ancestors)
let parentStatuses = context.ancestors.filter { parentIDs.contains($0.id) }
// todo: should this really be blindly adding all the descendants?
await mastodonController.persistentContainer.addAll(statuses: parentStatuses + context.descendants)
self.contextLoaded(mainStatus: mainStatus, context: context, parentIDs: parentIDs)
} catch {
let error = error as! Client.Error
self.loadingState = .loadedMain
// todo: should this really be blindly adding all the descendants?
self.mastodonController.persistentContainer.addAll(statuses: parentStatuses + context.descendants) {
DispatchQueue.main.async {
self.contextLoaded(mainStatus: mainStatus, context: context, parentIDs: parentIDs)
}
}
case let .failure(error):
DispatchQueue.main.async {
self.loadingState = .loadedMain
let config = ToastConfiguration(from: error, with: "Error Loading Content", in: self) { [weak self] (toast) in
toast.dismissToast(animated: true)
self?.loadContext(for: mainStatus)
}
self.showToast(configuration: config, animated: true)
}
let config = ToastConfiguration(from: error, with: "Error Loading Content", in: self) { [weak self] (toast) in
toast.dismissToast(animated: true)
await self?.loadContext(for: mainStatus)
}
self.showToast(configuration: config, animated: true)
}
}
@ -230,10 +234,19 @@ class ConversationTableViewController: EnhancedTableViewController {
}
self.dataSource.apply(snapshot, animatingDifferences: false) {
// ensure that the main status is on-screen after newly loaded statuses are added
let item: Item
let position: UITableView.ScrollPosition
if self.statusIDToScrollToOnLoad == self.mainStatusID {
item = mainStatusItem
position = .middle
} else {
item = Item.status(id: self.statusIDToScrollToOnLoad, state: .unknown)
position = .top
}
// ensure that the status is on-screen after newly loaded statuses are added
// todo: should this not happen if the user has already started scrolling (e.g. because the main status is very long)?
if let indexPath = self.dataSource.indexPath(for: mainStatusItem) {
self.tableView.scrollToRow(at: indexPath, at: .middle, animated: false)
if let indexPath = self.dataSource.indexPath(for: item) {
self.tableView.scrollToRow(at: indexPath, at: position, animated: false)
}
}
@ -314,8 +327,10 @@ class ConversationTableViewController: EnhancedTableViewController {
let sameAuthorStatuses = currentNode.children.filter({ $0.status.account.id == node.status.account.id })
if sameAuthorStatuses.count == 1 {
next = sameAuthorStatuses[0]
let nonSameAuthorChildren = currentNode.children.filter { $0.status.id != sameAuthorStatuses[0].status.id }
snapshot.appendItems([.expandThread(childThreads: nonSameAuthorChildren, inline: true)])
} else {
snapshot.appendItems([.expandThread(childThreads: currentNode.children)])
snapshot.appendItems([.expandThread(childThreads: currentNode.children, inline: false)])
break
}
}
@ -340,9 +355,11 @@ class ConversationTableViewController: EnhancedTableViewController {
// MARK: - Table view delegate
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
if case .expandThread = dataSource.itemIdentifier(for: indexPath),
if case let .expandThread(childThreads: childThreads, inline: _) = dataSource.itemIdentifier(for: indexPath),
case let .status(id: id, state: state) = dataSource.itemIdentifier(for: IndexPath(row: indexPath.row - 1, section: indexPath.section)) {
self.selected(status: id, state: state)
let conv = ConversationTableViewController(for: id, state: state, mastodonController: mastodonController)
conv.statusIDToScrollToOnLoad = childThreads.first!.status.id
show(conv)
} else {
super.tableView(tableView, didSelectRowAt: indexPath)
}
@ -399,14 +416,14 @@ extension ConversationTableViewController {
}
enum Item: Hashable {
case status(id: String, state: StatusState)
case expandThread(childThreads: [ConversationNode])
case expandThread(childThreads: [ConversationNode], inline: Bool)
static func == (lhs: Item, rhs: Item) -> Bool {
switch (lhs, rhs) {
case let (.status(id: a, state: _), .status(id: b, state: _)):
return a == b
case let (.expandThread(childThreads: a), .expandThread(childThreads: b)):
return zip(a, b).allSatisfy { $0.status.id == $1.status.id }
case let (.expandThread(childThreads: a, inline: aInline), .expandThread(childThreads: b, inline: bInline)):
return zip(a, b).allSatisfy { $0.status.id == $1.status.id } && aInline == bInline
default:
return false
}
@ -417,9 +434,10 @@ extension ConversationTableViewController {
case let .status(id: id, state: _):
hasher.combine("status")
hasher.combine(id)
case let .expandThread(childThreads: children):
case let .expandThread(childThreads: children, inline: inline):
hasher.combine("expandThread")
hasher.combine(children.map(\.status.id))
hasher.combine(inline)
}
}
}

View File

@ -10,34 +10,49 @@ import UIKit
class ExpandThreadTableViewCell: UITableViewCell {
@IBOutlet weak var stackViewLeadingConstraint: NSLayoutConstraint!
@IBOutlet weak var avatarContainerView: UIView!
@IBOutlet weak var avatarContainerWidthConstraint: NSLayoutConstraint!
@IBOutlet weak var replyCountLabel: UILabel!
var avatarImageViews: [UIImageView] = []
private var threadLinkView: UIView!
private var threadLinkViewFullHeightConstraint: NSLayoutConstraint!
private var threadLinkViewShortHeightConstraint: NSLayoutConstraint!
private var avatarImageViews: [UIImageView] = []
private var avatarRequests: [ImageCache.Request] = []
override func awakeFromNib() {
super.awakeFromNib()
let prevThreadLinkView = UIView()
prevThreadLinkView.translatesAutoresizingMaskIntoConstraints = false
prevThreadLinkView.backgroundColor = tintColor.withAlphaComponent(0.5)
prevThreadLinkView.layer.cornerRadius = 2.5
prevThreadLinkView.layer.maskedCorners = [.layerMinXMaxYCorner, .layerMaxXMaxYCorner]
contentView.addSubview(prevThreadLinkView)
threadLinkView = UIView()
threadLinkView.translatesAutoresizingMaskIntoConstraints = false
threadLinkView.backgroundColor = tintColor.withAlphaComponent(0.5)
threadLinkView.layer.cornerRadius = 2.5
addSubview(threadLinkView)
threadLinkViewFullHeightConstraint = threadLinkView.bottomAnchor.constraint(equalTo: bottomAnchor)
threadLinkViewShortHeightConstraint = threadLinkView.bottomAnchor.constraint(equalTo: avatarContainerView.topAnchor, constant: -2)
NSLayoutConstraint.activate([
prevThreadLinkView.widthAnchor.constraint(equalToConstant: 5),
prevThreadLinkView.centerXAnchor.constraint(equalTo: leadingAnchor, constant: 16 + 25),
prevThreadLinkView.topAnchor.constraint(equalTo: topAnchor),
prevThreadLinkView.bottomAnchor.constraint(equalTo: avatarContainerView.topAnchor, constant: -2),
threadLinkView.widthAnchor.constraint(equalToConstant: 5),
threadLinkView.centerXAnchor.constraint(equalTo: leadingAnchor, constant: 25 + 16 /* system spacing */),
threadLinkView.topAnchor.constraint(equalTo: topAnchor),
threadLinkViewFullHeightConstraint,
])
NotificationCenter.default.addObserver(self, selector: #selector(preferencesChanged), name: .preferencesChanged, object: nil)
}
func updateUI(childThreads: [ConversationNode]) {
let format = NSLocalizedString("expand threads count", comment: "expand conversation threads button label")
func updateUI(childThreads: [ConversationNode], inline: Bool) {
stackViewLeadingConstraint.constant = inline ? 50 + 4 : 0
threadLinkView.layer.maskedCorners = inline ? [] : [.layerMinXMaxYCorner, .layerMaxXMaxYCorner]
threadLinkViewFullHeightConstraint.isActive = inline
threadLinkViewShortHeightConstraint.isActive = !inline
let format: String
if inline {
format = NSLocalizedString("expand threads inline count", comment: "expnad converstaion threads inline button label")
} else {
format = NSLocalizedString("expand threads count", comment: "expand conversation threads button label")
}
replyCountLabel.text = String.localizedStringWithFormat(format, childThreads.count)
let accounts = childThreads.map(\.status.account).uniques().prefix(3)

View File

@ -1,9 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="17701" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES">
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="20037" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES">
<device id="retina6_1" orientation="portrait" appearance="light"/>
<dependencies>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="17703"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="20020"/>
<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"/>
@ -62,6 +62,7 @@
<outlet property="avatarContainerView" destination="eFB-F1-d3A" id="xGo-40-nn7"/>
<outlet property="avatarContainerWidthConstraint" destination="tiI-Rj-gjh" id="34n-ev-EKi"/>
<outlet property="replyCountLabel" destination="Dcm-ll-GeE" id="E4m-xk-DiQ"/>
<outlet property="stackViewLeadingConstraint" destination="iD5-Av-ORS" id="Try-cG-8uA"/>
</connections>
<point key="canvasLocation" x="132" y="132"/>
</tableViewCell>

View File

@ -123,6 +123,7 @@ extension DraftsTableViewController: UITableViewDragDelegate {
func tableView(_ tableView: UITableView, itemsForBeginning session: UIDragSession, at indexPath: IndexPath) -> [UIDragItem] {
let draft = self.draft(for: indexPath)
let activity = UserActivityManager.editDraftActivity(id: draft.id, accountID: account.id)
activity.displaysAuxiliaryScene = true
let provider = NSItemProvider(object: activity)
return [UIDragItem(itemProvider: provider)]
}

View File

@ -574,13 +574,17 @@ extension ExploreViewController: UICollectionViewDragDelegate {
let provider: NSItemProvider
switch item {
case .bookmarks:
provider = NSItemProvider(object: UserActivityManager.bookmarksActivity())
let activity = UserActivityManager.bookmarksActivity()
activity.displaysAuxiliaryScene = true
provider = NSItemProvider(object: activity)
case let .list(list):
guard let activity = UserActivityManager.showTimelineActivity(timeline: .list(id: list.id), accountID: accountID) else { return [] }
activity.displaysAuxiliaryScene = true
provider = NSItemProvider(object: activity)
case let .savedHashtag(hashtag):
provider = NSItemProvider(object: hashtag.url as NSURL)
if let activity = UserActivityManager.showTimelineActivity(timeline: .tag(hashtag: hashtag.name), accountID: accountID) {
activity.displaysAuxiliaryScene = true
provider.registerObject(activity, visibility: .all)
}
case let .savedInstance(url):

View File

@ -183,6 +183,7 @@ extension ProfileDirectoryViewController: UICollectionViewDragDelegate {
}
let provider = NSItemProvider(object: account.url as NSURL)
let activity = UserActivityManager.showProfileActivity(id: account.id, accountID: currentAccountID)
activity.displaysAuxiliaryScene = true
provider.registerObject(activity, visibility: .all)
return [UIDragItem(itemProvider: provider)]
}

View File

@ -90,6 +90,7 @@ class TrendingHashtagsViewController: EnhancedTableViewController {
}
let provider = NSItemProvider(object: hashtag.url as NSURL)
if let activity = UserActivityManager.showTimelineActivity(timeline: .tag(hashtag: hashtag.name), accountID: mastodonController.accountInfo!.id) {
activity.displaysAuxiliaryScene = true
provider.registerObject(activity, visibility: .all)
}
return [UIDragItem(itemProvider: provider)]

View File

@ -19,6 +19,11 @@ class MainSidebarViewController: UIViewController {
private weak var mastodonController: MastodonController!
weak var sidebarDelegate: MainSidebarViewControllerDelegate?
var onViewDidLoad: (() -> Void)? = nil {
willSet {
precondition(onViewDidLoad == nil)
}
}
private var collectionView: UICollectionView!
private var dataSource: UICollectionViewDiffableDataSource<Section, Item>!
@ -95,6 +100,8 @@ class MainSidebarViewController: UIViewController {
NotificationCenter.default.addObserver(self, selector: #selector(reloadSavedHashtags), name: .savedHashtagsChanged, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(reloadSavedInstances), name: .savedInstancesChanged, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(preferencesChanged), name: .preferencesChanged, object: nil)
onViewDidLoad?()
}
func select(item: Item, animated: Bool) {
@ -553,6 +560,7 @@ extension MainSidebarViewController: UICollectionViewDelegate {
collectionView.contextMenuInteraction?.menuAppearance == .rich {
return nil
}
activity.displaysAuxiliaryScene = true
return UIContextMenuConfiguration(identifier: nil, previewProvider: nil) { (_) in
return UIMenu(children: [
UIWindowScene.ActivationAction({ action in
@ -573,6 +581,7 @@ extension MainSidebarViewController: UICollectionViewDragDelegate {
return []
}
activity.displaysAuxiliaryScene = true
let provider = NSItemProvider(object: activity)
return [UIDragItem(itemProvider: provider)]
}

View File

@ -59,10 +59,15 @@ class MainSplitViewController: UISplitViewController {
switcher.itemOrientation = .iconsLeading
switcher.view.translatesAutoresizingMaskIntoConstraints = false
switcher.delegate = self
sidebar.view.addGestureRecognizer(switcher.createSwitcherGesture())
let tapRecognizer = UITapGestureRecognizer(target: self, action: #selector(sidebarTapped))
tapRecognizer.cancelsTouchesInView = false
sidebar.view.addGestureRecognizer(tapRecognizer)
// accessing .view unconditionally loads the view, which we don't want to happen
// because the sidebar view not being loaded is how we know not to transfer nav state
// in splitViewControllerDidCollapse on devices where the sidebar is never shown
sidebar.onViewDidLoad = { [unowned self] in
self.sidebar.view.addGestureRecognizer(switcher.createSwitcherGesture())
let tapRecognizer = UITapGestureRecognizer(target: self, action: #selector(sidebarTapped))
tapRecognizer.cancelsTouchesInView = false
self.sidebar.view.addGestureRecognizer(tapRecognizer)
}
}
tabBarViewController = MainTabBarViewController(mastodonController: mastodonController)

View File

@ -42,6 +42,8 @@ class MainTabBarViewController: UITabBarController, UITabBarControllerDelegate {
override func viewDidLoad() {
super.viewDidLoad()
stateRestorationLogger.info("MainTabBarViewController: viewDidLoad, selectedIndex=\(self.selectedIndex, privacy: .public)")
self.delegate = self
@ -75,6 +77,16 @@ class MainTabBarViewController: UITabBarController, UITabBarControllerDelegate {
tabBar.isSpringLoaded = true
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
stateRestorationLogger.info("MainTabBarViewController: viewWillAppear, selectedIndex=\(self.selectedIndex, privacy: .public)")
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
stateRestorationLogger.info("MainTabBarViewController: viewDidAppear, selectedIndex=\(self.selectedIndex, privacy: .public)")
}
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
@ -240,6 +252,7 @@ extension MainTabBarViewController: TuskerRootViewController {
self.selectedIndex = tab.rawValue
}
} else {
stateRestorationLogger.info("MainTabBarViewController: selecting \(String(describing: tab), privacy: .public)")
selectedIndex = tab.rawValue
}
}

View File

@ -10,13 +10,25 @@ import Foundation
extension NSUserActivity {
var displaysAuxiliaryScene: Bool {
get {
(userInfo?["displaysAuxiliaryScene"] as? Bool) ?? false
}
set {
if userInfo == nil {
userInfo = [:]
}
userInfo!["displaysAuxiliaryScene"] = newValue
}
}
convenience init(type: UserActivityType) {
self.init(activityType: type.rawValue)
}
func handleResume() -> Bool {
func handleResume(manager: UserActivityManager) -> Bool {
guard let type = UserActivityType(rawValue: activityType) else { return false }
type.handle(self)
type.handle(manager)(self)
return true
}

View File

@ -9,25 +9,32 @@
import UIKit
import Intents
import Pachyderm
import OSLog
private let logger = Logger(subsystem: Bundle.main.bundleIdentifier!, category: "UserActivityManager")
class UserActivityManager {
private let scene: UIWindowScene
init(scene: UIWindowScene) {
self.scene = scene
}
// MARK: - Utils
private static let encoder = PropertyListEncoder()
private static let decoder = PropertyListDecoder()
private static var mastodonController: MastodonController {
let scene = UIApplication.shared.activeOrBackgroundScene!
return scene.session.mastodonController!
private var mastodonController: MastodonController {
scene.session.mastodonController!
}
private static func getMainViewController() -> TuskerRootViewController {
let scene = UIApplication.shared.connectedScenes.compactMap { $0 as? UIWindowScene }.first!
let window = scene.windows.first { $0.isKeyWindow }!
private func getMainViewController() -> TuskerRootViewController {
let window = scene.windows.first { $0.isKeyWindow } ?? scene.windows.first!
return window.rootViewController as! TuskerRootViewController
}
private static func present(_ vc: UIViewController, animated: Bool = true) {
private func present(_ vc: UIViewController, animated: Bool = true) {
getMainViewController().present(vc, animated: animated)
}
@ -66,7 +73,7 @@ class UserActivityManager {
return activity
}
static func handleNewPost(activity: NSUserActivity) {
func handleNewPost(activity: NSUserActivity) {
// TODO: check not currently showing compose screen
let mentioning = activity.userInfo?["mentioning"] as? String
let draft = mastodonController.createDraft(mentioningAcct: mentioning)
@ -111,14 +118,14 @@ class UserActivityManager {
return activity
}
static func handleCheckNotifications(activity: NSUserActivity) {
func handleCheckNotifications(activity: NSUserActivity) {
let mainViewController = getMainViewController()
mainViewController.select(tab: .notifications)
if let navigationController = mainViewController.getTabController(tab: .notifications) as? UINavigationController,
let notificationsPageController = navigationController.viewControllers.first as? NotificationsPageViewController {
let notificationsPageController = navigationController.viewControllers.first as? NotificationsPageViewController {
navigationController.popToRootViewController(animated: false)
notificationsPageController.loadViewIfNeeded()
notificationsPageController.selectMode(getNotificationsMode(from: activity) ?? Preferences.shared.defaultNotificationsMode)
notificationsPageController.selectMode(Self.getNotificationsMode(from: activity) ?? Preferences.shared.defaultNotificationsMode)
}
}
@ -168,11 +175,11 @@ class UserActivityManager {
let data = activity.userInfo?["timelineData"] as? Data else {
return nil
}
return try? decoder.decode(Timeline.self, from: data)
return try? UserActivityManager.decoder.decode(Timeline.self, from: data)
}
static func handleShowTimeline(activity: NSUserActivity) {
guard let timeline = getTimeline(from: activity) else { return }
func handleShowTimeline(activity: NSUserActivity) {
guard let timeline = Self.getTimeline(from: activity) else { return }
let mainViewController = getMainViewController()
mainViewController.select(tab: .timelines)
@ -228,7 +235,7 @@ class UserActivityManager {
return activity
}
static func handleSearch(activity: NSUserActivity) {
func handleSearch(activity: NSUserActivity) {
let mainViewController = getMainViewController()
mainViewController.select(tab: .explore)
if let navigationController = mainViewController.getTabController(tab: .explore) as? UINavigationController,
@ -247,7 +254,7 @@ class UserActivityManager {
return activity
}
static func handleBookmarks(activity: NSUserActivity) {
func handleBookmarks(activity: NSUserActivity) {
let mainViewController = getMainViewController()
mainViewController.select(tab: .explore)
if let navigationController = mainViewController.getTabController(tab: .explore) as? UINavigationController {
@ -265,7 +272,7 @@ class UserActivityManager {
return activity
}
static func handleMyProfile(activity: NSUserActivity) {
func handleMyProfile(activity: NSUserActivity) {
let mainViewController = getMainViewController()
mainViewController.select(tab: .myProfile)
}

View File

@ -21,7 +21,7 @@ enum UserActivityType: String {
}
extension UserActivityType {
var handle: (NSUserActivity) -> Void {
var handle: (UserActivityManager) -> (NSUserActivity) -> Void {
switch self {
case .mainScene:
fatalError("cannot handle main scene activity")

View File

@ -115,6 +115,7 @@ extension AccountTableViewCell: DraggableTableViewCell {
}
let provider = NSItemProvider(object: account.url as NSURL)
let activity = UserActivityManager.showProfileActivity(id: account.id, accountID: currentAccountID)
activity.displaysAuxiliaryScene = true
provider.registerObject(activity, visibility: .all)
return [UIDragItem(itemProvider: provider)]
}

View File

@ -223,6 +223,7 @@ extension FollowNotificationGroupTableViewCell: DraggableTableViewCell {
let notification = group.notifications[0]
let provider = NSItemProvider(object: notification.account.url as NSURL)
let activity = UserActivityManager.showProfileActivity(id: notification.account.id, accountID: mastodonController.accountInfo!.id)
activity.displaysAuxiliaryScene = true
provider.registerObject(activity, visibility: .all)
return [UIDragItem(itemProvider: provider)]
}

View File

@ -206,6 +206,7 @@ extension FollowRequestNotificationTableViewCell: DraggableTableViewCell {
func dragItemsForBeginning(session: UIDragSession) -> [UIDragItem] {
let provider = NSItemProvider(object: account.url as NSURL)
let activity = UserActivityManager.showProfileActivity(id: account.id, accountID: mastodonController.accountInfo!.id)
activity.displaysAuxiliaryScene = true
provider.registerObject(activity, visibility: .all)
return [UIDragItem(itemProvider: provider)]
}

View File

@ -21,6 +21,7 @@ class StatusPollView: UIView {
}()
weak var mastodonController: MastodonController!
weak var toastableViewController: ToastableViewController?
private var statusID: String!
private(set) var poll: Poll?
@ -151,7 +152,14 @@ class StatusPollView: UIView {
mastodonController.run(request) { (response) in
switch response {
case let .failure(error):
fatalError("error voting in poll: \(error)")
DispatchQueue.main.async {
self.updateUI(status: self.mastodonController.persistentContainer.status(for: self.statusID)!, poll: self.poll)
if let toastable = self.toastableViewController {
let config = ToastConfiguration(from: error, with: "Error Voting", in: toastable, retryAction: nil)
toastable.showToast(configuration: config, animated: true)
}
}
case let .success(poll, _):
let container = self.mastodonController.persistentContainer

View File

@ -211,6 +211,7 @@ class BaseStatusTableViewCell: UITableViewCell {
pollView.isHidden = status.poll == nil
pollView.mastodonController = mastodonController
pollView.toastableViewController = delegate?.toastableViewController
pollView.updateUI(status: status, poll: status.poll)
}
@ -530,6 +531,7 @@ extension BaseStatusTableViewCell: UIDragInteractionDelegate {
}
let provider = NSItemProvider(object: account.url as NSURL)
let activity = UserActivityManager.showProfileActivity(id: accountID, accountID: currentAccountID)
activity.displaysAuxiliaryScene = true
provider.registerObject(activity, visibility: .all)
return [UIDragItem(itemProvider: provider)]
}

View File

@ -356,6 +356,7 @@ extension TimelineStatusTableViewCell: DraggableTableViewCell {
}
let provider = NSItemProvider(object: status.url! as NSURL)
let activity = UserActivityManager.showConversationActivity(mainStatusID: status.id, accountID: accountID)
activity.displaysAuxiliaryScene = true
provider.registerObject(activity, visibility: .all)
return [UIDragItem(itemProvider: provider)]
}

View File

@ -57,6 +57,14 @@ extension ToastConfiguration {
viewController.present(reporter, animated: true)
}
}
init(from error: Client.Error, with title: String, in viewController: UIViewController, retryAction: @escaping @MainActor (ToastView) async -> Void) {
self.init(from: error, with: title, in: viewController) { toast in
Task {
await retryAction(toast)
}
}
}
}
fileprivate extension Client.Error {

View File

@ -61,6 +61,22 @@
<string>%u replies</string>
</dict>
</dict>
<key>expand threads inline count</key>
<dict>
<key>NSStringLocalizedFormatKey</key>
<string>%#@replies@</string>
<key>replies</key>
<dict>
<key>NSStringFormatSpecTypeKey</key>
<string>NSStringPluralRuleType</string>
<key>NSStringFormatValueTypeKey</key>
<string>u</string>
<key>one</key>
<string>1 more reply</string>
<key>other</key>
<string>%u more replies</string>
</dict>
</dict>
<key>favorites count</key>
<dict>
<key>NSStringLocalizedFormatKey</key>