forked from zelo72/mastodon-ios
Merge branch 'release/0.5.0'
This commit is contained in:
commit
5041d3fa24
|
@ -44,6 +44,9 @@
|
||||||
"controls": {
|
"controls": {
|
||||||
"actions": {
|
"actions": {
|
||||||
"back": "Back",
|
"back": "Back",
|
||||||
|
"next": "Next",
|
||||||
|
"previous": "Previous",
|
||||||
|
"open": "Open",
|
||||||
"add": "Add",
|
"add": "Add",
|
||||||
"remove": "Remove",
|
"remove": "Remove",
|
||||||
"edit": "Edit",
|
"edit": "Edit",
|
||||||
|
@ -68,6 +71,7 @@
|
||||||
"find_people": "Find people to follow",
|
"find_people": "Find people to follow",
|
||||||
"manually_search": "Manually search instead",
|
"manually_search": "Manually search instead",
|
||||||
"skip": "Skip",
|
"skip": "Skip",
|
||||||
|
"reply": "Reply",
|
||||||
"report_user": "Report %s",
|
"report_user": "Report %s",
|
||||||
"block_domain": "Block %s",
|
"block_domain": "Block %s",
|
||||||
"unblock_domain": "Unblock %s",
|
"unblock_domain": "Unblock %s",
|
||||||
|
@ -80,6 +84,30 @@
|
||||||
"notification": "Notification",
|
"notification": "Notification",
|
||||||
"profile": "Profile"
|
"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": {
|
"status": {
|
||||||
"user_reblogged": "%s reblogged",
|
"user_reblogged": "%s reblogged",
|
||||||
"user_replied_to": "Replied to %s",
|
"user_replied_to": "Replied to %s",
|
||||||
|
@ -340,6 +368,10 @@
|
||||||
"private": "Followers only",
|
"private": "Followers only",
|
||||||
"direct": "Only people I mention"
|
"direct": "Only people I mention"
|
||||||
},
|
},
|
||||||
|
"auto_complete": {
|
||||||
|
"single_people_talking": "%ld people talking",
|
||||||
|
"multiple_people_talking": "%ld people talking"
|
||||||
|
},
|
||||||
"accessibility": {
|
"accessibility": {
|
||||||
"append_attachment": "Append attachment",
|
"append_attachment": "Append attachment",
|
||||||
"append_poll": "Append poll",
|
"append_poll": "Append poll",
|
||||||
|
@ -350,6 +382,14 @@
|
||||||
"post_visibility_menu": "Post visibility menu",
|
"post_visibility_menu": "Post visibility menu",
|
||||||
"input_limit_remains_count": "Input limit remains %ld",
|
"input_limit_remains_count": "Input limit remains %ld",
|
||||||
"input_limit_exceeds_count": "Input limit exceeds %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": {
|
"profile": {
|
||||||
|
@ -426,6 +466,10 @@
|
||||||
"poll": "Your poll has ended",
|
"poll": "Your poll has ended",
|
||||||
"mention": "mentioned you",
|
"mention": "mentioned you",
|
||||||
"follow_request": "request to follow you"
|
"follow_request": "request to follow you"
|
||||||
|
},
|
||||||
|
"keyobard": {
|
||||||
|
"show_everything": "Show Everything",
|
||||||
|
"show_mentions": "Show Mentions"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"thread": {
|
"thread": {
|
||||||
|
@ -473,6 +517,9 @@
|
||||||
"clear": "Clear Media Cache",
|
"clear": "Clear Media Cache",
|
||||||
"signout": "Sign Out"
|
"signout": "Sign Out"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"keyboard": {
|
||||||
|
"close_settings_window": "Close Settings Window"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"report": {
|
"report": {
|
||||||
|
@ -484,6 +531,13 @@
|
||||||
"send": "Send Report",
|
"send": "Send Report",
|
||||||
"skip_to_send": "Send without comment",
|
"skip_to_send": "Send without comment",
|
||||||
"text_placeholder": "Type or paste additional comments"
|
"text_placeholder": "Type or paste additional comments"
|
||||||
|
},
|
||||||
|
"preview": {
|
||||||
|
"keyboard": {
|
||||||
|
"close_preview": "Close Preview",
|
||||||
|
"show_next": "Show Next",
|
||||||
|
"show_previous": "Show Previous"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -189,12 +189,20 @@
|
||||||
DB0140CF25C42AEE00F9F3CF /* OSLog.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB0140CE25C42AEE00F9F3CF /* OSLog.swift */; };
|
DB0140CF25C42AEE00F9F3CF /* OSLog.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB0140CE25C42AEE00F9F3CF /* OSLog.swift */; };
|
||||||
DB02CDAB26256A9500D0A2AF /* ThreadReplyLoaderTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB02CDAA26256A9500D0A2AF /* ThreadReplyLoaderTableViewCell.swift */; };
|
DB02CDAB26256A9500D0A2AF /* ThreadReplyLoaderTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB02CDAA26256A9500D0A2AF /* ThreadReplyLoaderTableViewCell.swift */; };
|
||||||
DB02CDBF2625AE5000D0A2AF /* AdaptiveUserInterfaceStyleBarButtonItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB02CDBE2625AE5000D0A2AF /* AdaptiveUserInterfaceStyleBarButtonItem.swift */; };
|
DB02CDBF2625AE5000D0A2AF /* AdaptiveUserInterfaceStyleBarButtonItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB02CDBE2625AE5000D0A2AF /* AdaptiveUserInterfaceStyleBarButtonItem.swift */; };
|
||||||
|
DB040ECD26526EA600BEE9D8 /* ComposeCollectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB040ECC26526EA600BEE9D8 /* ComposeCollectionView.swift */; };
|
||||||
|
DB040ED126538E3D00BEE9D8 /* Trie.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB040ED026538E3C00BEE9D8 /* Trie.swift */; };
|
||||||
DB084B5725CBC56C00F898ED /* Status.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB084B5625CBC56C00F898ED /* Status.swift */; };
|
DB084B5725CBC56C00F898ED /* Status.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB084B5625CBC56C00F898ED /* Status.swift */; };
|
||||||
DB0AC6FC25CD02E600D75117 /* APIService+Instance.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB0AC6FB25CD02E600D75117 /* APIService+Instance.swift */; };
|
DB0AC6FC25CD02E600D75117 /* APIService+Instance.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB0AC6FB25CD02E600D75117 /* APIService+Instance.swift */; };
|
||||||
DB0F8150264D1E2500F2A12B /* PickServerLoaderTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB0F814F264D1E2500F2A12B /* PickServerLoaderTableViewCell.swift */; };
|
DB0F8150264D1E2500F2A12B /* PickServerLoaderTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB0F814F264D1E2500F2A12B /* PickServerLoaderTableViewCell.swift */; };
|
||||||
DB118A8225E4B6E600FAB162 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = DB118A8125E4B6E600FAB162 /* Preview Assets.xcassets */; };
|
DB118A8225E4B6E600FAB162 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = DB118A8125E4B6E600FAB162 /* Preview Assets.xcassets */; };
|
||||||
DB118A8C25E4BFB500FAB162 /* HighlightDimmableButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB118A8B25E4BFB500FAB162 /* HighlightDimmableButton.swift */; };
|
DB118A8C25E4BFB500FAB162 /* HighlightDimmableButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB118A8B25E4BFB500FAB162 /* HighlightDimmableButton.swift */; };
|
||||||
DB1D186C25EF5BA7003F1F23 /* PollTableView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB1D186B25EF5BA7003F1F23 /* PollTableView.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 */; };
|
DB1E346825F518E20079D7DF /* CategoryPickerSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB1E346725F518E20079D7DF /* CategoryPickerSection.swift */; };
|
||||||
DB1E347825F519300079D7DF /* PickServerItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB1E347725F519300079D7DF /* PickServerItem.swift */; };
|
DB1E347825F519300079D7DF /* PickServerItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB1E347725F519300079D7DF /* PickServerItem.swift */; };
|
||||||
DB1FD43625F26899004CFCFC /* MastodonPickServerViewModel+LoadIndexedServerState.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB1FD43525F26899004CFCFC /* MastodonPickServerViewModel+LoadIndexedServerState.swift */; };
|
DB1FD43625F26899004CFCFC /* MastodonPickServerViewModel+LoadIndexedServerState.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB1FD43525F26899004CFCFC /* MastodonPickServerViewModel+LoadIndexedServerState.swift */; };
|
||||||
|
@ -206,8 +214,6 @@
|
||||||
DB2B3AE925E38850007045F9 /* UIViewPreview.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB2B3AE825E38850007045F9 /* UIViewPreview.swift */; };
|
DB2B3AE925E38850007045F9 /* UIViewPreview.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB2B3AE825E38850007045F9 /* UIViewPreview.swift */; };
|
||||||
DB2F073525E8ECF000957B2D /* AuthenticationViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB2F073325E8ECF000957B2D /* AuthenticationViewModel.swift */; };
|
DB2F073525E8ECF000957B2D /* AuthenticationViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB2F073325E8ECF000957B2D /* AuthenticationViewModel.swift */; };
|
||||||
DB2FF510260B113300ADA9FE /* ComposeStatusPollExpiresOptionCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB2FF50F260B113300ADA9FE /* ComposeStatusPollExpiresOptionCollectionViewCell.swift */; };
|
DB2FF510260B113300ADA9FE /* ComposeStatusPollExpiresOptionCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB2FF50F260B113300ADA9FE /* ComposeStatusPollExpiresOptionCollectionViewCell.swift */; };
|
||||||
DB35B0B32643D821006AC73B /* TwitterTextEditor in Frameworks */ = {isa = PBXBuildFile; productRef = DB35B0B22643D821006AC73B /* TwitterTextEditor */; };
|
|
||||||
DB35B0B42643D821006AC73B /* TwitterTextEditor in Embed Frameworks */ = {isa = PBXBuildFile; productRef = DB35B0B22643D821006AC73B /* TwitterTextEditor */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; };
|
|
||||||
DB35FC1F2612F1D9006193C9 /* ProfileRelationshipActionButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB35FC1E2612F1D9006193C9 /* ProfileRelationshipActionButton.swift */; };
|
DB35FC1F2612F1D9006193C9 /* ProfileRelationshipActionButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB35FC1E2612F1D9006193C9 /* ProfileRelationshipActionButton.swift */; };
|
||||||
DB35FC252612FD7A006193C9 /* ProfileFieldView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB35FC242612FD7A006193C9 /* ProfileFieldView.swift */; };
|
DB35FC252612FD7A006193C9 /* ProfileFieldView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB35FC242612FD7A006193C9 /* ProfileFieldView.swift */; };
|
||||||
DB35FC2F26130172006193C9 /* MastodonField.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB35FC2E26130172006193C9 /* MastodonField.swift */; };
|
DB35FC2F26130172006193C9 /* MastodonField.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB35FC2E26130172006193C9 /* MastodonField.swift */; };
|
||||||
|
@ -311,6 +317,11 @@
|
||||||
DB6D9F7D26358ED4008423CD /* SettingsSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB6D9F7C26358ED4008423CD /* SettingsSection.swift */; };
|
DB6D9F7D26358ED4008423CD /* SettingsSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB6D9F7C26358ED4008423CD /* SettingsSection.swift */; };
|
||||||
DB6D9F8426358EEC008423CD /* SettingsItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB6D9F8326358EEC008423CD /* SettingsItem.swift */; };
|
DB6D9F8426358EEC008423CD /* SettingsItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB6D9F8326358EEC008423CD /* SettingsItem.swift */; };
|
||||||
DB6D9F9726367249008423CD /* SettingsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB6D9F9626367249008423CD /* SettingsViewController.swift */; };
|
DB6D9F9726367249008423CD /* SettingsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB6D9F9626367249008423CD /* SettingsViewController.swift */; };
|
||||||
|
DB6F5E2F264E5518009108F4 /* MastodonRegex.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB6F5E2E264E5518009108F4 /* MastodonRegex.swift */; };
|
||||||
|
DB6F5E32264E7410009108F4 /* TwitterTextEditor in Frameworks */ = {isa = PBXBuildFile; productRef = DB6F5E31264E7410009108F4 /* TwitterTextEditor */; };
|
||||||
|
DB6F5E33264E7410009108F4 /* TwitterTextEditor in Embed Frameworks */ = {isa = PBXBuildFile; productRef = DB6F5E31264E7410009108F4 /* TwitterTextEditor */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; };
|
||||||
|
DB6F5E35264E78E7009108F4 /* AutoCompleteViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB6F5E34264E78E7009108F4 /* AutoCompleteViewController.swift */; };
|
||||||
|
DB6F5E38264E994A009108F4 /* AutoCompleteTopChevronView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB6F5E37264E994A009108F4 /* AutoCompleteTopChevronView.swift */; };
|
||||||
DB71FD2C25F86A5100512AE1 /* AvatarStackContainerButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB71FD2B25F86A5100512AE1 /* AvatarStackContainerButton.swift */; };
|
DB71FD2C25F86A5100512AE1 /* AvatarStackContainerButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB71FD2B25F86A5100512AE1 /* AvatarStackContainerButton.swift */; };
|
||||||
DB71FD3625F8A16C00512AE1 /* APIService+Persist+PersistMemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB71FD3525F8A16C00512AE1 /* APIService+Persist+PersistMemo.swift */; };
|
DB71FD3625F8A16C00512AE1 /* APIService+Persist+PersistMemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB71FD3525F8A16C00512AE1 /* APIService+Persist+PersistMemo.swift */; };
|
||||||
DB71FD3C25F8A1C500512AE1 /* APIService+Persist+PersistCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB71FD3B25F8A1C500512AE1 /* APIService+Persist+PersistCache.swift */; };
|
DB71FD3C25F8A1C500512AE1 /* APIService+Persist+PersistCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB71FD3B25F8A1C500512AE1 /* APIService+Persist+PersistCache.swift */; };
|
||||||
|
@ -414,6 +425,12 @@
|
||||||
DBB525852612D6DD002F1F29 /* ProfileStatusDashboardMeterView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBB525842612D6DD002F1F29 /* ProfileStatusDashboardMeterView.swift */; };
|
DBB525852612D6DD002F1F29 /* ProfileStatusDashboardMeterView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBB525842612D6DD002F1F29 /* ProfileStatusDashboardMeterView.swift */; };
|
||||||
DBB9759C262462E1004620BD /* ThreadMetaView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBB9759B262462E1004620BD /* ThreadMetaView.swift */; };
|
DBB9759C262462E1004620BD /* ThreadMetaView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBB9759B262462E1004620BD /* ThreadMetaView.swift */; };
|
||||||
DBBE1B4525F3474B0081417A /* MastodonPickServerAppearance.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBBE1B4425F3474B0081417A /* MastodonPickServerAppearance.swift */; };
|
DBBE1B4525F3474B0081417A /* MastodonPickServerAppearance.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBBE1B4425F3474B0081417A /* MastodonPickServerAppearance.swift */; };
|
||||||
|
DBBF1DBF2652401B00E5B703 /* AutoCompleteViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBBF1DBE2652401B00E5B703 /* AutoCompleteViewModel.swift */; };
|
||||||
|
DBBF1DC226524D2900E5B703 /* AutoCompleteTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBBF1DC126524D2900E5B703 /* AutoCompleteTableViewCell.swift */; };
|
||||||
|
DBBF1DC5265251C300E5B703 /* AutoCompleteViewModel+Diffable.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBBF1DC4265251C300E5B703 /* AutoCompleteViewModel+Diffable.swift */; };
|
||||||
|
DBBF1DC7265251D400E5B703 /* AutoCompleteViewModel+State.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBBF1DC6265251D400E5B703 /* AutoCompleteViewModel+State.swift */; };
|
||||||
|
DBBF1DC92652538500E5B703 /* AutoCompleteSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBBF1DC82652538500E5B703 /* AutoCompleteSection.swift */; };
|
||||||
|
DBBF1DCB2652539E00E5B703 /* AutoCompleteItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBBF1DCA2652539E00E5B703 /* AutoCompleteItem.swift */; };
|
||||||
DBC7A672260C897100E57475 /* StatusContentWarningEditorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBC7A671260C897100E57475 /* StatusContentWarningEditorView.swift */; };
|
DBC7A672260C897100E57475 /* StatusContentWarningEditorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBC7A671260C897100E57475 /* StatusContentWarningEditorView.swift */; };
|
||||||
DBC7A67C260DFADE00E57475 /* StatusPublishService.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBC7A67B260DFADE00E57475 /* StatusPublishService.swift */; };
|
DBC7A67C260DFADE00E57475 /* StatusPublishService.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBC7A67B260DFADE00E57475 /* StatusPublishService.swift */; };
|
||||||
DBCBED1726132DB500B49291 /* UserTimelineViewModel+Diffable.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBCBED1626132DB500B49291 /* UserTimelineViewModel+Diffable.swift */; };
|
DBCBED1726132DB500B49291 /* UserTimelineViewModel+Diffable.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBCBED1626132DB500B49291 /* UserTimelineViewModel+Diffable.swift */; };
|
||||||
|
@ -527,7 +544,7 @@
|
||||||
files = (
|
files = (
|
||||||
DB6804872637CD4C00430867 /* AppShared.framework in Embed Frameworks */,
|
DB6804872637CD4C00430867 /* AppShared.framework in Embed Frameworks */,
|
||||||
DB89BA0425C10FD0008580ED /* CoreDataStack.framework in Embed Frameworks */,
|
DB89BA0425C10FD0008580ED /* CoreDataStack.framework in Embed Frameworks */,
|
||||||
DB35B0B42643D821006AC73B /* TwitterTextEditor in Embed Frameworks */,
|
DB6F5E33264E7410009108F4 /* TwitterTextEditor in Embed Frameworks */,
|
||||||
);
|
);
|
||||||
name = "Embed Frameworks";
|
name = "Embed Frameworks";
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
@ -731,6 +748,8 @@
|
||||||
DB0140CE25C42AEE00F9F3CF /* OSLog.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OSLog.swift; sourceTree = "<group>"; };
|
DB0140CE25C42AEE00F9F3CF /* OSLog.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OSLog.swift; sourceTree = "<group>"; };
|
||||||
DB02CDAA26256A9500D0A2AF /* ThreadReplyLoaderTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThreadReplyLoaderTableViewCell.swift; sourceTree = "<group>"; };
|
DB02CDAA26256A9500D0A2AF /* ThreadReplyLoaderTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThreadReplyLoaderTableViewCell.swift; sourceTree = "<group>"; };
|
||||||
DB02CDBE2625AE5000D0A2AF /* AdaptiveUserInterfaceStyleBarButtonItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AdaptiveUserInterfaceStyleBarButtonItem.swift; sourceTree = "<group>"; };
|
DB02CDBE2625AE5000D0A2AF /* AdaptiveUserInterfaceStyleBarButtonItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AdaptiveUserInterfaceStyleBarButtonItem.swift; sourceTree = "<group>"; };
|
||||||
|
DB040ECC26526EA600BEE9D8 /* ComposeCollectionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeCollectionView.swift; sourceTree = "<group>"; };
|
||||||
|
DB040ED026538E3C00BEE9D8 /* Trie.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Trie.swift; sourceTree = "<group>"; };
|
||||||
DB084B5625CBC56C00F898ED /* Status.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Status.swift; sourceTree = "<group>"; };
|
DB084B5625CBC56C00F898ED /* Status.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Status.swift; sourceTree = "<group>"; };
|
||||||
DB0AC6FB25CD02E600D75117 /* APIService+Instance.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "APIService+Instance.swift"; sourceTree = "<group>"; };
|
DB0AC6FB25CD02E600D75117 /* APIService+Instance.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "APIService+Instance.swift"; sourceTree = "<group>"; };
|
||||||
DB0F814D264CFFD300F2A12B /* ar */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ar; path = ar.lproj/Localizable.strings; sourceTree = "<group>"; };
|
DB0F814D264CFFD300F2A12B /* ar */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ar; path = ar.lproj/Localizable.strings; sourceTree = "<group>"; };
|
||||||
|
@ -739,6 +758,12 @@
|
||||||
DB118A8125E4B6E600FAB162 /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = "<group>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
DB1E346725F518E20079D7DF /* CategoryPickerSection.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CategoryPickerSection.swift; sourceTree = "<group>"; };
|
||||||
DB1E347725F519300079D7DF /* PickServerItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PickServerItem.swift; sourceTree = "<group>"; };
|
DB1E347725F519300079D7DF /* PickServerItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PickServerItem.swift; sourceTree = "<group>"; };
|
||||||
DB1FD43525F26899004CFCFC /* MastodonPickServerViewModel+LoadIndexedServerState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MastodonPickServerViewModel+LoadIndexedServerState.swift"; sourceTree = "<group>"; };
|
DB1FD43525F26899004CFCFC /* MastodonPickServerViewModel+LoadIndexedServerState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MastodonPickServerViewModel+LoadIndexedServerState.swift"; sourceTree = "<group>"; };
|
||||||
|
@ -853,6 +878,9 @@
|
||||||
DB6D9F7C26358ED4008423CD /* SettingsSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsSection.swift; sourceTree = "<group>"; };
|
DB6D9F7C26358ED4008423CD /* SettingsSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsSection.swift; sourceTree = "<group>"; };
|
||||||
DB6D9F8326358EEC008423CD /* SettingsItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsItem.swift; sourceTree = "<group>"; };
|
DB6D9F8326358EEC008423CD /* SettingsItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsItem.swift; sourceTree = "<group>"; };
|
||||||
DB6D9F9626367249008423CD /* SettingsViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SettingsViewController.swift; sourceTree = "<group>"; };
|
DB6D9F9626367249008423CD /* SettingsViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SettingsViewController.swift; sourceTree = "<group>"; };
|
||||||
|
DB6F5E2E264E5518009108F4 /* MastodonRegex.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonRegex.swift; sourceTree = "<group>"; };
|
||||||
|
DB6F5E34264E78E7009108F4 /* AutoCompleteViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AutoCompleteViewController.swift; sourceTree = "<group>"; };
|
||||||
|
DB6F5E37264E994A009108F4 /* AutoCompleteTopChevronView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AutoCompleteTopChevronView.swift; sourceTree = "<group>"; };
|
||||||
DB71FD2B25F86A5100512AE1 /* AvatarStackContainerButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AvatarStackContainerButton.swift; sourceTree = "<group>"; };
|
DB71FD2B25F86A5100512AE1 /* AvatarStackContainerButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AvatarStackContainerButton.swift; sourceTree = "<group>"; };
|
||||||
DB71FD3525F8A16C00512AE1 /* APIService+Persist+PersistMemo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "APIService+Persist+PersistMemo.swift"; sourceTree = "<group>"; };
|
DB71FD3525F8A16C00512AE1 /* APIService+Persist+PersistMemo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "APIService+Persist+PersistMemo.swift"; sourceTree = "<group>"; };
|
||||||
DB71FD3B25F8A1C500512AE1 /* APIService+Persist+PersistCache.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "APIService+Persist+PersistCache.swift"; sourceTree = "<group>"; };
|
DB71FD3B25F8A1C500512AE1 /* APIService+Persist+PersistCache.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "APIService+Persist+PersistCache.swift"; sourceTree = "<group>"; };
|
||||||
|
@ -956,6 +984,12 @@
|
||||||
DBB525842612D6DD002F1F29 /* ProfileStatusDashboardMeterView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileStatusDashboardMeterView.swift; sourceTree = "<group>"; };
|
DBB525842612D6DD002F1F29 /* ProfileStatusDashboardMeterView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileStatusDashboardMeterView.swift; sourceTree = "<group>"; };
|
||||||
DBB9759B262462E1004620BD /* ThreadMetaView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThreadMetaView.swift; sourceTree = "<group>"; };
|
DBB9759B262462E1004620BD /* ThreadMetaView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThreadMetaView.swift; sourceTree = "<group>"; };
|
||||||
DBBE1B4425F3474B0081417A /* MastodonPickServerAppearance.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonPickServerAppearance.swift; sourceTree = "<group>"; };
|
DBBE1B4425F3474B0081417A /* MastodonPickServerAppearance.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonPickServerAppearance.swift; sourceTree = "<group>"; };
|
||||||
|
DBBF1DBE2652401B00E5B703 /* AutoCompleteViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AutoCompleteViewModel.swift; sourceTree = "<group>"; };
|
||||||
|
DBBF1DC126524D2900E5B703 /* AutoCompleteTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AutoCompleteTableViewCell.swift; sourceTree = "<group>"; };
|
||||||
|
DBBF1DC4265251C300E5B703 /* AutoCompleteViewModel+Diffable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AutoCompleteViewModel+Diffable.swift"; sourceTree = "<group>"; };
|
||||||
|
DBBF1DC6265251D400E5B703 /* AutoCompleteViewModel+State.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AutoCompleteViewModel+State.swift"; sourceTree = "<group>"; };
|
||||||
|
DBBF1DC82652538500E5B703 /* AutoCompleteSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AutoCompleteSection.swift; sourceTree = "<group>"; };
|
||||||
|
DBBF1DCA2652539E00E5B703 /* AutoCompleteItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AutoCompleteItem.swift; sourceTree = "<group>"; };
|
||||||
DBC7A671260C897100E57475 /* StatusContentWarningEditorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusContentWarningEditorView.swift; sourceTree = "<group>"; };
|
DBC7A671260C897100E57475 /* StatusContentWarningEditorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusContentWarningEditorView.swift; sourceTree = "<group>"; };
|
||||||
DBC7A67B260DFADE00E57475 /* StatusPublishService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusPublishService.swift; sourceTree = "<group>"; };
|
DBC7A67B260DFADE00E57475 /* StatusPublishService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusPublishService.swift; sourceTree = "<group>"; };
|
||||||
DBCBED1626132DB500B49291 /* UserTimelineViewModel+Diffable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UserTimelineViewModel+Diffable.swift"; sourceTree = "<group>"; };
|
DBCBED1626132DB500B49291 /* UserTimelineViewModel+Diffable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UserTimelineViewModel+Diffable.swift"; sourceTree = "<group>"; };
|
||||||
|
@ -993,7 +1027,7 @@
|
||||||
isa = PBXFrameworksBuildPhase;
|
isa = PBXFrameworksBuildPhase;
|
||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
files = (
|
files = (
|
||||||
DB35B0B32643D821006AC73B /* TwitterTextEditor in Frameworks */,
|
DB6F5E32264E7410009108F4 /* TwitterTextEditor in Frameworks */,
|
||||||
DB0140BD25C40D7500F9F3CF /* CommonOSLog in Frameworks */,
|
DB0140BD25C40D7500F9F3CF /* CommonOSLog in Frameworks */,
|
||||||
DB89BA0325C10FD0008580ED /* CoreDataStack.framework in Frameworks */,
|
DB89BA0325C10FD0008580ED /* CoreDataStack.framework in Frameworks */,
|
||||||
2D42FF6125C8177C004A627A /* ActiveLabel in Frameworks */,
|
2D42FF6125C8177C004A627A /* ActiveLabel in Frameworks */,
|
||||||
|
@ -1219,6 +1253,8 @@
|
||||||
2DF75BA025D0E29D00694EC8 /* StatusProvider+StatusTableViewCellDelegate.swift */,
|
2DF75BA025D0E29D00694EC8 /* StatusProvider+StatusTableViewCellDelegate.swift */,
|
||||||
DB59F0FD25EF5D96001F1DAB /* StatusProvider+UITableViewDelegate.swift */,
|
DB59F0FD25EF5D96001F1DAB /* StatusProvider+UITableViewDelegate.swift */,
|
||||||
DB71FD4525F8C6D200512AE1 /* StatusProvider+UITableViewDataSourcePrefetching.swift */,
|
DB71FD4525F8C6D200512AE1 /* StatusProvider+UITableViewDataSourcePrefetching.swift */,
|
||||||
|
DB1D843526579DB5000346B3 /* StatusProvider+TableViewControllerNavigateable.swift */,
|
||||||
|
DB1D842B26551A1C000346B3 /* StatusProvider+StatusTableViewKeyCommandNavigateable.swift */,
|
||||||
);
|
);
|
||||||
path = StatusProvider;
|
path = StatusProvider;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
|
@ -1319,6 +1355,9 @@
|
||||||
2D38F20725CD491300561493 /* DisposeBagCollectable.swift */,
|
2D38F20725CD491300561493 /* DisposeBagCollectable.swift */,
|
||||||
5DF1058425F88AE500D6C0D4 /* NeedsDependency+AVPlayerViewControllerDelegate.swift */,
|
5DF1058425F88AE500D6C0D4 /* NeedsDependency+AVPlayerViewControllerDelegate.swift */,
|
||||||
DBE3CE12261D7D4200430CC6 /* StatusTableViewControllerAspect.swift */,
|
DBE3CE12261D7D4200430CC6 /* StatusTableViewControllerAspect.swift */,
|
||||||
|
DB1D84372657B275000346B3 /* SegmentedControlNavigateable.swift */,
|
||||||
|
DB1D843326579931000346B3 /* TableViewControllerNavigateable.swift */,
|
||||||
|
DB1D842D26552C4D000346B3 /* StatusTableViewControllerNavigateable.swift */,
|
||||||
);
|
);
|
||||||
path = Protocol;
|
path = Protocol;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
|
@ -1362,6 +1401,7 @@
|
||||||
DB447680260B3ED600B66B82 /* CustomEmojiPickerSection.swift */,
|
DB447680260B3ED600B66B82 /* CustomEmojiPickerSection.swift */,
|
||||||
DB6D9F7C26358ED4008423CD /* SettingsSection.swift */,
|
DB6D9F7C26358ED4008423CD /* SettingsSection.swift */,
|
||||||
5BB04FF4262F0E6D0043BFF6 /* ReportSection.swift */,
|
5BB04FF4262F0E6D0043BFF6 /* ReportSection.swift */,
|
||||||
|
DBBF1DC82652538500E5B703 /* AutoCompleteSection.swift */,
|
||||||
);
|
);
|
||||||
path = Section;
|
path = Section;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
|
@ -1422,6 +1462,7 @@
|
||||||
DB66729B25F9F91F00D60309 /* ComposeStatusItem.swift */,
|
DB66729B25F9F91F00D60309 /* ComposeStatusItem.swift */,
|
||||||
DB44768A260B3F2100B66B82 /* CustomEmojiPickerItem.swift */,
|
DB44768A260B3F2100B66B82 /* CustomEmojiPickerItem.swift */,
|
||||||
DB6D9F8326358EEC008423CD /* SettingsItem.swift */,
|
DB6D9F8326358EEC008423CD /* SettingsItem.swift */,
|
||||||
|
DBBF1DCA2652539E00E5B703 /* AutoCompleteItem.swift */,
|
||||||
);
|
);
|
||||||
path = Item;
|
path = Item;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
|
@ -1754,6 +1795,7 @@
|
||||||
DB49A61325FF2C5600B98345 /* EmojiService.swift */,
|
DB49A61325FF2C5600B98345 /* EmojiService.swift */,
|
||||||
DB49A61E25FF32AA00B98345 /* EmojiService+CustomEmojiViewModel.swift */,
|
DB49A61E25FF32AA00B98345 /* EmojiService+CustomEmojiViewModel.swift */,
|
||||||
DB49A62425FF334C00B98345 /* EmojiService+CustomEmojiViewModel+LoadState.swift */,
|
DB49A62425FF334C00B98345 /* EmojiService+CustomEmojiViewModel+LoadState.swift */,
|
||||||
|
DB040ED026538E3C00BEE9D8 /* Trie.swift */,
|
||||||
);
|
);
|
||||||
path = EmojiService;
|
path = EmojiService;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
|
@ -1764,6 +1806,7 @@
|
||||||
DB5086BD25CC0D9900C2C187 /* SplashPreference.swift */,
|
DB5086BD25CC0D9900C2C187 /* SplashPreference.swift */,
|
||||||
DB6D1B3C2636857500ACB481 /* AppearancePreference.swift */,
|
DB6D1B3C2636857500ACB481 /* AppearancePreference.swift */,
|
||||||
DBE54AC52636C89F004E7C0B /* NotificationPreference.swift */,
|
DBE54AC52636C89F004E7C0B /* NotificationPreference.swift */,
|
||||||
|
DB1D842F26566512000346B3 /* KeyboardPreference.swift */,
|
||||||
);
|
);
|
||||||
path = Preference;
|
path = Preference;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
|
@ -1771,6 +1814,7 @@
|
||||||
DB55D32225FB4D320002F825 /* View */ = {
|
DB55D32225FB4D320002F825 /* View */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
|
DB040ECC26526EA600BEE9D8 /* ComposeCollectionView.swift */,
|
||||||
DBA0A11225FB3FC10079C110 /* ComposeToolbarView.swift */,
|
DBA0A11225FB3FC10079C110 /* ComposeToolbarView.swift */,
|
||||||
DB8190C52601FF0400020C08 /* AttachmentContainerView.swift */,
|
DB8190C52601FF0400020C08 /* AttachmentContainerView.swift */,
|
||||||
DB9A486B26032AC1008B817C /* AttachmentContainerView+EmptyStateView.swift */,
|
DB9A486B26032AC1008B817C /* AttachmentContainerView+EmptyStateView.swift */,
|
||||||
|
@ -1874,6 +1918,19 @@
|
||||||
path = MastodonSDK;
|
path = MastodonSDK;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
};
|
};
|
||||||
|
DB6F5E36264E78EA009108F4 /* AutoComplete */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
DBBF1DC02652402000E5B703 /* View */,
|
||||||
|
DBBF1DC326524D3100E5B703 /* Cell */,
|
||||||
|
DB6F5E34264E78E7009108F4 /* AutoCompleteViewController.swift */,
|
||||||
|
DBBF1DBE2652401B00E5B703 /* AutoCompleteViewModel.swift */,
|
||||||
|
DBBF1DC4265251C300E5B703 /* AutoCompleteViewModel+Diffable.swift */,
|
||||||
|
DBBF1DC6265251D400E5B703 /* AutoCompleteViewModel+State.swift */,
|
||||||
|
);
|
||||||
|
path = AutoComplete;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
DB72602125E36A2500235243 /* ServerRules */ = {
|
DB72602125E36A2500235243 /* ServerRules */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
|
@ -1894,6 +1951,7 @@
|
||||||
DB789A1025F9F29B0071ACA0 /* Compose */ = {
|
DB789A1025F9F29B0071ACA0 /* Compose */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
|
DB6F5E36264E78EA009108F4 /* AutoComplete */,
|
||||||
DB55D32225FB4D320002F825 /* View */,
|
DB55D32225FB4D320002F825 /* View */,
|
||||||
DB789A2125F9F76D0071ACA0 /* CollectionViewCell */,
|
DB789A2125F9F76D0071ACA0 /* CollectionViewCell */,
|
||||||
DB789A0A25F9F2950071ACA0 /* ComposeViewController.swift */,
|
DB789A0A25F9F2950071ACA0 /* ComposeViewController.swift */,
|
||||||
|
@ -2199,6 +2257,7 @@
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
2D42FF6A25C817D2004A627A /* MastodonStatusContent.swift */,
|
2D42FF6A25C817D2004A627A /* MastodonStatusContent.swift */,
|
||||||
|
DB6F5E2E264E5518009108F4 /* MastodonRegex.swift */,
|
||||||
DB35FC2E26130172006193C9 /* MastodonField.swift */,
|
DB35FC2E26130172006193C9 /* MastodonField.swift */,
|
||||||
DBAE3FA82617106E004B8251 /* MastodonMetricFormatter.swift */,
|
DBAE3FA82617106E004B8251 /* MastodonMetricFormatter.swift */,
|
||||||
);
|
);
|
||||||
|
@ -2302,6 +2361,22 @@
|
||||||
path = View;
|
path = View;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
};
|
};
|
||||||
|
DBBF1DC02652402000E5B703 /* View */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
DB6F5E37264E994A009108F4 /* AutoCompleteTopChevronView.swift */,
|
||||||
|
);
|
||||||
|
path = View;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
|
DBBF1DC326524D3100E5B703 /* Cell */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
DBBF1DC126524D2900E5B703 /* AutoCompleteTableViewCell.swift */,
|
||||||
|
);
|
||||||
|
path = Cell;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
DBCBED2226132E1D00B49291 /* FetchedResultsController */ = {
|
DBCBED2226132E1D00B49291 /* FetchedResultsController */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
|
@ -2400,7 +2475,7 @@
|
||||||
2D939AC725EE14620076FA61 /* CropViewController */,
|
2D939AC725EE14620076FA61 /* CropViewController */,
|
||||||
DB9A487D2603456B008B817C /* UITextView+Placeholder */,
|
DB9A487D2603456B008B817C /* UITextView+Placeholder */,
|
||||||
DBB525072611EAC0002F1F29 /* Tabman */,
|
DBB525072611EAC0002F1F29 /* Tabman */,
|
||||||
DB35B0B22643D821006AC73B /* TwitterTextEditor */,
|
DB6F5E31264E7410009108F4 /* TwitterTextEditor */,
|
||||||
);
|
);
|
||||||
productName = Mastodon;
|
productName = Mastodon;
|
||||||
productReference = DB427DD225BAA00100D1B89D /* Mastodon.app */;
|
productReference = DB427DD225BAA00100D1B89D /* Mastodon.app */;
|
||||||
|
@ -2589,7 +2664,7 @@
|
||||||
DBB525062611EAC0002F1F29 /* XCRemoteSwiftPackageReference "Tabman" */,
|
DBB525062611EAC0002F1F29 /* XCRemoteSwiftPackageReference "Tabman" */,
|
||||||
DBF8AE842632992700C9C23C /* XCRemoteSwiftPackageReference "Base85" */,
|
DBF8AE842632992700C9C23C /* XCRemoteSwiftPackageReference "Base85" */,
|
||||||
DB6804722637CC1200430867 /* XCRemoteSwiftPackageReference "KeychainAccess" */,
|
DB6804722637CC1200430867 /* XCRemoteSwiftPackageReference "KeychainAccess" */,
|
||||||
DB35B0B12643D821006AC73B /* XCRemoteSwiftPackageReference "TwitterTextEditor" */,
|
DB6F5E30264E7410009108F4 /* XCRemoteSwiftPackageReference "TwitterTextEditor" */,
|
||||||
);
|
);
|
||||||
productRefGroup = DB427DD325BAA00100D1B89D /* Products */;
|
productRefGroup = DB427DD325BAA00100D1B89D /* Products */;
|
||||||
projectDirPath = "";
|
projectDirPath = "";
|
||||||
|
@ -2841,11 +2916,13 @@
|
||||||
DBE3CDCF261C42ED00430CC6 /* TimelineHeaderView.swift in Sources */,
|
DBE3CDCF261C42ED00430CC6 /* TimelineHeaderView.swift in Sources */,
|
||||||
DBAE3F8E2616E0B1004B8251 /* APIService+Block.swift in Sources */,
|
DBAE3F8E2616E0B1004B8251 /* APIService+Block.swift in Sources */,
|
||||||
5DF1057F25F88A4100D6C0D4 /* TouchBlockingView.swift in Sources */,
|
5DF1057F25F88A4100D6C0D4 /* TouchBlockingView.swift in Sources */,
|
||||||
|
DB1D843426579931000346B3 /* TableViewControllerNavigateable.swift in Sources */,
|
||||||
0FAA0FDF25E0B57E0017CCDE /* WelcomeViewController.swift in Sources */,
|
0FAA0FDF25E0B57E0017CCDE /* WelcomeViewController.swift in Sources */,
|
||||||
2D206B8C25F6015000143C56 /* AudioPlaybackService.swift in Sources */,
|
2D206B8C25F6015000143C56 /* AudioPlaybackService.swift in Sources */,
|
||||||
2D59819B25E4A581000FB903 /* MastodonConfirmEmailViewController.swift in Sources */,
|
2D59819B25E4A581000FB903 /* MastodonConfirmEmailViewController.swift in Sources */,
|
||||||
DB45FB1D25CA9D23005A8AC7 /* APIService+HomeTimeline.swift in Sources */,
|
DB45FB1D25CA9D23005A8AC7 /* APIService+HomeTimeline.swift in Sources */,
|
||||||
DB49A61425FF2C5600B98345 /* EmojiService.swift in Sources */,
|
DB49A61425FF2C5600B98345 /* EmojiService.swift in Sources */,
|
||||||
|
DBBF1DC7265251D400E5B703 /* AutoCompleteViewModel+State.swift in Sources */,
|
||||||
DBCC3B9526157E6E0045B23D /* APIService+Relationship.swift in Sources */,
|
DBCC3B9526157E6E0045B23D /* APIService+Relationship.swift in Sources */,
|
||||||
2D7631B325C159F700929FB9 /* Item.swift in Sources */,
|
2D7631B325C159F700929FB9 /* Item.swift in Sources */,
|
||||||
5DF1054125F886D400D6C0D4 /* ViedeoPlaybackService.swift in Sources */,
|
5DF1054125F886D400D6C0D4 /* ViedeoPlaybackService.swift in Sources */,
|
||||||
|
@ -2996,8 +3073,10 @@
|
||||||
0FB3D2FE25E4CB6400AAD544 /* PickServerTitleCell.swift in Sources */,
|
0FB3D2FE25E4CB6400AAD544 /* PickServerTitleCell.swift in Sources */,
|
||||||
5DA732CC2629CEF500A92342 /* UIView+Remove.swift in Sources */,
|
5DA732CC2629CEF500A92342 /* UIView+Remove.swift in Sources */,
|
||||||
2D38F1DF25CD46A400561493 /* HomeTimelineViewController+Provider.swift in Sources */,
|
2D38F1DF25CD46A400561493 /* HomeTimelineViewController+Provider.swift in Sources */,
|
||||||
|
DB1D843026566512000346B3 /* KeyboardPreference.swift in Sources */,
|
||||||
2D206B9225F60EA700143C56 /* UIControl.swift in Sources */,
|
2D206B9225F60EA700143C56 /* UIControl.swift in Sources */,
|
||||||
2D9DB96B263A91D1007C1D71 /* APIService+DomainBlock.swift in Sources */,
|
2D9DB96B263A91D1007C1D71 /* APIService+DomainBlock.swift in Sources */,
|
||||||
|
DBBF1DC92652538500E5B703 /* AutoCompleteSection.swift in Sources */,
|
||||||
2D46975E25C2A54100CF4AA9 /* NSLayoutConstraint.swift in Sources */,
|
2D46975E25C2A54100CF4AA9 /* NSLayoutConstraint.swift in Sources */,
|
||||||
2D45E5BF25C9549700A6D639 /* PublicTimelineViewModel+State.swift in Sources */,
|
2D45E5BF25C9549700A6D639 /* PublicTimelineViewModel+State.swift in Sources */,
|
||||||
DB8AF55D25C138B7002E6C99 /* UIViewController.swift in Sources */,
|
DB8AF55D25C138B7002E6C99 /* UIViewController.swift in Sources */,
|
||||||
|
@ -3019,6 +3098,7 @@
|
||||||
DB44768B260B3F2100B66B82 /* CustomEmojiPickerItem.swift in Sources */,
|
DB44768B260B3F2100B66B82 /* CustomEmojiPickerItem.swift in Sources */,
|
||||||
2DF75BA125D0E29D00694EC8 /* StatusProvider+StatusTableViewCellDelegate.swift in Sources */,
|
2DF75BA125D0E29D00694EC8 /* StatusProvider+StatusTableViewCellDelegate.swift in Sources */,
|
||||||
5DF1056425F887CB00D6C0D4 /* AVPlayer.swift in Sources */,
|
5DF1056425F887CB00D6C0D4 /* AVPlayer.swift in Sources */,
|
||||||
|
DBBF1DCB2652539E00E5B703 /* AutoCompleteItem.swift in Sources */,
|
||||||
2DA6054725F716A2006356F9 /* PlaybackState.swift in Sources */,
|
2DA6054725F716A2006356F9 /* PlaybackState.swift in Sources */,
|
||||||
DB35FC1F2612F1D9006193C9 /* ProfileRelationshipActionButton.swift in Sources */,
|
DB35FC1F2612F1D9006193C9 /* ProfileRelationshipActionButton.swift in Sources */,
|
||||||
DBC7A672260C897100E57475 /* StatusContentWarningEditorView.swift in Sources */,
|
DBC7A672260C897100E57475 /* StatusContentWarningEditorView.swift in Sources */,
|
||||||
|
@ -3035,6 +3115,7 @@
|
||||||
2D084B8D26258EA3003AA3AF /* NotificationViewModel+diffable.swift in Sources */,
|
2D084B8D26258EA3003AA3AF /* NotificationViewModel+diffable.swift in Sources */,
|
||||||
DB6D1B24263684C600ACB481 /* UserDefaults.swift in Sources */,
|
DB6D1B24263684C600ACB481 /* UserDefaults.swift in Sources */,
|
||||||
DB1D186C25EF5BA7003F1F23 /* PollTableView.swift in Sources */,
|
DB1D186C25EF5BA7003F1F23 /* PollTableView.swift in Sources */,
|
||||||
|
DB6F5E2F264E5518009108F4 /* MastodonRegex.swift in Sources */,
|
||||||
2D5981A125E4A593000FB903 /* MastodonConfirmEmailViewModel.swift in Sources */,
|
2D5981A125E4A593000FB903 /* MastodonConfirmEmailViewModel.swift in Sources */,
|
||||||
DB87D4452609BE0500D12C0D /* ComposeStatusPollOptionCollectionViewCell.swift in Sources */,
|
DB87D4452609BE0500D12C0D /* ComposeStatusPollOptionCollectionViewCell.swift in Sources */,
|
||||||
DB6D9F7D26358ED4008423CD /* SettingsSection.swift in Sources */,
|
DB6D9F7D26358ED4008423CD /* SettingsSection.swift in Sources */,
|
||||||
|
@ -3045,6 +3126,7 @@
|
||||||
DB6C8C0F25F0A6AE00AAA452 /* Mastodon+Entity+Error.swift in Sources */,
|
DB6C8C0F25F0A6AE00AAA452 /* Mastodon+Entity+Error.swift in Sources */,
|
||||||
DB1E346825F518E20079D7DF /* CategoryPickerSection.swift in Sources */,
|
DB1E346825F518E20079D7DF /* CategoryPickerSection.swift in Sources */,
|
||||||
2D61254D262547C200299647 /* APIService+Notification.swift in Sources */,
|
2D61254D262547C200299647 /* APIService+Notification.swift in Sources */,
|
||||||
|
DB040ED126538E3D00BEE9D8 /* Trie.swift in Sources */,
|
||||||
DBB525642612C988002F1F29 /* MeProfileViewModel.swift in Sources */,
|
DBB525642612C988002F1F29 /* MeProfileViewModel.swift in Sources */,
|
||||||
5BB04FE9262EFC300043BFF6 /* ReportedStatusTableviewCell.swift in Sources */,
|
5BB04FE9262EFC300043BFF6 /* ReportedStatusTableviewCell.swift in Sources */,
|
||||||
DBAE3F822615DDA3004B8251 /* ProfileViewController+UserProvider.swift in Sources */,
|
DBAE3F822615DDA3004B8251 /* ProfileViewController+UserProvider.swift in Sources */,
|
||||||
|
@ -3057,6 +3139,7 @@
|
||||||
DB6180E626391B550018D199 /* MediaPreviewTransitionController.swift in Sources */,
|
DB6180E626391B550018D199 /* MediaPreviewTransitionController.swift in Sources */,
|
||||||
5BB04FDB262EA3070043BFF6 /* ReportHeaderView.swift in Sources */,
|
5BB04FDB262EA3070043BFF6 /* ReportHeaderView.swift in Sources */,
|
||||||
DB2F073525E8ECF000957B2D /* AuthenticationViewModel.swift in Sources */,
|
DB2F073525E8ECF000957B2D /* AuthenticationViewModel.swift in Sources */,
|
||||||
|
DBBF1DC5265251C300E5B703 /* AutoCompleteViewModel+Diffable.swift in Sources */,
|
||||||
DB68A04A25E9027700CFDF14 /* AdaptiveStatusBarStyleNavigationController.swift in Sources */,
|
DB68A04A25E9027700CFDF14 /* AdaptiveStatusBarStyleNavigationController.swift in Sources */,
|
||||||
0FB3D33825E6401400AAD544 /* PickServerCell.swift in Sources */,
|
0FB3D33825E6401400AAD544 /* PickServerCell.swift in Sources */,
|
||||||
DB6D9F8426358EEC008423CD /* SettingsItem.swift in Sources */,
|
DB6D9F8426358EEC008423CD /* SettingsItem.swift in Sources */,
|
||||||
|
@ -3068,6 +3151,7 @@
|
||||||
DB45FADD25CA6F6B005A8AC7 /* APIService+CoreData+MastodonUser.swift in Sources */,
|
DB45FADD25CA6F6B005A8AC7 /* APIService+CoreData+MastodonUser.swift in Sources */,
|
||||||
2D32EABA25CB9B0500C9ED86 /* UIView.swift in Sources */,
|
2D32EABA25CB9B0500C9ED86 /* UIView.swift in Sources */,
|
||||||
2D38F20825CD491300561493 /* DisposeBagCollectable.swift in Sources */,
|
2D38F20825CD491300561493 /* DisposeBagCollectable.swift in Sources */,
|
||||||
|
DB6F5E35264E78E7009108F4 /* AutoCompleteViewController.swift in Sources */,
|
||||||
0F20220D26134E3F000C64BF /* HashtagTimelineViewModel+LoadLatestState.swift in Sources */,
|
0F20220D26134E3F000C64BF /* HashtagTimelineViewModel+LoadLatestState.swift in Sources */,
|
||||||
DBCC3B8F26148F7B0045B23D /* CachedProfileViewModel.swift in Sources */,
|
DBCC3B8F26148F7B0045B23D /* CachedProfileViewModel.swift in Sources */,
|
||||||
DB49A63D25FF609300B98345 /* PlayerContainerView+MediaTypeIndicotorView.swift in Sources */,
|
DB49A63D25FF609300B98345 /* PlayerContainerView+MediaTypeIndicotorView.swift in Sources */,
|
||||||
|
@ -3116,6 +3200,7 @@
|
||||||
5B90C460262599800002E742 /* SettingsAppearanceTableViewCell.swift in Sources */,
|
5B90C460262599800002E742 /* SettingsAppearanceTableViewCell.swift in Sources */,
|
||||||
DB8AF54425C13647002E6C99 /* SceneCoordinator.swift in Sources */,
|
DB8AF54425C13647002E6C99 /* SceneCoordinator.swift in Sources */,
|
||||||
5DF1058525F88AE500D6C0D4 /* NeedsDependency+AVPlayerViewControllerDelegate.swift in Sources */,
|
5DF1058525F88AE500D6C0D4 /* NeedsDependency+AVPlayerViewControllerDelegate.swift in Sources */,
|
||||||
|
DB1D84382657B275000346B3 /* SegmentedControlNavigateable.swift in Sources */,
|
||||||
DB447697260B439000B66B82 /* CustomEmojiPickerHeaderCollectionReusableView.swift in Sources */,
|
DB447697260B439000B66B82 /* CustomEmojiPickerHeaderCollectionReusableView.swift in Sources */,
|
||||||
DB45FAF925CA80A2005A8AC7 /* APIService+CoreData+MastodonAuthentication.swift in Sources */,
|
DB45FAF925CA80A2005A8AC7 /* APIService+CoreData+MastodonAuthentication.swift in Sources */,
|
||||||
2D04F42525C255B9003F936F /* APIService+PublicTimeline.swift in Sources */,
|
2D04F42525C255B9003F936F /* APIService+PublicTimeline.swift in Sources */,
|
||||||
|
@ -3135,13 +3220,16 @@
|
||||||
DB8AF52E25C13561002E6C99 /* ViewStateStore.swift in Sources */,
|
DB8AF52E25C13561002E6C99 /* ViewStateStore.swift in Sources */,
|
||||||
2DA7D04A25CA52CB00804E11 /* TimelineBottomLoaderTableViewCell.swift in Sources */,
|
2DA7D04A25CA52CB00804E11 /* TimelineBottomLoaderTableViewCell.swift in Sources */,
|
||||||
2D76318325C14E8F00929FB9 /* PublicTimelineViewModel+Diffable.swift in Sources */,
|
2D76318325C14E8F00929FB9 /* PublicTimelineViewModel+Diffable.swift in Sources */,
|
||||||
|
DBBF1DBF2652401B00E5B703 /* AutoCompleteViewModel.swift in Sources */,
|
||||||
DB6180FA26391F2E0018D199 /* MediaPreviewViewModel.swift in Sources */,
|
DB6180FA26391F2E0018D199 /* MediaPreviewViewModel.swift in Sources */,
|
||||||
2D198655261C3C4300F0B013 /* SearchViewModel+LoadOldestState.swift in Sources */,
|
2D198655261C3C4300F0B013 /* SearchViewModel+LoadOldestState.swift in Sources */,
|
||||||
0F202227261411BB000C64BF /* HashtagTimelineViewController+Provider.swift in Sources */,
|
0F202227261411BB000C64BF /* HashtagTimelineViewController+Provider.swift in Sources */,
|
||||||
2D7631A825C1535600929FB9 /* StatusTableViewCell.swift in Sources */,
|
2D7631A825C1535600929FB9 /* StatusTableViewCell.swift in Sources */,
|
||||||
2D76316525C14BD100929FB9 /* PublicTimelineViewController.swift in Sources */,
|
2D76316525C14BD100929FB9 /* PublicTimelineViewController.swift in Sources */,
|
||||||
|
DB1D842E26552C4D000346B3 /* StatusTableViewControllerNavigateable.swift in Sources */,
|
||||||
DB938F1F2624382F00E5B6C1 /* ThreadViewModel+Diffable.swift in Sources */,
|
DB938F1F2624382F00E5B6C1 /* ThreadViewModel+Diffable.swift in Sources */,
|
||||||
2D69CFF425CA9E2200C3A1B2 /* LoadMoreConfigurableTableViewContainer.swift in Sources */,
|
2D69CFF425CA9E2200C3A1B2 /* LoadMoreConfigurableTableViewContainer.swift in Sources */,
|
||||||
|
DB040ECD26526EA600BEE9D8 /* ComposeCollectionView.swift in Sources */,
|
||||||
DB482A4B261340A7008AE74C /* APIService+UserTimeline.swift in Sources */,
|
DB482A4B261340A7008AE74C /* APIService+UserTimeline.swift in Sources */,
|
||||||
DB427DD825BAA00100D1B89D /* SceneDelegate.swift in Sources */,
|
DB427DD825BAA00100D1B89D /* SceneDelegate.swift in Sources */,
|
||||||
0F2021FB2613262F000C64BF /* HashtagTimelineViewController.swift in Sources */,
|
0F2021FB2613262F000C64BF /* HashtagTimelineViewController.swift in Sources */,
|
||||||
|
@ -3160,6 +3248,7 @@
|
||||||
DB49A62525FF334C00B98345 /* EmojiService+CustomEmojiViewModel+LoadState.swift in Sources */,
|
DB49A62525FF334C00B98345 /* EmojiService+CustomEmojiViewModel+LoadState.swift in Sources */,
|
||||||
DB4924E226312AB200E9DB22 /* NotificationService.swift in Sources */,
|
DB4924E226312AB200E9DB22 /* NotificationService.swift in Sources */,
|
||||||
DB6D9F6F2635807F008423CD /* Setting.swift in Sources */,
|
DB6D9F6F2635807F008423CD /* Setting.swift in Sources */,
|
||||||
|
DB6F5E38264E994A009108F4 /* AutoCompleteTopChevronView.swift in Sources */,
|
||||||
DBB525412611ED54002F1F29 /* ProfileHeaderViewController.swift in Sources */,
|
DBB525412611ED54002F1F29 /* ProfileHeaderViewController.swift in Sources */,
|
||||||
DB9D6BFF25E4F5940051B173 /* ProfileViewController.swift in Sources */,
|
DB9D6BFF25E4F5940051B173 /* ProfileViewController.swift in Sources */,
|
||||||
0FB3D30825E524C600AAD544 /* PickServerCategoriesCell.swift in Sources */,
|
0FB3D30825E524C600AAD544 /* PickServerCategoriesCell.swift in Sources */,
|
||||||
|
@ -3168,6 +3257,7 @@
|
||||||
DBB525362611ECEB002F1F29 /* UserTimelineViewController.swift in Sources */,
|
DBB525362611ECEB002F1F29 /* UserTimelineViewController.swift in Sources */,
|
||||||
DB6D1B3D2636857500ACB481 /* AppearancePreference.swift in Sources */,
|
DB6D1B3D2636857500ACB481 /* AppearancePreference.swift in Sources */,
|
||||||
DB938F3326243D6200E5B6C1 /* TimelineTopLoaderTableViewCell.swift in Sources */,
|
DB938F3326243D6200E5B6C1 /* TimelineTopLoaderTableViewCell.swift in Sources */,
|
||||||
|
DBBF1DC226524D2900E5B703 /* AutoCompleteTableViewCell.swift in Sources */,
|
||||||
2D38F1FE25CD481700561493 /* StatusProvider.swift in Sources */,
|
2D38F1FE25CD481700561493 /* StatusProvider.swift in Sources */,
|
||||||
5B24BBE2262DB19100A9381B /* APIService+Report.swift in Sources */,
|
5B24BBE2262DB19100A9381B /* APIService+Report.swift in Sources */,
|
||||||
5BB04FF5262F0E6D0043BFF6 /* ReportSection.swift in Sources */,
|
5BB04FF5262F0E6D0043BFF6 /* ReportSection.swift in Sources */,
|
||||||
|
@ -3175,7 +3265,9 @@
|
||||||
DB66729C25F9F91F00D60309 /* ComposeStatusItem.swift in Sources */,
|
DB66729C25F9F91F00D60309 /* ComposeStatusItem.swift in Sources */,
|
||||||
DB6180E326391A4C0018D199 /* ViewControllerAnimatedTransitioning.swift in Sources */,
|
DB6180E326391A4C0018D199 /* ViewControllerAnimatedTransitioning.swift in Sources */,
|
||||||
0FB3D31E25E534C700AAD544 /* PickServerCategoryCollectionViewCell.swift in Sources */,
|
0FB3D31E25E534C700AAD544 /* PickServerCategoryCollectionViewCell.swift in Sources */,
|
||||||
|
DB1D842C26551A1C000346B3 /* StatusProvider+StatusTableViewKeyCommandNavigateable.swift in Sources */,
|
||||||
5DF1057925F88A1D00D6C0D4 /* PlayerContainerView.swift in Sources */,
|
5DF1057925F88A1D00D6C0D4 /* PlayerContainerView.swift in Sources */,
|
||||||
|
DB1D843626579DB5000346B3 /* StatusProvider+TableViewControllerNavigateable.swift in Sources */,
|
||||||
DB6D9F4926353FD7008423CD /* Subscription.swift in Sources */,
|
DB6D9F4926353FD7008423CD /* Subscription.swift in Sources */,
|
||||||
DB45FB0F25CA87D0005A8AC7 /* AuthenticationService.swift in Sources */,
|
DB45FB0F25CA87D0005A8AC7 /* AuthenticationService.swift in Sources */,
|
||||||
DB6180ED26391C6C0018D199 /* TransitioningMath.swift in Sources */,
|
DB6180ED26391C6C0018D199 /* TransitioningMath.swift in Sources */,
|
||||||
|
@ -3494,7 +3586,7 @@
|
||||||
CLANG_ENABLE_MODULES = YES;
|
CLANG_ENABLE_MODULES = YES;
|
||||||
CODE_SIGN_ENTITLEMENTS = Mastodon/Mastodon.entitlements;
|
CODE_SIGN_ENTITLEMENTS = Mastodon/Mastodon.entitlements;
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 4;
|
CURRENT_PROJECT_VERSION = 5;
|
||||||
DEVELOPMENT_ASSET_PATHS = "Mastodon/Resources/Preview\\ Assets.xcassets";
|
DEVELOPMENT_ASSET_PATHS = "Mastodon/Resources/Preview\\ Assets.xcassets";
|
||||||
DEVELOPMENT_TEAM = 7LFDZ96332;
|
DEVELOPMENT_TEAM = 7LFDZ96332;
|
||||||
INFOPLIST_FILE = Mastodon/Info.plist;
|
INFOPLIST_FILE = Mastodon/Info.plist;
|
||||||
|
@ -3502,7 +3594,7 @@
|
||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"@executable_path/Frameworks",
|
"@executable_path/Frameworks",
|
||||||
);
|
);
|
||||||
MARKETING_VERSION = 0.4.0;
|
MARKETING_VERSION = 0.5.0;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = org.joinmastodon.Mastodon;
|
PRODUCT_BUNDLE_IDENTIFIER = org.joinmastodon.Mastodon;
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||||
|
@ -3521,7 +3613,7 @@
|
||||||
CLANG_ENABLE_MODULES = YES;
|
CLANG_ENABLE_MODULES = YES;
|
||||||
CODE_SIGN_ENTITLEMENTS = Mastodon/Mastodon.entitlements;
|
CODE_SIGN_ENTITLEMENTS = Mastodon/Mastodon.entitlements;
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 4;
|
CURRENT_PROJECT_VERSION = 5;
|
||||||
DEVELOPMENT_ASSET_PATHS = "Mastodon/Resources/Preview\\ Assets.xcassets";
|
DEVELOPMENT_ASSET_PATHS = "Mastodon/Resources/Preview\\ Assets.xcassets";
|
||||||
DEVELOPMENT_TEAM = 7LFDZ96332;
|
DEVELOPMENT_TEAM = 7LFDZ96332;
|
||||||
INFOPLIST_FILE = Mastodon/Info.plist;
|
INFOPLIST_FILE = Mastodon/Info.plist;
|
||||||
|
@ -3529,7 +3621,7 @@
|
||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"@executable_path/Frameworks",
|
"@executable_path/Frameworks",
|
||||||
);
|
);
|
||||||
MARKETING_VERSION = 0.4.0;
|
MARKETING_VERSION = 0.5.0;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = org.joinmastodon.Mastodon;
|
PRODUCT_BUNDLE_IDENTIFIER = org.joinmastodon.Mastodon;
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||||
|
@ -3784,6 +3876,7 @@
|
||||||
buildSettings = {
|
buildSettings = {
|
||||||
CODE_SIGN_ENTITLEMENTS = NotificationService/NotificationService.entitlements;
|
CODE_SIGN_ENTITLEMENTS = NotificationService/NotificationService.entitlements;
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
|
CURRENT_PROJECT_VERSION = 5;
|
||||||
DEVELOPMENT_TEAM = 7LFDZ96332;
|
DEVELOPMENT_TEAM = 7LFDZ96332;
|
||||||
INFOPLIST_FILE = NotificationService/Info.plist;
|
INFOPLIST_FILE = NotificationService/Info.plist;
|
||||||
LD_RUNPATH_SEARCH_PATHS = (
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
|
@ -3791,6 +3884,7 @@
|
||||||
"@executable_path/Frameworks",
|
"@executable_path/Frameworks",
|
||||||
"@executable_path/../../Frameworks",
|
"@executable_path/../../Frameworks",
|
||||||
);
|
);
|
||||||
|
MARKETING_VERSION = 0.5.0;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = org.joinmastodon.Mastodon.NotificationService;
|
PRODUCT_BUNDLE_IDENTIFIER = org.joinmastodon.Mastodon.NotificationService;
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
SKIP_INSTALL = YES;
|
SKIP_INSTALL = YES;
|
||||||
|
@ -3805,6 +3899,7 @@
|
||||||
buildSettings = {
|
buildSettings = {
|
||||||
CODE_SIGN_ENTITLEMENTS = NotificationService/NotificationService.entitlements;
|
CODE_SIGN_ENTITLEMENTS = NotificationService/NotificationService.entitlements;
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
|
CURRENT_PROJECT_VERSION = 5;
|
||||||
DEVELOPMENT_TEAM = 7LFDZ96332;
|
DEVELOPMENT_TEAM = 7LFDZ96332;
|
||||||
INFOPLIST_FILE = NotificationService/Info.plist;
|
INFOPLIST_FILE = NotificationService/Info.plist;
|
||||||
LD_RUNPATH_SEARCH_PATHS = (
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
|
@ -3812,6 +3907,7 @@
|
||||||
"@executable_path/Frameworks",
|
"@executable_path/Frameworks",
|
||||||
"@executable_path/../../Frameworks",
|
"@executable_path/../../Frameworks",
|
||||||
);
|
);
|
||||||
|
MARKETING_VERSION = 0.5.0;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = org.joinmastodon.Mastodon.NotificationService;
|
PRODUCT_BUNDLE_IDENTIFIER = org.joinmastodon.Mastodon.NotificationService;
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
SKIP_INSTALL = YES;
|
SKIP_INSTALL = YES;
|
||||||
|
@ -3938,14 +4034,6 @@
|
||||||
minimumVersion = 0.1.1;
|
minimumVersion = 0.1.1;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
DB35B0B12643D821006AC73B /* XCRemoteSwiftPackageReference "TwitterTextEditor" */ = {
|
|
||||||
isa = XCRemoteSwiftPackageReference;
|
|
||||||
repositoryURL = "https://github.com/twitter/TwitterTextEditor";
|
|
||||||
requirement = {
|
|
||||||
kind = upToNextMajorVersion;
|
|
||||||
minimumVersion = 1.1.0;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
DB3D0FF125BAA61700EAA174 /* XCRemoteSwiftPackageReference "AlamofireImage" */ = {
|
DB3D0FF125BAA61700EAA174 /* XCRemoteSwiftPackageReference "AlamofireImage" */ = {
|
||||||
isa = XCRemoteSwiftPackageReference;
|
isa = XCRemoteSwiftPackageReference;
|
||||||
repositoryURL = "https://github.com/Alamofire/AlamofireImage.git";
|
repositoryURL = "https://github.com/Alamofire/AlamofireImage.git";
|
||||||
|
@ -3970,6 +4058,14 @@
|
||||||
minimumVersion = 4.2.2;
|
minimumVersion = 4.2.2;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
DB6F5E30264E7410009108F4 /* XCRemoteSwiftPackageReference "TwitterTextEditor" */ = {
|
||||||
|
isa = XCRemoteSwiftPackageReference;
|
||||||
|
repositoryURL = "https://github.com/MainasuK/TwitterTextEditor.git";
|
||||||
|
requirement = {
|
||||||
|
branch = "feature/expose-layout";
|
||||||
|
kind = branch;
|
||||||
|
};
|
||||||
|
};
|
||||||
DB9A487C2603456B008B817C /* XCRemoteSwiftPackageReference "UITextView-Placeholder" */ = {
|
DB9A487C2603456B008B817C /* XCRemoteSwiftPackageReference "UITextView-Placeholder" */ = {
|
||||||
isa = XCRemoteSwiftPackageReference;
|
isa = XCRemoteSwiftPackageReference;
|
||||||
repositoryURL = "https://github.com/MainasuK/UITextView-Placeholder";
|
repositoryURL = "https://github.com/MainasuK/UITextView-Placeholder";
|
||||||
|
@ -4031,11 +4127,6 @@
|
||||||
package = DB0140BB25C40D7500F9F3CF /* XCRemoteSwiftPackageReference "CommonOSLog" */;
|
package = DB0140BB25C40D7500F9F3CF /* XCRemoteSwiftPackageReference "CommonOSLog" */;
|
||||||
productName = CommonOSLog;
|
productName = CommonOSLog;
|
||||||
};
|
};
|
||||||
DB35B0B22643D821006AC73B /* TwitterTextEditor */ = {
|
|
||||||
isa = XCSwiftPackageProductDependency;
|
|
||||||
package = DB35B0B12643D821006AC73B /* XCRemoteSwiftPackageReference "TwitterTextEditor" */;
|
|
||||||
productName = TwitterTextEditor;
|
|
||||||
};
|
|
||||||
DB3D0FF225BAA61700EAA174 /* AlamofireImage */ = {
|
DB3D0FF225BAA61700EAA174 /* AlamofireImage */ = {
|
||||||
isa = XCSwiftPackageProductDependency;
|
isa = XCSwiftPackageProductDependency;
|
||||||
package = DB3D0FF125BAA61700EAA174 /* XCRemoteSwiftPackageReference "AlamofireImage" */;
|
package = DB3D0FF125BAA61700EAA174 /* XCRemoteSwiftPackageReference "AlamofireImage" */;
|
||||||
|
@ -4056,6 +4147,11 @@
|
||||||
package = DB3D0FF125BAA61700EAA174 /* XCRemoteSwiftPackageReference "AlamofireImage" */;
|
package = DB3D0FF125BAA61700EAA174 /* XCRemoteSwiftPackageReference "AlamofireImage" */;
|
||||||
productName = AlamofireImage;
|
productName = AlamofireImage;
|
||||||
};
|
};
|
||||||
|
DB6F5E31264E7410009108F4 /* TwitterTextEditor */ = {
|
||||||
|
isa = XCSwiftPackageProductDependency;
|
||||||
|
package = DB6F5E30264E7410009108F4 /* XCRemoteSwiftPackageReference "TwitterTextEditor" */;
|
||||||
|
productName = TwitterTextEditor;
|
||||||
|
};
|
||||||
DB9A487D2603456B008B817C /* UITextView+Placeholder */ = {
|
DB9A487D2603456B008B817C /* UITextView+Placeholder */ = {
|
||||||
isa = XCSwiftPackageProductDependency;
|
isa = XCSwiftPackageProductDependency;
|
||||||
package = DB9A487C2603456B008B817C /* XCRemoteSwiftPackageReference "UITextView-Placeholder" */;
|
package = DB9A487C2603456B008B817C /* XCRemoteSwiftPackageReference "UITextView-Placeholder" */;
|
||||||
|
|
|
@ -12,7 +12,7 @@
|
||||||
<key>CoreDataStack.xcscheme_^#shared#^_</key>
|
<key>CoreDataStack.xcscheme_^#shared#^_</key>
|
||||||
<dict>
|
<dict>
|
||||||
<key>orderHint</key>
|
<key>orderHint</key>
|
||||||
<integer>14</integer>
|
<integer>15</integer>
|
||||||
</dict>
|
</dict>
|
||||||
<key>Mastodon - RTL.xcscheme_^#shared#^_</key>
|
<key>Mastodon - RTL.xcscheme_^#shared#^_</key>
|
||||||
<dict>
|
<dict>
|
||||||
|
@ -32,7 +32,7 @@
|
||||||
<key>NotificationService.xcscheme_^#shared#^_</key>
|
<key>NotificationService.xcscheme_^#shared#^_</key>
|
||||||
<dict>
|
<dict>
|
||||||
<key>orderHint</key>
|
<key>orderHint</key>
|
||||||
<integer>15</integer>
|
<integer>14</integer>
|
||||||
</dict>
|
</dict>
|
||||||
</dict>
|
</dict>
|
||||||
<key>SuppressBuildableAutocreation</key>
|
<key>SuppressBuildableAutocreation</key>
|
||||||
|
|
|
@ -69,7 +69,7 @@
|
||||||
"repositoryURL": "https://github.com/onevcat/Kingfisher.git",
|
"repositoryURL": "https://github.com/onevcat/Kingfisher.git",
|
||||||
"state": {
|
"state": {
|
||||||
"branch": null,
|
"branch": null,
|
||||||
"revision": "bbc4bc4def7eb05a7ba8e1219f80ee9be327334e",
|
"revision": "15d199e84677303a7004ed2c5ecaa1a90f3863f8",
|
||||||
"version": "6.2.1"
|
"version": "6.2.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -138,11 +138,11 @@
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"package": "TwitterTextEditor",
|
"package": "TwitterTextEditor",
|
||||||
"repositoryURL": "https://github.com/twitter/TwitterTextEditor",
|
"repositoryURL": "https://github.com/MainasuK/TwitterTextEditor.git",
|
||||||
"state": {
|
"state": {
|
||||||
"branch": null,
|
"branch": "feature/expose-layout",
|
||||||
"revision": "dfe0edc3bcb6703ee2fd0e627f95e726b63e732a",
|
"revision": "c208329b23dcb3c8c7192de34776440d625a26a4",
|
||||||
"version": "1.1.0"
|
"version": null
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
|
@ -0,0 +1,55 @@
|
||||||
|
//
|
||||||
|
// AutoCompleteItem.swift
|
||||||
|
// Mastodon
|
||||||
|
//
|
||||||
|
// Created by MainasuK Cirno on 2021-5-17.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
import MastodonSDK
|
||||||
|
|
||||||
|
enum AutoCompleteItem {
|
||||||
|
case hashtag(tag: Mastodon.Entity.Tag)
|
||||||
|
case hashtagV1(tag: String)
|
||||||
|
case account(account: Mastodon.Entity.Account)
|
||||||
|
case emoji(emoji: Mastodon.Entity.Emoji)
|
||||||
|
case bottomLoader
|
||||||
|
}
|
||||||
|
|
||||||
|
extension AutoCompleteItem: Equatable {
|
||||||
|
static func == (lhs: AutoCompleteItem, rhs: AutoCompleteItem) -> Bool {
|
||||||
|
switch (lhs, rhs) {
|
||||||
|
case (.hashtag(let tagLeft), hashtag(let tagRight)):
|
||||||
|
return tagLeft.name == tagRight.name
|
||||||
|
case (.hashtagV1(let tagLeft), hashtagV1(let tagRight)):
|
||||||
|
return tagLeft == tagRight
|
||||||
|
case (.account(let accountLeft), account(let accountRight)):
|
||||||
|
return accountLeft.id == accountRight.id
|
||||||
|
case (.emoji(let emojiLeft), .emoji(let emojiRight)):
|
||||||
|
return emojiLeft.shortcode == emojiRight.shortcode
|
||||||
|
case (.bottomLoader, .bottomLoader):
|
||||||
|
return true
|
||||||
|
default:
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension AutoCompleteItem: Hashable {
|
||||||
|
func hash(into hasher: inout Hasher) {
|
||||||
|
switch self {
|
||||||
|
case .hashtag(let tag):
|
||||||
|
hasher.combine(tag.name)
|
||||||
|
hasher.combine(tag.url)
|
||||||
|
case .hashtagV1(let tag):
|
||||||
|
hasher.combine(tag)
|
||||||
|
case .account(let account):
|
||||||
|
hasher.combine(account.id)
|
||||||
|
case .emoji(let emoji):
|
||||||
|
hasher.combine(emoji.shortcode)
|
||||||
|
hasher.combine(emoji.url)
|
||||||
|
case .bottomLoader:
|
||||||
|
hasher.combine(String(describing: AutoCompleteItem.bottomLoader.self))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -6,6 +6,7 @@
|
||||||
//
|
//
|
||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
|
import Combine
|
||||||
import MastodonSDK
|
import MastodonSDK
|
||||||
|
|
||||||
/// Note: update Equatable when change case
|
/// Note: update Equatable when change case
|
||||||
|
@ -19,11 +20,11 @@ enum PickServerItem {
|
||||||
|
|
||||||
extension PickServerItem {
|
extension PickServerItem {
|
||||||
final class ServerItemAttribute: Equatable, Hashable {
|
final class ServerItemAttribute: Equatable, Hashable {
|
||||||
var isLast: Bool
|
var isLast: CurrentValueSubject<Bool, Never>
|
||||||
var isExpand: Bool
|
var isExpand: Bool
|
||||||
|
|
||||||
init(isLast: Bool, isExpand: Bool) {
|
init(isLast: Bool, isExpand: Bool) {
|
||||||
self.isLast = isLast
|
self.isLast = CurrentValueSubject(isLast)
|
||||||
self.isExpand = isExpand
|
self.isExpand = isExpand
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,90 @@
|
||||||
|
//
|
||||||
|
// AutoCompleteSection.swift
|
||||||
|
// Mastodon
|
||||||
|
//
|
||||||
|
// Created by MainasuK Cirno on 2021-5-17.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
import MastodonSDK
|
||||||
|
|
||||||
|
enum AutoCompleteSection: Equatable, Hashable {
|
||||||
|
case main
|
||||||
|
}
|
||||||
|
|
||||||
|
extension AutoCompleteSection {
|
||||||
|
|
||||||
|
static func tableViewDiffableDataSource(
|
||||||
|
for tableView: UITableView
|
||||||
|
) -> UITableViewDiffableDataSource<AutoCompleteSection, AutoCompleteItem> {
|
||||||
|
UITableViewDiffableDataSource(tableView: tableView) { tableView, indexPath, item in
|
||||||
|
switch item {
|
||||||
|
case .hashtag(let hashtag):
|
||||||
|
let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: AutoCompleteTableViewCell.self), for: indexPath) as! AutoCompleteTableViewCell
|
||||||
|
configureHashtag(cell: cell, hashtag: hashtag)
|
||||||
|
return cell
|
||||||
|
case .hashtagV1(let hashtagName):
|
||||||
|
let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: AutoCompleteTableViewCell.self), for: indexPath) as! AutoCompleteTableViewCell
|
||||||
|
configureHashtag(cell: cell, hashtagName: hashtagName)
|
||||||
|
return cell
|
||||||
|
case .account(let account):
|
||||||
|
let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: AutoCompleteTableViewCell.self), for: indexPath) as! AutoCompleteTableViewCell
|
||||||
|
configureAccount(cell: cell, account: account)
|
||||||
|
return cell
|
||||||
|
case .emoji(let emoji):
|
||||||
|
let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: AutoCompleteTableViewCell.self), for: indexPath) as! AutoCompleteTableViewCell
|
||||||
|
configureEmoji(cell: cell, emoji: emoji)
|
||||||
|
return cell
|
||||||
|
case .bottomLoader:
|
||||||
|
let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: TimelineBottomLoaderTableViewCell.self), for: indexPath) as! TimelineBottomLoaderTableViewCell
|
||||||
|
cell.startAnimating()
|
||||||
|
return cell
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
extension AutoCompleteSection {
|
||||||
|
|
||||||
|
private static func configureHashtag(cell: AutoCompleteTableViewCell, hashtag: Mastodon.Entity.Tag) {
|
||||||
|
cell.titleLabel.text = "#" + hashtag.name
|
||||||
|
cell.subtitleLabel.text = {
|
||||||
|
let count = (hashtag.history ?? [])
|
||||||
|
.sorted(by: { $0.day > $1.day })
|
||||||
|
.prefix(2)
|
||||||
|
.compactMap { Int($0.accounts) }
|
||||||
|
.reduce(0, +)
|
||||||
|
if count > 1 {
|
||||||
|
return L10n.Scene.Compose.AutoComplete.multiplePeopleTalking(count)
|
||||||
|
} else {
|
||||||
|
return L10n.Scene.Compose.AutoComplete.singlePeopleTalking(count)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
cell.avatarImageView.isHidden = true
|
||||||
|
}
|
||||||
|
|
||||||
|
private static func configureHashtag(cell: AutoCompleteTableViewCell, hashtagName: String) {
|
||||||
|
cell.titleLabel.text = "#" + hashtagName
|
||||||
|
cell.subtitleLabel.text = " "
|
||||||
|
cell.avatarImageView.isHidden = true
|
||||||
|
}
|
||||||
|
|
||||||
|
private static func configureAccount(cell: AutoCompleteTableViewCell, account: Mastodon.Entity.Account) {
|
||||||
|
cell.titleLabel.text = {
|
||||||
|
guard !account.displayName.isEmpty else { return account.username }
|
||||||
|
return account.displayName
|
||||||
|
}()
|
||||||
|
cell.subtitleLabel.text = "@" + account.acct
|
||||||
|
cell.avatarImageView.isHidden = false
|
||||||
|
cell.configure(with: AvatarConfigurableViewConfiguration(avatarImageURL: URL(string: account.avatar)))
|
||||||
|
}
|
||||||
|
|
||||||
|
private static func configureEmoji(cell: AutoCompleteTableViewCell, emoji: Mastodon.Entity.Emoji) {
|
||||||
|
cell.titleLabel.text = ":" + emoji.shortcode + ":"
|
||||||
|
cell.subtitleLabel.text = " "
|
||||||
|
cell.avatarImageView.isHidden = false
|
||||||
|
cell.configure(with: AvatarConfigurableViewConfiguration(avatarImageURL: URL(string: emoji.url)))
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -37,6 +37,7 @@ extension ComposeStatusSection {
|
||||||
repliedToCellFrameSubscriber: CurrentValueSubject<CGRect, Never>,
|
repliedToCellFrameSubscriber: CurrentValueSubject<CGRect, Never>,
|
||||||
customEmojiPickerInputViewModel: CustomEmojiPickerInputViewModel,
|
customEmojiPickerInputViewModel: CustomEmojiPickerInputViewModel,
|
||||||
textEditorViewTextAttributesDelegate: TextEditorViewTextAttributesDelegate,
|
textEditorViewTextAttributesDelegate: TextEditorViewTextAttributesDelegate,
|
||||||
|
textEditorViewChangeObserver: TextEditorViewChangeObserver,
|
||||||
composeStatusAttachmentTableViewCellDelegate: ComposeStatusAttachmentCollectionViewCellDelegate,
|
composeStatusAttachmentTableViewCellDelegate: ComposeStatusAttachmentCollectionViewCellDelegate,
|
||||||
composeStatusPollOptionCollectionViewCellDelegate: ComposeStatusPollOptionCollectionViewCellDelegate,
|
composeStatusPollOptionCollectionViewCellDelegate: ComposeStatusPollOptionCollectionViewCellDelegate,
|
||||||
composeStatusNewPollOptionCollectionViewCellDelegate: ComposeStatusPollOptionAppendEntryCollectionViewCellDelegate,
|
composeStatusNewPollOptionCollectionViewCellDelegate: ComposeStatusPollOptionAppendEntryCollectionViewCellDelegate,
|
||||||
|
@ -45,6 +46,7 @@ extension ComposeStatusSection {
|
||||||
UICollectionViewDiffableDataSource(collectionView: collectionView) { [
|
UICollectionViewDiffableDataSource(collectionView: collectionView) { [
|
||||||
weak customEmojiPickerInputViewModel,
|
weak customEmojiPickerInputViewModel,
|
||||||
weak textEditorViewTextAttributesDelegate,
|
weak textEditorViewTextAttributesDelegate,
|
||||||
|
weak textEditorViewChangeObserver,
|
||||||
weak composeStatusAttachmentTableViewCellDelegate,
|
weak composeStatusAttachmentTableViewCellDelegate,
|
||||||
weak composeStatusPollOptionCollectionViewCellDelegate,
|
weak composeStatusPollOptionCollectionViewCellDelegate,
|
||||||
weak composeStatusNewPollOptionCollectionViewCellDelegate,
|
weak composeStatusNewPollOptionCollectionViewCellDelegate,
|
||||||
|
@ -92,6 +94,7 @@ extension ComposeStatusSection {
|
||||||
}
|
}
|
||||||
ComposeStatusSection.configureStatusContent(cell: cell, attribute: attribute)
|
ComposeStatusSection.configureStatusContent(cell: cell, attribute: attribute)
|
||||||
cell.textEditorView.textAttributesDelegate = textEditorViewTextAttributesDelegate
|
cell.textEditorView.textAttributesDelegate = textEditorViewTextAttributesDelegate
|
||||||
|
cell.textEditorViewChangeObserver = textEditorViewChangeObserver // relay
|
||||||
cell.composeContent
|
cell.composeContent
|
||||||
.removeDuplicates()
|
.removeDuplicates()
|
||||||
.receive(on: DispatchQueue.main)
|
.receive(on: DispatchQueue.main)
|
||||||
|
@ -259,7 +262,7 @@ extension ComposeStatusSection {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protocol CustomEmojiReplacableTextInput: AnyObject {
|
protocol CustomEmojiReplaceableTextInput: AnyObject {
|
||||||
var inputView: UIView? { get set }
|
var inputView: UIView? { get set }
|
||||||
func reloadInputViews()
|
func reloadInputViews()
|
||||||
|
|
||||||
|
@ -270,14 +273,14 @@ protocol CustomEmojiReplacableTextInput: AnyObject {
|
||||||
}
|
}
|
||||||
|
|
||||||
class CustomEmojiReplacableTextInputReference {
|
class CustomEmojiReplacableTextInputReference {
|
||||||
weak var value: CustomEmojiReplacableTextInput?
|
weak var value: CustomEmojiReplaceableTextInput?
|
||||||
|
|
||||||
init(value: CustomEmojiReplacableTextInput? = nil) {
|
init(value: CustomEmojiReplaceableTextInput? = nil) {
|
||||||
self.value = value
|
self.value = value
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
extension TextEditorView: CustomEmojiReplacableTextInput {
|
extension TextEditorView: CustomEmojiReplaceableTextInput {
|
||||||
func insertText(_ text: String) {
|
func insertText(_ text: String) {
|
||||||
try? updateByReplacing(range: selectedRange, with: text, selectedRange: nil)
|
try? updateByReplacing(range: selectedRange, with: text, selectedRange: nil)
|
||||||
}
|
}
|
||||||
|
@ -287,14 +290,14 @@ extension TextEditorView: CustomEmojiReplacableTextInput {
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
extension UITextField: CustomEmojiReplacableTextInput { }
|
extension UITextField: CustomEmojiReplaceableTextInput { }
|
||||||
extension UITextView: CustomEmojiReplacableTextInput { }
|
extension UITextView: CustomEmojiReplaceableTextInput { }
|
||||||
|
|
||||||
extension ComposeStatusSection {
|
extension ComposeStatusSection {
|
||||||
|
|
||||||
static func configureCustomEmojiPicker(
|
static func configureCustomEmojiPicker(
|
||||||
viewModel: CustomEmojiPickerInputViewModel?,
|
viewModel: CustomEmojiPickerInputViewModel?,
|
||||||
customEmojiReplacableTextInput: CustomEmojiReplacableTextInput,
|
customEmojiReplacableTextInput: CustomEmojiReplaceableTextInput,
|
||||||
disposeBag: inout Set<AnyCancellable>
|
disposeBag: inout Set<AnyCancellable>
|
||||||
) {
|
) {
|
||||||
guard let viewModel = viewModel else { return }
|
guard let viewModel = viewModel else { return }
|
||||||
|
|
|
@ -83,16 +83,24 @@ extension PickServerSection {
|
||||||
|
|
||||||
cell.updateExpandMode(mode: attribute.isExpand ? .expand : .collapse)
|
cell.updateExpandMode(mode: attribute.isExpand ? .expand : .collapse)
|
||||||
|
|
||||||
if attribute.isLast {
|
attribute.isLast
|
||||||
|
.receive(on: DispatchQueue.main)
|
||||||
|
.sink { [weak cell] isLast in
|
||||||
|
guard let cell = cell else { return }
|
||||||
|
if isLast {
|
||||||
cell.containerView.layer.maskedCorners = [
|
cell.containerView.layer.maskedCorners = [
|
||||||
.layerMinXMaxYCorner,
|
.layerMinXMaxYCorner,
|
||||||
.layerMaxXMaxYCorner
|
.layerMaxXMaxYCorner
|
||||||
]
|
]
|
||||||
cell.containerView.layer.cornerCurve = .continuous
|
cell.containerView.layer.cornerCurve = .continuous
|
||||||
cell.containerView.layer.cornerRadius = MastodonPickServerAppearance.tableViewCornerRadius
|
cell.containerView.layer.cornerRadius = MastodonPickServerAppearance.tableViewCornerRadius
|
||||||
|
cell.containerView.layer.masksToBounds = true
|
||||||
} else {
|
} else {
|
||||||
cell.containerView.layer.cornerRadius = 0
|
cell.containerView.layer.cornerRadius = 0
|
||||||
|
cell.containerView.layer.masksToBounds = false
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
.store(in: &cell.disposeBag)
|
||||||
|
|
||||||
cell.expandMode
|
cell.expandMode
|
||||||
.receive(on: DispatchQueue.main)
|
.receive(on: DispatchQueue.main)
|
||||||
|
@ -152,8 +160,10 @@ extension PickServerSection {
|
||||||
]
|
]
|
||||||
cell.containerView.layer.cornerCurve = .continuous
|
cell.containerView.layer.cornerCurve = .continuous
|
||||||
cell.containerView.layer.cornerRadius = MastodonPickServerAppearance.tableViewCornerRadius
|
cell.containerView.layer.cornerRadius = MastodonPickServerAppearance.tableViewCornerRadius
|
||||||
|
cell.containerView.layer.masksToBounds = true
|
||||||
} else {
|
} else {
|
||||||
cell.containerView.layer.cornerRadius = 0
|
cell.containerView.layer.cornerRadius = 0
|
||||||
|
cell.containerView.layer.masksToBounds = false
|
||||||
}
|
}
|
||||||
|
|
||||||
attribute.isNoResult ? cell.stopAnimating() : cell.startAnimating()
|
attribute.isNoResult ? cell.stopAnimating() : cell.startAnimating()
|
||||||
|
|
|
@ -152,41 +152,4 @@ extension ActiveLabel {
|
||||||
return elements
|
return elements
|
||||||
}
|
}
|
||||||
|
|
||||||
// public override func accessibilityElementCount() -> Int {
|
|
||||||
// return 1 + activeEntities.count
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// public override func accessibilityElement(at index: Int) -> Any? {
|
|
||||||
// if index == 0 {
|
|
||||||
// let element = ActiveLabelAccessibilityElement(accessibilityContainer: self)
|
|
||||||
// element.accessibilityTraits = .staticText
|
|
||||||
// element.accessibilityLabel = accessibilityLabel
|
|
||||||
// element.accessibilityFrame = superview!.convert(frame, to: nil)
|
|
||||||
// element.index = index
|
|
||||||
// return element
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// let index = index - 1
|
|
||||||
// guard index < activeEntities.count else { return nil }
|
|
||||||
// let eneity = activeEntities[index]
|
|
||||||
// guard let element = eneity.accessibilityElement(in: self) else { return nil }
|
|
||||||
//
|
|
||||||
// var glyphRange = NSRange()
|
|
||||||
// layoutManager.characterRange(forGlyphRange: eneity.range, actualGlyphRange: &glyphRange)
|
|
||||||
// let rect = layoutManager.boundingRect(forGlyphRange: glyphRange, in: textContainer)
|
|
||||||
// element.accessibilityFrame = self.convert(rect, to: nil)
|
|
||||||
// element.accessibilityContainer = self
|
|
||||||
//
|
|
||||||
// return element
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// public override func index(ofAccessibilityElement element: Any) -> Int {
|
|
||||||
// guard let element = element as? ActiveLabelAccessibilityElement,
|
|
||||||
// let index = element.index else {
|
|
||||||
// return NSNotFound
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// return index
|
|
||||||
// }
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,15 +7,6 @@
|
||||||
|
|
||||||
import UIKit
|
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 {
|
extension UITableView {
|
||||||
|
|
||||||
func deselectRow(with transitionCoordinator: UIViewControllerTransitionCoordinator?, animated: Bool) {
|
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")
|
internal static let findPeople = L10n.tr("Localizable", "Common.Controls.Actions.FindPeople")
|
||||||
/// Manually search instead
|
/// Manually search instead
|
||||||
internal static let manuallySearch = L10n.tr("Localizable", "Common.Controls.Actions.ManuallySearch")
|
internal static let manuallySearch = L10n.tr("Localizable", "Common.Controls.Actions.ManuallySearch")
|
||||||
|
/// Next
|
||||||
|
internal static let next = L10n.tr("Localizable", "Common.Controls.Actions.Next")
|
||||||
/// OK
|
/// OK
|
||||||
internal static let ok = L10n.tr("Localizable", "Common.Controls.Actions.Ok")
|
internal static let ok = L10n.tr("Localizable", "Common.Controls.Actions.Ok")
|
||||||
|
/// Open
|
||||||
|
internal static let `open` = L10n.tr("Localizable", "Common.Controls.Actions.Open")
|
||||||
/// Open in Safari
|
/// Open in Safari
|
||||||
internal static let openInSafari = L10n.tr("Localizable", "Common.Controls.Actions.OpenInSafari")
|
internal static let openInSafari = L10n.tr("Localizable", "Common.Controls.Actions.OpenInSafari")
|
||||||
/// Preview
|
/// Preview
|
||||||
internal static let preview = L10n.tr("Localizable", "Common.Controls.Actions.Preview")
|
internal static let preview = L10n.tr("Localizable", "Common.Controls.Actions.Preview")
|
||||||
|
/// Previous
|
||||||
|
internal static let previous = L10n.tr("Localizable", "Common.Controls.Actions.Previous")
|
||||||
/// Remove
|
/// Remove
|
||||||
internal static let remove = L10n.tr("Localizable", "Common.Controls.Actions.Remove")
|
internal static let remove = L10n.tr("Localizable", "Common.Controls.Actions.Remove")
|
||||||
|
/// Reply
|
||||||
|
internal static let reply = L10n.tr("Localizable", "Common.Controls.Actions.Reply")
|
||||||
/// Report %@
|
/// Report %@
|
||||||
internal static func reportUser(_ p1: Any) -> String {
|
internal static func reportUser(_ p1: Any) -> String {
|
||||||
return L10n.tr("Localizable", "Common.Controls.Actions.ReportUser", String(describing: p1))
|
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))
|
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 {
|
internal enum Status {
|
||||||
/// content warning
|
/// content warning
|
||||||
internal static let contentWarning = L10n.tr("Localizable", "Common.Controls.Status.ContentWarning")
|
internal static let contentWarning = L10n.tr("Localizable", "Common.Controls.Status.ContentWarning")
|
||||||
|
@ -376,10 +426,38 @@ internal enum L10n {
|
||||||
/// video
|
/// video
|
||||||
internal static let video = L10n.tr("Localizable", "Scene.Compose.Attachment.Video")
|
internal static let video = L10n.tr("Localizable", "Scene.Compose.Attachment.Video")
|
||||||
}
|
}
|
||||||
|
internal enum AutoComplete {
|
||||||
|
/// %ld people talking
|
||||||
|
internal static func multiplePeopleTalking(_ p1: Int) -> String {
|
||||||
|
return L10n.tr("Localizable", "Scene.Compose.AutoComplete.MultiplePeopleTalking", p1)
|
||||||
|
}
|
||||||
|
/// %ld people talking
|
||||||
|
internal static func singlePeopleTalking(_ p1: Int) -> String {
|
||||||
|
return L10n.tr("Localizable", "Scene.Compose.AutoComplete.SinglePeopleTalking", p1)
|
||||||
|
}
|
||||||
|
}
|
||||||
internal enum ContentWarning {
|
internal enum ContentWarning {
|
||||||
/// Write an accurate warning here...
|
/// Write an accurate warning here...
|
||||||
internal static let placeholder = L10n.tr("Localizable", "Scene.Compose.ContentWarning.Placeholder")
|
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 {
|
internal enum MediaSelection {
|
||||||
/// Browse
|
/// Browse
|
||||||
internal static let browse = L10n.tr("Localizable", "Scene.Compose.MediaSelection.Browse")
|
internal static let browse = L10n.tr("Localizable", "Scene.Compose.MediaSelection.Browse")
|
||||||
|
@ -498,6 +576,12 @@ internal enum L10n {
|
||||||
/// rebloged your post
|
/// rebloged your post
|
||||||
internal static let reblog = L10n.tr("Localizable", "Scene.Notification.Action.Reblog")
|
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 {
|
internal enum Title {
|
||||||
/// Everything
|
/// Everything
|
||||||
internal static let everything = L10n.tr("Localizable", "Scene.Notification.Title.Everything")
|
internal static let everything = L10n.tr("Localizable", "Scene.Notification.Title.Everything")
|
||||||
|
@ -505,6 +589,16 @@ internal enum L10n {
|
||||||
internal static let mentions = L10n.tr("Localizable", "Scene.Notification.Title.Mentions")
|
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 {
|
internal enum Profile {
|
||||||
/// %@ posts
|
/// %@ posts
|
||||||
internal static func subtitle(_ p1: Any) -> String {
|
internal static func subtitle(_ p1: Any) -> String {
|
||||||
|
@ -812,6 +906,10 @@ internal enum L10n {
|
||||||
internal enum Settings {
|
internal enum Settings {
|
||||||
/// Settings
|
/// Settings
|
||||||
internal static let title = L10n.tr("Localizable", "Scene.Settings.Title")
|
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 Section {
|
||||||
internal enum Appearance {
|
internal enum Appearance {
|
||||||
/// Automatic
|
/// Automatic
|
||||||
|
|
|
@ -0,0 +1,25 @@
|
||||||
|
//
|
||||||
|
// MastodonRegex.swift
|
||||||
|
// Mastodon
|
||||||
|
//
|
||||||
|
// Created by MainasuK Cirno on 2021-5-14.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
enum MastodonRegex {
|
||||||
|
/// mention, hashtag.
|
||||||
|
/// @...
|
||||||
|
/// #...
|
||||||
|
static let highlightPattern = "(?:@([a-zA-Z0-9_]+)(@[a-zA-Z0-9_.-]+)?|#([^\\s.]+))"
|
||||||
|
/// emoji
|
||||||
|
/// :shortcode:
|
||||||
|
/// accept ^\B: or \s: but not accept \B: to force user input a space to make emoji take effect
|
||||||
|
/// precondition :\B with following space
|
||||||
|
static let emojiPattern = "(?:(^\\B:|\\s:)([a-zA-Z0-9_]+)(:\\B(?=\\s)))"
|
||||||
|
/// mention, hashtag, emoji
|
||||||
|
/// @…
|
||||||
|
/// #…
|
||||||
|
/// :…
|
||||||
|
static let autoCompletePattern = "(?:@([a-zA-Z0-9_]+)(@[a-zA-Z0-9_.-]+)?|#([^\\s.]+))|(^\\B:|\\s:)([a-zA-Z0-9_]+)"
|
||||||
|
}
|
|
@ -2,8 +2,6 @@
|
||||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
<plist version="1.0">
|
<plist version="1.0">
|
||||||
<dict>
|
<dict>
|
||||||
<key>ITSAppUsesNonExemptEncryption</key>
|
|
||||||
<false/>
|
|
||||||
<key>CFBundleDevelopmentRegion</key>
|
<key>CFBundleDevelopmentRegion</key>
|
||||||
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
||||||
<key>CFBundleExecutable</key>
|
<key>CFBundleExecutable</key>
|
||||||
|
@ -20,6 +18,8 @@
|
||||||
<string>$(MARKETING_VERSION)</string>
|
<string>$(MARKETING_VERSION)</string>
|
||||||
<key>CFBundleVersion</key>
|
<key>CFBundleVersion</key>
|
||||||
<string>$(CURRENT_PROJECT_VERSION)</string>
|
<string>$(CURRENT_PROJECT_VERSION)</string>
|
||||||
|
<key>ITSAppUsesNonExemptEncryption</key>
|
||||||
|
<false/>
|
||||||
<key>LSApplicationQueriesSchemes</key>
|
<key>LSApplicationQueriesSchemes</key>
|
||||||
<array>
|
<array>
|
||||||
<string>sparrow</string>
|
<string>sparrow</string>
|
||||||
|
|
|
@ -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) {
|
static func coordinateToStatusAuthorProfileScene(for target: Target, provider: StatusProvider, cell: UITableViewCell) {
|
||||||
_coordinateToStatusAuthorProfileScene(
|
_coordinateToStatusAuthorProfileScene(
|
||||||
for: target,
|
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>) {
|
private static func _responseToStatusLikeAction(provider: StatusProvider, status: Future<Status?, Never>) {
|
||||||
// prepare authentication
|
// prepare authentication
|
||||||
guard let activeMastodonAuthenticationBox = provider.context.authenticationService.activeMastodonAuthenticationBox.value else {
|
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>) {
|
private static func _responseToStatusReblogAction(provider: StatusProvider, status: Future<Status?, Never>) {
|
||||||
// prepare authentication
|
// prepare authentication
|
||||||
guard let activeMastodonAuthenticationBox = provider.context.authenticationService.activeMastodonAuthenticationBox.value else {
|
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>) {
|
private static func _responseToStatusReplyAction(provider: StatusProvider, status: Future<Status?, Never>) {
|
||||||
status
|
status
|
||||||
.sink { [weak provider] status in
|
.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>) {
|
private static func _responseToStatusContentWarningRevealAction(dependency: NeedsDependency, status: Future<Status?, Never>) {
|
||||||
status
|
status
|
||||||
.compactMap { [weak dependency] status -> AnyPublisher<Status?, Never>? in
|
.compactMap { [weak dependency] status -> AnyPublisher<Status?, Never>? in
|
||||||
|
|
|
@ -7,6 +7,7 @@
|
||||||
|
|
||||||
import UIKit
|
import UIKit
|
||||||
import AVKit
|
import AVKit
|
||||||
|
import GameController
|
||||||
|
|
||||||
// Check List Last Updated
|
// Check List Last Updated
|
||||||
// - HomeViewController: 2021/4/30
|
// - HomeViewController: 2021/4/30
|
||||||
|
@ -34,6 +35,12 @@ protocol StatusTableViewControllerAspect: UIViewController {
|
||||||
extension StatusTableViewControllerAspect {
|
extension StatusTableViewControllerAspect {
|
||||||
/// [UI] hook to deselect row in the transitioning for the table view
|
/// [UI] hook to deselect row in the transitioning for the table view
|
||||||
func aspectViewWillAppear(_ animated: Bool) {
|
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)
|
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
|
/// [UI] hook to cache table view cell height
|
||||||
func aspectTableView(_ tableView: UITableView, didEndDisplaying cell: UITableViewCell, forRowAt indexPath: IndexPath) {
|
func aspectTableView(_ tableView: UITableView, didEndDisplaying cell: UITableViewCell, forRowAt indexPath: IndexPath) {
|
||||||
cacheTableView(tableView, didEndDisplaying: cell, forRowAt: indexPath)
|
cacheTableView(tableView, didEndDisplaying: cell, forRowAt: indexPath)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
extension StatusTableViewControllerAspect where Self: StatusTableViewCellDelegate & TableViewCellHeightCacheableContainer & StatusProvider {
|
extension StatusTableViewControllerAspect where Self: StatusProvider & StatusTableViewCellDelegate & TableViewCellHeightCacheableContainer {
|
||||||
/// [Media] hook to notify video service
|
/// [Media] hook to notify video service
|
||||||
/// [UI] hook to cache table view cell height
|
/// [UI] hook to cache table view cell height
|
||||||
func aspectTableView(_ tableView: UITableView, didEndDisplaying cell: UITableViewCell, forRowAt indexPath: IndexPath) {
|
func aspectTableView(_ tableView: UITableView, didEndDisplaying cell: UITableViewCell, forRowAt indexPath: IndexPath) {
|
||||||
|
|
|
@ -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
|
import UIKit
|
||||||
|
|
||||||
protocol TableViewCellHeightCacheableContainer: StatusProvider {
|
protocol TableViewCellHeightCacheableContainer {
|
||||||
var cellFrameCache: NSCache<NSNumber, NSValue> { get }
|
var cellFrameCache: NSCache<NSNumber, NSValue> { get }
|
||||||
|
func cacheTableView(_ tableView: UITableView, didEndDisplaying cell: UITableViewCell, forRowAt indexPath: IndexPath)
|
||||||
|
func handleTableView(_ tableView: UITableView, estimatedHeightForRowAt indexPath: IndexPath) -> CGFloat
|
||||||
}
|
}
|
||||||
|
|
||||||
extension TableViewCellHeightCacheableContainer {
|
extension TableViewCellHeightCacheableContainer where Self: StatusProvider {
|
||||||
|
|
||||||
func cacheTableView(_ tableView: UITableView, didEndDisplaying cell: UITableViewCell, forRowAt indexPath: IndexPath) {
|
func cacheTableView(_ tableView: UITableView, didEndDisplaying cell: UITableViewCell, forRowAt indexPath: IndexPath) {
|
||||||
guard let item = item(for: nil, indexPath: indexPath) else { return }
|
guard let item = item(for: nil, indexPath: indexPath) else { return }
|
||||||
|
|
|
@ -0,0 +1,79 @@
|
||||||
|
//
|
||||||
|
// TableViewControllerNavigateable.swift
|
||||||
|
// Mastodon
|
||||||
|
//
|
||||||
|
// Created by MainasuK Cirno on 2021-5-21.
|
||||||
|
//
|
||||||
|
|
||||||
|
import os.log
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
typealias TableViewControllerNavigateable = TableViewControllerNavigateableCore & TableViewControllerNavigateableRelay
|
||||||
|
|
||||||
|
protocol TableViewControllerNavigateableCore: AnyObject {
|
||||||
|
var tableView: UITableView { get }
|
||||||
|
var overrideNavigationScrollPosition: UITableView.ScrollPosition? { get set }
|
||||||
|
var navigationKeyCommands: [UIKeyCommand] { get }
|
||||||
|
|
||||||
|
func navigateKeyCommandHandler(_ sender: UIKeyCommand)
|
||||||
|
func navigate(direction: TableViewNavigationDirection)
|
||||||
|
func open()
|
||||||
|
func back()
|
||||||
|
}
|
||||||
|
|
||||||
|
extension TableViewControllerNavigateableCore {
|
||||||
|
var overrideNavigationScrollPosition: UITableView.ScrollPosition? {
|
||||||
|
get { return nil }
|
||||||
|
set { }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc protocol TableViewControllerNavigateableRelay: AnyObject {
|
||||||
|
func navigateKeyCommandHandlerRelay(_ sender: UIKeyCommand)
|
||||||
|
}
|
||||||
|
|
||||||
|
enum TableViewNavigationDirection {
|
||||||
|
case up
|
||||||
|
case down
|
||||||
|
}
|
||||||
|
|
||||||
|
enum TableViewNavigation: String, CaseIterable {
|
||||||
|
case up
|
||||||
|
case down
|
||||||
|
case back // pop
|
||||||
|
case open
|
||||||
|
|
||||||
|
var title: String {
|
||||||
|
switch self {
|
||||||
|
case .up: return L10n.Common.Controls.Actions.previous
|
||||||
|
case .down: return L10n.Common.Controls.Actions.next
|
||||||
|
case .back: return L10n.Common.Controls.Actions.back
|
||||||
|
case .open: return L10n.Common.Controls.Actions.open
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// UIKeyCommand input
|
||||||
|
var input: String {
|
||||||
|
switch self {
|
||||||
|
case .up: return "k"
|
||||||
|
case .down: return "j"
|
||||||
|
case .back: return "h"
|
||||||
|
case .open: return "l" // little "L"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var modifierFlags: UIKeyModifierFlags {
|
||||||
|
switch self {
|
||||||
|
case .up: return []
|
||||||
|
case .down: return []
|
||||||
|
case .back: return []
|
||||||
|
case .open: return []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var propertyList: Any {
|
||||||
|
return rawValue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
|
@ -30,10 +30,14 @@ Please check your internet connection.";
|
||||||
"Common.Controls.Actions.Edit" = "Edit";
|
"Common.Controls.Actions.Edit" = "Edit";
|
||||||
"Common.Controls.Actions.FindPeople" = "Find people to follow";
|
"Common.Controls.Actions.FindPeople" = "Find people to follow";
|
||||||
"Common.Controls.Actions.ManuallySearch" = "Manually search instead";
|
"Common.Controls.Actions.ManuallySearch" = "Manually search instead";
|
||||||
|
"Common.Controls.Actions.Next" = "Next";
|
||||||
"Common.Controls.Actions.Ok" = "OK";
|
"Common.Controls.Actions.Ok" = "OK";
|
||||||
|
"Common.Controls.Actions.Open" = "Open";
|
||||||
"Common.Controls.Actions.OpenInSafari" = "Open in Safari";
|
"Common.Controls.Actions.OpenInSafari" = "Open in Safari";
|
||||||
"Common.Controls.Actions.Preview" = "Preview";
|
"Common.Controls.Actions.Preview" = "Preview";
|
||||||
|
"Common.Controls.Actions.Previous" = "Previous";
|
||||||
"Common.Controls.Actions.Remove" = "Remove";
|
"Common.Controls.Actions.Remove" = "Remove";
|
||||||
|
"Common.Controls.Actions.Reply" = "Reply";
|
||||||
"Common.Controls.Actions.ReportUser" = "Report %@";
|
"Common.Controls.Actions.ReportUser" = "Report %@";
|
||||||
"Common.Controls.Actions.Save" = "Save";
|
"Common.Controls.Actions.Save" = "Save";
|
||||||
"Common.Controls.Actions.SavePhoto" = "Save photo";
|
"Common.Controls.Actions.SavePhoto" = "Save photo";
|
||||||
|
@ -64,6 +68,22 @@ Please check your internet connection.";
|
||||||
"Common.Controls.Firendship.UnblockUser" = "Unblock %@";
|
"Common.Controls.Firendship.UnblockUser" = "Unblock %@";
|
||||||
"Common.Controls.Firendship.Unmute" = "Unmute";
|
"Common.Controls.Firendship.Unmute" = "Unmute";
|
||||||
"Common.Controls.Firendship.UnmuteUser" = "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.Favorite" = "Favorite";
|
||||||
"Common.Controls.Status.Actions.Menu" = "Menu";
|
"Common.Controls.Status.Actions.Menu" = "Menu";
|
||||||
"Common.Controls.Status.Actions.Reblog" = "Reblog";
|
"Common.Controls.Status.Actions.Reblog" = "Reblog";
|
||||||
|
@ -125,9 +145,17 @@ uploaded to Mastodon.";
|
||||||
"Scene.Compose.Attachment.DescriptionVideo" = "Describe what’s happening for low vision people...";
|
"Scene.Compose.Attachment.DescriptionVideo" = "Describe what’s happening for low vision people...";
|
||||||
"Scene.Compose.Attachment.Photo" = "photo";
|
"Scene.Compose.Attachment.Photo" = "photo";
|
||||||
"Scene.Compose.Attachment.Video" = "video";
|
"Scene.Compose.Attachment.Video" = "video";
|
||||||
|
"Scene.Compose.AutoComplete.MultiplePeopleTalking" = "%ld people talking";
|
||||||
|
"Scene.Compose.AutoComplete.SinglePeopleTalking" = "%ld people talking";
|
||||||
"Scene.Compose.ComposeAction" = "Publish";
|
"Scene.Compose.ComposeAction" = "Publish";
|
||||||
"Scene.Compose.ContentInputPlaceholder" = "Type or paste what's on your mind";
|
"Scene.Compose.ContentInputPlaceholder" = "Type or paste what's on your mind";
|
||||||
"Scene.Compose.ContentWarning.Placeholder" = "Write an accurate warning here...";
|
"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.Browse" = "Browse";
|
||||||
"Scene.Compose.MediaSelection.Camera" = "Take Photo";
|
"Scene.Compose.MediaSelection.Camera" = "Take Photo";
|
||||||
"Scene.Compose.MediaSelection.PhotoLibrary" = "Photo Library";
|
"Scene.Compose.MediaSelection.PhotoLibrary" = "Photo Library";
|
||||||
|
@ -171,8 +199,13 @@ tap the link to confirm your account.";
|
||||||
"Scene.Notification.Action.Mention" = "mentioned you";
|
"Scene.Notification.Action.Mention" = "mentioned you";
|
||||||
"Scene.Notification.Action.Poll" = "Your poll has ended";
|
"Scene.Notification.Action.Poll" = "Your poll has ended";
|
||||||
"Scene.Notification.Action.Reblog" = "rebloged your post";
|
"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.Everything" = "Everything";
|
||||||
"Scene.Notification.Title.Mentions" = "Mentions";
|
"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.CountFollowers" = "%ld followers";
|
||||||
"Scene.Profile.Dashboard.Accessibility.CountFollowing" = "%ld following";
|
"Scene.Profile.Dashboard.Accessibility.CountFollowing" = "%ld following";
|
||||||
"Scene.Profile.Dashboard.Accessibility.CountPosts" = "%ld posts";
|
"Scene.Profile.Dashboard.Accessibility.CountPosts" = "%ld posts";
|
||||||
|
@ -270,6 +303,7 @@ any server.";
|
||||||
"Scene.ServerRules.Subtitle" = "These rules are set by the admins of %@.";
|
"Scene.ServerRules.Subtitle" = "These rules are set by the admins of %@.";
|
||||||
"Scene.ServerRules.TermsOfService" = "terms of service";
|
"Scene.ServerRules.TermsOfService" = "terms of service";
|
||||||
"Scene.ServerRules.Title" = "Some ground rules.";
|
"Scene.ServerRules.Title" = "Some ground rules.";
|
||||||
|
"Scene.Settings.Keyboard.CloseSettingsWindow" = "Close Settings Window";
|
||||||
"Scene.Settings.Section.Appearance.Automatic" = "Automatic";
|
"Scene.Settings.Section.Appearance.Automatic" = "Automatic";
|
||||||
"Scene.Settings.Section.Appearance.Dark" = "Always Dark";
|
"Scene.Settings.Section.Appearance.Dark" = "Always Dark";
|
||||||
"Scene.Settings.Section.Appearance.Light" = "Always Light";
|
"Scene.Settings.Section.Appearance.Light" = "Always Light";
|
||||||
|
|
|
@ -30,10 +30,14 @@ Please check your internet connection.";
|
||||||
"Common.Controls.Actions.Edit" = "Edit";
|
"Common.Controls.Actions.Edit" = "Edit";
|
||||||
"Common.Controls.Actions.FindPeople" = "Find people to follow";
|
"Common.Controls.Actions.FindPeople" = "Find people to follow";
|
||||||
"Common.Controls.Actions.ManuallySearch" = "Manually search instead";
|
"Common.Controls.Actions.ManuallySearch" = "Manually search instead";
|
||||||
|
"Common.Controls.Actions.Next" = "Next";
|
||||||
"Common.Controls.Actions.Ok" = "OK";
|
"Common.Controls.Actions.Ok" = "OK";
|
||||||
|
"Common.Controls.Actions.Open" = "Open";
|
||||||
"Common.Controls.Actions.OpenInSafari" = "Open in Safari";
|
"Common.Controls.Actions.OpenInSafari" = "Open in Safari";
|
||||||
"Common.Controls.Actions.Preview" = "Preview";
|
"Common.Controls.Actions.Preview" = "Preview";
|
||||||
|
"Common.Controls.Actions.Previous" = "Previous";
|
||||||
"Common.Controls.Actions.Remove" = "Remove";
|
"Common.Controls.Actions.Remove" = "Remove";
|
||||||
|
"Common.Controls.Actions.Reply" = "Reply";
|
||||||
"Common.Controls.Actions.ReportUser" = "Report %@";
|
"Common.Controls.Actions.ReportUser" = "Report %@";
|
||||||
"Common.Controls.Actions.Save" = "Save";
|
"Common.Controls.Actions.Save" = "Save";
|
||||||
"Common.Controls.Actions.SavePhoto" = "Save photo";
|
"Common.Controls.Actions.SavePhoto" = "Save photo";
|
||||||
|
@ -64,6 +68,22 @@ Please check your internet connection.";
|
||||||
"Common.Controls.Firendship.UnblockUser" = "Unblock %@";
|
"Common.Controls.Firendship.UnblockUser" = "Unblock %@";
|
||||||
"Common.Controls.Firendship.Unmute" = "Unmute";
|
"Common.Controls.Firendship.Unmute" = "Unmute";
|
||||||
"Common.Controls.Firendship.UnmuteUser" = "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.Favorite" = "Favorite";
|
||||||
"Common.Controls.Status.Actions.Menu" = "Menu";
|
"Common.Controls.Status.Actions.Menu" = "Menu";
|
||||||
"Common.Controls.Status.Actions.Reblog" = "Reblog";
|
"Common.Controls.Status.Actions.Reblog" = "Reblog";
|
||||||
|
@ -125,9 +145,17 @@ uploaded to Mastodon.";
|
||||||
"Scene.Compose.Attachment.DescriptionVideo" = "Describe what’s happening for low vision people...";
|
"Scene.Compose.Attachment.DescriptionVideo" = "Describe what’s happening for low vision people...";
|
||||||
"Scene.Compose.Attachment.Photo" = "photo";
|
"Scene.Compose.Attachment.Photo" = "photo";
|
||||||
"Scene.Compose.Attachment.Video" = "video";
|
"Scene.Compose.Attachment.Video" = "video";
|
||||||
|
"Scene.Compose.AutoComplete.MultiplePeopleTalking" = "%ld people talking";
|
||||||
|
"Scene.Compose.AutoComplete.SinglePeopleTalking" = "%ld people talking";
|
||||||
"Scene.Compose.ComposeAction" = "Publish";
|
"Scene.Compose.ComposeAction" = "Publish";
|
||||||
"Scene.Compose.ContentInputPlaceholder" = "Type or paste what's on your mind";
|
"Scene.Compose.ContentInputPlaceholder" = "Type or paste what's on your mind";
|
||||||
"Scene.Compose.ContentWarning.Placeholder" = "Write an accurate warning here...";
|
"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.Browse" = "Browse";
|
||||||
"Scene.Compose.MediaSelection.Camera" = "Take Photo";
|
"Scene.Compose.MediaSelection.Camera" = "Take Photo";
|
||||||
"Scene.Compose.MediaSelection.PhotoLibrary" = "Photo Library";
|
"Scene.Compose.MediaSelection.PhotoLibrary" = "Photo Library";
|
||||||
|
@ -171,8 +199,13 @@ tap the link to confirm your account.";
|
||||||
"Scene.Notification.Action.Mention" = "mentioned you";
|
"Scene.Notification.Action.Mention" = "mentioned you";
|
||||||
"Scene.Notification.Action.Poll" = "Your poll has ended";
|
"Scene.Notification.Action.Poll" = "Your poll has ended";
|
||||||
"Scene.Notification.Action.Reblog" = "rebloged your post";
|
"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.Everything" = "Everything";
|
||||||
"Scene.Notification.Title.Mentions" = "Mentions";
|
"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.CountFollowers" = "%ld followers";
|
||||||
"Scene.Profile.Dashboard.Accessibility.CountFollowing" = "%ld following";
|
"Scene.Profile.Dashboard.Accessibility.CountFollowing" = "%ld following";
|
||||||
"Scene.Profile.Dashboard.Accessibility.CountPosts" = "%ld posts";
|
"Scene.Profile.Dashboard.Accessibility.CountPosts" = "%ld posts";
|
||||||
|
@ -270,6 +303,7 @@ any server.";
|
||||||
"Scene.ServerRules.Subtitle" = "These rules are set by the admins of %@.";
|
"Scene.ServerRules.Subtitle" = "These rules are set by the admins of %@.";
|
||||||
"Scene.ServerRules.TermsOfService" = "terms of service";
|
"Scene.ServerRules.TermsOfService" = "terms of service";
|
||||||
"Scene.ServerRules.Title" = "Some ground rules.";
|
"Scene.ServerRules.Title" = "Some ground rules.";
|
||||||
|
"Scene.Settings.Keyboard.CloseSettingsWindow" = "Close Settings Window";
|
||||||
"Scene.Settings.Section.Appearance.Automatic" = "Automatic";
|
"Scene.Settings.Section.Appearance.Automatic" = "Automatic";
|
||||||
"Scene.Settings.Section.Appearance.Dark" = "Always Dark";
|
"Scene.Settings.Section.Appearance.Dark" = "Always Dark";
|
||||||
"Scene.Settings.Section.Appearance.Light" = "Always Light";
|
"Scene.Settings.Section.Appearance.Light" = "Always Light";
|
||||||
|
|
|
@ -0,0 +1,117 @@
|
||||||
|
//
|
||||||
|
// AutoCompleteViewController.swift
|
||||||
|
// Mastodon
|
||||||
|
//
|
||||||
|
// Created by MainasuK Cirno on 2021-5-14.
|
||||||
|
//
|
||||||
|
|
||||||
|
import os.log
|
||||||
|
import UIKit
|
||||||
|
import Combine
|
||||||
|
|
||||||
|
protocol AutoCompleteViewControllerDelegate: AnyObject {
|
||||||
|
func autoCompleteViewController(_ viewController: AutoCompleteViewController, didSelectItem item: AutoCompleteItem)
|
||||||
|
}
|
||||||
|
|
||||||
|
final class AutoCompleteViewController: UIViewController {
|
||||||
|
|
||||||
|
static let chevronViewHeight: CGFloat = 24
|
||||||
|
|
||||||
|
var viewModel: AutoCompleteViewModel!
|
||||||
|
var disposeBag = Set<AnyCancellable>()
|
||||||
|
|
||||||
|
weak var delegate: AutoCompleteViewControllerDelegate?
|
||||||
|
|
||||||
|
let chevronView = AutoCompleteTopChevronView()
|
||||||
|
let containerBackgroundView: UIView = {
|
||||||
|
let view = UIView()
|
||||||
|
view.backgroundColor = Asset.Colors.Background.systemBackground.color
|
||||||
|
return view
|
||||||
|
}()
|
||||||
|
|
||||||
|
let tableView: UITableView = {
|
||||||
|
let tableView = ControlContainableTableView()
|
||||||
|
tableView.register(AutoCompleteTableViewCell.self, forCellReuseIdentifier: String(describing: AutoCompleteTableViewCell.self))
|
||||||
|
tableView.register(TimelineBottomLoaderTableViewCell.self, forCellReuseIdentifier: String(describing: TimelineBottomLoaderTableViewCell.self))
|
||||||
|
tableView.rowHeight = UITableView.automaticDimension
|
||||||
|
tableView.separatorStyle = .none
|
||||||
|
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
|
||||||
|
}()
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
extension AutoCompleteViewController {
|
||||||
|
|
||||||
|
override func viewDidLoad() {
|
||||||
|
super.viewDidLoad()
|
||||||
|
|
||||||
|
view.backgroundColor = .clear
|
||||||
|
|
||||||
|
chevronView.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
view.addSubview(chevronView)
|
||||||
|
NSLayoutConstraint.activate([
|
||||||
|
chevronView.topAnchor.constraint(equalTo: view.topAnchor),
|
||||||
|
chevronView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
|
||||||
|
chevronView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
|
||||||
|
chevronView.heightAnchor.constraint(equalToConstant: AutoCompleteViewController.chevronViewHeight)
|
||||||
|
])
|
||||||
|
|
||||||
|
containerBackgroundView.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
view.addSubview(containerBackgroundView)
|
||||||
|
NSLayoutConstraint.activate([
|
||||||
|
containerBackgroundView.topAnchor.constraint(equalTo: chevronView.topAnchor),
|
||||||
|
containerBackgroundView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
|
||||||
|
containerBackgroundView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
|
||||||
|
containerBackgroundView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
|
||||||
|
])
|
||||||
|
view.bringSubviewToFront(chevronView)
|
||||||
|
containerBackgroundView.preservesSuperviewLayoutMargins = true
|
||||||
|
containerBackgroundView.isUserInteractionEnabled = true
|
||||||
|
|
||||||
|
tableView.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
containerBackgroundView.addSubview(tableView)
|
||||||
|
NSLayoutConstraint.activate([
|
||||||
|
tableView.topAnchor.constraint(equalTo: containerBackgroundView.topAnchor),
|
||||||
|
tableView.leadingAnchor.constraint(equalTo: containerBackgroundView.leadingAnchor),
|
||||||
|
tableView.trailingAnchor.constraint(equalTo: containerBackgroundView.trailingAnchor),
|
||||||
|
tableView.bottomAnchor.constraint(equalTo: containerBackgroundView.bottomAnchor),
|
||||||
|
])
|
||||||
|
|
||||||
|
tableView.delegate = self
|
||||||
|
viewModel.setupDiffableDataSource(for: tableView)
|
||||||
|
|
||||||
|
// bind to layout chevron
|
||||||
|
viewModel.symbolBoundingRect
|
||||||
|
.receive(on: DispatchQueue.main)
|
||||||
|
.sink { [weak self] symbolBoundingRect in
|
||||||
|
guard let self = self else { return }
|
||||||
|
self.chevronView.chevronMinX = symbolBoundingRect.midX - 0.5 * AutoCompleteTopChevronView.chevronSize.width
|
||||||
|
self.chevronView.setNeedsLayout()
|
||||||
|
self.containerBackgroundView.layer.mask = self.chevronView.invertMask(in: self.view.bounds)
|
||||||
|
}
|
||||||
|
.store(in: &disposeBag)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - UITableViewDelegate
|
||||||
|
extension AutoCompleteViewController: UITableViewDelegate {
|
||||||
|
|
||||||
|
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
|
||||||
|
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: indexPath: %s", ((#file as NSString).lastPathComponent), #line, #function, indexPath.debugDescription)
|
||||||
|
tableView.deselectRow(at: indexPath, animated: true)
|
||||||
|
|
||||||
|
guard let diffableDataSource = viewModel.diffableDataSource else { return }
|
||||||
|
guard let item = diffableDataSource.itemIdentifier(for: indexPath) else { return }
|
||||||
|
delegate?.autoCompleteViewController(self, didSelectItem: item)
|
||||||
|
}
|
||||||
|
|
||||||
|
func tableView(_ tableView: UITableView, didHighlightRowAt indexPath: IndexPath) {
|
||||||
|
tableView.deselectRow(at: indexPath, animated: true)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,22 @@
|
||||||
|
//
|
||||||
|
// AutoCompleteViewModel+Diffable.swift
|
||||||
|
// Mastodon
|
||||||
|
//
|
||||||
|
// Created by MainasuK Cirno on 2021-5-17.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
extension AutoCompleteViewModel {
|
||||||
|
|
||||||
|
func setupDiffableDataSource(
|
||||||
|
for tableView: UITableView
|
||||||
|
) {
|
||||||
|
diffableDataSource = AutoCompleteSection.tableViewDiffableDataSource(for: tableView)
|
||||||
|
|
||||||
|
var snapshot = NSDiffableDataSourceSnapshot<AutoCompleteSection, AutoCompleteItem>()
|
||||||
|
snapshot.appendSections([.main])
|
||||||
|
diffableDataSource?.apply(snapshot)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,206 @@
|
||||||
|
//
|
||||||
|
// AutoCompleteViewModel+State.swift
|
||||||
|
// Mastodon
|
||||||
|
//
|
||||||
|
// Created by MainasuK Cirno on 2021-5-17.
|
||||||
|
//
|
||||||
|
|
||||||
|
import os.log
|
||||||
|
import Foundation
|
||||||
|
import GameplayKit
|
||||||
|
import MastodonSDK
|
||||||
|
|
||||||
|
extension AutoCompleteViewModel {
|
||||||
|
class State: GKState {
|
||||||
|
weak var viewModel: AutoCompleteViewModel?
|
||||||
|
|
||||||
|
init(viewModel: AutoCompleteViewModel) {
|
||||||
|
self.viewModel = viewModel
|
||||||
|
}
|
||||||
|
|
||||||
|
override func didEnter(from previousState: GKState?) {
|
||||||
|
os_log("%{public}s[%{public}ld], %{public}s: enter %s, previous: %s", ((#file as NSString).lastPathComponent), #line, #function, self.debugDescription, previousState.debugDescription)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension AutoCompleteViewModel.State {
|
||||||
|
class Initial: AutoCompleteViewModel.State {
|
||||||
|
override func isValidNextState(_ stateClass: AnyClass) -> Bool {
|
||||||
|
guard let viewModel = viewModel else { return false }
|
||||||
|
switch stateClass {
|
||||||
|
case is Loading.Type:
|
||||||
|
return !viewModel.inputText.value.isEmpty
|
||||||
|
default:
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class Loading: AutoCompleteViewModel.State {
|
||||||
|
|
||||||
|
var previoursSearchText = ""
|
||||||
|
|
||||||
|
override func isValidNextState(_ stateClass: AnyClass) -> Bool {
|
||||||
|
guard let viewModel = viewModel else { return false }
|
||||||
|
switch stateClass {
|
||||||
|
case is Loading.Type:
|
||||||
|
return previoursSearchText != viewModel.inputText.value
|
||||||
|
case is Fail.Type:
|
||||||
|
return true
|
||||||
|
case is Idle.Type:
|
||||||
|
return true
|
||||||
|
default:
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override func didEnter(from previousState: GKState?) {
|
||||||
|
super.didEnter(from: previousState)
|
||||||
|
guard let viewModel = viewModel, let stateMachine = stateMachine else { return }
|
||||||
|
|
||||||
|
let searchText = viewModel.inputText.value
|
||||||
|
let searchType = AutoCompleteViewModel.SearchType(inputText: searchText) ?? .default
|
||||||
|
if searchText != previoursSearchText {
|
||||||
|
reset(searchText: searchText)
|
||||||
|
}
|
||||||
|
|
||||||
|
switch searchType {
|
||||||
|
case .emoji:
|
||||||
|
Loading.fetchLocalEmoji(
|
||||||
|
searchText: searchText,
|
||||||
|
viewModel: viewModel,
|
||||||
|
stateMachine: stateMachine
|
||||||
|
)
|
||||||
|
default:
|
||||||
|
Loading.queryRemoteEnitity(
|
||||||
|
searchText: searchText,
|
||||||
|
viewModel: viewModel,
|
||||||
|
stateMachine: stateMachine
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static func fetchLocalEmoji(
|
||||||
|
searchText: String,
|
||||||
|
viewModel: AutoCompleteViewModel,
|
||||||
|
stateMachine: GKStateMachine
|
||||||
|
) {
|
||||||
|
guard let customEmojiViewModel = viewModel.customEmojiViewModel.value else {
|
||||||
|
stateMachine.enter(Fail.self)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
guard let emojiTrie = customEmojiViewModel.emojiTrie.value else {
|
||||||
|
stateMachine.enter(Fail.self)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let searchPattern = ArraySlice(String(searchText.dropFirst()))
|
||||||
|
let passthroughs = emojiTrie.passthrough(searchPattern)
|
||||||
|
let matchingEmojis = passthroughs
|
||||||
|
.map { $0.values } // [Set<Emoji>]
|
||||||
|
.map { set in set.compactMap { $0 as? Mastodon.Entity.Emoji } } // [[Emoji]]
|
||||||
|
.flatMap { $0 } // [Emoji]
|
||||||
|
let items: [AutoCompleteItem] = matchingEmojis.map { emoji in
|
||||||
|
AutoCompleteItem.emoji(emoji: emoji)
|
||||||
|
}
|
||||||
|
stateMachine.enter(Idle.self)
|
||||||
|
viewModel.autoCompleteItems.value = items
|
||||||
|
}
|
||||||
|
|
||||||
|
private static func queryRemoteEnitity(
|
||||||
|
searchText: String,
|
||||||
|
viewModel: AutoCompleteViewModel,
|
||||||
|
stateMachine: GKStateMachine
|
||||||
|
) {
|
||||||
|
guard let activeMastodonAuthenticationBox = viewModel.context.authenticationService.activeMastodonAuthenticationBox.value else {
|
||||||
|
stateMachine.enter(Fail.self)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
let domain = activeMastodonAuthenticationBox.domain
|
||||||
|
|
||||||
|
let searchText = viewModel.inputText.value
|
||||||
|
let searchType = AutoCompleteViewModel.SearchType(inputText: searchText) ?? .default
|
||||||
|
|
||||||
|
let q = String(searchText.dropFirst())
|
||||||
|
let query = Mastodon.API.V2.Search.Query(
|
||||||
|
q: q,
|
||||||
|
type: searchType.mastodonSearchType,
|
||||||
|
maxID: nil,
|
||||||
|
offset: nil,
|
||||||
|
following: nil
|
||||||
|
)
|
||||||
|
viewModel.context.apiService.search(
|
||||||
|
domain: domain,
|
||||||
|
query: query,
|
||||||
|
mastodonAuthenticationBox: activeMastodonAuthenticationBox
|
||||||
|
)
|
||||||
|
.receive(on: DispatchQueue.main)
|
||||||
|
.sink { completion in
|
||||||
|
switch completion {
|
||||||
|
case .failure(let error):
|
||||||
|
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: auto-complete fail: %s", ((#file as NSString).lastPathComponent), #line, #function, error.localizedDescription)
|
||||||
|
stateMachine.enter(Fail.self)
|
||||||
|
case .finished:
|
||||||
|
break
|
||||||
|
}
|
||||||
|
} receiveValue: { response in
|
||||||
|
guard viewModel.inputText.value == searchText else { return } // discard if not matching
|
||||||
|
|
||||||
|
var items: [AutoCompleteItem] = []
|
||||||
|
items.append(contentsOf: response.value.accounts.map { AutoCompleteItem.account(account: $0) })
|
||||||
|
items.append(contentsOf: response.value.hashtags.map { AutoCompleteItem.hashtag(tag: $0) })
|
||||||
|
stateMachine.enter(Idle.self)
|
||||||
|
viewModel.autoCompleteItems.value = items
|
||||||
|
}
|
||||||
|
.store(in: &viewModel.disposeBag)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func reset(searchText: String) {
|
||||||
|
let previoursSearchType = AutoCompleteViewModel.SearchType(inputText: previoursSearchText)
|
||||||
|
previoursSearchText = searchText
|
||||||
|
let currentSearchType = AutoCompleteViewModel.SearchType(inputText: searchText)
|
||||||
|
// reset when search type change
|
||||||
|
if previoursSearchType != currentSearchType {
|
||||||
|
viewModel?.autoCompleteItems.value = []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
class Idle: AutoCompleteViewModel.State {
|
||||||
|
override func isValidNextState(_ stateClass: AnyClass) -> Bool {
|
||||||
|
switch stateClass {
|
||||||
|
case is Loading.Type:
|
||||||
|
return true
|
||||||
|
default:
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class Fail: AutoCompleteViewModel.State {
|
||||||
|
|
||||||
|
override func isValidNextState(_ stateClass: AnyClass) -> Bool {
|
||||||
|
switch stateClass {
|
||||||
|
case is Loading.Type:
|
||||||
|
return true
|
||||||
|
default:
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override func didEnter(from previousState: GKState?) {
|
||||||
|
super.didEnter(from: previousState)
|
||||||
|
guard let _ = viewModel, let stateMachine = stateMachine else { return }
|
||||||
|
|
||||||
|
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: retry loading 3s later…", ((#file as NSString).lastPathComponent), #line, #function)
|
||||||
|
DispatchQueue.main.asyncAfter(deadline: .now() + 3) {
|
||||||
|
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: retry loading", ((#file as NSString).lastPathComponent), #line, #function)
|
||||||
|
stateMachine.enter(Loading.self)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,107 @@
|
||||||
|
//
|
||||||
|
// AutoCompleteViewModel.swift
|
||||||
|
// Mastodon
|
||||||
|
//
|
||||||
|
// Created by MainasuK Cirno on 2021-5-17.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
import Combine
|
||||||
|
import GameplayKit
|
||||||
|
import MastodonSDK
|
||||||
|
|
||||||
|
final class AutoCompleteViewModel {
|
||||||
|
|
||||||
|
var disposeBag = Set<AnyCancellable>()
|
||||||
|
|
||||||
|
// input
|
||||||
|
let context: AppContext
|
||||||
|
let inputText = CurrentValueSubject<String, Never>("") // contains "@" or "#" prefix
|
||||||
|
let symbolBoundingRect = CurrentValueSubject<CGRect, Never>(.zero)
|
||||||
|
let customEmojiViewModel = CurrentValueSubject<EmojiService.CustomEmojiViewModel?, Never>(nil)
|
||||||
|
|
||||||
|
// output
|
||||||
|
var autoCompleteItems = CurrentValueSubject<[AutoCompleteItem], Never>([])
|
||||||
|
var diffableDataSource: UITableViewDiffableDataSource<AutoCompleteSection, AutoCompleteItem>!
|
||||||
|
private(set) lazy var stateMachine: GKStateMachine = {
|
||||||
|
// exclude timeline middle fetcher state
|
||||||
|
let stateMachine = GKStateMachine(states: [
|
||||||
|
State.Initial(viewModel: self),
|
||||||
|
State.Loading(viewModel: self),
|
||||||
|
State.Fail(viewModel: self),
|
||||||
|
State.Idle(viewModel: self),
|
||||||
|
])
|
||||||
|
stateMachine.enter(State.Initial.self)
|
||||||
|
return stateMachine
|
||||||
|
}()
|
||||||
|
|
||||||
|
init(context: AppContext) {
|
||||||
|
self.context = context
|
||||||
|
|
||||||
|
autoCompleteItems
|
||||||
|
.receive(on: DispatchQueue.main)
|
||||||
|
.sink { [weak self] items in
|
||||||
|
guard let self = self else { return }
|
||||||
|
guard let diffableDataSource = self.diffableDataSource else { return }
|
||||||
|
|
||||||
|
var snapshot = NSDiffableDataSourceSnapshot<AutoCompleteSection, AutoCompleteItem>()
|
||||||
|
snapshot.appendSections([.main])
|
||||||
|
snapshot.appendItems(items, toSection: .main)
|
||||||
|
if let currentState = self.stateMachine.currentState {
|
||||||
|
switch currentState {
|
||||||
|
case is State.Loading, is State.Fail:
|
||||||
|
if items.isEmpty {
|
||||||
|
snapshot.appendItems([.bottomLoader], toSection: .main)
|
||||||
|
}
|
||||||
|
case is State.Idle:
|
||||||
|
// TODO: handle no results
|
||||||
|
break
|
||||||
|
default:
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
diffableDataSource.defaultRowAnimation = .fade
|
||||||
|
diffableDataSource.apply(snapshot)
|
||||||
|
}
|
||||||
|
.store(in: &disposeBag)
|
||||||
|
|
||||||
|
inputText
|
||||||
|
.removeDuplicates()
|
||||||
|
.receive(on: DispatchQueue.main)
|
||||||
|
.sink { [weak self] inputText in
|
||||||
|
guard let self = self else { return }
|
||||||
|
self.stateMachine.enter(State.Loading.self)
|
||||||
|
}
|
||||||
|
.store(in: &disposeBag)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
extension AutoCompleteViewModel {
|
||||||
|
enum SearchType {
|
||||||
|
case accounts
|
||||||
|
case hashtags
|
||||||
|
case emoji
|
||||||
|
case `default`
|
||||||
|
|
||||||
|
public var mastodonSearchType: Mastodon.API.V2.Search.SearchType? {
|
||||||
|
switch self {
|
||||||
|
case .accounts: return .accounts
|
||||||
|
case .hashtags: return .hashtags
|
||||||
|
case .emoji: return nil
|
||||||
|
case .default: return .default
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
init?(inputText: String) {
|
||||||
|
let prefix = inputText.first ?? Character("_")
|
||||||
|
switch prefix {
|
||||||
|
case "@": self = .accounts
|
||||||
|
case "#": self = .hashtags
|
||||||
|
case ":": self = .emoji
|
||||||
|
default: return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,156 @@
|
||||||
|
//
|
||||||
|
// AutoCompleteTableViewCell.swift
|
||||||
|
// Mastodon
|
||||||
|
//
|
||||||
|
// Created by MainasuK Cirno on 2021-5-17.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
final class AutoCompleteTableViewCell: UITableViewCell {
|
||||||
|
|
||||||
|
static let avatarImageSize = CGSize(width: 42, height: 42)
|
||||||
|
static let avatarImageCornerRadius: CGFloat = 4
|
||||||
|
static let avatarToLabelSpacing: CGFloat = 12
|
||||||
|
|
||||||
|
let containerStackView: UIStackView = {
|
||||||
|
let stackView = UIStackView()
|
||||||
|
stackView.axis = .horizontal
|
||||||
|
stackView.spacing = 12
|
||||||
|
return stackView
|
||||||
|
}()
|
||||||
|
|
||||||
|
let contentStackView: UIStackView = {
|
||||||
|
let stackView = UIStackView()
|
||||||
|
stackView.axis = .vertical
|
||||||
|
stackView.distribution = .fillEqually
|
||||||
|
return stackView
|
||||||
|
}()
|
||||||
|
|
||||||
|
let avatarImageView = UIImageView()
|
||||||
|
|
||||||
|
let titleLabel: UILabel = {
|
||||||
|
let label = UILabel()
|
||||||
|
label.font = UIFontMetrics(forTextStyle: .headline).scaledFont(for: .systemFont(ofSize: 17, weight: .semibold), maximumPointSize: 22)
|
||||||
|
label.textColor = Asset.Colors.Label.highlight.color
|
||||||
|
label.text = "Title"
|
||||||
|
return label
|
||||||
|
}()
|
||||||
|
|
||||||
|
let subtitleLabel: UILabel = {
|
||||||
|
let label = UILabel()
|
||||||
|
label.font = UIFontMetrics(forTextStyle: .subheadline).scaledFont(for: .systemFont(ofSize: 15, weight: .regular), maximumPointSize: 20)
|
||||||
|
label.textColor = Asset.Colors.Label.secondary.color
|
||||||
|
label.text = "subtitle"
|
||||||
|
return label
|
||||||
|
}()
|
||||||
|
|
||||||
|
override func prepareForReuse() {
|
||||||
|
super.prepareForReuse()
|
||||||
|
avatarImageView.af.cancelImageRequest()
|
||||||
|
avatarImageView.kf.cancelDownloadTask()
|
||||||
|
}
|
||||||
|
|
||||||
|
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
|
||||||
|
super.init(style: style, reuseIdentifier: reuseIdentifier)
|
||||||
|
_init()
|
||||||
|
}
|
||||||
|
|
||||||
|
required init?(coder: NSCoder) {
|
||||||
|
super.init(coder: coder)
|
||||||
|
_init()
|
||||||
|
}
|
||||||
|
|
||||||
|
override func setHighlighted(_ highlighted: Bool, animated: Bool) {
|
||||||
|
super.setHighlighted(highlighted, animated: animated)
|
||||||
|
|
||||||
|
// workaround for hitTest trigger highlighted issue
|
||||||
|
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
|
||||||
|
if self.isHighlighted {
|
||||||
|
self.setHighlighted(false, animated: true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
extension AutoCompleteTableViewCell {
|
||||||
|
|
||||||
|
private func _init() {
|
||||||
|
backgroundColor = .clear
|
||||||
|
|
||||||
|
let topPaddingView = UIView()
|
||||||
|
let bottomPaddingView = UIView()
|
||||||
|
|
||||||
|
topPaddingView.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
contentView.addSubview(topPaddingView)
|
||||||
|
NSLayoutConstraint.activate([
|
||||||
|
topPaddingView.topAnchor.constraint(equalTo: contentView.topAnchor),
|
||||||
|
topPaddingView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor),
|
||||||
|
topPaddingView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor),
|
||||||
|
topPaddingView.heightAnchor.constraint(equalToConstant: 12).priority(.defaultHigh),
|
||||||
|
])
|
||||||
|
|
||||||
|
containerStackView.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
contentView.addSubview(containerStackView)
|
||||||
|
NSLayoutConstraint.activate([
|
||||||
|
containerStackView.topAnchor.constraint(equalTo: topPaddingView.bottomAnchor),
|
||||||
|
containerStackView.leadingAnchor.constraint(equalTo: contentView.readableContentGuide.leadingAnchor),
|
||||||
|
containerStackView.trailingAnchor.constraint(equalTo: contentView.readableContentGuide.trailingAnchor),
|
||||||
|
])
|
||||||
|
|
||||||
|
avatarImageView.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
containerStackView.addArrangedSubview(avatarImageView)
|
||||||
|
NSLayoutConstraint.activate([
|
||||||
|
avatarImageView.heightAnchor.constraint(equalToConstant: AutoCompleteTableViewCell.avatarImageSize.height).priority(.required - 1),
|
||||||
|
avatarImageView.widthAnchor.constraint(equalToConstant: AutoCompleteTableViewCell.avatarImageSize.width).priority(.required - 1),
|
||||||
|
])
|
||||||
|
containerStackView.addArrangedSubview(contentStackView)
|
||||||
|
contentStackView.addArrangedSubview(titleLabel)
|
||||||
|
contentStackView.addArrangedSubview(subtitleLabel)
|
||||||
|
|
||||||
|
bottomPaddingView.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
contentView.addSubview(bottomPaddingView)
|
||||||
|
NSLayoutConstraint.activate([
|
||||||
|
bottomPaddingView.topAnchor.constraint(equalTo: contentStackView.bottomAnchor),
|
||||||
|
bottomPaddingView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor),
|
||||||
|
bottomPaddingView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor),
|
||||||
|
bottomPaddingView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor),
|
||||||
|
bottomPaddingView.heightAnchor.constraint(equalTo: topPaddingView.heightAnchor, multiplier: 1.0),
|
||||||
|
])
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - AvatarConfigurableView
|
||||||
|
extension AutoCompleteTableViewCell: AvatarConfigurableView {
|
||||||
|
static var configurableAvatarImageSize: CGSize { avatarImageSize }
|
||||||
|
static var configurableAvatarImageCornerRadius: CGFloat { avatarImageCornerRadius }
|
||||||
|
var configurableAvatarImageView: UIImageView? { avatarImageView }
|
||||||
|
var configurableAvatarButton: UIButton? { nil }
|
||||||
|
}
|
||||||
|
|
||||||
|
#if canImport(SwiftUI) && DEBUG
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
struct AutoCompleteTableViewCell_Previews: PreviewProvider {
|
||||||
|
|
||||||
|
static var previews: some View {
|
||||||
|
Group {
|
||||||
|
UIViewPreview() {
|
||||||
|
let cell = AutoCompleteTableViewCell()
|
||||||
|
return cell
|
||||||
|
}
|
||||||
|
.previewLayout(.fixed(width: 375, height: 66))
|
||||||
|
UIViewPreview() {
|
||||||
|
let cell = AutoCompleteTableViewCell()
|
||||||
|
return cell
|
||||||
|
}
|
||||||
|
.preferredColorScheme(.dark)
|
||||||
|
.previewLayout(.fixed(width: 375, height: 66))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
#endif
|
|
@ -0,0 +1,178 @@
|
||||||
|
//
|
||||||
|
// AutoCompleteTopChevronView.swift
|
||||||
|
// Mastodon
|
||||||
|
//
|
||||||
|
// Created by MainasuK Cirno on 2021-5-14.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
final class AutoCompleteTopChevronView: UIView {
|
||||||
|
|
||||||
|
static let chevronSize = CGSize(width: 20, height: 12)
|
||||||
|
|
||||||
|
private let shadowView = UIView()
|
||||||
|
private let shadowLayer = CAShapeLayer()
|
||||||
|
private let maskLayer = CAShapeLayer()
|
||||||
|
|
||||||
|
var chevronMinX: CGFloat = 0
|
||||||
|
var topViewBackgroundColor = Asset.Scene.Compose.background.color {
|
||||||
|
didSet { setNeedsLayout() }
|
||||||
|
}
|
||||||
|
var bottomViewBackgroundColor = Asset.Colors.Background.systemBackground.color {
|
||||||
|
didSet { setNeedsLayout() }
|
||||||
|
}
|
||||||
|
|
||||||
|
override init(frame: CGRect) {
|
||||||
|
super.init(frame: frame)
|
||||||
|
_init()
|
||||||
|
}
|
||||||
|
|
||||||
|
required init?(coder: NSCoder) {
|
||||||
|
super.init(coder: coder)
|
||||||
|
_init()
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
extension AutoCompleteTopChevronView {
|
||||||
|
|
||||||
|
var standardizedChevronMinX: CGFloat {
|
||||||
|
min(max(chevronMinX, 0), bounds.width - AutoCompleteTopChevronView.chevronSize.width)
|
||||||
|
}
|
||||||
|
|
||||||
|
var edgePoints: [CGPoint] {
|
||||||
|
[
|
||||||
|
CGPoint(x: standardizedChevronMinX, y: bounds.maxY),
|
||||||
|
CGPoint(x: standardizedChevronMinX + 0.5 * AutoCompleteTopChevronView.chevronSize.width, y: bounds.maxY - AutoCompleteTopChevronView.chevronSize.height),
|
||||||
|
CGPoint(x: standardizedChevronMinX + AutoCompleteTopChevronView.chevronSize.width, y: bounds.maxY),
|
||||||
|
CGPoint(x: bounds.width, y: bounds.maxY),
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
extension AutoCompleteTopChevronView {
|
||||||
|
|
||||||
|
private func _init() {
|
||||||
|
clipsToBounds = false
|
||||||
|
backgroundColor = .clear
|
||||||
|
isUserInteractionEnabled = false
|
||||||
|
|
||||||
|
shadowView.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
addSubview(shadowView)
|
||||||
|
NSLayoutConstraint.activate([
|
||||||
|
shadowView.topAnchor.constraint(equalTo: topAnchor),
|
||||||
|
shadowView.leadingAnchor.constraint(equalTo: leadingAnchor),
|
||||||
|
shadowView.trailingAnchor.constraint(equalTo: trailingAnchor),
|
||||||
|
shadowView.bottomAnchor.constraint(equalTo: bottomAnchor),
|
||||||
|
])
|
||||||
|
|
||||||
|
shadowLayer.fillColor = topViewBackgroundColor.cgColor
|
||||||
|
shadowView.layer.addSublayer(shadowLayer)
|
||||||
|
}
|
||||||
|
|
||||||
|
override func layoutSubviews() {
|
||||||
|
super.layoutSubviews()
|
||||||
|
|
||||||
|
// 1. setup shadow with chevron
|
||||||
|
shadowLayer.path = {
|
||||||
|
let path = UIBezierPath()
|
||||||
|
path.move(to: CGPoint(x: 0, y: bounds.maxY))
|
||||||
|
// bottom edge
|
||||||
|
for point in edgePoints {
|
||||||
|
path.addLine(to: point)
|
||||||
|
}
|
||||||
|
// up egde
|
||||||
|
path.addLine(to: CGPoint(x: bounds.maxX, y: 0))
|
||||||
|
path.addLine(to: CGPoint(x: 0, y: 0))
|
||||||
|
path.close()
|
||||||
|
return path.cgPath
|
||||||
|
}()
|
||||||
|
shadowLayer.fillColor = topViewBackgroundColor.cgColor
|
||||||
|
|
||||||
|
// 2. setup mask to clip shadow
|
||||||
|
maskLayer.path = {
|
||||||
|
let path = UIBezierPath()
|
||||||
|
path.move(to: CGPoint(x: 0, y: bounds.maxY))
|
||||||
|
// up edge
|
||||||
|
for point in edgePoints {
|
||||||
|
path.addLine(to: CGPoint(x: point.x, y: point.y - 2)) // move up 2pt
|
||||||
|
}
|
||||||
|
// bottom egde
|
||||||
|
path.addLine(to: CGPoint(x: bounds.maxX, y: 2 * bounds.maxY))
|
||||||
|
path.addLine(to: CGPoint(x: 0, y: 2 * bounds.maxY))
|
||||||
|
path.close()
|
||||||
|
return path.cgPath
|
||||||
|
}()
|
||||||
|
maskLayer.fillColor = UIColor.red.cgColor
|
||||||
|
shadowView.layer.setupShadow(color: UIColor.black.withAlphaComponent(0.25))
|
||||||
|
shadowView.layer.mask = maskLayer
|
||||||
|
|
||||||
|
layer.mask = maskLayer
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
extension AutoCompleteTopChevronView {
|
||||||
|
func invertMask(in rect: CGRect) -> CAShapeLayer {
|
||||||
|
let path = UIBezierPath()
|
||||||
|
path.move(to: CGPoint(x: 0, y: bounds.maxY))
|
||||||
|
// top edge
|
||||||
|
for point in edgePoints {
|
||||||
|
path.addLine(to: point)
|
||||||
|
}
|
||||||
|
// bottom edge
|
||||||
|
path.addLine(to: CGPoint(x: rect.maxX, y: rect.maxY))
|
||||||
|
path.addLine(to: CGPoint(x: 0, y: rect.maxY))
|
||||||
|
path.close()
|
||||||
|
|
||||||
|
let mask = CAShapeLayer()
|
||||||
|
mask.fillColor = UIColor.red.cgColor
|
||||||
|
mask.path = path.cgPath
|
||||||
|
|
||||||
|
return mask
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#if canImport(SwiftUI) && DEBUG
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
struct AutoCompleteTopChevronView_Previews: PreviewProvider {
|
||||||
|
|
||||||
|
static var previews: some View {
|
||||||
|
Group {
|
||||||
|
UIViewPreview(width: 375) {
|
||||||
|
let view = AutoCompleteTopChevronView()
|
||||||
|
view.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
NSLayoutConstraint.activate([
|
||||||
|
view.widthAnchor.constraint(equalToConstant: 375),
|
||||||
|
view.heightAnchor.constraint(equalToConstant: 100),
|
||||||
|
])
|
||||||
|
view.chevronMinX = 10
|
||||||
|
return view
|
||||||
|
}
|
||||||
|
.background(Color(Asset.Scene.Compose.background.color))
|
||||||
|
.padding(20)
|
||||||
|
.previewLayout(.fixed(width: 375 + 40, height: 100 + 40))
|
||||||
|
UIViewPreview(width: 375) {
|
||||||
|
let view = AutoCompleteTopChevronView()
|
||||||
|
view.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
NSLayoutConstraint.activate([
|
||||||
|
view.widthAnchor.constraint(equalToConstant: 375),
|
||||||
|
view.heightAnchor.constraint(equalToConstant: 100),
|
||||||
|
])
|
||||||
|
view.chevronMinX = 10
|
||||||
|
return view
|
||||||
|
}
|
||||||
|
.background(Color(Asset.Scene.Compose.background.color))
|
||||||
|
.preferredColorScheme(.dark)
|
||||||
|
.padding(20)
|
||||||
|
.previewLayout(.fixed(width: 375 + 40, height: 100 + 40))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
#endif
|
|
@ -18,6 +18,7 @@ final class ComposeStatusContentCollectionViewCell: UICollectionViewCell {
|
||||||
|
|
||||||
let statusContentWarningEditorView = StatusContentWarningEditorView()
|
let statusContentWarningEditorView = StatusContentWarningEditorView()
|
||||||
|
|
||||||
|
let textEditorViewContainerView = UIView()
|
||||||
let textEditorView: TextEditorView = {
|
let textEditorView: TextEditorView = {
|
||||||
let textEditorView = TextEditorView()
|
let textEditorView = TextEditorView()
|
||||||
textEditorView.font = .preferredFont(forTextStyle: .body)
|
textEditorView.font = .preferredFont(forTextStyle: .body)
|
||||||
|
@ -28,6 +29,9 @@ final class ComposeStatusContentCollectionViewCell: UICollectionViewCell {
|
||||||
return textEditorView
|
return textEditorView
|
||||||
}()
|
}()
|
||||||
|
|
||||||
|
// input
|
||||||
|
weak var textEditorViewChangeObserver: TextEditorViewChangeObserver?
|
||||||
|
|
||||||
// output
|
// output
|
||||||
let composeContent = PassthroughSubject<String, Never>()
|
let composeContent = PassthroughSubject<String, Never>()
|
||||||
let contentWarningContent = PassthroughSubject<String, Never>()
|
let contentWarningContent = PassthroughSubject<String, Never>()
|
||||||
|
@ -48,6 +52,7 @@ extension ComposeStatusContentCollectionViewCell {
|
||||||
|
|
||||||
private func _init() {
|
private func _init() {
|
||||||
// selectionStyle = .none
|
// selectionStyle = .none
|
||||||
|
layer.zPosition = 999
|
||||||
preservesSuperviewLayoutMargins = true
|
preservesSuperviewLayoutMargins = true
|
||||||
|
|
||||||
statusContentWarningEditorView.translatesAutoresizingMaskIntoConstraints = false
|
statusContentWarningEditorView.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
@ -75,13 +80,23 @@ extension ComposeStatusContentCollectionViewCell {
|
||||||
statusView.setContentHuggingPriority(.defaultHigh, for: .vertical)
|
statusView.setContentHuggingPriority(.defaultHigh, for: .vertical)
|
||||||
statusView.setContentCompressionResistancePriority(.required - 1, for: .vertical)
|
statusView.setContentCompressionResistancePriority(.required - 1, for: .vertical)
|
||||||
|
|
||||||
textEditorView.translatesAutoresizingMaskIntoConstraints = false
|
textEditorViewContainerView.translatesAutoresizingMaskIntoConstraints = false
|
||||||
contentView.addSubview(textEditorView)
|
contentView.addSubview(textEditorViewContainerView)
|
||||||
NSLayoutConstraint.activate([
|
NSLayoutConstraint.activate([
|
||||||
textEditorView.topAnchor.constraint(equalTo: statusView.bottomAnchor, constant: 10),
|
textEditorViewContainerView.topAnchor.constraint(equalTo: statusView.bottomAnchor, constant: 10),
|
||||||
textEditorView.leadingAnchor.constraint(equalTo: contentView.layoutMarginsGuide.leadingAnchor),
|
textEditorViewContainerView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor),
|
||||||
textEditorView.trailingAnchor.constraint(equalTo: contentView.layoutMarginsGuide.trailingAnchor),
|
textEditorViewContainerView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor),
|
||||||
contentView.bottomAnchor.constraint(equalTo: textEditorView.bottomAnchor, constant: 10),
|
contentView.bottomAnchor.constraint(equalTo: textEditorViewContainerView.bottomAnchor, constant: 10),
|
||||||
|
])
|
||||||
|
textEditorViewContainerView.preservesSuperviewLayoutMargins = true
|
||||||
|
|
||||||
|
textEditorView.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
textEditorViewContainerView.addSubview(textEditorView)
|
||||||
|
NSLayoutConstraint.activate([
|
||||||
|
textEditorView.topAnchor.constraint(equalTo: textEditorViewContainerView.topAnchor),
|
||||||
|
textEditorView.leadingAnchor.constraint(equalTo: textEditorViewContainerView.readableContentGuide.leadingAnchor),
|
||||||
|
textEditorView.trailingAnchor.constraint(equalTo: textEditorViewContainerView.readableContentGuide.trailingAnchor),
|
||||||
|
textEditorView.bottomAnchor.constraint(equalTo: textEditorViewContainerView.bottomAnchor),
|
||||||
textEditorView.heightAnchor.constraint(greaterThanOrEqualToConstant: 44).priority(.defaultHigh),
|
textEditorView.heightAnchor.constraint(greaterThanOrEqualToConstant: 44).priority(.defaultHigh),
|
||||||
])
|
])
|
||||||
textEditorView.setContentCompressionResistancePriority(.required - 2, for: .vertical)
|
textEditorView.setContentCompressionResistancePriority(.required - 2, for: .vertical)
|
||||||
|
@ -98,6 +113,10 @@ extension ComposeStatusContentCollectionViewCell {
|
||||||
// MARK: - TextEditorViewChangeObserver
|
// MARK: - TextEditorViewChangeObserver
|
||||||
extension ComposeStatusContentCollectionViewCell: TextEditorViewChangeObserver {
|
extension ComposeStatusContentCollectionViewCell: TextEditorViewChangeObserver {
|
||||||
func textEditorView(_ textEditorView: TextEditorView, didChangeWithChangeResult changeResult: TextEditorViewChangeResult) {
|
func textEditorView(_ textEditorView: TextEditorView, didChangeWithChangeResult changeResult: TextEditorViewChangeResult) {
|
||||||
|
defer {
|
||||||
|
textEditorViewChangeObserver?.textEditorView(textEditorView, didChangeWithChangeResult: changeResult)
|
||||||
|
}
|
||||||
|
|
||||||
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: text: %s", ((#file as NSString).lastPathComponent), #line, #function, textEditorView.text)
|
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: text: %s", ((#file as NSString).lastPathComponent), #line, #function, textEditorView.text)
|
||||||
guard changeResult.isTextChanged else { return }
|
guard changeResult.isTextChanged else { return }
|
||||||
composeContent.send(textEditorView.text)
|
composeContent.send(textEditorView.text)
|
||||||
|
|
|
@ -15,6 +15,8 @@ import TwitterTextEditor
|
||||||
|
|
||||||
final class ComposeViewController: UIViewController, NeedsDependency {
|
final class ComposeViewController: UIViewController, NeedsDependency {
|
||||||
|
|
||||||
|
static let minAutoCompleteVisibleHeight: CGFloat = 100
|
||||||
|
|
||||||
weak var context: AppContext! { willSet { precondition(!isViewLoaded) } }
|
weak var context: AppContext! { willSet { precondition(!isViewLoaded) } }
|
||||||
weak var coordinator: SceneCoordinator! { willSet { precondition(!isViewLoaded) } }
|
weak var coordinator: SceneCoordinator! { willSet { precondition(!isViewLoaded) } }
|
||||||
|
|
||||||
|
@ -35,14 +37,16 @@ final class ComposeViewController: UIViewController, NeedsDependency {
|
||||||
button.adjustsImageWhenHighlighted = false
|
button.adjustsImageWhenHighlighted = false
|
||||||
return button
|
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 = {
|
private(set) lazy var publishBarButtonItem: UIBarButtonItem = {
|
||||||
let barButtonItem = UIBarButtonItem(customView: publishButton)
|
let barButtonItem = UIBarButtonItem(customView: publishButton)
|
||||||
return barButtonItem
|
return barButtonItem
|
||||||
}()
|
}()
|
||||||
|
|
||||||
let collectionView: UICollectionView = {
|
let collectionView: ComposeCollectionView = {
|
||||||
let collectionViewLayout = ComposeViewController.createLayout()
|
let collectionViewLayout = ComposeViewController.createLayout()
|
||||||
let collectionView = UICollectionView(frame: .zero, collectionViewLayout: collectionViewLayout)
|
let collectionView = ComposeCollectionView(frame: .zero, collectionViewLayout: collectionViewLayout)
|
||||||
collectionView.register(ComposeRepliedToStatusContentCollectionViewCell.self, forCellWithReuseIdentifier: String(describing: ComposeRepliedToStatusContentCollectionViewCell.self))
|
collectionView.register(ComposeRepliedToStatusContentCollectionViewCell.self, forCellWithReuseIdentifier: String(describing: ComposeRepliedToStatusContentCollectionViewCell.self))
|
||||||
collectionView.register(ComposeStatusContentCollectionViewCell.self, forCellWithReuseIdentifier: String(describing: ComposeStatusContentCollectionViewCell.self))
|
collectionView.register(ComposeStatusContentCollectionViewCell.self, forCellWithReuseIdentifier: String(describing: ComposeStatusContentCollectionViewCell.self))
|
||||||
collectionView.register(ComposeStatusAttachmentCollectionViewCell.self, forCellWithReuseIdentifier: String(describing: ComposeStatusAttachmentCollectionViewCell.self))
|
collectionView.register(ComposeStatusAttachmentCollectionViewCell.self, forCellWithReuseIdentifier: String(describing: ComposeStatusAttachmentCollectionViewCell.self))
|
||||||
|
@ -93,6 +97,16 @@ final class ComposeViewController: UIViewController, NeedsDependency {
|
||||||
return documentPickerController
|
return documentPickerController
|
||||||
}()
|
}()
|
||||||
|
|
||||||
|
private(set) lazy var autoCompleteViewController: AutoCompleteViewController = {
|
||||||
|
let viewController = AutoCompleteViewController()
|
||||||
|
viewController.viewModel = AutoCompleteViewModel(context: context)
|
||||||
|
viewController.delegate = self
|
||||||
|
viewModel.customEmojiViewModel
|
||||||
|
.assign(to: \.value, on: viewController.viewModel.customEmojiViewModel)
|
||||||
|
.store(in: &disposeBag)
|
||||||
|
return viewController
|
||||||
|
}()
|
||||||
|
|
||||||
deinit {
|
deinit {
|
||||||
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function)
|
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function)
|
||||||
}
|
}
|
||||||
|
@ -126,7 +140,7 @@ extension ComposeViewController {
|
||||||
}
|
}
|
||||||
.store(in: &disposeBag)
|
.store(in: &disposeBag)
|
||||||
view.backgroundColor = Asset.Scene.Compose.background.color
|
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
|
navigationItem.rightBarButtonItem = publishBarButtonItem
|
||||||
publishButton.addTarget(self, action: #selector(ComposeViewController.publishBarButtonItemPressed(_:)), for: .touchUpInside)
|
publishButton.addTarget(self, action: #selector(ComposeViewController.publishBarButtonItemPressed(_:)), for: .touchUpInside)
|
||||||
|
|
||||||
|
@ -166,6 +180,7 @@ extension ComposeViewController {
|
||||||
dependency: self,
|
dependency: self,
|
||||||
customEmojiPickerInputViewModel: viewModel.customEmojiPickerInputViewModel,
|
customEmojiPickerInputViewModel: viewModel.customEmojiPickerInputViewModel,
|
||||||
textEditorViewTextAttributesDelegate: self,
|
textEditorViewTextAttributesDelegate: self,
|
||||||
|
textEditorViewChangeObserver: self,
|
||||||
composeStatusAttachmentTableViewCellDelegate: self,
|
composeStatusAttachmentTableViewCellDelegate: self,
|
||||||
composeStatusPollOptionCollectionViewCellDelegate: self,
|
composeStatusPollOptionCollectionViewCellDelegate: self,
|
||||||
composeStatusNewPollOptionCollectionViewCellDelegate: self,
|
composeStatusNewPollOptionCollectionViewCellDelegate: self,
|
||||||
|
@ -181,26 +196,27 @@ extension ComposeViewController {
|
||||||
dependency: self
|
dependency: self
|
||||||
)
|
)
|
||||||
|
|
||||||
// respond scrollView overlap change
|
|
||||||
//view.layoutIfNeeded()
|
|
||||||
// update layout when keyboard show/dismiss
|
// update layout when keyboard show/dismiss
|
||||||
Publishers.CombineLatest4(
|
let keyboardEventPublishers = Publishers.CombineLatest3(
|
||||||
KeyboardResponderService.shared.isShow.eraseToAnyPublisher(),
|
KeyboardResponderService.shared.isShow,
|
||||||
KeyboardResponderService.shared.state.eraseToAnyPublisher(),
|
KeyboardResponderService.shared.state,
|
||||||
KeyboardResponderService.shared.endFrame.eraseToAnyPublisher(),
|
KeyboardResponderService.shared.endFrame
|
||||||
viewModel.isCustomEmojiComposing.eraseToAnyPublisher()
|
|
||||||
)
|
)
|
||||||
.sink(receiveValue: { [weak self] isShow, state, endFrame, isCustomEmojiComposing in
|
Publishers.CombineLatest3(
|
||||||
|
keyboardEventPublishers,
|
||||||
|
viewModel.isCustomEmojiComposing,
|
||||||
|
viewModel.autoCompleteInfo
|
||||||
|
)
|
||||||
|
.sink(receiveValue: { [weak self] keyboardEvents, isCustomEmojiComposing, autoCompleteInfo in
|
||||||
guard let self = self else { return }
|
guard let self = self else { return }
|
||||||
|
|
||||||
|
let (isShow, state, endFrame) = keyboardEvents
|
||||||
let extraMargin: CGFloat = {
|
let extraMargin: CGFloat = {
|
||||||
if self.view.safeAreaInsets.bottom == .zero {
|
var margin = self.composeToolbarView.frame.height
|
||||||
// needs extra margin for zero inset device to workaround UIKit issue
|
if autoCompleteInfo != nil {
|
||||||
return self.composeToolbarView.frame.height
|
margin += ComposeViewController.minAutoCompleteVisibleHeight
|
||||||
} else {
|
|
||||||
// default some magic 16 extra margin
|
|
||||||
return 16
|
|
||||||
}
|
}
|
||||||
|
return margin
|
||||||
}()
|
}()
|
||||||
|
|
||||||
// update keyboard background color
|
// update keyboard background color
|
||||||
|
@ -208,6 +224,17 @@ extension ComposeViewController {
|
||||||
guard isShow, state == .dock else {
|
guard isShow, state == .dock else {
|
||||||
self.collectionView.contentInset.bottom = self.view.safeAreaInsets.bottom + extraMargin
|
self.collectionView.contentInset.bottom = self.view.safeAreaInsets.bottom + extraMargin
|
||||||
self.collectionView.verticalScrollIndicatorInsets.bottom = self.view.safeAreaInsets.bottom + extraMargin
|
self.collectionView.verticalScrollIndicatorInsets.bottom = self.view.safeAreaInsets.bottom + extraMargin
|
||||||
|
|
||||||
|
if let superView = self.autoCompleteViewController.tableView.superview {
|
||||||
|
let autoCompleteTableViewBottomInset: CGFloat = {
|
||||||
|
let tableViewFrameInWindow = superView.convert(self.autoCompleteViewController.tableView.frame, to: nil)
|
||||||
|
let padding = tableViewFrameInWindow.maxY + self.composeToolbarView.frame.height + AutoCompleteViewController.chevronViewHeight - self.view.frame.maxY
|
||||||
|
return max(0, padding)
|
||||||
|
}()
|
||||||
|
self.autoCompleteViewController.tableView.contentInset.bottom = autoCompleteTableViewBottomInset
|
||||||
|
self.autoCompleteViewController.tableView.verticalScrollIndicatorInsets.bottom = autoCompleteTableViewBottomInset
|
||||||
|
}
|
||||||
|
|
||||||
UIView.animate(withDuration: 0.3) {
|
UIView.animate(withDuration: 0.3) {
|
||||||
self.composeToolbarViewBottomLayoutConstraint.constant = self.view.safeAreaInsets.bottom
|
self.composeToolbarViewBottomLayoutConstraint.constant = self.view.safeAreaInsets.bottom
|
||||||
if self.view.window != nil {
|
if self.view.window != nil {
|
||||||
|
@ -220,29 +247,61 @@ extension ComposeViewController {
|
||||||
// isShow AND dock state
|
// isShow AND dock state
|
||||||
self.systemKeyboardHeight = endFrame.height
|
self.systemKeyboardHeight = endFrame.height
|
||||||
|
|
||||||
|
// adjust inset for auto-complete
|
||||||
|
let autoCompleteTableViewBottomInset: CGFloat = {
|
||||||
|
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)
|
||||||
|
}()
|
||||||
|
self.autoCompleteViewController.tableView.contentInset.bottom = autoCompleteTableViewBottomInset
|
||||||
|
self.autoCompleteViewController.tableView.verticalScrollIndicatorInsets.bottom = autoCompleteTableViewBottomInset
|
||||||
|
|
||||||
|
// adjust inset for collectionView
|
||||||
let contentFrame = self.view.convert(self.collectionView.frame, to: nil)
|
let contentFrame = self.view.convert(self.collectionView.frame, to: nil)
|
||||||
let padding = contentFrame.maxY - endFrame.minY
|
let padding = contentFrame.maxY + extraMargin - endFrame.minY
|
||||||
guard padding > 0 else {
|
guard padding > 0 else {
|
||||||
self.collectionView.contentInset.bottom = self.view.safeAreaInsets.bottom + extraMargin
|
self.collectionView.contentInset.bottom = self.view.safeAreaInsets.bottom + extraMargin
|
||||||
self.collectionView.verticalScrollIndicatorInsets.bottom = self.view.safeAreaInsets.bottom + extraMargin
|
self.collectionView.verticalScrollIndicatorInsets.bottom = self.view.safeAreaInsets.bottom + extraMargin
|
||||||
UIView.animate(withDuration: 0.3) {
|
|
||||||
self.composeToolbarViewBottomLayoutConstraint.constant = self.view.safeAreaInsets.bottom
|
|
||||||
self.view.layoutIfNeeded()
|
|
||||||
}
|
|
||||||
self.updateKeyboardBackground(isKeyboardDisplay: false)
|
self.updateKeyboardBackground(isKeyboardDisplay: false)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
self.collectionView.contentInset.bottom = padding + extraMargin
|
self.collectionView.contentInset.bottom = padding
|
||||||
self.collectionView.verticalScrollIndicatorInsets.bottom = padding + extraMargin
|
self.collectionView.verticalScrollIndicatorInsets.bottom = padding
|
||||||
UIView.animate(withDuration: 0.3) {
|
UIView.animate(withDuration: 0.3) {
|
||||||
self.composeToolbarViewBottomLayoutConstraint.constant = padding
|
self.composeToolbarViewBottomLayoutConstraint.constant = endFrame.height
|
||||||
self.view.layoutIfNeeded()
|
self.view.layoutIfNeeded()
|
||||||
}
|
}
|
||||||
self.updateKeyboardBackground(isKeyboardDisplay: isShow)
|
self.updateKeyboardBackground(isKeyboardDisplay: isShow)
|
||||||
})
|
})
|
||||||
.store(in: &disposeBag)
|
.store(in: &disposeBag)
|
||||||
|
|
||||||
|
// bind auto-complete
|
||||||
|
viewModel.autoCompleteInfo
|
||||||
|
.receive(on: DispatchQueue.main)
|
||||||
|
.sink { [weak self] info in
|
||||||
|
guard let self = self else { return }
|
||||||
|
guard let textEditorView = self.textEditorView() else { return }
|
||||||
|
if self.autoCompleteViewController.view.superview == nil {
|
||||||
|
self.autoCompleteViewController.view.frame = self.view.bounds
|
||||||
|
// add to container view. seealso: `viewDidLayoutSubviews()`
|
||||||
|
textEditorView.superview!.addSubview(self.autoCompleteViewController.view)
|
||||||
|
self.addChild(self.autoCompleteViewController)
|
||||||
|
self.autoCompleteViewController.didMove(toParent: self)
|
||||||
|
self.autoCompleteViewController.view.isHidden = true
|
||||||
|
self.collectionView.autoCompleteViewController = self.autoCompleteViewController
|
||||||
|
}
|
||||||
|
self.autoCompleteViewController.view.isHidden = info == nil
|
||||||
|
guard let info = info else { return }
|
||||||
|
let symbolBoundingRectInContainer = textEditorView.convert(info.symbolBoundingRect, to: self.autoCompleteViewController.chevronView)
|
||||||
|
self.autoCompleteViewController.view.frame.origin.y = info.textBoundingRect.maxY
|
||||||
|
self.autoCompleteViewController.viewModel.symbolBoundingRect.value = symbolBoundingRectInContainer
|
||||||
|
self.autoCompleteViewController.viewModel.inputText.value = String(info.inputText)
|
||||||
|
}
|
||||||
|
.store(in: &disposeBag)
|
||||||
|
|
||||||
// bind publish bar button state
|
// bind publish bar button state
|
||||||
viewModel.isPublishBarButtonItemEnabled
|
viewModel.isPublishBarButtonItemEnabled
|
||||||
.receive(on: DispatchQueue.main)
|
.receive(on: DispatchQueue.main)
|
||||||
|
@ -382,6 +441,18 @@ extension ComposeViewController {
|
||||||
viewModel.traitCollectionDidChangePublisher.send()
|
viewModel.traitCollectionDidChangePublisher.send()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override func viewDidLayoutSubviews() {
|
||||||
|
super.viewDidLayoutSubviews()
|
||||||
|
|
||||||
|
// pin autoCompleteViewController frame to window
|
||||||
|
if let containerView = autoCompleteViewController.view.superview {
|
||||||
|
let viewFrameInWindow = containerView.convert(autoCompleteViewController.view.frame, to: nil)
|
||||||
|
if viewFrameInWindow.origin.x != 0 {
|
||||||
|
autoCompleteViewController.view.frame.origin.x = -viewFrameInWindow.origin.x
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
extension ComposeViewController {
|
extension ComposeViewController {
|
||||||
|
@ -600,10 +671,8 @@ extension ComposeViewController: TextEditorViewTextAttributesDelegate {
|
||||||
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: update: %s", ((#file as NSString).lastPathComponent), #line, #function, string)
|
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: update: %s", ((#file as NSString).lastPathComponent), #line, #function, string)
|
||||||
|
|
||||||
let stringRange = NSRange(location: 0, length: string.length)
|
let stringRange = NSRange(location: 0, length: string.length)
|
||||||
let highlightMatches = string.matches(pattern: "(?:@([a-zA-Z0-9_]+)(@[a-zA-Z0-9_.-]+)?|#([^\\s.]+))")
|
let highlightMatches = string.matches(pattern: MastodonRegex.highlightPattern)
|
||||||
// accept ^\B: or \s: but not accept \B: to force user input a space to make emoji take effect
|
let emojiMatches = string.matches(pattern: MastodonRegex.emojiPattern)
|
||||||
// precondition :\B with following space
|
|
||||||
let emojiMatches = string.matches(pattern: "(?:(^\\B:|\\s:)([a-zA-Z0-9_]+)(:\\B(?=\\s)))")
|
|
||||||
// only accept http/https scheme
|
// only accept http/https scheme
|
||||||
let urlMatches = string.matches(pattern: "(?i)https?://\\S+(?:/|\\b)")
|
let urlMatches = string.matches(pattern: "(?i)https?://\\S+(?:/|\\b)")
|
||||||
|
|
||||||
|
@ -729,6 +798,115 @@ extension ComposeViewController: TextEditorViewTextAttributesDelegate {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MARK: - TextEditorViewChangeObserver
|
||||||
|
extension ComposeViewController: TextEditorViewChangeObserver {
|
||||||
|
|
||||||
|
func textEditorView(_ textEditorView: TextEditorView, didChangeWithChangeResult changeResult: TextEditorViewChangeResult) {
|
||||||
|
guard var autoCompeletion = ComposeViewController.scanAutoCompleteInfo(textEditorView: textEditorView) else {
|
||||||
|
viewModel.autoCompleteInfo.value = nil
|
||||||
|
return
|
||||||
|
}
|
||||||
|
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: auto complete %s (%s)", ((#file as NSString).lastPathComponent), #line, #function, String(autoCompeletion.toHighlightEndString), String(autoCompeletion.toCursorString))
|
||||||
|
|
||||||
|
// get layout text bounding rect
|
||||||
|
var glyphRange = NSRange()
|
||||||
|
textEditorView.layoutManager.characterRange(forGlyphRange: NSRange(autoCompeletion.toCursorRange, in: textEditorView.text), actualGlyphRange: &glyphRange)
|
||||||
|
let textContainer = textEditorView.layoutManager.textContainers[0]
|
||||||
|
let textBoundingRect = textEditorView.layoutManager.boundingRect(forGlyphRange: glyphRange, in: textContainer)
|
||||||
|
|
||||||
|
let retryLayoutTimes = viewModel.autoCompleteRetryLayoutTimes.value
|
||||||
|
guard textBoundingRect.size != .zero else {
|
||||||
|
viewModel.autoCompleteRetryLayoutTimes.value += 1
|
||||||
|
// avoid infinite loop
|
||||||
|
guard retryLayoutTimes < 3 else { return }
|
||||||
|
// needs retry calculate layout when the rect position changing
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
self.textEditorView(textEditorView, didChangeWithChangeResult: changeResult)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
viewModel.autoCompleteRetryLayoutTimes.value = 0
|
||||||
|
|
||||||
|
// get symbol bounding rect
|
||||||
|
textEditorView.layoutManager.characterRange(forGlyphRange: NSRange(autoCompeletion.symbolRange, in: textEditorView.text), actualGlyphRange: &glyphRange)
|
||||||
|
let symbolBoundingRect = textEditorView.layoutManager.boundingRect(forGlyphRange: glyphRange, in: textContainer)
|
||||||
|
|
||||||
|
// set bounding rect and trigger layout
|
||||||
|
autoCompeletion.textBoundingRect = textBoundingRect
|
||||||
|
autoCompeletion.symbolBoundingRect = symbolBoundingRect
|
||||||
|
viewModel.autoCompleteInfo.value = autoCompeletion
|
||||||
|
}
|
||||||
|
|
||||||
|
struct AutoCompleteInfo {
|
||||||
|
// model
|
||||||
|
let inputText: Substring
|
||||||
|
// range
|
||||||
|
let symbolRange: Range<String.Index>
|
||||||
|
let symbolString: Substring
|
||||||
|
let toCursorRange: Range<String.Index>
|
||||||
|
let toCursorString: Substring
|
||||||
|
let toHighlightEndRange: Range<String.Index>
|
||||||
|
let toHighlightEndString: Substring
|
||||||
|
// geometry
|
||||||
|
var textBoundingRect: CGRect = .zero
|
||||||
|
var symbolBoundingRect: CGRect = .zero
|
||||||
|
}
|
||||||
|
|
||||||
|
private static func scanAutoCompleteInfo(textEditorView: TextEditorView) -> AutoCompleteInfo? {
|
||||||
|
let text = textEditorView.text
|
||||||
|
let cursorLocation = textEditorView.selectedRange.location
|
||||||
|
let cursorIndex = text.index(text.startIndex, offsetBy: cursorLocation)
|
||||||
|
guard cursorLocation > 0, !text.isEmpty else { return nil }
|
||||||
|
|
||||||
|
let _highlighStartIndex: String.Index? = {
|
||||||
|
var index = text.index(text.startIndex, offsetBy: cursorLocation - 1)
|
||||||
|
while index > text.startIndex {
|
||||||
|
let char = text[index]
|
||||||
|
if char == "@" || char == "#" || char == ":" {
|
||||||
|
return index
|
||||||
|
}
|
||||||
|
index = text.index(before: index)
|
||||||
|
}
|
||||||
|
assert(index == text.startIndex)
|
||||||
|
let char = text[index]
|
||||||
|
if char == "@" || char == "#" || char == ":" {
|
||||||
|
return index
|
||||||
|
} else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
guard let highlighStartIndex = _highlighStartIndex else { return nil }
|
||||||
|
let scanRange = NSRange(highlighStartIndex..<text.endIndex, in: text)
|
||||||
|
|
||||||
|
guard let match = text.firstMatch(pattern: MastodonRegex.autoCompletePattern, options: [], range: scanRange) else { return nil }
|
||||||
|
let matchRange = match.range(at: 0)
|
||||||
|
let matchStartIndex = text.index(text.startIndex, offsetBy: matchRange.location)
|
||||||
|
let matchEndIndex = text.index(matchStartIndex, offsetBy: matchRange.length)
|
||||||
|
|
||||||
|
guard matchStartIndex == highlighStartIndex, matchEndIndex >= cursorIndex else { return nil }
|
||||||
|
let symbolRange = highlighStartIndex..<text.index(after: highlighStartIndex)
|
||||||
|
let symbolString = text[symbolRange]
|
||||||
|
let toCursorRange = highlighStartIndex..<cursorIndex
|
||||||
|
let toCursorString = text[toCursorRange]
|
||||||
|
let toHighlightEndRange = matchStartIndex..<matchEndIndex
|
||||||
|
let toHighlightEndString = text[toHighlightEndRange]
|
||||||
|
|
||||||
|
let inputText = toHighlightEndString
|
||||||
|
let autoCompleteInfo = AutoCompleteInfo(
|
||||||
|
inputText: inputText,
|
||||||
|
symbolRange: symbolRange,
|
||||||
|
symbolString: symbolString,
|
||||||
|
toCursorRange: toCursorRange,
|
||||||
|
toCursorString: toCursorString,
|
||||||
|
toHighlightEndRange: toHighlightEndRange,
|
||||||
|
toHighlightEndString: toHighlightEndString
|
||||||
|
)
|
||||||
|
return autoCompleteInfo
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
// MARK: - ComposeToolbarViewDelegate
|
// MARK: - ComposeToolbarViewDelegate
|
||||||
extension ComposeViewController: ComposeToolbarViewDelegate {
|
extension ComposeViewController: ComposeToolbarViewDelegate {
|
||||||
|
|
||||||
|
@ -814,7 +992,7 @@ extension ComposeViewController {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - UITableViewDelegate
|
// MARK: - UICollectionViewDelegate
|
||||||
extension ComposeViewController: UICollectionViewDelegate {
|
extension ComposeViewController: UICollectionViewDelegate {
|
||||||
|
|
||||||
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
|
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
|
||||||
|
@ -1047,3 +1225,165 @@ extension ComposeViewController: ComposeStatusPollExpiresOptionCollectionViewCel
|
||||||
viewModel.pollExpiresOptionAttribute.expiresOption.value = expiresOption
|
viewModel.pollExpiresOptionAttribute.expiresOption.value = expiresOption
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MARK: - AutoCompleteViewControllerDelegate
|
||||||
|
extension ComposeViewController: AutoCompleteViewControllerDelegate {
|
||||||
|
func autoCompleteViewController(_ viewController: AutoCompleteViewController, didSelectItem item: AutoCompleteItem) {
|
||||||
|
guard let info = viewModel.autoCompleteInfo.value else { return }
|
||||||
|
let _replacedText: String? = {
|
||||||
|
var text: String
|
||||||
|
switch item {
|
||||||
|
case .hashtag(let hashtag):
|
||||||
|
text = "#" + hashtag.name
|
||||||
|
case .hashtagV1(let hashtagName):
|
||||||
|
text = "#" + hashtagName
|
||||||
|
case .account(let account):
|
||||||
|
text = "@" + account.acct
|
||||||
|
case .emoji(let emoji):
|
||||||
|
text = ":" + emoji.shortcode + ":"
|
||||||
|
case .bottomLoader:
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
text.append(" ")
|
||||||
|
return text
|
||||||
|
}()
|
||||||
|
guard let replacedText = _replacedText else { return }
|
||||||
|
|
||||||
|
guard let textEditorView = textEditorView() else { return }
|
||||||
|
let text = textEditorView.text
|
||||||
|
|
||||||
|
do {
|
||||||
|
try textEditorView.updateByReplacing(range: NSRange(info.toHighlightEndRange, in: text), with: replacedText)
|
||||||
|
viewModel.autoCompleteInfo.value = nil
|
||||||
|
} catch {
|
||||||
|
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: auto complete fail %s", ((#file as NSString).lastPathComponent), #line, #function, error.localizedDescription)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
|
@ -17,6 +17,7 @@ extension ComposeViewModel {
|
||||||
dependency: NeedsDependency,
|
dependency: NeedsDependency,
|
||||||
customEmojiPickerInputViewModel: CustomEmojiPickerInputViewModel,
|
customEmojiPickerInputViewModel: CustomEmojiPickerInputViewModel,
|
||||||
textEditorViewTextAttributesDelegate: TextEditorViewTextAttributesDelegate,
|
textEditorViewTextAttributesDelegate: TextEditorViewTextAttributesDelegate,
|
||||||
|
textEditorViewChangeObserver: TextEditorViewChangeObserver,
|
||||||
composeStatusAttachmentTableViewCellDelegate: ComposeStatusAttachmentCollectionViewCellDelegate,
|
composeStatusAttachmentTableViewCellDelegate: ComposeStatusAttachmentCollectionViewCellDelegate,
|
||||||
composeStatusPollOptionCollectionViewCellDelegate: ComposeStatusPollOptionCollectionViewCellDelegate,
|
composeStatusPollOptionCollectionViewCellDelegate: ComposeStatusPollOptionCollectionViewCellDelegate,
|
||||||
composeStatusNewPollOptionCollectionViewCellDelegate: ComposeStatusPollOptionAppendEntryCollectionViewCellDelegate,
|
composeStatusNewPollOptionCollectionViewCellDelegate: ComposeStatusPollOptionAppendEntryCollectionViewCellDelegate,
|
||||||
|
@ -30,6 +31,7 @@ extension ComposeViewModel {
|
||||||
repliedToCellFrameSubscriber: repliedToCellFrame,
|
repliedToCellFrameSubscriber: repliedToCellFrame,
|
||||||
customEmojiPickerInputViewModel: customEmojiPickerInputViewModel,
|
customEmojiPickerInputViewModel: customEmojiPickerInputViewModel,
|
||||||
textEditorViewTextAttributesDelegate: textEditorViewTextAttributesDelegate,
|
textEditorViewTextAttributesDelegate: textEditorViewTextAttributesDelegate,
|
||||||
|
textEditorViewChangeObserver: textEditorViewChangeObserver,
|
||||||
composeStatusAttachmentTableViewCellDelegate: composeStatusAttachmentTableViewCellDelegate,
|
composeStatusAttachmentTableViewCellDelegate: composeStatusAttachmentTableViewCellDelegate,
|
||||||
composeStatusPollOptionCollectionViewCellDelegate: composeStatusPollOptionCollectionViewCellDelegate,
|
composeStatusPollOptionCollectionViewCellDelegate: composeStatusPollOptionCollectionViewCellDelegate,
|
||||||
composeStatusNewPollOptionCollectionViewCellDelegate: composeStatusNewPollOptionCollectionViewCellDelegate,
|
composeStatusNewPollOptionCollectionViewCellDelegate: composeStatusNewPollOptionCollectionViewCellDelegate,
|
||||||
|
|
|
@ -31,6 +31,8 @@ final class ComposeViewModel {
|
||||||
let activeAuthenticationBox: CurrentValueSubject<AuthenticationService.MastodonAuthenticationBox?, Never>
|
let activeAuthenticationBox: CurrentValueSubject<AuthenticationService.MastodonAuthenticationBox?, Never>
|
||||||
let traitCollectionDidChangePublisher = CurrentValueSubject<Void, Never>(Void()) // use CurrentValueSubject to make intial event emit
|
let traitCollectionDidChangePublisher = CurrentValueSubject<Void, Never>(Void()) // use CurrentValueSubject to make intial event emit
|
||||||
let repliedToCellFrame = CurrentValueSubject<CGRect, Never>(.zero)
|
let repliedToCellFrame = CurrentValueSubject<CGRect, Never>(.zero)
|
||||||
|
let autoCompleteRetryLayoutTimes = CurrentValueSubject<Int, Never>(0)
|
||||||
|
let autoCompleteInfo = CurrentValueSubject<ComposeViewController.AutoCompleteInfo?, Never>(nil)
|
||||||
|
|
||||||
// output
|
// output
|
||||||
var diffableDataSource: UICollectionViewDiffableDataSource<ComposeStatusSection, ComposeStatusItem>!
|
var diffableDataSource: UICollectionViewDiffableDataSource<ComposeStatusSection, ComposeStatusItem>!
|
||||||
|
@ -59,8 +61,8 @@ final class ComposeViewModel {
|
||||||
let characterCount = CurrentValueSubject<Int, Never>(0)
|
let characterCount = CurrentValueSubject<Int, Never>(0)
|
||||||
let collectionViewState = CurrentValueSubject<CollectionViewState, Never>(.fold)
|
let collectionViewState = CurrentValueSubject<CollectionViewState, Never>(.fold)
|
||||||
|
|
||||||
// for hashtag: #<hashag>' '
|
// for hashtag: "#<hashag> "
|
||||||
// for mention: @<mention>' '
|
// for mention: "@<mention> "
|
||||||
private(set) var preInsertedContent: String?
|
private(set) var preInsertedContent: String?
|
||||||
|
|
||||||
// custom emojis
|
// custom emojis
|
||||||
|
|
|
@ -0,0 +1,28 @@
|
||||||
|
//
|
||||||
|
// ComposeCollectionView.swift
|
||||||
|
// Mastodon
|
||||||
|
//
|
||||||
|
// Created by MainasuK Cirno on 2021-5-17.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
final class ComposeCollectionView: UICollectionView {
|
||||||
|
|
||||||
|
weak var autoCompleteViewController: AutoCompleteViewController?
|
||||||
|
|
||||||
|
// adjust hitTest for auto-complete
|
||||||
|
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
|
||||||
|
guard let autoCompleteViewController = autoCompleteViewController else {
|
||||||
|
return super.hitTest(point, with: event)
|
||||||
|
}
|
||||||
|
|
||||||
|
let thePoint = convert(point, to: autoCompleteViewController.view)
|
||||||
|
if let hitView = autoCompleteViewController.view.hitTest(thePoint, with: event) {
|
||||||
|
return hitView
|
||||||
|
} else {
|
||||||
|
return super.hitTest(point, with: event)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -30,7 +30,7 @@ extension CustomEmojiPickerInputViewModel {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func append(customEmojiReplacableTextInput textInput: CustomEmojiReplacableTextInput) {
|
func append(customEmojiReplacableTextInput textInput: CustomEmojiReplaceableTextInput) {
|
||||||
removeEmptyReferences()
|
removeEmptyReferences()
|
||||||
|
|
||||||
let isContains = customEmojiReplacableTextInputReferences.contains(where: { element in
|
let isContains = customEmojiReplacableTextInputReferences.contains(where: { element in
|
||||||
|
|
|
@ -339,3 +339,20 @@ extension HashtagTimelineViewController: StatusTableViewCellDelegate {
|
||||||
weak var playerViewControllerDelegate: AVPlayerViewControllerDelegate? { return self }
|
weak var playerViewControllerDelegate: AVPlayerViewControllerDelegate? { return self }
|
||||||
func parent() -> UIViewController { 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)
|
.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) {
|
override func viewWillAppear(_ animated: Bool) {
|
||||||
|
@ -122,14 +141,7 @@ extension NotificationViewController {
|
||||||
extension NotificationViewController {
|
extension NotificationViewController {
|
||||||
@objc private func segmentedControlValueChanged(_ sender: UISegmentedControl) {
|
@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)
|
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)!
|
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) {
|
func cacheTableView(_ tableView: UITableView, didEndDisplaying cell: UITableViewCell, forRowAt indexPath: IndexPath) {
|
||||||
guard let diffableDataSource = viewModel.diffableDataSource else { return }
|
guard let diffableDataSource = viewModel.diffableDataSource else { return }
|
||||||
guard let item = diffableDataSource.itemIdentifier(for: indexPath) else { return }
|
guard let item = diffableDataSource.itemIdentifier(for: indexPath) else { return }
|
||||||
|
@ -171,6 +191,13 @@ extension NotificationViewController: UITableViewDelegate {
|
||||||
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
|
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
|
||||||
guard let diffableDataSource = viewModel.diffableDataSource else { return }
|
guard let diffableDataSource = viewModel.diffableDataSource else { return }
|
||||||
guard let item = diffableDataSource.itemIdentifier(for: indexPath) else { return }
|
guard let item = diffableDataSource.itemIdentifier(for: indexPath) else { return }
|
||||||
|
open(item: item)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
extension NotificationViewController {
|
||||||
|
private func open(item: NotificationItem) {
|
||||||
switch item {
|
switch item {
|
||||||
case .notification(let objectID, _):
|
case .notification(let objectID, _):
|
||||||
let notification = context.managedObjectContext.object(with: objectID) as! MastodonNotification
|
let notification = context.managedObjectContext.object(with: objectID) as! MastodonNotification
|
||||||
|
@ -256,3 +283,154 @@ extension NotificationViewController: LoadMoreConfigurableTableViewContainer {
|
||||||
var loadMoreConfigurableTableView: UITableView { tableView }
|
var loadMoreConfigurableTableView: UITableView { tableView }
|
||||||
var loadMoreConfigurableStateMachine: GKStateMachine { viewModel.loadoldestStateMachine }
|
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 os.log
|
||||||
import UIKit
|
import UIKit
|
||||||
import Combine
|
import Combine
|
||||||
|
import GameController
|
||||||
|
|
||||||
final class MastodonPickServerViewController: UIViewController, NeedsDependency {
|
final class MastodonPickServerViewController: UIViewController, NeedsDependency {
|
||||||
|
|
||||||
|
@ -47,6 +48,7 @@ final class MastodonPickServerViewController: UIViewController, NeedsDependency
|
||||||
button.translatesAutoresizingMaskIntoConstraints = false
|
button.translatesAutoresizingMaskIntoConstraints = false
|
||||||
return button
|
return button
|
||||||
}()
|
}()
|
||||||
|
var nextStepButtonBottomLayoutConstraint: NSLayoutConstraint!
|
||||||
|
|
||||||
deinit {
|
deinit {
|
||||||
tableViewObservation = nil
|
tableViewObservation = nil
|
||||||
|
@ -76,11 +78,13 @@ extension MastodonPickServerViewController {
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
view.addSubview(nextStepButton)
|
view.addSubview(nextStepButton)
|
||||||
|
nextStepButtonBottomLayoutConstraint = view.bottomAnchor.constraint(equalTo: nextStepButton.bottomAnchor, constant: 0).priority(.defaultHigh)
|
||||||
NSLayoutConstraint.activate([
|
NSLayoutConstraint.activate([
|
||||||
nextStepButton.leadingAnchor.constraint(equalTo: view.readableContentGuide.leadingAnchor, constant: MastodonPickServerViewController.actionButtonMargin),
|
nextStepButton.leadingAnchor.constraint(equalTo: view.readableContentGuide.leadingAnchor, constant: MastodonPickServerViewController.actionButtonMargin),
|
||||||
view.readableContentGuide.trailingAnchor.constraint(equalTo: nextStepButton.trailingAnchor, constant: MastodonPickServerViewController.actionButtonMargin),
|
view.readableContentGuide.trailingAnchor.constraint(equalTo: nextStepButton.trailingAnchor, constant: MastodonPickServerViewController.actionButtonMargin),
|
||||||
nextStepButton.heightAnchor.constraint(equalToConstant: MastodonPickServerViewController.actionButtonHeight).priority(.defaultHigh),
|
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
|
// fix AutoLayout warning when observe before view appear
|
||||||
|
@ -124,6 +128,37 @@ extension MastodonPickServerViewController {
|
||||||
])
|
])
|
||||||
view.sendSubviewToBack(emptyStateView)
|
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 {
|
switch viewModel.mode {
|
||||||
case .signIn:
|
case .signIn:
|
||||||
nextStepButton.setTitle(L10n.Common.Controls.Actions.signIn, for: .normal)
|
nextStepButton.setTitle(L10n.Common.Controls.Actions.signIn, for: .normal)
|
||||||
|
|
|
@ -109,7 +109,7 @@ extension MastodonPickServerViewModel {
|
||||||
var serverItems: [PickServerItem] = []
|
var serverItems: [PickServerItem] = []
|
||||||
for server in indexedServers {
|
for server in indexedServers {
|
||||||
let attribute = oldSnapshotServerItemAttributeDict[server.domain] ?? PickServerItem.ServerItemAttribute(isLast: false, isExpand: false)
|
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)
|
let item = PickServerItem.server(server: server, attribute: attribute)
|
||||||
guard !serverItems.contains(item) else { continue }
|
guard !serverItems.contains(item) else { continue }
|
||||||
serverItems.append(item)
|
serverItems.append(item)
|
||||||
|
@ -119,7 +119,7 @@ extension MastodonPickServerViewModel {
|
||||||
if !unindexedServers.isEmpty {
|
if !unindexedServers.isEmpty {
|
||||||
for server in unindexedServers {
|
for server in unindexedServers {
|
||||||
let attribute = oldSnapshotServerItemAttributeDict[server.domain] ?? PickServerItem.ServerItemAttribute(isLast: false, isExpand: false)
|
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)
|
let item = PickServerItem.server(server: server, attribute: attribute)
|
||||||
guard !serverItems.contains(item) else { continue }
|
guard !serverItems.contains(item) else { continue }
|
||||||
serverItems.append(item)
|
serverItems.append(item)
|
||||||
|
@ -134,7 +134,7 @@ extension MastodonPickServerViewModel {
|
||||||
}
|
}
|
||||||
|
|
||||||
if case let .server(_, attribute) = serverItems.last {
|
if case let .server(_, attribute) = serverItems.last {
|
||||||
attribute.isLast = true
|
attribute.isLast.value = true
|
||||||
}
|
}
|
||||||
if case let .loader(attribute) = serverItems.last {
|
if case let .loader(attribute) = serverItems.last {
|
||||||
attribute.isLast = true
|
attribute.isLast = true
|
||||||
|
|
|
@ -173,3 +173,20 @@ extension FavoriteViewController: LoadMoreConfigurableTableViewContainer {
|
||||||
var loadMoreConfigurableStateMachine: GKStateMachine { return viewModel.stateMachine }
|
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
|
viewModel.isEditing
|
||||||
.handleEvents(receiveOutput: { [weak self] isEditing in
|
.handleEvents(receiveOutput: { [weak self] isEditing in
|
||||||
guard let self = self else { return }
|
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
|
// dismiss keyboard if needs
|
||||||
if !isEditing { self.view.endEditing(true) }
|
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, postDashboardMeterViewDidPressed dashboardMeterView: ProfileStatusDashboardMeterView) {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func profileHeaderView(_ profileHeaderView: ProfileHeaderView, profileStatusDashboardView: ProfileStatusDashboardView, followingDashboardMeterViewDidPressed dwingDashboardMeterView: 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) {
|
func profileHeaderView(_ profileHeaderView: ProfileHeaderView, profileStatusDashboardView: ProfileStatusDashboardView, followersDashboardMeterViewDidPressed dwersDashboardMeterView: ProfileStatusDashboardMeterView) {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -869,3 +874,26 @@ extension ProfileViewController: ProfileHeaderViewDelegate {
|
||||||
extension ProfileViewController: ScrollViewContainer {
|
extension ProfileViewController: ScrollViewContainer {
|
||||||
var scrollView: UIScrollView { return overlayScrollView }
|
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)
|
super.pageboyViewController(pageboyViewController, didScrollToPageAt: index, direction: direction, animated: animated)
|
||||||
|
|
||||||
let viewController = viewModel.viewControllers[index]
|
let viewController = viewModel.viewControllers[index]
|
||||||
|
(viewController as? StatusTableViewControllerNavigateable)?.overrideNavigationScrollPosition = .top
|
||||||
pagingDelegate?.profilePagingViewController(self, didScrollToPostCustomScrollViewContainerController: viewController, atIndex: index)
|
pagingDelegate?.profilePagingViewController(self, didScrollToPostCustomScrollViewContainerController: viewController, atIndex: index)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// make key commands works
|
||||||
|
override var canBecomeFirstResponder: Bool {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
deinit {
|
deinit {
|
||||||
os_log("%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function)
|
os_log("%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function)
|
||||||
}
|
}
|
||||||
|
@ -44,4 +50,28 @@ extension ProfilePagingViewController {
|
||||||
dataSource = viewModel
|
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 {
|
deinit {
|
||||||
os_log("%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function)
|
os_log("%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
extension ProfileSegmentedViewController {
|
extension ProfileSegmentedViewController {
|
||||||
|
|
|
@ -34,6 +34,8 @@ final class UserTimelineViewController: UIViewController, NeedsDependency, Media
|
||||||
return tableView
|
return tableView
|
||||||
}()
|
}()
|
||||||
|
|
||||||
|
var overrideNavigationScrollPosition: UITableView.ScrollPosition? = nil
|
||||||
|
|
||||||
deinit {
|
deinit {
|
||||||
os_log("%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function)
|
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 loadMoreConfigurableTableView: UITableView { return tableView }
|
||||||
var loadMoreConfigurableStateMachine: GKStateMachine { return viewModel.stateMachine }
|
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
|
#if canImport(SwiftUI) && DEBUG
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
|
|
|
@ -56,7 +56,6 @@ final class StatusTableViewCell: UITableViewCell, StatusCell {
|
||||||
var disposeBag = Set<AnyCancellable>()
|
var disposeBag = Set<AnyCancellable>()
|
||||||
var pollCountdownSubscription: AnyCancellable?
|
var pollCountdownSubscription: AnyCancellable?
|
||||||
var observations = Set<NSKeyValueObservation>()
|
var observations = Set<NSKeyValueObservation>()
|
||||||
private var selectionBackgroundViewObservation: NSKeyValueObservation?
|
|
||||||
|
|
||||||
let statusView = StatusView()
|
let statusView = StatusView()
|
||||||
let threadMetaStackView = UIStackView()
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -33,6 +33,7 @@ extension EmojiService {
|
||||||
}()
|
}()
|
||||||
let emojis = CurrentValueSubject<[Mastodon.Entity.Emoji], Never>([])
|
let emojis = CurrentValueSubject<[Mastodon.Entity.Emoji], Never>([])
|
||||||
let emojiDict = CurrentValueSubject<[String: [Mastodon.Entity.Emoji]], Never>([:])
|
let emojiDict = CurrentValueSubject<[String: [Mastodon.Entity.Emoji]], Never>([:])
|
||||||
|
let emojiTrie = CurrentValueSubject<Trie<Character>?, Never>(nil)
|
||||||
|
|
||||||
private var learnedEmoji: Set<String> = Set()
|
private var learnedEmoji: Set<String> = Set()
|
||||||
|
|
||||||
|
@ -44,6 +45,19 @@ extension EmojiService {
|
||||||
.map { Dictionary(grouping: $0, by: { $0.shortcode }) }
|
.map { Dictionary(grouping: $0, by: { $0.shortcode }) }
|
||||||
.assign(to: \.value, on: emojiDict)
|
.assign(to: \.value, on: emojiDict)
|
||||||
.store(in: &disposeBag)
|
.store(in: &disposeBag)
|
||||||
|
|
||||||
|
emojis
|
||||||
|
.map { emojis -> Trie<Character>? in
|
||||||
|
guard !emojis.isEmpty else { return nil }
|
||||||
|
var trie: Trie<Character> = Trie()
|
||||||
|
for emoji in emojis {
|
||||||
|
let key = emoji.shortcode.lowercased()
|
||||||
|
trie.inserted(Array(key).slice, value: emoji)
|
||||||
|
}
|
||||||
|
return trie
|
||||||
|
}
|
||||||
|
.assign(to: \.value, on: emojiTrie)
|
||||||
|
.store(in: &disposeBag)
|
||||||
}
|
}
|
||||||
|
|
||||||
func emoji(shortcode: String) -> Mastodon.Entity.Emoji? {
|
func emoji(shortcode: String) -> Mastodon.Entity.Emoji? {
|
||||||
|
|
|
@ -0,0 +1,115 @@
|
||||||
|
//
|
||||||
|
// AutoCompleteViewModel+Trie.swift
|
||||||
|
// Mastodon
|
||||||
|
//
|
||||||
|
// Created by MainasuK Cirno on 2021-5-18.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
struct Trie<Element: Hashable> {
|
||||||
|
let isElement: Bool
|
||||||
|
let valueSet: NSMutableSet
|
||||||
|
var children: [Element: Trie<Element>]
|
||||||
|
}
|
||||||
|
|
||||||
|
extension Trie {
|
||||||
|
init() {
|
||||||
|
isElement = false
|
||||||
|
valueSet = NSMutableSet()
|
||||||
|
children = [:]
|
||||||
|
}
|
||||||
|
|
||||||
|
init(_ key: ArraySlice<Element>, value: Any) {
|
||||||
|
if let (head, tail) = key.decomposed {
|
||||||
|
let children = [head: Trie(tail, value: value)]
|
||||||
|
self = Trie(isElement: false, valueSet: NSMutableSet(), children: children)
|
||||||
|
} else {
|
||||||
|
self = Trie(isElement: true, valueSet: NSMutableSet(object: value), children: [:])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension Trie {
|
||||||
|
var elements: [[Element]] {
|
||||||
|
var result: [[Element]] = isElement ? [[]] : []
|
||||||
|
for (key, value) in children {
|
||||||
|
result += value.elements.map { [key] + $0 }
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//extension Array {
|
||||||
|
// var slice: ArraySlice<Element> {
|
||||||
|
// return ArraySlice(self)
|
||||||
|
// }
|
||||||
|
//}
|
||||||
|
|
||||||
|
extension ArraySlice {
|
||||||
|
var decomposed: (Element, ArraySlice<Element>)? {
|
||||||
|
return isEmpty ? nil : (self[startIndex], self.dropFirst())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension Trie {
|
||||||
|
func lookup(key: ArraySlice<Element>) -> Bool {
|
||||||
|
guard let (head, tail) = key.decomposed else { return isElement }
|
||||||
|
guard let subtrie = children[head] else { return false }
|
||||||
|
return subtrie.lookup(key: tail)
|
||||||
|
}
|
||||||
|
|
||||||
|
func lookup(key: ArraySlice<Element>) -> Trie<Element>? {
|
||||||
|
guard let (head, tail) = key.decomposed else { return self }
|
||||||
|
guard let remainder = children[head] else { return nil }
|
||||||
|
return remainder.lookup(key: tail)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension Trie {
|
||||||
|
func complete(key: ArraySlice<Element>) -> [[Element]] {
|
||||||
|
return lookup(key: key)?.elements ?? []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension Trie {
|
||||||
|
mutating func inserted(_ key: ArraySlice<Element>, value: Any) {
|
||||||
|
guard let (head, tail) = key.decomposed else {
|
||||||
|
self.valueSet.add(value)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if var nextTrie = children[head] {
|
||||||
|
nextTrie.inserted(tail, value: value)
|
||||||
|
children[head] = nextTrie
|
||||||
|
} else {
|
||||||
|
children[head] = Trie(tail, value: value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension Trie {
|
||||||
|
func passthrough(_ key: ArraySlice<Element>) -> [Trie<Element>] {
|
||||||
|
guard let (head, tail) = key.decomposed else {
|
||||||
|
return [self]
|
||||||
|
}
|
||||||
|
|
||||||
|
let passthroughed = children[head]?.passthrough(tail) ?? []
|
||||||
|
if isElement {
|
||||||
|
return passthroughed + [self]
|
||||||
|
} else {
|
||||||
|
return passthroughed
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var values: NSSet {
|
||||||
|
let valueSet = NSMutableSet(set: self.valueSet)
|
||||||
|
for (key, value) in children {
|
||||||
|
valueSet.addObjects(from: Array(value.values))
|
||||||
|
}
|
||||||
|
|
||||||
|
return valueSet
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
|
@ -73,3 +73,18 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#if DEBUG
|
||||||
|
class TestWindow: UIWindow {
|
||||||
|
|
||||||
|
override func sendEvent(_ event: UIEvent) {
|
||||||
|
event.allTouches?.forEach({ (touch) in
|
||||||
|
let location = touch.location(in: self)
|
||||||
|
let view = hitTest(location, with: event)
|
||||||
|
print(view.debugDescription)
|
||||||
|
})
|
||||||
|
|
||||||
|
super.sendEvent(event)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
|
|
@ -17,9 +17,9 @@
|
||||||
<key>CFBundlePackageType</key>
|
<key>CFBundlePackageType</key>
|
||||||
<string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string>
|
<string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string>
|
||||||
<key>CFBundleShortVersionString</key>
|
<key>CFBundleShortVersionString</key>
|
||||||
<string>1.0</string>
|
<string>$(MARKETING_VERSION)</string>
|
||||||
<key>CFBundleVersion</key>
|
<key>CFBundleVersion</key>
|
||||||
<string>1</string>
|
<string>$(CURRENT_PROJECT_VERSION)</string>
|
||||||
<key>NSExtension</key>
|
<key>NSExtension</key>
|
||||||
<dict>
|
<dict>
|
||||||
<key>NSExtensionPointIdentifier</key>
|
<key>NSExtensionPointIdentifier</key>
|
||||||
|
|
Loading…
Reference in New Issue