Compare commits

...

5 Commits

6 changed files with 89 additions and 64 deletions

View File

@ -218,9 +218,9 @@ class MainSceneDelegate: UIResponder, UIWindowSceneDelegate, TuskerSceneDelegate
} else { } else {
direction = .none direction = .none
} }
container.setRoot(newRoot, animating: direction) container.setRoot(newRoot, for: account, animating: direction)
} else { } else {
window!.rootViewController = AccountSwitchingContainerViewController(root: newRoot) window!.rootViewController = AccountSwitchingContainerViewController(root: newRoot, for: account)
} }
} }

View File

@ -9,29 +9,9 @@
import SwiftUI import SwiftUI
import Pachyderm import Pachyderm
struct MainComposeTextView: View { struct MainComposeTextView: View, PlaceholderViewProvider {
@ObservedObject var draft: Draft @ObservedObject var draft: Draft
@State private var placeholder: Text = { @State private var placeholder: PlaceholderView = Self.placeholderView()
let components = Calendar.current.dateComponents([.month, .day], from: Date())
if components.month == 3 && components.day == 14 {
if Date().formatted(date: .numeric, time: .omitted).starts(with: "3") {
return Text("Happy π day!")
}
} else if components.month == 9 && components.day == 5 {
// https://weirder.earth/@noracodes/109276419847254552
// https://retrocomputing.stackexchange.com/questions/14763/what-warning-was-given-on-attempting-to-post-to-usenet-circa-1990
return Text("This program posts news to thousands of machines throughout the entire populated world. Please be sure you know what you are doing.").italic()
} else if components.month == 9 && components.day == 21 {
return Text("Do you remember?")
} else if components.month == 10 && components.day == 31 {
if .random() {
return Text("Post something spooky!")
} else {
return Text("Any questions?")
}
}
return Text("What's on your mind?")
}()
let minHeight: CGFloat = 150 let minHeight: CGFloat = 150
@State private var height: CGFloat? @State private var height: CGFloat?
@ -68,6 +48,38 @@ struct MainComposeTextView: View {
} }
} }
} }
@ViewBuilder
static func placeholderView() -> some View {
let components = Calendar.current.dateComponents([.month, .day], from: Date())
if components.month == 3 && components.day == 14,
Date().formatted(date: .numeric, time: .omitted).starts(with: "3") {
Text("Happy π day!")
} else if components.month == 4 && components.day == 1 {
Text("April Fool's!").rotationEffect(.radians(.pi), anchor: .center)
} else if components.month == 9 && components.day == 5 {
// https://weirder.earth/@noracodes/109276419847254552
// https://retrocomputing.stackexchange.com/questions/14763/what-warning-was-given-on-attempting-to-post-to-usenet-circa-1990
Text("This program posts news to thousands of machines throughout the entire populated world. Please be sure you know what you are doing.").italic()
} else if components.month == 9 && components.day == 21 {
Text("Do you remember?")
} else if components.month == 10 && components.day == 31 {
if .random() {
Text("Post something spooky!")
} else {
Text("Any questions?")
}
} else {
Text("What's on your mind?")
}
}
}
// exists to provide access to the type alias since the @State property needs it to be explicit
private protocol PlaceholderViewProvider {
associatedtype PlaceholderView: View
@ViewBuilder
static func placeholderView() -> PlaceholderView
} }
struct MainComposeWrappedTextView: UIViewRepresentable { struct MainComposeWrappedTextView: UIViewRepresentable {

View File

@ -11,9 +11,13 @@ import ScreenCorners
class AccountSwitchingContainerViewController: UIViewController { class AccountSwitchingContainerViewController: UIViewController {
private var currentAccountID: String
private(set) var root: TuskerRootViewController private(set) var root: TuskerRootViewController
init(root: TuskerRootViewController) { private var userActivities: [String: NSUserActivity] = [:]
init(root: TuskerRootViewController, for account: LocalData.UserAccountInfo) {
self.currentAccountID = account.id
self.root = root self.root = root
super.init(nibName: nil, bundle: nil) super.init(nibName: nil, bundle: nil)
@ -29,14 +33,27 @@ class AccountSwitchingContainerViewController: UIViewController {
embedChild(root) embedChild(root)
} }
func setRoot(_ newRoot: TuskerRootViewController, animating direction: AnimationDirection) { func setRoot(_ newRoot: TuskerRootViewController, for account: LocalData.UserAccountInfo, animating direction: AnimationDirection) {
let oldRoot = self.root let oldRoot = self.root
if direction == .none { if direction == .none {
oldRoot.removeViewAndController() oldRoot.removeViewAndController()
} }
if let activity = oldRoot.stateRestorationActivity() {
stateRestorationLogger.debug("AccountSwitchingContainer: saving \(activity.activityType, privacy: .public) for \(self.currentAccountID, privacy: .public)")
userActivities[currentAccountID] = activity
}
self.currentAccountID = account.id
self.root = newRoot self.root = newRoot
embedChild(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 direction != .none {
if UIAccessibility.prefersCrossFadeTransitions { if UIAccessibility.prefersCrossFadeTransitions {
newRoot.view.alpha = 0 newRoot.view.alpha = 0

View File

@ -14,12 +14,13 @@ class TimelineGapCollectionViewCell: UICollectionViewCell {
private let indicator = UIActivityIndicatorView(style: .medium) private let indicator = UIActivityIndicatorView(style: .medium)
private let chevronView = AnimatingChevronView() private let chevronView = AnimatingChevronView()
private let fillView = UIView()
var fillGap: ((TimelineGapDirection) async -> Void)? var fillGap: ((TimelineGapDirection) async -> Void)?
override var isHighlighted: Bool { override var isHighlighted: Bool {
didSet { didSet {
backgroundColor = isHighlighted ? .appFill : .appGroupedBackground backgroundColor = isHighlighted ? .appFill : .appSecondaryBackground
} }
} }
@ -38,17 +39,22 @@ class TimelineGapCollectionViewCell: UICollectionViewCell {
override init(frame: CGRect) { override init(frame: CGRect) {
super.init(frame: frame) super.init(frame: frame)
backgroundColor = .appGroupedBackground backgroundColor = .appSecondaryBackground
indicator.isHidden = true fillView.backgroundColor = .tintColor
indicator.color = .tintColor fillView.alpha = 0.75
fillView.layer.masksToBounds = true
fillView.layer.cornerRadius = 8
fillView.translatesAutoresizingMaskIntoConstraints = false
contentView.addSubview(fillView)
let label = UILabel() let label = UILabel()
label.text = "Load more" label.text = "Load more"
label.font = .preferredFont(forTextStyle: .headline) label.font = .preferredFont(forTextStyle: .headline)
label.adjustsFontForContentSizeCategory = true label.adjustsFontForContentSizeCategory = true
label.textColor = .tintColor label.textColor = .white
chevronView.tintColor = .white
chevronView.update(direction: .above) chevronView.update(direction: .above)
let stack = UIStackView(arrangedSubviews: [ let stack = UIStackView(arrangedSubviews: [
@ -60,15 +66,25 @@ class TimelineGapCollectionViewCell: UICollectionViewCell {
stack.translatesAutoresizingMaskIntoConstraints = false stack.translatesAutoresizingMaskIntoConstraints = false
contentView.addSubview(stack) contentView.addSubview(stack)
indicator.isHidden = true
indicator.color = .tintColor
indicator.translatesAutoresizingMaskIntoConstraints = false indicator.translatesAutoresizingMaskIntoConstraints = false
contentView.addSubview(indicator) contentView.addSubview(indicator)
NSLayoutConstraint.activate([ NSLayoutConstraint.activate([
stack.centerXAnchor.constraint(equalTo: contentView.centerXAnchor), stack.centerXAnchor.constraint(equalTo: contentView.centerXAnchor),
stack.centerYAnchor.constraint(equalTo: contentView.centerYAnchor), stack.centerYAnchor.constraint(equalTo: contentView.centerYAnchor),
indicator.trailingAnchor.constraint(equalTo: stack.leadingAnchor, constant: -8),
fillView.leadingAnchor.constraint(equalTo: stack.leadingAnchor, constant: -8),
// optical centering, babey
fillView.trailingAnchor.constraint(equalTo: stack.trailingAnchor, constant: 10),
fillView.topAnchor.constraint(equalTo: stack.topAnchor, constant: -4),
fillView.bottomAnchor.constraint(equalTo: stack.bottomAnchor, constant: 4),
indicator.trailingAnchor.constraint(equalTo: stack.leadingAnchor, constant: -12),
indicator.centerYAnchor.constraint(equalTo: contentView.centerYAnchor), indicator.centerYAnchor.constraint(equalTo: contentView.centerYAnchor),
contentView.heightAnchor.constraint(equalToConstant: 44),
contentView.heightAnchor.constraint(equalToConstant: 52),
]) ])
} }

View File

@ -718,27 +718,15 @@ class TimelineViewController: UIViewController, TimelineLikeCollectionViewContro
} }
@objc func refresh() { @objc func refresh() {
Task { Task { @MainActor in
if case .notLoadedInitial = controller.state { if case .notLoadedInitial = controller.state {
await controller.loadInitial() await controller.loadInitial()
#if !targetEnvironment(macCatalyst)
collectionView.refreshControl?.endRefreshing()
#endif
} else { } else {
@MainActor await controller.loadNewer()
func loadNewerAndEndRefreshing() async {
await controller.loadNewer()
#if !targetEnvironment(macCatalyst)
collectionView.refreshControl?.endRefreshing()
#endif
}
// I'm not sure whether this should move into TimelineLikeController/TimelineLikeCollectionViewController
let (_, presentItems) = await (loadNewerAndEndRefreshing(), try? loadInitial())
if let presentItems, !presentItems.isEmpty {
insertPresentItemsAndShowJumpToast(presentItems)
}
} }
#if !targetEnvironment(macCatalyst)
collectionView.refreshControl?.endRefreshing()
#endif
} }
} }

View File

@ -32,15 +32,6 @@ class UserActivityManager {
scene.session.mastodonController! scene.session.mastodonController!
} }
private func getMainViewController() -> TuskerRootViewController {
let window = scene.windows.first { $0.isKeyWindow } ?? scene.windows.first!
return window.rootViewController as! TuskerRootViewController
}
private func present(_ vc: UIViewController, animated: Bool = true) {
getMainViewController().present(vc, animated: animated)
}
static func getAccount(from activity: NSUserActivity) -> LocalData.UserAccountInfo? { static func getAccount(from activity: NSUserActivity) -> LocalData.UserAccountInfo? {
guard let id = activity.userInfo?["accountID"] as? String else { guard let id = activity.userInfo?["accountID"] as? String else {
return nil return nil
@ -213,25 +204,26 @@ class UserActivityManager {
func handleShowTimeline(activity: NSUserActivity) { func handleShowTimeline(activity: NSUserActivity) {
guard let (timeline, positionInfo) = Self.getTimeline(from: activity) else { return } guard let (timeline, positionInfo) = Self.getTimeline(from: activity) else { return }
let timelineVC: TimelineViewController var timelineVC: TimelineViewController?
if let pinned = PinnedTimeline(timeline: timeline), if let pinned = PinnedTimeline(timeline: timeline),
mastodonController.accountPreferences.pinnedTimelines.contains(pinned) { mastodonController.accountPreferences.pinnedTimelines.contains(pinned) {
context.select(route: .timelines) context.select(route: .timelines)
context.popToRoot() context.popToRoot()
let pageController = context.topViewController as! TimelinesPageViewController let pageController = context.topViewController as! TimelinesPageViewController
pageController.selectTimeline(pinned, animated: false) pageController.selectTimeline(pinned, animated: false)
timelineVC = pageController.currentViewController as! TimelineViewController timelineVC = pageController.currentViewController as? TimelineViewController
} else if case .list(let id) = timeline { } else if case .list(let id) = timeline {
context.select(route: .list(id: id)) context.select(route: .list(id: id))
timelineVC = context.topViewController! as! TimelineViewController timelineVC = context.topViewController as? TimelineViewController
} else { } else {
context.select(route: .explore) context.select(route: .explore)
context.popToRoot() context.popToRoot()
timelineVC = TimelineViewController(for: timeline, mastodonController: mastodonController) timelineVC = TimelineViewController(for: timeline, mastodonController: mastodonController)
context.push(timelineVC) context.push(timelineVC!)
} }
if let positionInfo, if let timelineVC,
let positionInfo,
context.isHandoff { context.isHandoff {
Task { Task {
await timelineVC.restoreStateFromHandoff(statusIDs: positionInfo.statusIDs, centerStatusID: positionInfo.centerStatusID) await timelineVC.restoreStateFromHandoff(statusIDs: positionInfo.statusIDs, centerStatusID: positionInfo.centerStatusID)