feat: add navigation shortcut for notification scene
This commit is contained in:
parent
c44ced7501
commit
aec7a1f5ea
|
@ -44,6 +44,9 @@
|
||||||
"controls": {
|
"controls": {
|
||||||
"actions": {
|
"actions": {
|
||||||
"back": "Back",
|
"back": "Back",
|
||||||
|
"next": "Next",
|
||||||
|
"previous": "Previous",
|
||||||
|
"open": "Open",
|
||||||
"add": "Add",
|
"add": "Add",
|
||||||
"remove": "Remove",
|
"remove": "Remove",
|
||||||
"edit": "Edit",
|
"edit": "Edit",
|
||||||
|
|
|
@ -200,6 +200,8 @@
|
||||||
DB1D842C26551A1C000346B3 /* StatusProvider+StatusTableViewKeyCommandNavigateable.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB1D842B26551A1C000346B3 /* StatusProvider+StatusTableViewKeyCommandNavigateable.swift */; };
|
DB1D842C26551A1C000346B3 /* StatusProvider+StatusTableViewKeyCommandNavigateable.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB1D842B26551A1C000346B3 /* StatusProvider+StatusTableViewKeyCommandNavigateable.swift */; };
|
||||||
DB1D842E26552C4D000346B3 /* StatusTableViewControllerNavigateable.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB1D842D26552C4D000346B3 /* StatusTableViewControllerNavigateable.swift */; };
|
DB1D842E26552C4D000346B3 /* StatusTableViewControllerNavigateable.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB1D842D26552C4D000346B3 /* StatusTableViewControllerNavigateable.swift */; };
|
||||||
DB1D843026566512000346B3 /* KeyboardPreference.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB1D842F26566512000346B3 /* KeyboardPreference.swift */; };
|
DB1D843026566512000346B3 /* KeyboardPreference.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB1D842F26566512000346B3 /* KeyboardPreference.swift */; };
|
||||||
|
DB1D843426579931000346B3 /* TableViewControllerNavigateable.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB1D843326579931000346B3 /* TableViewControllerNavigateable.swift */; };
|
||||||
|
DB1D843626579DB5000346B3 /* StatusProvider+TableViewControllerNavigateable.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB1D843526579DB5000346B3 /* StatusProvider+TableViewControllerNavigateable.swift */; };
|
||||||
DB1E346825F518E20079D7DF /* CategoryPickerSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB1E346725F518E20079D7DF /* CategoryPickerSection.swift */; };
|
DB1E346825F518E20079D7DF /* CategoryPickerSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB1E346725F518E20079D7DF /* CategoryPickerSection.swift */; };
|
||||||
DB1E347825F519300079D7DF /* PickServerItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB1E347725F519300079D7DF /* PickServerItem.swift */; };
|
DB1E347825F519300079D7DF /* PickServerItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB1E347725F519300079D7DF /* PickServerItem.swift */; };
|
||||||
DB1FD43625F26899004CFCFC /* MastodonPickServerViewModel+LoadIndexedServerState.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB1FD43525F26899004CFCFC /* MastodonPickServerViewModel+LoadIndexedServerState.swift */; };
|
DB1FD43625F26899004CFCFC /* MastodonPickServerViewModel+LoadIndexedServerState.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB1FD43525F26899004CFCFC /* MastodonPickServerViewModel+LoadIndexedServerState.swift */; };
|
||||||
|
@ -758,6 +760,8 @@
|
||||||
DB1D842B26551A1C000346B3 /* StatusProvider+StatusTableViewKeyCommandNavigateable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "StatusProvider+StatusTableViewKeyCommandNavigateable.swift"; sourceTree = "<group>"; };
|
DB1D842B26551A1C000346B3 /* StatusProvider+StatusTableViewKeyCommandNavigateable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "StatusProvider+StatusTableViewKeyCommandNavigateable.swift"; sourceTree = "<group>"; };
|
||||||
DB1D842D26552C4D000346B3 /* StatusTableViewControllerNavigateable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusTableViewControllerNavigateable.swift; sourceTree = "<group>"; };
|
DB1D842D26552C4D000346B3 /* StatusTableViewControllerNavigateable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusTableViewControllerNavigateable.swift; sourceTree = "<group>"; };
|
||||||
DB1D842F26566512000346B3 /* KeyboardPreference.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyboardPreference.swift; sourceTree = "<group>"; };
|
DB1D842F26566512000346B3 /* KeyboardPreference.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyboardPreference.swift; sourceTree = "<group>"; };
|
||||||
|
DB1D843326579931000346B3 /* TableViewControllerNavigateable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TableViewControllerNavigateable.swift; sourceTree = "<group>"; };
|
||||||
|
DB1D843526579DB5000346B3 /* StatusProvider+TableViewControllerNavigateable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "StatusProvider+TableViewControllerNavigateable.swift"; sourceTree = "<group>"; };
|
||||||
DB1E346725F518E20079D7DF /* CategoryPickerSection.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CategoryPickerSection.swift; sourceTree = "<group>"; };
|
DB1E346725F518E20079D7DF /* CategoryPickerSection.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CategoryPickerSection.swift; sourceTree = "<group>"; };
|
||||||
DB1E347725F519300079D7DF /* PickServerItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PickServerItem.swift; sourceTree = "<group>"; };
|
DB1E347725F519300079D7DF /* PickServerItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PickServerItem.swift; sourceTree = "<group>"; };
|
||||||
DB1FD43525F26899004CFCFC /* MastodonPickServerViewModel+LoadIndexedServerState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MastodonPickServerViewModel+LoadIndexedServerState.swift"; sourceTree = "<group>"; };
|
DB1FD43525F26899004CFCFC /* MastodonPickServerViewModel+LoadIndexedServerState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MastodonPickServerViewModel+LoadIndexedServerState.swift"; sourceTree = "<group>"; };
|
||||||
|
@ -1247,6 +1251,7 @@
|
||||||
2DF75BA025D0E29D00694EC8 /* StatusProvider+StatusTableViewCellDelegate.swift */,
|
2DF75BA025D0E29D00694EC8 /* StatusProvider+StatusTableViewCellDelegate.swift */,
|
||||||
DB59F0FD25EF5D96001F1DAB /* StatusProvider+UITableViewDelegate.swift */,
|
DB59F0FD25EF5D96001F1DAB /* StatusProvider+UITableViewDelegate.swift */,
|
||||||
DB71FD4525F8C6D200512AE1 /* StatusProvider+UITableViewDataSourcePrefetching.swift */,
|
DB71FD4525F8C6D200512AE1 /* StatusProvider+UITableViewDataSourcePrefetching.swift */,
|
||||||
|
DB1D843526579DB5000346B3 /* StatusProvider+TableViewControllerNavigateable.swift */,
|
||||||
DB1D842B26551A1C000346B3 /* StatusProvider+StatusTableViewKeyCommandNavigateable.swift */,
|
DB1D842B26551A1C000346B3 /* StatusProvider+StatusTableViewKeyCommandNavigateable.swift */,
|
||||||
);
|
);
|
||||||
path = StatusProvider;
|
path = StatusProvider;
|
||||||
|
@ -1348,6 +1353,7 @@
|
||||||
2D38F20725CD491300561493 /* DisposeBagCollectable.swift */,
|
2D38F20725CD491300561493 /* DisposeBagCollectable.swift */,
|
||||||
5DF1058425F88AE500D6C0D4 /* NeedsDependency+AVPlayerViewControllerDelegate.swift */,
|
5DF1058425F88AE500D6C0D4 /* NeedsDependency+AVPlayerViewControllerDelegate.swift */,
|
||||||
DBE3CE12261D7D4200430CC6 /* StatusTableViewControllerAspect.swift */,
|
DBE3CE12261D7D4200430CC6 /* StatusTableViewControllerAspect.swift */,
|
||||||
|
DB1D843326579931000346B3 /* TableViewControllerNavigateable.swift */,
|
||||||
DB1D842D26552C4D000346B3 /* StatusTableViewControllerNavigateable.swift */,
|
DB1D842D26552C4D000346B3 /* StatusTableViewControllerNavigateable.swift */,
|
||||||
);
|
);
|
||||||
path = Protocol;
|
path = Protocol;
|
||||||
|
@ -2907,6 +2913,7 @@
|
||||||
DBE3CDCF261C42ED00430CC6 /* TimelineHeaderView.swift in Sources */,
|
DBE3CDCF261C42ED00430CC6 /* TimelineHeaderView.swift in Sources */,
|
||||||
DBAE3F8E2616E0B1004B8251 /* APIService+Block.swift in Sources */,
|
DBAE3F8E2616E0B1004B8251 /* APIService+Block.swift in Sources */,
|
||||||
5DF1057F25F88A4100D6C0D4 /* TouchBlockingView.swift in Sources */,
|
5DF1057F25F88A4100D6C0D4 /* TouchBlockingView.swift in Sources */,
|
||||||
|
DB1D843426579931000346B3 /* TableViewControllerNavigateable.swift in Sources */,
|
||||||
0FAA0FDF25E0B57E0017CCDE /* WelcomeViewController.swift in Sources */,
|
0FAA0FDF25E0B57E0017CCDE /* WelcomeViewController.swift in Sources */,
|
||||||
2D206B8C25F6015000143C56 /* AudioPlaybackService.swift in Sources */,
|
2D206B8C25F6015000143C56 /* AudioPlaybackService.swift in Sources */,
|
||||||
2D59819B25E4A581000FB903 /* MastodonConfirmEmailViewController.swift in Sources */,
|
2D59819B25E4A581000FB903 /* MastodonConfirmEmailViewController.swift in Sources */,
|
||||||
|
@ -3256,6 +3263,7 @@
|
||||||
0FB3D31E25E534C700AAD544 /* PickServerCategoryCollectionViewCell.swift in Sources */,
|
0FB3D31E25E534C700AAD544 /* PickServerCategoryCollectionViewCell.swift in Sources */,
|
||||||
DB1D842C26551A1C000346B3 /* StatusProvider+StatusTableViewKeyCommandNavigateable.swift in Sources */,
|
DB1D842C26551A1C000346B3 /* StatusProvider+StatusTableViewKeyCommandNavigateable.swift in Sources */,
|
||||||
5DF1057925F88A1D00D6C0D4 /* PlayerContainerView.swift in Sources */,
|
5DF1057925F88A1D00D6C0D4 /* PlayerContainerView.swift in Sources */,
|
||||||
|
DB1D843626579DB5000346B3 /* StatusProvider+TableViewControllerNavigateable.swift in Sources */,
|
||||||
DB6D9F4926353FD7008423CD /* Subscription.swift in Sources */,
|
DB6D9F4926353FD7008423CD /* Subscription.swift in Sources */,
|
||||||
DB45FB0F25CA87D0005A8AC7 /* AuthenticationService.swift in Sources */,
|
DB45FB0F25CA87D0005A8AC7 /* AuthenticationService.swift in Sources */,
|
||||||
DB6180ED26391C6C0018D199 /* TransitioningMath.swift in Sources */,
|
DB6180ED26391C6C0018D199 /* TransitioningMath.swift in Sources */,
|
||||||
|
|
|
@ -102,12 +102,18 @@ internal enum L10n {
|
||||||
internal static let findPeople = L10n.tr("Localizable", "Common.Controls.Actions.FindPeople")
|
internal static let findPeople = L10n.tr("Localizable", "Common.Controls.Actions.FindPeople")
|
||||||
/// Manually search instead
|
/// Manually search instead
|
||||||
internal static let manuallySearch = L10n.tr("Localizable", "Common.Controls.Actions.ManuallySearch")
|
internal static let manuallySearch = L10n.tr("Localizable", "Common.Controls.Actions.ManuallySearch")
|
||||||
|
/// Next
|
||||||
|
internal static let next = L10n.tr("Localizable", "Common.Controls.Actions.Next")
|
||||||
/// OK
|
/// OK
|
||||||
internal static let ok = L10n.tr("Localizable", "Common.Controls.Actions.Ok")
|
internal static let ok = L10n.tr("Localizable", "Common.Controls.Actions.Ok")
|
||||||
|
/// Open
|
||||||
|
internal static let `open` = L10n.tr("Localizable", "Common.Controls.Actions.Open")
|
||||||
/// Open in Safari
|
/// Open in Safari
|
||||||
internal static let openInSafari = L10n.tr("Localizable", "Common.Controls.Actions.OpenInSafari")
|
internal static let openInSafari = L10n.tr("Localizable", "Common.Controls.Actions.OpenInSafari")
|
||||||
/// Preview
|
/// Preview
|
||||||
internal static let preview = L10n.tr("Localizable", "Common.Controls.Actions.Preview")
|
internal static let preview = L10n.tr("Localizable", "Common.Controls.Actions.Preview")
|
||||||
|
/// Previous
|
||||||
|
internal static let previous = L10n.tr("Localizable", "Common.Controls.Actions.Previous")
|
||||||
/// Remove
|
/// Remove
|
||||||
internal static let remove = L10n.tr("Localizable", "Common.Controls.Actions.Remove")
|
internal static let remove = L10n.tr("Localizable", "Common.Controls.Actions.Remove")
|
||||||
/// Reply
|
/// Reply
|
||||||
|
|
|
@ -8,18 +8,35 @@
|
||||||
import os.log
|
import os.log
|
||||||
import UIKit
|
import UIKit
|
||||||
|
|
||||||
extension StatusTableViewCellDelegate where Self: StatusProvider & StatusTableViewControllerNavigateable {
|
extension StatusTableViewControllerNavigateableCore where Self: StatusProvider & StatusTableViewControllerNavigateableRelay {
|
||||||
|
|
||||||
func keyCommandHandler(_ sender: UIKeyCommand) {
|
var statusNavigationKeyCommands: [UIKeyCommand] {
|
||||||
|
StatusTableViewNavigation.allCases.map { navigation in
|
||||||
|
UIKeyCommand(
|
||||||
|
title: navigation.title,
|
||||||
|
image: nil,
|
||||||
|
action: #selector(Self.statusKeyCommandHandlerRelay(_:)),
|
||||||
|
input: navigation.input,
|
||||||
|
modifierFlags: navigation.modifierFlags,
|
||||||
|
propertyList: navigation.propertyList,
|
||||||
|
alternates: [],
|
||||||
|
discoverabilityTitle: nil,
|
||||||
|
attributes: [],
|
||||||
|
state: .off
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
extension StatusTableViewControllerNavigateableCore where Self: StatusProvider {
|
||||||
|
|
||||||
|
func statusKeyCommandHandler(_ sender: UIKeyCommand) {
|
||||||
guard let rawValue = sender.propertyList as? String,
|
guard let rawValue = sender.propertyList as? String,
|
||||||
let navigation = StatusTableViewNavigation(rawValue: rawValue) else { return }
|
let navigation = StatusTableViewNavigation(rawValue: rawValue) else { return }
|
||||||
|
|
||||||
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: %s", ((#file as NSString).lastPathComponent), #line, #function, navigation.title)
|
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: %s", ((#file as NSString).lastPathComponent), #line, #function, navigation.title)
|
||||||
switch navigation {
|
switch navigation {
|
||||||
case .up: navigateStatus(direction: .up)
|
|
||||||
case .down: navigateStatus(direction: .down)
|
|
||||||
case .back: backTimeline()
|
|
||||||
case .openStatus: openStatus()
|
|
||||||
case .openAuthorProfile: openAuthorProfile()
|
case .openAuthorProfile: openAuthorProfile()
|
||||||
case .openRebloggerProfile: openRebloggerProfile()
|
case .openRebloggerProfile: openRebloggerProfile()
|
||||||
case .replyStatus: replyStatus()
|
case .replyStatus: replyStatus()
|
||||||
|
@ -32,108 +49,8 @@ extension StatusTableViewCellDelegate where Self: StatusProvider & StatusTableVi
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// navigate status up/down
|
|
||||||
extension StatusTableViewCellDelegate where Self: StatusProvider & StatusTableViewControllerNavigateable {
|
|
||||||
|
|
||||||
private func navigateStatus(direction: StatusTableViewNavigationDirection) {
|
|
||||||
if let indexPathForSelectedRow = tableView.indexPathForSelectedRow {
|
|
||||||
// navigate up/down on the current selected item
|
|
||||||
navigateToStatus(direction: direction, indexPath: indexPathForSelectedRow)
|
|
||||||
} else {
|
|
||||||
// set first visible item selected
|
|
||||||
navigateToFirstVisibleStatus()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private func navigateToStatus(direction: StatusTableViewNavigationDirection, indexPath: IndexPath) {
|
|
||||||
guard let diffableDataSource = tableViewDiffableDataSource else { return }
|
|
||||||
let items = diffableDataSource.snapshot().itemIdentifiers
|
|
||||||
guard let selectedItem = diffableDataSource.itemIdentifier(for: indexPath),
|
|
||||||
let selectedItemIndex = items.firstIndex(of: selectedItem) else {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
let _navigateToItem: Item? = {
|
|
||||||
var index = selectedItemIndex
|
|
||||||
while 0..<items.count ~= index {
|
|
||||||
index = {
|
|
||||||
switch direction {
|
|
||||||
case .up: return index - 1
|
|
||||||
case .down: return index + 1
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
guard 0..<items.count ~= index else { return nil }
|
|
||||||
let item = items[index]
|
|
||||||
|
|
||||||
guard Self.validNavigateableItem(item) else { continue }
|
|
||||||
return item
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}()
|
|
||||||
|
|
||||||
guard let item = _navigateToItem, let indexPath = diffableDataSource.indexPath(for: item) else { return }
|
|
||||||
let scrollPosition: UITableView.ScrollPosition = overrideNavigationScrollPosition ?? Self.navigateScrollPosition(tableView: tableView, indexPath: indexPath)
|
|
||||||
tableView.selectRow(at: indexPath, animated: true, scrollPosition: scrollPosition)
|
|
||||||
}
|
|
||||||
|
|
||||||
private func navigateToFirstVisibleStatus() {
|
|
||||||
guard let indexPathsForVisibleRows = tableView.indexPathsForVisibleRows else { return }
|
|
||||||
guard let diffableDataSource = tableViewDiffableDataSource else { return }
|
|
||||||
|
|
||||||
var visibleItems: [Item] = indexPathsForVisibleRows.sorted().compactMap { indexPath in
|
|
||||||
guard let item = diffableDataSource.itemIdentifier(for: indexPath) else { return nil }
|
|
||||||
guard Self.validNavigateableItem(item) else { return nil }
|
|
||||||
return item
|
|
||||||
}
|
|
||||||
if indexPathsForVisibleRows.first?.row != 0, visibleItems.count > 1 {
|
|
||||||
// drop first when visible not the first cell of table
|
|
||||||
visibleItems.removeFirst()
|
|
||||||
}
|
|
||||||
guard let item = visibleItems.first, let indexPath = diffableDataSource.indexPath(for: item) else { return }
|
|
||||||
let scrollPosition: UITableView.ScrollPosition = overrideNavigationScrollPosition ?? Self.navigateScrollPosition(tableView: tableView, indexPath: indexPath)
|
|
||||||
tableView.selectRow(at: indexPath, animated: true, scrollPosition: scrollPosition)
|
|
||||||
}
|
|
||||||
|
|
||||||
static func validNavigateableItem(_ item: Item) -> Bool {
|
|
||||||
switch item {
|
|
||||||
case .homeTimelineIndex,
|
|
||||||
.status,
|
|
||||||
.root, .leaf, .reply:
|
|
||||||
return true
|
|
||||||
default:
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// check is visible and not the first and last
|
|
||||||
static func navigateScrollPosition(tableView: UITableView, indexPath: IndexPath) -> UITableView.ScrollPosition {
|
|
||||||
let middleVisibleIndexPaths = (tableView.indexPathsForVisibleRows ?? [])
|
|
||||||
.sorted()
|
|
||||||
.dropFirst()
|
|
||||||
.dropLast()
|
|
||||||
guard middleVisibleIndexPaths.contains(indexPath) else {
|
|
||||||
return .top
|
|
||||||
}
|
|
||||||
guard middleVisibleIndexPaths.count > 2 else {
|
|
||||||
return .middle
|
|
||||||
}
|
|
||||||
return .none
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
// status coordinate
|
// status coordinate
|
||||||
extension StatusTableViewCellDelegate where Self: StatusProvider & StatusTableViewControllerNavigateable {
|
extension StatusTableViewControllerNavigateableCore where Self: StatusProvider {
|
||||||
|
|
||||||
private func openStatus() {
|
|
||||||
guard let indexPathForSelectedRow = tableView.indexPathForSelectedRow else { return }
|
|
||||||
StatusProviderFacade.coordinateToStatusThreadScene(for: .primary, provider: self, indexPath: indexPathForSelectedRow)
|
|
||||||
}
|
|
||||||
|
|
||||||
private func backTimeline() {
|
|
||||||
UserDefaults.shared.backKeyCommandPressDate = Date()
|
|
||||||
navigationController?.popViewController(animated: true)
|
|
||||||
}
|
|
||||||
|
|
||||||
private func openAuthorProfile() {
|
private func openAuthorProfile() {
|
||||||
guard let indexPathForSelectedRow = tableView.indexPathForSelectedRow else { return }
|
guard let indexPathForSelectedRow = tableView.indexPathForSelectedRow else { return }
|
||||||
|
@ -163,7 +80,7 @@ extension StatusTableViewCellDelegate where Self: StatusProvider & StatusTableVi
|
||||||
}
|
}
|
||||||
|
|
||||||
// toggle
|
// toggle
|
||||||
extension StatusTableViewCellDelegate where Self: StatusProvider & StatusTableViewControllerNavigateable {
|
extension StatusTableViewControllerNavigateableCore where Self: StatusProvider {
|
||||||
|
|
||||||
private func toggleReblog() {
|
private func toggleReblog() {
|
||||||
guard let indexPathForSelectedRow = tableView.indexPathForSelectedRow else { return }
|
guard let indexPathForSelectedRow = tableView.indexPathForSelectedRow else { return }
|
||||||
|
@ -181,24 +98,3 @@ extension StatusTableViewCellDelegate where Self: StatusProvider & StatusTableVi
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
extension StatusTableViewCellDelegate where Self: StatusProvider & StatusTableViewControllerNavigateable {
|
|
||||||
|
|
||||||
var statusNavigationKeyCommands: [UIKeyCommand] {
|
|
||||||
StatusTableViewNavigation.allCases.map { navigation in
|
|
||||||
UIKeyCommand(
|
|
||||||
title: navigation.title,
|
|
||||||
image: nil,
|
|
||||||
action: #selector(Self.keyCommandHandlerRelay(_:)),
|
|
||||||
input: navigation.input,
|
|
||||||
modifierFlags: navigation.modifierFlags,
|
|
||||||
propertyList: navigation.propertyList,
|
|
||||||
alternates: [],
|
|
||||||
discoverabilityTitle: nil,
|
|
||||||
attributes: [],
|
|
||||||
state: .off
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
|
@ -0,0 +1,153 @@
|
||||||
|
//
|
||||||
|
// StatusProvider+TableViewControllerNavigateable.swift
|
||||||
|
// Mastodon
|
||||||
|
//
|
||||||
|
// Created by MainasuK Cirno on 2021-5-21.
|
||||||
|
//
|
||||||
|
|
||||||
|
import os.log
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
extension TableViewControllerNavigateableCore where Self: TableViewControllerNavigateableRelay {
|
||||||
|
var navigationKeyCommands: [UIKeyCommand] {
|
||||||
|
TableViewNavigation.allCases.map { navigation in
|
||||||
|
UIKeyCommand(
|
||||||
|
title: navigation.title,
|
||||||
|
image: nil,
|
||||||
|
action: #selector(Self.navigateKeyCommandHandlerRelay(_:)),
|
||||||
|
input: navigation.input,
|
||||||
|
modifierFlags: navigation.modifierFlags,
|
||||||
|
propertyList: navigation.propertyList,
|
||||||
|
alternates: [],
|
||||||
|
discoverabilityTitle: nil,
|
||||||
|
attributes: [],
|
||||||
|
state: .off
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension TableViewControllerNavigateableCore {
|
||||||
|
|
||||||
|
func navigateKeyCommandHandler(_ sender: UIKeyCommand) {
|
||||||
|
guard let rawValue = sender.propertyList as? String,
|
||||||
|
let navigation = TableViewNavigation(rawValue: rawValue) else { return }
|
||||||
|
|
||||||
|
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: %s", ((#file as NSString).lastPathComponent), #line, #function, navigation.title)
|
||||||
|
switch navigation {
|
||||||
|
case .up: navigate(direction: .up)
|
||||||
|
case .down: navigate(direction: .down)
|
||||||
|
case .back: back()
|
||||||
|
case .open: open()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// navigate status up/down
|
||||||
|
extension TableViewControllerNavigateableCore where Self: StatusProvider {
|
||||||
|
|
||||||
|
func navigate(direction: TableViewNavigationDirection) {
|
||||||
|
if let indexPathForSelectedRow = tableView.indexPathForSelectedRow {
|
||||||
|
// navigate up/down on the current selected item
|
||||||
|
navigateToStatus(direction: direction, indexPath: indexPathForSelectedRow)
|
||||||
|
} else {
|
||||||
|
// set first visible item selected
|
||||||
|
navigateToFirstVisibleStatus()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func navigateToStatus(direction: TableViewNavigationDirection, indexPath: IndexPath) {
|
||||||
|
guard let diffableDataSource = tableViewDiffableDataSource else { return }
|
||||||
|
let items = diffableDataSource.snapshot().itemIdentifiers
|
||||||
|
guard let selectedItem = diffableDataSource.itemIdentifier(for: indexPath),
|
||||||
|
let selectedItemIndex = items.firstIndex(of: selectedItem) else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let _navigateToItem: Item? = {
|
||||||
|
var index = selectedItemIndex
|
||||||
|
while 0..<items.count ~= index {
|
||||||
|
index = {
|
||||||
|
switch direction {
|
||||||
|
case .up: return index - 1
|
||||||
|
case .down: return index + 1
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
guard 0..<items.count ~= index else { return nil }
|
||||||
|
let item = items[index]
|
||||||
|
|
||||||
|
guard Self.validNavigateableItem(item) else { continue }
|
||||||
|
return item
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}()
|
||||||
|
|
||||||
|
guard let item = _navigateToItem, let indexPath = diffableDataSource.indexPath(for: item) else { return }
|
||||||
|
let scrollPosition: UITableView.ScrollPosition = overrideNavigationScrollPosition ?? Self.navigateScrollPosition(tableView: tableView, indexPath: indexPath)
|
||||||
|
tableView.selectRow(at: indexPath, animated: true, scrollPosition: scrollPosition)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func navigateToFirstVisibleStatus() {
|
||||||
|
guard let indexPathsForVisibleRows = tableView.indexPathsForVisibleRows else { return }
|
||||||
|
guard let diffableDataSource = tableViewDiffableDataSource else { return }
|
||||||
|
|
||||||
|
var visibleItems: [Item] = indexPathsForVisibleRows.sorted().compactMap { indexPath in
|
||||||
|
guard let item = diffableDataSource.itemIdentifier(for: indexPath) else { return nil }
|
||||||
|
guard Self.validNavigateableItem(item) else { return nil }
|
||||||
|
return item
|
||||||
|
}
|
||||||
|
if indexPathsForVisibleRows.first?.row != 0, visibleItems.count > 1 {
|
||||||
|
// drop first when visible not the first cell of table
|
||||||
|
visibleItems.removeFirst()
|
||||||
|
}
|
||||||
|
guard let item = visibleItems.first, let indexPath = diffableDataSource.indexPath(for: item) else { return }
|
||||||
|
let scrollPosition: UITableView.ScrollPosition = overrideNavigationScrollPosition ?? Self.navigateScrollPosition(tableView: tableView, indexPath: indexPath)
|
||||||
|
tableView.selectRow(at: indexPath, animated: true, scrollPosition: scrollPosition)
|
||||||
|
}
|
||||||
|
|
||||||
|
static func validNavigateableItem(_ item: Item) -> Bool {
|
||||||
|
switch item {
|
||||||
|
case .homeTimelineIndex,
|
||||||
|
.status,
|
||||||
|
.root, .leaf, .reply:
|
||||||
|
return true
|
||||||
|
default:
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
extension TableViewControllerNavigateableCore {
|
||||||
|
// check is visible and not the first and last
|
||||||
|
static func navigateScrollPosition(tableView: UITableView, indexPath: IndexPath) -> UITableView.ScrollPosition {
|
||||||
|
let middleVisibleIndexPaths = (tableView.indexPathsForVisibleRows ?? [])
|
||||||
|
.sorted()
|
||||||
|
.dropFirst()
|
||||||
|
.dropLast()
|
||||||
|
guard middleVisibleIndexPaths.contains(indexPath) else {
|
||||||
|
return .top
|
||||||
|
}
|
||||||
|
guard middleVisibleIndexPaths.count > 2 else {
|
||||||
|
return .middle
|
||||||
|
}
|
||||||
|
return .none
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
extension TableViewControllerNavigateableCore where Self: StatusProvider {
|
||||||
|
func open() {
|
||||||
|
guard let indexPathForSelectedRow = tableView.indexPathForSelectedRow else { return }
|
||||||
|
StatusProviderFacade.coordinateToStatusThreadScene(for: .primary, provider: self, indexPath: indexPathForSelectedRow)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension TableViewControllerNavigateableCore where Self: UIViewController {
|
||||||
|
func back() {
|
||||||
|
UserDefaults.shared.backKeyCommandPressDate = Date()
|
||||||
|
navigationController?.popViewController(animated: true)
|
||||||
|
}
|
||||||
|
}
|
|
@ -86,14 +86,14 @@ extension StatusTableViewControllerAspect where Self: StatusTableViewCellDelegat
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
extension StatusTableViewControllerAspect where Self: TableViewCellHeightCacheableContainer & StatusProvider {
|
extension StatusTableViewControllerAspect where Self: TableViewCellHeightCacheableContainer {
|
||||||
/// [UI] hook to cache table view cell height
|
/// [UI] hook to cache table view cell height
|
||||||
func aspectTableView(_ tableView: UITableView, didEndDisplaying cell: UITableViewCell, forRowAt indexPath: IndexPath) {
|
func aspectTableView(_ tableView: UITableView, didEndDisplaying cell: UITableViewCell, forRowAt indexPath: IndexPath) {
|
||||||
cacheTableView(tableView, didEndDisplaying: cell, forRowAt: indexPath)
|
cacheTableView(tableView, didEndDisplaying: cell, forRowAt: indexPath)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
extension StatusTableViewControllerAspect where Self: StatusTableViewCellDelegate & TableViewCellHeightCacheableContainer & StatusProvider {
|
extension StatusTableViewControllerAspect where Self: StatusProvider & StatusTableViewCellDelegate & TableViewCellHeightCacheableContainer {
|
||||||
/// [Media] hook to notify video service
|
/// [Media] hook to notify video service
|
||||||
/// [UI] hook to cache table view cell height
|
/// [UI] hook to cache table view cell height
|
||||||
func aspectTableView(_ tableView: UITableView, didEndDisplaying cell: UITableViewCell, forRowAt indexPath: IndexPath) {
|
func aspectTableView(_ tableView: UITableView, didEndDisplaying cell: UITableViewCell, forRowAt indexPath: IndexPath) {
|
||||||
|
|
|
@ -10,10 +10,9 @@ import UIKit
|
||||||
|
|
||||||
typealias StatusTableViewControllerNavigateable = StatusTableViewControllerNavigateableCore & StatusTableViewControllerNavigateableRelay
|
typealias StatusTableViewControllerNavigateable = StatusTableViewControllerNavigateableCore & StatusTableViewControllerNavigateableRelay
|
||||||
|
|
||||||
protocol StatusTableViewControllerNavigateableCore: AnyObject {
|
protocol StatusTableViewControllerNavigateableCore: TableViewControllerNavigateableCore {
|
||||||
var tableView: UITableView { get }
|
var statusNavigationKeyCommands: [UIKeyCommand] { get }
|
||||||
var overrideNavigationScrollPosition: UITableView.ScrollPosition? { get set }
|
func statusKeyCommandHandler(_ sender: UIKeyCommand)
|
||||||
func keyCommandHandler(_ sender: UIKeyCommand)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
extension StatusTableViewControllerNavigateableCore {
|
extension StatusTableViewControllerNavigateableCore {
|
||||||
|
@ -23,21 +22,11 @@ extension StatusTableViewControllerNavigateableCore {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc protocol StatusTableViewControllerNavigateableRelay: AnyObject {
|
@objc protocol StatusTableViewControllerNavigateableRelay: TableViewControllerNavigateableRelay {
|
||||||
func keyCommandHandlerRelay(_ sender: UIKeyCommand)
|
func statusKeyCommandHandlerRelay(_ sender: UIKeyCommand)
|
||||||
}
|
}
|
||||||
|
|
||||||
enum StatusTableViewNavigationDirection {
|
|
||||||
case up
|
|
||||||
case down
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
enum StatusTableViewNavigation: String, CaseIterable {
|
enum StatusTableViewNavigation: String, CaseIterable {
|
||||||
case up
|
|
||||||
case down
|
|
||||||
case back // pop
|
|
||||||
case openStatus
|
|
||||||
case openAuthorProfile
|
case openAuthorProfile
|
||||||
case openRebloggerProfile
|
case openRebloggerProfile
|
||||||
case replyStatus
|
case replyStatus
|
||||||
|
@ -48,10 +37,6 @@ enum StatusTableViewNavigation: String, CaseIterable {
|
||||||
|
|
||||||
var title: String {
|
var title: String {
|
||||||
switch self {
|
switch self {
|
||||||
case .up: return L10n.Common.Controls.Keyboard.Timeline.previousStatus
|
|
||||||
case .down: return L10n.Common.Controls.Keyboard.Timeline.nextStatus
|
|
||||||
case .back: return L10n.Common.Controls.Actions.back
|
|
||||||
case .openStatus: return L10n.Common.Controls.Keyboard.Timeline.openStatus
|
|
||||||
case .openAuthorProfile: return L10n.Common.Controls.Keyboard.Timeline.openAuthorProfile
|
case .openAuthorProfile: return L10n.Common.Controls.Keyboard.Timeline.openAuthorProfile
|
||||||
case .openRebloggerProfile: return L10n.Common.Controls.Keyboard.Timeline.openRebloggerProfile
|
case .openRebloggerProfile: return L10n.Common.Controls.Keyboard.Timeline.openRebloggerProfile
|
||||||
case .replyStatus: return L10n.Common.Controls.Keyboard.Timeline.replyStatus
|
case .replyStatus: return L10n.Common.Controls.Keyboard.Timeline.replyStatus
|
||||||
|
@ -65,10 +50,6 @@ enum StatusTableViewNavigation: String, CaseIterable {
|
||||||
// UIKeyCommand input
|
// UIKeyCommand input
|
||||||
var input: String {
|
var input: String {
|
||||||
switch self {
|
switch self {
|
||||||
case .up: return "k"
|
|
||||||
case .down: return "j"
|
|
||||||
case .back: return "h"
|
|
||||||
case .openStatus: return "l" // little "L"
|
|
||||||
case .openAuthorProfile: return "p"
|
case .openAuthorProfile: return "p"
|
||||||
case .openRebloggerProfile: return "p" // + option
|
case .openRebloggerProfile: return "p" // + option
|
||||||
case .replyStatus: return "n" // + shift + command
|
case .replyStatus: return "n" // + shift + command
|
||||||
|
@ -81,10 +62,6 @@ enum StatusTableViewNavigation: String, CaseIterable {
|
||||||
|
|
||||||
var modifierFlags: UIKeyModifierFlags {
|
var modifierFlags: UIKeyModifierFlags {
|
||||||
switch self {
|
switch self {
|
||||||
case .up: return []
|
|
||||||
case .down: return []
|
|
||||||
case .back: return []
|
|
||||||
case .openStatus: return []
|
|
||||||
case .openAuthorProfile: return []
|
case .openAuthorProfile: return []
|
||||||
case .openRebloggerProfile: return [.alternate]
|
case .openRebloggerProfile: return [.alternate]
|
||||||
case .replyStatus: return [.shift, .alternate]
|
case .replyStatus: return [.shift, .alternate]
|
||||||
|
|
|
@ -7,11 +7,13 @@
|
||||||
|
|
||||||
import UIKit
|
import UIKit
|
||||||
|
|
||||||
protocol TableViewCellHeightCacheableContainer: StatusProvider {
|
protocol TableViewCellHeightCacheableContainer {
|
||||||
var cellFrameCache: NSCache<NSNumber, NSValue> { get }
|
var cellFrameCache: NSCache<NSNumber, NSValue> { get }
|
||||||
|
func cacheTableView(_ tableView: UITableView, didEndDisplaying cell: UITableViewCell, forRowAt indexPath: IndexPath)
|
||||||
|
func handleTableView(_ tableView: UITableView, estimatedHeightForRowAt indexPath: IndexPath) -> CGFloat
|
||||||
}
|
}
|
||||||
|
|
||||||
extension TableViewCellHeightCacheableContainer {
|
extension TableViewCellHeightCacheableContainer where Self: StatusProvider {
|
||||||
|
|
||||||
func cacheTableView(_ tableView: UITableView, didEndDisplaying cell: UITableViewCell, forRowAt indexPath: IndexPath) {
|
func cacheTableView(_ tableView: UITableView, didEndDisplaying cell: UITableViewCell, forRowAt indexPath: IndexPath) {
|
||||||
guard let item = item(for: nil, indexPath: indexPath) else { return }
|
guard let item = item(for: nil, indexPath: indexPath) else { return }
|
||||||
|
|
|
@ -0,0 +1,79 @@
|
||||||
|
//
|
||||||
|
// TableViewControllerNavigateable.swift
|
||||||
|
// Mastodon
|
||||||
|
//
|
||||||
|
// Created by MainasuK Cirno on 2021-5-21.
|
||||||
|
//
|
||||||
|
|
||||||
|
import os.log
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
typealias TableViewControllerNavigateable = TableViewControllerNavigateableCore & TableViewControllerNavigateableRelay
|
||||||
|
|
||||||
|
protocol TableViewControllerNavigateableCore: AnyObject {
|
||||||
|
var tableView: UITableView { get }
|
||||||
|
var overrideNavigationScrollPosition: UITableView.ScrollPosition? { get set }
|
||||||
|
var navigationKeyCommands: [UIKeyCommand] { get }
|
||||||
|
|
||||||
|
func navigateKeyCommandHandler(_ sender: UIKeyCommand)
|
||||||
|
func navigate(direction: TableViewNavigationDirection)
|
||||||
|
func open()
|
||||||
|
func back()
|
||||||
|
}
|
||||||
|
|
||||||
|
extension TableViewControllerNavigateableCore {
|
||||||
|
var overrideNavigationScrollPosition: UITableView.ScrollPosition? {
|
||||||
|
get { return nil }
|
||||||
|
set { }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc protocol TableViewControllerNavigateableRelay: AnyObject {
|
||||||
|
func navigateKeyCommandHandlerRelay(_ sender: UIKeyCommand)
|
||||||
|
}
|
||||||
|
|
||||||
|
enum TableViewNavigationDirection {
|
||||||
|
case up
|
||||||
|
case down
|
||||||
|
}
|
||||||
|
|
||||||
|
enum TableViewNavigation: String, CaseIterable {
|
||||||
|
case up
|
||||||
|
case down
|
||||||
|
case back // pop
|
||||||
|
case open
|
||||||
|
|
||||||
|
var title: String {
|
||||||
|
switch self {
|
||||||
|
case .up: return L10n.Common.Controls.Actions.previous
|
||||||
|
case .down: return L10n.Common.Controls.Actions.next
|
||||||
|
case .back: return L10n.Common.Controls.Actions.back
|
||||||
|
case .open: return L10n.Common.Controls.Actions.open
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// UIKeyCommand input
|
||||||
|
var input: String {
|
||||||
|
switch self {
|
||||||
|
case .up: return "k"
|
||||||
|
case .down: return "j"
|
||||||
|
case .back: return "h"
|
||||||
|
case .open: return "l" // little "L"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var modifierFlags: UIKeyModifierFlags {
|
||||||
|
switch self {
|
||||||
|
case .up: return []
|
||||||
|
case .down: return []
|
||||||
|
case .back: return []
|
||||||
|
case .open: return []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var propertyList: Any {
|
||||||
|
return rawValue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
|
@ -30,9 +30,12 @@ Please check your internet connection.";
|
||||||
"Common.Controls.Actions.Edit" = "Edit";
|
"Common.Controls.Actions.Edit" = "Edit";
|
||||||
"Common.Controls.Actions.FindPeople" = "Find people to follow";
|
"Common.Controls.Actions.FindPeople" = "Find people to follow";
|
||||||
"Common.Controls.Actions.ManuallySearch" = "Manually search instead";
|
"Common.Controls.Actions.ManuallySearch" = "Manually search instead";
|
||||||
|
"Common.Controls.Actions.Next" = "Next";
|
||||||
"Common.Controls.Actions.Ok" = "OK";
|
"Common.Controls.Actions.Ok" = "OK";
|
||||||
|
"Common.Controls.Actions.Open" = "Open";
|
||||||
"Common.Controls.Actions.OpenInSafari" = "Open in Safari";
|
"Common.Controls.Actions.OpenInSafari" = "Open in Safari";
|
||||||
"Common.Controls.Actions.Preview" = "Preview";
|
"Common.Controls.Actions.Preview" = "Preview";
|
||||||
|
"Common.Controls.Actions.Previous" = "Previous";
|
||||||
"Common.Controls.Actions.Remove" = "Remove";
|
"Common.Controls.Actions.Remove" = "Remove";
|
||||||
"Common.Controls.Actions.Reply" = "Reply";
|
"Common.Controls.Actions.Reply" = "Reply";
|
||||||
"Common.Controls.Actions.ReportUser" = "Report %@";
|
"Common.Controls.Actions.ReportUser" = "Report %@";
|
||||||
|
|
|
@ -30,9 +30,12 @@ Please check your internet connection.";
|
||||||
"Common.Controls.Actions.Edit" = "Edit";
|
"Common.Controls.Actions.Edit" = "Edit";
|
||||||
"Common.Controls.Actions.FindPeople" = "Find people to follow";
|
"Common.Controls.Actions.FindPeople" = "Find people to follow";
|
||||||
"Common.Controls.Actions.ManuallySearch" = "Manually search instead";
|
"Common.Controls.Actions.ManuallySearch" = "Manually search instead";
|
||||||
|
"Common.Controls.Actions.Next" = "Next";
|
||||||
"Common.Controls.Actions.Ok" = "OK";
|
"Common.Controls.Actions.Ok" = "OK";
|
||||||
|
"Common.Controls.Actions.Open" = "Open";
|
||||||
"Common.Controls.Actions.OpenInSafari" = "Open in Safari";
|
"Common.Controls.Actions.OpenInSafari" = "Open in Safari";
|
||||||
"Common.Controls.Actions.Preview" = "Preview";
|
"Common.Controls.Actions.Preview" = "Preview";
|
||||||
|
"Common.Controls.Actions.Previous" = "Previous";
|
||||||
"Common.Controls.Actions.Remove" = "Remove";
|
"Common.Controls.Actions.Remove" = "Remove";
|
||||||
"Common.Controls.Actions.Reply" = "Reply";
|
"Common.Controls.Actions.Reply" = "Reply";
|
||||||
"Common.Controls.Actions.ReportUser" = "Report %@";
|
"Common.Controls.Actions.ReportUser" = "Report %@";
|
||||||
|
|
|
@ -342,13 +342,17 @@ extension HashtagTimelineViewController: StatusTableViewCellDelegate {
|
||||||
|
|
||||||
extension HashtagTimelineViewController {
|
extension HashtagTimelineViewController {
|
||||||
override var keyCommands: [UIKeyCommand]? {
|
override var keyCommands: [UIKeyCommand]? {
|
||||||
return statusNavigationKeyCommands
|
return navigationKeyCommands + statusNavigationKeyCommands
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - StatusTableViewControllerNavigateable
|
// MARK: - StatusTableViewControllerNavigateable
|
||||||
extension HashtagTimelineViewController: StatusTableViewControllerNavigateable {
|
extension HashtagTimelineViewController: StatusTableViewControllerNavigateable {
|
||||||
@objc func keyCommandHandlerRelay(_ sender: UIKeyCommand) {
|
@objc func navigateKeyCommandHandlerRelay(_ sender: UIKeyCommand) {
|
||||||
keyCommandHandler(sender)
|
navigateKeyCommandHandler(sender)
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc func statusKeyCommandHandlerRelay(_ sender: UIKeyCommand) {
|
||||||
|
statusKeyCommandHandler(sender)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -540,13 +540,17 @@ extension HomeTimelineViewController: HomeTimelineNavigationBarTitleViewDelegate
|
||||||
|
|
||||||
extension HomeTimelineViewController {
|
extension HomeTimelineViewController {
|
||||||
override var keyCommands: [UIKeyCommand]? {
|
override var keyCommands: [UIKeyCommand]? {
|
||||||
return statusNavigationKeyCommands
|
return navigationKeyCommands + statusNavigationKeyCommands
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - StatusTableViewControllerNavigateable
|
// MARK: - StatusTableViewControllerNavigateable
|
||||||
extension HomeTimelineViewController: StatusTableViewControllerNavigateable {
|
extension HomeTimelineViewController: StatusTableViewControllerNavigateable {
|
||||||
@objc func keyCommandHandlerRelay(_ sender: UIKeyCommand) {
|
@objc func navigateKeyCommandHandlerRelay(_ sender: UIKeyCommand) {
|
||||||
keyCommandHandler(sender)
|
navigateKeyCommandHandler(sender)
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc func statusKeyCommandHandlerRelay(_ sender: UIKeyCommand) {
|
||||||
|
statusKeyCommandHandler(sender)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -141,7 +141,15 @@ extension NotificationViewController {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
extension NotificationViewController {
|
// MARK: - StatusTableViewControllerAspect
|
||||||
|
extension NotificationViewController: StatusTableViewControllerAspect { }
|
||||||
|
|
||||||
|
// MARK: - TableViewCellHeightCacheableContainer
|
||||||
|
extension NotificationViewController: TableViewCellHeightCacheableContainer {
|
||||||
|
var cellFrameCache: NSCache<NSNumber, NSValue> {
|
||||||
|
viewModel.cellFrameCache
|
||||||
|
}
|
||||||
|
|
||||||
func cacheTableView(_ tableView: UITableView, didEndDisplaying cell: UITableViewCell, forRowAt indexPath: IndexPath) {
|
func cacheTableView(_ tableView: UITableView, didEndDisplaying cell: UITableViewCell, forRowAt indexPath: IndexPath) {
|
||||||
guard let diffableDataSource = viewModel.diffableDataSource else { return }
|
guard let diffableDataSource = viewModel.diffableDataSource else { return }
|
||||||
guard let item = diffableDataSource.itemIdentifier(for: indexPath) else { return }
|
guard let item = diffableDataSource.itemIdentifier(for: indexPath) else { return }
|
||||||
|
@ -171,6 +179,13 @@ extension NotificationViewController: UITableViewDelegate {
|
||||||
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
|
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
|
||||||
guard let diffableDataSource = viewModel.diffableDataSource else { return }
|
guard let diffableDataSource = viewModel.diffableDataSource else { return }
|
||||||
guard let item = diffableDataSource.itemIdentifier(for: indexPath) else { return }
|
guard let item = diffableDataSource.itemIdentifier(for: indexPath) else { return }
|
||||||
|
open(item: item)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
extension NotificationViewController {
|
||||||
|
private func open(item: NotificationItem) {
|
||||||
switch item {
|
switch item {
|
||||||
case .notification(let objectID, _):
|
case .notification(let objectID, _):
|
||||||
let notification = context.managedObjectContext.object(with: objectID) as! MastodonNotification
|
let notification = context.managedObjectContext.object(with: objectID) as! MastodonNotification
|
||||||
|
@ -256,3 +271,92 @@ extension NotificationViewController: LoadMoreConfigurableTableViewContainer {
|
||||||
var loadMoreConfigurableTableView: UITableView { tableView }
|
var loadMoreConfigurableTableView: UITableView { tableView }
|
||||||
var loadMoreConfigurableStateMachine: GKStateMachine { viewModel.loadoldestStateMachine }
|
var loadMoreConfigurableStateMachine: GKStateMachine { viewModel.loadoldestStateMachine }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
extension NotificationViewController {
|
||||||
|
override var keyCommands: [UIKeyCommand]? {
|
||||||
|
return navigationKeyCommands
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension NotificationViewController: TableViewControllerNavigateable {
|
||||||
|
|
||||||
|
func navigate(direction: TableViewNavigationDirection) {
|
||||||
|
if let indexPathForSelectedRow = tableView.indexPathForSelectedRow {
|
||||||
|
// navigate up/down on the current selected item
|
||||||
|
navigateToStatus(direction: direction, indexPath: indexPathForSelectedRow)
|
||||||
|
} else {
|
||||||
|
// set first visible item selected
|
||||||
|
navigateToFirstVisibleStatus()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func navigateToStatus(direction: TableViewNavigationDirection, indexPath: IndexPath) {
|
||||||
|
guard let diffableDataSource = viewModel.diffableDataSource else { return }
|
||||||
|
let items = diffableDataSource.snapshot().itemIdentifiers
|
||||||
|
guard let selectedItem = diffableDataSource.itemIdentifier(for: indexPath),
|
||||||
|
let selectedItemIndex = items.firstIndex(of: selectedItem) else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let _navigateToItem: NotificationItem? = {
|
||||||
|
var index = selectedItemIndex
|
||||||
|
while 0..<items.count ~= index {
|
||||||
|
index = {
|
||||||
|
switch direction {
|
||||||
|
case .up: return index - 1
|
||||||
|
case .down: return index + 1
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
guard 0..<items.count ~= index else { return nil }
|
||||||
|
let item = items[index]
|
||||||
|
|
||||||
|
guard Self.validNavigateableItem(item) else { continue }
|
||||||
|
return item
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}()
|
||||||
|
|
||||||
|
guard let item = _navigateToItem, let indexPath = diffableDataSource.indexPath(for: item) else { return }
|
||||||
|
let scrollPosition: UITableView.ScrollPosition = overrideNavigationScrollPosition ?? Self.navigateScrollPosition(tableView: tableView, indexPath: indexPath)
|
||||||
|
tableView.selectRow(at: indexPath, animated: true, scrollPosition: scrollPosition)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func navigateToFirstVisibleStatus() {
|
||||||
|
guard let indexPathsForVisibleRows = tableView.indexPathsForVisibleRows else { return }
|
||||||
|
guard let diffableDataSource = viewModel.diffableDataSource else { return }
|
||||||
|
|
||||||
|
var visibleItems: [NotificationItem] = indexPathsForVisibleRows.sorted().compactMap { indexPath in
|
||||||
|
guard let item = diffableDataSource.itemIdentifier(for: indexPath) else { return nil }
|
||||||
|
guard Self.validNavigateableItem(item) else { return nil }
|
||||||
|
return item
|
||||||
|
}
|
||||||
|
if indexPathsForVisibleRows.first?.row != 0, visibleItems.count > 1 {
|
||||||
|
// drop first when visible not the first cell of table
|
||||||
|
visibleItems.removeFirst()
|
||||||
|
}
|
||||||
|
guard let item = visibleItems.first, let indexPath = diffableDataSource.indexPath(for: item) else { return }
|
||||||
|
let scrollPosition: UITableView.ScrollPosition = overrideNavigationScrollPosition ?? Self.navigateScrollPosition(tableView: tableView, indexPath: indexPath)
|
||||||
|
tableView.selectRow(at: indexPath, animated: true, scrollPosition: scrollPosition)
|
||||||
|
}
|
||||||
|
|
||||||
|
static func validNavigateableItem(_ item: NotificationItem) -> Bool {
|
||||||
|
switch item {
|
||||||
|
case .notification:
|
||||||
|
return true
|
||||||
|
default:
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func open() {
|
||||||
|
guard let indexPathForSelectedRow = tableView.indexPathForSelectedRow else { return }
|
||||||
|
guard let diffableDataSource = viewModel.diffableDataSource else { return }
|
||||||
|
guard let item = diffableDataSource.itemIdentifier(for: indexPathForSelectedRow) else { return }
|
||||||
|
open(item: item)
|
||||||
|
}
|
||||||
|
|
||||||
|
func navigateKeyCommandHandlerRelay(_ sender: UIKeyCommand) {
|
||||||
|
navigateKeyCommandHandler(sender)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
|
@ -176,13 +176,17 @@ extension FavoriteViewController: LoadMoreConfigurableTableViewContainer {
|
||||||
|
|
||||||
extension FavoriteViewController {
|
extension FavoriteViewController {
|
||||||
override var keyCommands: [UIKeyCommand]? {
|
override var keyCommands: [UIKeyCommand]? {
|
||||||
return statusNavigationKeyCommands
|
return navigationKeyCommands + statusNavigationKeyCommands
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - StatusTableViewControllerNavigateable
|
// MARK: - StatusTableViewControllerNavigateable
|
||||||
extension FavoriteViewController: StatusTableViewControllerNavigateable {
|
extension FavoriteViewController: StatusTableViewControllerNavigateable {
|
||||||
@objc func keyCommandHandlerRelay(_ sender: UIKeyCommand) {
|
@objc func navigateKeyCommandHandlerRelay(_ sender: UIKeyCommand) {
|
||||||
keyCommandHandler(sender)
|
navigateKeyCommandHandler(sender)
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc func statusKeyCommandHandlerRelay(_ sender: UIKeyCommand) {
|
||||||
|
statusKeyCommandHandler(sender)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -65,7 +65,7 @@ extension ProfilePagingViewController {
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc func keyCommandHandlerRelay(_ sender: UIKeyCommand) {
|
@objc func keyCommandHandlerRelay(_ sender: UIKeyCommand) {
|
||||||
(currentViewController as? StatusTableViewControllerNavigateable)?.keyCommandHandlerRelay(sender)
|
(currentViewController as? StatusTableViewControllerNavigateable)?.statusKeyCommandHandlerRelay(sender)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -190,19 +190,17 @@ extension UserTimelineViewController: LoadMoreConfigurableTableViewContainer {
|
||||||
|
|
||||||
extension UserTimelineViewController {
|
extension UserTimelineViewController {
|
||||||
override var keyCommands: [UIKeyCommand]? {
|
override var keyCommands: [UIKeyCommand]? {
|
||||||
return [
|
return navigationKeyCommands + statusNavigationKeyCommands
|
||||||
UIKeyCommand(title: "Test", image: nil, action: #selector(UserTimelineViewController.test(_:)), input: "t", modifierFlags: [], propertyList: nil, alternates: [], discoverabilityTitle: nil, attributes: [], state: .off)
|
|
||||||
] + statusNavigationKeyCommands
|
|
||||||
}
|
|
||||||
|
|
||||||
@objc private func test(_ sender: UIKeyCommand) {
|
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - StatusTableViewControllerNavigateable
|
// MARK: - StatusTableViewControllerNavigateable
|
||||||
extension UserTimelineViewController: StatusTableViewControllerNavigateable {
|
extension UserTimelineViewController: StatusTableViewControllerNavigateable {
|
||||||
@objc func keyCommandHandlerRelay(_ sender: UIKeyCommand) {
|
@objc func navigateKeyCommandHandlerRelay(_ sender: UIKeyCommand) {
|
||||||
keyCommandHandler(sender)
|
navigateKeyCommandHandler(sender)
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc func statusKeyCommandHandlerRelay(_ sender: UIKeyCommand) {
|
||||||
|
statusKeyCommandHandler(sender)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -230,13 +230,17 @@ extension ThreadViewController: ThreadReplyLoaderTableViewCellDelegate {
|
||||||
|
|
||||||
extension ThreadViewController {
|
extension ThreadViewController {
|
||||||
override var keyCommands: [UIKeyCommand]? {
|
override var keyCommands: [UIKeyCommand]? {
|
||||||
return statusNavigationKeyCommands
|
return navigationKeyCommands + statusNavigationKeyCommands
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - StatusTableViewControllerNavigateable
|
// MARK: - StatusTableViewControllerNavigateable
|
||||||
extension ThreadViewController: StatusTableViewControllerNavigateable {
|
extension ThreadViewController: StatusTableViewControllerNavigateable {
|
||||||
@objc func keyCommandHandlerRelay(_ sender: UIKeyCommand) {
|
@objc func navigateKeyCommandHandlerRelay(_ sender: UIKeyCommand) {
|
||||||
keyCommandHandler(sender)
|
navigateKeyCommandHandler(sender)
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc func statusKeyCommandHandlerRelay(_ sender: UIKeyCommand) {
|
||||||
|
statusKeyCommandHandler(sender)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue