diff --git a/Localization/app.json b/Localization/app.json index 0830a08b3..34dd5221b 100644 --- a/Localization/app.json +++ b/Localization/app.json @@ -44,6 +44,9 @@ "controls": { "actions": { "back": "Back", + "next": "Next", + "previous": "Previous", + "open": "Open", "add": "Add", "remove": "Remove", "edit": "Edit", diff --git a/Mastodon.xcodeproj/project.pbxproj b/Mastodon.xcodeproj/project.pbxproj index 37a480c77..b838d6c5c 100644 --- a/Mastodon.xcodeproj/project.pbxproj +++ b/Mastodon.xcodeproj/project.pbxproj @@ -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 = ""; }; DB1D842D26552C4D000346B3 /* StatusTableViewControllerNavigateable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusTableViewControllerNavigateable.swift; sourceTree = ""; }; DB1D842F26566512000346B3 /* KeyboardPreference.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyboardPreference.swift; sourceTree = ""; }; + DB1D843326579931000346B3 /* TableViewControllerNavigateable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TableViewControllerNavigateable.swift; sourceTree = ""; }; + DB1D843526579DB5000346B3 /* StatusProvider+TableViewControllerNavigateable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "StatusProvider+TableViewControllerNavigateable.swift"; sourceTree = ""; }; DB1E346725F518E20079D7DF /* CategoryPickerSection.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CategoryPickerSection.swift; sourceTree = ""; }; DB1E347725F519300079D7DF /* PickServerItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PickServerItem.swift; sourceTree = ""; }; DB1FD43525F26899004CFCFC /* MastodonPickServerViewModel+LoadIndexedServerState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MastodonPickServerViewModel+LoadIndexedServerState.swift"; sourceTree = ""; }; @@ -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 */, diff --git a/Mastodon/Generated/Strings.swift b/Mastodon/Generated/Strings.swift index 42498794c..588f12caa 100644 --- a/Mastodon/Generated/Strings.swift +++ b/Mastodon/Generated/Strings.swift @@ -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 diff --git a/Mastodon/Protocol/StatusProvider/StatusProvider+StatusTableViewKeyCommandNavigateable.swift b/Mastodon/Protocol/StatusProvider/StatusProvider+StatusTableViewKeyCommandNavigateable.swift index 3055b0d04..4503057a1 100644 --- a/Mastodon/Protocol/StatusProvider/StatusProvider+StatusTableViewKeyCommandNavigateable.swift +++ b/Mastodon/Protocol/StatusProvider/StatusProvider+StatusTableViewKeyCommandNavigateable.swift @@ -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.. 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 - ) - } - } - -} diff --git a/Mastodon/Protocol/StatusProvider/StatusProvider+TableViewControllerNavigateable.swift b/Mastodon/Protocol/StatusProvider/StatusProvider+TableViewControllerNavigateable.swift new file mode 100644 index 000000000..8be4acd59 --- /dev/null +++ b/Mastodon/Protocol/StatusProvider/StatusProvider+TableViewControllerNavigateable.swift @@ -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.. 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) + } +} diff --git a/Mastodon/Protocol/StatusTableViewControllerAspect.swift b/Mastodon/Protocol/StatusTableViewControllerAspect.swift index 58ffd6e55..c118b1206 100644 --- a/Mastodon/Protocol/StatusTableViewControllerAspect.swift +++ b/Mastodon/Protocol/StatusTableViewControllerAspect.swift @@ -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) { diff --git a/Mastodon/Protocol/StatusTableViewControllerNavigateable.swift b/Mastodon/Protocol/StatusTableViewControllerNavigateable.swift index cd88abe0e..ad869fbd1 100644 --- a/Mastodon/Protocol/StatusTableViewControllerNavigateable.swift +++ b/Mastodon/Protocol/StatusTableViewControllerNavigateable.swift @@ -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] diff --git a/Mastodon/Protocol/TableViewCellHeightCacheableContainer.swift b/Mastodon/Protocol/TableViewCellHeightCacheableContainer.swift index 0907db56f..8ae7398c9 100644 --- a/Mastodon/Protocol/TableViewCellHeightCacheableContainer.swift +++ b/Mastodon/Protocol/TableViewCellHeightCacheableContainer.swift @@ -7,11 +7,13 @@ import UIKit -protocol TableViewCellHeightCacheableContainer: StatusProvider { +protocol TableViewCellHeightCacheableContainer { var cellFrameCache: NSCache { 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 } diff --git a/Mastodon/Protocol/TableViewControllerNavigateable.swift b/Mastodon/Protocol/TableViewControllerNavigateable.swift new file mode 100644 index 000000000..a70ab7014 --- /dev/null +++ b/Mastodon/Protocol/TableViewControllerNavigateable.swift @@ -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 + } +} + + diff --git a/Mastodon/Resources/ar.lproj/Localizable.strings b/Mastodon/Resources/ar.lproj/Localizable.strings index 55197f9bd..bd7c9f295 100644 --- a/Mastodon/Resources/ar.lproj/Localizable.strings +++ b/Mastodon/Resources/ar.lproj/Localizable.strings @@ -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 %@"; diff --git a/Mastodon/Resources/en.lproj/Localizable.strings b/Mastodon/Resources/en.lproj/Localizable.strings index 55197f9bd..bd7c9f295 100644 --- a/Mastodon/Resources/en.lproj/Localizable.strings +++ b/Mastodon/Resources/en.lproj/Localizable.strings @@ -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 %@"; diff --git a/Mastodon/Scene/HashtagTimeline/HashtagTimelineViewController.swift b/Mastodon/Scene/HashtagTimeline/HashtagTimelineViewController.swift index 5a30f6592..172842102 100644 --- a/Mastodon/Scene/HashtagTimeline/HashtagTimelineViewController.swift +++ b/Mastodon/Scene/HashtagTimeline/HashtagTimelineViewController.swift @@ -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) } } diff --git a/Mastodon/Scene/HomeTimeline/HomeTimelineViewController.swift b/Mastodon/Scene/HomeTimeline/HomeTimelineViewController.swift index 47f2ae049..bb6a651bc 100644 --- a/Mastodon/Scene/HomeTimeline/HomeTimelineViewController.swift +++ b/Mastodon/Scene/HomeTimeline/HomeTimelineViewController.swift @@ -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) } } diff --git a/Mastodon/Scene/Notification/NotificationViewController.swift b/Mastodon/Scene/Notification/NotificationViewController.swift index aea9d3318..6ef820a8d 100644 --- a/Mastodon/Scene/Notification/NotificationViewController.swift +++ b/Mastodon/Scene/Notification/NotificationViewController.swift @@ -141,7 +141,15 @@ extension NotificationViewController { } } -extension NotificationViewController { +// MARK: - StatusTableViewControllerAspect +extension NotificationViewController: StatusTableViewControllerAspect { } + +// MARK: - TableViewCellHeightCacheableContainer +extension NotificationViewController: TableViewCellHeightCacheableContainer { + var cellFrameCache: NSCache { + 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.. 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) + } + +} diff --git a/Mastodon/Scene/Profile/Favorite/FavoriteViewController.swift b/Mastodon/Scene/Profile/Favorite/FavoriteViewController.swift index 0a91c2afd..46b88796e 100644 --- a/Mastodon/Scene/Profile/Favorite/FavoriteViewController.swift +++ b/Mastodon/Scene/Profile/Favorite/FavoriteViewController.swift @@ -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) } } diff --git a/Mastodon/Scene/Profile/Segmented/Paging/ProfilePagingViewController.swift b/Mastodon/Scene/Profile/Segmented/Paging/ProfilePagingViewController.swift index f74198629..0ae2187c3 100644 --- a/Mastodon/Scene/Profile/Segmented/Paging/ProfilePagingViewController.swift +++ b/Mastodon/Scene/Profile/Segmented/Paging/ProfilePagingViewController.swift @@ -65,7 +65,7 @@ extension ProfilePagingViewController { } @objc func keyCommandHandlerRelay(_ sender: UIKeyCommand) { - (currentViewController as? StatusTableViewControllerNavigateable)?.keyCommandHandlerRelay(sender) + (currentViewController as? StatusTableViewControllerNavigateable)?.statusKeyCommandHandlerRelay(sender) } } diff --git a/Mastodon/Scene/Profile/Timeline/UserTimelineViewController.swift b/Mastodon/Scene/Profile/Timeline/UserTimelineViewController.swift index de3afabb5..10b82fd17 100644 --- a/Mastodon/Scene/Profile/Timeline/UserTimelineViewController.swift +++ b/Mastodon/Scene/Profile/Timeline/UserTimelineViewController.swift @@ -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) } } diff --git a/Mastodon/Scene/Thread/ThreadViewController.swift b/Mastodon/Scene/Thread/ThreadViewController.swift index 10dd291a6..2023d1b1d 100644 --- a/Mastodon/Scene/Thread/ThreadViewController.swift +++ b/Mastodon/Scene/Thread/ThreadViewController.swift @@ -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) } }