feat: add navigation shortcut for notification scene

This commit is contained in:
CMK 2021-05-21 16:52:47 +08:00
parent c44ced7501
commit aec7a1f5ea
18 changed files with 433 additions and 185 deletions

View File

@ -44,6 +44,9 @@
"controls": {
"actions": {
"back": "Back",
"next": "Next",
"previous": "Previous",
"open": "Open",
"add": "Add",
"remove": "Remove",
"edit": "Edit",

View File

@ -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 */,

View File

@ -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

View File

@ -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
)
}
}
}

View File

@ -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)
}
}

View File

@ -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) {

View File

@ -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]

View File

@ -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 }

View File

@ -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
}
}

View File

@ -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 %@";

View File

@ -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 %@";

View File

@ -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)
}
}

View File

@ -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)
}
}

View File

@ -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)
}
}

View File

@ -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)
}
}

View File

@ -65,7 +65,7 @@ extension ProfilePagingViewController {
}
@objc func keyCommandHandlerRelay(_ sender: UIKeyCommand) {
(currentViewController as? StatusTableViewControllerNavigateable)?.keyCommandHandlerRelay(sender)
(currentViewController as? StatusTableViewControllerNavigateable)?.statusKeyCommandHandlerRelay(sender)
}
}

View File

@ -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)
}
}

View File

@ -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)
}
}