Merge branch 'feature/iPad-keyboard' into develop
This commit is contained in:
commit
26309572ec
|
@ -44,6 +44,9 @@
|
|||
"controls": {
|
||||
"actions": {
|
||||
"back": "Back",
|
||||
"next": "Next",
|
||||
"previous": "Previous",
|
||||
"open": "Open",
|
||||
"add": "Add",
|
||||
"remove": "Remove",
|
||||
"edit": "Edit",
|
||||
|
@ -68,6 +71,7 @@
|
|||
"find_people": "Find people to follow",
|
||||
"manually_search": "Manually search instead",
|
||||
"skip": "Skip",
|
||||
"reply": "Reply",
|
||||
"report_user": "Report %s",
|
||||
"block_domain": "Block %s",
|
||||
"unblock_domain": "Unblock %s",
|
||||
|
@ -80,6 +84,30 @@
|
|||
"notification": "Notification",
|
||||
"profile": "Profile"
|
||||
},
|
||||
"keyboard": {
|
||||
"common": {
|
||||
"switch_to_tab": "Switch to %s",
|
||||
"compose_new_post": "Compose New Post",
|
||||
"show_favorites": "Show Favorites",
|
||||
"open_settings": "Open Settings"
|
||||
},
|
||||
"timeline": {
|
||||
"previous_status": "Previous Status",
|
||||
"next_status": "Next Status",
|
||||
"open_status": "Open Status",
|
||||
"open_author_profile": "Open Author Profile",
|
||||
"open_reblogger_profile": "Open Reblogger Profile",
|
||||
"reply_status": "Reply Status",
|
||||
"toggle_reblog": "Toggle Status Reblog",
|
||||
"toggle_favorite": "Toggle Status Favorite",
|
||||
"toggle_content_warning": "Toggle Content Warning",
|
||||
"preview_image": "Preview Image"
|
||||
},
|
||||
"segmented_control": {
|
||||
"previous_section": "Previous Section",
|
||||
"next_section": "Next Section"
|
||||
}
|
||||
},
|
||||
"status": {
|
||||
"user_reblogged": "%s reblogged",
|
||||
"user_replied_to": "Replied to %s",
|
||||
|
@ -354,6 +382,14 @@
|
|||
"post_visibility_menu": "Post visibility menu",
|
||||
"input_limit_remains_count": "Input limit remains %ld",
|
||||
"input_limit_exceeds_count": "Input limit exceeds %ld"
|
||||
},
|
||||
"keyboard": {
|
||||
"discard_post": "Discard Post",
|
||||
"publish_post": "Publish Post",
|
||||
"toggle_poll": "Toggle Poll",
|
||||
"toggle_content_warning": "Toggle Content Warning",
|
||||
"append_attachment_entry": "Append Attachment - %s",
|
||||
"select_visibility_entry": "Select Visibility - %s"
|
||||
}
|
||||
},
|
||||
"profile": {
|
||||
|
@ -430,6 +466,10 @@
|
|||
"poll": "Your poll has ended",
|
||||
"mention": "mentioned you",
|
||||
"follow_request": "request to follow you"
|
||||
},
|
||||
"keyobard": {
|
||||
"show_everything": "Show Everything",
|
||||
"show_mentions": "Show Mentions"
|
||||
}
|
||||
},
|
||||
"thread": {
|
||||
|
@ -477,6 +517,9 @@
|
|||
"clear": "Clear Media Cache",
|
||||
"signout": "Sign Out"
|
||||
}
|
||||
},
|
||||
"keyboard": {
|
||||
"close_settings_window": "Close Settings Window"
|
||||
}
|
||||
},
|
||||
"report": {
|
||||
|
@ -488,6 +531,13 @@
|
|||
"send": "Send Report",
|
||||
"skip_to_send": "Send without comment",
|
||||
"text_placeholder": "Type or paste additional comments"
|
||||
},
|
||||
"preview": {
|
||||
"keyboard": {
|
||||
"close_preview": "Close Preview",
|
||||
"show_next": "Show Next",
|
||||
"show_previous": "Show Previous"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -197,6 +197,12 @@
|
|||
DB118A8225E4B6E600FAB162 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = DB118A8125E4B6E600FAB162 /* Preview Assets.xcassets */; };
|
||||
DB118A8C25E4BFB500FAB162 /* HighlightDimmableButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB118A8B25E4BFB500FAB162 /* HighlightDimmableButton.swift */; };
|
||||
DB1D186C25EF5BA7003F1F23 /* PollTableView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB1D186B25EF5BA7003F1F23 /* PollTableView.swift */; };
|
||||
DB1D842C26551A1C000346B3 /* StatusProvider+StatusTableViewKeyCommandNavigateable.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB1D842B26551A1C000346B3 /* StatusProvider+StatusTableViewKeyCommandNavigateable.swift */; };
|
||||
DB1D842E26552C4D000346B3 /* StatusTableViewControllerNavigateable.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB1D842D26552C4D000346B3 /* StatusTableViewControllerNavigateable.swift */; };
|
||||
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 */; };
|
||||
DB1D84382657B275000346B3 /* SegmentedControlNavigateable.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB1D84372657B275000346B3 /* SegmentedControlNavigateable.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 */; };
|
||||
|
@ -752,6 +758,12 @@
|
|||
DB118A8125E4B6E600FAB162 /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = "<group>"; };
|
||||
DB118A8B25E4BFB500FAB162 /* HighlightDimmableButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HighlightDimmableButton.swift; sourceTree = "<group>"; };
|
||||
DB1D186B25EF5BA7003F1F23 /* PollTableView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PollTableView.swift; sourceTree = "<group>"; };
|
||||
DB1D842B26551A1C000346B3 /* StatusProvider+StatusTableViewKeyCommandNavigateable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "StatusProvider+StatusTableViewKeyCommandNavigateable.swift"; sourceTree = "<group>"; };
|
||||
DB1D842D26552C4D000346B3 /* StatusTableViewControllerNavigateable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusTableViewControllerNavigateable.swift; sourceTree = "<group>"; };
|
||||
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>"; };
|
||||
DB1D84372657B275000346B3 /* SegmentedControlNavigateable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SegmentedControlNavigateable.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>"; };
|
||||
|
@ -1241,6 +1253,8 @@
|
|||
2DF75BA025D0E29D00694EC8 /* StatusProvider+StatusTableViewCellDelegate.swift */,
|
||||
DB59F0FD25EF5D96001F1DAB /* StatusProvider+UITableViewDelegate.swift */,
|
||||
DB71FD4525F8C6D200512AE1 /* StatusProvider+UITableViewDataSourcePrefetching.swift */,
|
||||
DB1D843526579DB5000346B3 /* StatusProvider+TableViewControllerNavigateable.swift */,
|
||||
DB1D842B26551A1C000346B3 /* StatusProvider+StatusTableViewKeyCommandNavigateable.swift */,
|
||||
);
|
||||
path = StatusProvider;
|
||||
sourceTree = "<group>";
|
||||
|
@ -1341,6 +1355,9 @@
|
|||
2D38F20725CD491300561493 /* DisposeBagCollectable.swift */,
|
||||
5DF1058425F88AE500D6C0D4 /* NeedsDependency+AVPlayerViewControllerDelegate.swift */,
|
||||
DBE3CE12261D7D4200430CC6 /* StatusTableViewControllerAspect.swift */,
|
||||
DB1D84372657B275000346B3 /* SegmentedControlNavigateable.swift */,
|
||||
DB1D843326579931000346B3 /* TableViewControllerNavigateable.swift */,
|
||||
DB1D842D26552C4D000346B3 /* StatusTableViewControllerNavigateable.swift */,
|
||||
);
|
||||
path = Protocol;
|
||||
sourceTree = "<group>";
|
||||
|
@ -1789,6 +1806,7 @@
|
|||
DB5086BD25CC0D9900C2C187 /* SplashPreference.swift */,
|
||||
DB6D1B3C2636857500ACB481 /* AppearancePreference.swift */,
|
||||
DBE54AC52636C89F004E7C0B /* NotificationPreference.swift */,
|
||||
DB1D842F26566512000346B3 /* KeyboardPreference.swift */,
|
||||
);
|
||||
path = Preference;
|
||||
sourceTree = "<group>";
|
||||
|
@ -2898,6 +2916,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 */,
|
||||
|
@ -3054,6 +3073,7 @@
|
|||
0FB3D2FE25E4CB6400AAD544 /* PickServerTitleCell.swift in Sources */,
|
||||
5DA732CC2629CEF500A92342 /* UIView+Remove.swift in Sources */,
|
||||
2D38F1DF25CD46A400561493 /* HomeTimelineViewController+Provider.swift in Sources */,
|
||||
DB1D843026566512000346B3 /* KeyboardPreference.swift in Sources */,
|
||||
2D206B9225F60EA700143C56 /* UIControl.swift in Sources */,
|
||||
2D9DB96B263A91D1007C1D71 /* APIService+DomainBlock.swift in Sources */,
|
||||
DBBF1DC92652538500E5B703 /* AutoCompleteSection.swift in Sources */,
|
||||
|
@ -3180,6 +3200,7 @@
|
|||
5B90C460262599800002E742 /* SettingsAppearanceTableViewCell.swift in Sources */,
|
||||
DB8AF54425C13647002E6C99 /* SceneCoordinator.swift in Sources */,
|
||||
5DF1058525F88AE500D6C0D4 /* NeedsDependency+AVPlayerViewControllerDelegate.swift in Sources */,
|
||||
DB1D84382657B275000346B3 /* SegmentedControlNavigateable.swift in Sources */,
|
||||
DB447697260B439000B66B82 /* CustomEmojiPickerHeaderCollectionReusableView.swift in Sources */,
|
||||
DB45FAF925CA80A2005A8AC7 /* APIService+CoreData+MastodonAuthentication.swift in Sources */,
|
||||
2D04F42525C255B9003F936F /* APIService+PublicTimeline.swift in Sources */,
|
||||
|
@ -3205,6 +3226,7 @@
|
|||
0F202227261411BB000C64BF /* HashtagTimelineViewController+Provider.swift in Sources */,
|
||||
2D7631A825C1535600929FB9 /* StatusTableViewCell.swift in Sources */,
|
||||
2D76316525C14BD100929FB9 /* PublicTimelineViewController.swift in Sources */,
|
||||
DB1D842E26552C4D000346B3 /* StatusTableViewControllerNavigateable.swift in Sources */,
|
||||
DB938F1F2624382F00E5B6C1 /* ThreadViewModel+Diffable.swift in Sources */,
|
||||
2D69CFF425CA9E2200C3A1B2 /* LoadMoreConfigurableTableViewContainer.swift in Sources */,
|
||||
DB040ECD26526EA600BEE9D8 /* ComposeCollectionView.swift in Sources */,
|
||||
|
@ -3243,7 +3265,9 @@
|
|||
DB66729C25F9F91F00D60309 /* ComposeStatusItem.swift in Sources */,
|
||||
DB6180E326391A4C0018D199 /* ViewControllerAnimatedTransitioning.swift in Sources */,
|
||||
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 */,
|
||||
|
|
|
@ -12,7 +12,7 @@
|
|||
<key>CoreDataStack.xcscheme_^#shared#^_</key>
|
||||
<dict>
|
||||
<key>orderHint</key>
|
||||
<integer>14</integer>
|
||||
<integer>15</integer>
|
||||
</dict>
|
||||
<key>Mastodon - RTL.xcscheme_^#shared#^_</key>
|
||||
<dict>
|
||||
|
@ -32,7 +32,7 @@
|
|||
<key>NotificationService.xcscheme_^#shared#^_</key>
|
||||
<dict>
|
||||
<key>orderHint</key>
|
||||
<integer>15</integer>
|
||||
<integer>14</integer>
|
||||
</dict>
|
||||
</dict>
|
||||
<key>SuppressBuildableAutocreation</key>
|
||||
|
|
|
@ -6,6 +6,7 @@
|
|||
//
|
||||
|
||||
import Foundation
|
||||
import Combine
|
||||
import MastodonSDK
|
||||
|
||||
/// Note: update Equatable when change case
|
||||
|
@ -19,11 +20,11 @@ enum PickServerItem {
|
|||
|
||||
extension PickServerItem {
|
||||
final class ServerItemAttribute: Equatable, Hashable {
|
||||
var isLast: Bool
|
||||
var isLast: CurrentValueSubject<Bool, Never>
|
||||
var isExpand: Bool
|
||||
|
||||
init(isLast: Bool, isExpand: Bool) {
|
||||
self.isLast = isLast
|
||||
self.isLast = CurrentValueSubject(isLast)
|
||||
self.isExpand = isExpand
|
||||
}
|
||||
|
||||
|
|
|
@ -83,16 +83,24 @@ extension PickServerSection {
|
|||
|
||||
cell.updateExpandMode(mode: attribute.isExpand ? .expand : .collapse)
|
||||
|
||||
if attribute.isLast {
|
||||
cell.containerView.layer.maskedCorners = [
|
||||
.layerMinXMaxYCorner,
|
||||
.layerMaxXMaxYCorner
|
||||
]
|
||||
cell.containerView.layer.cornerCurve = .continuous
|
||||
cell.containerView.layer.cornerRadius = MastodonPickServerAppearance.tableViewCornerRadius
|
||||
} else {
|
||||
cell.containerView.layer.cornerRadius = 0
|
||||
}
|
||||
attribute.isLast
|
||||
.receive(on: DispatchQueue.main)
|
||||
.sink { [weak cell] isLast in
|
||||
guard let cell = cell else { return }
|
||||
if isLast {
|
||||
cell.containerView.layer.maskedCorners = [
|
||||
.layerMinXMaxYCorner,
|
||||
.layerMaxXMaxYCorner
|
||||
]
|
||||
cell.containerView.layer.cornerCurve = .continuous
|
||||
cell.containerView.layer.cornerRadius = MastodonPickServerAppearance.tableViewCornerRadius
|
||||
cell.containerView.layer.masksToBounds = true
|
||||
} else {
|
||||
cell.containerView.layer.cornerRadius = 0
|
||||
cell.containerView.layer.masksToBounds = false
|
||||
}
|
||||
}
|
||||
.store(in: &cell.disposeBag)
|
||||
|
||||
cell.expandMode
|
||||
.receive(on: DispatchQueue.main)
|
||||
|
@ -152,8 +160,10 @@ extension PickServerSection {
|
|||
]
|
||||
cell.containerView.layer.cornerCurve = .continuous
|
||||
cell.containerView.layer.cornerRadius = MastodonPickServerAppearance.tableViewCornerRadius
|
||||
cell.containerView.layer.masksToBounds = true
|
||||
} else {
|
||||
cell.containerView.layer.cornerRadius = 0
|
||||
cell.containerView.layer.masksToBounds = false
|
||||
}
|
||||
|
||||
attribute.isNoResult ? cell.stopAnimating() : cell.startAnimating()
|
||||
|
|
|
@ -7,15 +7,6 @@
|
|||
|
||||
import UIKit
|
||||
|
||||
extension UITableView {
|
||||
|
||||
// static let groupedTableViewPaddingHeaderViewHeight: CGFloat = 16
|
||||
// static var groupedTableViewPaddingHeaderView: UIView {
|
||||
// return UIView(frame: CGRect(x: 0, y: 0, width: 100, height: groupedTableViewPaddingHeaderViewHeight))
|
||||
// }
|
||||
|
||||
}
|
||||
|
||||
extension UITableView {
|
||||
|
||||
func deselectRow(with transitionCoordinator: UIViewControllerTransitionCoordinator?, animated: Bool) {
|
||||
|
|
|
@ -112,3 +112,16 @@ extension UIViewController {
|
|||
}
|
||||
|
||||
}
|
||||
|
||||
extension UIViewController {
|
||||
|
||||
/// https://stackoverflow.com/a/27301207/3797903
|
||||
var isModal: Bool {
|
||||
let presentingIsModal = presentingViewController != nil
|
||||
let presentingIsNavigation = navigationController != nil && navigationController?.presentingViewController?.presentedViewController == navigationController
|
||||
let presentingIsTabBar = tabBarController?.presentingViewController is UITabBarController
|
||||
|
||||
return presentingIsModal || presentingIsNavigation || presentingIsTabBar
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -102,14 +102,22 @@ 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
|
||||
internal static let reply = L10n.tr("Localizable", "Common.Controls.Actions.Reply")
|
||||
/// Report %@
|
||||
internal static func reportUser(_ p1: Any) -> String {
|
||||
return L10n.tr("Localizable", "Common.Controls.Actions.ReportUser", String(describing: p1))
|
||||
|
@ -189,6 +197,48 @@ internal enum L10n {
|
|||
return L10n.tr("Localizable", "Common.Controls.Firendship.UnmuteUser", String(describing: p1))
|
||||
}
|
||||
}
|
||||
internal enum Keyboard {
|
||||
internal enum Common {
|
||||
/// Compose New Post
|
||||
internal static let composeNewPost = L10n.tr("Localizable", "Common.Controls.Keyboard.Common.ComposeNewPost")
|
||||
/// Open Settings
|
||||
internal static let openSettings = L10n.tr("Localizable", "Common.Controls.Keyboard.Common.OpenSettings")
|
||||
/// Show Favorites
|
||||
internal static let showFavorites = L10n.tr("Localizable", "Common.Controls.Keyboard.Common.ShowFavorites")
|
||||
/// Switch to %@
|
||||
internal static func switchToTab(_ p1: Any) -> String {
|
||||
return L10n.tr("Localizable", "Common.Controls.Keyboard.Common.SwitchToTab", String(describing: p1))
|
||||
}
|
||||
}
|
||||
internal enum SegmentedControl {
|
||||
/// Next Section
|
||||
internal static let nextSection = L10n.tr("Localizable", "Common.Controls.Keyboard.SegmentedControl.NextSection")
|
||||
/// Previous Section
|
||||
internal static let previousSection = L10n.tr("Localizable", "Common.Controls.Keyboard.SegmentedControl.PreviousSection")
|
||||
}
|
||||
internal enum Timeline {
|
||||
/// Next Status
|
||||
internal static let nextStatus = L10n.tr("Localizable", "Common.Controls.Keyboard.Timeline.NextStatus")
|
||||
/// Open Author Profile
|
||||
internal static let openAuthorProfile = L10n.tr("Localizable", "Common.Controls.Keyboard.Timeline.OpenAuthorProfile")
|
||||
/// Open Reblogger Profile
|
||||
internal static let openRebloggerProfile = L10n.tr("Localizable", "Common.Controls.Keyboard.Timeline.OpenRebloggerProfile")
|
||||
/// Open Status
|
||||
internal static let openStatus = L10n.tr("Localizable", "Common.Controls.Keyboard.Timeline.OpenStatus")
|
||||
/// Preview Image
|
||||
internal static let previewImage = L10n.tr("Localizable", "Common.Controls.Keyboard.Timeline.PreviewImage")
|
||||
/// Previous Status
|
||||
internal static let previousStatus = L10n.tr("Localizable", "Common.Controls.Keyboard.Timeline.PreviousStatus")
|
||||
/// Reply Status
|
||||
internal static let replyStatus = L10n.tr("Localizable", "Common.Controls.Keyboard.Timeline.ReplyStatus")
|
||||
/// Toggle Content Warning
|
||||
internal static let toggleContentWarning = L10n.tr("Localizable", "Common.Controls.Keyboard.Timeline.ToggleContentWarning")
|
||||
/// Toggle Status Favorite
|
||||
internal static let toggleFavorite = L10n.tr("Localizable", "Common.Controls.Keyboard.Timeline.ToggleFavorite")
|
||||
/// Toggle Status Reblog
|
||||
internal static let toggleReblog = L10n.tr("Localizable", "Common.Controls.Keyboard.Timeline.ToggleReblog")
|
||||
}
|
||||
}
|
||||
internal enum Status {
|
||||
/// content warning
|
||||
internal static let contentWarning = L10n.tr("Localizable", "Common.Controls.Status.ContentWarning")
|
||||
|
@ -390,6 +440,24 @@ internal enum L10n {
|
|||
/// Write an accurate warning here...
|
||||
internal static let placeholder = L10n.tr("Localizable", "Scene.Compose.ContentWarning.Placeholder")
|
||||
}
|
||||
internal enum Keyboard {
|
||||
/// Append Attachment - %@
|
||||
internal static func appendAttachmentEntry(_ p1: Any) -> String {
|
||||
return L10n.tr("Localizable", "Scene.Compose.Keyboard.AppendAttachmentEntry", String(describing: p1))
|
||||
}
|
||||
/// Discard Post
|
||||
internal static let discardPost = L10n.tr("Localizable", "Scene.Compose.Keyboard.DiscardPost")
|
||||
/// Publish Post
|
||||
internal static let publishPost = L10n.tr("Localizable", "Scene.Compose.Keyboard.PublishPost")
|
||||
/// Select Visibility - %@
|
||||
internal static func selectVisibilityEntry(_ p1: Any) -> String {
|
||||
return L10n.tr("Localizable", "Scene.Compose.Keyboard.SelectVisibilityEntry", String(describing: p1))
|
||||
}
|
||||
/// Toggle Content Warning
|
||||
internal static let toggleContentWarning = L10n.tr("Localizable", "Scene.Compose.Keyboard.ToggleContentWarning")
|
||||
/// Toggle Poll
|
||||
internal static let togglePoll = L10n.tr("Localizable", "Scene.Compose.Keyboard.TogglePoll")
|
||||
}
|
||||
internal enum MediaSelection {
|
||||
/// Browse
|
||||
internal static let browse = L10n.tr("Localizable", "Scene.Compose.MediaSelection.Browse")
|
||||
|
@ -508,6 +576,12 @@ internal enum L10n {
|
|||
/// rebloged your post
|
||||
internal static let reblog = L10n.tr("Localizable", "Scene.Notification.Action.Reblog")
|
||||
}
|
||||
internal enum Keyobard {
|
||||
/// Show Everything
|
||||
internal static let showEverything = L10n.tr("Localizable", "Scene.Notification.Keyobard.ShowEverything")
|
||||
/// Show Mentions
|
||||
internal static let showMentions = L10n.tr("Localizable", "Scene.Notification.Keyobard.ShowMentions")
|
||||
}
|
||||
internal enum Title {
|
||||
/// Everything
|
||||
internal static let everything = L10n.tr("Localizable", "Scene.Notification.Title.Everything")
|
||||
|
@ -515,6 +589,16 @@ internal enum L10n {
|
|||
internal static let mentions = L10n.tr("Localizable", "Scene.Notification.Title.Mentions")
|
||||
}
|
||||
}
|
||||
internal enum Preview {
|
||||
internal enum Keyboard {
|
||||
/// Close Preview
|
||||
internal static let closePreview = L10n.tr("Localizable", "Scene.Preview.Keyboard.ClosePreview")
|
||||
/// Show Next
|
||||
internal static let showNext = L10n.tr("Localizable", "Scene.Preview.Keyboard.ShowNext")
|
||||
/// Show Previous
|
||||
internal static let showPrevious = L10n.tr("Localizable", "Scene.Preview.Keyboard.ShowPrevious")
|
||||
}
|
||||
}
|
||||
internal enum Profile {
|
||||
/// %@ posts
|
||||
internal static func subtitle(_ p1: Any) -> String {
|
||||
|
@ -822,6 +906,10 @@ internal enum L10n {
|
|||
internal enum Settings {
|
||||
/// Settings
|
||||
internal static let title = L10n.tr("Localizable", "Scene.Settings.Title")
|
||||
internal enum Keyboard {
|
||||
/// Close Settings Window
|
||||
internal static let closeSettingsWindow = L10n.tr("Localizable", "Scene.Settings.Keyboard.CloseSettingsWindow")
|
||||
}
|
||||
internal enum Section {
|
||||
internal enum Appearance {
|
||||
/// Automatic
|
||||
|
|
|
@ -0,0 +1,20 @@
|
|||
//
|
||||
// KeyboardPreference.swift
|
||||
// Mastodon
|
||||
//
|
||||
// Created by MainasuK Cirno on 2021-5-20.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
|
||||
extension UserDefaults {
|
||||
|
||||
@objc dynamic var backKeyCommandPressDate: Date? {
|
||||
get {
|
||||
register(defaults: [#function: Date().timeIntervalSinceReferenceDate])
|
||||
return Date(timeIntervalSinceReferenceDate: double(forKey: #function))
|
||||
}
|
||||
set { self[#function] = newValue?.timeIntervalSinceReferenceDate }
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,95 @@
|
|||
//
|
||||
// SegmentedControlNavigateable.swift
|
||||
// Mastodon
|
||||
//
|
||||
// Created by MainasuK Cirno on 2021-5-21.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
|
||||
typealias SegmentedControlNavigateable = SegmentedControlNavigateableCore & SegmentedControlNavigateableRelay
|
||||
|
||||
protocol SegmentedControlNavigateableCore: AnyObject {
|
||||
var navigateableSegmentedControl: UISegmentedControl { get }
|
||||
var segmentedControlNavigateKeyCommands: [UIKeyCommand] { get }
|
||||
|
||||
func segmentedControlNavigateKeyCommandHandler(_ sender: UIKeyCommand)
|
||||
func navigate(direction: SegmentedControlNavigationDirection)
|
||||
}
|
||||
|
||||
@objc protocol SegmentedControlNavigateableRelay: AnyObject {
|
||||
func segmentedControlNavigateKeyCommandHandlerRelay(_ sender: UIKeyCommand)
|
||||
}
|
||||
|
||||
enum SegmentedControlNavigationDirection: String, CaseIterable {
|
||||
case previous
|
||||
case next
|
||||
|
||||
var title: String {
|
||||
switch self {
|
||||
case .previous: return L10n.Common.Controls.Keyboard.SegmentedControl.previousSection
|
||||
case .next: return L10n.Common.Controls.Keyboard.SegmentedControl.nextSection
|
||||
}
|
||||
}
|
||||
|
||||
// UIKeyCommand input
|
||||
var input: String {
|
||||
switch self {
|
||||
case .previous: return "["
|
||||
case .next: return "]"
|
||||
}
|
||||
}
|
||||
|
||||
var modifierFlags: UIKeyModifierFlags {
|
||||
switch self {
|
||||
case .previous: return [.shift, .command]
|
||||
case .next: return [.shift, .command]
|
||||
}
|
||||
}
|
||||
|
||||
var propertyList: Any {
|
||||
return rawValue
|
||||
}
|
||||
}
|
||||
|
||||
extension SegmentedControlNavigateableCore where Self: SegmentedControlNavigateableRelay {
|
||||
var segmentedControlNavigateKeyCommands: [UIKeyCommand] {
|
||||
SegmentedControlNavigationDirection.allCases.map { direction in
|
||||
UIKeyCommand(
|
||||
title: direction.title,
|
||||
image: nil,
|
||||
action: #selector(Self.segmentedControlNavigateKeyCommandHandlerRelay(_:)),
|
||||
input: direction.input,
|
||||
modifierFlags: direction.modifierFlags,
|
||||
propertyList: direction.propertyList,
|
||||
alternates: [],
|
||||
discoverabilityTitle: nil,
|
||||
attributes: [],
|
||||
state: .off
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
func segmentedControlNavigateKeyCommandHandler(_ sender: UIKeyCommand) {
|
||||
guard let rawValue = sender.propertyList as? String,
|
||||
let direction = SegmentedControlNavigationDirection(rawValue: rawValue) else { return }
|
||||
navigate(direction: direction)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension SegmentedControlNavigateableCore {
|
||||
func navigate(direction: SegmentedControlNavigationDirection) {
|
||||
let index: Int = {
|
||||
let selectedIndex = navigateableSegmentedControl.selectedSegmentIndex
|
||||
switch direction {
|
||||
case .previous: return selectedIndex - 1
|
||||
case .next: return selectedIndex + 1
|
||||
}
|
||||
}()
|
||||
|
||||
guard 0..<navigateableSegmentedControl.numberOfSegments ~= index else { return }
|
||||
navigateableSegmentedControl.selectedSegmentIndex = index
|
||||
navigateableSegmentedControl.sendActions(for: .valueChanged)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,100 @@
|
|||
//
|
||||
// StatusProvider+KeyCommands.swift
|
||||
// Mastodon
|
||||
//
|
||||
// Created by MainasuK Cirno on 2021-5-19.
|
||||
//
|
||||
|
||||
import os.log
|
||||
import UIKit
|
||||
|
||||
extension StatusTableViewControllerNavigateableCore where Self: StatusProvider & StatusTableViewControllerNavigateableRelay {
|
||||
|
||||
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 .openAuthorProfile: openAuthorProfile()
|
||||
case .openRebloggerProfile: openRebloggerProfile()
|
||||
case .replyStatus: replyStatus()
|
||||
case .toggleReblog: toggleReblog()
|
||||
case .toggleFavorite: toggleFavorite()
|
||||
case .toggleContentWarning: toggleContentWarning()
|
||||
case .previewImage: previewImage()
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// status coordinate
|
||||
extension StatusTableViewControllerNavigateableCore where Self: StatusProvider {
|
||||
|
||||
private func openAuthorProfile() {
|
||||
guard let indexPathForSelectedRow = tableView.indexPathForSelectedRow else { return }
|
||||
StatusProviderFacade.coordinateToStatusAuthorProfileScene(for: .primary, provider: self, indexPath: indexPathForSelectedRow)
|
||||
}
|
||||
|
||||
private func openRebloggerProfile() {
|
||||
guard let indexPathForSelectedRow = tableView.indexPathForSelectedRow else { return }
|
||||
StatusProviderFacade.coordinateToStatusAuthorProfileScene(for: .secondary, provider: self, indexPath: indexPathForSelectedRow)
|
||||
}
|
||||
|
||||
private func replyStatus() {
|
||||
guard let indexPathForSelectedRow = tableView.indexPathForSelectedRow else { return }
|
||||
StatusProviderFacade.responseToStatusReplyAction(provider: self, indexPath: indexPathForSelectedRow)
|
||||
}
|
||||
|
||||
private func previewImage() {
|
||||
guard let indexPathForSelectedRow = tableView.indexPathForSelectedRow else { return }
|
||||
guard let provider = self as? (StatusProvider & MediaPreviewableViewController) else { return }
|
||||
guard let cell = tableView.cellForRow(at: indexPathForSelectedRow),
|
||||
let presentable = cell as? MosaicImageViewContainerPresentable else { return }
|
||||
let mosaicImageView = presentable.mosaicImageViewContainer
|
||||
guard let imageView = mosaicImageView.imageViews.first else { return }
|
||||
StatusProviderFacade.coordinateToStatusMediaPreviewScene(provider: provider, cell: cell, mosaicImageView: mosaicImageView, didTapImageView: imageView, atIndex: 0)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// toggle
|
||||
extension StatusTableViewControllerNavigateableCore where Self: StatusProvider {
|
||||
|
||||
private func toggleReblog() {
|
||||
guard let indexPathForSelectedRow = tableView.indexPathForSelectedRow else { return }
|
||||
StatusProviderFacade.responseToStatusReblogAction(provider: self, indexPath: indexPathForSelectedRow)
|
||||
}
|
||||
|
||||
private func toggleFavorite() {
|
||||
guard let indexPathForSelectedRow = tableView.indexPathForSelectedRow else { return }
|
||||
StatusProviderFacade.responseToStatusLikeAction(provider: self, indexPath: indexPathForSelectedRow)
|
||||
}
|
||||
|
||||
private func toggleContentWarning() {
|
||||
guard let indexPathForSelectedRow = tableView.indexPathForSelectedRow else { return }
|
||||
StatusProviderFacade.responseToStatusContentWarningRevealAction(provider: self, indexPath: indexPathForSelectedRow)
|
||||
}
|
||||
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -25,6 +25,14 @@ extension StatusProviderFacade {
|
|||
)
|
||||
}
|
||||
|
||||
static func coordinateToStatusAuthorProfileScene(for target: Target, provider: StatusProvider, indexPath: IndexPath) {
|
||||
_coordinateToStatusAuthorProfileScene(
|
||||
for: target,
|
||||
provider: provider,
|
||||
status: provider.status(for: nil, indexPath: indexPath)
|
||||
)
|
||||
}
|
||||
|
||||
static func coordinateToStatusAuthorProfileScene(for target: Target, provider: StatusProvider, cell: UITableViewCell) {
|
||||
_coordinateToStatusAuthorProfileScene(
|
||||
for: target,
|
||||
|
@ -189,6 +197,13 @@ extension StatusProviderFacade {
|
|||
)
|
||||
}
|
||||
|
||||
static func responseToStatusLikeAction(provider: StatusProvider, indexPath: IndexPath) {
|
||||
_responseToStatusLikeAction(
|
||||
provider: provider,
|
||||
status: provider.status(for: nil, indexPath: indexPath)
|
||||
)
|
||||
}
|
||||
|
||||
private static func _responseToStatusLikeAction(provider: StatusProvider, status: Future<Status?, Never>) {
|
||||
// prepare authentication
|
||||
guard let activeMastodonAuthenticationBox = provider.context.authenticationService.activeMastodonAuthenticationBox.value else {
|
||||
|
@ -292,6 +307,13 @@ extension StatusProviderFacade {
|
|||
)
|
||||
}
|
||||
|
||||
static func responseToStatusReblogAction(provider: StatusProvider, indexPath: IndexPath) {
|
||||
_responseToStatusReblogAction(
|
||||
provider: provider,
|
||||
status: provider.status(for: nil, indexPath: indexPath)
|
||||
)
|
||||
}
|
||||
|
||||
private static func _responseToStatusReblogAction(provider: StatusProvider, status: Future<Status?, Never>) {
|
||||
// prepare authentication
|
||||
guard let activeMastodonAuthenticationBox = provider.context.authenticationService.activeMastodonAuthenticationBox.value else {
|
||||
|
@ -400,6 +422,13 @@ extension StatusProviderFacade {
|
|||
)
|
||||
}
|
||||
|
||||
static func responseToStatusReplyAction(provider: StatusProvider, indexPath: IndexPath) {
|
||||
_responseToStatusReplyAction(
|
||||
provider: provider,
|
||||
status: provider.status(for: nil, indexPath: indexPath)
|
||||
)
|
||||
}
|
||||
|
||||
private static func _responseToStatusReplyAction(provider: StatusProvider, status: Future<Status?, Never>) {
|
||||
status
|
||||
.sink { [weak provider] status in
|
||||
|
@ -450,6 +479,13 @@ extension StatusProviderFacade {
|
|||
)
|
||||
}
|
||||
|
||||
static func responseToStatusContentWarningRevealAction(provider: StatusProvider, indexPath: IndexPath) {
|
||||
_responseToStatusContentWarningRevealAction(
|
||||
dependency: provider,
|
||||
status: provider.status(for: nil, indexPath: indexPath)
|
||||
)
|
||||
}
|
||||
|
||||
private static func _responseToStatusContentWarningRevealAction(dependency: NeedsDependency, status: Future<Status?, Never>) {
|
||||
status
|
||||
.compactMap { [weak dependency] status -> AnyPublisher<Status?, Never>? in
|
||||
|
|
|
@ -7,8 +7,9 @@
|
|||
|
||||
import UIKit
|
||||
import AVKit
|
||||
import GameController
|
||||
|
||||
// Check List Last Updated
|
||||
// Check List Last Updated
|
||||
// - HomeViewController: 2021/4/30
|
||||
// - FavoriteViewController: 2021/4/30
|
||||
// - HashtagTimelineViewController: 2021/4/30
|
||||
|
@ -34,6 +35,12 @@ protocol StatusTableViewControllerAspect: UIViewController {
|
|||
extension StatusTableViewControllerAspect {
|
||||
/// [UI] hook to deselect row in the transitioning for the table view
|
||||
func aspectViewWillAppear(_ animated: Bool) {
|
||||
if GCKeyboard.coalesced != nil, let backKeyCommandPressDate = UserDefaults.shared.backKeyCommandPressDate {
|
||||
guard backKeyCommandPressDate.timeIntervalSinceNow <= -0.5 else {
|
||||
// break if interval greater than 0.5s
|
||||
return
|
||||
}
|
||||
}
|
||||
tableView.deselectRow(with: transitionCoordinator, animated: animated)
|
||||
}
|
||||
}
|
||||
|
@ -79,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) {
|
||||
|
|
|
@ -0,0 +1,78 @@
|
|||
//
|
||||
// StatusTableViewControllerNavigateable.swift
|
||||
// Mastodon
|
||||
//
|
||||
// Created by MainasuK Cirno on 2021-5-19.
|
||||
//
|
||||
|
||||
import os.log
|
||||
import UIKit
|
||||
|
||||
typealias StatusTableViewControllerNavigateable = StatusTableViewControllerNavigateableCore & StatusTableViewControllerNavigateableRelay
|
||||
|
||||
protocol StatusTableViewControllerNavigateableCore: TableViewControllerNavigateableCore {
|
||||
var statusNavigationKeyCommands: [UIKeyCommand] { get }
|
||||
func statusKeyCommandHandler(_ sender: UIKeyCommand)
|
||||
}
|
||||
|
||||
extension StatusTableViewControllerNavigateableCore {
|
||||
var overrideNavigationScrollPosition: UITableView.ScrollPosition? {
|
||||
get { return nil }
|
||||
set { }
|
||||
}
|
||||
}
|
||||
|
||||
@objc protocol StatusTableViewControllerNavigateableRelay: TableViewControllerNavigateableRelay {
|
||||
func statusKeyCommandHandlerRelay(_ sender: UIKeyCommand)
|
||||
}
|
||||
|
||||
enum StatusTableViewNavigation: String, CaseIterable {
|
||||
case openAuthorProfile
|
||||
case openRebloggerProfile
|
||||
case replyStatus
|
||||
case toggleReblog
|
||||
case toggleFavorite
|
||||
case toggleContentWarning
|
||||
case previewImage
|
||||
|
||||
var title: String {
|
||||
switch self {
|
||||
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
|
||||
case .toggleReblog: return L10n.Common.Controls.Keyboard.Timeline.toggleReblog
|
||||
case .toggleFavorite: return L10n.Common.Controls.Keyboard.Timeline.toggleFavorite
|
||||
case .toggleContentWarning: return L10n.Common.Controls.Keyboard.Timeline.toggleContentWarning
|
||||
case .previewImage: return L10n.Common.Controls.Keyboard.Timeline.previewImage
|
||||
}
|
||||
}
|
||||
|
||||
// UIKeyCommand input
|
||||
var input: String {
|
||||
switch self {
|
||||
case .openAuthorProfile: return "p"
|
||||
case .openRebloggerProfile: return "p" // + option
|
||||
case .replyStatus: return "n" // + shift + command
|
||||
case .toggleReblog: return "r"
|
||||
case .toggleFavorite: return "f"
|
||||
case .toggleContentWarning: return "o"
|
||||
case .previewImage: return "i"
|
||||
}
|
||||
}
|
||||
|
||||
var modifierFlags: UIKeyModifierFlags {
|
||||
switch self {
|
||||
case .openAuthorProfile: return []
|
||||
case .openRebloggerProfile: return [.alternate]
|
||||
case .replyStatus: return [.shift, .alternate]
|
||||
case .toggleReblog: return []
|
||||
case .toggleFavorite: return []
|
||||
case .toggleContentWarning: return []
|
||||
case .previewImage: return []
|
||||
}
|
||||
}
|
||||
|
||||
var propertyList: Any {
|
||||
return rawValue
|
||||
}
|
||||
}
|
|
@ -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,10 +30,14 @@ 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 %@";
|
||||
"Common.Controls.Actions.Save" = "Save";
|
||||
"Common.Controls.Actions.SavePhoto" = "Save photo";
|
||||
|
@ -64,6 +68,22 @@ Please check your internet connection.";
|
|||
"Common.Controls.Firendship.UnblockUser" = "Unblock %@";
|
||||
"Common.Controls.Firendship.Unmute" = "Unmute";
|
||||
"Common.Controls.Firendship.UnmuteUser" = "Unmute %@";
|
||||
"Common.Controls.Keyboard.Common.ComposeNewPost" = "Compose New Post";
|
||||
"Common.Controls.Keyboard.Common.OpenSettings" = "Open Settings";
|
||||
"Common.Controls.Keyboard.Common.ShowFavorites" = "Show Favorites";
|
||||
"Common.Controls.Keyboard.Common.SwitchToTab" = "Switch to %@";
|
||||
"Common.Controls.Keyboard.SegmentedControl.NextSection" = "Next Section";
|
||||
"Common.Controls.Keyboard.SegmentedControl.PreviousSection" = "Previous Section";
|
||||
"Common.Controls.Keyboard.Timeline.NextStatus" = "Next Status";
|
||||
"Common.Controls.Keyboard.Timeline.OpenAuthorProfile" = "Open Author Profile";
|
||||
"Common.Controls.Keyboard.Timeline.OpenRebloggerProfile" = "Open Reblogger Profile";
|
||||
"Common.Controls.Keyboard.Timeline.OpenStatus" = "Open Status";
|
||||
"Common.Controls.Keyboard.Timeline.PreviewImage" = "Preview Image";
|
||||
"Common.Controls.Keyboard.Timeline.PreviousStatus" = "Previous Status";
|
||||
"Common.Controls.Keyboard.Timeline.ReplyStatus" = "Reply Status";
|
||||
"Common.Controls.Keyboard.Timeline.ToggleContentWarning" = "Toggle Content Warning";
|
||||
"Common.Controls.Keyboard.Timeline.ToggleFavorite" = "Toggle Status Favorite";
|
||||
"Common.Controls.Keyboard.Timeline.ToggleReblog" = "Toggle Status Reblog";
|
||||
"Common.Controls.Status.Actions.Favorite" = "Favorite";
|
||||
"Common.Controls.Status.Actions.Menu" = "Menu";
|
||||
"Common.Controls.Status.Actions.Reblog" = "Reblog";
|
||||
|
@ -130,6 +150,12 @@ uploaded to Mastodon.";
|
|||
"Scene.Compose.ComposeAction" = "Publish";
|
||||
"Scene.Compose.ContentInputPlaceholder" = "Type or paste what's on your mind";
|
||||
"Scene.Compose.ContentWarning.Placeholder" = "Write an accurate warning here...";
|
||||
"Scene.Compose.Keyboard.AppendAttachmentEntry" = "Append Attachment - %@";
|
||||
"Scene.Compose.Keyboard.DiscardPost" = "Discard Post";
|
||||
"Scene.Compose.Keyboard.PublishPost" = "Publish Post";
|
||||
"Scene.Compose.Keyboard.SelectVisibilityEntry" = "Select Visibility - %@";
|
||||
"Scene.Compose.Keyboard.ToggleContentWarning" = "Toggle Content Warning";
|
||||
"Scene.Compose.Keyboard.TogglePoll" = "Toggle Poll";
|
||||
"Scene.Compose.MediaSelection.Browse" = "Browse";
|
||||
"Scene.Compose.MediaSelection.Camera" = "Take Photo";
|
||||
"Scene.Compose.MediaSelection.PhotoLibrary" = "Photo Library";
|
||||
|
@ -173,8 +199,13 @@ tap the link to confirm your account.";
|
|||
"Scene.Notification.Action.Mention" = "mentioned you";
|
||||
"Scene.Notification.Action.Poll" = "Your poll has ended";
|
||||
"Scene.Notification.Action.Reblog" = "rebloged your post";
|
||||
"Scene.Notification.Keyobard.ShowEverything" = "Show Everything";
|
||||
"Scene.Notification.Keyobard.ShowMentions" = "Show Mentions";
|
||||
"Scene.Notification.Title.Everything" = "Everything";
|
||||
"Scene.Notification.Title.Mentions" = "Mentions";
|
||||
"Scene.Preview.Keyboard.ClosePreview" = "Close Preview";
|
||||
"Scene.Preview.Keyboard.ShowNext" = "Show Next";
|
||||
"Scene.Preview.Keyboard.ShowPrevious" = "Show Previous";
|
||||
"Scene.Profile.Dashboard.Accessibility.CountFollowers" = "%ld followers";
|
||||
"Scene.Profile.Dashboard.Accessibility.CountFollowing" = "%ld following";
|
||||
"Scene.Profile.Dashboard.Accessibility.CountPosts" = "%ld posts";
|
||||
|
@ -272,6 +303,7 @@ any server.";
|
|||
"Scene.ServerRules.Subtitle" = "These rules are set by the admins of %@.";
|
||||
"Scene.ServerRules.TermsOfService" = "terms of service";
|
||||
"Scene.ServerRules.Title" = "Some ground rules.";
|
||||
"Scene.Settings.Keyboard.CloseSettingsWindow" = "Close Settings Window";
|
||||
"Scene.Settings.Section.Appearance.Automatic" = "Automatic";
|
||||
"Scene.Settings.Section.Appearance.Dark" = "Always Dark";
|
||||
"Scene.Settings.Section.Appearance.Light" = "Always Light";
|
||||
|
|
|
@ -30,10 +30,14 @@ 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 %@";
|
||||
"Common.Controls.Actions.Save" = "Save";
|
||||
"Common.Controls.Actions.SavePhoto" = "Save photo";
|
||||
|
@ -64,6 +68,22 @@ Please check your internet connection.";
|
|||
"Common.Controls.Firendship.UnblockUser" = "Unblock %@";
|
||||
"Common.Controls.Firendship.Unmute" = "Unmute";
|
||||
"Common.Controls.Firendship.UnmuteUser" = "Unmute %@";
|
||||
"Common.Controls.Keyboard.Common.ComposeNewPost" = "Compose New Post";
|
||||
"Common.Controls.Keyboard.Common.OpenSettings" = "Open Settings";
|
||||
"Common.Controls.Keyboard.Common.ShowFavorites" = "Show Favorites";
|
||||
"Common.Controls.Keyboard.Common.SwitchToTab" = "Switch to %@";
|
||||
"Common.Controls.Keyboard.SegmentedControl.NextSection" = "Next Section";
|
||||
"Common.Controls.Keyboard.SegmentedControl.PreviousSection" = "Previous Section";
|
||||
"Common.Controls.Keyboard.Timeline.NextStatus" = "Next Status";
|
||||
"Common.Controls.Keyboard.Timeline.OpenAuthorProfile" = "Open Author Profile";
|
||||
"Common.Controls.Keyboard.Timeline.OpenRebloggerProfile" = "Open Reblogger Profile";
|
||||
"Common.Controls.Keyboard.Timeline.OpenStatus" = "Open Status";
|
||||
"Common.Controls.Keyboard.Timeline.PreviewImage" = "Preview Image";
|
||||
"Common.Controls.Keyboard.Timeline.PreviousStatus" = "Previous Status";
|
||||
"Common.Controls.Keyboard.Timeline.ReplyStatus" = "Reply Status";
|
||||
"Common.Controls.Keyboard.Timeline.ToggleContentWarning" = "Toggle Content Warning";
|
||||
"Common.Controls.Keyboard.Timeline.ToggleFavorite" = "Toggle Status Favorite";
|
||||
"Common.Controls.Keyboard.Timeline.ToggleReblog" = "Toggle Status Reblog";
|
||||
"Common.Controls.Status.Actions.Favorite" = "Favorite";
|
||||
"Common.Controls.Status.Actions.Menu" = "Menu";
|
||||
"Common.Controls.Status.Actions.Reblog" = "Reblog";
|
||||
|
@ -130,6 +150,12 @@ uploaded to Mastodon.";
|
|||
"Scene.Compose.ComposeAction" = "Publish";
|
||||
"Scene.Compose.ContentInputPlaceholder" = "Type or paste what's on your mind";
|
||||
"Scene.Compose.ContentWarning.Placeholder" = "Write an accurate warning here...";
|
||||
"Scene.Compose.Keyboard.AppendAttachmentEntry" = "Append Attachment - %@";
|
||||
"Scene.Compose.Keyboard.DiscardPost" = "Discard Post";
|
||||
"Scene.Compose.Keyboard.PublishPost" = "Publish Post";
|
||||
"Scene.Compose.Keyboard.SelectVisibilityEntry" = "Select Visibility - %@";
|
||||
"Scene.Compose.Keyboard.ToggleContentWarning" = "Toggle Content Warning";
|
||||
"Scene.Compose.Keyboard.TogglePoll" = "Toggle Poll";
|
||||
"Scene.Compose.MediaSelection.Browse" = "Browse";
|
||||
"Scene.Compose.MediaSelection.Camera" = "Take Photo";
|
||||
"Scene.Compose.MediaSelection.PhotoLibrary" = "Photo Library";
|
||||
|
@ -173,8 +199,13 @@ tap the link to confirm your account.";
|
|||
"Scene.Notification.Action.Mention" = "mentioned you";
|
||||
"Scene.Notification.Action.Poll" = "Your poll has ended";
|
||||
"Scene.Notification.Action.Reblog" = "rebloged your post";
|
||||
"Scene.Notification.Keyobard.ShowEverything" = "Show Everything";
|
||||
"Scene.Notification.Keyobard.ShowMentions" = "Show Mentions";
|
||||
"Scene.Notification.Title.Everything" = "Everything";
|
||||
"Scene.Notification.Title.Mentions" = "Mentions";
|
||||
"Scene.Preview.Keyboard.ClosePreview" = "Close Preview";
|
||||
"Scene.Preview.Keyboard.ShowNext" = "Show Next";
|
||||
"Scene.Preview.Keyboard.ShowPrevious" = "Show Previous";
|
||||
"Scene.Profile.Dashboard.Accessibility.CountFollowers" = "%ld followers";
|
||||
"Scene.Profile.Dashboard.Accessibility.CountFollowing" = "%ld following";
|
||||
"Scene.Profile.Dashboard.Accessibility.CountPosts" = "%ld posts";
|
||||
|
@ -272,6 +303,7 @@ any server.";
|
|||
"Scene.ServerRules.Subtitle" = "These rules are set by the admins of %@.";
|
||||
"Scene.ServerRules.TermsOfService" = "terms of service";
|
||||
"Scene.ServerRules.Title" = "Some ground rules.";
|
||||
"Scene.Settings.Keyboard.CloseSettingsWindow" = "Close Settings Window";
|
||||
"Scene.Settings.Section.Appearance.Automatic" = "Automatic";
|
||||
"Scene.Settings.Section.Appearance.Dark" = "Always Dark";
|
||||
"Scene.Settings.Section.Appearance.Light" = "Always Light";
|
||||
|
|
|
@ -38,6 +38,7 @@ final class AutoCompleteViewController: UIViewController {
|
|||
tableView.backgroundColor = .clear
|
||||
tableView.contentInset.top = AutoCompleteViewController.chevronViewHeight
|
||||
tableView.verticalScrollIndicatorInsets.top = AutoCompleteViewController.chevronViewHeight
|
||||
tableView.showsVerticalScrollIndicator = false // avoid duplicate to the compose collection view indicator
|
||||
return tableView
|
||||
}()
|
||||
|
||||
|
|
|
@ -52,6 +52,7 @@ extension ComposeStatusContentCollectionViewCell {
|
|||
|
||||
private func _init() {
|
||||
// selectionStyle = .none
|
||||
layer.zPosition = 999
|
||||
preservesSuperviewLayoutMargins = true
|
||||
|
||||
statusContentWarningEditorView.translatesAutoresizingMaskIntoConstraints = false
|
||||
|
|
|
@ -37,6 +37,8 @@ final class ComposeViewController: UIViewController, NeedsDependency {
|
|||
button.adjustsImageWhenHighlighted = false
|
||||
return button
|
||||
}()
|
||||
|
||||
private(set) lazy var cancelBarButtonItem = UIBarButtonItem(title: L10n.Common.Controls.Actions.cancel, style: .plain, target: self, action: #selector(ComposeViewController.cancelBarButtonItemPressed(_:)))
|
||||
private(set) lazy var publishBarButtonItem: UIBarButtonItem = {
|
||||
let barButtonItem = UIBarButtonItem(customView: publishButton)
|
||||
return barButtonItem
|
||||
|
@ -138,7 +140,7 @@ extension ComposeViewController {
|
|||
}
|
||||
.store(in: &disposeBag)
|
||||
view.backgroundColor = Asset.Scene.Compose.background.color
|
||||
navigationItem.leftBarButtonItem = UIBarButtonItem(title: L10n.Common.Controls.Actions.cancel, style: .plain, target: self, action: #selector(ComposeViewController.cancelBarButtonItemPressed(_:)))
|
||||
navigationItem.leftBarButtonItem = cancelBarButtonItem
|
||||
navigationItem.rightBarButtonItem = publishBarButtonItem
|
||||
publishButton.addTarget(self, action: #selector(ComposeViewController.publishBarButtonItemPressed(_:)), for: .touchUpInside)
|
||||
|
||||
|
@ -247,7 +249,8 @@ extension ComposeViewController {
|
|||
|
||||
// adjust inset for auto-complete
|
||||
let autoCompleteTableViewBottomInset: CGFloat = {
|
||||
let tableViewFrameInWindow = self.autoCompleteViewController.tableView.superview!.convert(self.autoCompleteViewController.tableView.frame, to: nil)
|
||||
guard let superview = self.autoCompleteViewController.tableView.superview else { return .zero }
|
||||
let tableViewFrameInWindow = superview.convert(self.autoCompleteViewController.tableView.frame, to: nil)
|
||||
let padding = tableViewFrameInWindow.maxY + self.composeToolbarView.frame.height + AutoCompleteViewController.chevronViewHeight - endFrame.minY
|
||||
return max(0, padding)
|
||||
}()
|
||||
|
@ -1257,3 +1260,130 @@ extension ComposeViewController: AutoCompleteViewControllerDelegate {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension ComposeViewController {
|
||||
override var keyCommands: [UIKeyCommand]? {
|
||||
composeKeyCommands
|
||||
}
|
||||
}
|
||||
|
||||
extension ComposeViewController {
|
||||
|
||||
enum ComposeKeyCommand: String, CaseIterable {
|
||||
case discardPost
|
||||
case publishPost
|
||||
case mediaBrowse
|
||||
case mediaPhotoLibrary
|
||||
case mediaCamera
|
||||
case togglePoll
|
||||
case toggleContentWarning
|
||||
case selectVisibilityPublic
|
||||
case selectVisibilityUnlisted
|
||||
case selectVisibilityPrivate
|
||||
case selectVisibilityDirect
|
||||
|
||||
var title: String {
|
||||
switch self {
|
||||
case .discardPost: return L10n.Scene.Compose.Keyboard.discardPost
|
||||
case .publishPost: return L10n.Scene.Compose.Keyboard.publishPost
|
||||
case .mediaBrowse: return L10n.Scene.Compose.Keyboard.appendAttachmentEntry(L10n.Scene.Compose.MediaSelection.browse)
|
||||
case .mediaPhotoLibrary: return L10n.Scene.Compose.Keyboard.appendAttachmentEntry(L10n.Scene.Compose.MediaSelection.photoLibrary)
|
||||
case .mediaCamera: return L10n.Scene.Compose.Keyboard.appendAttachmentEntry(L10n.Scene.Compose.MediaSelection.camera)
|
||||
case .togglePoll: return L10n.Scene.Compose.Keyboard.togglePoll
|
||||
case .toggleContentWarning: return L10n.Scene.Compose.Keyboard.toggleContentWarning
|
||||
case .selectVisibilityPublic: return L10n.Scene.Compose.Keyboard.selectVisibilityEntry(L10n.Scene.Compose.Visibility.public)
|
||||
case .selectVisibilityUnlisted: return L10n.Scene.Compose.Keyboard.selectVisibilityEntry(L10n.Scene.Compose.Visibility.unlisted)
|
||||
case .selectVisibilityPrivate: return L10n.Scene.Compose.Keyboard.selectVisibilityEntry(L10n.Scene.Compose.Visibility.private)
|
||||
case .selectVisibilityDirect: return L10n.Scene.Compose.Keyboard.selectVisibilityEntry(L10n.Scene.Compose.Visibility.direct)
|
||||
}
|
||||
}
|
||||
|
||||
// UIKeyCommand input
|
||||
var input: String {
|
||||
switch self {
|
||||
case .discardPost: return "w" // + command
|
||||
case .publishPost: return "\r" // (enter) + command
|
||||
case .mediaBrowse: return "b" // + option + command
|
||||
case .mediaPhotoLibrary: return "p" // + option + command
|
||||
case .mediaCamera: return "c" // + option + command
|
||||
case .togglePoll: return "p" // + shift + command
|
||||
case .toggleContentWarning: return "c" // + shift + command
|
||||
case .selectVisibilityPublic: return "1" // + command
|
||||
case .selectVisibilityUnlisted: return "2" // + command
|
||||
case .selectVisibilityPrivate: return "3" // + command
|
||||
case .selectVisibilityDirect: return "4" // + command
|
||||
}
|
||||
}
|
||||
|
||||
var modifierFlags: UIKeyModifierFlags {
|
||||
switch self {
|
||||
case .discardPost: return [.command]
|
||||
case .publishPost: return [.command]
|
||||
case .mediaBrowse: return [.alternate, .command]
|
||||
case .mediaPhotoLibrary: return [.alternate, .command]
|
||||
case .mediaCamera: return [.alternate, .command]
|
||||
case .togglePoll: return [.shift, .command]
|
||||
case .toggleContentWarning: return [.shift, .command]
|
||||
case .selectVisibilityPublic: return [.command]
|
||||
case .selectVisibilityUnlisted: return [.command]
|
||||
case .selectVisibilityPrivate: return [.command]
|
||||
case .selectVisibilityDirect: return [.command]
|
||||
}
|
||||
}
|
||||
|
||||
var propertyList: Any {
|
||||
return rawValue
|
||||
}
|
||||
}
|
||||
|
||||
var composeKeyCommands: [UIKeyCommand]? {
|
||||
ComposeKeyCommand.allCases.map { command in
|
||||
UIKeyCommand(
|
||||
title: command.title,
|
||||
image: nil,
|
||||
action: #selector(Self.composeKeyCommandHandler(_:)),
|
||||
input: command.input,
|
||||
modifierFlags: command.modifierFlags,
|
||||
propertyList: command.propertyList,
|
||||
alternates: [],
|
||||
discoverabilityTitle: nil,
|
||||
attributes: [],
|
||||
state: .off
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@objc private func composeKeyCommandHandler(_ sender: UIKeyCommand) {
|
||||
guard let rawValue = sender.propertyList as? String,
|
||||
let command = ComposeKeyCommand(rawValue: rawValue) else { return }
|
||||
|
||||
switch command {
|
||||
case .discardPost:
|
||||
cancelBarButtonItemPressed(cancelBarButtonItem)
|
||||
case .publishPost:
|
||||
publishBarButtonItemPressed(publishBarButtonItem)
|
||||
case .mediaBrowse:
|
||||
present(documentPickerController, animated: true, completion: nil)
|
||||
case .mediaPhotoLibrary:
|
||||
present(imagePicker, animated: true, completion: nil)
|
||||
case .mediaCamera:
|
||||
guard UIImagePickerController.isSourceTypeAvailable(.camera) else {
|
||||
return
|
||||
}
|
||||
present(imagePickerController, animated: true, completion: nil)
|
||||
case .togglePoll:
|
||||
composeToolbarView.pollButton.sendActions(for: .touchUpInside)
|
||||
case .toggleContentWarning:
|
||||
composeToolbarView.contentWarningButton.sendActions(for: .touchUpInside)
|
||||
case .selectVisibilityPublic:
|
||||
viewModel.selectedStatusVisibility.value = .public
|
||||
case .selectVisibilityUnlisted:
|
||||
viewModel.selectedStatusVisibility.value = .unlisted
|
||||
case .selectVisibilityPrivate:
|
||||
viewModel.selectedStatusVisibility.value = .private
|
||||
case .selectVisibilityDirect:
|
||||
viewModel.selectedStatusVisibility.value = .direct
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -339,3 +339,20 @@ extension HashtagTimelineViewController: StatusTableViewCellDelegate {
|
|||
weak var playerViewControllerDelegate: AVPlayerViewControllerDelegate? { return self }
|
||||
func parent() -> UIViewController { return self }
|
||||
}
|
||||
|
||||
extension HashtagTimelineViewController {
|
||||
override var keyCommands: [UIKeyCommand]? {
|
||||
return navigationKeyCommands + statusNavigationKeyCommands
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - StatusTableViewControllerNavigateable
|
||||
extension HashtagTimelineViewController: StatusTableViewControllerNavigateable {
|
||||
@objc func navigateKeyCommandHandlerRelay(_ sender: UIKeyCommand) {
|
||||
navigateKeyCommandHandler(sender)
|
||||
}
|
||||
|
||||
@objc func statusKeyCommandHandlerRelay(_ sender: UIKeyCommand) {
|
||||
statusKeyCommandHandler(sender)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -537,3 +537,20 @@ extension HomeTimelineViewController: HomeTimelineNavigationBarTitleViewDelegate
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension HomeTimelineViewController {
|
||||
override var keyCommands: [UIKeyCommand]? {
|
||||
return navigationKeyCommands + statusNavigationKeyCommands
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - StatusTableViewControllerNavigateable
|
||||
extension HomeTimelineViewController: StatusTableViewControllerNavigateable {
|
||||
@objc func navigateKeyCommandHandlerRelay(_ sender: UIKeyCommand) {
|
||||
navigateKeyCommandHandler(sender)
|
||||
}
|
||||
|
||||
@objc func statusKeyCommandHandlerRelay(_ sender: UIKeyCommand) {
|
||||
statusKeyCommandHandler(sender)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -189,3 +189,153 @@ extension MainTabBarController {
|
|||
}
|
||||
|
||||
}
|
||||
|
||||
// HIG: keyboard UX
|
||||
// https://developer.apple.com/design/human-interface-guidelines/macos/user-interaction/keyboard/
|
||||
extension MainTabBarController {
|
||||
|
||||
var switchToTabKeyCommands: [UIKeyCommand] {
|
||||
var commands: [UIKeyCommand] = []
|
||||
for (i, tab) in Tab.allCases.enumerated() {
|
||||
let title = L10n.Common.Controls.Keyboard.Common.switchToTab(tab.title)
|
||||
let input = String(i + 1)
|
||||
let command = UIKeyCommand(
|
||||
title: title,
|
||||
image: nil,
|
||||
action: #selector(MainTabBarController.switchToTabKeyCommandHandler(_:)),
|
||||
input: input,
|
||||
modifierFlags: .command,
|
||||
propertyList: tab.rawValue,
|
||||
alternates: [],
|
||||
discoverabilityTitle: nil,
|
||||
attributes: [],
|
||||
state: .off
|
||||
)
|
||||
commands.append(command)
|
||||
}
|
||||
return commands
|
||||
}
|
||||
|
||||
var showFavoritesKeyCommand: UIKeyCommand {
|
||||
UIKeyCommand(
|
||||
title: L10n.Common.Controls.Keyboard.Common.showFavorites,
|
||||
image: nil,
|
||||
action: #selector(MainTabBarController.showFavoritesKeyCommandHandler(_:)),
|
||||
input: "f",
|
||||
modifierFlags: .command,
|
||||
propertyList: nil,
|
||||
alternates: [],
|
||||
discoverabilityTitle: nil,
|
||||
attributes: [],
|
||||
state: .off
|
||||
)
|
||||
}
|
||||
|
||||
var openSettingsKeyCommand: UIKeyCommand {
|
||||
UIKeyCommand(
|
||||
title: L10n.Common.Controls.Keyboard.Common.openSettings,
|
||||
image: nil,
|
||||
action: #selector(MainTabBarController.openSettingsKeyCommandHandler(_:)),
|
||||
input: ",",
|
||||
modifierFlags: .command,
|
||||
propertyList: nil,
|
||||
alternates: [],
|
||||
discoverabilityTitle: nil,
|
||||
attributes: [],
|
||||
state: .off
|
||||
)
|
||||
}
|
||||
|
||||
var composeNewPostKeyCommand: UIKeyCommand {
|
||||
UIKeyCommand(
|
||||
title: L10n.Common.Controls.Keyboard.Common.composeNewPost,
|
||||
image: nil,
|
||||
action: #selector(MainTabBarController.composeNewPostKeyCommandHandler(_:)),
|
||||
input: "n",
|
||||
modifierFlags: .command,
|
||||
propertyList: nil,
|
||||
alternates: [],
|
||||
discoverabilityTitle: nil,
|
||||
attributes: [],
|
||||
state: .off
|
||||
)
|
||||
}
|
||||
|
||||
override var keyCommands: [UIKeyCommand]? {
|
||||
guard let topMost = self.topMost else {
|
||||
return []
|
||||
}
|
||||
|
||||
var commands: [UIKeyCommand] = []
|
||||
|
||||
if topMost.isModal {
|
||||
|
||||
} else {
|
||||
// switch tabs
|
||||
commands.append(contentsOf: switchToTabKeyCommands)
|
||||
|
||||
// show compose
|
||||
if !(self.topMost is ComposeViewController) {
|
||||
commands.append(composeNewPostKeyCommand)
|
||||
}
|
||||
|
||||
// show favorites
|
||||
if !(self.topMost is FavoriteViewController) {
|
||||
commands.append(showFavoritesKeyCommand)
|
||||
}
|
||||
|
||||
// open settings
|
||||
if context.settingService.currentSetting.value != nil {
|
||||
commands.append(openSettingsKeyCommand)
|
||||
}
|
||||
}
|
||||
|
||||
return commands
|
||||
}
|
||||
|
||||
@objc private func switchToTabKeyCommandHandler(_ sender: UIKeyCommand) {
|
||||
guard let rawValue = sender.propertyList as? Int,
|
||||
let tab = Tab(rawValue: rawValue) else { return }
|
||||
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: %s", ((#file as NSString).lastPathComponent), #line, #function, tab.title)
|
||||
|
||||
guard let index = Tab.allCases.firstIndex(of: tab) else { return }
|
||||
let previousTab = Tab(rawValue: selectedIndex)
|
||||
selectedIndex = index
|
||||
|
||||
if let previousTab = previousTab {
|
||||
switch (tab, previousTab) {
|
||||
case (.home, .home):
|
||||
guard let navigationController = topMost?.navigationController else { return }
|
||||
if navigationController.viewControllers.count > 1 {
|
||||
// pop to top when previous tab position already is home
|
||||
navigationController.popToRootViewController(animated: true)
|
||||
} else if let homeTimelineViewController = topMost as? HomeTimelineViewController {
|
||||
// trigger scrollToTop if topMost is home timeline
|
||||
homeTimelineViewController.scrollToTop(animated: true)
|
||||
}
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@objc private func showFavoritesKeyCommandHandler(_ sender: UIKeyCommand) {
|
||||
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function)
|
||||
let favoriteViewModel = FavoriteViewModel(context: context)
|
||||
coordinator.present(scene: .favorite(viewModel: favoriteViewModel), from: nil, transition: .show)
|
||||
}
|
||||
|
||||
@objc private func openSettingsKeyCommandHandler(_ sender: UIKeyCommand) {
|
||||
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function)
|
||||
guard let setting = context.settingService.currentSetting.value else { return }
|
||||
let settingsViewModel = SettingsViewModel(context: context, setting: setting)
|
||||
coordinator.present(scene: .settings(viewModel: settingsViewModel), from: nil, transition: .modal(animated: true, completion: nil))
|
||||
}
|
||||
|
||||
@objc private func composeNewPostKeyCommandHandler(_ sender: UIKeyCommand) {
|
||||
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function)
|
||||
let composeViewModel = ComposeViewModel(context: context, composeKind: .post)
|
||||
coordinator.present(scene: .compose(viewModel: composeViewModel), from: nil, transition: .modal(animated: true, completion: nil))
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -240,3 +240,74 @@ extension MediaPreviewViewController: MediaPreviewImageViewControllerDelegate {
|
|||
}
|
||||
|
||||
}
|
||||
|
||||
extension MediaPreviewViewController {
|
||||
|
||||
var closeKeyCommand: UIKeyCommand {
|
||||
UIKeyCommand(
|
||||
title: L10n.Scene.Preview.Keyboard.closePreview,
|
||||
image: nil,
|
||||
action: #selector(MediaPreviewViewController.closePreviewKeyCommandHandler(_:)),
|
||||
input: "i",
|
||||
modifierFlags: [],
|
||||
propertyList: nil,
|
||||
alternates: [],
|
||||
discoverabilityTitle: nil,
|
||||
attributes: [],
|
||||
state: .off
|
||||
)
|
||||
}
|
||||
|
||||
var showNextKeyCommand: UIKeyCommand {
|
||||
UIKeyCommand(
|
||||
title: L10n.Scene.Preview.Keyboard.closePreview,
|
||||
image: nil,
|
||||
action: #selector(MediaPreviewViewController.showNextKeyCommandHandler(_:)),
|
||||
input: "j",
|
||||
modifierFlags: [],
|
||||
propertyList: nil,
|
||||
alternates: [],
|
||||
discoverabilityTitle: nil,
|
||||
attributes: [],
|
||||
state: .off
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
var showPreviousKeyCommand: UIKeyCommand {
|
||||
UIKeyCommand(
|
||||
title: L10n.Scene.Preview.Keyboard.closePreview,
|
||||
image: nil,
|
||||
action: #selector(MediaPreviewViewController.showPreviousKeyCommandHandler(_:)),
|
||||
input: "k",
|
||||
modifierFlags: [],
|
||||
propertyList: nil,
|
||||
alternates: [],
|
||||
discoverabilityTitle: nil,
|
||||
attributes: [],
|
||||
state: .off
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
override var keyCommands: [UIKeyCommand] {
|
||||
return [
|
||||
closeKeyCommand,
|
||||
showNextKeyCommand,
|
||||
showPreviousKeyCommand,
|
||||
]
|
||||
}
|
||||
|
||||
@objc private func closePreviewKeyCommandHandler(_ sender: UIKeyCommand) {
|
||||
dismiss(animated: true, completion: nil)
|
||||
}
|
||||
|
||||
@objc private func showNextKeyCommandHandler(_ sender: UIKeyCommand) {
|
||||
pagingViewConttroller.scrollToPage(.next, animated: true, completion: nil)
|
||||
}
|
||||
|
||||
@objc private func showPreviousKeyCommandHandler(_ sender: UIKeyCommand) {
|
||||
pagingViewConttroller.scrollToPage(.previous, animated: true, completion: nil)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -81,6 +81,25 @@ extension NotificationViewController {
|
|||
}
|
||||
}
|
||||
.store(in: &disposeBag)
|
||||
|
||||
viewModel.selectedIndex
|
||||
.removeDuplicates()
|
||||
.receive(on: DispatchQueue.main)
|
||||
.sink { [weak self] segment in
|
||||
guard let self = self else { return }
|
||||
self.segmentControl.selectedSegmentIndex = segment.rawValue
|
||||
|
||||
guard let domain = self.viewModel.activeMastodonAuthenticationBox.value?.domain, let userID = self.viewModel.activeMastodonAuthenticationBox.value?.userID else {
|
||||
return
|
||||
}
|
||||
switch segment {
|
||||
case .EveryThing:
|
||||
self.viewModel.notificationPredicate.value = MastodonNotification.predicate(domain: domain, userID: userID)
|
||||
case .Mentions:
|
||||
self.viewModel.notificationPredicate.value = MastodonNotification.predicate(domain: domain, userID: userID, typeRaw: Mastodon.Entity.Notification.NotificationType.mention.rawValue)
|
||||
}
|
||||
}
|
||||
.store(in: &disposeBag)
|
||||
}
|
||||
|
||||
override func viewWillAppear(_ animated: Bool) {
|
||||
|
@ -122,14 +141,7 @@ extension NotificationViewController {
|
|||
extension NotificationViewController {
|
||||
@objc private func segmentedControlValueChanged(_ sender: UISegmentedControl) {
|
||||
os_log("%{public}s[%{public}ld], %{public}s: select at index: %ld", (#file as NSString).lastPathComponent, #line, #function, sender.selectedSegmentIndex)
|
||||
guard let domain = viewModel.activeMastodonAuthenticationBox.value?.domain, let userID = viewModel.activeMastodonAuthenticationBox.value?.userID else {
|
||||
return
|
||||
}
|
||||
if sender.selectedSegmentIndex == NotificationViewModel.NotificationSegment.EveryThing.rawValue {
|
||||
viewModel.notificationPredicate.value = MastodonNotification.predicate(domain: domain, userID: userID)
|
||||
} else {
|
||||
viewModel.notificationPredicate.value = MastodonNotification.predicate(domain: domain, userID: userID, typeRaw: Mastodon.Entity.Notification.NotificationType.mention.rawValue)
|
||||
}
|
||||
|
||||
viewModel.selectedIndex.value = NotificationViewModel.NotificationSegment(rawValue: sender.selectedSegmentIndex)!
|
||||
}
|
||||
|
||||
|
@ -141,7 +153,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 +191,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 +283,154 @@ extension NotificationViewController: LoadMoreConfigurableTableViewContainer {
|
|||
var loadMoreConfigurableTableView: UITableView { tableView }
|
||||
var loadMoreConfigurableStateMachine: GKStateMachine { viewModel.loadoldestStateMachine }
|
||||
}
|
||||
|
||||
extension NotificationViewController {
|
||||
|
||||
enum CategorySwitch: String, CaseIterable {
|
||||
case showEverything
|
||||
case showMentions
|
||||
|
||||
var title: String {
|
||||
switch self {
|
||||
case .showEverything: return L10n.Scene.Notification.Keyobard.showEverything
|
||||
case .showMentions: return L10n.Scene.Notification.Keyobard.showMentions
|
||||
}
|
||||
}
|
||||
|
||||
// UIKeyCommand input
|
||||
var input: String {
|
||||
switch self {
|
||||
case .showEverything: return "[" // + shift + command
|
||||
case .showMentions: return "]" // + shift + command
|
||||
}
|
||||
}
|
||||
|
||||
var modifierFlags: UIKeyModifierFlags {
|
||||
switch self {
|
||||
case .showEverything: return [.shift, .command]
|
||||
case .showMentions: return [.shift, .command]
|
||||
}
|
||||
}
|
||||
|
||||
var propertyList: Any {
|
||||
return rawValue
|
||||
}
|
||||
}
|
||||
|
||||
var categorySwitchKeyCommands: [UIKeyCommand] {
|
||||
CategorySwitch.allCases.map { category in
|
||||
UIKeyCommand(
|
||||
title: category.title,
|
||||
image: nil,
|
||||
action: #selector(NotificationViewController.showCategory(_:)),
|
||||
input: category.input,
|
||||
modifierFlags: category.modifierFlags,
|
||||
propertyList: category.propertyList,
|
||||
alternates: [],
|
||||
discoverabilityTitle: nil,
|
||||
attributes: [],
|
||||
state: .off
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@objc private func showCategory(_ sender: UIKeyCommand) {
|
||||
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function)
|
||||
guard let rawValue = sender.propertyList as? String,
|
||||
let category = CategorySwitch(rawValue: rawValue) else { return }
|
||||
|
||||
switch category {
|
||||
case .showEverything:
|
||||
viewModel.selectedIndex.value = .EveryThing
|
||||
case .showMentions:
|
||||
viewModel.selectedIndex.value = .Mentions
|
||||
}
|
||||
}
|
||||
|
||||
override var keyCommands: [UIKeyCommand]? {
|
||||
return categorySwitchKeyCommands + 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)
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -8,6 +8,7 @@
|
|||
import os.log
|
||||
import UIKit
|
||||
import Combine
|
||||
import GameController
|
||||
|
||||
final class MastodonPickServerViewController: UIViewController, NeedsDependency {
|
||||
|
||||
|
@ -47,6 +48,7 @@ final class MastodonPickServerViewController: UIViewController, NeedsDependency
|
|||
button.translatesAutoresizingMaskIntoConstraints = false
|
||||
return button
|
||||
}()
|
||||
var nextStepButtonBottomLayoutConstraint: NSLayoutConstraint!
|
||||
|
||||
deinit {
|
||||
tableViewObservation = nil
|
||||
|
@ -76,11 +78,13 @@ extension MastodonPickServerViewController {
|
|||
#endif
|
||||
|
||||
view.addSubview(nextStepButton)
|
||||
nextStepButtonBottomLayoutConstraint = view.bottomAnchor.constraint(equalTo: nextStepButton.bottomAnchor, constant: 0).priority(.defaultHigh)
|
||||
NSLayoutConstraint.activate([
|
||||
nextStepButton.leadingAnchor.constraint(equalTo: view.readableContentGuide.leadingAnchor, constant: MastodonPickServerViewController.actionButtonMargin),
|
||||
view.readableContentGuide.trailingAnchor.constraint(equalTo: nextStepButton.trailingAnchor, constant: MastodonPickServerViewController.actionButtonMargin),
|
||||
nextStepButton.heightAnchor.constraint(equalToConstant: MastodonPickServerViewController.actionButtonHeight).priority(.defaultHigh),
|
||||
view.layoutMarginsGuide.bottomAnchor.constraint(equalTo: nextStepButton.bottomAnchor, constant: WelcomeViewController.viewBottomPaddingHeight),
|
||||
view.safeAreaLayoutGuide.bottomAnchor.constraint(greaterThanOrEqualTo: nextStepButton.bottomAnchor, constant: WelcomeViewController.viewBottomPaddingHeight),
|
||||
nextStepButtonBottomLayoutConstraint,
|
||||
])
|
||||
|
||||
// fix AutoLayout warning when observe before view appear
|
||||
|
@ -124,6 +128,37 @@ extension MastodonPickServerViewController {
|
|||
])
|
||||
view.sendSubviewToBack(emptyStateView)
|
||||
|
||||
// update layout when keyboard show/dismiss
|
||||
let keyboardEventPublishers = Publishers.CombineLatest3(
|
||||
KeyboardResponderService.shared.isShow,
|
||||
KeyboardResponderService.shared.state,
|
||||
KeyboardResponderService.shared.endFrame
|
||||
)
|
||||
|
||||
keyboardEventPublishers
|
||||
.sink { [weak self] keyboardEvents in
|
||||
guard let self = self else { return }
|
||||
let (isShow, state, endFrame) = keyboardEvents
|
||||
|
||||
// guard external keyboard connected
|
||||
guard isShow, state == .dock, GCKeyboard.coalesced != nil else {
|
||||
self.nextStepButtonBottomLayoutConstraint.constant = WelcomeViewController.viewBottomPaddingHeight
|
||||
return
|
||||
}
|
||||
|
||||
let externalKeyboardToolbarHeight = self.view.frame.maxY - endFrame.minY
|
||||
guard externalKeyboardToolbarHeight > 0 else {
|
||||
self.nextStepButtonBottomLayoutConstraint.constant = WelcomeViewController.viewBottomPaddingHeight
|
||||
return
|
||||
}
|
||||
|
||||
UIView.animate(withDuration: 0.3) {
|
||||
self.nextStepButtonBottomLayoutConstraint.constant = externalKeyboardToolbarHeight + 16
|
||||
self.view.layoutIfNeeded()
|
||||
}
|
||||
}
|
||||
.store(in: &disposeBag)
|
||||
|
||||
switch viewModel.mode {
|
||||
case .signIn:
|
||||
nextStepButton.setTitle(L10n.Common.Controls.Actions.signIn, for: .normal)
|
||||
|
|
|
@ -109,7 +109,7 @@ extension MastodonPickServerViewModel {
|
|||
var serverItems: [PickServerItem] = []
|
||||
for server in indexedServers {
|
||||
let attribute = oldSnapshotServerItemAttributeDict[server.domain] ?? PickServerItem.ServerItemAttribute(isLast: false, isExpand: false)
|
||||
attribute.isLast = false
|
||||
attribute.isLast.value = false
|
||||
let item = PickServerItem.server(server: server, attribute: attribute)
|
||||
guard !serverItems.contains(item) else { continue }
|
||||
serverItems.append(item)
|
||||
|
@ -119,7 +119,7 @@ extension MastodonPickServerViewModel {
|
|||
if !unindexedServers.isEmpty {
|
||||
for server in unindexedServers {
|
||||
let attribute = oldSnapshotServerItemAttributeDict[server.domain] ?? PickServerItem.ServerItemAttribute(isLast: false, isExpand: false)
|
||||
attribute.isLast = false
|
||||
attribute.isLast.value = false
|
||||
let item = PickServerItem.server(server: server, attribute: attribute)
|
||||
guard !serverItems.contains(item) else { continue }
|
||||
serverItems.append(item)
|
||||
|
@ -134,7 +134,7 @@ extension MastodonPickServerViewModel {
|
|||
}
|
||||
|
||||
if case let .server(_, attribute) = serverItems.last {
|
||||
attribute.isLast = true
|
||||
attribute.isLast.value = true
|
||||
}
|
||||
if case let .loader(attribute) = serverItems.last {
|
||||
attribute.isLast = true
|
||||
|
|
|
@ -173,3 +173,20 @@ extension FavoriteViewController: LoadMoreConfigurableTableViewContainer {
|
|||
var loadMoreConfigurableStateMachine: GKStateMachine { return viewModel.stateMachine }
|
||||
}
|
||||
|
||||
|
||||
extension FavoriteViewController {
|
||||
override var keyCommands: [UIKeyCommand]? {
|
||||
return navigationKeyCommands + statusNavigationKeyCommands
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - StatusTableViewControllerNavigateable
|
||||
extension FavoriteViewController: StatusTableViewControllerNavigateable {
|
||||
@objc func navigateKeyCommandHandlerRelay(_ sender: UIKeyCommand) {
|
||||
navigateKeyCommandHandler(sender)
|
||||
}
|
||||
|
||||
@objc func statusKeyCommandHandlerRelay(_ sender: UIKeyCommand) {
|
||||
statusKeyCommandHandler(sender)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -440,6 +440,13 @@ extension ProfileViewController {
|
|||
viewModel.isEditing
|
||||
.handleEvents(receiveOutput: { [weak self] isEditing in
|
||||
guard let self = self else { return }
|
||||
// set firset responder for key command
|
||||
if !isEditing {
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
|
||||
self.profileSegmentedViewController.pagingViewController.becomeFirstResponder()
|
||||
}
|
||||
}
|
||||
|
||||
// dismiss keyboard if needs
|
||||
if !isEditing { self.view.endEditing(true) }
|
||||
|
||||
|
@ -852,7 +859,6 @@ extension ProfileViewController: ProfileHeaderViewDelegate {
|
|||
}
|
||||
|
||||
func profileHeaderView(_ profileHeaderView: ProfileHeaderView, profileStatusDashboardView: ProfileStatusDashboardView, postDashboardMeterViewDidPressed dashboardMeterView: ProfileStatusDashboardMeterView) {
|
||||
|
||||
}
|
||||
|
||||
func profileHeaderView(_ profileHeaderView: ProfileHeaderView, profileStatusDashboardView: ProfileStatusDashboardView, followingDashboardMeterViewDidPressed dwingDashboardMeterView: ProfileStatusDashboardMeterView) {
|
||||
|
@ -860,7 +866,6 @@ extension ProfileViewController: ProfileHeaderViewDelegate {
|
|||
}
|
||||
|
||||
func profileHeaderView(_ profileHeaderView: ProfileHeaderView, profileStatusDashboardView: ProfileStatusDashboardView, followersDashboardMeterViewDidPressed dwersDashboardMeterView: ProfileStatusDashboardMeterView) {
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -869,3 +874,26 @@ extension ProfileViewController: ProfileHeaderViewDelegate {
|
|||
extension ProfileViewController: ScrollViewContainer {
|
||||
var scrollView: UIScrollView { return overlayScrollView }
|
||||
}
|
||||
|
||||
extension ProfileViewController {
|
||||
|
||||
override var keyCommands: [UIKeyCommand]? {
|
||||
if !viewModel.isEditing.value {
|
||||
return segmentedControlNavigateKeyCommands
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// MARK: - SegmentedControlNavigateable
|
||||
extension ProfileViewController: SegmentedControlNavigateable {
|
||||
var navigateableSegmentedControl: UISegmentedControl {
|
||||
profileHeaderViewController.pageSegmentedControl
|
||||
}
|
||||
|
||||
@objc func segmentedControlNavigateKeyCommandHandlerRelay(_ sender: UIKeyCommand) {
|
||||
segmentedControlNavigateKeyCommandHandler(sender)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -26,9 +26,15 @@ final class ProfilePagingViewController: TabmanViewController {
|
|||
super.pageboyViewController(pageboyViewController, didScrollToPageAt: index, direction: direction, animated: animated)
|
||||
|
||||
let viewController = viewModel.viewControllers[index]
|
||||
(viewController as? StatusTableViewControllerNavigateable)?.overrideNavigationScrollPosition = .top
|
||||
pagingDelegate?.profilePagingViewController(self, didScrollToPostCustomScrollViewContainerController: viewController, atIndex: index)
|
||||
}
|
||||
|
||||
// make key commands works
|
||||
override var canBecomeFirstResponder: Bool {
|
||||
return true
|
||||
}
|
||||
|
||||
deinit {
|
||||
os_log("%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function)
|
||||
}
|
||||
|
@ -43,5 +49,29 @@ extension ProfilePagingViewController {
|
|||
view.backgroundColor = .clear
|
||||
dataSource = viewModel
|
||||
}
|
||||
|
||||
override func viewDidAppear(_ animated: Bool) {
|
||||
super.viewDidAppear(animated)
|
||||
|
||||
becomeFirstResponder()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// workaround to fix tab man responder chain issue
|
||||
extension ProfilePagingViewController {
|
||||
|
||||
override var keyCommands: [UIKeyCommand]? {
|
||||
return currentViewController?.keyCommands
|
||||
}
|
||||
|
||||
@objc func navigateKeyCommandHandlerRelay(_ sender: UIKeyCommand) {
|
||||
(currentViewController as? StatusTableViewControllerNavigateable)?.navigateKeyCommandHandlerRelay(sender)
|
||||
|
||||
}
|
||||
|
||||
@objc func statusKeyCommandHandlerRelay(_ sender: UIKeyCommand) {
|
||||
(currentViewController as? StatusTableViewControllerNavigateable)?.statusKeyCommandHandlerRelay(sender)
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -14,6 +14,7 @@ final class ProfileSegmentedViewController: UIViewController {
|
|||
deinit {
|
||||
os_log("%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension ProfileSegmentedViewController {
|
||||
|
|
|
@ -14,7 +14,7 @@ import GameplayKit
|
|||
|
||||
// TODO: adopt MediaPreviewableViewController
|
||||
final class UserTimelineViewController: UIViewController, NeedsDependency, MediaPreviewableViewController {
|
||||
|
||||
|
||||
weak var context: AppContext! { willSet { precondition(!isViewLoaded) } }
|
||||
weak var coordinator: SceneCoordinator! { willSet { precondition(!isViewLoaded) } }
|
||||
|
||||
|
@ -34,6 +34,8 @@ final class UserTimelineViewController: UIViewController, NeedsDependency, Media
|
|||
return tableView
|
||||
}()
|
||||
|
||||
var overrideNavigationScrollPosition: UITableView.ScrollPosition? = nil
|
||||
|
||||
deinit {
|
||||
os_log("%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function)
|
||||
}
|
||||
|
@ -185,3 +187,20 @@ extension UserTimelineViewController: LoadMoreConfigurableTableViewContainer {
|
|||
var loadMoreConfigurableTableView: UITableView { return tableView }
|
||||
var loadMoreConfigurableStateMachine: GKStateMachine { return viewModel.stateMachine }
|
||||
}
|
||||
|
||||
extension UserTimelineViewController {
|
||||
override var keyCommands: [UIKeyCommand]? {
|
||||
return navigationKeyCommands + statusNavigationKeyCommands
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - StatusTableViewControllerNavigateable
|
||||
extension UserTimelineViewController: StatusTableViewControllerNavigateable {
|
||||
@objc func navigateKeyCommandHandlerRelay(_ sender: UIKeyCommand) {
|
||||
navigateKeyCommandHandler(sender)
|
||||
}
|
||||
|
||||
@objc func statusKeyCommandHandlerRelay(_ sender: UIKeyCommand) {
|
||||
statusKeyCommandHandler(sender)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -439,6 +439,41 @@ extension SettingsViewController: ActiveLabelDelegate {
|
|||
}
|
||||
}
|
||||
|
||||
// MARK: - UIAdaptivePresentationControllerDelegate
|
||||
extension SettingsViewController: UIAdaptivePresentationControllerDelegate {
|
||||
func adaptivePresentationStyle(for controller: UIPresentationController, traitCollection: UITraitCollection) -> UIModalPresentationStyle {
|
||||
return .formSheet
|
||||
}
|
||||
}
|
||||
|
||||
extension SettingsViewController {
|
||||
|
||||
var closeKeyCommand: UIKeyCommand {
|
||||
UIKeyCommand(
|
||||
title: L10n.Scene.Settings.Keyboard.closeSettingsWindow,
|
||||
image: nil,
|
||||
action: #selector(SettingsViewController.closeSettingsWindowKeyCommandHandler(_:)),
|
||||
input: "w",
|
||||
modifierFlags: .command,
|
||||
propertyList: nil,
|
||||
alternates: [],
|
||||
discoverabilityTitle: nil,
|
||||
attributes: [],
|
||||
state: .off
|
||||
)
|
||||
}
|
||||
|
||||
override var keyCommands: [UIKeyCommand]? {
|
||||
return [closeKeyCommand]
|
||||
}
|
||||
|
||||
@objc private func closeSettingsWindowKeyCommandHandler(_ sender: UIKeyCommand) {
|
||||
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function)
|
||||
dismiss(animated: true, completion: nil)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
#if canImport(SwiftUI) && DEBUG
|
||||
import SwiftUI
|
||||
|
||||
|
|
|
@ -56,7 +56,6 @@ final class StatusTableViewCell: UITableViewCell, StatusCell {
|
|||
var disposeBag = Set<AnyCancellable>()
|
||||
var pollCountdownSubscription: AnyCancellable?
|
||||
var observations = Set<NSKeyValueObservation>()
|
||||
private var selectionBackgroundViewObservation: NSKeyValueObservation?
|
||||
|
||||
let statusView = StatusView()
|
||||
let threadMetaStackView = UIStackView()
|
||||
|
|
|
@ -227,3 +227,20 @@ extension ThreadViewController: ThreadReplyLoaderTableViewCellDelegate {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension ThreadViewController {
|
||||
override var keyCommands: [UIKeyCommand]? {
|
||||
return navigationKeyCommands + statusNavigationKeyCommands
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - StatusTableViewControllerNavigateable
|
||||
extension ThreadViewController: StatusTableViewControllerNavigateable {
|
||||
@objc func navigateKeyCommandHandlerRelay(_ sender: UIKeyCommand) {
|
||||
navigateKeyCommandHandler(sender)
|
||||
}
|
||||
|
||||
@objc func statusKeyCommandHandlerRelay(_ sender: UIKeyCommand) {
|
||||
statusKeyCommandHandler(sender)
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue