Compare commits
7 Commits
d3187ce2c4
...
cc0da2ec54
Author | SHA1 | Date |
---|---|---|
Shadowfacts | cc0da2ec54 | |
Shadowfacts | a2868739c2 | |
Shadowfacts | 2f75510889 | |
Shadowfacts | 46332cd1b9 | |
Shadowfacts | 21e9ca990d | |
Shadowfacts | 1a02319894 | |
Shadowfacts | 4a95ccccdb |
|
@ -9,6 +9,9 @@
|
||||||
import UIKit
|
import UIKit
|
||||||
import CrashReporter
|
import CrashReporter
|
||||||
import CoreData
|
import CoreData
|
||||||
|
import OSLog
|
||||||
|
|
||||||
|
let stateRestorationLogger = Logger(subsystem: Bundle.main.bundleIdentifier!, category: "StateRestoration")
|
||||||
|
|
||||||
@UIApplicationMain
|
@UIApplicationMain
|
||||||
class AppDelegate: UIResponder, UIApplicationDelegate {
|
class AppDelegate: UIResponder, UIApplicationDelegate {
|
||||||
|
@ -93,7 +96,12 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
|
||||||
.bookmarks,
|
.bookmarks,
|
||||||
.myProfile,
|
.myProfile,
|
||||||
.showProfile:
|
.showProfile:
|
||||||
return "auxiliary"
|
if activity.displaysAuxiliaryScene {
|
||||||
|
stateRestorationLogger.info("Using auxiliary scene for \(type.rawValue, privacy: .public)")
|
||||||
|
return "auxiliary"
|
||||||
|
} else {
|
||||||
|
return "main-scene"
|
||||||
|
}
|
||||||
|
|
||||||
case .newPost:
|
case .newPost:
|
||||||
return "compose"
|
return "compose"
|
||||||
|
|
|
@ -88,6 +88,16 @@ class MastodonCachePersistentStore: NSPersistentContainer {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@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) {
|
func addAll(statuses: [Status], completion: (() -> Void)? = nil) {
|
||||||
backgroundContext.perform {
|
backgroundContext.perform {
|
||||||
statuses.forEach { self.upsert(status: $0, context: self.backgroundContext) }
|
statuses.forEach { self.upsert(status: $0, context: self.backgroundContext) }
|
||||||
|
@ -99,6 +109,14 @@ class MastodonCachePersistentStore: NSPersistentContainer {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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? {
|
func account(for id: String, in context: NSManagedObjectContext? = nil) -> AccountMO? {
|
||||||
let context = context ?? viewContext
|
let context = context ?? viewContext
|
||||||
let request: NSFetchRequest<AccountMO> = AccountMO.fetchRequest()
|
let request: NSFetchRequest<AccountMO> = AccountMO.fetchRequest()
|
||||||
|
|
|
@ -28,6 +28,7 @@ class MainSceneDelegate: UIResponder, UIWindowSceneDelegate {
|
||||||
if let activity = connectionOptions.userActivities.first ?? session.stateRestorationActivity {
|
if let activity = connectionOptions.userActivities.first ?? session.stateRestorationActivity {
|
||||||
launchActivity = activity
|
launchActivity = activity
|
||||||
}
|
}
|
||||||
|
stateRestorationLogger.info("MainSceneDelegate.launchActivity = \(self.launchActivity?.activityType ?? "nil", privacy: .public)")
|
||||||
|
|
||||||
window = UIWindow(windowScene: windowScene)
|
window = UIWindow(windowScene: windowScene)
|
||||||
|
|
||||||
|
@ -69,7 +70,8 @@ class MainSceneDelegate: UIResponder, UIWindowSceneDelegate {
|
||||||
}
|
}
|
||||||
|
|
||||||
func scene(_ scene: UIScene, continue userActivity: NSUserActivity) {
|
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) {
|
func windowScene(_ windowScene: UIWindowScene, performActionFor shortcutItem: UIApplicationShortcutItem, completionHandler: @escaping (Bool) -> Void) {
|
||||||
|
@ -169,6 +171,11 @@ class MainSceneDelegate: UIResponder, UIWindowSceneDelegate {
|
||||||
}
|
}
|
||||||
|
|
||||||
activateAccount(account, animated: false)
|
activateAccount(account, animated: false)
|
||||||
|
|
||||||
|
if let activity = launchActivity,
|
||||||
|
activity.activityType != UserActivityType.mainScene.rawValue {
|
||||||
|
_ = activity.handleResume(manager: UserActivityManager(scene: window!.windowScene!))
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
window!.rootViewController = createOnboardingUI()
|
window!.rootViewController = createOnboardingUI()
|
||||||
}
|
}
|
||||||
|
|
|
@ -31,6 +31,7 @@ class ConversationTableViewController: EnhancedTableViewController {
|
||||||
|
|
||||||
let mainStatusID: String
|
let mainStatusID: String
|
||||||
let mainStatusState: StatusState
|
let mainStatusState: StatusState
|
||||||
|
var statusIDToScrollToOnLoad: String
|
||||||
private(set) var dataSource: UITableViewDiffableDataSource<Section, Item>!
|
private(set) var dataSource: UITableViewDiffableDataSource<Section, Item>!
|
||||||
|
|
||||||
var showStatusesAutomatically = false
|
var showStatusesAutomatically = false
|
||||||
|
@ -41,6 +42,7 @@ class ConversationTableViewController: EnhancedTableViewController {
|
||||||
init(for mainStatusID: String, state: StatusState = .unknown, mastodonController: MastodonController) {
|
init(for mainStatusID: String, state: StatusState = .unknown, mastodonController: MastodonController) {
|
||||||
self.mainStatusID = mainStatusID
|
self.mainStatusID = mainStatusID
|
||||||
self.mainStatusState = state
|
self.mainStatusState = state
|
||||||
|
self.statusIDToScrollToOnLoad = mainStatusID
|
||||||
self.mastodonController = mastodonController
|
self.mastodonController = mastodonController
|
||||||
|
|
||||||
super.init(style: .plain)
|
super.init(style: .plain)
|
||||||
|
@ -115,52 +117,59 @@ class ConversationTableViewController: EnhancedTableViewController {
|
||||||
|
|
||||||
return cell
|
return cell
|
||||||
|
|
||||||
case let .expandThread(childThreads: childThreads):
|
case let .expandThread(childThreads: childThreads, inline: inline):
|
||||||
let cell = tableView.dequeueReusableCell(withIdentifier: "expandThreadCell", for: indexPath) as! ExpandThreadTableViewCell
|
let cell = tableView.dequeueReusableCell(withIdentifier: "expandThreadCell", for: indexPath) as! ExpandThreadTableViewCell
|
||||||
cell.updateUI(childThreads: childThreads)
|
cell.updateUI(childThreads: childThreads, inline: inline)
|
||||||
return cell
|
return cell
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
let initialImage = showStatusesAutomatically ? ConversationTableViewController.hidePostsImage : ConversationTableViewController.showPostsImage
|
if #available(iOS 15.0, *) {
|
||||||
visibilityBarButtonItem = UIBarButtonItem(image: initialImage, style: .plain, target: self, action: #selector(toggleVisibilityButtonPressed))
|
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
|
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 }
|
guard loadingState == .unloaded else { return }
|
||||||
|
|
||||||
if let mainStatus = mastodonController.persistentContainer.status(for: mainStatusID) {
|
if let mainStatus = mastodonController.persistentContainer.status(for: mainStatusID) {
|
||||||
self.mainStatusLoaded(mainStatus)
|
await mainStatusLoaded(mainStatus)
|
||||||
} else {
|
} else {
|
||||||
loadingState = .loadingMain
|
loadingState = .loadingMain
|
||||||
let request = Client.getStatus(id: mainStatusID)
|
let req = Client.getStatus(id: mainStatusID)
|
||||||
mastodonController.run(request) { (response) in
|
do {
|
||||||
switch response {
|
let (status, _) = try await mastodonController.run(req)
|
||||||
case let .success(status, _):
|
let statusMO = mastodonController.persistentContainer.addOrUpdateOnViewContext(status: status)
|
||||||
let viewContext = self.mastodonController.persistentContainer.viewContext
|
await mainStatusLoaded(statusMO)
|
||||||
self.mastodonController.persistentContainer.addOrUpdate(status: status, context: viewContext) { (statusMO) in
|
} catch {
|
||||||
self.mainStatusLoaded(statusMO)
|
let error = error as! Client.Error
|
||||||
}
|
loadingState = .unloaded
|
||||||
|
let config = ToastConfiguration(from: error, with: "Error Loading Status", in: self) { [weak self] toast in
|
||||||
case let .failure(error):
|
toast.dismissToast(animated: true)
|
||||||
DispatchQueue.main.async {
|
await self?.loadMainStatus()
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
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)
|
let mainStatusItem = Item.status(id: mainStatusID, state: mainStatusState)
|
||||||
|
|
||||||
var snapshot = NSDiffableDataSourceSnapshot<Section, Item>()
|
var snapshot = NSDiffableDataSourceSnapshot<Section, Item>()
|
||||||
|
@ -170,10 +179,11 @@ class ConversationTableViewController: EnhancedTableViewController {
|
||||||
|
|
||||||
loadingState = .loadedMain
|
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 }
|
guard loadingState == .loadedMain else { return }
|
||||||
|
|
||||||
loadingState = .loadingContext
|
loadingState = .loadingContext
|
||||||
|
@ -183,30 +193,24 @@ class ConversationTableViewController: EnhancedTableViewController {
|
||||||
|
|
||||||
// todo: it would be nice to cache these contexts
|
// todo: it would be nice to cache these contexts
|
||||||
let request = Status.getContext(mainStatusID)
|
let request = Status.getContext(mainStatusID)
|
||||||
mastodonController.run(request) { response in
|
do {
|
||||||
switch response {
|
let (context, _) = try await mastodonController.run(request)
|
||||||
case let .success(context, _):
|
let parentIDs = self.getDirectParents(inReplyTo: mainStatusInReplyToID, from: context.ancestors)
|
||||||
let parentIDs = self.getDirectParents(inReplyTo: mainStatusInReplyToID, from: context.ancestors)
|
let parentStatuses = context.ancestors.filter { parentIDs.contains($0.id) }
|
||||||
let parentStatuses = context.ancestors.filter { parentIDs.contains($0.id) }
|
|
||||||
|
|
||||||
// todo: should this really be blindly adding all the descendants?
|
// todo: should this really be blindly adding all the descendants?
|
||||||
self.mastodonController.persistentContainer.addAll(statuses: parentStatuses + context.descendants) {
|
await mastodonController.persistentContainer.addAll(statuses: parentStatuses + context.descendants)
|
||||||
DispatchQueue.main.async {
|
self.contextLoaded(mainStatus: mainStatus, context: context, parentIDs: parentIDs)
|
||||||
self.contextLoaded(mainStatus: mainStatus, context: context, parentIDs: parentIDs)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
case let .failure(error):
|
} catch {
|
||||||
DispatchQueue.main.async {
|
let error = error as! Client.Error
|
||||||
self.loadingState = .loadedMain
|
self.loadingState = .loadedMain
|
||||||
|
|
||||||
let config = ToastConfiguration(from: error, with: "Error Loading Content", in: self) { [weak self] (toast) in
|
let config = ToastConfiguration(from: error, with: "Error Loading Content", in: self) { [weak self] (toast) in
|
||||||
toast.dismissToast(animated: true)
|
toast.dismissToast(animated: true)
|
||||||
self?.loadContext(for: mainStatus)
|
await self?.loadContext(for: mainStatus)
|
||||||
}
|
|
||||||
self.showToast(configuration: config, animated: true)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
self.showToast(configuration: config, animated: true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -230,10 +234,19 @@ class ConversationTableViewController: EnhancedTableViewController {
|
||||||
}
|
}
|
||||||
|
|
||||||
self.dataSource.apply(snapshot, animatingDifferences: false) {
|
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)?
|
// 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) {
|
if let indexPath = self.dataSource.indexPath(for: item) {
|
||||||
self.tableView.scrollToRow(at: indexPath, at: .middle, animated: false)
|
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 })
|
let sameAuthorStatuses = currentNode.children.filter({ $0.status.account.id == node.status.account.id })
|
||||||
if sameAuthorStatuses.count == 1 {
|
if sameAuthorStatuses.count == 1 {
|
||||||
next = sameAuthorStatuses[0]
|
next = sameAuthorStatuses[0]
|
||||||
|
let nonSameAuthorChildren = currentNode.children.filter { $0.status.id != sameAuthorStatuses[0].status.id }
|
||||||
|
snapshot.appendItems([.expandThread(childThreads: nonSameAuthorChildren, inline: true)])
|
||||||
} else {
|
} else {
|
||||||
snapshot.appendItems([.expandThread(childThreads: currentNode.children)])
|
snapshot.appendItems([.expandThread(childThreads: currentNode.children, inline: false)])
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -340,9 +355,11 @@ class ConversationTableViewController: EnhancedTableViewController {
|
||||||
// MARK: - Table view delegate
|
// MARK: - Table view delegate
|
||||||
|
|
||||||
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
|
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)) {
|
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 {
|
} else {
|
||||||
super.tableView(tableView, didSelectRowAt: indexPath)
|
super.tableView(tableView, didSelectRowAt: indexPath)
|
||||||
}
|
}
|
||||||
|
@ -399,14 +416,14 @@ extension ConversationTableViewController {
|
||||||
}
|
}
|
||||||
enum Item: Hashable {
|
enum Item: Hashable {
|
||||||
case status(id: String, state: StatusState)
|
case status(id: String, state: StatusState)
|
||||||
case expandThread(childThreads: [ConversationNode])
|
case expandThread(childThreads: [ConversationNode], inline: Bool)
|
||||||
|
|
||||||
static func == (lhs: Item, rhs: Item) -> Bool {
|
static func == (lhs: Item, rhs: Item) -> Bool {
|
||||||
switch (lhs, rhs) {
|
switch (lhs, rhs) {
|
||||||
case let (.status(id: a, state: _), .status(id: b, state: _)):
|
case let (.status(id: a, state: _), .status(id: b, state: _)):
|
||||||
return a == b
|
return a == b
|
||||||
case let (.expandThread(childThreads: a), .expandThread(childThreads: b)):
|
case let (.expandThread(childThreads: a, inline: aInline), .expandThread(childThreads: b, inline: bInline)):
|
||||||
return zip(a, b).allSatisfy { $0.status.id == $1.status.id }
|
return zip(a, b).allSatisfy { $0.status.id == $1.status.id } && aInline == bInline
|
||||||
default:
|
default:
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
@ -417,9 +434,10 @@ extension ConversationTableViewController {
|
||||||
case let .status(id: id, state: _):
|
case let .status(id: id, state: _):
|
||||||
hasher.combine("status")
|
hasher.combine("status")
|
||||||
hasher.combine(id)
|
hasher.combine(id)
|
||||||
case let .expandThread(childThreads: children):
|
case let .expandThread(childThreads: children, inline: inline):
|
||||||
hasher.combine("expandThread")
|
hasher.combine("expandThread")
|
||||||
hasher.combine(children.map(\.status.id))
|
hasher.combine(children.map(\.status.id))
|
||||||
|
hasher.combine(inline)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,34 +10,49 @@ import UIKit
|
||||||
|
|
||||||
class ExpandThreadTableViewCell: UITableViewCell {
|
class ExpandThreadTableViewCell: UITableViewCell {
|
||||||
|
|
||||||
|
@IBOutlet weak var stackViewLeadingConstraint: NSLayoutConstraint!
|
||||||
@IBOutlet weak var avatarContainerView: UIView!
|
@IBOutlet weak var avatarContainerView: UIView!
|
||||||
@IBOutlet weak var avatarContainerWidthConstraint: NSLayoutConstraint!
|
@IBOutlet weak var avatarContainerWidthConstraint: NSLayoutConstraint!
|
||||||
@IBOutlet weak var replyCountLabel: UILabel!
|
@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] = []
|
private var avatarRequests: [ImageCache.Request] = []
|
||||||
|
|
||||||
override func awakeFromNib() {
|
override func awakeFromNib() {
|
||||||
super.awakeFromNib()
|
super.awakeFromNib()
|
||||||
|
|
||||||
let prevThreadLinkView = UIView()
|
threadLinkView = UIView()
|
||||||
prevThreadLinkView.translatesAutoresizingMaskIntoConstraints = false
|
threadLinkView.translatesAutoresizingMaskIntoConstraints = false
|
||||||
prevThreadLinkView.backgroundColor = tintColor.withAlphaComponent(0.5)
|
threadLinkView.backgroundColor = tintColor.withAlphaComponent(0.5)
|
||||||
prevThreadLinkView.layer.cornerRadius = 2.5
|
threadLinkView.layer.cornerRadius = 2.5
|
||||||
prevThreadLinkView.layer.maskedCorners = [.layerMinXMaxYCorner, .layerMaxXMaxYCorner]
|
addSubview(threadLinkView)
|
||||||
contentView.addSubview(prevThreadLinkView)
|
threadLinkViewFullHeightConstraint = threadLinkView.bottomAnchor.constraint(equalTo: bottomAnchor)
|
||||||
|
threadLinkViewShortHeightConstraint = threadLinkView.bottomAnchor.constraint(equalTo: avatarContainerView.topAnchor, constant: -2)
|
||||||
NSLayoutConstraint.activate([
|
NSLayoutConstraint.activate([
|
||||||
prevThreadLinkView.widthAnchor.constraint(equalToConstant: 5),
|
threadLinkView.widthAnchor.constraint(equalToConstant: 5),
|
||||||
prevThreadLinkView.centerXAnchor.constraint(equalTo: leadingAnchor, constant: 16 + 25),
|
threadLinkView.centerXAnchor.constraint(equalTo: leadingAnchor, constant: 25 + 16 /* system spacing */),
|
||||||
prevThreadLinkView.topAnchor.constraint(equalTo: topAnchor),
|
threadLinkView.topAnchor.constraint(equalTo: topAnchor),
|
||||||
prevThreadLinkView.bottomAnchor.constraint(equalTo: avatarContainerView.topAnchor, constant: -2),
|
threadLinkViewFullHeightConstraint,
|
||||||
])
|
])
|
||||||
|
|
||||||
NotificationCenter.default.addObserver(self, selector: #selector(preferencesChanged), name: .preferencesChanged, object: nil)
|
NotificationCenter.default.addObserver(self, selector: #selector(preferencesChanged), name: .preferencesChanged, object: nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
func updateUI(childThreads: [ConversationNode]) {
|
func updateUI(childThreads: [ConversationNode], inline: Bool) {
|
||||||
let format = NSLocalizedString("expand threads count", comment: "expand conversation threads button label")
|
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)
|
replyCountLabel.text = String.localizedStringWithFormat(format, childThreads.count)
|
||||||
|
|
||||||
let accounts = childThreads.map(\.status.account).uniques().prefix(3)
|
let accounts = childThreads.map(\.status.account).uniques().prefix(3)
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?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"/>
|
<device id="retina6_1" orientation="portrait" appearance="light"/>
|
||||||
<dependencies>
|
<dependencies>
|
||||||
<deployment identifier="iOS"/>
|
<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="Safe area layout guides" minToolsVersion="9.0"/>
|
||||||
<capability name="System colors in document resources" minToolsVersion="11.0"/>
|
<capability name="System colors in document resources" minToolsVersion="11.0"/>
|
||||||
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.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="avatarContainerView" destination="eFB-F1-d3A" id="xGo-40-nn7"/>
|
||||||
<outlet property="avatarContainerWidthConstraint" destination="tiI-Rj-gjh" id="34n-ev-EKi"/>
|
<outlet property="avatarContainerWidthConstraint" destination="tiI-Rj-gjh" id="34n-ev-EKi"/>
|
||||||
<outlet property="replyCountLabel" destination="Dcm-ll-GeE" id="E4m-xk-DiQ"/>
|
<outlet property="replyCountLabel" destination="Dcm-ll-GeE" id="E4m-xk-DiQ"/>
|
||||||
|
<outlet property="stackViewLeadingConstraint" destination="iD5-Av-ORS" id="Try-cG-8uA"/>
|
||||||
</connections>
|
</connections>
|
||||||
<point key="canvasLocation" x="132" y="132"/>
|
<point key="canvasLocation" x="132" y="132"/>
|
||||||
</tableViewCell>
|
</tableViewCell>
|
||||||
|
|
|
@ -123,6 +123,7 @@ extension DraftsTableViewController: UITableViewDragDelegate {
|
||||||
func tableView(_ tableView: UITableView, itemsForBeginning session: UIDragSession, at indexPath: IndexPath) -> [UIDragItem] {
|
func tableView(_ tableView: UITableView, itemsForBeginning session: UIDragSession, at indexPath: IndexPath) -> [UIDragItem] {
|
||||||
let draft = self.draft(for: indexPath)
|
let draft = self.draft(for: indexPath)
|
||||||
let activity = UserActivityManager.editDraftActivity(id: draft.id, accountID: account.id)
|
let activity = UserActivityManager.editDraftActivity(id: draft.id, accountID: account.id)
|
||||||
|
activity.displaysAuxiliaryScene = true
|
||||||
let provider = NSItemProvider(object: activity)
|
let provider = NSItemProvider(object: activity)
|
||||||
return [UIDragItem(itemProvider: provider)]
|
return [UIDragItem(itemProvider: provider)]
|
||||||
}
|
}
|
||||||
|
|
|
@ -574,13 +574,17 @@ extension ExploreViewController: UICollectionViewDragDelegate {
|
||||||
let provider: NSItemProvider
|
let provider: NSItemProvider
|
||||||
switch item {
|
switch item {
|
||||||
case .bookmarks:
|
case .bookmarks:
|
||||||
provider = NSItemProvider(object: UserActivityManager.bookmarksActivity())
|
let activity = UserActivityManager.bookmarksActivity()
|
||||||
|
activity.displaysAuxiliaryScene = true
|
||||||
|
provider = NSItemProvider(object: activity)
|
||||||
case let .list(list):
|
case let .list(list):
|
||||||
guard let activity = UserActivityManager.showTimelineActivity(timeline: .list(id: list.id), accountID: accountID) else { return [] }
|
guard let activity = UserActivityManager.showTimelineActivity(timeline: .list(id: list.id), accountID: accountID) else { return [] }
|
||||||
|
activity.displaysAuxiliaryScene = true
|
||||||
provider = NSItemProvider(object: activity)
|
provider = NSItemProvider(object: activity)
|
||||||
case let .savedHashtag(hashtag):
|
case let .savedHashtag(hashtag):
|
||||||
provider = NSItemProvider(object: hashtag.url as NSURL)
|
provider = NSItemProvider(object: hashtag.url as NSURL)
|
||||||
if let activity = UserActivityManager.showTimelineActivity(timeline: .tag(hashtag: hashtag.name), accountID: accountID) {
|
if let activity = UserActivityManager.showTimelineActivity(timeline: .tag(hashtag: hashtag.name), accountID: accountID) {
|
||||||
|
activity.displaysAuxiliaryScene = true
|
||||||
provider.registerObject(activity, visibility: .all)
|
provider.registerObject(activity, visibility: .all)
|
||||||
}
|
}
|
||||||
case let .savedInstance(url):
|
case let .savedInstance(url):
|
||||||
|
|
|
@ -183,6 +183,7 @@ extension ProfileDirectoryViewController: UICollectionViewDragDelegate {
|
||||||
}
|
}
|
||||||
let provider = NSItemProvider(object: account.url as NSURL)
|
let provider = NSItemProvider(object: account.url as NSURL)
|
||||||
let activity = UserActivityManager.showProfileActivity(id: account.id, accountID: currentAccountID)
|
let activity = UserActivityManager.showProfileActivity(id: account.id, accountID: currentAccountID)
|
||||||
|
activity.displaysAuxiliaryScene = true
|
||||||
provider.registerObject(activity, visibility: .all)
|
provider.registerObject(activity, visibility: .all)
|
||||||
return [UIDragItem(itemProvider: provider)]
|
return [UIDragItem(itemProvider: provider)]
|
||||||
}
|
}
|
||||||
|
|
|
@ -90,6 +90,7 @@ class TrendingHashtagsViewController: EnhancedTableViewController {
|
||||||
}
|
}
|
||||||
let provider = NSItemProvider(object: hashtag.url as NSURL)
|
let provider = NSItemProvider(object: hashtag.url as NSURL)
|
||||||
if let activity = UserActivityManager.showTimelineActivity(timeline: .tag(hashtag: hashtag.name), accountID: mastodonController.accountInfo!.id) {
|
if let activity = UserActivityManager.showTimelineActivity(timeline: .tag(hashtag: hashtag.name), accountID: mastodonController.accountInfo!.id) {
|
||||||
|
activity.displaysAuxiliaryScene = true
|
||||||
provider.registerObject(activity, visibility: .all)
|
provider.registerObject(activity, visibility: .all)
|
||||||
}
|
}
|
||||||
return [UIDragItem(itemProvider: provider)]
|
return [UIDragItem(itemProvider: provider)]
|
||||||
|
|
|
@ -19,6 +19,11 @@ class MainSidebarViewController: UIViewController {
|
||||||
private weak var mastodonController: MastodonController!
|
private weak var mastodonController: MastodonController!
|
||||||
|
|
||||||
weak var sidebarDelegate: MainSidebarViewControllerDelegate?
|
weak var sidebarDelegate: MainSidebarViewControllerDelegate?
|
||||||
|
var onViewDidLoad: (() -> Void)? = nil {
|
||||||
|
willSet {
|
||||||
|
precondition(onViewDidLoad == nil)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private var collectionView: UICollectionView!
|
private var collectionView: UICollectionView!
|
||||||
private var dataSource: UICollectionViewDiffableDataSource<Section, Item>!
|
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(reloadSavedHashtags), name: .savedHashtagsChanged, object: nil)
|
||||||
NotificationCenter.default.addObserver(self, selector: #selector(reloadSavedInstances), name: .savedInstancesChanged, object: nil)
|
NotificationCenter.default.addObserver(self, selector: #selector(reloadSavedInstances), name: .savedInstancesChanged, object: nil)
|
||||||
NotificationCenter.default.addObserver(self, selector: #selector(preferencesChanged), name: .preferencesChanged, object: nil)
|
NotificationCenter.default.addObserver(self, selector: #selector(preferencesChanged), name: .preferencesChanged, object: nil)
|
||||||
|
|
||||||
|
onViewDidLoad?()
|
||||||
}
|
}
|
||||||
|
|
||||||
func select(item: Item, animated: Bool) {
|
func select(item: Item, animated: Bool) {
|
||||||
|
@ -553,6 +560,7 @@ extension MainSidebarViewController: UICollectionViewDelegate {
|
||||||
collectionView.contextMenuInteraction?.menuAppearance == .rich {
|
collectionView.contextMenuInteraction?.menuAppearance == .rich {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
activity.displaysAuxiliaryScene = true
|
||||||
return UIContextMenuConfiguration(identifier: nil, previewProvider: nil) { (_) in
|
return UIContextMenuConfiguration(identifier: nil, previewProvider: nil) { (_) in
|
||||||
return UIMenu(children: [
|
return UIMenu(children: [
|
||||||
UIWindowScene.ActivationAction({ action in
|
UIWindowScene.ActivationAction({ action in
|
||||||
|
@ -573,6 +581,7 @@ extension MainSidebarViewController: UICollectionViewDragDelegate {
|
||||||
return []
|
return []
|
||||||
}
|
}
|
||||||
|
|
||||||
|
activity.displaysAuxiliaryScene = true
|
||||||
let provider = NSItemProvider(object: activity)
|
let provider = NSItemProvider(object: activity)
|
||||||
return [UIDragItem(itemProvider: provider)]
|
return [UIDragItem(itemProvider: provider)]
|
||||||
}
|
}
|
||||||
|
|
|
@ -59,10 +59,15 @@ class MainSplitViewController: UISplitViewController {
|
||||||
switcher.itemOrientation = .iconsLeading
|
switcher.itemOrientation = .iconsLeading
|
||||||
switcher.view.translatesAutoresizingMaskIntoConstraints = false
|
switcher.view.translatesAutoresizingMaskIntoConstraints = false
|
||||||
switcher.delegate = self
|
switcher.delegate = self
|
||||||
sidebar.view.addGestureRecognizer(switcher.createSwitcherGesture())
|
// accessing .view unconditionally loads the view, which we don't want to happen
|
||||||
let tapRecognizer = UITapGestureRecognizer(target: self, action: #selector(sidebarTapped))
|
// because the sidebar view not being loaded is how we know not to transfer nav state
|
||||||
tapRecognizer.cancelsTouchesInView = false
|
// in splitViewControllerDidCollapse on devices where the sidebar is never shown
|
||||||
sidebar.view.addGestureRecognizer(tapRecognizer)
|
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)
|
tabBarViewController = MainTabBarViewController(mastodonController: mastodonController)
|
||||||
|
|
|
@ -43,6 +43,8 @@ class MainTabBarViewController: UITabBarController, UITabBarControllerDelegate {
|
||||||
override func viewDidLoad() {
|
override func viewDidLoad() {
|
||||||
super.viewDidLoad()
|
super.viewDidLoad()
|
||||||
|
|
||||||
|
stateRestorationLogger.info("MainTabBarViewController: viewDidLoad, selectedIndex=\(self.selectedIndex, privacy: .public)")
|
||||||
|
|
||||||
self.delegate = self
|
self.delegate = self
|
||||||
|
|
||||||
composePlaceholder = UIViewController()
|
composePlaceholder = UIViewController()
|
||||||
|
@ -75,6 +77,16 @@ class MainTabBarViewController: UITabBarController, UITabBarControllerDelegate {
|
||||||
tabBar.isSpringLoaded = true
|
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() {
|
override func viewDidLayoutSubviews() {
|
||||||
super.viewDidLayoutSubviews()
|
super.viewDidLayoutSubviews()
|
||||||
|
|
||||||
|
@ -240,6 +252,7 @@ extension MainTabBarViewController: TuskerRootViewController {
|
||||||
self.selectedIndex = tab.rawValue
|
self.selectedIndex = tab.rawValue
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
stateRestorationLogger.info("MainTabBarViewController: selecting \(String(describing: tab), privacy: .public)")
|
||||||
selectedIndex = tab.rawValue
|
selectedIndex = tab.rawValue
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,13 +10,25 @@ import Foundation
|
||||||
|
|
||||||
extension NSUserActivity {
|
extension NSUserActivity {
|
||||||
|
|
||||||
|
var displaysAuxiliaryScene: Bool {
|
||||||
|
get {
|
||||||
|
(userInfo?["displaysAuxiliaryScene"] as? Bool) ?? false
|
||||||
|
}
|
||||||
|
set {
|
||||||
|
if userInfo == nil {
|
||||||
|
userInfo = [:]
|
||||||
|
}
|
||||||
|
userInfo!["displaysAuxiliaryScene"] = newValue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
convenience init(type: UserActivityType) {
|
convenience init(type: UserActivityType) {
|
||||||
self.init(activityType: type.rawValue)
|
self.init(activityType: type.rawValue)
|
||||||
}
|
}
|
||||||
|
|
||||||
func handleResume() -> Bool {
|
func handleResume(manager: UserActivityManager) -> Bool {
|
||||||
guard let type = UserActivityType(rawValue: activityType) else { return false }
|
guard let type = UserActivityType(rawValue: activityType) else { return false }
|
||||||
type.handle(self)
|
type.handle(manager)(self)
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -9,25 +9,32 @@
|
||||||
import UIKit
|
import UIKit
|
||||||
import Intents
|
import Intents
|
||||||
import Pachyderm
|
import Pachyderm
|
||||||
|
import OSLog
|
||||||
|
|
||||||
|
private let logger = Logger(subsystem: Bundle.main.bundleIdentifier!, category: "UserActivityManager")
|
||||||
|
|
||||||
class UserActivityManager {
|
class UserActivityManager {
|
||||||
|
|
||||||
|
private let scene: UIWindowScene
|
||||||
|
|
||||||
|
init(scene: UIWindowScene) {
|
||||||
|
self.scene = scene
|
||||||
|
}
|
||||||
|
|
||||||
// MARK: - Utils
|
// MARK: - Utils
|
||||||
private static let encoder = PropertyListEncoder()
|
private static let encoder = PropertyListEncoder()
|
||||||
private static let decoder = PropertyListDecoder()
|
private static let decoder = PropertyListDecoder()
|
||||||
|
|
||||||
private static var mastodonController: MastodonController {
|
private var mastodonController: MastodonController {
|
||||||
let scene = UIApplication.shared.activeOrBackgroundScene!
|
scene.session.mastodonController!
|
||||||
return scene.session.mastodonController!
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static func getMainViewController() -> TuskerRootViewController {
|
private func getMainViewController() -> TuskerRootViewController {
|
||||||
let scene = UIApplication.shared.connectedScenes.compactMap { $0 as? UIWindowScene }.first!
|
let window = scene.windows.first { $0.isKeyWindow } ?? scene.windows.first!
|
||||||
let window = scene.windows.first { $0.isKeyWindow }!
|
|
||||||
return window.rootViewController as! TuskerRootViewController
|
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)
|
getMainViewController().present(vc, animated: animated)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -66,7 +73,7 @@ class UserActivityManager {
|
||||||
return activity
|
return activity
|
||||||
}
|
}
|
||||||
|
|
||||||
static func handleNewPost(activity: NSUserActivity) {
|
func handleNewPost(activity: NSUserActivity) {
|
||||||
// TODO: check not currently showing compose screen
|
// TODO: check not currently showing compose screen
|
||||||
let mentioning = activity.userInfo?["mentioning"] as? String
|
let mentioning = activity.userInfo?["mentioning"] as? String
|
||||||
let draft = mastodonController.createDraft(mentioningAcct: mentioning)
|
let draft = mastodonController.createDraft(mentioningAcct: mentioning)
|
||||||
|
@ -111,14 +118,14 @@ class UserActivityManager {
|
||||||
return activity
|
return activity
|
||||||
}
|
}
|
||||||
|
|
||||||
static func handleCheckNotifications(activity: NSUserActivity) {
|
func handleCheckNotifications(activity: NSUserActivity) {
|
||||||
let mainViewController = getMainViewController()
|
let mainViewController = getMainViewController()
|
||||||
mainViewController.select(tab: .notifications)
|
mainViewController.select(tab: .notifications)
|
||||||
if let navigationController = mainViewController.getTabController(tab: .notifications) as? UINavigationController,
|
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)
|
navigationController.popToRootViewController(animated: false)
|
||||||
notificationsPageController.loadViewIfNeeded()
|
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 {
|
let data = activity.userInfo?["timelineData"] as? Data else {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
return try? decoder.decode(Timeline.self, from: data)
|
return try? UserActivityManager.decoder.decode(Timeline.self, from: data)
|
||||||
}
|
}
|
||||||
|
|
||||||
static func handleShowTimeline(activity: NSUserActivity) {
|
func handleShowTimeline(activity: NSUserActivity) {
|
||||||
guard let timeline = getTimeline(from: activity) else { return }
|
guard let timeline = Self.getTimeline(from: activity) else { return }
|
||||||
|
|
||||||
let mainViewController = getMainViewController()
|
let mainViewController = getMainViewController()
|
||||||
mainViewController.select(tab: .timelines)
|
mainViewController.select(tab: .timelines)
|
||||||
|
@ -228,7 +235,7 @@ class UserActivityManager {
|
||||||
return activity
|
return activity
|
||||||
}
|
}
|
||||||
|
|
||||||
static func handleSearch(activity: NSUserActivity) {
|
func handleSearch(activity: NSUserActivity) {
|
||||||
let mainViewController = getMainViewController()
|
let mainViewController = getMainViewController()
|
||||||
mainViewController.select(tab: .explore)
|
mainViewController.select(tab: .explore)
|
||||||
if let navigationController = mainViewController.getTabController(tab: .explore) as? UINavigationController,
|
if let navigationController = mainViewController.getTabController(tab: .explore) as? UINavigationController,
|
||||||
|
@ -247,7 +254,7 @@ class UserActivityManager {
|
||||||
return activity
|
return activity
|
||||||
}
|
}
|
||||||
|
|
||||||
static func handleBookmarks(activity: NSUserActivity) {
|
func handleBookmarks(activity: NSUserActivity) {
|
||||||
let mainViewController = getMainViewController()
|
let mainViewController = getMainViewController()
|
||||||
mainViewController.select(tab: .explore)
|
mainViewController.select(tab: .explore)
|
||||||
if let navigationController = mainViewController.getTabController(tab: .explore) as? UINavigationController {
|
if let navigationController = mainViewController.getTabController(tab: .explore) as? UINavigationController {
|
||||||
|
@ -265,7 +272,7 @@ class UserActivityManager {
|
||||||
return activity
|
return activity
|
||||||
}
|
}
|
||||||
|
|
||||||
static func handleMyProfile(activity: NSUserActivity) {
|
func handleMyProfile(activity: NSUserActivity) {
|
||||||
let mainViewController = getMainViewController()
|
let mainViewController = getMainViewController()
|
||||||
mainViewController.select(tab: .myProfile)
|
mainViewController.select(tab: .myProfile)
|
||||||
}
|
}
|
||||||
|
|
|
@ -21,7 +21,7 @@ enum UserActivityType: String {
|
||||||
}
|
}
|
||||||
|
|
||||||
extension UserActivityType {
|
extension UserActivityType {
|
||||||
var handle: (NSUserActivity) -> Void {
|
var handle: (UserActivityManager) -> (NSUserActivity) -> Void {
|
||||||
switch self {
|
switch self {
|
||||||
case .mainScene:
|
case .mainScene:
|
||||||
fatalError("cannot handle main scene activity")
|
fatalError("cannot handle main scene activity")
|
||||||
|
|
|
@ -115,6 +115,7 @@ extension AccountTableViewCell: DraggableTableViewCell {
|
||||||
}
|
}
|
||||||
let provider = NSItemProvider(object: account.url as NSURL)
|
let provider = NSItemProvider(object: account.url as NSURL)
|
||||||
let activity = UserActivityManager.showProfileActivity(id: account.id, accountID: currentAccountID)
|
let activity = UserActivityManager.showProfileActivity(id: account.id, accountID: currentAccountID)
|
||||||
|
activity.displaysAuxiliaryScene = true
|
||||||
provider.registerObject(activity, visibility: .all)
|
provider.registerObject(activity, visibility: .all)
|
||||||
return [UIDragItem(itemProvider: provider)]
|
return [UIDragItem(itemProvider: provider)]
|
||||||
}
|
}
|
||||||
|
|
|
@ -223,6 +223,7 @@ extension FollowNotificationGroupTableViewCell: DraggableTableViewCell {
|
||||||
let notification = group.notifications[0]
|
let notification = group.notifications[0]
|
||||||
let provider = NSItemProvider(object: notification.account.url as NSURL)
|
let provider = NSItemProvider(object: notification.account.url as NSURL)
|
||||||
let activity = UserActivityManager.showProfileActivity(id: notification.account.id, accountID: mastodonController.accountInfo!.id)
|
let activity = UserActivityManager.showProfileActivity(id: notification.account.id, accountID: mastodonController.accountInfo!.id)
|
||||||
|
activity.displaysAuxiliaryScene = true
|
||||||
provider.registerObject(activity, visibility: .all)
|
provider.registerObject(activity, visibility: .all)
|
||||||
return [UIDragItem(itemProvider: provider)]
|
return [UIDragItem(itemProvider: provider)]
|
||||||
}
|
}
|
||||||
|
|
|
@ -206,6 +206,7 @@ extension FollowRequestNotificationTableViewCell: DraggableTableViewCell {
|
||||||
func dragItemsForBeginning(session: UIDragSession) -> [UIDragItem] {
|
func dragItemsForBeginning(session: UIDragSession) -> [UIDragItem] {
|
||||||
let provider = NSItemProvider(object: account.url as NSURL)
|
let provider = NSItemProvider(object: account.url as NSURL)
|
||||||
let activity = UserActivityManager.showProfileActivity(id: account.id, accountID: mastodonController.accountInfo!.id)
|
let activity = UserActivityManager.showProfileActivity(id: account.id, accountID: mastodonController.accountInfo!.id)
|
||||||
|
activity.displaysAuxiliaryScene = true
|
||||||
provider.registerObject(activity, visibility: .all)
|
provider.registerObject(activity, visibility: .all)
|
||||||
return [UIDragItem(itemProvider: provider)]
|
return [UIDragItem(itemProvider: provider)]
|
||||||
}
|
}
|
||||||
|
|
|
@ -21,6 +21,7 @@ class StatusPollView: UIView {
|
||||||
}()
|
}()
|
||||||
|
|
||||||
weak var mastodonController: MastodonController!
|
weak var mastodonController: MastodonController!
|
||||||
|
weak var toastableViewController: ToastableViewController?
|
||||||
|
|
||||||
private var statusID: String!
|
private var statusID: String!
|
||||||
private(set) var poll: Poll?
|
private(set) var poll: Poll?
|
||||||
|
@ -151,7 +152,14 @@ class StatusPollView: UIView {
|
||||||
mastodonController.run(request) { (response) in
|
mastodonController.run(request) { (response) in
|
||||||
switch response {
|
switch response {
|
||||||
case let .failure(error):
|
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, _):
|
case let .success(poll, _):
|
||||||
let container = self.mastodonController.persistentContainer
|
let container = self.mastodonController.persistentContainer
|
||||||
|
|
|
@ -211,6 +211,7 @@ class BaseStatusTableViewCell: UITableViewCell {
|
||||||
|
|
||||||
pollView.isHidden = status.poll == nil
|
pollView.isHidden = status.poll == nil
|
||||||
pollView.mastodonController = mastodonController
|
pollView.mastodonController = mastodonController
|
||||||
|
pollView.toastableViewController = delegate?.toastableViewController
|
||||||
pollView.updateUI(status: status, poll: status.poll)
|
pollView.updateUI(status: status, poll: status.poll)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -530,6 +531,7 @@ extension BaseStatusTableViewCell: UIDragInteractionDelegate {
|
||||||
}
|
}
|
||||||
let provider = NSItemProvider(object: account.url as NSURL)
|
let provider = NSItemProvider(object: account.url as NSURL)
|
||||||
let activity = UserActivityManager.showProfileActivity(id: accountID, accountID: currentAccountID)
|
let activity = UserActivityManager.showProfileActivity(id: accountID, accountID: currentAccountID)
|
||||||
|
activity.displaysAuxiliaryScene = true
|
||||||
provider.registerObject(activity, visibility: .all)
|
provider.registerObject(activity, visibility: .all)
|
||||||
return [UIDragItem(itemProvider: provider)]
|
return [UIDragItem(itemProvider: provider)]
|
||||||
}
|
}
|
||||||
|
|
|
@ -356,6 +356,7 @@ extension TimelineStatusTableViewCell: DraggableTableViewCell {
|
||||||
}
|
}
|
||||||
let provider = NSItemProvider(object: status.url! as NSURL)
|
let provider = NSItemProvider(object: status.url! as NSURL)
|
||||||
let activity = UserActivityManager.showConversationActivity(mainStatusID: status.id, accountID: accountID)
|
let activity = UserActivityManager.showConversationActivity(mainStatusID: status.id, accountID: accountID)
|
||||||
|
activity.displaysAuxiliaryScene = true
|
||||||
provider.registerObject(activity, visibility: .all)
|
provider.registerObject(activity, visibility: .all)
|
||||||
return [UIDragItem(itemProvider: provider)]
|
return [UIDragItem(itemProvider: provider)]
|
||||||
}
|
}
|
||||||
|
|
|
@ -57,6 +57,14 @@ extension ToastConfiguration {
|
||||||
viewController.present(reporter, animated: true)
|
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 {
|
fileprivate extension Client.Error {
|
||||||
|
|
|
@ -61,6 +61,22 @@
|
||||||
<string>%u replies</string>
|
<string>%u replies</string>
|
||||||
</dict>
|
</dict>
|
||||||
</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>
|
<key>favorites count</key>
|
||||||
<dict>
|
<dict>
|
||||||
<key>NSStringLocalizedFormatKey</key>
|
<key>NSStringLocalizedFormatKey</key>
|
||||||
|
|
Loading…
Reference in New Issue