Compare commits
No commits in common. "426b31d46cdd12b53292f42fada59b52f3b1ee8f" and "b560bcd8dc3522b3ccf1d44662b5c332849f0aa7" have entirely different histories.
426b31d46c
...
b560bcd8dc
|
@ -420,10 +420,6 @@ extension Client {
|
|||
public let requestEndpoint: Endpoint
|
||||
public let type: ErrorType
|
||||
|
||||
#if DEBUG
|
||||
public static let debug = Error(request: Client.getStatuses(timeline: .home), type: .invalidResponse)
|
||||
#endif
|
||||
|
||||
init<ResultType: Decodable>(request: Request<ResultType>, type: ErrorType) {
|
||||
self.requestMethod = request.method
|
||||
self.requestEndpoint = request.endpoint
|
||||
|
|
|
@ -33,8 +33,6 @@
|
|||
D6114E1727F8BB210080E273 /* VersionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6114E1627F8BB210080E273 /* VersionTests.swift */; };
|
||||
D611C2CF232DC61100C86A49 /* HashtagTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D611C2CD232DC61100C86A49 /* HashtagTableViewCell.swift */; };
|
||||
D611C2D0232DC61100C86A49 /* HashtagTableViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = D611C2CE232DC61100C86A49 /* HashtagTableViewCell.xib */; };
|
||||
D61A45E628DC0F2F002BE511 /* ConfirmLoadMoreCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D61A45E528DC0F2F002BE511 /* ConfirmLoadMoreCollectionViewCell.swift */; };
|
||||
D61A45E828DF477D002BE511 /* LoadingCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D61A45E728DF477D002BE511 /* LoadingCollectionViewCell.swift */; };
|
||||
D61AC1D5232E9FA600C54D2D /* InstanceSelectorTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D61AC1D4232E9FA600C54D2D /* InstanceSelectorTableViewController.swift */; };
|
||||
D61AC1D8232EA42D00C54D2D /* InstanceTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D61AC1D6232EA42D00C54D2D /* InstanceTableViewCell.swift */; };
|
||||
D61AC1D9232EA42D00C54D2D /* InstanceTableViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = D61AC1D7232EA42D00C54D2D /* InstanceTableViewCell.xib */; };
|
||||
|
@ -100,6 +98,7 @@
|
|||
D641C77F213DC78A004B4513 /* InlineTextAttachment.swift in Sources */ = {isa = PBXBuildFile; fileRef = D641C77E213DC78A004B4513 /* InlineTextAttachment.swift */; };
|
||||
D6420AEE26BED18B00ED8175 /* PublicTimelineDescriptionTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6420AEC26BED18B00ED8175 /* PublicTimelineDescriptionTableViewCell.swift */; };
|
||||
D6420AEF26BED18B00ED8175 /* PublicTimelineDescriptionTableViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = D6420AED26BED18B00ED8175 /* PublicTimelineDescriptionTableViewCell.xib */; };
|
||||
D6434EB3215B1856001A919A /* XCBRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6434EB2215B1856001A919A /* XCBRequest.swift */; };
|
||||
D646C956213B365700269FB5 /* LargeImageExpandAnimationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D646C955213B365700269FB5 /* LargeImageExpandAnimationController.swift */; };
|
||||
D646C958213B367000269FB5 /* LargeImageShrinkAnimationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D646C957213B367000269FB5 /* LargeImageShrinkAnimationController.swift */; };
|
||||
D646C95A213B5D0500269FB5 /* LargeImageInteractionController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D646C959213B5D0500269FB5 /* LargeImageInteractionController.swift */; };
|
||||
|
@ -116,6 +115,7 @@
|
|||
D64D0AAD2128D88B005A6F37 /* LocalData.swift in Sources */ = {isa = PBXBuildFile; fileRef = D64D0AAC2128D88B005A6F37 /* LocalData.swift */; };
|
||||
D64D0AB12128D9AE005A6F37 /* OnboardingViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D64D0AB02128D9AE005A6F37 /* OnboardingViewController.swift */; };
|
||||
D64D8CA92463B494006B0BAA /* MultiThreadDictionary.swift in Sources */ = {isa = PBXBuildFile; fileRef = D64D8CA82463B494006B0BAA /* MultiThreadDictionary.swift */; };
|
||||
D64F80E2215875CC00BEF393 /* XCBActionType.swift in Sources */ = {isa = PBXBuildFile; fileRef = D64F80E1215875CC00BEF393 /* XCBActionType.swift */; };
|
||||
D65234D325618EFA001AF9CF /* TimelineTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D65234D225618EFA001AF9CF /* TimelineTableViewController.swift */; };
|
||||
D65234E12561AA68001AF9CF /* NotificationsTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D65234E02561AA68001AF9CF /* NotificationsTableViewController.swift */; };
|
||||
D6531DEE246B81C9000F9538 /* GifvAttachmentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6531DED246B81C9000F9538 /* GifvAttachmentView.swift */; };
|
||||
|
@ -142,14 +142,17 @@
|
|||
D667E5F52135BCD50057A976 /* ConversationTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D667E5F42135BCD50057A976 /* ConversationTableViewController.swift */; };
|
||||
D667E5F82135C3040057A976 /* Mastodon+Equatable.swift in Sources */ = {isa = PBXBuildFile; fileRef = D667E5F72135C3040057A976 /* Mastodon+Equatable.swift */; };
|
||||
D66A77BB233838DC0058F1EC /* UIFont+Traits.swift in Sources */ = {isa = PBXBuildFile; fileRef = D66A77BA233838DC0058F1EC /* UIFont+Traits.swift */; };
|
||||
D66C900B28DAB7FD00217BF2 /* TimelineViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D66C900A28DAB7FD00217BF2 /* TimelineViewController.swift */; };
|
||||
D674A50927F9128D00BA03AC /* Pachyderm in Frameworks */ = {isa = PBXBuildFile; productRef = D674A50827F9128D00BA03AC /* Pachyderm */; };
|
||||
D6757A7C2157E01900721E32 /* XCBManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6757A7B2157E01900721E32 /* XCBManager.swift */; };
|
||||
D6757A7E2157E02600721E32 /* XCBRequestSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6757A7D2157E02600721E32 /* XCBRequestSpec.swift */; };
|
||||
D6757A822157E8FA00721E32 /* XCBSession.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6757A812157E8FA00721E32 /* XCBSession.swift */; };
|
||||
D677284824ECBCB100C732D3 /* ComposeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D677284724ECBCB100C732D3 /* ComposeView.swift */; };
|
||||
D677284A24ECBDF400C732D3 /* ComposeCurrentAccount.swift in Sources */ = {isa = PBXBuildFile; fileRef = D677284924ECBDF400C732D3 /* ComposeCurrentAccount.swift */; };
|
||||
D677284C24ECBE9100C732D3 /* ComposeAvatarImageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D677284B24ECBE9100C732D3 /* ComposeAvatarImageView.swift */; };
|
||||
D677284E24ECC01D00C732D3 /* Draft.swift in Sources */ = {isa = PBXBuildFile; fileRef = D677284D24ECC01D00C732D3 /* Draft.swift */; };
|
||||
D67895BC24671E6D00D4CD9E /* PKDrawing+Render.swift in Sources */ = {isa = PBXBuildFile; fileRef = D67895BB24671E6D00D4CD9E /* PKDrawing+Render.swift */; };
|
||||
D67895C0246870DE00D4CD9E /* LocalAccountAvatarView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D67895BF246870DE00D4CD9E /* LocalAccountAvatarView.swift */; };
|
||||
D679C09F215850EF00DA27FE /* XCBActions.swift in Sources */ = {isa = PBXBuildFile; fileRef = D679C09E215850EF00DA27FE /* XCBActions.swift */; };
|
||||
D67B506D250B291200FAECFB /* BlurHashDecode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D67B506C250B291200FAECFB /* BlurHashDecode.swift */; };
|
||||
D67C1795266D57D10070F250 /* FastAccountSwitcherIndicatorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D67C1794266D57D10070F250 /* FastAccountSwitcherIndicatorView.swift */; };
|
||||
D67C57AD21E265FC00C3118B /* LargeAccountDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D67C57AC21E265FC00C3118B /* LargeAccountDetailView.swift */; };
|
||||
|
@ -165,7 +168,6 @@
|
|||
D686BBE324FBF8110068E6AA /* WrappedProgressView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D686BBE224FBF8110068E6AA /* WrappedProgressView.swift */; };
|
||||
D6895DC228D65274006341DA /* CustomAlertController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6895DC128D65274006341DA /* CustomAlertController.swift */; };
|
||||
D6895DC428D65342006341DA /* ConfirmReblogStatusPreviewView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6895DC328D65342006341DA /* ConfirmReblogStatusPreviewView.swift */; };
|
||||
D6895DE928D962C2006341DA /* TimelineLikeController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6895DE828D962C2006341DA /* TimelineLikeController.swift */; };
|
||||
D68ACE5D279B1ABA001CE8EB /* AssetPickerControlCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D68ACE5C279B1ABA001CE8EB /* AssetPickerControlCollectionViewCell.swift */; };
|
||||
D68C2AE325869BAB00548EFF /* AuxiliarySceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = D68C2AE225869BAB00548EFF /* AuxiliarySceneDelegate.swift */; };
|
||||
D68E525B24A3D77E0054355A /* TuskerRootViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D68E525A24A3D77E0054355A /* TuskerRootViewController.swift */; };
|
||||
|
@ -378,8 +380,6 @@
|
|||
D6114E1627F8BB210080E273 /* VersionTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VersionTests.swift; sourceTree = "<group>"; };
|
||||
D611C2CD232DC61100C86A49 /* HashtagTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HashtagTableViewCell.swift; sourceTree = "<group>"; };
|
||||
D611C2CE232DC61100C86A49 /* HashtagTableViewCell.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = HashtagTableViewCell.xib; sourceTree = "<group>"; };
|
||||
D61A45E528DC0F2F002BE511 /* ConfirmLoadMoreCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConfirmLoadMoreCollectionViewCell.swift; sourceTree = "<group>"; };
|
||||
D61A45E728DF477D002BE511 /* LoadingCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoadingCollectionViewCell.swift; sourceTree = "<group>"; };
|
||||
D61AC1D4232E9FA600C54D2D /* InstanceSelectorTableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InstanceSelectorTableViewController.swift; sourceTree = "<group>"; };
|
||||
D61AC1D6232EA42D00C54D2D /* InstanceTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InstanceTableViewCell.swift; sourceTree = "<group>"; };
|
||||
D61AC1D7232EA42D00C54D2D /* InstanceTableViewCell.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = InstanceTableViewCell.xib; sourceTree = "<group>"; };
|
||||
|
@ -445,6 +445,7 @@
|
|||
D641C77E213DC78A004B4513 /* InlineTextAttachment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InlineTextAttachment.swift; sourceTree = "<group>"; };
|
||||
D6420AEC26BED18B00ED8175 /* PublicTimelineDescriptionTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PublicTimelineDescriptionTableViewCell.swift; sourceTree = "<group>"; };
|
||||
D6420AED26BED18B00ED8175 /* PublicTimelineDescriptionTableViewCell.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = PublicTimelineDescriptionTableViewCell.xib; sourceTree = "<group>"; };
|
||||
D6434EB2215B1856001A919A /* XCBRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XCBRequest.swift; sourceTree = "<group>"; };
|
||||
D646C955213B365700269FB5 /* LargeImageExpandAnimationController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LargeImageExpandAnimationController.swift; sourceTree = "<group>"; };
|
||||
D646C957213B367000269FB5 /* LargeImageShrinkAnimationController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LargeImageShrinkAnimationController.swift; sourceTree = "<group>"; };
|
||||
D646C959213B5D0500269FB5 /* LargeImageInteractionController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LargeImageInteractionController.swift; sourceTree = "<group>"; };
|
||||
|
@ -461,6 +462,7 @@
|
|||
D64D0AAC2128D88B005A6F37 /* LocalData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalData.swift; sourceTree = "<group>"; };
|
||||
D64D0AB02128D9AE005A6F37 /* OnboardingViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingViewController.swift; sourceTree = "<group>"; };
|
||||
D64D8CA82463B494006B0BAA /* MultiThreadDictionary.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MultiThreadDictionary.swift; sourceTree = "<group>"; };
|
||||
D64F80E1215875CC00BEF393 /* XCBActionType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XCBActionType.swift; sourceTree = "<group>"; };
|
||||
D65234D225618EFA001AF9CF /* TimelineTableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineTableViewController.swift; sourceTree = "<group>"; };
|
||||
D65234E02561AA68001AF9CF /* NotificationsTableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationsTableViewController.swift; sourceTree = "<group>"; };
|
||||
D6531DED246B81C9000F9538 /* GifvAttachmentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GifvAttachmentView.swift; sourceTree = "<group>"; };
|
||||
|
@ -489,14 +491,17 @@
|
|||
D667E5F42135BCD50057A976 /* ConversationTableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConversationTableViewController.swift; sourceTree = "<group>"; };
|
||||
D667E5F72135C3040057A976 /* Mastodon+Equatable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Mastodon+Equatable.swift"; sourceTree = "<group>"; };
|
||||
D66A77BA233838DC0058F1EC /* UIFont+Traits.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIFont+Traits.swift"; sourceTree = "<group>"; };
|
||||
D66C900A28DAB7FD00217BF2 /* TimelineViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineViewController.swift; sourceTree = "<group>"; };
|
||||
D674A50727F910F300BA03AC /* Pachyderm */ = {isa = PBXFileReference; lastKnownFileType = wrapper; path = Pachyderm; sourceTree = "<group>"; };
|
||||
D6757A7B2157E01900721E32 /* XCBManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XCBManager.swift; sourceTree = "<group>"; };
|
||||
D6757A7D2157E02600721E32 /* XCBRequestSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XCBRequestSpec.swift; sourceTree = "<group>"; };
|
||||
D6757A812157E8FA00721E32 /* XCBSession.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XCBSession.swift; sourceTree = "<group>"; };
|
||||
D677284724ECBCB100C732D3 /* ComposeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeView.swift; sourceTree = "<group>"; };
|
||||
D677284924ECBDF400C732D3 /* ComposeCurrentAccount.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeCurrentAccount.swift; sourceTree = "<group>"; };
|
||||
D677284B24ECBE9100C732D3 /* ComposeAvatarImageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeAvatarImageView.swift; sourceTree = "<group>"; };
|
||||
D677284D24ECC01D00C732D3 /* Draft.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Draft.swift; sourceTree = "<group>"; };
|
||||
D67895BB24671E6D00D4CD9E /* PKDrawing+Render.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "PKDrawing+Render.swift"; sourceTree = "<group>"; };
|
||||
D67895BF246870DE00D4CD9E /* LocalAccountAvatarView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalAccountAvatarView.swift; sourceTree = "<group>"; };
|
||||
D679C09E215850EF00DA27FE /* XCBActions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XCBActions.swift; sourceTree = "<group>"; };
|
||||
D67B506C250B291200FAECFB /* BlurHashDecode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlurHashDecode.swift; sourceTree = "<group>"; };
|
||||
D67C1794266D57D10070F250 /* FastAccountSwitcherIndicatorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FastAccountSwitcherIndicatorView.swift; sourceTree = "<group>"; };
|
||||
D67C57AC21E265FC00C3118B /* LargeAccountDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LargeAccountDetailView.swift; sourceTree = "<group>"; };
|
||||
|
@ -512,7 +517,6 @@
|
|||
D686BBE224FBF8110068E6AA /* WrappedProgressView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WrappedProgressView.swift; sourceTree = "<group>"; };
|
||||
D6895DC128D65274006341DA /* CustomAlertController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomAlertController.swift; sourceTree = "<group>"; };
|
||||
D6895DC328D65342006341DA /* ConfirmReblogStatusPreviewView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConfirmReblogStatusPreviewView.swift; sourceTree = "<group>"; };
|
||||
D6895DE828D962C2006341DA /* TimelineLikeController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineLikeController.swift; sourceTree = "<group>"; };
|
||||
D68ACE5C279B1ABA001CE8EB /* AssetPickerControlCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AssetPickerControlCollectionViewCell.swift; sourceTree = "<group>"; };
|
||||
D68C2AE225869BAB00548EFF /* AuxiliarySceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuxiliarySceneDelegate.swift; sourceTree = "<group>"; };
|
||||
D68E525A24A3D77E0054355A /* TuskerRootViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TuskerRootViewController.swift; sourceTree = "<group>"; };
|
||||
|
@ -890,11 +894,10 @@
|
|||
D641C781213DD7DD004B4513 /* Timeline */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
D6945C3123AC4D36005C403C /* HashtagTimelineViewController.swift */,
|
||||
D6945C3723AC739F005C403C /* InstanceTimelineViewController.swift */,
|
||||
D6BC9DD6232D7811002CA326 /* TimelinesPageViewController.swift */,
|
||||
D65234D225618EFA001AF9CF /* TimelineTableViewController.swift */,
|
||||
D66C900A28DAB7FD00217BF2 /* TimelineViewController.swift */,
|
||||
D6945C3123AC4D36005C403C /* HashtagTimelineViewController.swift */,
|
||||
D6945C3723AC739F005C403C /* InstanceTimelineViewController.swift */,
|
||||
);
|
||||
path = Timeline;
|
||||
sourceTree = "<group>";
|
||||
|
@ -1122,6 +1125,19 @@
|
|||
path = Extensions;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
D6757A7A2157E00100721E32 /* XCallbackURL */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
D6757A7B2157E01900721E32 /* XCBManager.swift */,
|
||||
D6757A7D2157E02600721E32 /* XCBRequestSpec.swift */,
|
||||
D6434EB2215B1856001A919A /* XCBRequest.swift */,
|
||||
D6757A812157E8FA00721E32 /* XCBSession.swift */,
|
||||
D64F80E1215875CC00BEF393 /* XCBActionType.swift */,
|
||||
D679C09E215850EF00DA27FE /* XCBActions.swift */,
|
||||
);
|
||||
path = XCallbackURL;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
D67B506B250B28FF00FAECFB /* Vendor */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
|
@ -1250,7 +1266,6 @@
|
|||
D6DD2A44273D6C5700386A6C /* GIFImageView.swift */,
|
||||
D641C77E213DC78A004B4513 /* InlineTextAttachment.swift */,
|
||||
D620483323D3801D008A63EF /* LinkTextView.swift */,
|
||||
D61A45E728DF477D002BE511 /* LoadingCollectionViewCell.swift */,
|
||||
D6262C9928D01C4B00390C1F /* LoadingTableViewCell.swift */,
|
||||
D68E6F58253C9969001A1B4C /* MultiSourceEmojiLabel.swift */,
|
||||
D627943A23A55BA600D38C68 /* NavigableTableViewCell.swift */,
|
||||
|
@ -1354,17 +1369,16 @@
|
|||
D60E2F2B24423EAD005F8713 /* LazilyDecoding.swift */,
|
||||
D64D0AAC2128D88B005A6F37 /* LocalData.swift */,
|
||||
D6AC956623C4347E008C9946 /* MainSceneDelegate.swift */,
|
||||
D6B81F432560390300F6E31D /* MenuController.swift */,
|
||||
D64D8CA82463B494006B0BAA /* MultiThreadDictionary.swift */,
|
||||
D6945C2E23AC47C3005C403C /* SavedDataManager.swift */,
|
||||
D6895DE828D962C2006341DA /* TimelineLikeController.swift */,
|
||||
D6C693EE216192C2007D6A6D /* TuskerNavigationDelegate.swift */,
|
||||
D6DFC69F242C4CCC00ACC392 /* WeakArray.swift */,
|
||||
D62E9988279DB2D100C26176 /* InstanceFeatures.swift */,
|
||||
D63D8DF32850FE7A008D95E1 /* ViewTags.swift */,
|
||||
D6D4DDD6212518A200E1C4BB /* Assets.xcassets */,
|
||||
D6AEBB3F2321640F00E5038B /* Activities */,
|
||||
D6F953F121251A2F00CF0F2B /* API */,
|
||||
D6F1F84E2193B9BE00F5FE67 /* Caching */,
|
||||
D6F953F121251A2F00CF0F2B /* Controllers */,
|
||||
D6370B9924421FE00092A7FF /* CoreData */,
|
||||
D667E5F62135C2ED0057A976 /* Extensions */,
|
||||
D6D4DDD8212518A200E1C4BB /* LaunchScreen.storyboard */,
|
||||
|
@ -1372,9 +1386,11 @@
|
|||
D61959D2241E846D00A37B8E /* Models */,
|
||||
D663626021360A9600C9CBA2 /* Preferences */,
|
||||
D641C780213DD7C4004B4513 /* Screens */,
|
||||
D6E9CDA6281A426700BBC98E /* Services */,
|
||||
D62D241E217AA46B005076CC /* Shortcuts */,
|
||||
D67B506B250B28FF00FAECFB /* Vendor */,
|
||||
D6BED1722126661300F02DA0 /* Views */,
|
||||
D6757A7A2157E00100721E32 /* XCallbackURL */,
|
||||
);
|
||||
path = Tusker;
|
||||
sourceTree = "<group>";
|
||||
|
@ -1410,7 +1426,6 @@
|
|||
children = (
|
||||
D6DEA0DC268400C300FE896A /* ConfirmLoadMoreTableViewCell.swift */,
|
||||
D6DEA0DD268400C300FE896A /* ConfirmLoadMoreTableViewCell.xib */,
|
||||
D61A45E528DC0F2F002BE511 /* ConfirmLoadMoreCollectionViewCell.swift */,
|
||||
);
|
||||
path = "Confirm Load More Cell";
|
||||
sourceTree = "<group>";
|
||||
|
@ -1428,6 +1443,14 @@
|
|||
path = OpenInTusker;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
D6E9CDA6281A426700BBC98E /* Services */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
D6E9CDA7281A427800BBC98E /* PostService.swift */,
|
||||
);
|
||||
path = Services;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
D6F1F84E2193B9BE00F5FE67 /* Caching */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
|
@ -1450,14 +1473,13 @@
|
|||
path = "Crash Reporter";
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
D6F953F121251A2F00CF0F2B /* API */ = {
|
||||
D6F953F121251A2F00CF0F2B /* Controllers */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
D62E9988279DB2D100C26176 /* InstanceFeatures.swift */,
|
||||
D6F953EF21251A2900CF0F2B /* MastodonController.swift */,
|
||||
D6E9CDA7281A427800BBC98E /* PostService.swift */,
|
||||
D6B81F432560390300F6E31D /* MenuController.swift */,
|
||||
);
|
||||
path = API;
|
||||
path = Controllers;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
/* End PBXGroup section */
|
||||
|
@ -1729,6 +1751,7 @@
|
|||
D62275A624F1C81800B82A16 /* ComposeReplyView.swift in Sources */,
|
||||
D60E2F292442372B005F8713 /* AccountMO.swift in Sources */,
|
||||
D6412B0324AFF6A600F5412E /* TabBarScrollableViewController.swift in Sources */,
|
||||
D6757A822157E8FA00721E32 /* XCBSession.swift in Sources */,
|
||||
D6093FB725BE0CF3004811E6 /* TrendHistoryView.swift in Sources */,
|
||||
D6DEA0DE268400C300FE896A /* ConfirmLoadMoreTableViewCell.swift in Sources */,
|
||||
D6EBF01723C55E0D00AE061B /* UISceneSession+MastodonController.swift in Sources */,
|
||||
|
@ -1802,7 +1825,6 @@
|
|||
D6B4A4FF2506B81A000C81C1 /* AccountDisplayNameLabel.swift in Sources */,
|
||||
D63D8DF42850FE7A008D95E1 /* ViewTags.swift in Sources */,
|
||||
D6B053A623BD2D0C00A066FA /* AssetCollectionViewController.swift in Sources */,
|
||||
D61A45E828DF477D002BE511 /* LoadingCollectionViewCell.swift in Sources */,
|
||||
D67B506D250B291200FAECFB /* BlurHashDecode.swift in Sources */,
|
||||
D62275A024F1677200B82A16 /* ComposeHostingController.swift in Sources */,
|
||||
04DACE8E212CC7CC009840C4 /* ImageCache.swift in Sources */,
|
||||
|
@ -1827,6 +1849,7 @@
|
|||
D663626421360D2300C9CBA2 /* AvatarStyle.swift in Sources */,
|
||||
D6D3FDE024F41B8400FF50A5 /* ComposeContainerView.swift in Sources */,
|
||||
D693A72A25CF8C1E003A14E2 /* ProfileDirectoryViewController.swift in Sources */,
|
||||
D679C09F215850EF00DA27FE /* XCBActions.swift in Sources */,
|
||||
D693A72F25CF91C6003A14E2 /* FeaturedProfileCollectionViewCell.swift in Sources */,
|
||||
D627943223A5466600D38C68 /* SelectableTableViewCell.swift in Sources */,
|
||||
D6DD353F22F502EC00A9563A /* Preferences+Notification.swift in Sources */,
|
||||
|
@ -1850,7 +1873,6 @@
|
|||
D62E9985279CA23900C26176 /* URLSession+Development.swift in Sources */,
|
||||
D622757424EDF1CD00B82A16 /* ComposeAttachmentsList.swift in Sources */,
|
||||
D64AAE9526C88C5000FC57FB /* ToastableViewController.swift in Sources */,
|
||||
D6895DE928D962C2006341DA /* TimelineLikeController.swift in Sources */,
|
||||
D6420AEE26BED18B00ED8175 /* PublicTimelineDescriptionTableViewCell.swift in Sources */,
|
||||
D6E0DC8E216EDF1E00369478 /* Previewing.swift in Sources */,
|
||||
D6B93667281D937300237D0E /* MainSidebarMyProfileCollectionViewCell.swift in Sources */,
|
||||
|
@ -1868,6 +1890,7 @@
|
|||
D6C94D872139E62700CB5196 /* LargeImageViewController.swift in Sources */,
|
||||
D6B053AB23BD2F1400A066FA /* AssetCollectionViewCell.swift in Sources */,
|
||||
D622757A24EE21D900B82A16 /* ComposeAttachmentRow.swift in Sources */,
|
||||
D6434EB3215B1856001A919A /* XCBRequest.swift in Sources */,
|
||||
D6E4269D2532A3E100C02E1C /* FuzzyMatcher.swift in Sources */,
|
||||
D681E4D3246E2AFF0053414F /* MuteConversationActivity.swift in Sources */,
|
||||
D6969EA0240C8384002843CE /* EmojiLabel.swift in Sources */,
|
||||
|
@ -1883,7 +1906,6 @@
|
|||
D6412B0D24B0D4CF00F5412E /* ProfileHeaderView.swift in Sources */,
|
||||
D641C77F213DC78A004B4513 /* InlineTextAttachment.swift in Sources */,
|
||||
D627943523A5525100D38C68 /* StatusActivity.swift in Sources */,
|
||||
D61A45E628DC0F2F002BE511 /* ConfirmLoadMoreCollectionViewCell.swift in Sources */,
|
||||
D663626C21361C6700C9CBA2 /* Account+Preferences.swift in Sources */,
|
||||
D67895C0246870DE00D4CD9E /* LocalAccountAvatarView.swift in Sources */,
|
||||
D6333B372137838300CE884A /* AttributedString+Helpers.swift in Sources */,
|
||||
|
@ -1897,12 +1919,12 @@
|
|||
D6114E1327F89B440080E273 /* TrendingLinkTableViewCell.swift in Sources */,
|
||||
D6262C9A28D01C4B00390C1F /* LoadingTableViewCell.swift in Sources */,
|
||||
D68C2AE325869BAB00548EFF /* AuxiliarySceneDelegate.swift in Sources */,
|
||||
D66C900B28DAB7FD00217BF2 /* TimelineViewController.swift in Sources */,
|
||||
D6AEBB432321685E00E5038B /* OpenInSafariActivity.swift in Sources */,
|
||||
D6C693FE2162FEEA007D6A6D /* UIViewController+Children.swift in Sources */,
|
||||
D64D0AB12128D9AE005A6F37 /* OnboardingViewController.swift in Sources */,
|
||||
D67C1795266D57D10070F250 /* FastAccountSwitcherIndicatorView.swift in Sources */,
|
||||
D627FF76217E923E00CC0648 /* DraftsManager.swift in Sources */,
|
||||
D64F80E2215875CC00BEF393 /* XCBActionType.swift in Sources */,
|
||||
04586B4322B301470021BD04 /* AppearancePrefsView.swift in Sources */,
|
||||
D6FF9860255C717400845181 /* AccountSwitchingContainerViewController.swift in Sources */,
|
||||
D6E77D0B286D426E00D8B732 /* TrendingLinkCardCollectionViewCell.swift in Sources */,
|
||||
|
@ -1936,6 +1958,7 @@
|
|||
D6412B0524B0227D00F5412E /* ProfileViewController.swift in Sources */,
|
||||
D64BC18A23C16487000D0238 /* UnpinStatusActivity.swift in Sources */,
|
||||
D64D8CA92463B494006B0BAA /* MultiThreadDictionary.swift in Sources */,
|
||||
D6757A7C2157E01900721E32 /* XCBManager.swift in Sources */,
|
||||
D65234D325618EFA001AF9CF /* TimelineTableViewController.swift in Sources */,
|
||||
D68E6F5F253C9B2D001A1B4C /* BaseEmojiLabel.swift in Sources */,
|
||||
D6F0B12B24A3071C001E48C3 /* MainSplitViewController.swift in Sources */,
|
||||
|
@ -1949,6 +1972,7 @@
|
|||
D6093F9B25BDD4B9004811E6 /* HashtagSearchResultsViewController.swift in Sources */,
|
||||
D6E4267725327FB400C02E1C /* ComposeAutocompleteView.swift in Sources */,
|
||||
D6E77D09286D25FA00D8B732 /* TrendingHashtagCollectionViewCell.swift in Sources */,
|
||||
D6757A7E2157E02600721E32 /* XCBRequestSpec.swift in Sources */,
|
||||
D677284E24ECC01D00C732D3 /* Draft.swift in Sources */,
|
||||
D667E5F12134D5050057A976 /* UIViewController+Delegates.swift in Sources */,
|
||||
D6BC8748219738E1006163F1 /* EnhancedTableViewController.swift in Sources */,
|
||||
|
|
|
@ -95,3 +95,17 @@ struct MenuController {
|
|||
}
|
||||
|
||||
}
|
||||
|
||||
extension MenuController {
|
||||
class SidebarItem: NSObject, NSCopying {
|
||||
let item: MainSidebarViewController.Item
|
||||
|
||||
init(item: MainSidebarViewController.Item) {
|
||||
self.item = item
|
||||
}
|
||||
|
||||
func copy(with zone: NSZone? = nil) -> Any {
|
||||
return SidebarItem(item: self.item)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -59,7 +59,9 @@ class MainSceneDelegate: UIResponder, UIWindowSceneDelegate {
|
|||
|
||||
let url = URLContexts.first!.url
|
||||
|
||||
if var components = URLComponents(url: url, resolvingAgainstBaseURL: false),
|
||||
if url.host == "x-callback-url" {
|
||||
_ = XCBManager.handle(url: url)
|
||||
} else if var components = URLComponents(url: url, resolvingAgainstBaseURL: false),
|
||||
let rootViewController = rootViewController {
|
||||
components.scheme = "https"
|
||||
let query = components.string!
|
||||
|
|
|
@ -1,396 +0,0 @@
|
|||
//
|
||||
// TimelineViewController.swift
|
||||
// Tusker
|
||||
//
|
||||
// Created by Shadowfacts on 9/20/22.
|
||||
// Copyright © 2022 Shadowfacts. All rights reserved.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import Pachyderm
|
||||
import Combine
|
||||
|
||||
import SwiftSoup
|
||||
|
||||
// TODO: gonna need a thing to replicate all of EnhancedTableViewController
|
||||
|
||||
class TimelineViewController: UIViewController {
|
||||
|
||||
let timeline: Timeline
|
||||
weak var mastodonController: MastodonController!
|
||||
|
||||
private var controller: TimelineLikeController<TimelineItem>!
|
||||
private var confirmLoadMore = PassthroughSubject<Void, Never>()
|
||||
private var newer: RequestRange?
|
||||
private var older: RequestRange?
|
||||
|
||||
private var collectionView: UICollectionView {
|
||||
view as! UICollectionView
|
||||
}
|
||||
private var dataSource: UICollectionViewDiffableDataSource<Section, Item>!
|
||||
|
||||
init(for timeline: Timeline, mastodonController: MastodonController!) {
|
||||
self.timeline = timeline
|
||||
self.mastodonController = mastodonController
|
||||
|
||||
super.init(nibName: nil, bundle: nil)
|
||||
|
||||
self.controller = TimelineLikeController(delegate: self)
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
override func loadView() {
|
||||
var config = UICollectionLayoutListConfiguration(appearance: .plain)
|
||||
// TODO: swipe actions
|
||||
// config.trailingSwipeActionsConfigurationProvider =
|
||||
config.itemSeparatorHandler = { [unowned self] indexPath, sectionSeparatorConfiguration in
|
||||
if let item = self.dataSource.itemIdentifier(for: indexPath),
|
||||
item.hideSeparators {
|
||||
var config = sectionSeparatorConfiguration
|
||||
config.topSeparatorVisibility = .hidden
|
||||
config.bottomSeparatorVisibility = .hidden
|
||||
return config
|
||||
} else {
|
||||
return sectionSeparatorConfiguration
|
||||
}
|
||||
}
|
||||
let layout = UICollectionViewCompositionalLayout.list(using: config)
|
||||
view = UICollectionView(frame: .zero, collectionViewLayout: layout)
|
||||
// TODO: delegates
|
||||
collectionView.delegate = self
|
||||
// collectionView.dragDelegate = self
|
||||
|
||||
dataSource = createDataSource()
|
||||
applyInitialSnapshot()
|
||||
|
||||
#if !targetEnvironment(macCatalyst)
|
||||
let refreshControl = UIRefreshControl(frame: .zero, primaryAction: UIAction(handler: { [unowned self] _ in
|
||||
Task {
|
||||
await self.controller.loadNewer()
|
||||
self.collectionView.refreshControl!.endRefreshing()
|
||||
}
|
||||
}))
|
||||
collectionView.refreshControl = refreshControl
|
||||
#endif
|
||||
}
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
|
||||
// TODO: refresh key command
|
||||
}
|
||||
|
||||
private func createDataSource() -> UICollectionViewDiffableDataSource<Section, Item> {
|
||||
let listCell = UICollectionView.CellRegistration<UICollectionViewListCell, Item> { [unowned self] cell, indexPath, item in
|
||||
guard case .status(id: let id, state: _) = item,
|
||||
let status = mastodonController.persistentContainer.status(for: id) else {
|
||||
fatalError()
|
||||
}
|
||||
var config = cell.defaultContentConfiguration()
|
||||
let doc = try! SwiftSoup.parseBodyFragment(status.content)
|
||||
config.text = try! doc.text()
|
||||
cell.contentConfiguration = config
|
||||
}
|
||||
collectionView.register(LoadingCollectionViewCell.self, forCellWithReuseIdentifier: "loadingIndicator")
|
||||
collectionView.register(ConfirmLoadMoreCollectionViewCell.self, forCellWithReuseIdentifier: "confirmLoadMore")
|
||||
return UICollectionViewDiffableDataSource(collectionView: collectionView) { [unowned self] collectionView, indexPath, itemIdentifier in
|
||||
switch itemIdentifier {
|
||||
case .status(_, _):
|
||||
return collectionView.dequeueConfiguredReusableCell(using: listCell, for: indexPath, item: itemIdentifier)
|
||||
case .loadingIndicator:
|
||||
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "loadingIndicator", for: indexPath) as! LoadingCollectionViewCell
|
||||
cell.indicator.startAnimating()
|
||||
return cell
|
||||
case .confirmLoadMore:
|
||||
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "confirmLoadMore", for: indexPath) as! ConfirmLoadMoreCollectionViewCell
|
||||
cell.confirmLoadMore = self.confirmLoadMore
|
||||
Task {
|
||||
if case .loadingOlder(_, _) = await controller.state {
|
||||
cell.isLoading = true
|
||||
} else {
|
||||
cell.isLoading = false
|
||||
}
|
||||
}
|
||||
return cell
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func applyInitialSnapshot() {
|
||||
// TODO: this might not be necessary
|
||||
// TODO: yes it is, for public timeline descriptions
|
||||
}
|
||||
|
||||
override func viewWillAppear(_ animated: Bool) {
|
||||
super.viewWillAppear(animated)
|
||||
|
||||
Task {
|
||||
await controller.loadInitial()
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension TimelineViewController {
|
||||
enum Section: Hashable {
|
||||
case header
|
||||
case statuses
|
||||
case footer
|
||||
}
|
||||
enum Item: Hashable {
|
||||
case status(id: String, state: StatusState)
|
||||
case loadingIndicator
|
||||
case confirmLoadMore
|
||||
// // TODO: remove local param from this
|
||||
// case publicTimelineDescription(local: Bool)
|
||||
|
||||
static func ==(lhs: Item, rhs: Item) -> Bool {
|
||||
switch (lhs, rhs) {
|
||||
case let (.status(id: a, state: _), .status(id: b, state: _)):
|
||||
return a == b
|
||||
case (.loadingIndicator, .loadingIndicator):
|
||||
return true
|
||||
case (.confirmLoadMore, .confirmLoadMore):
|
||||
return true
|
||||
// case let (.publicTimelineDescription(local: a), .publicTimelineDescription(local: b)):
|
||||
// return a == b
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func hash(into hasher: inout Hasher) {
|
||||
switch self {
|
||||
case .status(id: let id, state: _):
|
||||
hasher.combine(0)
|
||||
hasher.combine(id)
|
||||
case .loadingIndicator:
|
||||
hasher.combine(1)
|
||||
case .confirmLoadMore:
|
||||
hasher.combine(2)
|
||||
// case .publicTimelineDescription(local: let local):
|
||||
// hasher.combine(3)
|
||||
// hasher.combine(local)
|
||||
}
|
||||
}
|
||||
|
||||
var hideSeparators: Bool {
|
||||
switch self {
|
||||
case .loadingIndicator:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension TimelineViewController: TimelineLikeControllerDelegate {
|
||||
typealias TimelineItem = String // status ID
|
||||
|
||||
func loadInitial() async throws -> [TimelineItem] {
|
||||
guard let mastodonController else {
|
||||
throw Error.noClient
|
||||
}
|
||||
|
||||
try await Task.sleep(nanoseconds: 2 * NSEC_PER_SEC)
|
||||
|
||||
let request = Client.getStatuses(timeline: timeline)
|
||||
let (statuses, _) = try await mastodonController.run(request)
|
||||
|
||||
if !statuses.isEmpty {
|
||||
newer = .after(id: statuses.first!.id, count: nil)
|
||||
older = .before(id: statuses.last!.id, count: nil)
|
||||
}
|
||||
|
||||
return await withCheckedContinuation { continuation in
|
||||
mastodonController.persistentContainer.addAll(statuses: statuses) {
|
||||
continuation.resume(returning: statuses.map(\.id))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func loadNewer() async throws -> [TimelineItem] {
|
||||
guard let newer else {
|
||||
throw Error.noNewer
|
||||
}
|
||||
|
||||
let request = Client.getStatuses(timeline: timeline, range: newer)
|
||||
let (statuses, _) = try await mastodonController.run(request)
|
||||
|
||||
guard !statuses.isEmpty else {
|
||||
throw Error.allCaughtUp
|
||||
}
|
||||
|
||||
self.newer = .after(id: statuses.first!.id, count: nil)
|
||||
|
||||
return await withCheckedContinuation { continuation in
|
||||
mastodonController.persistentContainer.addAll(statuses: statuses) {
|
||||
continuation.resume(returning: statuses.map(\.id))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func loadOlder() async throws -> [TimelineItem] {
|
||||
guard let older else {
|
||||
throw Error.noOlder
|
||||
}
|
||||
|
||||
try await Task.sleep(nanoseconds: 2 * NSEC_PER_SEC)
|
||||
|
||||
let request = Client.getStatuses(timeline: timeline, range: older)
|
||||
let (statuses, _) = try await mastodonController.run(request)
|
||||
|
||||
if !statuses.isEmpty {
|
||||
self.older = .before(id: statuses.last!.id, count: nil)
|
||||
}
|
||||
|
||||
return await withCheckedContinuation { continuation in
|
||||
mastodonController.persistentContainer.addAll(statuses: statuses) {
|
||||
continuation.resume(returning: statuses.map(\.id))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func canLoadOlder() async -> Bool {
|
||||
if Preferences.shared.disableInfiniteScrolling {
|
||||
var snapshot = dataSource.snapshot()
|
||||
if !snapshot.itemIdentifiers.contains(.confirmLoadMore) {
|
||||
if !snapshot.sectionIdentifiers.contains(.footer) {
|
||||
snapshot.appendSections([.footer])
|
||||
}
|
||||
snapshot.appendItems([.confirmLoadMore], toSection: .footer)
|
||||
await dataSource.apply(snapshot, animatingDifferences: false)
|
||||
}
|
||||
for await _ in self.confirmLoadMore.values {
|
||||
return true
|
||||
}
|
||||
fatalError("unreachable")
|
||||
} else {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
func handleEvent(_ event: TimelineLikeController<TimelineItem>.Event) async {
|
||||
switch event {
|
||||
case .addLoadingIndicator:
|
||||
var snapshot = dataSource.snapshot()
|
||||
if !snapshot.sectionIdentifiers.contains(.footer) {
|
||||
snapshot.appendSections([.footer])
|
||||
}
|
||||
if snapshot.itemIdentifiers.contains(.confirmLoadMore) {
|
||||
snapshot.reconfigureItems([.confirmLoadMore])
|
||||
} else {
|
||||
snapshot.appendItems([.loadingIndicator], toSection: .footer)
|
||||
}
|
||||
await dataSource.apply(snapshot, animatingDifferences: false)
|
||||
|
||||
case .removeLoadingIndicator:
|
||||
let oldContentOffset = collectionView.contentOffset
|
||||
var snapshot = dataSource.snapshot()
|
||||
snapshot.deleteSections([.footer])
|
||||
await dataSource.apply(snapshot, animatingDifferences: false)
|
||||
collectionView.contentOffset = oldContentOffset
|
||||
|
||||
case .loadAllError(let error, _):
|
||||
let config = ToastConfiguration(from: error, with: "Error Loading", in: self) { [weak self] (toast: ToastView) in
|
||||
toast.dismissToast(animated: true)
|
||||
Task {
|
||||
await self?.controller.loadInitial()
|
||||
}
|
||||
}
|
||||
self.showToast(configuration: config, animated: true)
|
||||
|
||||
case .replaceAllItems(let ids, _):
|
||||
var snapshot = dataSource.snapshot()
|
||||
if snapshot.sectionIdentifiers.contains(.statuses) {
|
||||
snapshot.deleteSections([.statuses])
|
||||
}
|
||||
snapshot.appendSections([.statuses])
|
||||
snapshot.appendItems(ids.map { .status(id: $0, state: .unknown) }, toSection: .statuses)
|
||||
await dataSource.apply(snapshot, animatingDifferences: false)
|
||||
|
||||
case .loadNewerError(Error.allCaughtUp, _):
|
||||
var config = ToastConfiguration(title: "You're all caught up")
|
||||
config.edge = .top
|
||||
config.dismissAutomaticallyAfter = 2
|
||||
config.action = { (toast) in
|
||||
toast.dismissToast(animated: true)
|
||||
}
|
||||
self.showToast(configuration: config, animated: true)
|
||||
|
||||
case .loadNewerError(let error, _):
|
||||
let config = ToastConfiguration(from: error, with: "Error Loading Newer", in: self) { [weak self] (toast: ToastView) in
|
||||
toast.dismissToast(animated: true)
|
||||
Task {
|
||||
await self?.controller.loadNewer()
|
||||
}
|
||||
}
|
||||
self.showToast(configuration: config, animated: true)
|
||||
|
||||
case .prependItems(let ids, _):
|
||||
var snapshot = dataSource.snapshot()
|
||||
let items = ids.map { Item.status(id: $0, state: .unknown) }
|
||||
let first = snapshot.itemIdentifiers(inSection: .statuses).first
|
||||
if let first {
|
||||
snapshot.insertItems(items, beforeItem: first)
|
||||
} else {
|
||||
snapshot.appendItems(items, toSection: .statuses)
|
||||
}
|
||||
await dataSource.apply(snapshot, animatingDifferences: false)
|
||||
|
||||
if let first,
|
||||
let indexPath = dataSource.indexPath(for: first) {
|
||||
// TODO: i can't tell if this actually works or not
|
||||
// maintain the current position in the list (don't scroll to top)
|
||||
collectionView.scrollToItem(at: indexPath, at: .top, animated: false)
|
||||
}
|
||||
|
||||
case .loadOlderError(let error, _):
|
||||
let config = ToastConfiguration(from: error, with: "Error Loading Older", in: self) { [weak self] (toast: ToastView) in
|
||||
toast.dismissToast(animated: true)
|
||||
Task {
|
||||
await self?.controller.loadOlder()
|
||||
}
|
||||
}
|
||||
self.showToast(configuration: config, animated: true)
|
||||
|
||||
case .appendItems(let ids, _):
|
||||
var snapshot = dataSource.snapshot()
|
||||
if snapshot.itemIdentifiers.contains(.confirmLoadMore) {
|
||||
snapshot.deleteItems([.confirmLoadMore])
|
||||
}
|
||||
snapshot.appendItems(ids.map { .status(id: $0, state: .unknown) }, toSection: .statuses)
|
||||
await dataSource.apply(snapshot, animatingDifferences: false)
|
||||
}
|
||||
}
|
||||
|
||||
enum Error: Swift.Error {
|
||||
case noClient
|
||||
case noNewer
|
||||
case noOlder
|
||||
case allCaughtUp
|
||||
}
|
||||
}
|
||||
|
||||
extension TimelineViewController: UICollectionViewDelegate {
|
||||
func collectionView(_ collectionView: UICollectionView, willDisplay cell: UICollectionViewCell, forItemAt indexPath: IndexPath) {
|
||||
guard case .statuses = dataSource.sectionIdentifier(for: indexPath.section),
|
||||
case .status(_, _) = dataSource.itemIdentifier(for: indexPath) else {
|
||||
return
|
||||
}
|
||||
|
||||
let itemsInSection = collectionView.numberOfItems(inSection: indexPath.section)
|
||||
if indexPath.row == itemsInSection - 1 {
|
||||
Task {
|
||||
await controller.loadOlder()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension TimelineViewController: ToastableViewController {
|
||||
}
|
|
@ -19,7 +19,7 @@ class TimelinesPageViewController: SegmentedPageViewController {
|
|||
init(mastodonController: MastodonController) {
|
||||
self.mastodonController = mastodonController
|
||||
|
||||
let home = TimelineViewController(for: .home, mastodonController: mastodonController)
|
||||
let home = TimelineTableViewController(for: .home, mastodonController: mastodonController)
|
||||
home.title = homeTitle
|
||||
|
||||
let federated = TimelineTableViewController(for: .public(local: false), mastodonController: mastodonController)
|
||||
|
|
|
@ -1,255 +0,0 @@
|
|||
//
|
||||
// TimelineLikeController.swift
|
||||
// Tusker
|
||||
//
|
||||
// Created by Shadowfacts on 9/19/22.
|
||||
// Copyright © 2022 Shadowfacts. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import OSLog
|
||||
|
||||
protocol TimelineLikeControllerDelegate<TimelineItem>: AnyObject {
|
||||
associatedtype TimelineItem
|
||||
|
||||
func loadInitial() async throws -> [TimelineItem]
|
||||
|
||||
func loadNewer() async throws -> [TimelineItem]
|
||||
|
||||
func loadOlder() async throws -> [TimelineItem]
|
||||
|
||||
func canLoadOlder() async -> Bool
|
||||
|
||||
func handleEvent(_ event: TimelineLikeController<TimelineItem>.Event) async
|
||||
}
|
||||
|
||||
private let logger = Logger(subsystem: Bundle.main.bundleIdentifier!, category: "TimelineLikeController")
|
||||
|
||||
actor TimelineLikeController<Item> {
|
||||
|
||||
unowned var delegate: any TimelineLikeControllerDelegate<Item>
|
||||
|
||||
private(set) var state = State.idle {
|
||||
willSet {
|
||||
precondition(state.canTransition(to: newValue))
|
||||
logger.debug("State: \(self.state.debugDescription, privacy: .public) -> \(newValue.debugDescription, privacy: .public)")
|
||||
}
|
||||
}
|
||||
|
||||
init(delegate: any TimelineLikeControllerDelegate<Item>) {
|
||||
self.delegate = delegate
|
||||
}
|
||||
|
||||
func loadInitial() async {
|
||||
guard state == .idle else {
|
||||
return
|
||||
}
|
||||
let token = LoadAttemptToken()
|
||||
state = .loadingInitial(token, hasAddedLoadingIndicator: false)
|
||||
let loadingIndicator = DeferredLoadingIndicator(owner: self, state: state, addedIndicatorState: .loadingInitial(token, hasAddedLoadingIndicator: true))
|
||||
do {
|
||||
let items = try await delegate.loadInitial()
|
||||
guard case .loadingInitial(token, _) = state else {
|
||||
return
|
||||
}
|
||||
await loadingIndicator.end()
|
||||
await emit(event: .replaceAllItems(items, token))
|
||||
state = .idle
|
||||
} catch {
|
||||
await loadingIndicator.end()
|
||||
await emit(event: .loadAllError(error, token))
|
||||
state = .idle
|
||||
}
|
||||
}
|
||||
|
||||
func loadNewer() async {
|
||||
guard state == .idle else {
|
||||
return
|
||||
}
|
||||
let token = LoadAttemptToken()
|
||||
state = .loadingNewer(token)
|
||||
do {
|
||||
let items = try await delegate.loadNewer()
|
||||
guard case .loadingNewer(token) = state else {
|
||||
return
|
||||
}
|
||||
await emit(event: .prependItems(items, token))
|
||||
state = .idle
|
||||
} catch {
|
||||
await emit(event: .loadNewerError(error, token))
|
||||
state = .idle
|
||||
}
|
||||
}
|
||||
|
||||
func loadOlder() async {
|
||||
guard state == .idle else {
|
||||
return
|
||||
}
|
||||
let token = LoadAttemptToken()
|
||||
// TODO: does the waiting state need to include the token?
|
||||
// TODO: does this even need to be a separate state? maybe we should just await the delegate's permission, since it can suspend until user input. then the prompt could appear, and the user could scroll back to the top and still be able to refresh
|
||||
// state = .waitingForLoadOlderPermission
|
||||
guard await delegate.canLoadOlder() else {
|
||||
// state = .idle
|
||||
return
|
||||
}
|
||||
state = .loadingOlder(token, hasAddedLoadingIndicator: false)
|
||||
let loadingIndicator = DeferredLoadingIndicator(owner: self, state: state, addedIndicatorState: .loadingOlder(token, hasAddedLoadingIndicator: true))
|
||||
do {
|
||||
let items = try await delegate.loadOlder()
|
||||
guard case .loadingOlder(token, _) = state else {
|
||||
return
|
||||
}
|
||||
await loadingIndicator.end()
|
||||
await emit(event: .appendItems(items, token))
|
||||
state = .idle
|
||||
} catch {
|
||||
await loadingIndicator.end()
|
||||
await emit(event: .loadOlderError(error, token))
|
||||
state = .idle
|
||||
}
|
||||
}
|
||||
|
||||
private func transition(to newState: State) {
|
||||
self.state = newState
|
||||
}
|
||||
|
||||
private func emit(event: Event) async {
|
||||
precondition(state.canEmit(event: event))
|
||||
await delegate.handleEvent(event)
|
||||
}
|
||||
|
||||
enum State: Equatable, CustomDebugStringConvertible {
|
||||
case idle
|
||||
case loadingInitial(LoadAttemptToken, hasAddedLoadingIndicator: Bool)
|
||||
case loadingNewer(LoadAttemptToken)
|
||||
// case waitingForLoadOlderPermission
|
||||
case loadingOlder(LoadAttemptToken, hasAddedLoadingIndicator: Bool)
|
||||
|
||||
var debugDescription: String {
|
||||
switch self {
|
||||
case .idle:
|
||||
return "idle"
|
||||
case .loadingInitial(let token, let hasAddedLoadingIndicator):
|
||||
return "loadingInitial(\(ObjectIdentifier(token)), hasAddedLoadingIndicator: \(hasAddedLoadingIndicator))"
|
||||
case .loadingNewer(let token):
|
||||
return "loadingNewer(\(ObjectIdentifier(token)))"
|
||||
// case .waitingForLoadOlderPermission:
|
||||
// return "waitingForLoadOlderPermission"
|
||||
case .loadingOlder(let token, let hasAddedLoadingIndicator):
|
||||
return "loadingOlder(\(ObjectIdentifier(token)), hasAddedLoadingIndicator: \(hasAddedLoadingIndicator))"
|
||||
}
|
||||
}
|
||||
|
||||
func canTransition(to: State) -> Bool {
|
||||
switch self {
|
||||
case .idle:
|
||||
switch to {
|
||||
case .loadingInitial(_, _), .loadingNewer(_)/*, .waitingForLoadOlderPermission*/, .loadingOlder(_, _):
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
case .loadingInitial(let token, let hasAddedLoadingIndicator):
|
||||
return to == .idle || (!hasAddedLoadingIndicator && to == .loadingInitial(token, hasAddedLoadingIndicator: true))
|
||||
case .loadingNewer(_):
|
||||
return to == .idle
|
||||
case .loadingOlder(let token, let hasAddedLoadingIndicator):
|
||||
return to == .idle || (!hasAddedLoadingIndicator && to == .loadingOlder(token, hasAddedLoadingIndicator: true))
|
||||
// case .waitingForLoadOlderPermission:
|
||||
// switch to {
|
||||
// case .idle, .loadingOlder(_, _):
|
||||
// return true
|
||||
// default:
|
||||
// return false
|
||||
// }
|
||||
}
|
||||
}
|
||||
|
||||
func canEmit(event: Event) -> Bool {
|
||||
switch event {
|
||||
case .addLoadingIndicator:
|
||||
switch self {
|
||||
case .loadingInitial(_, _), .loadingOlder(_, _):
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
case .removeLoadingIndicator:
|
||||
switch self {
|
||||
case .loadingInitial(_, hasAddedLoadingIndicator: true), .loadingOlder(_, hasAddedLoadingIndicator: true):
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
case .loadAllError(_, let token), .replaceAllItems(_, let token):
|
||||
switch self {
|
||||
case .loadingInitial(token, _):
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
case .loadNewerError(_, let token), .prependItems(_, let token):
|
||||
switch self {
|
||||
case .loadingNewer(token):
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
case .loadOlderError(_, let token), .appendItems(_, let token):
|
||||
switch self {
|
||||
case .loadingOlder(token, _):
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum Event {
|
||||
case addLoadingIndicator
|
||||
case removeLoadingIndicator
|
||||
case loadAllError(Error, LoadAttemptToken)
|
||||
case replaceAllItems([Item], LoadAttemptToken)
|
||||
case loadNewerError(Error, LoadAttemptToken)
|
||||
case prependItems([Item], LoadAttemptToken)
|
||||
case loadOlderError(Error, LoadAttemptToken)
|
||||
case appendItems([Item], LoadAttemptToken)
|
||||
}
|
||||
|
||||
class LoadAttemptToken: Equatable {
|
||||
static func ==(lhs: LoadAttemptToken, rhs: LoadAttemptToken) -> Bool {
|
||||
return lhs === rhs
|
||||
}
|
||||
}
|
||||
|
||||
class DeferredLoadingIndicator {
|
||||
private let owner: TimelineLikeController<Item>
|
||||
private let addedIndicatorState: State
|
||||
private let task: Task<Void, Error>
|
||||
|
||||
init(owner: TimelineLikeController<Item>, state: State, addedIndicatorState: State) {
|
||||
self.owner = owner
|
||||
self.addedIndicatorState = addedIndicatorState
|
||||
self.task = Task {
|
||||
try await Task.sleep(nanoseconds: 150 * NSEC_PER_MSEC)
|
||||
guard await state == owner.state else {
|
||||
return
|
||||
}
|
||||
await owner.emit(event: .addLoadingIndicator)
|
||||
await owner.transition(to: addedIndicatorState)
|
||||
}
|
||||
}
|
||||
|
||||
func end() async {
|
||||
let state = await owner.state
|
||||
if state == addedIndicatorState {
|
||||
await owner.emit(event: .removeLoadingIndicator)
|
||||
} else {
|
||||
task.cancel()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -1,67 +0,0 @@
|
|||
//
|
||||
// ConfirmLoadMoreCollectionViewCell.swift
|
||||
// Tusker
|
||||
//
|
||||
// Created by Shadowfacts on 9/21/22.
|
||||
// Copyright © 2022 Shadowfacts. All rights reserved.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import Combine
|
||||
|
||||
class ConfirmLoadMoreCollectionViewCell: UICollectionViewCell {
|
||||
|
||||
var confirmLoadMore: PassthroughSubject<Void, Never>?
|
||||
var isLoading: Bool {
|
||||
get {
|
||||
button.configuration?.showsActivityIndicator ?? false
|
||||
}
|
||||
set {
|
||||
var config = button.configuration!
|
||||
config.showsActivityIndicator = newValue
|
||||
button.configuration = config
|
||||
}
|
||||
}
|
||||
|
||||
private var button: UIButton!
|
||||
|
||||
override init(frame: CGRect) {
|
||||
super.init(frame: frame)
|
||||
|
||||
backgroundColor = .secondarySystemBackground
|
||||
|
||||
let label = UILabel()
|
||||
label.text = "Infinite scrolling is off. Do you want to keep going?"
|
||||
label.textColor = .secondaryLabel
|
||||
label.textAlignment = .natural
|
||||
label.numberOfLines = 0
|
||||
|
||||
var config = UIButton.Configuration.tinted()
|
||||
config.title = "Load More"
|
||||
config.imagePadding = 4
|
||||
button = UIButton(configuration: config, primaryAction: UIAction(handler: { [unowned self] _ in
|
||||
self.confirmLoadMore?.send()
|
||||
}))
|
||||
|
||||
let stack = UIStackView(arrangedSubviews: [
|
||||
label,
|
||||
button,
|
||||
])
|
||||
stack.axis = .vertical
|
||||
stack.distribution = .fill
|
||||
stack.spacing = 4
|
||||
stack.translatesAutoresizingMaskIntoConstraints = false
|
||||
contentView.addSubview(stack)
|
||||
NSLayoutConstraint.activate([
|
||||
stack.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 16),
|
||||
stack.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -16),
|
||||
stack.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 8),
|
||||
stack.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -8),
|
||||
])
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
}
|
|
@ -1,29 +0,0 @@
|
|||
//
|
||||
// LoadingCollectionViewCell.swift
|
||||
// Tusker
|
||||
//
|
||||
// Created by Shadowfacts on 9/24/22.
|
||||
// Copyright © 2022 Shadowfacts. All rights reserved.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
|
||||
class LoadingCollectionViewCell: UICollectionViewCell {
|
||||
let indicator = UIActivityIndicatorView(style: .medium)
|
||||
|
||||
override init(frame: CGRect) {
|
||||
super.init(frame: frame)
|
||||
|
||||
indicator.translatesAutoresizingMaskIntoConstraints = false
|
||||
contentView.addSubview(indicator)
|
||||
NSLayoutConstraint.activate([
|
||||
indicator.centerXAnchor.constraint(equalTo: contentView.centerXAnchor),
|
||||
indicator.topAnchor.constraint(equalToSystemSpacingBelow: contentView.topAnchor, multiplier: 1),
|
||||
contentView.bottomAnchor.constraint(equalToSystemSpacingBelow: indicator.bottomAnchor, multiplier: 1),
|
||||
])
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
}
|
|
@ -35,36 +35,30 @@ struct ToastConfiguration {
|
|||
}
|
||||
|
||||
extension ToastConfiguration {
|
||||
init(from error: Error, with title: String, in viewController: UIViewController, retryAction: ((ToastView) -> Void)?) {
|
||||
init(from error: Client.Error, with title: String, in viewController: UIViewController, retryAction: ((ToastView) -> Void)?) {
|
||||
self.init(title: title)
|
||||
// localizedDescription is statically dispatched, so we need to call it after the downcast
|
||||
if let error = error as? Client.Error {
|
||||
self.subtitle = error.localizedDescription
|
||||
self.systemImageName = error.systemImageName
|
||||
self.longPressAction = { [unowned viewController] toast in
|
||||
toast.dismissToast(animated: true)
|
||||
let text = """
|
||||
\(title):
|
||||
\(error.requestMethod.name) \(error.requestEndpoint)
|
||||
|
||||
\(error.type)
|
||||
"""
|
||||
let reporter = IssueReporterViewController.create(reportText: text, dismiss: { [unowned viewController] in
|
||||
viewController.dismiss(animated: true)
|
||||
})
|
||||
viewController.present(reporter, animated: true)
|
||||
}
|
||||
} else {
|
||||
self.subtitle = error.localizedDescription
|
||||
self.systemImageName = "exclamationmark.triangle"
|
||||
}
|
||||
self.subtitle = error.localizedDescription
|
||||
self.systemImageName = error.systemImageName
|
||||
if let retryAction = retryAction {
|
||||
self.actionTitle = "Retry"
|
||||
self.action = retryAction
|
||||
}
|
||||
self.longPressAction = { [unowned viewController] toast in
|
||||
toast.dismissToast(animated: true)
|
||||
let text = """
|
||||
\(title):
|
||||
\(error.requestMethod.name) \(error.requestEndpoint)
|
||||
|
||||
\(error.type)
|
||||
"""
|
||||
let reporter = IssueReporterViewController.create(reportText: text, dismiss: { [unowned viewController] in
|
||||
viewController.dismiss(animated: true)
|
||||
})
|
||||
viewController.present(reporter, animated: true)
|
||||
}
|
||||
}
|
||||
|
||||
init(from error: Error, with title: String, in viewController: UIViewController, retryAction: @escaping @MainActor (ToastView) async -> Void) {
|
||||
init(from error: Client.Error, with title: String, in viewController: UIViewController, retryAction: @escaping @MainActor (ToastView) async -> Void) {
|
||||
self.init(from: error, with: title, in: viewController) { toast in
|
||||
Task {
|
||||
await retryAction(toast)
|
||||
|
|
|
@ -0,0 +1,29 @@
|
|||
//
|
||||
// XCBActionType.swift
|
||||
// Tusker
|
||||
//
|
||||
// Created by Shadowfacts on 9/23/18.
|
||||
// Copyright © 2018 Shadowfacts. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
enum XCBActionType: String {
|
||||
// Statuses
|
||||
case showStatus
|
||||
case postStatus
|
||||
case getStatus
|
||||
case favoriteStatus
|
||||
case reblogStatus
|
||||
// Accounts
|
||||
case showAccount
|
||||
case getAccount
|
||||
case getCurrentUser
|
||||
case followUser
|
||||
// Search
|
||||
case search
|
||||
|
||||
var path: String {
|
||||
return "/\(rawValue)"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,354 @@
|
|||
//
|
||||
// XCBActions.swift
|
||||
// Tusker
|
||||
//
|
||||
// Created by Shadowfacts on 9/23/18.
|
||||
// Copyright © 2018 Shadowfacts. All rights reserved.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import Pachyderm
|
||||
import SwiftSoup
|
||||
|
||||
struct XCBActions {
|
||||
|
||||
// MARK: - Utils
|
||||
private static var mastodonController: MastodonController {
|
||||
let scene = UIApplication.shared.activeOrBackgroundScene!
|
||||
return scene.session.mastodonController!
|
||||
}
|
||||
|
||||
private static func getMainTabBarController() -> MainTabBarViewController {
|
||||
let scene = UIApplication.shared.connectedScenes.compactMap { $0 as? UIWindowScene }.first!
|
||||
let window = scene.windows.first { $0.isKeyWindow }!
|
||||
return window.rootViewController as! MainTabBarViewController
|
||||
}
|
||||
|
||||
private static func show(_ vc: UIViewController) {
|
||||
let tabBarController = getMainTabBarController()
|
||||
if tabBarController.presentedViewController != nil {
|
||||
tabBarController.presentedViewController?.dismiss(animated: false)
|
||||
}
|
||||
tabBarController.selectedViewController!.show(vc, sender: nil)
|
||||
}
|
||||
|
||||
private static func present(_ vc: UIViewController, animated: Bool = true) {
|
||||
getMainTabBarController().present(vc, animated: animated)
|
||||
}
|
||||
|
||||
private static func getStatus(from request: XCBRequest, session: XCBSession, completion: @escaping (Status) -> Void) {
|
||||
if let id = request.arguments["statusID"] {
|
||||
let request = Client.getStatus(id: id)
|
||||
mastodonController.run(request) { (response) in
|
||||
guard case let .success(status, _) = response else {
|
||||
session.complete(with: .error, additionalData: [
|
||||
"error": "Could not get status with ID \(id)"
|
||||
])
|
||||
return
|
||||
}
|
||||
completion(status)
|
||||
}
|
||||
} else if let searchQuery = request.arguments["statusURL"] {
|
||||
let request = Client.search(query: searchQuery)
|
||||
mastodonController.run(request) { (response) in
|
||||
if case let .success(results, _) = response,
|
||||
let status = results.statuses.first {
|
||||
completion(status)
|
||||
} else {
|
||||
session.complete(with: .error, additionalData: [
|
||||
"error": "Could not find status by searching '\(searchQuery)'"
|
||||
])
|
||||
}
|
||||
}
|
||||
} else {
|
||||
session.complete(with: .error, additionalData: [
|
||||
"error": "No status provided. Specify either instance-local statusID or remote statusURL."
|
||||
])
|
||||
}
|
||||
}
|
||||
|
||||
private static func getAccount(from request: XCBRequest, session: XCBSession, completion: @escaping (Account) -> Void) {
|
||||
if let id = request.arguments["accountID"] {
|
||||
let request = Client.getAccount(id: id)
|
||||
mastodonController.run(request) { (response) in
|
||||
guard case let .success(account, _) = response else {
|
||||
session.complete(with: .error, additionalData: [
|
||||
"error": "Could not get account with ID \(id)"
|
||||
])
|
||||
return
|
||||
}
|
||||
completion(account)
|
||||
}
|
||||
} else if let searchQuery = request.arguments["accountURL"] {
|
||||
let request = Client.search(query: searchQuery)
|
||||
mastodonController.run(request) { (response) in
|
||||
if case let .success(results, _) = response {
|
||||
if let account = results.accounts.first {
|
||||
completion(account)
|
||||
} else {
|
||||
session.complete(with: .error, additionalData: [
|
||||
"error": "Could not find account by searching '\(searchQuery)'"
|
||||
])
|
||||
}
|
||||
} else if case let .failure(error) = response {
|
||||
session.complete(with: .error, additionalData: [
|
||||
"error": error.localizedDescription
|
||||
])
|
||||
}
|
||||
}
|
||||
} else if let acct = request.arguments["acct"] {
|
||||
let request = Client.searchForAccount(query: acct)
|
||||
mastodonController.run(request) { (response) in
|
||||
if case let .success(accounts, _) = response {
|
||||
if let account = accounts.first {
|
||||
completion(account)
|
||||
} else {
|
||||
session.complete(with: .error, additionalData: [
|
||||
"error": "Could not find account \(acct)"
|
||||
])
|
||||
}
|
||||
} else if case let .failure(error) = response {
|
||||
session.complete(with: .error, additionalData: [
|
||||
"error": error.localizedDescription
|
||||
])
|
||||
}
|
||||
}
|
||||
} else {
|
||||
session.complete(with: .error, additionalData: [
|
||||
"error": "No status provided. Specify either instance-local ID, account URL, or qualified username."
|
||||
])
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Statuses
|
||||
static func showStatus(_ request: XCBRequest, _ session: XCBSession, _ silent: Bool?) {
|
||||
getStatus(from: request, session: session) { (status) in
|
||||
DispatchQueue.main.async {
|
||||
let vc = ConversationTableViewController(for: status.id, mastodonController: mastodonController)
|
||||
show(vc)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static func postStatus(_ request: XCBRequest, _ session: XCBSession, _ silent: Bool?) {
|
||||
let mentioning = request.arguments["mentioning"]
|
||||
let text = request.arguments["text"]
|
||||
|
||||
if silent ?? false {
|
||||
var status = ""
|
||||
if let mentioning = mentioning { status += mentioning }
|
||||
if let text = text { status += text }
|
||||
guard CharacterCounter.count(text: status) <= mastodonController.instance.maxStatusCharacters ?? 500 else {
|
||||
session.complete(with: .error, additionalData: [
|
||||
"error": "Too many characters. Instance maximum is \(mastodonController.instance.maxStatusCharacters ?? 500)"
|
||||
])
|
||||
return
|
||||
}
|
||||
let request = Client.createStatus(text: status, visibility: Preferences.shared.defaultPostVisibility)
|
||||
mastodonController.run(request) { response in
|
||||
if case let .success(status, _) = response {
|
||||
session.complete(with: .success, additionalData: [
|
||||
"statusURL": status.url?.absoluteString,
|
||||
"statusURI": status.uri
|
||||
])
|
||||
} else if case let .failure(error) = response {
|
||||
session.complete(with: .error, additionalData: [
|
||||
"error": error.localizedDescription
|
||||
])
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// todo: use text param
|
||||
let draft = mastodonController.createDraft(mentioningAcct: mentioning)
|
||||
let compose = ComposeHostingController(draft: draft, mastodonController: mastodonController)
|
||||
// compose.xcbSession = session
|
||||
let vc = UINavigationController(rootViewController: compose)
|
||||
present(vc)
|
||||
}
|
||||
}
|
||||
|
||||
static func getStatus(_ request: XCBRequest, _ session: XCBSession, _ silent: Bool?) {
|
||||
getStatus(from: request, session: session) { (status) in
|
||||
let html = Bool(request.arguments["html"] ?? "false") ?? false
|
||||
let content: String
|
||||
if html {
|
||||
content = status.content
|
||||
} else {
|
||||
do {
|
||||
let doc = try SwiftSoup.parse(status.content)
|
||||
content = try doc.body()!.text()
|
||||
} catch {
|
||||
session.complete(with: .error, additionalData: [
|
||||
"error": error.localizedDescription
|
||||
])
|
||||
return
|
||||
}
|
||||
}
|
||||
session.complete(with: .success, additionalData: [
|
||||
"url": status.url?.absoluteString,
|
||||
"uri": status.uri,
|
||||
"id": status.id,
|
||||
"account": status.account.acct,
|
||||
"inReplyTo": status.inReplyToID,
|
||||
"posted": status.createdAt.timeIntervalSince1970.description,
|
||||
"content": content,
|
||||
"reblog": status.reblog?.id
|
||||
])
|
||||
}
|
||||
}
|
||||
|
||||
static func favoriteStatus(_ request: XCBRequest, _ session: XCBSession, _ silent: Bool?) {
|
||||
statusAction(request: Status.favourite, alertTitle: "Favorite status?", request, session, silent)
|
||||
}
|
||||
|
||||
static func reblogStatus(_ request: XCBRequest, _ session: XCBSession, _ silent: Bool?) {
|
||||
statusAction(request: { Status.reblog($0) }, alertTitle: "Reblog status?", request, session, silent)
|
||||
}
|
||||
|
||||
static func statusAction(request: @escaping (String) -> Request<Status>, alertTitle: String, _ url: XCBRequest, _ session: XCBSession, _ silent: Bool?) {
|
||||
func performAction(status: Status, completion: ((Status) -> Void)?) {
|
||||
mastodonController.run(request(status.id)) { (response) in
|
||||
if case let .success(status, _) = response {
|
||||
completion?(status)
|
||||
session.complete(with: .success, additionalData: [
|
||||
"statusURL": status.url?.absoluteString,
|
||||
"statusURI": status.uri
|
||||
])
|
||||
} else if case let .failure(error) = response {
|
||||
session.complete(with: .error, additionalData: [
|
||||
"error": error.localizedDescription
|
||||
])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func favorite(_ status: Status) {
|
||||
if silent ?? false {
|
||||
performAction(status: status, completion: nil)
|
||||
} else {
|
||||
let vc = ConversationTableViewController(for: status.id, mastodonController: mastodonController)
|
||||
DispatchQueue.main.async {
|
||||
show(vc)
|
||||
}
|
||||
let alertController = UIAlertController(title: alertTitle, message: nil, preferredStyle: .alert)
|
||||
alertController.addAction(UIAlertAction(title: "Ok", style: .default, handler: { (_) in
|
||||
performAction(status: status, completion: { (status) in
|
||||
DispatchQueue.main.async {
|
||||
vc.tableView.reloadData()
|
||||
}
|
||||
})
|
||||
}))
|
||||
alertController.addAction(UIAlertAction(title: "Cancel", style: .cancel, handler: { (_) in
|
||||
session.complete(with: .cancel)
|
||||
}))
|
||||
DispatchQueue.main.async {
|
||||
present(alertController)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
getStatus(from: url, session: session, completion: favorite)
|
||||
}
|
||||
|
||||
// MARK: - Accounts
|
||||
static func showAccount(_ request: XCBRequest, _ session: XCBSession, _ silent: Bool?) {
|
||||
getAccount(from: request, session: session) { (account) in
|
||||
DispatchQueue.main.async {
|
||||
let vc = ProfileViewController(accountID: account.id, mastodonController: mastodonController)
|
||||
show(vc)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static func getAccount(_ request: XCBRequest, _ session: XCBSession, _ silent: Bool?) {
|
||||
getAccount(from: request, session: session) { (account) in
|
||||
session.complete(with: .success, additionalData: [
|
||||
"username": account.acct,
|
||||
"displayName": account.displayName,
|
||||
"locked": account.locked.description,
|
||||
"followers": account.followersCount.description,
|
||||
"following": account.followingCount.description,
|
||||
"url": account.url.absoluteString,
|
||||
"avatarURL": account.avatar?.absoluteString,
|
||||
"headerURL": account.header?.absoluteString
|
||||
])
|
||||
}
|
||||
}
|
||||
|
||||
static func getCurrentUser(_ request: XCBRequest, _ session: XCBSession, _ silent: Bool?) {
|
||||
let account = mastodonController.account!
|
||||
session.complete(with: .success, additionalData: [
|
||||
"username": account.acct,
|
||||
"displayName": account.displayName,
|
||||
"locked": account.locked.description,
|
||||
"followers": account.followersCount.description,
|
||||
"following": account.followingCount.description,
|
||||
"url": account.url.absoluteString,
|
||||
"avatarURL": account.avatar?.absoluteString,
|
||||
"headerURL": account.header?.absoluteString
|
||||
])
|
||||
}
|
||||
|
||||
static func followUser(_ request: XCBRequest, _ session: XCBSession, _ silent: Bool?) {
|
||||
func performAction(_ account: Account) {
|
||||
let request = Account.follow(account.id)
|
||||
mastodonController.run(request) { (response) in
|
||||
if case .success(_, _) = response {
|
||||
session.complete(with: .success, additionalData: [
|
||||
"url": account.url.absoluteString
|
||||
])
|
||||
} else if case let .failure(error) = response {
|
||||
session.complete(with: .error, additionalData: [
|
||||
"error": error.localizedDescription
|
||||
])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func follow(_ account: Account) {
|
||||
if silent ?? false {
|
||||
performAction(account)
|
||||
} else {
|
||||
let vc = ProfileViewController(accountID: account.id, mastodonController: mastodonController)
|
||||
DispatchQueue.main.async {
|
||||
show(vc)
|
||||
}
|
||||
// todo: update to use managed objects
|
||||
let alertController = UIAlertController(title: "Follow \(account.displayName)?", message: nil, preferredStyle: .alert)
|
||||
alertController.addAction(UIAlertAction(title: "Ok", style: .default, handler: { (_) in
|
||||
performAction(account)
|
||||
}))
|
||||
alertController.addAction(UIAlertAction(title: "Cancel", style: .cancel, handler: { (_) in
|
||||
session.complete(with: .cancel)
|
||||
}))
|
||||
DispatchQueue.main.async {
|
||||
present(alertController)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
getAccount(from: request, session: session, completion: follow)
|
||||
}
|
||||
|
||||
// MARK: - Search
|
||||
|
||||
static func search(_ request: XCBRequest, _ session: XCBSession, _ silent: Bool?) {
|
||||
let query = request.arguments["query"]!
|
||||
|
||||
let tabBarController = getMainTabBarController()
|
||||
if let navigationController = tabBarController.getTabController(tab: .explore) as? UINavigationController,
|
||||
let exploreController = navigationController.viewControllers.first as? ExploreViewController {
|
||||
|
||||
tabBarController.select(tab: .explore)
|
||||
navigationController.popToRootViewController(animated: false)
|
||||
|
||||
exploreController.loadViewIfNeeded()
|
||||
exploreController.searchController.isActive = true
|
||||
|
||||
exploreController.searchController.searchBar.text = query
|
||||
exploreController.resultsController.performSearch(query: query)
|
||||
} else {
|
||||
session.complete(with: .error)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,46 @@
|
|||
//
|
||||
// XCBManager.swift
|
||||
// Tusker
|
||||
//
|
||||
// Created by Shadowfacts on 9/23/18.
|
||||
// Copyright © 2018 Shadowfacts. All rights reserved.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
|
||||
class XCBManager {
|
||||
|
||||
static var specs: [XCBRequestSpec] = [
|
||||
// Statuses
|
||||
XCBRequestSpec(type: .showStatus, arguments: ["statusID": true, "statusURL": true], canRunSilently: false, action: XCBActions.showStatus),
|
||||
XCBRequestSpec(type: .getStatus, arguments: ["statusID": true, "statusURL": true, "html": true], canRunSilently: false, action: XCBActions.getStatus),
|
||||
XCBRequestSpec(type: .postStatus, arguments: ["mentioning": true, "text": true], canRunSilently: true, action: XCBActions.postStatus),
|
||||
XCBRequestSpec(type: .favoriteStatus, arguments: ["statusID": true, "statusURL": true], canRunSilently: true, action: XCBActions.favoriteStatus),
|
||||
XCBRequestSpec(type: .reblogStatus, arguments: ["statusID": true, "statusURL": true], canRunSilently: true, action: XCBActions.reblogStatus),
|
||||
// Accounts
|
||||
XCBRequestSpec(type: .showAccount, arguments: ["accountID": true, "accountURL": true, "acct": true], canRunSilently: false, action: XCBActions.showAccount),
|
||||
XCBRequestSpec(type: .getAccount, arguments: ["accountID": true, "accountURL": true, "acct": true], canRunSilently: false, action: XCBActions.getAccount),
|
||||
XCBRequestSpec(type: .getCurrentUser, arguments: [:], canRunSilently: false, action: XCBActions.getCurrentUser),
|
||||
XCBRequestSpec(type: .followUser, arguments: ["accountID": true, "accountURL": true, "acct": true], canRunSilently: true, action: XCBActions.followUser),
|
||||
// Search
|
||||
XCBRequestSpec(type: .search, arguments: ["query": false], canRunSilently: false, action: XCBActions.search),
|
||||
]
|
||||
|
||||
static var currentSession: XCBSession?
|
||||
|
||||
static func handle(url: URL) -> Bool {
|
||||
guard let components = URLComponents(url: url, resolvingAgainstBaseURL: true) else { return false }
|
||||
if let spec = specs.first(where: { $0.matches(components) }) {
|
||||
let request = XCBRequest(spec: spec, components: components)
|
||||
return spec.handle(request: request)
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
static func createSession(type: XCBActionType, request: XCBRequest) -> XCBSession {
|
||||
let session = XCBSession(type: type, request: request)
|
||||
currentSession = session
|
||||
return session
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,51 @@
|
|||
//
|
||||
// XCBRequest.swift
|
||||
// Tusker
|
||||
//
|
||||
// Created by Shadowfacts on 9/25/18.
|
||||
// Copyright © 2018 Shadowfacts. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
struct XCBRequest {
|
||||
let path: String
|
||||
let arguments: [String: String]
|
||||
let json: Bool
|
||||
let silent: Bool
|
||||
let source: String?
|
||||
let success: URL?
|
||||
let error: URL?
|
||||
let cancel: URL?
|
||||
|
||||
init(spec: XCBRequestSpec, components: URLComponents) {
|
||||
self.path = spec.path
|
||||
if let queryItems = components.queryItems {
|
||||
self.arguments = spec.arguments.reduce(into: [String: String](), { (result, el) in
|
||||
if let value = queryItems.first(where: { $0.name == el.key })?.value {
|
||||
result[el.key] = value
|
||||
}
|
||||
})
|
||||
source = queryItems.first(where: { $0.name == "x-source" }).flatMap { $0.value }
|
||||
success = queryItems.first(where: { $0.name == "x-success" }).flatMap { $0.value }.flatMap { URL(string: $0) }
|
||||
error = queryItems.first(where: { $0.name == "x-error" }).flatMap { $0.value }.flatMap { URL(string: $0) }
|
||||
cancel = queryItems.first(where: { $0.name == "x-cancel" }).flatMap { $0.value }.flatMap { URL(string: $0) }
|
||||
} else {
|
||||
self.arguments = [:]
|
||||
source = nil
|
||||
success = nil
|
||||
error = nil
|
||||
cancel = nil
|
||||
}
|
||||
if let arg = arguments["json"] {
|
||||
json = Bool(arg) ?? false
|
||||
} else {
|
||||
json = false
|
||||
}
|
||||
if spec.canRunSilently, let arg = arguments["silent"] {
|
||||
silent = Bool(arg) ?? false
|
||||
} else {
|
||||
silent = false
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,79 @@
|
|||
//
|
||||
// XCallbackURL.swift
|
||||
// Tusker
|
||||
//
|
||||
// Created by Shadowfacts on 9/23/18.
|
||||
// Copyright © 2018 Shadowfacts. All rights reserved.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
|
||||
typealias XCBAction = (_ url: XCBRequest, _ session: XCBSession, _ silent: Bool?) -> Void
|
||||
|
||||
struct XCBRequestSpec {
|
||||
|
||||
let path: String
|
||||
let type: XCBActionType
|
||||
let arguments: [String: Bool]
|
||||
let canRunSilently: Bool
|
||||
let action: XCBAction
|
||||
|
||||
init(type: XCBActionType, arguments: [String: Bool], canRunSilently: Bool, action: @escaping XCBAction) {
|
||||
self.path = type.path
|
||||
self.type = type
|
||||
self.canRunSilently = canRunSilently
|
||||
self.action = action
|
||||
var arguments = arguments
|
||||
if canRunSilently {
|
||||
arguments["silent"] = true
|
||||
}
|
||||
arguments["json"] = true
|
||||
self.arguments = arguments
|
||||
}
|
||||
|
||||
func handle(request: XCBRequest) -> Bool {
|
||||
let session = XCBManager.createSession(type: type, request: request)
|
||||
if canRunSilently && request.silent {
|
||||
if let source = request.source {
|
||||
let permission = Preferences.shared.silentActions[source] ?? .undecided
|
||||
switch permission {
|
||||
case .accepted:
|
||||
action(request, session, true)
|
||||
case .rejected:
|
||||
action(request, session, false)
|
||||
case .undecided:
|
||||
let alert = UIAlertController(title: "\(source) wants to perform actions silently", message: "Accepting will allow \(source) to perform actions without your confirmation, rejecting will always prompt for confirmation.", preferredStyle: .alert)
|
||||
alert.addAction(UIAlertAction(title: "Accept", style: .default, handler: { (_) in
|
||||
Preferences.shared.silentActions[source] = .accepted
|
||||
self.action(request, session, true)
|
||||
}))
|
||||
alert.addAction(UIAlertAction(title: "Reject", style: .default, handler: { (_) in
|
||||
Preferences.shared.silentActions[source] = .rejected
|
||||
self.action(request, session, false)
|
||||
}))
|
||||
UIApplication.shared.keyWindow!.rootViewController!.present(alert, animated: true)
|
||||
}
|
||||
} else {
|
||||
session.complete(with: .error, additionalData: [
|
||||
"error": "Cannot perform silent action without source app, x-source parameter must be specified."
|
||||
])
|
||||
}
|
||||
} else {
|
||||
action(request, session, nil)
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension XCBRequestSpec {
|
||||
func matches(_ components: URLComponents) -> Bool {
|
||||
guard path == components.path else { return false }
|
||||
for (name, optional) in arguments {
|
||||
if (!optional && components.queryItems?.first(where: { $0.name == name }) == nil) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
|
@ -0,0 +1,45 @@
|
|||
//
|
||||
// XCBSession.swift
|
||||
// Tusker
|
||||
//
|
||||
// Created by Shadowfacts on 9/23/18.
|
||||
// Copyright © 2018 Shadowfacts. All rights reserved.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
|
||||
class XCBSession {
|
||||
static let encoder = JSONEncoder()
|
||||
|
||||
let type: XCBActionType
|
||||
let request: XCBRequest
|
||||
|
||||
init(type: XCBActionType, request: XCBRequest) {
|
||||
self.type = type
|
||||
self.request = request
|
||||
}
|
||||
|
||||
func complete(with result: XCBSessionResult, additionalData: [String: String?]? = nil) {
|
||||
guard var url = result == .success ? request.success : result == .error ? request.error : request.cancel else { return }
|
||||
XCBManager.currentSession = nil
|
||||
if let additionalData = additionalData {
|
||||
var components = URLComponents(url: url, resolvingAgainstBaseURL: true)!
|
||||
components.queryItems = components.queryItems ?? []
|
||||
if request.json {
|
||||
let data = try! XCBSession.encoder.encode(additionalData)
|
||||
let response = String(data: data, encoding: .utf8)
|
||||
components.queryItems!.append(URLQueryItem(name: "response", value: response))
|
||||
} else {
|
||||
components.queryItems!.append(contentsOf: additionalData.map(URLQueryItem.init))
|
||||
}
|
||||
url = components.url!
|
||||
}
|
||||
DispatchQueue.main.async {
|
||||
UIApplication.shared.open(url, options: [:])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum XCBSessionResult {
|
||||
case success, error, cancel
|
||||
}
|
Loading…
Reference in New Issue