forked from zelo72/mastodon-ios
feat: add navigation shortcut for notification scene
This commit is contained in:
parent
c44ced7501
commit
aec7a1f5ea
|
@ -44,6 +44,9 @@
|
|||
"controls": {
|
||||
"actions": {
|
||||
"back": "Back",
|
||||
"next": "Next",
|
||||
"previous": "Previous",
|
||||
"open": "Open",
|
||||
"add": "Add",
|
||||
"remove": "Remove",
|
||||
"edit": "Edit",
|
||||
|
|
|
@ -200,6 +200,8 @@
|
|||
DB1D842C26551A1C000346B3 /* StatusProvider+StatusTableViewKeyCommandNavigateable.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB1D842B26551A1C000346B3 /* StatusProvider+StatusTableViewKeyCommandNavigateable.swift */; };
|
||||
DB1D842E26552C4D000346B3 /* StatusTableViewControllerNavigateable.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB1D842D26552C4D000346B3 /* StatusTableViewControllerNavigateable.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 */; };
|
||||
DB1E347825F519300079D7DF /* PickServerItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB1E347725F519300079D7DF /* PickServerItem.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>"; };
|
||||
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>"; };
|
||||
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>"; };
|
||||
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>"; };
|
||||
|
@ -1247,6 +1251,7 @@
|
|||
2DF75BA025D0E29D00694EC8 /* StatusProvider+StatusTableViewCellDelegate.swift */,
|
||||
DB59F0FD25EF5D96001F1DAB /* StatusProvider+UITableViewDelegate.swift */,
|
||||
DB71FD4525F8C6D200512AE1 /* StatusProvider+UITableViewDataSourcePrefetching.swift */,
|
||||
DB1D843526579DB5000346B3 /* StatusProvider+TableViewControllerNavigateable.swift */,
|
||||
DB1D842B26551A1C000346B3 /* StatusProvider+StatusTableViewKeyCommandNavigateable.swift */,
|
||||
);
|
||||
path = StatusProvider;
|
||||
|
@ -1348,6 +1353,7 @@
|
|||
2D38F20725CD491300561493 /* DisposeBagCollectable.swift */,
|
||||
5DF1058425F88AE500D6C0D4 /* NeedsDependency+AVPlayerViewControllerDelegate.swift */,
|
||||
DBE3CE12261D7D4200430CC6 /* StatusTableViewControllerAspect.swift */,
|
||||
DB1D843326579931000346B3 /* TableViewControllerNavigateable.swift */,
|
||||
DB1D842D26552C4D000346B3 /* StatusTableViewControllerNavigateable.swift */,
|
||||
);
|
||||
path = Protocol;
|
||||
|
@ -2907,6 +2913,7 @@
|
|||
DBE3CDCF261C42ED00430CC6 /* TimelineHeaderView.swift in Sources */,
|
||||
DBAE3F8E2616E0B1004B8251 /* APIService+Block.swift in Sources */,
|
||||
5DF1057F25F88A4100D6C0D4 /* TouchBlockingView.swift in Sources */,
|
||||
DB1D843426579931000346B3 /* TableViewControllerNavigateable.swift in Sources */,
|
||||
0FAA0FDF25E0B57E0017CCDE /* WelcomeViewController.swift in Sources */,
|
||||
2D206B8C25F6015000143C56 /* AudioPlaybackService.swift in Sources */,
|
||||
2D59819B25E4A581000FB903 /* MastodonConfirmEmailViewController.swift in Sources */,
|
||||
|
@ -3256,6 +3263,7 @@
|
|||
0FB3D31E25E534C700AAD544 /* PickServerCategoryCollectionViewCell.swift in Sources */,
|
||||
DB1D842C26551A1C000346B3 /* StatusProvider+StatusTableViewKeyCommandNavigateable.swift in Sources */,
|
||||
5DF1057925F88A1D00D6C0D4 /* PlayerContainerView.swift in Sources */,
|
||||
DB1D843626579DB5000346B3 /* StatusProvider+TableViewControllerNavigateable.swift in Sources */,
|
||||
DB6D9F4926353FD7008423CD /* Subscription.swift in Sources */,
|
||||
DB45FB0F25CA87D0005A8AC7 /* AuthenticationService.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")
|
||||
/// Manually search instead
|
||||
internal static let manuallySearch = L10n.tr("Localizable", "Common.Controls.Actions.ManuallySearch")
|
||||
/// Next
|
||||
internal static let next = L10n.tr("Localizable", "Common.Controls.Actions.Next")
|
||||
/// 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
|
||||
internal static let openInSafari = L10n.tr("Localizable", "Common.Controls.Actions.OpenInSafari")
|
||||
/// 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
|
||||
internal static let remove = L10n.tr("Localizable", "Common.Controls.Actions.Remove")
|
||||
/// Reply
|
||||
|
|
|
@ -8,18 +8,35 @@
|
|||
import os.log
|
||||
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,
|
||||
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)
|
||||
switch navigation {
|
||||
case .up: navigateStatus(direction: .up)
|
||||
case .down: navigateStatus(direction: .down)
|
||||
case .back: backTimeline()
|
||||
case .openStatus: openStatus()
|
||||
case .openAuthorProfile: openAuthorProfile()
|
||||
case .openRebloggerProfile: openRebloggerProfile()
|
||||
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
|
||||
extension StatusTableViewCellDelegate where Self: StatusProvider & StatusTableViewControllerNavigateable {
|
||||
|
||||
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)
|
||||
}
|
||||
extension StatusTableViewControllerNavigateableCore where Self: StatusProvider {
|
||||
|
||||
private func openAuthorProfile() {
|
||||
guard let indexPathForSelectedRow = tableView.indexPathForSelectedRow else { return }
|
||||
|
@ -163,7 +80,7 @@ extension StatusTableViewCellDelegate where Self: StatusProvider & StatusTableVi
|
|||
}
|
||||
|
||||
// toggle
|
||||
extension StatusTableViewCellDelegate where Self: StatusProvider & StatusTableViewControllerNavigateable {
|
||||
extension StatusTableViewControllerNavigateableCore where Self: StatusProvider {
|
||||
|
||||
private func toggleReblog() {
|
||||
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
|
||||
func aspectTableView(_ tableView: UITableView, didEndDisplaying cell: UITableViewCell, forRowAt indexPath: 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
|
||||
/// [UI] hook to cache table view cell height
|
||||
func aspectTableView(_ tableView: UITableView, didEndDisplaying cell: UITableViewCell, forRowAt indexPath: IndexPath) {
|
||||
|
|
|
@ -10,10 +10,9 @@ import UIKit
|
|||
|
||||
typealias StatusTableViewControllerNavigateable = StatusTableViewControllerNavigateableCore & StatusTableViewControllerNavigateableRelay
|
||||
|
||||
protocol StatusTableViewControllerNavigateableCore: AnyObject {
|
||||
var tableView: UITableView { get }
|
||||
var overrideNavigationScrollPosition: UITableView.ScrollPosition? { get set }
|
||||
func keyCommandHandler(_ sender: UIKeyCommand)
|
||||
protocol StatusTableViewControllerNavigateableCore: TableViewControllerNavigateableCore {
|
||||
var statusNavigationKeyCommands: [UIKeyCommand] { get }
|
||||
func statusKeyCommandHandler(_ sender: UIKeyCommand)
|
||||
}
|
||||
|
||||
extension StatusTableViewControllerNavigateableCore {
|
||||
|
@ -23,21 +22,11 @@ extension StatusTableViewControllerNavigateableCore {
|
|||
}
|
||||
}
|
||||
|
||||
@objc protocol StatusTableViewControllerNavigateableRelay: AnyObject {
|
||||
func keyCommandHandlerRelay(_ sender: UIKeyCommand)
|
||||
@objc protocol StatusTableViewControllerNavigateableRelay: TableViewControllerNavigateableRelay {
|
||||
func statusKeyCommandHandlerRelay(_ sender: UIKeyCommand)
|
||||
}
|
||||
|
||||
enum StatusTableViewNavigationDirection {
|
||||
case up
|
||||
case down
|
||||
}
|
||||
|
||||
|
||||
enum StatusTableViewNavigation: String, CaseIterable {
|
||||
case up
|
||||
case down
|
||||
case back // pop
|
||||
case openStatus
|
||||
case openAuthorProfile
|
||||
case openRebloggerProfile
|
||||
case replyStatus
|
||||
|
@ -48,10 +37,6 @@ enum StatusTableViewNavigation: String, CaseIterable {
|
|||
|
||||
var title: String {
|
||||
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 .openRebloggerProfile: return L10n.Common.Controls.Keyboard.Timeline.openRebloggerProfile
|
||||
case .replyStatus: return L10n.Common.Controls.Keyboard.Timeline.replyStatus
|
||||
|
@ -65,10 +50,6 @@ enum StatusTableViewNavigation: String, CaseIterable {
|
|||
// UIKeyCommand input
|
||||
var input: String {
|
||||
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 .openRebloggerProfile: return "p" // + option
|
||||
case .replyStatus: return "n" // + shift + command
|
||||
|
@ -81,10 +62,6 @@ enum StatusTableViewNavigation: String, CaseIterable {
|
|||
|
||||
var modifierFlags: UIKeyModifierFlags {
|
||||
switch self {
|
||||
case .up: return []
|
||||
case .down: return []
|
||||
case .back: return []
|
||||
case .openStatus: return []
|
||||
case .openAuthorProfile: return []
|
||||
case .openRebloggerProfile: return [.alternate]
|
||||
case .replyStatus: return [.shift, .alternate]
|
||||
|
|
|
@ -7,11 +7,13 @@
|
|||
|
||||
import UIKit
|
||||
|
||||
protocol TableViewCellHeightCacheableContainer: StatusProvider {
|
||||
protocol TableViewCellHeightCacheableContainer {
|
||||
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) {
|
||||
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.FindPeople" = "Find people to follow";
|
||||
"Common.Controls.Actions.ManuallySearch" = "Manually search instead";
|
||||
"Common.Controls.Actions.Next" = "Next";
|
||||
"Common.Controls.Actions.Ok" = "OK";
|
||||
"Common.Controls.Actions.Open" = "Open";
|
||||
"Common.Controls.Actions.OpenInSafari" = "Open in Safari";
|
||||
"Common.Controls.Actions.Preview" = "Preview";
|
||||
"Common.Controls.Actions.Previous" = "Previous";
|
||||
"Common.Controls.Actions.Remove" = "Remove";
|
||||
"Common.Controls.Actions.Reply" = "Reply";
|
||||
"Common.Controls.Actions.ReportUser" = "Report %@";
|
||||
|
|
|
@ -30,9 +30,12 @@ Please check your internet connection.";
|
|||
"Common.Controls.Actions.Edit" = "Edit";
|
||||
"Common.Controls.Actions.FindPeople" = "Find people to follow";
|
||||
"Common.Controls.Actions.ManuallySearch" = "Manually search instead";
|
||||
"Common.Controls.Actions.Next" = "Next";
|
||||
"Common.Controls.Actions.Ok" = "OK";
|
||||
"Common.Controls.Actions.Open" = "Open";
|
||||
"Common.Controls.Actions.OpenInSafari" = "Open in Safari";
|
||||
"Common.Controls.Actions.Preview" = "Preview";
|
||||
"Common.Controls.Actions.Previous" = "Previous";
|
||||
"Common.Controls.Actions.Remove" = "Remove";
|
||||
"Common.Controls.Actions.Reply" = "Reply";
|
||||
"Common.Controls.Actions.ReportUser" = "Report %@";
|
||||
|
|
|
@ -342,13 +342,17 @@ extension HashtagTimelineViewController: StatusTableViewCellDelegate {
|
|||
|
||||
extension HashtagTimelineViewController {
|
||||
override var keyCommands: [UIKeyCommand]? {
|
||||
return statusNavigationKeyCommands
|
||||
return navigationKeyCommands + statusNavigationKeyCommands
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - StatusTableViewControllerNavigateable
|
||||
extension HashtagTimelineViewController: StatusTableViewControllerNavigateable {
|
||||
@objc func keyCommandHandlerRelay(_ sender: UIKeyCommand) {
|
||||
keyCommandHandler(sender)
|
||||
@objc func navigateKeyCommandHandlerRelay(_ sender: UIKeyCommand) {
|
||||
navigateKeyCommandHandler(sender)
|
||||
}
|
||||
|
||||
@objc func statusKeyCommandHandlerRelay(_ sender: UIKeyCommand) {
|
||||
statusKeyCommandHandler(sender)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -540,13 +540,17 @@ extension HomeTimelineViewController: HomeTimelineNavigationBarTitleViewDelegate
|
|||
|
||||
extension HomeTimelineViewController {
|
||||
override var keyCommands: [UIKeyCommand]? {
|
||||
return statusNavigationKeyCommands
|
||||
return navigationKeyCommands + statusNavigationKeyCommands
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - StatusTableViewControllerNavigateable
|
||||
extension HomeTimelineViewController: StatusTableViewControllerNavigateable {
|
||||
@objc func keyCommandHandlerRelay(_ sender: UIKeyCommand) {
|
||||
keyCommandHandler(sender)
|
||||
extension HomeTimelineViewController: StatusTableViewControllerNavigateable {
|
||||
@objc func navigateKeyCommandHandlerRelay(_ sender: UIKeyCommand) {
|
||||
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) {
|
||||
guard let diffableDataSource = viewModel.diffableDataSource 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) {
|
||||
guard let diffableDataSource = viewModel.diffableDataSource else { return }
|
||||
guard let item = diffableDataSource.itemIdentifier(for: indexPath) else { return }
|
||||
open(item: item)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension NotificationViewController {
|
||||
private func open(item: NotificationItem) {
|
||||
switch item {
|
||||
case .notification(let objectID, _):
|
||||
let notification = context.managedObjectContext.object(with: objectID) as! MastodonNotification
|
||||
|
@ -256,3 +271,92 @@ extension NotificationViewController: LoadMoreConfigurableTableViewContainer {
|
|||
var loadMoreConfigurableTableView: UITableView { tableView }
|
||||
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 {
|
||||
override var keyCommands: [UIKeyCommand]? {
|
||||
return statusNavigationKeyCommands
|
||||
return navigationKeyCommands + statusNavigationKeyCommands
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - StatusTableViewControllerNavigateable
|
||||
extension FavoriteViewController: StatusTableViewControllerNavigateable {
|
||||
@objc func keyCommandHandlerRelay(_ sender: UIKeyCommand) {
|
||||
keyCommandHandler(sender)
|
||||
@objc func navigateKeyCommandHandlerRelay(_ sender: UIKeyCommand) {
|
||||
navigateKeyCommandHandler(sender)
|
||||
}
|
||||
|
||||
@objc func statusKeyCommandHandlerRelay(_ sender: UIKeyCommand) {
|
||||
statusKeyCommandHandler(sender)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -65,7 +65,7 @@ extension ProfilePagingViewController {
|
|||
}
|
||||
|
||||
@objc func keyCommandHandlerRelay(_ sender: UIKeyCommand) {
|
||||
(currentViewController as? StatusTableViewControllerNavigateable)?.keyCommandHandlerRelay(sender)
|
||||
(currentViewController as? StatusTableViewControllerNavigateable)?.statusKeyCommandHandlerRelay(sender)
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -190,19 +190,17 @@ extension UserTimelineViewController: LoadMoreConfigurableTableViewContainer {
|
|||
|
||||
extension UserTimelineViewController {
|
||||
override var keyCommands: [UIKeyCommand]? {
|
||||
return [
|
||||
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) {
|
||||
|
||||
return navigationKeyCommands + statusNavigationKeyCommands
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - StatusTableViewControllerNavigateable
|
||||
extension UserTimelineViewController: StatusTableViewControllerNavigateable {
|
||||
@objc func keyCommandHandlerRelay(_ sender: UIKeyCommand) {
|
||||
keyCommandHandler(sender)
|
||||
@objc func navigateKeyCommandHandlerRelay(_ sender: UIKeyCommand) {
|
||||
navigateKeyCommandHandler(sender)
|
||||
}
|
||||
|
||||
@objc func statusKeyCommandHandlerRelay(_ sender: UIKeyCommand) {
|
||||
statusKeyCommandHandler(sender)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -230,13 +230,17 @@ extension ThreadViewController: ThreadReplyLoaderTableViewCellDelegate {
|
|||
|
||||
extension ThreadViewController {
|
||||
override var keyCommands: [UIKeyCommand]? {
|
||||
return statusNavigationKeyCommands
|
||||
return navigationKeyCommands + statusNavigationKeyCommands
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - StatusTableViewControllerNavigateable
|
||||
extension ThreadViewController: StatusTableViewControllerNavigateable {
|
||||
@objc func keyCommandHandlerRelay(_ sender: UIKeyCommand) {
|
||||
keyCommandHandler(sender)
|
||||
@objc func navigateKeyCommandHandlerRelay(_ sender: UIKeyCommand) {
|
||||
navigateKeyCommandHandler(sender)
|
||||
}
|
||||
|
||||
@objc func statusKeyCommandHandlerRelay(_ sender: UIKeyCommand) {
|
||||
statusKeyCommandHandler(sender)
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue