diff --git a/Pachyderm/Client.swift b/Pachyderm/Client.swift index 89c9a573..e0de6b61 100644 --- a/Pachyderm/Client.swift +++ b/Pachyderm/Client.swift @@ -130,32 +130,32 @@ public class Client { } // MARK: - Self - public func getSelfAccount() -> Request { + public static func getSelfAccount() -> Request { return Request(method: .get, path: "/api/v1/accounts/verify_credentials") } - public func getFavourites() -> Request<[Status]> { + public static func getFavourites() -> Request<[Status]> { return Request<[Status]>(method: .get, path: "/api/v1/favourites") } - public func getRelationships(accounts: [String]? = nil) -> Request<[Relationship]> { + public static func getRelationships(accounts: [String]? = nil) -> Request<[Relationship]> { return Request<[Relationship]>(method: .get, path: "/api/v1/accounts/relationships", queryParameters: "id" => accounts) } - public func getInstance() -> Request { + public static func getInstance() -> Request { return Request(method: .get, path: "/api/v1/instance") } - public func getCustomEmoji() -> Request<[Emoji]> { + public static func getCustomEmoji() -> Request<[Emoji]> { return Request<[Emoji]>(method: .get, path: "/api/v1/custom_emojis") } // MARK: - Accounts - public func getAccount(id: String) -> Request { + public static func getAccount(id: String) -> Request { return Request(method: .get, path: "/api/v1/accounts/\(id)") } - public func searchForAccount(query: String, limit: Int? = nil, following: Bool? = nil) -> Request<[Account]> { + public static func searchForAccount(query: String, limit: Int? = nil, following: Bool? = nil) -> Request<[Account]> { return Request<[Account]>(method: .get, path: "/api/v1/accounts/search", queryParameters: [ "q" => query, "limit" => limit, @@ -164,32 +164,32 @@ public class Client { } // MARK: - Blocks - public func getBlocks() -> Request<[Account]> { + public static func getBlocks() -> Request<[Account]> { return Request<[Account]>(method: .get, path: "/api/v1/blocks") } - public func getDomainBlocks() -> Request<[String]> { + public static func getDomainBlocks() -> Request<[String]> { return Request<[String]>(method: .get, path: "api/v1/domain_blocks") } - public func block(domain: String) -> Request { + public static func block(domain: String) -> Request { return Request(method: .post, path: "/api/v1/domain_blocks", body: .parameters([ "domain" => domain ])) } - public func unblock(domain: String) -> Request { + public static func unblock(domain: String) -> Request { return Request(method: .delete, path: "/api/v1/domain_blocks", body: .parameters([ "domain" => domain ])) } // MARK: - Filters - public func getFilters() -> Request<[Filter]> { + public static func getFilters() -> Request<[Filter]> { return Request<[Filter]>(method: .get, path: "/api/v1/filters") } - public func createFilter(phrase: String, context: [Filter.Context], irreversible: Bool? = nil, wholeWord: Bool? = nil, expiresAt: Date? = nil) -> Request { + public static func createFilter(phrase: String, context: [Filter.Context], irreversible: Bool? = nil, wholeWord: Bool? = nil, expiresAt: Date? = nil) -> Request { return Request(method: .post, path: "/api/v1/filters", body: .parameters([ "phrase" => phrase, "irreversible" => irreversible, @@ -198,40 +198,40 @@ public class Client { ] + "context" => context.contextStrings)) } - public func getFilter(id: String) -> Request { + public static func getFilter(id: String) -> Request { return Request(method: .get, path: "/api/v1/filters/\(id)") } // MARK: - Follows - public func getFollowRequests(range: RequestRange = .default) -> Request<[Account]> { + public static func getFollowRequests(range: RequestRange = .default) -> Request<[Account]> { var request = Request<[Account]>(method: .get, path: "/api/v1/follow_requests") request.range = range return request } - public func getFollowSuggestions() -> Request<[Account]> { + public static func getFollowSuggestions() -> Request<[Account]> { return Request<[Account]>(method: .get, path: "/api/v1/suggestions") } - public func followRemote(acct: String) -> Request { + public static func followRemote(acct: String) -> Request { return Request(method: .post, path: "/api/v1/follows", body: .parameters(["uri" => acct])) } // MARK: - Lists - public func getLists() -> Request<[List]> { + public static func getLists() -> Request<[List]> { return Request<[List]>(method: .get, path: "/api/v1/lists") } - public func getList(id: String) -> Request { + public static func getList(id: String) -> Request { return Request(method: .get, path: "/api/v1/lists/\(id)") } - public func createList(title: String) -> Request { + public static func createList(title: String) -> Request { return Request(method: .post, path: "/api/v1/lists", body: .parameters(["title" => title])) } // MARK: - Media - public func upload(attachment: FormAttachment, description: String? = nil, focus: (Float, Float)? = nil) -> Request { + public static func upload(attachment: FormAttachment, description: String? = nil, focus: (Float, Float)? = nil) -> Request { return Request(method: .post, path: "/api/v1/media", body: .formData([ "description" => description, "focus" => focus @@ -239,14 +239,14 @@ public class Client { } // MARK: - Mutes - public func getMutes(range: RequestRange) -> Request<[Account]> { + public static func getMutes(range: RequestRange) -> Request<[Account]> { var request = Request<[Account]>(method: .get, path: "/api/v1/mutes") request.range = range return request } // MARK: - Notifications - public func getNotifications(excludeTypes: [Notification.Kind], range: RequestRange = .default) -> Request<[Notification]> { + public static func getNotifications(excludeTypes: [Notification.Kind], range: RequestRange = .default) -> Request<[Notification]> { var request = Request<[Notification]>(method: .get, path: "/api/v1/notifications", queryParameters: "exclude_types" => excludeTypes.map { $0.rawValue } ) @@ -254,16 +254,16 @@ public class Client { return request } - public func clearNotifications() -> Request { + public static func clearNotifications() -> Request { return Request(method: .post, path: "/api/v1/notifications/clear") } // MARK: - Reports - public func getReports() -> Request<[Report]> { + public static func getReports() -> Request<[Report]> { return Request<[Report]>(method: .get, path: "/api/v1/reports") } - public func report(account: Account, statuses: [Status], comment: String) -> Request { + public static func report(account: Account, statuses: [Status], comment: String) -> Request { return Request(method: .post, path: "/api/v1/reports", body: .parameters([ "account_id" => account.id, "comment" => comment @@ -271,7 +271,7 @@ public class Client { } // MARK: - Search - public func search(query: String, resolve: Bool? = nil, limit: Int? = nil) -> Request { + public static func search(query: String, resolve: Bool? = nil, limit: Int? = nil) -> Request { return Request(method: .get, path: "/api/v2/search", queryParameters: [ "q" => query, "resolve" => resolve, @@ -280,18 +280,18 @@ public class Client { } // MARK: - Statuses - public func getStatus(id: String) -> Request { + public static func getStatus(id: String) -> Request { return Request(method: .get, path: "/api/v1/statuses/\(id)") } - public func createStatus(text: String, - contentType: StatusContentType = .plain, - inReplyTo: String? = nil, - media: [Attachment]? = nil, - sensitive: Bool? = nil, - spoilerText: String? = nil, - visibility: Status.Visibility? = nil, - language: String? = nil) -> Request { + public static func createStatus(text: String, + contentType: StatusContentType = .plain, + inReplyTo: String? = nil, + media: [Attachment]? = nil, + sensitive: Bool? = nil, + spoilerText: String? = nil, + visibility: Status.Visibility? = nil, + language: String? = nil) -> Request { return Request(method: .post, path: "/api/v1/statuses", body: .parameters([ "status" => text, "content_type" => contentType.mimeType, @@ -304,13 +304,13 @@ public class Client { } // MARK: - Timelines - public func getStatuses(timeline: Timeline, range: RequestRange = .default) -> Request<[Status]> { + public static func getStatuses(timeline: Timeline, range: RequestRange = .default) -> Request<[Status]> { return timeline.request(range: range) } // MARK: Bookmarks - public func getBookmarks(range: RequestRange = .default) -> Request<[Status]> { + public static func getBookmarks(range: RequestRange = .default) -> Request<[Status]> { var request = Request<[Status]>(method: .get, path: "/api/v1/bookmarks") request.range = range return request diff --git a/Tusker.xcodeproj/project.pbxproj b/Tusker.xcodeproj/project.pbxproj index 60b99cc3..2521db92 100644 --- a/Tusker.xcodeproj/project.pbxproj +++ b/Tusker.xcodeproj/project.pbxproj @@ -122,6 +122,7 @@ D64BC18A23C16487000D0238 /* UnpinStatusActivity.swift in Sources */ = {isa = PBXBuildFile; fileRef = D64BC18923C16487000D0238 /* UnpinStatusActivity.swift */; }; D64BC18F23C18B9D000D0238 /* FollowRequestNotificationTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D64BC18D23C18B9D000D0238 /* FollowRequestNotificationTableViewCell.swift */; }; D64BC19023C18B9D000D0238 /* FollowRequestNotificationTableViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = D64BC18E23C18B9D000D0238 /* FollowRequestNotificationTableViewCell.xib */; }; + D64BC19223C271D9000D0238 /* MastodonActivity.swift in Sources */ = {isa = PBXBuildFile; fileRef = D64BC19123C271D9000D0238 /* MastodonActivity.swift */; }; D64D0AAD2128D88B005A6F37 /* LocalData.swift in Sources */ = {isa = PBXBuildFile; fileRef = D64D0AAC2128D88B005A6F37 /* LocalData.swift */; }; D64D0AB12128D9AE005A6F37 /* OnboardingViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D64D0AB02128D9AE005A6F37 /* OnboardingViewController.swift */; }; D64F80E2215875CC00BEF393 /* XCBActionType.swift in Sources */ = {isa = PBXBuildFile; fileRef = D64F80E1215875CC00BEF393 /* XCBActionType.swift */; }; @@ -156,10 +157,9 @@ D67C57B221E28FAD00C3118B /* ComposeStatusReplyView.xib in Resources */ = {isa = PBXBuildFile; fileRef = D67C57B121E28FAD00C3118B /* ComposeStatusReplyView.xib */; }; D67C57B421E2910700C3118B /* ComposeStatusReplyView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D67C57B321E2910700C3118B /* ComposeStatusReplyView.swift */; }; D68FEC4F232C5BC300C84F23 /* SegmentedPageViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D68FEC4E232C5BC300C84F23 /* SegmentedPageViewController.swift */; }; - D6945C2F23AC47C3005C403C /* SavedHashtagsManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6945C2E23AC47C3005C403C /* SavedHashtagsManager.swift */; }; + D6945C2F23AC47C3005C403C /* SavedDataManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6945C2E23AC47C3005C403C /* SavedDataManager.swift */; }; D6945C3223AC4D36005C403C /* HashtagTimelineViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6945C3123AC4D36005C403C /* HashtagTimelineViewController.swift */; }; D6945C3423AC6431005C403C /* AddSavedHashtagViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6945C3323AC6431005C403C /* AddSavedHashtagViewController.swift */; }; - D6945C3623AC6C09005C403C /* SavedInstancesManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6945C3523AC6C09005C403C /* SavedInstancesManager.swift */; }; D6945C3823AC739F005C403C /* InstanceTimelineViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6945C3723AC739F005C403C /* InstanceTimelineViewController.swift */; }; D6945C3A23AC75E2005C403C /* FindInstanceViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6945C3923AC75E2005C403C /* FindInstanceViewController.swift */; }; D6A3BC7723218E1300FD64D5 /* TimelineSegment.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6A3BC7323218C6E00FD64D5 /* TimelineSegment.swift */; }; @@ -177,6 +177,7 @@ D6A5BB2F23BBAC97003BF21D /* DelegatingResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6A5BB2E23BBAC97003BF21D /* DelegatingResponse.swift */; }; D6A5BB3123BBAD87003BF21D /* JSONResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6A5BB3023BBAD87003BF21D /* JSONResponse.swift */; }; D6A5FAF1217B7E05003DB2D9 /* ComposeViewController.xib in Resources */ = {isa = PBXBuildFile; fileRef = D6A5FAF0217B7E05003DB2D9 /* ComposeViewController.xib */; }; + D6AC956723C4347E008C9946 /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6AC956623C4347E008C9946 /* SceneDelegate.swift */; }; D6AEBB3E2321638100E5038B /* UIActivity+Types.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6AEBB3D2321638100E5038B /* UIActivity+Types.swift */; }; D6AEBB412321642700E5038B /* SendMesasgeActivity.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6AEBB402321642700E5038B /* SendMesasgeActivity.swift */; }; D6AEBB432321685E00E5038B /* OpenInSafariActivity.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6AEBB422321685E00E5038B /* OpenInSafariActivity.swift */; }; @@ -217,6 +218,8 @@ D6E0DC8E216EDF1E00369478 /* Previewing.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6E0DC8D216EDF1E00369478 /* Previewing.swift */; }; D6E6F26321603F8B006A8599 /* CharacterCounter.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6E6F26221603F8B006A8599 /* CharacterCounter.swift */; }; D6E6F26521604242006A8599 /* CharacterCounterTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6E6F26421604242006A8599 /* CharacterCounterTests.swift */; }; + D6EBF01523C55C0900AE061B /* UIApplication+Scenes.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6EBF01423C55C0900AE061B /* UIApplication+Scenes.swift */; }; + D6EBF01723C55E0D00AE061B /* UISceneSession+MastodonController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6EBF01623C55E0D00AE061B /* UISceneSession+MastodonController.swift */; }; D6F1F84D2193B56E00F5FE67 /* Cache.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6F1F84C2193B56E00F5FE67 /* Cache.swift */; }; D6F953EC212519E700CF0F2B /* TimelineTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6F953EB212519E700CF0F2B /* TimelineTableViewController.swift */; }; D6F953F021251A2900CF0F2B /* MastodonController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6F953EF21251A2900CF0F2B /* MastodonController.swift */; }; @@ -394,6 +397,7 @@ D64BC18923C16487000D0238 /* UnpinStatusActivity.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UnpinStatusActivity.swift; sourceTree = ""; }; D64BC18D23C18B9D000D0238 /* FollowRequestNotificationTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FollowRequestNotificationTableViewCell.swift; sourceTree = ""; }; D64BC18E23C18B9D000D0238 /* FollowRequestNotificationTableViewCell.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = FollowRequestNotificationTableViewCell.xib; sourceTree = ""; }; + D64BC19123C271D9000D0238 /* MastodonActivity.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonActivity.swift; sourceTree = ""; }; D64D0AAC2128D88B005A6F37 /* LocalData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalData.swift; sourceTree = ""; }; D64D0AB02128D9AE005A6F37 /* OnboardingViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingViewController.swift; sourceTree = ""; }; D64F80E1215875CC00BEF393 /* XCBActionType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XCBActionType.swift; sourceTree = ""; }; @@ -429,10 +433,9 @@ D67C57B121E28FAD00C3118B /* ComposeStatusReplyView.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = ComposeStatusReplyView.xib; sourceTree = ""; }; D67C57B321E2910700C3118B /* ComposeStatusReplyView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeStatusReplyView.swift; sourceTree = ""; }; D68FEC4E232C5BC300C84F23 /* SegmentedPageViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SegmentedPageViewController.swift; sourceTree = ""; }; - D6945C2E23AC47C3005C403C /* SavedHashtagsManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SavedHashtagsManager.swift; sourceTree = ""; }; + D6945C2E23AC47C3005C403C /* SavedDataManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SavedDataManager.swift; sourceTree = ""; }; D6945C3123AC4D36005C403C /* HashtagTimelineViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HashtagTimelineViewController.swift; sourceTree = ""; }; D6945C3323AC6431005C403C /* AddSavedHashtagViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddSavedHashtagViewController.swift; sourceTree = ""; }; - D6945C3523AC6C09005C403C /* SavedInstancesManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SavedInstancesManager.swift; sourceTree = ""; }; D6945C3723AC739F005C403C /* InstanceTimelineViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InstanceTimelineViewController.swift; sourceTree = ""; }; D6945C3923AC75E2005C403C /* FindInstanceViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = FindInstanceViewController.swift; path = Tusker/Screens/FindInstanceViewController.swift; sourceTree = SOURCE_ROOT; }; D6A3BC7323218C6E00FD64D5 /* TimelineSegment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineSegment.swift; sourceTree = ""; }; @@ -450,6 +453,7 @@ D6A5BB2E23BBAC97003BF21D /* DelegatingResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DelegatingResponse.swift; sourceTree = ""; }; D6A5BB3023BBAD87003BF21D /* JSONResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JSONResponse.swift; sourceTree = ""; }; D6A5FAF0217B7E05003DB2D9 /* ComposeViewController.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = ComposeViewController.xib; sourceTree = ""; }; + D6AC956623C4347E008C9946 /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = ""; }; D6AEBB3D2321638100E5038B /* UIActivity+Types.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIActivity+Types.swift"; sourceTree = ""; }; D6AEBB402321642700E5038B /* SendMesasgeActivity.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SendMesasgeActivity.swift; sourceTree = ""; }; D6AEBB422321685E00E5038B /* OpenInSafariActivity.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OpenInSafariActivity.swift; sourceTree = ""; }; @@ -494,6 +498,8 @@ D6E0DC8D216EDF1E00369478 /* Previewing.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Previewing.swift; sourceTree = ""; }; D6E6F26221603F8B006A8599 /* CharacterCounter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CharacterCounter.swift; sourceTree = ""; }; D6E6F26421604242006A8599 /* CharacterCounterTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CharacterCounterTests.swift; sourceTree = ""; }; + D6EBF01423C55C0900AE061B /* UIApplication+Scenes.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIApplication+Scenes.swift"; sourceTree = ""; }; + D6EBF01623C55E0D00AE061B /* UISceneSession+MastodonController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UISceneSession+MastodonController.swift"; sourceTree = ""; }; D6F1F84C2193B56E00F5FE67 /* Cache.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Cache.swift; sourceTree = ""; }; D6F953EB212519E700CF0F2B /* TimelineTableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineTableViewController.swift; sourceTree = ""; }; D6F953EF21251A2900CF0F2B /* MastodonController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonController.swift; sourceTree = ""; }; @@ -967,6 +973,8 @@ D67C57AE21E28EAD00C3118B /* Array+Uniques.swift */, 0450531E22B0097E00100BA2 /* Timline+UI.swift */, D66A77BA233838DC0058F1EC /* UIFont+Traits.swift */, + D6EBF01423C55C0900AE061B /* UIApplication+Scenes.swift */, + D6EBF01623C55E0D00AE061B /* UISceneSession+MastodonController.swift */, ); path = Extensions; sourceTree = ""; @@ -1061,6 +1069,7 @@ children = ( D6AEBB3D2321638100E5038B /* UIActivity+Types.swift */, D6AEBB422321685E00E5038B /* OpenInSafariActivity.swift */, + D64BC19123C271D9000D0238 /* MastodonActivity.swift */, D6AEBB4623216B0C00E5038B /* Account Activities */, D627943323A5523800D38C68 /* Status Activities */, ); @@ -1181,10 +1190,10 @@ isa = PBXGroup; children = ( D6D4DDCF212518A000E1C4BB /* AppDelegate.swift */, + D6AC956623C4347E008C9946 /* SceneDelegate.swift */, D64D0AAC2128D88B005A6F37 /* LocalData.swift */, D627FF75217E923E00CC0648 /* DraftsManager.swift */, - D6945C2E23AC47C3005C403C /* SavedHashtagsManager.swift */, - D6945C3523AC6C09005C403C /* SavedInstancesManager.swift */, + D6945C2E23AC47C3005C403C /* SavedDataManager.swift */, D6028B9A2150811100F223B9 /* MastodonCache.swift */, D6C693EE216192C2007D6A6D /* TuskerNavigationDelegate.swift */, D6F1F84E2193B9BE00F5FE67 /* Caching */, @@ -1620,6 +1629,7 @@ 0427033A22B31269000D31B6 /* AdvancedPrefsView.swift in Sources */, D626493C23C1000300612E6E /* AlbumTableViewCell.swift in Sources */, D6757A822157E8FA00721E32 /* XCBSession.swift in Sources */, + D6EBF01723C55E0D00AE061B /* UISceneSession+MastodonController.swift in Sources */, 04DACE8C212CB14B009840C4 /* MainTabBarViewController.swift in Sources */, D6AEBB412321642700E5038B /* SendMesasgeActivity.swift in Sources */, D6BC9DB1232C61BC002CA326 /* NotificationsPageViewController.swift in Sources */, @@ -1631,7 +1641,6 @@ D68FEC4F232C5BC300C84F23 /* SegmentedPageViewController.swift in Sources */, 0450531F22B0097E00100BA2 /* Timline+UI.swift in Sources */, D667E5F52135BCD50057A976 /* ConversationTableViewController.swift in Sources */, - D6945C3623AC6C09005C403C /* SavedInstancesManager.swift in Sources */, D6C7D27D22B6EBF800071952 /* AttachmentsContainerView.swift in Sources */, D620483823D38190008A63EF /* StatusContentTextView.swift in Sources */, D6F953F021251A2900CF0F2B /* MastodonController.swift in Sources */, @@ -1642,6 +1651,7 @@ D627943723A552C200D38C68 /* BookmarkStatusActivity.swift in Sources */, D62D2426217ABF63005076CC /* UserActivityType.swift in Sources */, D66362712136338600C9CBA2 /* ComposeViewController.swift in Sources */, + D6AC956723C4347E008C9946 /* SceneDelegate.swift in Sources */, D6BC9DD7232D7811002CA326 /* TimelinesPageViewController.swift in Sources */, D620483623D38075008A63EF /* ContentTextView.swift in Sources */, D6028B9B2150811100F223B9 /* MastodonCache.swift in Sources */, @@ -1676,15 +1686,17 @@ D63661C02381C144004B9E16 /* PreferencesNavigationController.swift in Sources */, D6B053A223BD2C0600A066FA /* AssetPickerViewController.swift in Sources */, D627944A23A6AD6100D38C68 /* BookmarksTableViewController.swift in Sources */, + D64BC19223C271D9000D0238 /* MastodonActivity.swift in Sources */, D6945C3A23AC75E2005C403C /* FindInstanceViewController.swift in Sources */, D6AEBB4523216AF800E5038B /* FollowAccountActivity.swift in Sources */, + D6EBF01523C55C0900AE061B /* UIApplication+Scenes.swift in Sources */, D6538945214D6D7500E3CEFC /* TableViewSwipeActionProvider.swift in Sources */, D6E0DC8E216EDF1E00369478 /* Previewing.swift in Sources */, D6BED174212667E900F02DA0 /* TimelineStatusTableViewCell.swift in Sources */, 0427033822B30F5F000D31B6 /* BehaviorPrefsView.swift in Sources */, D627943923A553B600D38C68 /* UnbookmarkStatusActivity.swift in Sources */, D64D0AAD2128D88B005A6F37 /* LocalData.swift in Sources */, - D6945C2F23AC47C3005C403C /* SavedHashtagsManager.swift in Sources */, + D6945C2F23AC47C3005C403C /* SavedDataManager.swift in Sources */, D6C94D892139E6EC00CB5196 /* AttachmentView.swift in Sources */, D6B053AE23BD322B00A066FA /* AssetPickerSheetContainerViewController.swift in Sources */, D6C693EF216192C2007D6A6D /* TuskerNavigationDelegate.swift in Sources */, diff --git a/Tusker/Activities/Account Activities/AccountActivity.swift b/Tusker/Activities/Account Activities/AccountActivity.swift index e12e2233..1b91a98e 100644 --- a/Tusker/Activities/Account Activities/AccountActivity.swift +++ b/Tusker/Activities/Account Activities/AccountActivity.swift @@ -9,7 +9,7 @@ import UIKit import Pachyderm -class AccountActivity: UIActivity { +class AccountActivity: MastodonActivity { override class var activityCategory: UIActivity.Category { return .action diff --git a/Tusker/Activities/Account Activities/FollowAccountActivity.swift b/Tusker/Activities/Account Activities/FollowAccountActivity.swift index 6747ec0f..ae89aa7e 100644 --- a/Tusker/Activities/Account Activities/FollowAccountActivity.swift +++ b/Tusker/Activities/Account Activities/FollowAccountActivity.swift @@ -28,9 +28,9 @@ class FollowAccountActivity: AccountActivity { UIImpactFeedbackGenerator(style: .medium).impactOccurred() let request = Account.follow(account.id) - MastodonController.client.run(request) { (response) in + mastodonController.run(request) { (response) in if case let .success(relationship, _) = response { - MastodonCache.add(relationship: relationship) + self.mastodonController.cache.add(relationship: relationship) } else { // todo: display error message UINotificationFeedbackGenerator().notificationOccurred(.error) diff --git a/Tusker/Activities/Account Activities/SendMesasgeActivity.swift b/Tusker/Activities/Account Activities/SendMesasgeActivity.swift index 0238b0ab..ea67c0b0 100644 --- a/Tusker/Activities/Account Activities/SendMesasgeActivity.swift +++ b/Tusker/Activities/Account Activities/SendMesasgeActivity.swift @@ -28,7 +28,7 @@ class SendMessageActivity: AccountActivity { override var activityViewController: UIViewController? { guard let account = account else { return nil } - return UINavigationController(rootViewController: ComposeViewController(mentioningAcct: account.acct)) + return UINavigationController(rootViewController: ComposeViewController(mentioningAcct: account.acct, mastodonController: mastodonController)) } } diff --git a/Tusker/Activities/Account Activities/UnfollowAccountActivity.swift b/Tusker/Activities/Account Activities/UnfollowAccountActivity.swift index 303042a5..493923d7 100644 --- a/Tusker/Activities/Account Activities/UnfollowAccountActivity.swift +++ b/Tusker/Activities/Account Activities/UnfollowAccountActivity.swift @@ -28,9 +28,9 @@ class UnfollowAccountActivity: AccountActivity { UIImpactFeedbackGenerator(style: .medium).impactOccurred() let request = Account.unfollow(account.id) - MastodonController.client.run(request) { (response) in + mastodonController.run(request) { (response) in if case let .success(relationship, _) = response { - MastodonCache.add(relationship: relationship) + self.mastodonController.cache.add(relationship: relationship) } else { // todo: display error message UINotificationFeedbackGenerator().notificationOccurred(.error) diff --git a/Tusker/Activities/MastodonActivity.swift b/Tusker/Activities/MastodonActivity.swift new file mode 100644 index 00000000..25fd5cc7 --- /dev/null +++ b/Tusker/Activities/MastodonActivity.swift @@ -0,0 +1,16 @@ +// +// MastodonActivity.swift +// Tusker +// +// Created by Shadowfacts on 1/5/20. +// Copyright © 2020 Shadowfacts. All rights reserved. +// + +import UIKit + +class MastodonActivity: UIActivity { + var mastodonController: MastodonController { + let scene = UIApplication.shared.activeOrBackgroundScene! + return scene.session.mastodonController! + } +} diff --git a/Tusker/Activities/Status Activities/BookmarkStatusActivity.swift b/Tusker/Activities/Status Activities/BookmarkStatusActivity.swift index b553af06..585f3471 100644 --- a/Tusker/Activities/Status Activities/BookmarkStatusActivity.swift +++ b/Tusker/Activities/Status Activities/BookmarkStatusActivity.swift @@ -27,9 +27,9 @@ class BookmarkStatusActivity: StatusActivity { guard let status = status else { return } let request = Status.bookmark(status) - MastodonController.client.run(request) { (response) in + mastodonController.run(request) { (response) in if case let .success(status, _) = response { - MastodonCache.add(status: status) + self.mastodonController.cache.add(status: status) } else { // todo: display error message UINotificationFeedbackGenerator().notificationOccurred(.error) diff --git a/Tusker/Activities/Status Activities/PinStatusActivity.swift b/Tusker/Activities/Status Activities/PinStatusActivity.swift index 66c56c10..40ef6cfd 100644 --- a/Tusker/Activities/Status Activities/PinStatusActivity.swift +++ b/Tusker/Activities/Status Activities/PinStatusActivity.swift @@ -26,9 +26,9 @@ class PinStatusActivity: StatusActivity { guard let status = status else { return } let request = Status.pin(status) - MastodonController.client.run(request) { (response) in + mastodonController.run(request) { (response) in if case let .success(status, _) = response { - MastodonCache.add(status: status) + self.mastodonController.cache.add(status: status) } else { // todo: display error message UINotificationFeedbackGenerator().notificationOccurred(.error) diff --git a/Tusker/Activities/Status Activities/StatusActivity.swift b/Tusker/Activities/Status Activities/StatusActivity.swift index 715b0a6e..3469b789 100644 --- a/Tusker/Activities/Status Activities/StatusActivity.swift +++ b/Tusker/Activities/Status Activities/StatusActivity.swift @@ -9,7 +9,7 @@ import UIKit import Pachyderm -class StatusActivity: UIActivity { +class StatusActivity: MastodonActivity { override class var activityCategory: UIActivity.Category { return .action diff --git a/Tusker/Activities/Status Activities/UnbookmarkStatusActivity.swift b/Tusker/Activities/Status Activities/UnbookmarkStatusActivity.swift index 0857348d..8cce299b 100644 --- a/Tusker/Activities/Status Activities/UnbookmarkStatusActivity.swift +++ b/Tusker/Activities/Status Activities/UnbookmarkStatusActivity.swift @@ -27,9 +27,9 @@ class UnbookmarkStatusActivity: StatusActivity { guard let status = status else { return } let request = Status.unbookmark(status) - MastodonController.client.run(request) { (response) in + mastodonController.run(request) { (response) in if case let .success(status, _) = response { - MastodonCache.add(status: status) + self.mastodonController.cache.add(status: status) } else { // todo: display error message UINotificationFeedbackGenerator().notificationOccurred(.error) diff --git a/Tusker/Activities/Status Activities/UnpinStatusActivity.swift b/Tusker/Activities/Status Activities/UnpinStatusActivity.swift index 2fc2fa85..a22df120 100644 --- a/Tusker/Activities/Status Activities/UnpinStatusActivity.swift +++ b/Tusker/Activities/Status Activities/UnpinStatusActivity.swift @@ -26,9 +26,9 @@ class UnpinStatusActivity: StatusActivity { guard let status = status else { return } let request = Status.unpin(status) - MastodonController.client.run(request) { (response) in + mastodonController.run(request) { (response) in if case let .success(status, _) = response { - MastodonCache.add(status: status) + self.mastodonController.cache.add(status: status) } else { // todo: display error message UINotificationFeedbackGenerator().notificationOccurred(.error) diff --git a/Tusker/AppDelegate.swift b/Tusker/AppDelegate.swift index 68e6c4a8..1854af62 100644 --- a/Tusker/AppDelegate.swift +++ b/Tusker/AppDelegate.swift @@ -11,117 +11,9 @@ import UIKit @UIApplicationMain class AppDelegate: UIResponder, UIApplicationDelegate { - var window: UIWindow? - func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { AppShortcutItem.createItems(for: application) - - window = UIWindow(frame: UIScreen.main.bounds) - - if LocalData.shared.onboardingComplete { - showAppUI() - } else { - showOnboardingUI() - } - - NotificationCenter.default.addObserver(self, selector: #selector(onUserLoggedOut), name: .userLoggedOut, object: nil) - - NotificationCenter.default.addObserver(self, selector: #selector(themePrefChanged), name: .themePreferenceChanged, object: nil) - themePrefChanged() - - window!.makeKeyAndVisible() - - if let shortcutItem = launchOptions?[.shortcutItem] as? UIApplicationShortcutItem { - _ = AppShortcutItem.handle(shortcutItem) - } - return true } - func application(_ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey : Any] = [:]) -> Bool { - if url.host == "x-callback-url" { - return XCBManager.handle(url: url) - } else if var components = URLComponents(url: url, resolvingAgainstBaseURL: false), - let tabBarController = window!.rootViewController as? MainTabBarViewController, - let exploreNavController = tabBarController.getTabController(tab: .explore) as? UINavigationController, - let exploreController = exploreNavController.viewControllers.first as? ExploreViewController { - - tabBarController.select(tab: .explore) - exploreNavController.popToRootViewController(animated: false) - - exploreController.loadViewIfNeeded() - exploreController.searchController.isActive = true - - components.scheme = "https" - let query = components.url!.absoluteString - exploreController.searchController.searchBar.text = query - exploreController.resultsController.performSearch(query: query) - - return true - } - return false - } - - func application(_ application: UIApplication, continue userActivity: NSUserActivity, restorationHandler: @escaping ([UIUserActivityRestoring]?) -> Void) -> Bool { - return userActivity.handleResume() - } - - func application(_ application: UIApplication, performActionFor shortcutItem: UIApplicationShortcutItem, completionHandler: @escaping (Bool) -> Void) { - completionHandler(AppShortcutItem.handle(shortcutItem)) - } - - func applicationWillResignActive(_ application: UIApplication) { - // Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state. - // Use this method to pause ongoing tasks, disable timers, and invalidate graphics rendering callbacks. Games should use this method to pause the game. - } - - func applicationDidEnterBackground(_ application: UIApplication) { - // Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later. - // If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits. - Preferences.save() - DraftsManager.save() - } - - func applicationWillEnterForeground(_ application: UIApplication) { - // Called as part of the transition from the background to the active state; here you can undo many of the changes made on entering the background. - } - - func applicationDidBecomeActive(_ application: UIApplication) { - // Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface. - } - - func applicationWillTerminate(_ application: UIApplication) { - // Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:. - } - - func showAppUI() { - MastodonController.createClient() - MastodonController.getOwnAccount() - MastodonController.getOwnInstance() - - let tabBarController = MainTabBarViewController() - window!.rootViewController = tabBarController - } - - func showOnboardingUI() { - let onboarding = OnboardingViewController() - onboarding.onboardingDelegate = self - window!.rootViewController = onboarding - } - - @objc func onUserLoggedOut() { - showOnboardingUI() - } - - @objc func themePrefChanged() { - window?.overrideUserInterfaceStyle = Preferences.shared.theme - } - -} - -extension AppDelegate: OnboardingViewControllerDelegate { - func didFinishOnboarding() { - LocalData.shared.onboardingComplete = true - showAppUI() - } } diff --git a/Tusker/Controllers/MastodonController.swift b/Tusker/Controllers/MastodonController.swift index bf376e9b..5e2ae194 100644 --- a/Tusker/Controllers/MastodonController.swift +++ b/Tusker/Controllers/MastodonController.swift @@ -10,64 +10,87 @@ import Foundation import Pachyderm class MastodonController { + + static private(set) var all = [LocalData.UserAccountInfo: MastodonController]() - static var client: Client! + @available(*, message: "do something less dumb") + static var first: MastodonController { all.first!.value } - static var account: Account! - static var instance: Instance! - - private init() {} - - static func createClient() { - guard let url = LocalData.shared.instanceURL else { fatalError("Can't connect without instance URL") } - - client = Client(baseURL: url) - - client.clientID = LocalData.shared.clientID - client.clientSecret = LocalData.shared.clientSecret - client.accessToken = LocalData.shared.accessToken + static func getForAccount(_ account: LocalData.UserAccountInfo) -> MastodonController { + if let controller = all[account] { + return controller + } else { + let controller = MastodonController(instanceURL: account.instanceURL) + controller.accountInfo = account + controller.client.clientID = account.clientID + controller.client.clientSecret = account.clientSecret + controller.client.accessToken = account.accessToken + all[account] = controller + return controller + } } - static func registerApp(completion: @escaping () -> Void) { - guard LocalData.shared.clientID == nil, - LocalData.shared.clientSecret == nil else { - completion() + private(set) lazy var cache = MastodonCache(mastodonController: self) + + let instanceURL: URL + private(set) var accountInfo: LocalData.UserAccountInfo? + + let client: Client! + + var account: Account! + var instance: Instance! + + init(instanceURL: URL) { + self.instanceURL = instanceURL + self.accountInfo = nil + self.client = Client(baseURL: instanceURL) + } + + func run(_ request: Request, completion: @escaping Client.Callback) { + client.run(request, completion: completion) + } + + func registerApp(completion: @escaping (_ clientID: String, _ clientSecret: String) -> Void) { + guard client.clientID == nil, + client.clientSecret == nil else { + + completion(client.clientID!, client.clientSecret!) return } - + client.registerApp(name: "Tusker", redirectURI: "tusker://oauth", scopes: [.read, .write, .follow]) { response in guard case let .success(app, _) = response else { fatalError() } - LocalData.shared.clientID = app.clientID - LocalData.shared.clientSecret = app.clientSecret - completion() + self.client.clientID = app.clientID + self.client.clientSecret = app.clientSecret + completion(app.clientID, app.clientSecret) } } - static func authorize(authorizationCode: String, completion: @escaping () -> Void) { + func authorize(authorizationCode: String, completion: @escaping (_ accessToken: String) -> Void) { client.getAccessToken(authorizationCode: authorizationCode, redirectURI: "tusker://oauth") { response in guard case let .success(settings, _) = response else { fatalError() } - LocalData.shared.accessToken = settings.accessToken - completion() + self.client.accessToken = settings.accessToken + completion(settings.accessToken) } } - static func getOwnAccount(completion: ((Account) -> Void)? = nil) { + func getOwnAccount(completion: ((Account) -> Void)? = nil) { if account != nil { completion?(account) } else { - let request = client.getSelfAccount() - client.run(request) { response in + let request = Client.getSelfAccount() + run(request) { response in guard case let .success(account, _) = response else { fatalError() } self.account = account - MastodonCache.add(account: account) + self.cache.add(account: account) completion?(account) } } } - static func getOwnInstance() { - let request = client.getInstance() - client.run(request) { (response) in + func getOwnInstance() { + let request = Client.getInstance() + run(request) { (response) in guard case let .success(instance, _) = response else { fatalError() } self.instance = instance } diff --git a/Tusker/DraftsManager.swift b/Tusker/DraftsManager.swift index 23e9a5eb..39d182ca 100644 --- a/Tusker/DraftsManager.swift +++ b/Tusker/DraftsManager.swift @@ -39,8 +39,8 @@ class DraftsManager: Codable { return drafts.sorted(by: { $0.lastModified > $1.lastModified }) } - func create(text: String, contentWarning: String?, inReplyToID: String?, attachments: [DraftAttachment]) -> Draft { - let draft = Draft(text: text, contentWarning: contentWarning, inReplyToID: inReplyToID, attachments: attachments) + func create(accountID: String, text: String, contentWarning: String?, inReplyToID: String?, attachments: [DraftAttachment]) -> Draft { + let draft = Draft(accountID: accountID, text: text, contentWarning: contentWarning, inReplyToID: inReplyToID, attachments: attachments) drafts.append(draft) return draft } @@ -55,14 +55,16 @@ class DraftsManager: Codable { extension DraftsManager { class Draft: Codable, Equatable { let id: UUID + private(set) var accountID: String private(set) var text: String private(set) var contentWarning: String? private(set) var attachments: [DraftAttachment] private(set) var inReplyToID: String? private(set) var lastModified: Date - init(text: String, contentWarning: String?, inReplyToID: String?, attachments: [DraftAttachment], lastModified: Date = Date()) { + init(accountID: String, text: String, contentWarning: String?, inReplyToID: String?, attachments: [DraftAttachment], lastModified: Date = Date()) { self.id = UUID() + self.accountID = accountID self.text = text self.contentWarning = contentWarning self.inReplyToID = inReplyToID @@ -70,7 +72,8 @@ extension DraftsManager { self.lastModified = lastModified } - func update(text: String, contentWarning: String?, attachments: [DraftAttachment]) { + func update(accountID: String, text: String, contentWarning: String?, attachments: [DraftAttachment]) { + self.accountID = accountID self.text = text self.contentWarning = contentWarning self.lastModified = Date() diff --git a/Tusker/Extensions/UIApplication+Scenes.swift b/Tusker/Extensions/UIApplication+Scenes.swift new file mode 100644 index 00000000..ce5ab857 --- /dev/null +++ b/Tusker/Extensions/UIApplication+Scenes.swift @@ -0,0 +1,25 @@ +// +// UIApplication+Scenes.swift +// Tusker +// +// Created by Shadowfacts on 1/7/20. +// Copyright © 2020 Shadowfacts. All rights reserved. +// + +import UIKit + +extension UIApplication { + + var activeScene: UIScene? { + connectedScenes.first { $0.activationState == .foregroundActive } + } + + var backgroundScene: UIScene? { + connectedScenes.first { $0.activationState == .background } + } + + var activeOrBackgroundScene: UIScene? { + activeScene ?? backgroundScene + } + +} diff --git a/Tusker/Extensions/UISceneSession+MastodonController.swift b/Tusker/Extensions/UISceneSession+MastodonController.swift new file mode 100644 index 00000000..efa4e638 --- /dev/null +++ b/Tusker/Extensions/UISceneSession+MastodonController.swift @@ -0,0 +1,32 @@ +// +// UISceneSession+MastodonController.swift +// Tusker +// +// Created by Shadowfacts on 1/7/20. +// Copyright © 2020 Shadowfacts. All rights reserved. +// + +import UIKit + +extension UISceneSession { + + var mastodonController: MastodonController? { + get { + return userInfo?["mastodonController"] as? MastodonController + } + set { + if let newValue = newValue { + if userInfo == nil { + userInfo = ["mastodonController": newValue] + } else { + userInfo!["mastodonController"] = newValue + } + } else { + if userInfo != nil { + userInfo?.removeValue(forKey: "mastodonController") + } + } + } + } + +} diff --git a/Tusker/Info.plist b/Tusker/Info.plist index 7a90b6e8..60a0a65f 100644 --- a/Tusker/Info.plist +++ b/Tusker/Info.plist @@ -2,25 +2,6 @@ - NSAppTransportSecurity - - NSExceptionDomains - - localhost - - NSExceptionAllowsInsecureHTTPLoads - - - - - NSUserActivityTypes - - $(PRODUCT_BUNDLE_IDENTIFIER).activity.show-timeline - $(PRODUCT_BUNDLE_IDENTIFIER).activity.check-notifications - $(PRODUCT_BUNDLE_IDENTIFIER).activity.check-mentions - $(PRODUCT_BUNDLE_IDENTIFIER).activity.new-post - $(PRODUCT_BUNDLE_IDENTIFIER).activity.search - CFBundleDevelopmentRegion $(DEVELOPMENT_LANGUAGE) CFBundleExecutable @@ -52,14 +33,52 @@ 1 LSRequiresIPhoneOS - NSMicrophoneUsageDescription - Post videos from the camera. + NSAppTransportSecurity + + NSExceptionDomains + + localhost + + NSExceptionAllowsInsecureHTTPLoads + + + + NSCameraUsageDescription Post photos and videos from the camera. + NSMicrophoneUsageDescription + Post videos from the camera. NSPhotoLibraryAddUsageDescription Save photos directly from other people's posts. NSPhotoLibraryUsageDescription Post photos from the photo library. + NSUserActivityTypes + + $(PRODUCT_BUNDLE_IDENTIFIER).activity.show-timeline + $(PRODUCT_BUNDLE_IDENTIFIER).activity.check-notifications + $(PRODUCT_BUNDLE_IDENTIFIER).activity.check-mentions + $(PRODUCT_BUNDLE_IDENTIFIER).activity.new-post + $(PRODUCT_BUNDLE_IDENTIFIER).activity.search + + UIApplicationSceneManifest + + UISceneConfigurations + + UIWindowSceneSessionRoleApplication + + + UISceneClassName + UIWindowScene + UISceneDelegateClassName + $(PRODUCT_MODULE_NAME).SceneDelegate + UISceneConfigurationName + main-scene + + + + UIApplicationSupportsMultipleScenes + + UILaunchStoryboardName LaunchScreen UIRequiredDeviceCapabilities diff --git a/Tusker/LocalData.swift b/Tusker/LocalData.swift index f8ce0b10..f1fbfbd3 100644 --- a/Tusker/LocalData.swift +++ b/Tusker/LocalData.swift @@ -7,8 +7,9 @@ // import Foundation +import Combine -class LocalData { +class LocalData: ObservableObject { static let shared = LocalData() @@ -18,68 +19,130 @@ class LocalData { if ProcessInfo.processInfo.environment.keys.contains("UI_TESTING") { defaults = UserDefaults(suiteName: "\(Bundle.main.bundleIdentifier!).uitesting")! if ProcessInfo.processInfo.environment.keys.contains("UI_TESTING_LOGIN") { - defaults.set(true, forKey: onboardingCompleteKey) - defaults.set(URL(string: "http://localhost:8080")!, forKey: instanceURLKey) - defaults.set("client_id", forKey: clientIDKey) - defaults.set("client_secret", forKey: clientSecretKey) - defaults.set("access_token", forKey: accessTokenKey) + accounts = [ + UserAccountInfo( + id: UUID().uuidString, + instanceURL: URL(string: "http://localhost:8080")!, + clientID: "client_id", + clientSecret: "client_secret", + username: "admin", + accessToken: "access_token") + ] } } else { defaults = UserDefaults() } } - private let onboardingCompleteKey = "onboardingComplete" + private let accountsKey = "accounts" + var accounts: [UserAccountInfo] { + get { + if let array = defaults.array(forKey: accountsKey) as? [[String: String]] { + return array.compactMap { (info) in + guard let id = info["id"], + let instanceURL = info["instanceURL"], + let url = URL(string: instanceURL), + let clientId = info["clientID"], + let secret = info["clientSecret"], + let username = info["username"], + let accessToken = info["accessToken"] else { + return nil + } + return UserAccountInfo(id: id, instanceURL: url, clientID: clientId, clientSecret: secret, username: username, accessToken: accessToken) + } + } else { + return [] + } + } + set { + objectWillChange.send() + let array = newValue.map { (info) in + return [ + "id": info.id, + "instanceURL": info.instanceURL.absoluteString, + "clientID": info.clientID, + "clientSecret": info.clientSecret, + "username": info.username, + "accessToken": info.accessToken + ] + } + defaults.set(array, forKey: accountsKey) + } + } + + private let mostRecentAccountKey = "mostRecentAccount" + private var mostRecentAccount: String? { + get { + return defaults.string(forKey: mostRecentAccountKey) + } + set { + objectWillChange.send() + defaults.set(newValue, forKey: mostRecentAccountKey) + } + } + var onboardingComplete: Bool { - get { - return defaults.bool(forKey: onboardingCompleteKey) - } - set { - defaults.set(newValue, forKey: onboardingCompleteKey) - } + return !accounts.isEmpty } - private let instanceURLKey = "instanceURL" - var instanceURL: URL? { - get { - return defaults.url(forKey: instanceURLKey) - } - set { - defaults.set(newValue, forKey: instanceURLKey) + func addAccount(instanceURL url: URL, clientID: String, clientSecret secret: String, username: String, accessToken: String) -> UserAccountInfo { + var accounts = self.accounts + if let index = accounts.firstIndex(where: { $0.instanceURL == url && $0.username == username }) { + accounts.remove(at: index) } + let id = UUID().uuidString + let info = UserAccountInfo(id: id, instanceURL: url, clientID: clientID, clientSecret: secret, username: username, accessToken: accessToken) + accounts.append(info) + self.accounts = accounts + return info } - private let clientIDKey = "clientID" - var clientID: String? { - get { - return defaults.string(forKey: clientIDKey) - } - set { - defaults.set(newValue, forKey: clientIDKey) - } + func removeAccount(_ info: UserAccountInfo) { + accounts.removeAll(where: { $0.id == info.id }) } - private let clientSecretKey = "clientSecret" - var clientSecret: String? { - get { - return defaults.string(forKey: clientSecretKey) - } - set { - defaults.set(newValue, forKey: clientSecretKey) - } + func getAccount(id: String) -> UserAccountInfo? { + return accounts.first(where: { $0.id == id }) } - private let accessTokenKey = "accessToken" - var accessToken: String? { - get { - return defaults.string(forKey: accessTokenKey) + func getMostRecentAccount() -> UserAccountInfo? { + guard onboardingComplete else { return nil } + let mostRecent: UserAccountInfo? + if let id = mostRecentAccount { + mostRecent = accounts.first { $0.id == id } + } else { + mostRecent = nil } - set { - defaults.set(newValue, forKey: accessTokenKey) + return mostRecent ?? accounts.first! + } + + func setMostRecentAccount(_ account: UserAccountInfo?) { + mostRecentAccount = account?.id + } + +} + +extension LocalData { + struct UserAccountInfo: Equatable, Hashable { + let id: String + let instanceURL: URL + let clientID: String + let clientSecret: String + let username: String + let accessToken: String + + func hash(into hasher: inout Hasher) { + hasher.combine(id) + } + + static func ==(lhs: UserAccountInfo, rhs: UserAccountInfo) -> Bool { + return lhs.id == rhs.id } } } extension Notification.Name { - static let userLoggedOut = Notification.Name("userLoggedOut") + static let userLoggedOut = Notification.Name("Tusker.userLoggedOut") + static let addAccount = Notification.Name("Tusker.addAccount") + static let activateAccount = Notification.Name("Tusker.activateAccount") } diff --git a/Tusker/MastodonCache.swift b/Tusker/MastodonCache.swift index 5d888f28..9aec17c6 100644 --- a/Tusker/MastodonCache.swift +++ b/Tusker/MastodonCache.swift @@ -12,20 +12,26 @@ import Pachyderm class MastodonCache { - private static var statuses = CachedDictionary(name: "Statuses") - private static var accounts = CachedDictionary(name: "Accounts") - private static var relationships = CachedDictionary(name: "Relationships") - private static var notifications = CachedDictionary(name: "Notifications") + private var statuses = CachedDictionary(name: "Statuses") + private var accounts = CachedDictionary(name: "Accounts") + private var relationships = CachedDictionary(name: "Relationships") + private var notifications = CachedDictionary(name: "Notifications") - static let statusSubject = PassthroughSubject() - static let accountSubject = PassthroughSubject() + let statusSubject = PassthroughSubject() + let accountSubject = PassthroughSubject() + + weak var mastodonController: MastodonController? + + init(mastodonController: MastodonController) { + self.mastodonController = mastodonController + } // MARK: - Statuses - static func status(for id: String) -> Status? { + func status(for id: String) -> Status? { return statuses[id] } - static func set(status: Status, for id: String) { + func set(status: Status, for id: String) { statuses[id] = status add(account: status.account) if let reblog = status.reblog { @@ -36,100 +42,109 @@ class MastodonCache { statusSubject.send(status) } - static func status(for id: String, completion: @escaping (Status?) -> Void) { - let request = MastodonController.client.getStatus(id: id) - MastodonController.client.run(request) { response in + func status(for id: String, completion: @escaping (Status?) -> Void) { + guard let mastodonController = mastodonController else { + fatalError("The MastodonController for this cache has been deinitialized, so this cache should no longer exist. Are you storing a strong reference to it?") + } + let request = Client.getStatus(id: id) + mastodonController.run(request) { response in guard case let .success(status, _) = response else { completion(nil) return } - set(status: status, for: id) + self.set(status: status, for: id) completion(status) } } - static func add(status: Status) { + func add(status: Status) { set(status: status, for: status.id) } - static func addAll(statuses: [Status]) { + func addAll(statuses: [Status]) { statuses.forEach(add) } // MARK: - Accounts - static func account(for id: String) -> Account? { + func account(for id: String) -> Account? { return accounts[id] } - static func set(account: Account, for id: String) { + func set(account: Account, for id: String) { accounts[id] = account accountSubject.send(account) } - static func account(for id: String, completion: @escaping (Account?) -> Void) { - let request = MastodonController.client.getAccount(id: id) - MastodonController.client.run(request) { response in + func account(for id: String, completion: @escaping (Account?) -> Void) { + guard let mastodonController = mastodonController else { + fatalError("The MastodonController for this cache has been deinitialized, so this cache should no longer exist. Are you storing a strong reference to it?") + } + let request = Client.getAccount(id: id) + mastodonController.run(request) { response in guard case let .success(account, _) = response else { completion(nil) return } - set(account: account, for: account.id) + self.set(account: account, for: account.id) completion(account) } } - static func add(account: Account) { + func add(account: Account) { set(account: account, for: account.id) } - static func addAll(accounts: [Account]) { + func addAll(accounts: [Account]) { accounts.forEach(add) } // MARK: - Relationships - static func relationship(for id: String) -> Relationship? { + func relationship(for id: String) -> Relationship? { return relationships[id] } - static func set(relationship: Relationship, id: String) { + func set(relationship: Relationship, id: String) { relationships[id] = relationship } - static func relationship(for id: String, completion: @escaping (Relationship?) -> Void) { - let request = MastodonController.client.getRelationships(accounts: [id]) - MastodonController.client.run(request) { response in + func relationship(for id: String, completion: @escaping (Relationship?) -> Void) { + guard let mastodonController = mastodonController else { + fatalError("The MastodonController for this cache has been deinitialized, so this cache should no longer exist. Are you storing a strong reference to it?") + } + let request = Client.getRelationships(accounts: [id]) + mastodonController.run(request) { response in guard case let .success(relationships, _) = response, let relationship = relationships.first else { completion(nil) return } - set(relationship: relationship, id: relationship.id) + self.set(relationship: relationship, id: relationship.id) completion(relationship) } } - static func add(relationship: Relationship) { + func add(relationship: Relationship) { set(relationship: relationship, id: relationship.id) } - static func addAll(relationships: [Relationship]) { + func addAll(relationships: [Relationship]) { relationships.forEach(add) } // MARK: - Notifications - static func notification(for id: String) -> Pachyderm.Notification? { + func notification(for id: String) -> Pachyderm.Notification? { return notifications[id] } - static func set(notification: Pachyderm.Notification, id: String) { + func set(notification: Pachyderm.Notification, id: String) { notifications[id] = notification } - static func add(notification: Pachyderm.Notification) { + func add(notification: Pachyderm.Notification) { set(notification: notification, id: notification.id) } - static func addAll(notifications: [Pachyderm.Notification]) { + func addAll(notifications: [Pachyderm.Notification]) { notifications.forEach(add) } diff --git a/Tusker/SavedDataManager.swift b/Tusker/SavedDataManager.swift new file mode 100644 index 00000000..6d4696f9 --- /dev/null +++ b/Tusker/SavedDataManager.swift @@ -0,0 +1,113 @@ +// +// SavedDataManager.swift +// Tusker +// +// Created by Shadowfacts on 12/19/19. +// Copyright © 2019 Shadowfacts. All rights reserved. +// + +import Foundation +import Pachyderm + +class SavedDataManager: Codable { + private(set) static var shared: SavedDataManager = load() + + private static var documentsDirectory = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first! + private static var archiveURL = SavedDataManager.documentsDirectory.appendingPathComponent("saved_data").appendingPathExtension("plist") + + static func save() { + DispatchQueue.global(qos: .utility).async { + let encoder = PropertyListEncoder() + let data = try? encoder.encode(shared) + try? data?.write(to: archiveURL, options: .noFileProtection) + } + } + + static func load() -> SavedDataManager { + let decoder = PropertyListDecoder() + if let data = try? Data(contentsOf: archiveURL), + let savedHashtagsManager = try? decoder.decode(Self.self, from: data) { + return savedHashtagsManager + } + return SavedDataManager() + } + + private init() {} + + private var savedHashtags: [String: [Hashtag]] = [:] { + didSet { + SavedDataManager.save() + NotificationCenter.default.post(name: .savedHashtagsChanged, object: nil) + } + } + + private var savedInstances: [String: [URL]] = [:] { + didSet { + SavedDataManager.save() + NotificationCenter.default.post(name: .savedInstancesChanged, object: nil) + } + } + + func sortedHashtags(for account: LocalData.UserAccountInfo) -> [Hashtag] { + if let hashtags = savedHashtags[account.id] { + return hashtags.sorted(by: { $0.name < $1.name }) + } else { + return [] + } + } + + func isSaved(hashtag: Hashtag, for account: LocalData.UserAccountInfo) -> Bool { + return savedHashtags[account.id]?.contains(hashtag) ?? false + } + + func add(hashtag: Hashtag, for account: LocalData.UserAccountInfo) { + if isSaved(hashtag: hashtag, for: account) { + return + } + if var saved = savedHashtags[account.id] { + saved.append(hashtag) + savedHashtags[account.id] = saved + } else { + savedHashtags[account.id] = [hashtag] + } + } + + func remove(hashtag: Hashtag, for account: LocalData.UserAccountInfo) { + guard isSaved(hashtag: hashtag, for: account) else { return } + if var saved = savedHashtags[account.id] { + saved.removeAll(where: { $0.name == hashtag.name }) + savedHashtags[account.id] = saved + } + } + + func savedInstances(for account: LocalData.UserAccountInfo) -> [URL] { + return savedInstances[account.id] ?? [] + } + + func isSaved(instance url: URL, for account: LocalData.UserAccountInfo) -> Bool { + return savedInstances[account.id]?.contains(url) ?? false + } + + func add(instance url: URL, for account: LocalData.UserAccountInfo) { + if isSaved(instance: url, for: account) { return } + if var saved = savedInstances[account.id] { + saved.append(url) + savedInstances[account.id] = saved + } else { + savedInstances[account.id] = [url] + } + } + + func remove(instance url: URL, for account: LocalData.UserAccountInfo) { + guard isSaved(instance: url, for: account) else { return } + if var saved = savedInstances[account.id] { + saved.removeAll(where: { $0 == url }) + savedInstances[account.id] = saved + } + } +} + +extension Foundation.Notification.Name { + static let savedHashtagsChanged = Notification.Name("savedHashtagsChanged") + static let savedInstancesChanged = Notification.Name("savedInstancesChanged") +} diff --git a/Tusker/SavedHashtagsManager.swift b/Tusker/SavedHashtagsManager.swift deleted file mode 100644 index cf91aa9e..00000000 --- a/Tusker/SavedHashtagsManager.swift +++ /dev/null @@ -1,65 +0,0 @@ -// -// SavedHashtagsManager.swift -// Tusker -// -// Created by Shadowfacts on 12/19/19. -// Copyright © 2019 Shadowfacts. All rights reserved. -// - -import Foundation -import Pachyderm - -class SavedHashtagsManager: Codable { - private(set) static var shared: SavedHashtagsManager = load() - - private static var documentsDirectory = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first! - private static var archiveURL = SavedHashtagsManager.documentsDirectory.appendingPathComponent("saved_hashtags").appendingPathExtension("plist") - - static func save() { - DispatchQueue.global(qos: .utility).async { - let encoder = PropertyListEncoder() - let data = try? encoder.encode(shared) - try? data?.write(to: archiveURL, options: .noFileProtection) - } - } - - static func load() -> SavedHashtagsManager { - let decoder = PropertyListDecoder() - if let data = try? Data(contentsOf: archiveURL), - let savedHashtagsManager = try? decoder.decode(Self.self, from: data) { - return savedHashtagsManager - } - return SavedHashtagsManager() - } - - private init() {} - - private var savedHashtags: [Hashtag] = [] - var sorted: [Hashtag] { - return savedHashtags.sorted(by: { $0.name < $1.name }) - } - - func isSaved(_ hashtag: Hashtag) -> Bool { - return savedHashtags.contains(hashtag) - } - - func add(_ hashtag: Hashtag) { - if isSaved(hashtag) { - return - } - savedHashtags.append(hashtag) - SavedHashtagsManager.save() - NotificationCenter.default.post(name: .savedHashtagsChanged, object: nil) - } - - func remove(_ hashtag: Hashtag) { - guard isSaved(hashtag) else { return } - savedHashtags.removeAll(where: { $0.name == hashtag.name }) - SavedHashtagsManager.save() - NotificationCenter.default.post(name: .savedHashtagsChanged, object: nil) - } -} - -extension Foundation.Notification.Name { - static let savedHashtagsChanged = Notification.Name("savedHashtagsChanged") -} diff --git a/Tusker/SavedInstancesManager.swift b/Tusker/SavedInstancesManager.swift deleted file mode 100644 index 7bc466c1..00000000 --- a/Tusker/SavedInstancesManager.swift +++ /dev/null @@ -1,61 +0,0 @@ -// -// SavedInstancesManager.swift -// Tusker -// -// Created by Shadowfacts on 12/19/19. -// Copyright © 2019 Shadowfacts. All rights reserved. -// - -import Foundation - -class SavedInstanceManager: Codable { - private(set) static var shared: SavedInstanceManager = load() - - private static var documentsDirectory = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first! - private static var archiveURL = SavedInstanceManager.documentsDirectory.appendingPathComponent("saved_instances").appendingPathExtension("plist") - - static func save() { - DispatchQueue.global(qos: .utility).async { - let encoder = PropertyListEncoder() - let data = try? encoder.encode(shared) - try? data?.write(to: archiveURL, options: .noFileProtection) - } - } - - static func load() -> SavedInstanceManager { - let decoder = PropertyListDecoder() - if let data = try? Data(contentsOf: archiveURL), - let savedInstanceManager = try? decoder.decode(Self.self, from: data) { - return savedInstanceManager - } - return SavedInstanceManager() - } - - private init() {} - - private(set) var savedInstances: [URL] = [] - - func isSaved(_ url: URL) -> Bool { - return savedInstances.contains(url) - } - - func add(_ url: URL) { - if isSaved(url) { - return - } - savedInstances.append(url) - SavedInstanceManager.save() - NotificationCenter.default.post(name: .savedInstancesChanged, object: nil) - } - - func remove(_ url: URL) { - guard isSaved(url) else { return } - savedInstances.removeAll(where: { $0 == url }) - SavedInstanceManager.save() - NotificationCenter.default.post(name: .savedInstancesChanged, object: nil) - } -} - -extension Notification.Name { - static let savedInstancesChanged = Notification.Name("savedInstancesChanged") -} diff --git a/Tusker/SceneDelegate.swift b/Tusker/SceneDelegate.swift new file mode 100644 index 00000000..b2a172a2 --- /dev/null +++ b/Tusker/SceneDelegate.swift @@ -0,0 +1,151 @@ +// +// SceneDelegate.swift +// Tusker +// +// Created by Shadowfacts on 1/6/20. +// Copyright © 2020 Shadowfacts. All rights reserved. +// + +import UIKit +import Pachyderm + +class SceneDelegate: UIResponder, UIWindowSceneDelegate { + + var window: UIWindow? + + func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) { + // Use this method to optionally configure and attach the UIWindow `window` to the provided UIWindowScene `scene`. + // If using a storyboard, the `window` property will automatically be initialized and attached to the scene. + // This delegate does not imply the connecting scene or session are new (see `application:configurationForConnectingSceneSession` instead). + guard let windowScene = scene as? UIWindowScene else { return } + + window = UIWindow(windowScene: windowScene) + + if LocalData.shared.onboardingComplete { + if session.mastodonController == nil { + let account = LocalData.shared.getMostRecentAccount()! + session.mastodonController = MastodonController.getForAccount(account) + } + + showAppUI() + } else { + showOnboardingUI() + } + + window!.makeKeyAndVisible() + + NotificationCenter.default.addObserver(self, selector: #selector(themePrefChanged), name: .themePreferenceChanged, object: nil) + themePrefChanged() + + if let shortcutItem = connectionOptions.shortcutItem { + _ = AppShortcutItem.handle(shortcutItem) + } + } + + func scene(_ scene: UIScene, openURLContexts URLContexts: Set) { + if URLContexts.count > 1 { + fatalError("Cannot open more than 1 URL") + } + + let url = URLContexts.first!.url + + if url.host == "x-callback-url" { + _ = XCBManager.handle(url: url) + } else if var components = URLComponents(url: url, resolvingAgainstBaseURL: false), + let tabBarController = window!.rootViewController as? MainTabBarViewController, + let exploreNavController = tabBarController.getTabController(tab: .explore) as? UINavigationController, + let exploreController = exploreNavController.viewControllers.first as? ExploreViewController { + + tabBarController.select(tab: .explore) + exploreNavController.popToRootViewController(animated: false) + + exploreController.loadViewIfNeeded() + exploreController.searchController.isActive = true + + components.scheme = "https" + let query = url.absoluteString + exploreController.searchController.searchBar.text = query + exploreController.resultsController.performSearch(query: query) + } + } + + func scene(_ scene: UIScene, continue userActivity: NSUserActivity) { + _ = userActivity.handleResume() + } + + func windowScene(_ windowScene: UIWindowScene, performActionFor shortcutItem: UIApplicationShortcutItem, completionHandler: @escaping (Bool) -> Void) { + completionHandler(AppShortcutItem.handle(shortcutItem)) + } + + func sceneDidDisconnect(_ scene: UIScene) { + // Called as the scene is being released by the system. + // This occurs shortly after the scene enters the background, or when its session is discarded. + // Release any resources associated with this scene that can be re-created the next time the scene connects. + // The scene may re-connect later, as its session was not neccessarily discarded (see `application:didDiscardSceneSessions` instead). + } + + func sceneDidBecomeActive(_ scene: UIScene) { + // Called when the scene has moved from an inactive state to an active state. + // Use this method to restart any tasks that were paused (or not yet started) when the scene was inactive. + } + + func sceneWillResignActive(_ scene: UIScene) { + // Called when the scene will move from an active state to an inactive state. + // This may occur due to temporary interruptions (ex. an incoming phone call). + } + + func sceneWillEnterForeground(_ scene: UIScene) { + // Called as the scene transitions from the background to the foreground. + // Use this method to undo the changes made on entering the background. + } + + func sceneDidEnterBackground(_ scene: UIScene) { + // Called as the scene transitions from the foreground to the background. + // Use this method to save data, release shared resources, and store enough scene-specific state information + // to restore the scene back to its current state. + + Preferences.save() + DraftsManager.save() + } + + func activateAccount(_ account: LocalData.UserAccountInfo) { + LocalData.shared.setMostRecentAccount(account) + window!.windowScene!.session.mastodonController = MastodonController.getForAccount(account) + showAppUI() + } + + func logoutCurrent() { + LocalData.shared.removeAccount(LocalData.shared.getMostRecentAccount()!) + if LocalData.shared.onboardingComplete { + activateAccount(LocalData.shared.accounts.first!) + } else { + showOnboardingUI() + } + } + + func showAppUI() { + let mastodonController = window!.windowScene!.session.mastodonController! + mastodonController.getOwnAccount() + mastodonController.getOwnInstance() + + let tabBarController = MainTabBarViewController(mastodonController: mastodonController) + window!.rootViewController = tabBarController + } + + func showOnboardingUI() { + let onboarding = OnboardingViewController() + onboarding.onboardingDelegate = self + window!.rootViewController = onboarding + } + + @objc func themePrefChanged() { + window?.overrideUserInterfaceStyle = Preferences.shared.theme + } + +} + +extension SceneDelegate: OnboardingViewControllerDelegate { + func didFinishOnboarding(account: LocalData.UserAccountInfo) { + activateAccount(account) + } +} diff --git a/Tusker/Screens/Account List/AccountListTableViewController.swift b/Tusker/Screens/Account List/AccountListTableViewController.swift index 8635a017..c001a316 100644 --- a/Tusker/Screens/Account List/AccountListTableViewController.swift +++ b/Tusker/Screens/Account List/AccountListTableViewController.swift @@ -12,10 +12,13 @@ class AccountListTableViewController: EnhancedTableViewController { private let accountCell = "accountCell" + let mastodonController: MastodonController + let accountIDs: [String] - init(accountIDs: [String]) { + init(accountIDs: [String], mastodonController: MastodonController) { self.accountIDs = accountIDs + self.mastodonController = mastodonController super.init(style: .grouped) } @@ -50,12 +53,14 @@ class AccountListTableViewController: EnhancedTableViewController { guard let cell = tableView.dequeueReusableCell(withIdentifier: accountCell, for: indexPath) as? AccountTableViewCell else { fatalError() } let id = accountIDs[indexPath.row] - cell.updateUI(accountID: id) cell.delegate = self + cell.updateUI(accountID: id) return cell } } -extension AccountListTableViewController: TuskerNavigationDelegate {} +extension AccountListTableViewController: TuskerNavigationDelegate { + var apiController: MastodonController { mastodonController } +} diff --git a/Tusker/Screens/Bookmarks/BookmarksTableViewController.swift b/Tusker/Screens/Bookmarks/BookmarksTableViewController.swift index ec478fb9..ce83ee9a 100644 --- a/Tusker/Screens/Bookmarks/BookmarksTableViewController.swift +++ b/Tusker/Screens/Bookmarks/BookmarksTableViewController.swift @@ -13,6 +13,8 @@ class BookmarksTableViewController: EnhancedTableViewController { private let statusCell = "statusCell" + let mastodonController: MastodonController + var statuses: [(id: String, state: StatusState)] = [] { didSet { DispatchQueue.main.async { @@ -24,7 +26,9 @@ class BookmarksTableViewController: EnhancedTableViewController { var newer: RequestRange? var older: RequestRange? - init() { + init(mastodonController: MastodonController) { + self.mastodonController = mastodonController + super.init(style: .plain) title = NSLocalizedString("Bookmarks", comment: "bookmarks screen title") @@ -44,10 +48,10 @@ class BookmarksTableViewController: EnhancedTableViewController { tableView.prefetchDataSource = self - let request = MastodonController.client.getBookmarks() - MastodonController.client.run(request) { (response) in + let request = Client.getBookmarks() + mastodonController.run(request) { (response) in guard case let .success(statuses, pagination) = response else { fatalError() } - MastodonCache.addAll(statuses: statuses) + self.mastodonController.cache.addAll(statuses: statuses) self.statuses.append(contentsOf: statuses.map { ($0.id, .unknown) }) self.newer = pagination?.newer self.older = pagination?.older @@ -81,11 +85,11 @@ class BookmarksTableViewController: EnhancedTableViewController { return } - let request = MastodonController.client.getBookmarks(range: older) - MastodonController.client.run(request) { (response) in + let request = Client.getBookmarks(range: older) + mastodonController.run(request) { (response) in guard case let .success(newStatuses, pagination) = response else { fatalError() } self.older = pagination?.older - MastodonCache.addAll(statuses: newStatuses) + self.mastodonController.cache.addAll(statuses: newStatuses) self.statuses.append(contentsOf: newStatuses.map { ($0.id, .unknown) }) } } @@ -101,15 +105,15 @@ class BookmarksTableViewController: EnhancedTableViewController { override func tableView(_ tableView: UITableView, trailingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? { let cellConfig = (tableView.cellForRow(at: indexPath) as? TableViewSwipeActionProvider)?.trailingSwipeActionsConfiguration() - guard let status = MastodonCache.status(for: statuses[indexPath.row].id) else { + guard let status = mastodonController.cache.status(for: statuses[indexPath.row].id) else { return cellConfig } let unbookmarkAction = UIContextualAction(style: .destructive, title: NSLocalizedString("Unbookmark", comment: "unbookmark action title")) { (action, view, completion) in let request = Status.unbookmark(status) - MastodonController.client.run(request) { (response) in + self.mastodonController.run(request) { (response) in guard case let .success(newStatus, _) = response else { fatalError() } - MastodonCache.add(status: newStatus) + self.mastodonController.cache.add(status: newStatus) self.statuses.remove(at: indexPath.row) } } @@ -127,13 +131,13 @@ class BookmarksTableViewController: EnhancedTableViewController { } override func getSuggestedContextMenuActions(tableView: UITableView, indexPath: IndexPath, point: CGPoint) -> [UIAction] { - guard let status = MastodonCache.status(for: statuses[indexPath.row].id) else { return [] } + guard let status = mastodonController.cache.status(for: statuses[indexPath.row].id) else { return [] } return [ UIAction(title: NSLocalizedString("Unbookmark", comment: "unbookmark action title"), image: UIImage(systemName: "bookmark.fill"), identifier: .init("unbookmark"), discoverabilityTitle: nil, attributes: [], state: .off, handler: { (_) in let request = Status.unbookmark(status) - MastodonController.client.run(request) { (response) in + self.mastodonController.run(request) { (response) in guard case let .success(newStatus, _) = response else { fatalError() } - MastodonCache.add(status: newStatus) + self.mastodonController.cache.add(status: newStatus) self.statuses.remove(at: indexPath.row) } }) @@ -143,6 +147,8 @@ class BookmarksTableViewController: EnhancedTableViewController { } extension BookmarksTableViewController: StatusTableViewCellDelegate { + var apiController: MastodonController { mastodonController } + func statusCellCollapsedStateChanged(_ cell: BaseStatusTableViewCell) { tableView.beginUpdates() tableView.endUpdates() @@ -152,7 +158,7 @@ extension BookmarksTableViewController: StatusTableViewCellDelegate { extension BookmarksTableViewController: UITableViewDataSourcePrefetching { func tableView(_ tableView: UITableView, prefetchRowsAt indexPaths: [IndexPath]) { for indexPath in indexPaths { - guard let status = MastodonCache.status(for: statuses[indexPath.row].id) else { continue } + guard let status = mastodonController.cache.status(for: statuses[indexPath.row].id) else { continue } ImageCache.avatars.get(status.account.avatar, completion: nil) for attachment in status.attachments where attachment.kind == .image { ImageCache.attachments.get(attachment.url, completion: nil) @@ -162,7 +168,7 @@ extension BookmarksTableViewController: UITableViewDataSourcePrefetching { func tableView(_ tableView: UITableView, cancelPrefetchingForRowsAt indexPaths: [IndexPath]) { for indexPath in indexPaths { - guard let status = MastodonCache.status(for: statuses[indexPath.row].id) else { continue } + guard let status = mastodonController.cache.status(for: statuses[indexPath.row].id) else { continue } ImageCache.avatars.cancel(status.account.avatar) for attachment in status.attachments where attachment.kind == .image { ImageCache.attachments.cancel(attachment.url) diff --git a/Tusker/Screens/Compose/ComposeViewController.swift b/Tusker/Screens/Compose/ComposeViewController.swift index 977be20f..d4382ef9 100644 --- a/Tusker/Screens/Compose/ComposeViewController.swift +++ b/Tusker/Screens/Compose/ComposeViewController.swift @@ -12,6 +12,8 @@ import Intents class ComposeViewController: UIViewController { + weak var mastodonController: MastodonController! + var inReplyToID: String? var accountsToMention = [String]() var initialText: String? @@ -70,9 +72,11 @@ class ComposeViewController: UIViewController { @IBOutlet weak var postProgressView: SteppedProgressView! - init(inReplyTo inReplyToID: String? = nil, mentioningAcct: String? = nil, text: String? = nil) { + init(inReplyTo inReplyToID: String? = nil, mentioningAcct: String? = nil, text: String? = nil, mastodonController: MastodonController) { + self.mastodonController = mastodonController + self.inReplyToID = inReplyToID - if let inReplyToID = inReplyToID, let inReplyTo = MastodonCache.status(for: inReplyToID) { + if let inReplyToID = inReplyToID, let inReplyTo = mastodonController.cache.status(for: inReplyToID) { accountsToMention = [inReplyTo.account.acct] + inReplyTo.mentions.map { $0.acct } } else { accountsToMention = [] @@ -80,7 +84,7 @@ class ComposeViewController: UIViewController { if let mentioningAcct = mentioningAcct { accountsToMention.append(mentioningAcct) } - if let ownAccount = MastodonController.account { + if let ownAccount = mastodonController.account { accountsToMention.removeAll(where: { acct in ownAccount.acct == acct }) } accountsToMention = accountsToMention.uniques() @@ -127,7 +131,7 @@ class ComposeViewController: UIViewController { statusTextView.text = accountsToMention.map({ acct in "@\(acct) " }).joined() initialText = statusTextView.text - MastodonController.getOwnAccount { (account) in + mastodonController.getOwnAccount { (account) in DispatchQueue.main.async { self.selfDetailView.update(account: account) } @@ -150,13 +154,13 @@ class ComposeViewController: UIViewController { } if let inReplyToID = inReplyToID { - if let status = MastodonCache.status(for: inReplyToID) { + if let status = mastodonController.cache.status(for: inReplyToID) { updateInReplyTo(inReplyTo: status) } else { let loadingVC = LoadingViewController() embedChild(loadingVC) - MastodonCache.status(for: inReplyToID) { (status) in + mastodonController.cache.status(for: inReplyToID) { (status) in guard let status = status else { return } DispatchQueue.main.async { self.updateInReplyTo(inReplyTo: status) @@ -189,6 +193,7 @@ class ComposeViewController: UIViewController { } let replyView = ComposeStatusReplyView.create() + replyView.mastodonController = mastodonController replyView.updateUI(for: inReplyTo) stackView.insertArrangedSubview(replyView, at: 0) @@ -290,7 +295,7 @@ class ComposeViewController: UIViewController { func updateCharactersRemaining() { let count = CharacterCounter.count(text: statusTextView.text) let cwCount = contentWarningEnabled ? (contentWarningTextField.text?.count ?? 0) : 0 - let remaining = (MastodonController.instance.maxStatusCharacters ?? 500) - count - cwCount + let remaining = (mastodonController.instance.maxStatusCharacters ?? 500) - count - cwCount if remaining < 0 { charactersRemainingLabel.textColor = .red compositionState.formUnion(.tooManyCharacters) @@ -316,7 +321,7 @@ class ComposeViewController: UIViewController { } func updateAddAttachmentButton() { - switch MastodonController.instance.instanceType { + switch mastodonController.instance.instanceType { case .pleroma: addAttachmentButton.isEnabled = true case .mastodon: @@ -363,10 +368,11 @@ class ComposeViewController: UIViewController { attachments.append(.init(attachment: attachment, description: description)) } let cw = contentWarningEnabled ? contentWarningTextField.text : nil + let account = mastodonController.accountInfo! if let currentDraft = self.currentDraft { - currentDraft.update(text: self.statusTextView.text, contentWarning: cw, attachments: attachments) + currentDraft.update(accountID: account.id, text: self.statusTextView.text, contentWarning: cw, attachments: attachments) } else { - self.currentDraft = DraftsManager.shared.create(text: self.statusTextView.text, contentWarning: cw, inReplyToID: inReplyToID, attachments: attachments) + self.currentDraft = DraftsManager.shared.create(accountID: account.id, text: self.statusTextView.text, contentWarning: cw, inReplyToID: inReplyToID, attachments: attachments) } DraftsManager.save() } @@ -451,7 +457,7 @@ class ComposeViewController: UIViewController { } @objc func draftsButtonPressed() { - let draftsVC = DraftsTableViewController() + let draftsVC = DraftsTableViewController(account: mastodonController.accountInfo!) draftsVC.delegate = self present(UINavigationController(rootViewController: draftsVC), animated: true) } @@ -500,8 +506,8 @@ class ComposeViewController: UIViewController { compAttachment.getData { (data, mimeType) in self.postProgressView.step() - let request = MastodonController.client.upload(attachment: FormAttachment(mimeType: mimeType, data: data, fileName: "file"), description: description) - MastodonController.client.run(request) { (response) in + let request = Client.upload(attachment: FormAttachment(mimeType: mimeType, data: data, fileName: "file"), description: description) + self.mastodonController.run(request) { (response) in guard case let .success(attachment, _) = response else { fatalError() } attachments[index] = attachment @@ -519,7 +525,7 @@ class ComposeViewController: UIViewController { group.notify(queue: .main) { let attachments = attachments.compactMap { $0 } - let request = MastodonController.client.createStatus(text: text, + let request = Client.createStatus(text: text, contentType: Preferences.shared.statusContentType, inReplyTo: self.inReplyToID, media: attachments, @@ -527,10 +533,10 @@ class ComposeViewController: UIViewController { spoilerText: contentWarning, visibility: visibility, language: nil) - MastodonController.client.run(request) { (response) in + self.mastodonController.run(request) { (response) in guard case let .success(status, _) = response else { fatalError() } self.postedStatus = status - MastodonCache.add(status: status) + self.mastodonController.cache.add(status: status) if let draft = self.currentDraft { DraftsManager.shared.remove(draft) @@ -540,7 +546,7 @@ class ComposeViewController: UIViewController { self.postProgressView.step() self.dismiss(animated: true) - let conversationVC = ConversationTableViewController(for: status.id) + let conversationVC = ConversationTableViewController(for: status.id, mastodonController: self.mastodonController) self.show(conversationVC, sender: self) self.xcbSession?.complete(with: .success, additionalData: [ @@ -581,7 +587,7 @@ extension ComposeViewController: UITextViewDelegate { extension ComposeViewController: AssetPickerViewControllerDelegate { func assetPicker(_ assetPicker: AssetPickerViewController, shouldAllowAssetOfType type: CompositionAttachment.AttachmentType) -> Bool { - switch MastodonController.instance.instanceType { + switch mastodonController.instance.instanceType { case .pleroma: return true case .mastodon: @@ -618,7 +624,6 @@ extension ComposeViewController: DraftsTableViewControllerDelegate { func shouldSelectDraft(_ draft: DraftsManager.Draft, completion: @escaping (Bool) -> Void) { if draft.inReplyToID != self.inReplyToID { - // todo: better text for this let alertController = UIAlertController(title: "Different Reply", message: "The selected draft is a reply to a different status, do you wish to use it?", preferredStyle: .alert) alertController.addAction(UIAlertAction(title: "Cancel", style: .cancel, handler: { (_) in completion(false) diff --git a/Tusker/Screens/Compose/Drafts/DraftsTableViewController.swift b/Tusker/Screens/Compose/Drafts/DraftsTableViewController.swift index 3f71e3f2..6273d584 100644 --- a/Tusker/Screens/Compose/Drafts/DraftsTableViewController.swift +++ b/Tusker/Screens/Compose/Drafts/DraftsTableViewController.swift @@ -8,7 +8,7 @@ import UIKit -protocol DraftsTableViewControllerDelegate { +protocol DraftsTableViewControllerDelegate: class { func draftSelectionCanceled() func shouldSelectDraft(_ draft: DraftsManager.Draft, completion: @escaping (Bool) -> Void) func draftSelected(_ draft: DraftsManager.Draft) @@ -17,9 +17,14 @@ protocol DraftsTableViewControllerDelegate { class DraftsTableViewController: UITableViewController { - var delegate: DraftsTableViewControllerDelegate? + let account: LocalData.UserAccountInfo + weak var delegate: DraftsTableViewControllerDelegate? - init() { + var drafts = [DraftsManager.Draft]() + + init(account: LocalData.UserAccountInfo) { + self.account = account + super.init(nibName: "DraftsTableViewController", bundle: nil) title = "Drafts" @@ -37,10 +42,14 @@ class DraftsTableViewController: UITableViewController { tableView.estimatedRowHeight = 140 tableView.register(UINib(nibName: "DraftTableViewCell", bundle: nil), forCellReuseIdentifier: "draftCell") + + drafts = DraftsManager.shared.sorted.filter { (draft) in + draft.accountID == account.id + } } func draft(for indexPath: IndexPath) -> DraftsManager.Draft { - return DraftsManager.shared.sorted[indexPath.row] + return drafts[indexPath.row] } // MARK: - Table View Data Source @@ -50,7 +59,7 @@ class DraftsTableViewController: UITableViewController { } override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { - return DraftsManager.shared.drafts.count + return drafts.count } override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { diff --git a/Tusker/Screens/Conversation/ConversationTableViewController.swift b/Tusker/Screens/Conversation/ConversationTableViewController.swift index 1807dcf2..b225840d 100644 --- a/Tusker/Screens/Conversation/ConversationTableViewController.swift +++ b/Tusker/Screens/Conversation/ConversationTableViewController.swift @@ -15,6 +15,8 @@ class ConversationTableViewController: EnhancedTableViewController { static let showPostsImage = UIImage(systemName: "eye.fill")! static let hidePostsImage = UIImage(systemName: "eye.slash.fill")! + let mastodonController: MastodonController + let mainStatusID: String let mainStatusState: StatusState var statuses: [(id: String, state: StatusState)] = [] { @@ -28,9 +30,10 @@ class ConversationTableViewController: EnhancedTableViewController { var showStatusesAutomatically = false var visibilityBarButtonItem: UIBarButtonItem! - init(for mainStatusID: String, state: StatusState = .unknown) { + init(for mainStatusID: String, state: StatusState = .unknown, mastodonController: MastodonController) { self.mainStatusID = mainStatusID self.mainStatusState = state + self.mastodonController = mastodonController super.init(style: .plain) } @@ -55,14 +58,14 @@ class ConversationTableViewController: EnhancedTableViewController { statuses = [(mainStatusID, mainStatusState)] - guard let mainStatus = MastodonCache.status(for: mainStatusID) else { fatalError("Missing cached status \(mainStatusID)") } + guard let mainStatus = mastodonController.cache.status(for: mainStatusID) else { fatalError("Missing cached status \(mainStatusID)") } let request = Status.getContext(mainStatus) - MastodonController.client.run(request) { response in + mastodonController.run(request) { response in guard case let .success(context, _) = response else { fatalError() } let parents = self.getDirectParents(of: mainStatus, from: context.ancestors) - MastodonCache.addAll(statuses: parents) - MastodonCache.addAll(statuses: context.descendants) + self.mastodonController.cache.addAll(statuses: parents) + self.mastodonController.cache.addAll(statuses: context.descendants) self.statuses = parents.map { ($0.id, .unknown) } + self.statuses + context.descendants.map { ($0.id, .unknown) } let indexPath = IndexPath(row: parents.count, section: 0) DispatchQueue.main.async { @@ -101,14 +104,14 @@ class ConversationTableViewController: EnhancedTableViewController { guard let cell = tableView.dequeueReusableCell(withIdentifier: "mainStatusCell", for: indexPath) as? ConversationMainStatusTableViewCell else { fatalError() } cell.selectionStyle = .none cell.showStatusAutomatically = showStatusesAutomatically - cell.updateUI(statusID: id, state: state) cell.delegate = self + cell.updateUI(statusID: id, state: state) return cell } else { guard let cell = tableView.dequeueReusableCell(withIdentifier: "statusCell", for: indexPath) as? TimelineStatusTableViewCell else { fatalError() } cell.showStatusAutomatically = showStatusesAutomatically - cell.updateUI(statusID: id, state: state) cell.delegate = self + cell.updateUI(statusID: id, state: state) return cell } } @@ -155,6 +158,7 @@ class ConversationTableViewController: EnhancedTableViewController { } extension ConversationTableViewController: StatusTableViewCellDelegate { + var apiController: MastodonController { mastodonController } func statusCellCollapsedStateChanged(_ cell: BaseStatusTableViewCell) { // causes the table view to recalculate the cell heights tableView.beginUpdates() @@ -165,7 +169,7 @@ extension ConversationTableViewController: StatusTableViewCellDelegate { extension ConversationTableViewController: UITableViewDataSourcePrefetching { func tableView(_ tableView: UITableView, prefetchRowsAt indexPaths: [IndexPath]) { for indexPath in indexPaths { - guard let status = MastodonCache.status(for: statuses[indexPath.row].id) else { continue } + guard let status = mastodonController.cache.status(for: statuses[indexPath.row].id) else { continue } ImageCache.avatars.get(status.account.avatar, completion: nil) for attachment in status.attachments { ImageCache.attachments.get(attachment.url, completion: nil) @@ -175,7 +179,7 @@ extension ConversationTableViewController: UITableViewDataSourcePrefetching { func tableView(_ tableView: UITableView, cancelPrefetchingForRowsAt indexPaths: [IndexPath]) { for indexPath in indexPaths { - guard let status = MastodonCache.status(for: statuses[indexPath.row].id) else { continue } + guard let status = mastodonController.cache.status(for: statuses[indexPath.row].id) else { continue } ImageCache.avatars.cancel(status.account.avatar) for attachment in status.attachments { ImageCache.attachments.cancel(attachment.url) diff --git a/Tusker/Screens/Explore/AddSavedHashtagViewController.swift b/Tusker/Screens/Explore/AddSavedHashtagViewController.swift index f45eb025..65585f67 100644 --- a/Tusker/Screens/Explore/AddSavedHashtagViewController.swift +++ b/Tusker/Screens/Explore/AddSavedHashtagViewController.swift @@ -52,7 +52,7 @@ class AddSavedHashtagViewController: SearchResultsViewController { extension AddSavedHashtagViewController: SearchResultsViewControllerDelegate { func selectedSearchResult(hashtag: Hashtag) { - SavedHashtagsManager.shared.add(hashtag) + SavedDataManager.shared.add(hashtag: hashtag, for: mastodonController.accountInfo!) dismiss(animated: true) } } diff --git a/Tusker/Screens/Explore/ExploreViewController.swift b/Tusker/Screens/Explore/ExploreViewController.swift index 02d487e7..0254ddec 100644 --- a/Tusker/Screens/Explore/ExploreViewController.swift +++ b/Tusker/Screens/Explore/ExploreViewController.swift @@ -12,12 +12,16 @@ import Pachyderm class ExploreViewController: EnhancedTableViewController { + let mastodonController: MastodonController + var dataSource: DataSource! var resultsController: SearchResultsViewController! var searchController: UISearchController! - init() { + init(mastodonController: MastodonController) { + self.mastodonController = mastodonController + super.init(style: .insetGrouped) title = NSLocalizedString("Explore", comment: "explore tab title") @@ -77,18 +81,20 @@ class ExploreViewController: EnhancedTableViewController { }) dataSource.exploreController = self + let account = mastodonController.accountInfo! + var snapshot = NSDiffableDataSourceSnapshot() snapshot.appendSections([.bookmarks, .lists, .savedHashtags, .savedInstances]) snapshot.appendItems([.bookmarks], toSection: .bookmarks) snapshot.appendItems([.addList], toSection: .lists) - snapshot.appendItems(SavedHashtagsManager.shared.sorted.map { .savedHashtag($0) } + [.addSavedHashtag], toSection: .savedHashtags) - snapshot.appendItems(SavedInstanceManager.shared.savedInstances.map { .savedInstance($0) } + [.findInstance], toSection: .savedInstances) + snapshot.appendItems(SavedDataManager.shared.sortedHashtags(for: account).map { .savedHashtag($0) } + [.addSavedHashtag], toSection: .savedHashtags) + snapshot.appendItems(SavedDataManager.shared.savedInstances(for: account).map { .savedInstance($0) } + [.findInstance], toSection: .savedInstances) // the initial, static items should not be displayed with an animation UIView.performWithoutAnimation { dataSource.apply(snapshot) } - resultsController = SearchResultsViewController() + resultsController = SearchResultsViewController(mastodonController: mastodonController) resultsController.exploreNavigationController = self.navigationController! searchController = UISearchController(searchResultsController: resultsController) searchController.searchResultsUpdater = resultsController @@ -106,8 +112,8 @@ class ExploreViewController: EnhancedTableViewController { } func reloadLists() { - let request = MastodonController.client.getLists() - MastodonController.client.run(request) { (response) in + let request = Client.getLists() + mastodonController.run(request) { (response) in guard case let .success(lists, _) = response else { fatalError() } @@ -123,16 +129,18 @@ class ExploreViewController: EnhancedTableViewController { } @objc func savedHashtagsChanged() { + let account = mastodonController.accountInfo! var snapshot = dataSource.snapshot() snapshot.deleteItems(snapshot.itemIdentifiers(inSection: .savedHashtags)) - snapshot.appendItems(SavedHashtagsManager.shared.sorted.map { .savedHashtag($0) } + [.addSavedHashtag], toSection: .savedHashtags) + snapshot.appendItems(SavedDataManager.shared.sortedHashtags(for: account).map { .savedHashtag($0) } + [.addSavedHashtag], toSection: .savedHashtags) dataSource.apply(snapshot) } @objc func savedInstancesChanged() { + let account = mastodonController.accountInfo! var snapshot = dataSource.snapshot() snapshot.deleteItems(snapshot.itemIdentifiers(inSection: .savedInstances)) - snapshot.appendItems(SavedInstanceManager.shared.savedInstances.map { .savedInstance($0) } + [.findInstance], toSection: .savedInstances) + snapshot.appendItems(SavedDataManager.shared.savedInstances(for: account).map { .savedInstance($0) } + [.findInstance], toSection: .savedInstances) dataSource.apply(snapshot) } @@ -143,7 +151,7 @@ class ExploreViewController: EnhancedTableViewController { alert.addAction(UIAlertAction(title: NSLocalizedString("Delete List", comment: "delete list alert confirm button"), style: .destructive, handler: { (_) in let request = List.delete(list) - MastodonController.client.run(request) { (response) in + self.mastodonController.run(request) { (response) in guard case .success(_, _) = response else { fatalError() } @@ -159,11 +167,13 @@ class ExploreViewController: EnhancedTableViewController { } func removeSavedHashtag(_ hashtag: Hashtag) { - SavedHashtagsManager.shared.remove(hashtag) + let account = mastodonController.accountInfo! + SavedDataManager.shared.remove(hashtag: hashtag, for: account) } func removeSavedInstance(_ instanceURL: URL) { - SavedInstanceManager.shared.remove(instanceURL) + let account = mastodonController.accountInfo! + SavedDataManager.shared.remove(instance: instanceURL, for: account) } // MARK: - Table view delegate @@ -174,10 +184,10 @@ class ExploreViewController: EnhancedTableViewController { return case .bookmarks: - show(BookmarksTableViewController(), sender: nil) + show(BookmarksTableViewController(mastodonController: mastodonController), sender: nil) case let .list(list): - show(ListTimelineViewController(for: list), sender: nil) + show(ListTimelineViewController(for: list, mastodonController: mastodonController), sender: nil) case .addList: tableView.selectRow(at: nil, animated: true, scrollPosition: .none) @@ -189,14 +199,14 @@ class ExploreViewController: EnhancedTableViewController { fatalError() } - let request = MastodonController.client.createList(title: title) - MastodonController.client.run(request) { (response) in + let request = Client.createList(title: title) + self.mastodonController.run(request) { (response) in guard case let .success(list, _) = response else { fatalError() } self.reloadLists() DispatchQueue.main.async { - let listTimelineController = ListTimelineViewController(for: list) + let listTimelineController = ListTimelineViewController(for: list, mastodonController: self.mastodonController) listTimelineController.presentEditOnAppear = true self.show(listTimelineController, sender: nil) } @@ -205,19 +215,19 @@ class ExploreViewController: EnhancedTableViewController { present(alert, animated: true) case let .savedHashtag(hashtag): - show(HashtagTimelineViewController(for: hashtag), sender: nil) + show(HashtagTimelineViewController(for: hashtag, mastodonController: mastodonController), sender: nil) case .addSavedHashtag: tableView.selectRow(at: nil, animated: true, scrollPosition: .none) - let navController = UINavigationController(rootViewController: AddSavedHashtagViewController()) + let navController = UINavigationController(rootViewController: AddSavedHashtagViewController(mastodonController: mastodonController)) present(navController, animated: true) case let .savedInstance(url): - show(InstanceTimelineViewController(for: url), sender: nil) + show(InstanceTimelineViewController(for: url, parentMastodonController: mastodonController), sender: nil) case .findInstance: tableView.selectRow(at: nil, animated: true, scrollPosition: .none) - let findController = FindInstanceViewController() + let findController = FindInstanceViewController(parentMastodonController: mastodonController) findController.instanceTimelineDelegate = self let navController = UINavigationController(rootViewController: findController) present(navController, animated: true) @@ -344,7 +354,7 @@ extension ExploreViewController { extension ExploreViewController: InstanceTimelineViewControllerDelegate { func didSaveInstance(url: URL) { dismiss(animated: true) { - self.show(InstanceTimelineViewController(for: url), sender: nil) + self.show(InstanceTimelineViewController(for: url, parentMastodonController: self.mastodonController), sender: nil) } } diff --git a/Tusker/Screens/FindInstanceViewController.swift b/Tusker/Screens/FindInstanceViewController.swift index 70a3efbf..abe07e43 100644 --- a/Tusker/Screens/FindInstanceViewController.swift +++ b/Tusker/Screens/FindInstanceViewController.swift @@ -10,8 +10,20 @@ import UIKit class FindInstanceViewController: InstanceSelectorTableViewController { + weak var parentMastodonController: MastodonController? + var instanceTimelineDelegate: InstanceTimelineViewControllerDelegate? + init(parentMastodonController: MastodonController) { + self.parentMastodonController = parentMastodonController + + super.init() + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + override func viewDidLoad() { super.viewDidLoad() @@ -32,7 +44,7 @@ class FindInstanceViewController: InstanceSelectorTableViewController { extension FindInstanceViewController: InstanceSelectorTableViewControllerDelegate { func didSelectInstance(url: URL) { - let instanceTimelineController = InstanceTimelineViewController(for: url) + let instanceTimelineController = InstanceTimelineViewController(for: url, parentMastodonController: parentMastodonController!) instanceTimelineController.delegate = instanceTimelineDelegate show(instanceTimelineController, sender: self) } diff --git a/Tusker/Screens/Lists/EditListAccountsViewController.swift b/Tusker/Screens/Lists/EditListAccountsViewController.swift index ff7e2c1d..be46daf8 100644 --- a/Tusker/Screens/Lists/EditListAccountsViewController.swift +++ b/Tusker/Screens/Lists/EditListAccountsViewController.swift @@ -11,6 +11,8 @@ import Pachyderm class EditListAccountsViewController: EnhancedTableViewController { + let mastodonController: MastodonController + let list: List var dataSource: DataSource! @@ -20,8 +22,9 @@ class EditListAccountsViewController: EnhancedTableViewController { var searchResultsController: SearchResultsViewController! var searchController: UISearchController! - init(list: List) { + init(list: List, mastodonController: MastodonController) { self.list = list + self.mastodonController = mastodonController super.init(style: .plain) @@ -49,7 +52,7 @@ class EditListAccountsViewController: EnhancedTableViewController { }) dataSource.editListAccountsController = self - searchResultsController = SearchResultsViewController() + searchResultsController = SearchResultsViewController(mastodonController: mastodonController) searchResultsController.delegate = self searchResultsController.onlySections = [.accounts] searchController = UISearchController(searchResultsController: searchResultsController) @@ -70,14 +73,14 @@ class EditListAccountsViewController: EnhancedTableViewController { func loadAccounts() { let request = List.getAccounts(list) - MastodonController.client.run(request) { (response) in + mastodonController.run(request) { (response) in guard case let .success(accounts, pagination) = response else { fatalError() } self.nextRange = pagination?.older - MastodonCache.addAll(accounts: accounts) + self.mastodonController.cache.addAll(accounts: accounts) var snapshot = self.dataSource.snapshot() snapshot.deleteSections([.accounts]) @@ -109,7 +112,7 @@ class EditListAccountsViewController: EnhancedTableViewController { fatalError() } let request = List.update(self.list, title: text) - MastodonController.client.run(request) { (response) in + self.mastodonController.run(request) { (response) in guard case .success(_, _) = response else { fatalError() } @@ -143,7 +146,7 @@ extension EditListAccountsViewController { } let request = List.remove(editListAccountsController!.list, accounts: [id]) - MastodonController.client.run(request) { (response) in + editListAccountsController!.mastodonController.run(request) { (response) in guard case .success(_, _) = response else { fatalError() } @@ -157,7 +160,7 @@ extension EditListAccountsViewController { extension EditListAccountsViewController: SearchResultsViewControllerDelegate { func selectedSearchResult(account accountID: String) { let request = List.add(list, accounts: [accountID]) - MastodonController.client.run(request) { (response) in + mastodonController.run(request) { (response) in guard case .success(_, _) = response else { fatalError() } diff --git a/Tusker/Screens/Lists/ListTimelineViewController.swift b/Tusker/Screens/Lists/ListTimelineViewController.swift index 664d65ee..bf8dbac3 100644 --- a/Tusker/Screens/Lists/ListTimelineViewController.swift +++ b/Tusker/Screens/Lists/ListTimelineViewController.swift @@ -15,10 +15,10 @@ class ListTimelineViewController: TimelineTableViewController { var presentEditOnAppear = false - init(for list: List) { + init(for list: List, mastodonController: MastodonController) { self.list = list - super.init(for: .list(id: list.id)) + super.init(for: .list(id: list.id), mastodonController: mastodonController) title = list.title } @@ -42,7 +42,7 @@ class ListTimelineViewController: TimelineTableViewController { } func presentEdit(animated: Bool) { - let editListAccountsController = EditListAccountsViewController(list: list) + let editListAccountsController = EditListAccountsViewController(list: list, mastodonController: mastodonController) editListAccountsController.navigationItem.leftBarButtonItem = UIBarButtonItem(barButtonSystemItem: .done, target: self, action: #selector(doneButtonPressed)) let navController = UINavigationController(rootViewController: editListAccountsController) present(navController, animated: animated) diff --git a/Tusker/Screens/Main/MainTabBarViewController.swift b/Tusker/Screens/Main/MainTabBarViewController.swift index 50ca2284..932a257a 100644 --- a/Tusker/Screens/Main/MainTabBarViewController.swift +++ b/Tusker/Screens/Main/MainTabBarViewController.swift @@ -9,6 +9,8 @@ import UIKit class MainTabBarViewController: UITabBarController, UITabBarControllerDelegate { + + weak var mastodonController: MastodonController! override var supportedInterfaceOrientations: UIInterfaceOrientationMask { if UIDevice.current.userInterfaceIdiom == .phone { @@ -18,17 +20,27 @@ class MainTabBarViewController: UITabBarController, UITabBarControllerDelegate { } } + init(mastodonController: MastodonController) { + self.mastodonController = mastodonController + + super.init(nibName: nil, bundle: nil) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + override func viewDidLoad() { super.viewDidLoad() self.delegate = self viewControllers = [ - embedInNavigationController(TimelinesPageViewController()), - embedInNavigationController(NotificationsPageViewController()), - ComposeViewController(), - embedInNavigationController(ExploreViewController()), - embedInNavigationController(MyProfileTableViewController()), + embedInNavigationController(TimelinesPageViewController(mastodonController: mastodonController)), + embedInNavigationController(NotificationsPageViewController(mastodonController: mastodonController)), + ComposeViewController(mastodonController: mastodonController), + embedInNavigationController(ExploreViewController(mastodonController: mastodonController)), + embedInNavigationController(MyProfileTableViewController(mastodonController: mastodonController)), ] } @@ -49,7 +61,7 @@ class MainTabBarViewController: UITabBarController, UITabBarControllerDelegate { } func presentCompose() { - let compose = ComposeViewController() + let compose = ComposeViewController(mastodonController: mastodonController) let navigationController = embedInNavigationController(compose) navigationController.presentationController?.delegate = compose present(navigationController, animated: true) diff --git a/Tusker/Screens/Notifications/NotificationsPageViewController.swift b/Tusker/Screens/Notifications/NotificationsPageViewController.swift index 3e503e1b..6a23f5c6 100644 --- a/Tusker/Screens/Notifications/NotificationsPageViewController.swift +++ b/Tusker/Screens/Notifications/NotificationsPageViewController.swift @@ -13,13 +13,17 @@ class NotificationsPageViewController: SegmentedPageViewController { private let notificationsTitle = NSLocalizedString("Notifications", comment: "notifications tab title") private let mentionsTitle = NSLocalizedString("Mentions", comment: "mentions tab title") + + weak var mastodonController: MastodonController! - init() { - let notifications = NotificationsTableViewController(allowedTypes: Pachyderm.Notification.Kind.allCases) + init(mastodonController: MastodonController) { + self.mastodonController = mastodonController + + let notifications = NotificationsTableViewController(allowedTypes: Pachyderm.Notification.Kind.allCases, mastodonController: mastodonController) notifications.title = notificationsTitle notifications.userActivity = UserActivityManager.checkNotificationsActivity() - let mentions = NotificationsTableViewController(allowedTypes: [.mention]) + let mentions = NotificationsTableViewController(allowedTypes: [.mention], mastodonController: mastodonController) mentions.title = mentionsTitle mentions.userActivity = UserActivityManager.checkMentionsActivity() diff --git a/Tusker/Screens/Notifications/NotificationsTableViewController.swift b/Tusker/Screens/Notifications/NotificationsTableViewController.swift index 0bd72ca5..1ff65686 100644 --- a/Tusker/Screens/Notifications/NotificationsTableViewController.swift +++ b/Tusker/Screens/Notifications/NotificationsTableViewController.swift @@ -16,6 +16,8 @@ class NotificationsTableViewController: EnhancedTableViewController { private let followGroupCell = "followGroupCell" private let followRequestCell = "followRequestCell" + let mastodonController: MastodonController + let excludedTypes: [Pachyderm.Notification.Kind] let groupTypes = [Notification.Kind.favourite, .reblog, .follow] @@ -30,8 +32,9 @@ class NotificationsTableViewController: EnhancedTableViewController { var newer: RequestRange? var older: RequestRange? - init(allowedTypes: [Pachyderm.Notification.Kind]) { + init(allowedTypes: [Pachyderm.Notification.Kind], mastodonController: MastodonController) { self.excludedTypes = Array(Set(Pachyderm.Notification.Kind.allCases).subtracting(allowedTypes)) + self.mastodonController = mastodonController super.init(style: .plain) @@ -56,17 +59,17 @@ class NotificationsTableViewController: EnhancedTableViewController { tableView.prefetchDataSource = self - let request = MastodonController.client.getNotifications(excludeTypes: excludedTypes) - MastodonController.client.run(request) { result in + let request = Client.getNotifications(excludeTypes: excludedTypes) + mastodonController.run(request) { result in guard case let .success(notifications, pagination) = result else { fatalError() } let groups = NotificationGroup.createGroups(notifications: notifications, only: self.groupTypes) self.groups.append(contentsOf: groups) - MastodonCache.addAll(notifications: notifications) - MastodonCache.addAll(statuses: notifications.compactMap { $0.status }) - MastodonCache.addAll(accounts: notifications.map { $0.account }) + self.mastodonController.cache.addAll(notifications: notifications) + self.mastodonController.cache.addAll(statuses: notifications.compactMap { $0.status }) + self.mastodonController.cache.addAll(accounts: notifications.map { $0.account }) self.newer = pagination?.newer self.older = pagination?.older @@ -89,31 +92,31 @@ class NotificationsTableViewController: EnhancedTableViewController { switch group.kind { case .mention: - guard let notification = MastodonCache.notification(for: group.notificationIDs.first!), + guard let notification = mastodonController.cache.notification(for: group.notificationIDs.first!), let cell = tableView.dequeueReusableCell(withIdentifier: statusCell, for: indexPath) as? TimelineStatusTableViewCell else { fatalError() } - cell.updateUI(statusID: notification.status!.id, state: group.statusState!) cell.delegate = self + cell.updateUI(statusID: notification.status!.id, state: group.statusState!) return cell case .favourite, .reblog: guard let cell = tableView.dequeueReusableCell(withIdentifier: actionGroupCell, for: indexPath) as? ActionNotificationGroupTableViewCell else { fatalError() } - cell.updateUI(group: group) cell.delegate = self + cell.updateUI(group: group) return cell case .follow: guard let cell = tableView.dequeueReusableCell(withIdentifier: followGroupCell, for: indexPath) as? FollowNotificationGroupTableViewCell else { fatalError() } - cell.updateUI(group: group) cell.delegate = self + cell.updateUI(group: group) return cell case .followRequest: - guard let notification = MastodonCache.notification(for: group.notificationIDs.first!), + guard let notification = mastodonController.cache.notification(for: group.notificationIDs.first!), let cell = tableView.dequeueReusableCell(withIdentifier: followRequestCell, for: indexPath) as? FollowRequestNotificationTableViewCell else { fatalError() } - cell.updateUI(notification: notification) cell.delegate = self + cell.updateUI(notification: notification) return cell } } @@ -124,17 +127,17 @@ class NotificationsTableViewController: EnhancedTableViewController { if indexPath.row == groups.count - 1 { guard let older = older else { return } - let request = MastodonController.client.getNotifications(excludeTypes: excludedTypes, range: older) - MastodonController.client.run(request) { result in + let request = Client.getNotifications(excludeTypes: excludedTypes, range: older) + mastodonController.run(request) { result in guard case let .success(newNotifications, pagination) = result else { fatalError() } let groups = NotificationGroup.createGroups(notifications: newNotifications, only: self.groupTypes) self.groups.append(contentsOf: groups) - MastodonCache.addAll(notifications: newNotifications) - MastodonCache.addAll(statuses: newNotifications.compactMap { $0.status }) - MastodonCache.addAll(accounts: newNotifications.map { $0.account }) + self.mastodonController.cache.addAll(notifications: newNotifications) + self.mastodonController.cache.addAll(statuses: newNotifications.compactMap { $0.status }) + self.mastodonController.cache.addAll(accounts: newNotifications.map { $0.account }) self.older = pagination?.older } @@ -182,7 +185,7 @@ class NotificationsTableViewController: EnhancedTableViewController { .map(Pachyderm.Notification.dismiss(id:)) .forEach { (request) in group.enter() - MastodonController.client.run(request) { (response) in + mastodonController.run(request) { (response) in group.leave() } } @@ -196,17 +199,17 @@ class NotificationsTableViewController: EnhancedTableViewController { @objc func refreshNotifications(_ sender: Any) { guard let newer = newer else { return } - let request = MastodonController.client.getNotifications(excludeTypes: excludedTypes, range: newer) - MastodonController.client.run(request) { result in + let request = Client.getNotifications(excludeTypes: excludedTypes, range: newer) + mastodonController.run(request) { result in guard case let .success(newNotifications, pagination) = result else { fatalError() } let groups = NotificationGroup.createGroups(notifications: newNotifications, only: self.groupTypes) self.groups.insert(contentsOf: groups, at: 0) - MastodonCache.addAll(notifications: newNotifications) - MastodonCache.addAll(statuses: newNotifications.compactMap { $0.status }) - MastodonCache.addAll(accounts: newNotifications.map { $0.account }) + self.mastodonController.cache.addAll(notifications: newNotifications) + self.mastodonController.cache.addAll(statuses: newNotifications.compactMap { $0.status }) + self.mastodonController.cache.addAll(accounts: newNotifications.map { $0.account }) if let newer = pagination?.newer { self.newer = newer @@ -224,6 +227,7 @@ class NotificationsTableViewController: EnhancedTableViewController { } extension NotificationsTableViewController: StatusTableViewCellDelegate { + var apiController: MastodonController { mastodonController } func statusCellCollapsedStateChanged(_ cell: BaseStatusTableViewCell) { // causes the table view to recalculate the cell heights tableView.beginUpdates() @@ -235,7 +239,7 @@ extension NotificationsTableViewController: UITableViewDataSourcePrefetching { func tableView(_ tableView: UITableView, prefetchRowsAt indexPaths: [IndexPath]) { for indexPath in indexPaths { for notificationID in groups[indexPath.row].notificationIDs { - guard let notification = MastodonCache.notification(for: notificationID) else { continue } + guard let notification = mastodonController.cache.notification(for: notificationID) else { continue } ImageCache.avatars.get(notification.account.avatar, completion: nil) } } @@ -244,7 +248,7 @@ extension NotificationsTableViewController: UITableViewDataSourcePrefetching { func tableView(_ tableView: UITableView, cancelPrefetchingForRowsAt indexPaths: [IndexPath]) { for indexPath in indexPaths { for notificationID in groups[indexPath.row].notificationIDs { - guard let notification = MastodonCache.notification(for: notificationID) else { continue } + guard let notification = mastodonController.cache.notification(for: notificationID) else { continue } ImageCache.avatars.cancel(notification.account.avatar) } } diff --git a/Tusker/Screens/Onboarding/InstanceSelectorTableViewController.swift b/Tusker/Screens/Onboarding/InstanceSelectorTableViewController.swift index 4c55262a..b77e8bc9 100644 --- a/Tusker/Screens/Onboarding/InstanceSelectorTableViewController.swift +++ b/Tusker/Screens/Onboarding/InstanceSelectorTableViewController.swift @@ -10,7 +10,7 @@ import UIKit import Combine import Pachyderm -protocol InstanceSelectorTableViewControllerDelegate { +protocol InstanceSelectorTableViewControllerDelegate: class { func didSelectInstance(url: URL) } @@ -18,7 +18,7 @@ fileprivate let instanceCell = "instanceCell" class InstanceSelectorTableViewController: UITableViewController { - var delegate: InstanceSelectorTableViewControllerDelegate? + weak var delegate: InstanceSelectorTableViewControllerDelegate? var dataSource: DataSource! var searchController: UISearchController! @@ -115,7 +115,7 @@ class InstanceSelectorTableViewController: UITableViewController { let components = parseURLComponents(input: domain) let client = Client(baseURL: components.url!) - let request = client.getInstance() + let request = Client.getInstance() client.run(request) { (response) in var snapshot = self.dataSource.snapshot() snapshot.deleteItems(snapshot.itemIdentifiers(inSection: .selected)) diff --git a/Tusker/Screens/Onboarding/OnboardingViewController.swift b/Tusker/Screens/Onboarding/OnboardingViewController.swift index 7df60929..9bb2af60 100644 --- a/Tusker/Screens/Onboarding/OnboardingViewController.swift +++ b/Tusker/Screens/Onboarding/OnboardingViewController.swift @@ -10,7 +10,7 @@ import UIKit import AuthenticationServices protocol OnboardingViewControllerDelegate { - func didFinishOnboarding() + func didFinishOnboarding(account: LocalData.UserAccountInfo) } class OnboardingViewController: UINavigationController { @@ -44,15 +44,13 @@ class OnboardingViewController: UINavigationController { } extension OnboardingViewController: InstanceSelectorTableViewControllerDelegate { - func didSelectInstance(url: URL) { - LocalData.shared.instanceURL = url - MastodonController.createClient() - MastodonController.registerApp { - let clientID = LocalData.shared.clientID! - + func didSelectInstance(url instanceURL: URL) { + let mastodonController = MastodonController(instanceURL: instanceURL) + mastodonController.registerApp { (clientID, clientSecret) in + let callbackURL = "tusker://oauth" - var components = URLComponents(url: url, resolvingAgainstBaseURL: false)! + var components = URLComponents(url: instanceURL, resolvingAgainstBaseURL: false)! components.path = "/oauth/authorize" components.queryItems = [ URLQueryItem(name: "client_id", value: clientID), @@ -69,9 +67,13 @@ extension OnboardingViewController: InstanceSelectorTableViewControllerDelegate let item = components.queryItems?.first(where: { $0.name == "code" }), let authCode = item.value else { return } - MastodonController.authorize(authorizationCode: authCode) { - DispatchQueue.main.async { - self.onboardingDelegate?.didFinishOnboarding() + mastodonController.authorize(authorizationCode: authCode) { (accessToken) in + mastodonController.getOwnAccount { (account) in + DispatchQueue.main.async { + let accountInfo = LocalData.shared.addAccount(instanceURL: instanceURL, clientID: clientID, clientSecret: clientSecret, username: account.username, accessToken: accessToken) + + self.onboardingDelegate?.didFinishOnboarding(account: accountInfo) + } } } } diff --git a/Tusker/Screens/Preferences/PreferencesNavigationController.swift b/Tusker/Screens/Preferences/PreferencesNavigationController.swift index 0ac49c04..3b4d7b4f 100644 --- a/Tusker/Screens/Preferences/PreferencesNavigationController.swift +++ b/Tusker/Screens/Preferences/PreferencesNavigationController.swift @@ -10,9 +10,12 @@ import UIKit import SwiftUI class PreferencesNavigationController: UINavigationController { + + private var isSwitchingAccounts = false - init() { - let hostingController = UIHostingController(rootView: PreferencesView()) + init(mastodonController: MastodonController) { + let view = PreferencesView() + let hostingController = UIHostingController(rootView: view) super.init(rootViewController: hostingController) hostingController.navigationItem.rightBarButtonItem = UIBarButtonItem(barButtonSystemItem: .done, target: self, action: #selector(donePressed)) } @@ -20,16 +23,67 @@ class PreferencesNavigationController: UINavigationController { required init?(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") } + + override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + + NotificationCenter.default.addObserver(self, selector: #selector(showAddAccount), name: .addAccount, object: nil) + NotificationCenter.default.addObserver(self, selector: #selector(activateAccount(_:)), name: .activateAccount, object: nil) + NotificationCenter.default.addObserver(self, selector: #selector(userLoggedOut), name: .userLoggedOut, object: nil) + } override func viewWillDisappear(_ animated: Bool) { super.viewWillDisappear(animated) - // workaround for onDisappear not being called when a modally presented UIHostingController is dismissed - NotificationCenter.default.post(name: .preferencesChanged, object: nil) + if !isSwitchingAccounts { + // workaround for onDisappear not being called when a modally presented UIHostingController is dismissed + NotificationCenter.default.post(name: .preferencesChanged, object: nil) + } } @objc func donePressed() { dismiss(animated: true) } + + @objc func showAddAccount() { + let onboardingController = OnboardingViewController() + onboardingController.onboardingDelegate = self + onboardingController.instanceSelector.navigationItem.leftBarButtonItem = UIBarButtonItem(barButtonSystemItem: .cancel, target: self, action: #selector(cancelAddAccount)) + show(onboardingController, sender: self) + } + + @objc func cancelAddAccount() { + dismiss(animated: true) // dismisses instance selector + } + + @objc func activateAccount(_ notification: Notification) { + let account = notification.userInfo!["account"] as! LocalData.UserAccountInfo + let sceneDelegate = self.view.window!.windowScene!.delegate as! SceneDelegate + isSwitchingAccounts = true + dismiss(animated: true) { // dismiss preferences + sceneDelegate.activateAccount(account) + } + } + + @objc func userLoggedOut() { + let sceneDelegate = self.view.window!.windowScene!.delegate as! SceneDelegate + isSwitchingAccounts = true + dismiss(animated: true) { // dismiss preferences + sceneDelegate.logoutCurrent() + } + } } + +extension PreferencesNavigationController: OnboardingViewControllerDelegate { + func didFinishOnboarding(account: LocalData.UserAccountInfo) { + DispatchQueue.main.async { + let sceneDelegate = self.view.window!.windowScene!.delegate as! SceneDelegate + self.dismiss(animated: true) { // dismiss instance selector + self.dismiss(animated: true) { // dismiss preferences + sceneDelegate.activateAccount(account) + } + } + } + } +} diff --git a/Tusker/Screens/Preferences/PreferencesView.swift b/Tusker/Screens/Preferences/PreferencesView.swift index 9df96be2..b30daa52 100644 --- a/Tusker/Screens/Preferences/PreferencesView.swift +++ b/Tusker/Screens/Preferences/PreferencesView.swift @@ -7,20 +7,44 @@ import SwiftUI -struct PreferencesView : View { +struct PreferencesView: View { + @ObservedObject var localData = LocalData.shared @State private var showingLogoutConfirmation = false - + var body: some View { // workaround: the navigation view is provided by MyProfileTableViewController so that it can inject the Done button // NavigationView { List { Section { + ForEach(localData.accounts, id: \.accessToken) { (account) in + Button(action: { + NotificationCenter.default.post(name: .activateAccount, object: nil, userInfo: ["account": account]) + }) { + HStack { + Text(account.username) + .foregroundColor(.primary) + Spacer() + if account == self.localData.getMostRecentAccount() { + Image(systemName: "checkmark") + .renderingMode(.template) + .foregroundColor(.secondary) + } + } + } + } Button(action: { - self.showingLogoutConfirmation = true + NotificationCenter.default.post(name: .addAccount, object: nil) }) { - Text("Logout") - }.alert(isPresented: $showingLogoutConfirmation) { - Alert(title: Text("Are you sure you want to logout?"), message: nil, primaryButton: .destructive(Text("Logout"), action: self.logoutPressed), secondaryButton: .cancel()) + Text("Add Account...") + } + if localData.getMostRecentAccount() != nil { + Button(action: { + self.showingLogoutConfirmation = true + }) { + Text("Logout from current") + }.alert(isPresented: $showingLogoutConfirmation) { + Alert(title: Text("Are you sure you want to logout?"), message: nil, primaryButton: .destructive(Text("Logout"), action: self.logoutPressed), secondaryButton: .cancel()) + } } } @@ -49,11 +73,6 @@ struct PreferencesView : View { } func logoutPressed() { - LocalData.shared.onboardingComplete = false - LocalData.shared.instanceURL = nil - LocalData.shared.clientID = nil - LocalData.shared.clientSecret = nil - LocalData.shared.accessToken = nil NotificationCenter.default.post(name: .userLoggedOut, object: nil) } } @@ -61,7 +80,7 @@ struct PreferencesView : View { #if DEBUG struct PreferencesView_Previews : PreviewProvider { static var previews: some View { - PreferencesView() + return PreferencesView() } } #endif diff --git a/Tusker/Screens/Profile/MyProfileTableViewController.swift b/Tusker/Screens/Profile/MyProfileTableViewController.swift index 3e58ac59..09ac7884 100644 --- a/Tusker/Screens/Profile/MyProfileTableViewController.swift +++ b/Tusker/Screens/Profile/MyProfileTableViewController.swift @@ -11,14 +11,14 @@ import SwiftUI class MyProfileTableViewController: ProfileTableViewController { - init() { - super.init(accountID: nil) + init(mastodonController: MastodonController) { + super.init(accountID: nil, mastodonController: mastodonController) title = "My Profile" tabBarItem.image = UIImage(systemName: "person.fill") - MastodonController.getOwnAccount { (account) in + mastodonController.getOwnAccount { (account) in self.accountID = account.id ImageCache.avatars.get(account.avatar, completion: { (data) in @@ -50,7 +50,7 @@ class MyProfileTableViewController: ProfileTableViewController { } @objc func preferencesPressed() { - present(PreferencesNavigationController(), animated: true) + present(PreferencesNavigationController(mastodonController: mastodonController), animated: true) } @objc func closePreferences() { diff --git a/Tusker/Screens/Profile/ProfileTableViewController.swift b/Tusker/Screens/Profile/ProfileTableViewController.swift index 3386dcab..ee7a9cac 100644 --- a/Tusker/Screens/Profile/ProfileTableViewController.swift +++ b/Tusker/Screens/Profile/ProfileTableViewController.swift @@ -12,6 +12,8 @@ import SafariServices class ProfileTableViewController: EnhancedTableViewController { + weak var mastodonController: MastodonController! + var accountID: String! { didSet { if shouldLoadOnAccountIDSet { @@ -43,7 +45,9 @@ class ProfileTableViewController: EnhancedTableViewController { var shouldLoadOnAccountIDSet = false var loadingVC: LoadingViewController? = nil - init(accountID: String?) { + init(accountID: String?, mastodonController: MastodonController) { + self.mastodonController = mastodonController + self.accountID = accountID super.init(style: .plain) @@ -69,12 +73,12 @@ class ProfileTableViewController: EnhancedTableViewController { tableView.prefetchDataSource = self if let accountID = accountID { - if MastodonCache.account(for: accountID) != nil { + if mastodonController.cache.account(for: accountID) != nil { updateAccountUI() } else { loadingVC = LoadingViewController() embedChild(loadingVC!) - MastodonCache.account(for: accountID) { (account) in + mastodonController.cache.account(for: accountID) { (account) in guard account != nil else { let alert = UIAlertController(title: "Something Went Wrong", message: "Couldn't load the selected account", preferredStyle: .alert) alert.addAction(UIAlertAction(title: "OK", style: .default, handler: { (_) in @@ -108,14 +112,14 @@ class ProfileTableViewController: EnhancedTableViewController { getStatuses(onlyPinned: true) { (response) in guard case let .success(statuses, _) = response else { fatalError() } - MastodonCache.addAll(statuses: statuses) + self.mastodonController.cache.addAll(statuses: statuses) self.pinnedStatuses = statuses.map { ($0.id, .unknown) } } getStatuses() { response in guard case let .success(statuses, pagination) = response else { fatalError() } - MastodonCache.addAll(statuses: statuses) + self.mastodonController.cache.addAll(statuses: statuses) self.timelineSegments.append(statuses.map { ($0.id, .unknown) }) self.older = pagination?.older @@ -124,18 +128,18 @@ class ProfileTableViewController: EnhancedTableViewController { } @objc func updateUIForPreferences() { - guard let account = MastodonCache.account(for: accountID) else { fatalError("Missing cached account \(accountID!)") } + guard let accountID = accountID, let account = mastodonController.cache.account(for: accountID) else { return } navigationItem.title = account.realDisplayName } func getStatuses(for range: RequestRange = .default, onlyPinned: Bool = false, completion: @escaping Client.Callback<[Status]>) { let request = Account.getStatuses(accountID, range: range, onlyMedia: false, pinned: onlyPinned, excludeReplies: !Preferences.shared.showRepliesInProfiles) - MastodonController.client.run(request, completion: completion) + mastodonController.run(request, completion: completion) } func sendMessageMentioning() { - guard let account = MastodonCache.account(for: accountID) else { fatalError("Missing cached account \(accountID!)") } - let vc = UINavigationController(rootViewController: ComposeViewController(mentioningAcct: account.acct)) + guard let account = mastodonController.cache.account(for: accountID) else { fatalError("Missing cached account \(accountID!)") } + let vc = UINavigationController(rootViewController: ComposeViewController(mentioningAcct: account.acct, mastodonController: mastodonController)) present(vc, animated: true) } @@ -148,7 +152,7 @@ class ProfileTableViewController: EnhancedTableViewController { override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { if section == 0 { - return accountID == nil || MastodonCache.account(for: accountID) == nil ? 0 : 1 + return accountID == nil || mastodonController.cache.account(for: accountID) == nil ? 0 : 1 } else if section == 1 { return pinnedStatuses.count } else { @@ -168,14 +172,14 @@ class ProfileTableViewController: EnhancedTableViewController { guard let cell = tableView.dequeueReusableCell(withIdentifier: "statusCell", for: indexPath) as? TimelineStatusTableViewCell else { fatalError() } let (id, state) = pinnedStatuses[indexPath.row] cell.showPinned = true - cell.updateUI(statusID: id, state: state) cell.delegate = self + cell.updateUI(statusID: id, state: state) return cell default: guard let cell = tableView.dequeueReusableCell(withIdentifier: "statusCell", for: indexPath) as? TimelineStatusTableViewCell else { fatalError() } let (id, state) = timelineSegments[indexPath.section - 2][indexPath.row] - cell.updateUI(statusID: id, state: state) cell.delegate = self + cell.updateUI(statusID: id, state: state) return cell } } @@ -189,7 +193,7 @@ class ProfileTableViewController: EnhancedTableViewController { getStatuses(for: older) { response in guard case let .success(newStatuses, pagination) = response else { fatalError() } - MastodonCache.addAll(statuses: newStatuses) + self.mastodonController.cache.addAll(statuses: newStatuses) self.timelineSegments[indexPath.section - 2].append(contentsOf: newStatuses.map { ($0.id, .unknown) }) self.older = pagination?.older @@ -215,7 +219,7 @@ class ProfileTableViewController: EnhancedTableViewController { getStatuses(for: newer) { response in guard case let .success(newStatuses, pagination) = response else { fatalError() } - MastodonCache.addAll(statuses: newStatuses) + self.mastodonController.cache.addAll(statuses: newStatuses) self.timelineSegments[0].insert(contentsOf: newStatuses.map { ($0.id, .unknown) }, at: 0) if let newer = pagination?.newer { @@ -229,7 +233,7 @@ class ProfileTableViewController: EnhancedTableViewController { getStatuses(onlyPinned: true) { (response) in guard case let .success(newPinnedStatuses, _) = response else { fatalError() } - MastodonCache.addAll(statuses: newPinnedStatuses) + self.mastodonController.cache.addAll(statuses: newPinnedStatuses) let oldPinnedStatuses = self.pinnedStatuses var pinnedStatuses = [(id: String, state: StatusState)]() @@ -253,6 +257,8 @@ class ProfileTableViewController: EnhancedTableViewController { } extension ProfileTableViewController: StatusTableViewCellDelegate { + var apiController: MastodonController { mastodonController } + func statusCellCollapsedStateChanged(_ cell: BaseStatusTableViewCell) { // causes the table view to recalculate the cell heights tableView.beginUpdates() @@ -262,9 +268,9 @@ extension ProfileTableViewController: StatusTableViewCellDelegate { extension ProfileTableViewController: ProfileHeaderTableViewCellDelegate { func showMoreOptions(cell: ProfileHeaderTableViewCell) { - let account = MastodonCache.account(for: accountID)! + let account = mastodonController.cache.account(for: accountID)! - MastodonCache.relationship(for: account.id) { [weak self] (relationship) in + mastodonController.cache.relationship(for: account.id) { [weak self] (relationship) in guard let self = self else { return } var customActivities: [UIActivity] = [OpenInSafariActivity()] @@ -287,7 +293,7 @@ extension ProfileTableViewController: UITableViewDataSourcePrefetching { func tableView(_ tableView: UITableView, prefetchRowsAt indexPaths: [IndexPath]) { for indexPath in indexPaths where indexPath.section > 1 { let statusID = timelineSegments[indexPath.section - 2][indexPath.row].id - guard let status = MastodonCache.status(for: statusID) else { continue } + guard let status = mastodonController.cache.status(for: statusID) else { continue } ImageCache.avatars.get(status.account.avatar, completion: nil) for attachment in status.attachments { ImageCache.attachments.get(attachment.url, completion: nil) @@ -298,7 +304,7 @@ extension ProfileTableViewController: UITableViewDataSourcePrefetching { func tableView(_ tableView: UITableView, cancelPrefetchingForRowsAt indexPaths: [IndexPath]) { for indexPath in indexPaths where indexPath.section > 1 { let statusID = timelineSegments[indexPath.section - 2][indexPath.row].id - guard let status = MastodonCache.status(for: statusID) else { continue } + guard let status = mastodonController.cache.status(for: statusID) else { continue } ImageCache.avatars.cancel(status.account.avatar) for attachment in status.attachments { ImageCache.attachments.cancel(attachment.url) diff --git a/Tusker/Screens/Search/SearchResultsViewController.swift b/Tusker/Screens/Search/SearchResultsViewController.swift index e1388f42..9c973461 100644 --- a/Tusker/Screens/Search/SearchResultsViewController.swift +++ b/Tusker/Screens/Search/SearchResultsViewController.swift @@ -28,6 +28,8 @@ extension SearchResultsViewControllerDelegate { class SearchResultsViewController: EnhancedTableViewController { + let mastodonController: MastodonController! + weak var exploreNavigationController: UINavigationController? weak var delegate: SearchResultsViewControllerDelegate? @@ -40,7 +42,9 @@ class SearchResultsViewController: EnhancedTableViewController { let searchSubject = PassthroughSubject() var currentQuery: String? - init() { + init(mastodonController: MastodonController) { + self.mastodonController = mastodonController + super.init(style: .grouped) title = NSLocalizedString("Search", comment: "search screen title") @@ -61,18 +65,18 @@ class SearchResultsViewController: EnhancedTableViewController { switch item { case let .account(id): let cell = tableView.dequeueReusableCell(withIdentifier: accountCell, for: indexPath) as! AccountTableViewCell - cell.updateUI(accountID: id) cell.delegate = self + cell.updateUI(accountID: id) return cell case let .hashtag(tag): let cell = tableView.dequeueReusableCell(withIdentifier: hashtagCell, for: indexPath) as! HashtagTableViewCell - cell.updateUI(hashtag: tag) cell.delegate = self + cell.updateUI(hashtag: tag) return cell case let .status(id, state): let cell = tableView.dequeueReusableCell(withIdentifier: statusCell, for: indexPath) as! TimelineStatusTableViewCell - cell.updateUI(statusID: id, state: state) cell.delegate = self + cell.updateUI(statusID: id, state: state) return cell } }) @@ -117,8 +121,8 @@ class SearchResultsViewController: EnhancedTableViewController { activityIndicator.startAnimating() } - let request = MastodonController.client.search(query: query, resolve: true, limit: 10) - MastodonController.client.run(request) { (response) in + let request = Client.search(query: query, resolve: true, limit: 10) + mastodonController.run(request) { (response) in guard case let .success(results, _) = response else { fatalError() } DispatchQueue.main.async { @@ -132,7 +136,7 @@ class SearchResultsViewController: EnhancedTableViewController { if self.onlySections.contains(.accounts) && !results.accounts.isEmpty { snapshot.appendSections([.accounts]) snapshot.appendItems(results.accounts.map { .account($0.id) }, toSection: .accounts) - MastodonCache.addAll(accounts: results.accounts) + self.mastodonController.cache.addAll(accounts: results.accounts) } if self.onlySections.contains(.hashtags) && !results.hashtags.isEmpty { snapshot.appendSections([.hashtags]) @@ -141,8 +145,8 @@ class SearchResultsViewController: EnhancedTableViewController { if self.onlySections.contains(.statuses) && !results.statuses.isEmpty { snapshot.appendSections([.statuses]) snapshot.appendItems(results.statuses.map { .status($0.id, .unknown) }, toSection: .statuses) - MastodonCache.addAll(statuses: results.statuses) - MastodonCache.addAll(accounts: results.statuses.map { $0.account }) + self.mastodonController.cache.addAll(statuses: results.statuses) + self.mastodonController.cache.addAll(accounts: results.statuses.map { $0.account }) } self.dataSource.apply(snapshot) } @@ -217,6 +221,7 @@ extension SearchResultsViewController: UISearchBarDelegate { } extension SearchResultsViewController: StatusTableViewCellDelegate { + var apiController: MastodonController { mastodonController } func statusCellCollapsedStateChanged(_ cell: BaseStatusTableViewCell) { tableView.beginUpdates() tableView.endUpdates() diff --git a/Tusker/Screens/Status Action Account List/StatusActionAccountListTableViewController.swift b/Tusker/Screens/Status Action Account List/StatusActionAccountListTableViewController.swift index c5ccc6d2..dd60518c 100644 --- a/Tusker/Screens/Status Action Account List/StatusActionAccountListTableViewController.swift +++ b/Tusker/Screens/Status Action Account List/StatusActionAccountListTableViewController.swift @@ -14,6 +14,8 @@ class StatusActionAccountListTableViewController: EnhancedTableViewController { private let statusCell = "statusCell" private let accountCell = "accountCell" + let mastodonController: MastodonController + let actionType: ActionType let statusID: String var statusState: StatusState @@ -32,8 +34,11 @@ class StatusActionAccountListTableViewController: EnhancedTableViewController { - Parameter actionType The action that this VC is for. - Parameter statusID The ID of the status to show. - Parameter accountIDs The accounts that will be shown. If `nil` is passed, a request will be performed to load the accounts. + - Parameter mastodonController The `MastodonController` instance this view controller uses. */ - init(actionType: ActionType, statusID: String, statusState: StatusState, accountIDs: [String]?) { + init(actionType: ActionType, statusID: String, statusState: StatusState, accountIDs: [String]?, mastodonController: MastodonController) { + self.mastodonController = mastodonController + self.actionType = actionType self.statusID = statusID self.statusState = statusState @@ -68,16 +73,16 @@ class StatusActionAccountListTableViewController: EnhancedTableViewController { if accountIDs == nil { // account IDs haven't been set, so perform a request to load them - guard let status = MastodonCache.status(for: statusID) else { + guard let status = mastodonController.cache.status(for: statusID) else { fatalError("Missing cached status \(statusID)") } tableView.tableFooterView = UIActivityIndicatorView(style: .large) let request = actionType == .favorite ? Status.getFavourites(status) : Status.getReblogs(status) - MastodonController.client.run(request) { (response) in + mastodonController.run(request) { (response) in guard case let .success(accounts, _) = response else { fatalError() } - MastodonCache.addAll(accounts: accounts) + self.mastodonController.cache.addAll(accounts: accounts) DispatchQueue.main.async { self.accountIDs = accounts.map { $0.id } self.tableView.tableFooterView = nil @@ -111,14 +116,14 @@ class StatusActionAccountListTableViewController: EnhancedTableViewController { switch indexPath.section { case 0: guard let cell = tableView.dequeueReusableCell(withIdentifier: statusCell, for: indexPath) as? TimelineStatusTableViewCell else { fatalError() } - cell.updateUI(statusID: statusID, state: statusState) cell.delegate = self + cell.updateUI(statusID: statusID, state: statusState) return cell case 1: guard let accountIDs = accountIDs, let cell = tableView.dequeueReusableCell(withIdentifier: accountCell, for: indexPath) as? AccountTableViewCell else { fatalError() } - cell.updateUI(accountID: accountIDs[indexPath.row]) cell.delegate = self + cell.updateUI(accountID: accountIDs[indexPath.row]) return cell default: fatalError("Invalid section \(indexPath.section)") @@ -137,6 +142,7 @@ class StatusActionAccountListTableViewController: EnhancedTableViewController { } extension StatusActionAccountListTableViewController: StatusTableViewCellDelegate { + var apiController: MastodonController { mastodonController } func statusCellCollapsedStateChanged(_ cell: BaseStatusTableViewCell) { // causes the table view to recalculate the cell heights tableView.beginUpdates() diff --git a/Tusker/Screens/Timeline/HashtagTimelineViewController.swift b/Tusker/Screens/Timeline/HashtagTimelineViewController.swift index 0313a11e..ab8d5b6c 100644 --- a/Tusker/Screens/Timeline/HashtagTimelineViewController.swift +++ b/Tusker/Screens/Timeline/HashtagTimelineViewController.swift @@ -15,17 +15,17 @@ class HashtagTimelineViewController: TimelineTableViewController { var toggleSaveButton: UIBarButtonItem! var toggleSaveButtonTitle: String { - if SavedHashtagsManager.shared.isSaved(hashtag) { + if SavedDataManager.shared.isSaved(hashtag: hashtag, for: mastodonController.accountInfo!) { return NSLocalizedString("Unsave", comment: "unsave hashtag button") } else { return NSLocalizedString("Save", comment: "save hashtag button") } } - init(for hashtag: Hashtag) { + init(for hashtag: Hashtag, mastodonController: MastodonController) { self.hashtag = hashtag - super.init(for: .tag(hashtag: hashtag.name)) + super.init(for: .tag(hashtag: hashtag.name), mastodonController: mastodonController) } required init?(coder aDecoder: NSCoder) { @@ -48,10 +48,10 @@ class HashtagTimelineViewController: TimelineTableViewController { // MARK: - Interaction @objc func toggleSaveButtonPressed() { - if SavedHashtagsManager.shared.isSaved(hashtag) { - SavedHashtagsManager.shared.remove(hashtag) + if SavedDataManager.shared.isSaved(hashtag: hashtag, for: mastodonController.accountInfo!) { + SavedDataManager.shared.remove(hashtag: hashtag, for: mastodonController.accountInfo!) } else { - SavedHashtagsManager.shared.add(hashtag) + SavedDataManager.shared.add(hashtag: hashtag, for: mastodonController.accountInfo!) } } diff --git a/Tusker/Screens/Timeline/InstanceTimelineViewController.swift b/Tusker/Screens/Timeline/InstanceTimelineViewController.swift index 405c998f..2694bf6f 100644 --- a/Tusker/Screens/Timeline/InstanceTimelineViewController.swift +++ b/Tusker/Screens/Timeline/InstanceTimelineViewController.swift @@ -8,30 +8,38 @@ import UIKit -protocol InstanceTimelineViewControllerDelegate { +protocol InstanceTimelineViewControllerDelegate: class { func didSaveInstance(url: URL) func didUnsaveInstance(url: URL) } class InstanceTimelineViewController: TimelineTableViewController { - var delegate: InstanceTimelineViewControllerDelegate? + weak var delegate: InstanceTimelineViewControllerDelegate? + weak var parentMastodonController: MastodonController? + let instanceURL: URL + let instanceMastodonController: MastodonController var toggleSaveButton: UIBarButtonItem! var toggleSaveButtonTitle: String { - if SavedInstanceManager.shared.isSaved(instanceURL) { + if SavedDataManager.shared.isSaved(instance: instanceURL, for: parentMastodonController!.accountInfo!) { return NSLocalizedString("Unsave", comment: "unsave instance button") } else { return NSLocalizedString("Save", comment: "save instance button") } } - init(for url: URL) { - self.instanceURL = url + init(for url: URL, parentMastodonController: MastodonController) { + self.parentMastodonController = parentMastodonController - super.init(for: .instance(instanceURL: url)) + self.instanceURL = url + + // the timeline VC only stores a weak reference to it, so we need to store a strong reference to make sure it's not released immediately + instanceMastodonController = MastodonController(instanceURL: url) + + super.init(for: .instance(instanceURL: url), mastodonController: instanceMastodonController) } required init?(coder aDecoder: NSCoder) { @@ -51,6 +59,15 @@ class InstanceTimelineViewController: TimelineTableViewController { toggleSaveButton.title = toggleSaveButtonTitle } + // MARK: - Table view data source + + override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { + let cell = super.tableView(tableView, cellForRowAt: indexPath) as! TimelineStatusTableViewCell + cell.delegate = nil + cell.overrideMastodonController = mastodonController + return cell + } + // MARK: - Table view delegate override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { @@ -59,11 +76,11 @@ class InstanceTimelineViewController: TimelineTableViewController { // MARK: - Interaction @objc func toggleSaveButtonPressed() { - if SavedInstanceManager.shared.isSaved(instanceURL) { - SavedInstanceManager.shared.remove(instanceURL) + if SavedDataManager.shared.isSaved(instance: instanceURL, for: parentMastodonController!.accountInfo!) { + SavedDataManager.shared.remove(instance: instanceURL, for: parentMastodonController!.accountInfo!) delegate?.didUnsaveInstance(url: instanceURL) } else { - SavedInstanceManager.shared.add(instanceURL) + SavedDataManager.shared.add(instance: instanceURL, for: parentMastodonController!.accountInfo!) delegate?.didSaveInstance(url: instanceURL) } } diff --git a/Tusker/Screens/Timeline/TimelineTableViewController.swift b/Tusker/Screens/Timeline/TimelineTableViewController.swift index f7321f38..9966b510 100644 --- a/Tusker/Screens/Timeline/TimelineTableViewController.swift +++ b/Tusker/Screens/Timeline/TimelineTableViewController.swift @@ -12,6 +12,7 @@ import Pachyderm class TimelineTableViewController: EnhancedTableViewController { var timeline: Timeline! + weak var mastodonController: MastodonController! var timelineSegments: [[(id: String, state: StatusState)]] = [] { didSet { @@ -24,8 +25,9 @@ class TimelineTableViewController: EnhancedTableViewController { var newer: RequestRange? var older: RequestRange? - init(for timeline: Timeline) { + init(for timeline: Timeline, mastodonController: MastodonController) { self.timeline = timeline + self.mastodonController = mastodonController super.init(style: .plain) @@ -56,15 +58,14 @@ class TimelineTableViewController: EnhancedTableViewController { tableView.prefetchDataSource = self - guard MastodonController.client?.accessToken != nil else { return } loadInitialStatuses() } func loadInitialStatuses() { - let request = MastodonController.client.getStatuses(timeline: timeline) - MastodonController.client.run(request) { response in + let request = Client.getStatuses(timeline: timeline) + mastodonController.run(request) { response in guard case let .success(statuses, pagination) = response else { fatalError() } - MastodonCache.addAll(statuses: statuses) + self.mastodonController.cache.addAll(statuses: statuses) self.timelineSegments.insert(statuses.map { ($0.id, .unknown) }, at: 0) self.newer = pagination?.newer self.older = pagination?.older @@ -86,8 +87,8 @@ class TimelineTableViewController: EnhancedTableViewController { guard let cell = tableView.dequeueReusableCell(withIdentifier: "statusCell", for: indexPath) as? TimelineStatusTableViewCell else { fatalError() } let (id, state) = timelineSegments[indexPath.section][indexPath.row] - cell.updateUI(statusID: id, state: state) cell.delegate = self + cell.updateUI(statusID: id, state: state) return cell } @@ -99,11 +100,11 @@ class TimelineTableViewController: EnhancedTableViewController { indexPath.row == timelineSegments[indexPath.section].count - 1 { guard let older = older else { return } - let request = MastodonController.client.getStatuses(timeline: timeline, range: older) - MastodonController.client.run(request) { response in + let request = Client.getStatuses(timeline: timeline, range: older) + mastodonController.run(request) { response in guard case let .success(newStatuses, pagination) = response else { fatalError() } self.older = pagination?.older - MastodonCache.addAll(statuses: newStatuses) + self.mastodonController.cache.addAll(statuses: newStatuses) self.timelineSegments[self.timelineSegments.count - 1].append(contentsOf: newStatuses.map { ($0.id, .unknown) }) } } @@ -124,11 +125,11 @@ class TimelineTableViewController: EnhancedTableViewController { @objc func refreshStatuses(_ sender: Any) { guard let newer = newer else { return } - let request = MastodonController.client.getStatuses(timeline: timeline, range: newer) - MastodonController.client.run(request) { response in + let request = Client.getStatuses(timeline: timeline, range: newer) + mastodonController.run(request) { response in guard case let .success(newStatuses, pagination) = response else { fatalError() } - - MastodonCache.addAll(statuses: newStatuses) + self.newer = pagination?.newer + self.mastodonController.cache.addAll(statuses: newStatuses) self.timelineSegments[0].insert(contentsOf: newStatuses.map { ($0.id, .unknown) }, at: 0) if let newer = pagination?.newer { @@ -151,6 +152,8 @@ class TimelineTableViewController: EnhancedTableViewController { } extension TimelineTableViewController: StatusTableViewCellDelegate { + var apiController: MastodonController { mastodonController } + func statusCellCollapsedStateChanged(_ cell: BaseStatusTableViewCell) { // causes the table view to recalculate the cell heights tableView.beginUpdates() @@ -161,7 +164,7 @@ extension TimelineTableViewController: StatusTableViewCellDelegate { extension TimelineTableViewController: UITableViewDataSourcePrefetching { func tableView(_ tableView: UITableView, prefetchRowsAt indexPaths: [IndexPath]) { for indexPath in indexPaths { - guard let status = MastodonCache.status(for: statusID(for: indexPath)) else { continue } + guard let status = mastodonController.cache.status(for: statusID(for: indexPath)) else { continue } ImageCache.avatars.get(status.account.avatar, completion: nil) for attachment in status.attachments { ImageCache.attachments.get(attachment.url, completion: nil) @@ -171,7 +174,7 @@ extension TimelineTableViewController: UITableViewDataSourcePrefetching { func tableView(_ tableView: UITableView, cancelPrefetchingForRowsAt indexPaths: [IndexPath]) { for indexPath in indexPaths { - guard let status = MastodonCache.status(for: statusID(for: indexPath)) else { continue } + guard let status = mastodonController.cache.status(for: statusID(for: indexPath)) else { continue } ImageCache.avatars.cancel(status.account.avatar) for attachment in status.attachments { ImageCache.attachments.cancel(attachment.url) diff --git a/Tusker/Screens/Timeline/TimelinesPageViewController.swift b/Tusker/Screens/Timeline/TimelinesPageViewController.swift index 7fd95c3a..19fb6878 100644 --- a/Tusker/Screens/Timeline/TimelinesPageViewController.swift +++ b/Tusker/Screens/Timeline/TimelinesPageViewController.swift @@ -14,14 +14,18 @@ class TimelinesPageViewController: SegmentedPageViewController { private let federatedTitle = NSLocalizedString("Federated", comment: "federated timeline tab title") private let localTitle = NSLocalizedString("Local", comment: "local timeline tab title") - init() { - let home = TimelineTableViewController(for: .home) + weak var mastodonController: MastodonController! + + init(mastodonController: MastodonController) { + self.mastodonController = mastodonController + + let home = TimelineTableViewController(for: .home, mastodonController: mastodonController) home.title = homeTitle - let federated = TimelineTableViewController(for: .public(local: false)) + let federated = TimelineTableViewController(for: .public(local: false), mastodonController: mastodonController) federated.title = federatedTitle - let local = TimelineTableViewController(for: .public(local: true)) + let local = TimelineTableViewController(for: .public(local: true), mastodonController: mastodonController) local.title = localTitle super.init(titles: [ diff --git a/Tusker/Screens/Utilities/Previewing.swift b/Tusker/Screens/Utilities/Previewing.swift index 46a3793f..411fddae 100644 --- a/Tusker/Screens/Utilities/Previewing.swift +++ b/Tusker/Screens/Utilities/Previewing.swift @@ -22,8 +22,11 @@ protocol MenuPreviewProvider { extension MenuPreviewProvider { + private var mastodonController: MastodonController? { navigationDelegate?.apiController } + func actionsForProfile(accountID: String, sourceView: UIView?) -> [UIAction] { - guard let account = MastodonCache.account(for: accountID) else { return [] } + guard let mastodonController = mastodonController, + let account = mastodonController.cache.account(for: accountID) else { return [] } return [ createAction(identifier: "openinsafari", title: "Open in Safari", systemImageName: "safari", handler: { (_) in self.navigationDelegate?.selected(url: account.url) @@ -53,7 +56,8 @@ extension MenuPreviewProvider { } func actionsForStatus(statusID: String, sourceView: UIView?) -> [UIAction] { - guard let status = MastodonCache.status(for: statusID) else { return [] } + guard let mastodonController = mastodonController, + let status = mastodonController.cache.status(for: statusID) else { return [] } return [ createAction(identifier: "reply", title: "Reply", systemImageName: "arrowshape.turn.up.left", handler: { (_) in self.navigationDelegate?.reply(to: statusID) diff --git a/Tusker/Shortcuts/AppShortcutItems.swift b/Tusker/Shortcuts/AppShortcutItems.swift index 0e9dea80..d0efdffa 100644 --- a/Tusker/Shortcuts/AppShortcutItems.swift +++ b/Tusker/Shortcuts/AppShortcutItems.swift @@ -45,7 +45,9 @@ enum AppShortcutItem: String, CaseIterable { case .composePost: tab = .compose } - let controller = (UIApplication.shared.delegate!.window!!.rootViewController as! MainTabBarViewController) + let scene = UIApplication.shared.connectedScenes.compactMap { $0 as? UIWindowScene }.first! + let window = scene.windows.first { $0.isKeyWindow }! + let controller = window.rootViewController as! MainTabBarViewController controller.select(tab: tab) } } diff --git a/Tusker/Shortcuts/UserActivityManager.swift b/Tusker/Shortcuts/UserActivityManager.swift index b53373af..4c181663 100644 --- a/Tusker/Shortcuts/UserActivityManager.swift +++ b/Tusker/Shortcuts/UserActivityManager.swift @@ -15,8 +15,16 @@ class UserActivityManager { private static let encoder = PropertyListEncoder() private static let decoder = PropertyListDecoder() + private static var mastodonController: MastodonController { + let scene = UIApplication.shared.activeOrBackgroundScene! + return scene.session.mastodonController! + } + private static func getMainTabBarController() -> MainTabBarViewController { - return (UIApplication.shared.delegate! as! AppDelegate).window!.rootViewController as! MainTabBarViewController + let scene = UIApplication.shared.connectedScenes.compactMap { $0 as? UIWindowScene }.first! + let window = scene.windows.first { $0.isKeyWindow }! + return window.rootViewController as! MainTabBarViewController +// return (UIApplication.shared.delegate! as! AppDelegate).window!.rootViewController as! MainTabBarViewController } private static func present(_ vc: UIViewController, animated: Bool = true) { @@ -42,7 +50,8 @@ class UserActivityManager { static func handleNewPost(activity: NSUserActivity) { // TODO: check not currently showing compose screen let mentioning = activity.userInfo?["mentioning"] as? String - present(UINavigationController(rootViewController: ComposeViewController(mentioningAcct: mentioning))) + let composeVC = ComposeViewController(mentioningAcct: mentioning, mastodonController: mastodonController) + present(UINavigationController(rootViewController: composeVC)) } // MARK: - Check Notifications @@ -60,6 +69,7 @@ class UserActivityManager { if let navigationController = tabBarController.getTabController(tab: .notifications) as? UINavigationController, let notificationsPageController = navigationController.viewControllers.first as? NotificationsPageViewController { navigationController.popToRootViewController(animated: false) + notificationsPageController.loadViewIfNeeded() notificationsPageController.selectMode(.allNotifications) } } @@ -79,6 +89,7 @@ class UserActivityManager { if let navController = tabBarController.getTabController(tab: .notifications) as? UINavigationController, let notificationsPageController = navController.viewControllers.first as? NotificationsPageViewController { navController.popToRootViewController(animated: false) + notificationsPageController.loadViewIfNeeded() notificationsPageController.selectMode(.mentionsOnly) } } @@ -144,7 +155,8 @@ class UserActivityManager { rootController.segmentedControl.selectedSegmentIndex = index rootController.selectPage(at: index, animated: false) default: - navigationController.pushViewController(TimelineTableViewController(for: timeline), animated: false) + let timeline = TimelineTableViewController(for: timeline, mastodonController: mastodonController) + navigationController.pushViewController(timeline, animated: false) } } @@ -182,7 +194,7 @@ class UserActivityManager { tabBarController.select(tab: .explore) if let navigationController = tabBarController.getTabController(tab: .explore) as? UINavigationController { navigationController.popToRootViewController(animated: false) - navigationController.pushViewController(BookmarksTableViewController(), animated: false) + navigationController.pushViewController(BookmarksTableViewController(mastodonController: mastodonController), animated: false) } } diff --git a/Tusker/TuskerNavigationDelegate.swift b/Tusker/TuskerNavigationDelegate.swift index 43cf731f..fe3671ff 100644 --- a/Tusker/TuskerNavigationDelegate.swift +++ b/Tusker/TuskerNavigationDelegate.swift @@ -10,7 +10,9 @@ import UIKit import SafariServices import Pachyderm -protocol TuskerNavigationDelegate { +protocol TuskerNavigationDelegate: class { + + var apiController: MastodonController { get } func show(_ vc: UIViewController) @@ -74,15 +76,15 @@ extension TuskerNavigationDelegate where Self: UIViewController { return } - show(ProfileTableViewController(accountID: accountID), sender: self) + show(ProfileTableViewController(accountID: accountID, mastodonController: apiController), sender: self) } func selected(mention: Mention) { - show(ProfileTableViewController(accountID: mention.id), sender: self) + show(ProfileTableViewController(accountID: mention.id, mastodonController: apiController), sender: self) } func selected(tag: Hashtag) { - show(HashtagTimelineViewController(for: tag), sender: self) + show(HashtagTimelineViewController(for: tag, mastodonController: apiController), sender: self) } func selected(url: URL) { @@ -119,7 +121,7 @@ extension TuskerNavigationDelegate where Self: UIViewController { return } - show(ConversationTableViewController(for: statusID, state: state), sender: self) + show(ConversationTableViewController(for: statusID, state: state, mastodonController: apiController), sender: self) } // protocols can't have parameter defaults, so this stub is necessary to fulfill the protocol req @@ -128,7 +130,7 @@ extension TuskerNavigationDelegate where Self: UIViewController { } func compose(mentioning: String?) { - let compose = ComposeViewController(mentioningAcct: mentioning) + let compose = ComposeViewController(mentioningAcct: mentioning, mastodonController: apiController) let vc = UINavigationController(rootViewController: compose) vc.presentationController?.delegate = compose present(vc, animated: true) @@ -139,7 +141,7 @@ extension TuskerNavigationDelegate where Self: UIViewController { } func reply(to statusID: String, mentioningAcct: String?) { - let compose = ComposeViewController(inReplyTo: statusID, mentioningAcct: mentioningAcct) + let compose = ComposeViewController(inReplyTo: statusID, mentioningAcct: mentioningAcct, mastodonController: apiController) let vc = UINavigationController(rootViewController: compose) vc.presentationController?.delegate = compose present(vc, animated: true) @@ -202,7 +204,7 @@ extension TuskerNavigationDelegate where Self: UIViewController { } private func moreOptions(forStatus statusID: String) -> UIActivityViewController { - guard let status = MastodonCache.status(for: statusID) else { fatalError("Missing cached status \(statusID)") } + guard let status = apiController.cache.status(for: statusID) else { fatalError("Missing cached status \(statusID)") } guard let url = status.url else { fatalError("Missing url for status \(statusID)") } var customActivites: [UIActivity] = [OpenInSafariActivity()] @@ -210,7 +212,7 @@ extension TuskerNavigationDelegate where Self: UIViewController { customActivites.insert(bookmarked ? UnbookmarkStatusActivity() : BookmarkStatusActivity(), at: 0) } - if status.account == MastodonController.account, + if status.account == apiController.account, let pinned = status.pinned { customActivites.insert(pinned ? UnpinStatusActivity() : PinStatusActivity(), at: 1) } @@ -221,7 +223,7 @@ extension TuskerNavigationDelegate where Self: UIViewController { } private func moreOptions(forAccount accountID: String) -> UIActivityViewController { - guard let account = MastodonCache.account(for: accountID) else { fatalError("Missing cached account \(accountID)") } + guard let account = apiController.cache.account(for: accountID) else { fatalError("Missing cached account \(accountID)") } return moreOptions(forURL: account.url) } @@ -244,13 +246,13 @@ extension TuskerNavigationDelegate where Self: UIViewController { } func showFollowedByList(accountIDs: [String]) { - let vc = AccountListTableViewController(accountIDs: accountIDs) + let vc = AccountListTableViewController(accountIDs: accountIDs, mastodonController: apiController) vc.title = NSLocalizedString("Followed By", comment: "followed by accounts list title") show(vc, sender: self) } func statusActionAccountList(action: StatusActionAccountListTableViewController.ActionType, statusID: String, statusState state: StatusState, accountIDs: [String]?) -> StatusActionAccountListTableViewController { - return StatusActionAccountListTableViewController(actionType: action, statusID: statusID, statusState: state, accountIDs: accountIDs) + return StatusActionAccountListTableViewController(actionType: action, statusID: statusID, statusState: state, accountIDs: accountIDs, mastodonController: apiController) } } diff --git a/Tusker/Views/Account Cell/AccountTableViewCell.swift b/Tusker/Views/Account Cell/AccountTableViewCell.swift index 9dd1bc02..cf01f6ef 100644 --- a/Tusker/Views/Account Cell/AccountTableViewCell.swift +++ b/Tusker/Views/Account Cell/AccountTableViewCell.swift @@ -10,7 +10,8 @@ import UIKit class AccountTableViewCell: UITableViewCell { - var delegate: TuskerNavigationDelegate? + weak var delegate: TuskerNavigationDelegate? + var mastodonController: MastodonController! { delegate?.apiController } @IBOutlet weak var avatarImageView: UIImageView! @IBOutlet weak var displayNameLabel: UILabel! @@ -31,7 +32,7 @@ class AccountTableViewCell: UITableViewCell { @objc func updateUIForPrefrences() { avatarImageView.layer.cornerRadius = Preferences.shared.avatarStyle.cornerRadius(for: avatarImageView) - guard let account = MastodonCache.account(for: accountID) else { + guard let account = mastodonController.cache.account(for: accountID) else { fatalError("Missing cached account \(accountID!)") } displayNameLabel.text = account.realDisplayName @@ -39,7 +40,7 @@ class AccountTableViewCell: UITableViewCell { func updateUI(accountID: String) { self.accountID = accountID - guard let account = MastodonCache.account(for: accountID) else { + guard let account = mastodonController.cache.account(for: accountID) else { fatalError("Missing cached account \(accountID)") } @@ -68,9 +69,10 @@ extension AccountTableViewCell: MenuPreviewProvider { var navigationDelegate: TuskerNavigationDelegate? { return delegate } func getPreviewProviders(for location: CGPoint, sourceViewController: UIViewController) -> PreviewProviders? { + guard let mastodonController = mastodonController else { return nil } return ( - content: { ProfileTableViewController(accountID: self.accountID) }, - actions: { self.actionsForProfile(accountID: self.accountID, sourceView: self.avatarImageView) } - ) + content: { ProfileTableViewController(accountID: self.accountID, mastodonController: mastodonController) }, + actions: { self.actionsForProfile(accountID: self.accountID, sourceView: self.avatarImageView) } + ) } } diff --git a/Tusker/Views/Attachments/AttachmentView.swift b/Tusker/Views/Attachments/AttachmentView.swift index e65b2d8d..41bfad86 100644 --- a/Tusker/Views/Attachments/AttachmentView.swift +++ b/Tusker/Views/Attachments/AttachmentView.swift @@ -11,13 +11,13 @@ import Pachyderm import Gifu import AVFoundation -protocol AttachmentViewDelegate { +protocol AttachmentViewDelegate: class { func showAttachmentsGallery(startingAt index: Int) } class AttachmentView: UIImageView, GIFAnimatable { - var delegate: AttachmentViewDelegate? + weak var delegate: AttachmentViewDelegate? var playImageView: UIImageView! @@ -71,8 +71,8 @@ class AttachmentView: UIImageView, GIFAnimatable { } func loadImage() { - ImageCache.attachments.get(attachment.url) { (data) in - guard let data = data else { return } + ImageCache.attachments.get(attachment.url) { [weak self] (data) in + guard let self = self, let data = data else { return } DispatchQueue.main.async { if self.attachment.url.pathExtension == "gif" { self.animate(withGIFData: data) diff --git a/Tusker/Views/Attachments/AttachmentsContainerView.swift b/Tusker/Views/Attachments/AttachmentsContainerView.swift index 015f069e..d8e18f9c 100644 --- a/Tusker/Views/Attachments/AttachmentsContainerView.swift +++ b/Tusker/Views/Attachments/AttachmentsContainerView.swift @@ -11,7 +11,7 @@ import Pachyderm class AttachmentsContainerView: UIView { - var delegate: AttachmentViewDelegate? + weak var delegate: AttachmentViewDelegate? var statusID: String! var attachments: [Attachment]! @@ -37,8 +37,6 @@ class AttachmentsContainerView: UIView { createBlurView() createHideButton() - - NotificationCenter.default.addObserver(self, selector: #selector(updateUIForPreferences), name: .preferencesChanged, object: nil) } func getAttachmentView(for attachment: Attachment) -> AttachmentView? { @@ -176,11 +174,7 @@ class AttachmentsContainerView: UIView { self.isHidden = true } - updateUIForPreferences() - } - - @objc func updateUIForPreferences() { - contentHidden = Preferences.shared.blurAllMedia || (MastodonCache.status(for: statusID)?.sensitive ?? false) + contentHidden = Preferences.shared.blurAllMedia || status.sensitive } private func createAttachmentView(index: Int) -> AttachmentView { diff --git a/Tusker/Views/Compose Media/ComposeMediaView.swift b/Tusker/Views/Compose Media/ComposeMediaView.swift index 54c1efd7..cd7ba31f 100644 --- a/Tusker/Views/Compose Media/ComposeMediaView.swift +++ b/Tusker/Views/Compose Media/ComposeMediaView.swift @@ -10,14 +10,14 @@ import UIKit import Photos import AVFoundation -protocol ComposeMediaViewDelegate { +protocol ComposeMediaViewDelegate: class { func didRemoveMedia(_ mediaView: ComposeMediaView) func descriptionTextViewDidChange(_ mediaView: ComposeMediaView) } class ComposeMediaView: UIView { - var delegate: ComposeMediaViewDelegate? + weak var delegate: ComposeMediaViewDelegate? @IBOutlet weak var imageView: UIImageView! @IBOutlet weak var descriptionTextView: UITextView! diff --git a/Tusker/Views/Compose Status Reply/ComposeStatusReplyView.swift b/Tusker/Views/Compose Status Reply/ComposeStatusReplyView.swift index 1242ae4b..12f24cd5 100644 --- a/Tusker/Views/Compose Status Reply/ComposeStatusReplyView.swift +++ b/Tusker/Views/Compose Status Reply/ComposeStatusReplyView.swift @@ -10,6 +10,8 @@ import UIKit import Pachyderm class ComposeStatusReplyView: UIView { + + weak var mastodonController: MastodonController? @IBOutlet weak var avatarImageView: UIImageView! @IBOutlet weak var displayNameLabel: UILabel! @@ -34,6 +36,7 @@ class ComposeStatusReplyView: UIView { func updateUI(for status: Status) { displayNameLabel.text = status.account.realDisplayName usernameLabel.text = "@\(status.account.acct)" + statusContentTextView.overrideMastodonController = mastodonController statusContentTextView.statusID = status.id ImageCache.avatars.get(status.account.avatar) { (data) in diff --git a/Tusker/Views/ContentTextView.swift b/Tusker/Views/ContentTextView.swift index 5d63f721..ccf245ea 100644 --- a/Tusker/Views/ContentTextView.swift +++ b/Tusker/Views/ContentTextView.swift @@ -15,9 +15,10 @@ private let emojiRegex = try! NSRegularExpression(pattern: ":(\\w+):", options: class ContentTextView: LinkTextView { - // todo: should be weak - var navigationDelegate: TuskerNavigationDelegate? - + weak var navigationDelegate: TuskerNavigationDelegate? + weak var overrideMastodonController: MastodonController? + var mastodonController: MastodonController? { overrideMastodonController ?? navigationDelegate?.apiController } + var defaultFont: UIFont = .systemFont(ofSize: 17) var defaultColor: UIColor = .label @@ -230,9 +231,9 @@ class ContentTextView: LinkTextView { let text = (self.text as NSString).substring(with: range) if let mention = getMention(for: url, text: text) { - return ProfileTableViewController(accountID: mention.id) + return ProfileTableViewController(accountID: mention.id, mastodonController: mastodonController!) } else if let tag = getHashtag(for: url, text: text) { - return HashtagTimelineViewController(for: tag) + return HashtagTimelineViewController(for: tag, mastodonController: mastodonController!) } else { return SFSafariViewController(url: url) } diff --git a/Tusker/Views/Hashtag Cell/HashtagTableViewCell.swift b/Tusker/Views/Hashtag Cell/HashtagTableViewCell.swift index 387c7652..3a9008cf 100644 --- a/Tusker/Views/Hashtag Cell/HashtagTableViewCell.swift +++ b/Tusker/Views/Hashtag Cell/HashtagTableViewCell.swift @@ -11,8 +11,8 @@ import Pachyderm class HashtagTableViewCell: UITableViewCell { - var delegate: TuskerNavigationDelegate? - + weak var delegate: TuskerNavigationDelegate? + @IBOutlet weak var hashtagLabel: UILabel! var hashtag: Hashtag! diff --git a/Tusker/Views/Notifications/ActionNotificationGroupTableViewCell.swift b/Tusker/Views/Notifications/ActionNotificationGroupTableViewCell.swift index 33483d06..d4ea85cb 100644 --- a/Tusker/Views/Notifications/ActionNotificationGroupTableViewCell.swift +++ b/Tusker/Views/Notifications/ActionNotificationGroupTableViewCell.swift @@ -12,7 +12,8 @@ import SwiftSoup class ActionNotificationGroupTableViewCell: UITableViewCell { - var delegate: TuskerNavigationDelegate? + weak var delegate: TuskerNavigationDelegate? + var mastodonController: MastodonController! { delegate?.apiController } @IBOutlet weak var actionImageView: UIImageView! @IBOutlet weak var actionAvatarStackView: UIStackView! @@ -26,6 +27,10 @@ class ActionNotificationGroupTableViewCell: UITableViewCell { var authorAvatarURL: URL? var updateTimestampWorkItem: DispatchWorkItem? + deinit { + updateTimestampWorkItem?.cancel() + } + override func awakeFromNib() { super.awakeFromNib() @@ -33,7 +38,7 @@ class ActionNotificationGroupTableViewCell: UITableViewCell { } @objc func updateUIForPreferences() { - let people = group.notificationIDs.compactMap(MastodonCache.notification(for:)).map { $0.account } + let people = group.notificationIDs.compactMap(mastodonController.cache.notification(for:)).map { $0.account } updateActionLabel(people: people) for case let imageView as UIImageView in actionAvatarStackView.arrangedSubviews { @@ -47,7 +52,7 @@ class ActionNotificationGroupTableViewCell: UITableViewCell { } self.group = group - guard let firstNotification = MastodonCache.notification(for: group.notificationIDs.first!) else { fatalError() } + guard let firstNotification = mastodonController.cache.notification(for: group.notificationIDs.first!) else { fatalError() } let status = firstNotification.status! self.statusID = status.id @@ -62,7 +67,7 @@ class ActionNotificationGroupTableViewCell: UITableViewCell { fatalError() } - let people = group.notificationIDs.compactMap(MastodonCache.notification(for:)).map { $0.account } + let people = group.notificationIDs.compactMap(mastodonController.cache.notification(for:)).map { $0.account } actionAvatarStackView.arrangedSubviews.forEach { $0.removeFromSuperview() } for account in people { @@ -93,7 +98,7 @@ class ActionNotificationGroupTableViewCell: UITableViewCell { func updateTimestamp() { guard let id = group.notificationIDs.first, - let notification = MastodonCache.notification(for: id) else { + let notification = mastodonController.cache.notification(for: id) else { fatalError("Missing cached notification") } @@ -109,7 +114,9 @@ class ActionNotificationGroupTableViewCell: UITableViewCell { delay = nil } if let delay = delay { - updateTimestampWorkItem = DispatchWorkItem(block: self.updateTimestamp) + updateTimestampWorkItem = DispatchWorkItem { [unowned self] in + self.updateTimestamp() + } DispatchQueue.main.asyncAfter(deadline: .now() + delay, execute: updateTimestampWorkItem!) } else { updateTimestampWorkItem = nil @@ -155,7 +162,7 @@ class ActionNotificationGroupTableViewCell: UITableViewCell { extension ActionNotificationGroupTableViewCell: SelectableTableViewCell { func didSelectCell() { guard let delegate = delegate else { return } - let notifications = group.notificationIDs.compactMap(MastodonCache.notification(for:)) + let notifications = group.notificationIDs.compactMap(mastodonController.cache.notification(for:)) let accountIDs = notifications.map { $0.account.id } let action: StatusActionAccountListTableViewController.ActionType switch notifications.first!.kind { @@ -176,7 +183,7 @@ extension ActionNotificationGroupTableViewCell: MenuPreviewProvider { func getPreviewProviders(for location: CGPoint, sourceViewController: UIViewController) -> PreviewProviders? { return (content: { - let notifications = self.group.notificationIDs.compactMap(MastodonCache.notification(for:)) + let notifications = self.group.notificationIDs.compactMap(self.mastodonController.cache.notification(for:)) let accountIDs = notifications.map { $0.account.id } let action: StatusActionAccountListTableViewController.ActionType switch notifications.first!.kind { diff --git a/Tusker/Views/Notifications/FollowNotificationGroupTableViewCell.swift b/Tusker/Views/Notifications/FollowNotificationGroupTableViewCell.swift index cafb5c13..90d982fd 100644 --- a/Tusker/Views/Notifications/FollowNotificationGroupTableViewCell.swift +++ b/Tusker/Views/Notifications/FollowNotificationGroupTableViewCell.swift @@ -11,7 +11,8 @@ import Pachyderm class FollowNotificationGroupTableViewCell: UITableViewCell { - var delegate: TuskerNavigationDelegate? + weak var delegate: TuskerNavigationDelegate? + var mastodonController: MastodonController! { delegate?.apiController } @IBOutlet weak var avatarStackView: UIStackView! @IBOutlet weak var timestampLabel: UILabel! @@ -21,6 +22,10 @@ class FollowNotificationGroupTableViewCell: UITableViewCell { var updateTimestampWorkItem: DispatchWorkItem? + deinit { + updateTimestampWorkItem?.cancel() + } + override func awakeFromNib() { super.awakeFromNib() @@ -28,7 +33,7 @@ class FollowNotificationGroupTableViewCell: UITableViewCell { } @objc func updateUIForPreferences() { - let people = group.notificationIDs.compactMap(MastodonCache.notification(for:)).map { $0.account } + let people = group.notificationIDs.compactMap(mastodonController.cache.notification(for:)).map { $0.account } updateActionLabel(people: people) for case let imageView as UIImageView in avatarStackView.arrangedSubviews { @@ -39,7 +44,7 @@ class FollowNotificationGroupTableViewCell: UITableViewCell { func updateUI(group: NotificationGroup) { self.group = group - let people = group.notificationIDs.compactMap(MastodonCache.notification(for:)).map { $0.account } + let people = group.notificationIDs.compactMap(mastodonController.cache.notification(for:)).map { $0.account } updateActionLabel(people: people) updateTimestamp() @@ -81,7 +86,7 @@ class FollowNotificationGroupTableViewCell: UITableViewCell { func updateTimestamp() { guard let id = group.notificationIDs.first, - let notification = MastodonCache.notification(for: id) else { + let notification = mastodonController.cache.notification(for: id) else { fatalError("Missing cached notification") } @@ -97,7 +102,7 @@ class FollowNotificationGroupTableViewCell: UITableViewCell { delay = nil } if let delay = delay { - updateTimestampWorkItem = DispatchWorkItem { + updateTimestampWorkItem = DispatchWorkItem { [unowned self] in self.updateTimestamp() } DispatchQueue.main.asyncAfter(deadline: .now() + delay, execute: updateTimestampWorkItem!) @@ -117,7 +122,7 @@ class FollowNotificationGroupTableViewCell: UITableViewCell { extension FollowNotificationGroupTableViewCell: SelectableTableViewCell { func didSelectCell() { - let people = group.notificationIDs.compactMap(MastodonCache.notification(for:)).map { $0.account.id } + let people = group.notificationIDs.compactMap(mastodonController.cache.notification(for:)).map { $0.account.id } switch people.count { case 0: return @@ -133,12 +138,13 @@ extension FollowNotificationGroupTableViewCell: MenuPreviewProvider { var navigationDelegate: TuskerNavigationDelegate? { return delegate } func getPreviewProviders(for location: CGPoint, sourceViewController: UIViewController) -> PreviewProviders? { + guard let mastodonController = mastodonController else { return nil } return (content: { - let accountIDs = self.group.notificationIDs.compactMap(MastodonCache.notification(for:)).map { $0.account.id } + let accountIDs = self.group.notificationIDs.compactMap(mastodonController.cache.notification(for:)).map { $0.account.id } if accountIDs.count == 1 { - return ProfileTableViewController(accountID: accountIDs.first!) + return ProfileTableViewController(accountID: accountIDs.first!, mastodonController: mastodonController) } else { - return AccountListTableViewController(accountIDs: accountIDs) + return AccountListTableViewController(accountIDs: accountIDs, mastodonController: mastodonController) } }, actions: { return [] diff --git a/Tusker/Views/Notifications/FollowRequestNotificationTableViewCell.swift b/Tusker/Views/Notifications/FollowRequestNotificationTableViewCell.swift index d2587f43..f31fe7a1 100644 --- a/Tusker/Views/Notifications/FollowRequestNotificationTableViewCell.swift +++ b/Tusker/Views/Notifications/FollowRequestNotificationTableViewCell.swift @@ -11,7 +11,8 @@ import Pachyderm class FollowRequestNotificationTableViewCell: UITableViewCell { - var delegate: TuskerNavigationDelegate? + weak var delegate: TuskerNavigationDelegate? + var mastodonController: MastodonController! { delegate?.apiController } @IBOutlet weak var stackView: UIStackView! @IBOutlet weak var avatarImageView: UIImageView! @@ -26,6 +27,10 @@ class FollowRequestNotificationTableViewCell: UITableViewCell { var updateTimestampWorkItem: DispatchWorkItem? + deinit { + updateTimestampWorkItem?.cancel() + } + override func awakeFromNib() { super.awakeFromNib() @@ -71,7 +76,9 @@ class FollowRequestNotificationTableViewCell: UITableViewCell { delay = nil } if let delay = delay { - updateTimestampWorkItem = DispatchWorkItem(block: self.updateTimestamp) + updateTimestampWorkItem = DispatchWorkItem { [unowned self] in + self.updateTimestamp() + } DispatchQueue.main.asyncAfter(deadline: .now() + delay, execute: updateTimestampWorkItem!) } else { updateTimestampWorkItem = nil @@ -89,9 +96,9 @@ class FollowRequestNotificationTableViewCell: UITableViewCell { @IBAction func rejectButtonPressed() { let request = Account.rejectFollowRequest(account) - MastodonController.client.run(request) { (response) in + mastodonController.run(request) { (response) in guard case let .success(relationship, _) = response else { fatalError() } - MastodonCache.add(relationship: relationship) + self.mastodonController.cache.add(relationship: relationship) DispatchQueue.main.async { UINotificationFeedbackGenerator().notificationOccurred(.success) self.actionButtonsStackView.isHidden = true @@ -106,9 +113,9 @@ class FollowRequestNotificationTableViewCell: UITableViewCell { @IBAction func acceptButtonPressed() { let request = Account.authorizeFollowRequest(account) - MastodonController.client.run(request) { (response) in + mastodonController.run(request) { (response) in guard case let .success(relationship, _) = response else { fatalError() } - MastodonCache.add(relationship: relationship) + self.mastodonController.cache.add(relationship: relationship) DispatchQueue.main.async { UINotificationFeedbackGenerator().notificationOccurred(.success) self.actionButtonsStackView.isHidden = true @@ -133,8 +140,9 @@ extension FollowRequestNotificationTableViewCell: MenuPreviewProvider { var navigationDelegate: TuskerNavigationDelegate? { return delegate } func getPreviewProviders(for location: CGPoint, sourceViewController: UIViewController) -> PreviewProviders? { + guard let mastodonController = mastodonController else { return nil } return (content: { - return ProfileTableViewController(accountID: self.account.id) + return ProfileTableViewController(accountID: self.account.id, mastodonController: mastodonController) }, actions: { return [] }) diff --git a/Tusker/Views/Profile Header/ProfileHeaderTableViewCell.swift b/Tusker/Views/Profile Header/ProfileHeaderTableViewCell.swift index d9c64ebc..bf740fa5 100644 --- a/Tusker/Views/Profile Header/ProfileHeaderTableViewCell.swift +++ b/Tusker/Views/Profile Header/ProfileHeaderTableViewCell.swift @@ -15,7 +15,8 @@ protocol ProfileHeaderTableViewCellDelegate: TuskerNavigationDelegate { class ProfileHeaderTableViewCell: UITableViewCell { - var delegate: ProfileHeaderTableViewCellDelegate? + weak var delegate: ProfileHeaderTableViewCellDelegate? + var mastodonController: MastodonController! { delegate?.apiController } @IBOutlet weak var headerImageView: UIImageView! @IBOutlet weak var avatarContainerView: UIView! @@ -55,7 +56,7 @@ class ProfileHeaderTableViewCell: UITableViewCell { guard accountID != self.accountID else { return } self.accountID = accountID - guard let account = MastodonCache.account(for: accountID) else { fatalError("Missing cached account \(accountID)") } + guard let account = mastodonController.cache.account(for: accountID) else { fatalError("Missing cached account \(accountID)") } updateUIForPreferences() @@ -82,12 +83,12 @@ class ProfileHeaderTableViewCell: UITableViewCell { noteTextView.setTextFromHtml(account.note) noteTextView.setEmojis(account.emojis) - if accountID != MastodonController.account.id { + if accountID != mastodonController.account.id { // don't show relationship label for the user's own account - if let relationship = MastodonCache.relationship(for: accountID) { + if let relationship = mastodonController.cache.relationship(for: accountID) { followsYouLabel.isHidden = !relationship.followedBy } else { - MastodonCache.relationship(for: accountID) { relationship in + mastodonController.cache.relationship(for: accountID) { relationship in DispatchQueue.main.async { self.followsYouLabel.isHidden = !(relationship?.followedBy ?? false) } @@ -122,7 +123,7 @@ class ProfileHeaderTableViewCell: UITableViewCell { } @objc func updateUIForPreferences() { - guard let account = MastodonCache.account(for: accountID) else { fatalError("Missing cached account \(accountID!)") } + guard let account = mastodonController.cache.account(for: accountID) else { fatalError("Missing cached account \(accountID!)") } avatarContainerView.layer.cornerRadius = Preferences.shared.avatarStyle.cornerRadius(for: avatarContainerView) avatarImageView.layer.cornerRadius = Preferences.shared.avatarStyle.cornerRadius(for: avatarImageView) diff --git a/Tusker/Views/Status/BaseStatusTableViewCell.swift b/Tusker/Views/Status/BaseStatusTableViewCell.swift index a9118c31..6d43c28e 100644 --- a/Tusker/Views/Status/BaseStatusTableViewCell.swift +++ b/Tusker/Views/Status/BaseStatusTableViewCell.swift @@ -15,11 +15,14 @@ protocol StatusTableViewCellDelegate: TuskerNavigationDelegate { } class BaseStatusTableViewCell: UITableViewCell { - var delegate: StatusTableViewCellDelegate? { + + weak var delegate: StatusTableViewCellDelegate? { didSet { contentTextView.navigationDelegate = delegate } } + var overrideMastodonController: MastodonController? + var mastodonController: MastodonController! { overrideMastodonController ?? delegate?.apiController } @IBOutlet weak var avatarImageView: UIImageView! @IBOutlet weak var displayNameLabel: UILabel! @@ -92,20 +95,28 @@ class BaseStatusTableViewCell: UITableViewCell { attachmentsView.isAccessibilityElement = true NotificationCenter.default.addObserver(self, selector: #selector(updateUIForPreferences), name: .preferencesChanged, object: nil) + } + + open func createObserversIfNecessary() { + if statusUpdater == nil { + statusUpdater = mastodonController.cache.statusSubject + .filter { [unowned self] in $0.id == self.statusID } + .receive(on: DispatchQueue.main) + .sink { [unowned self] in self.updateStatusState(status: $0) } + } - statusUpdater = MastodonCache.statusSubject - .filter { $0.id == self.statusID } - .receive(on: DispatchQueue.main) - .sink(receiveValue: updateStatusState(status:)) - - accountUpdater = MastodonCache.accountSubject - .filter { $0.id == self.accountID } - .receive(on: DispatchQueue.main) - .sink(receiveValue: updateUI(account:)) + if accountUpdater == nil { + accountUpdater = mastodonController.cache.accountSubject + .filter { [unowned self] in $0.id == self.accountID } + .receive(on: DispatchQueue.main) + .sink { [unowned self] in self.updateUI(account: $0) } + } } func updateUI(statusID: String, state: StatusState) { - guard let status = MastodonCache.status(for: statusID) else { + createObserversIfNecessary() + + guard let status = mastodonController.cache.status(for: statusID) else { fatalError("Missing cached status") } self.statusID = statusID @@ -180,9 +191,10 @@ class BaseStatusTableViewCell: UITableViewCell { } @objc func updateUIForPreferences() { - guard let account = MastodonCache.account(for: accountID) else { return } + guard let account = mastodonController.cache.account(for: accountID) else { return } avatarImageView.layer.cornerRadius = Preferences.shared.avatarStyle.cornerRadius(for: avatarImageView) displayNameLabel.text = account.realDisplayName + attachmentsView.contentHidden = Preferences.shared.blurAllMedia || (mastodonController.cache.status(for: statusID)?.sensitive ?? false) } override func prepareForReuse() { @@ -240,18 +252,18 @@ class BaseStatusTableViewCell: UITableViewCell { } @IBAction func favoritePressed() { - guard let status = MastodonCache.status(for: statusID) else { fatalError("Missing cached status \(statusID!)") } + guard let status = mastodonController.cache.status(for: statusID) else { fatalError("Missing cached status \(statusID!)") } let oldValue = favorited favorited = !favorited let realStatus: Status = status.reblog ?? status let request = (favorited ? Status.favourite : Status.unfavourite)(realStatus) - MastodonController.client.run(request) { response in + mastodonController.run(request) { response in DispatchQueue.main.async { if case let .success(newStatus, _) = response { self.favorited = newStatus.favourited ?? false - MastodonCache.add(status: newStatus) + self.mastodonController.cache.add(status: newStatus) UIImpactFeedbackGenerator(style: .light).impactOccurred() } else { self.favorited = oldValue @@ -265,18 +277,18 @@ class BaseStatusTableViewCell: UITableViewCell { } @IBAction func reblogPressed() { - guard let status = MastodonCache.status(for: statusID) else { fatalError("Missing cached status \(statusID!)") } + guard let status = mastodonController.cache.status(for: statusID) else { fatalError("Missing cached status \(statusID!)") } let oldValue = reblogged reblogged = !reblogged let realStatus: Status = status.reblog ?? status let request = (reblogged ? Status.reblog : Status.unreblog)(realStatus) - MastodonController.client.run(request) { response in + mastodonController.run(request) { response in DispatchQueue.main.async { if case let .success(newStatus, _) = response { self.reblogged = newStatus.reblogged ?? false - MastodonCache.add(status: newStatus) + self.mastodonController.cache.add(status: newStatus) UIImpactFeedbackGenerator(style: .light).impactOccurred() } else { self.reblogged = oldValue @@ -303,7 +315,7 @@ class BaseStatusTableViewCell: UITableViewCell { extension BaseStatusTableViewCell: AttachmentViewDelegate { func showAttachmentsGallery(startingAt index: Int) { - guard let status = MastodonCache.status(for: statusID) else { fatalError("Missing cached status \(statusID!)") } + guard let status = mastodonController.cache.status(for: statusID) else { fatalError("Missing cached status \(statusID!)") } let sourceViews = status.attachments.map(attachmentsView.getAttachmentView(for:)) delegate?.showGallery(attachments: status.attachments, sourceViews: sourceViews, startIndex: index) } @@ -313,9 +325,10 @@ extension BaseStatusTableViewCell: MenuPreviewProvider { var navigationDelegate: TuskerNavigationDelegate? { return delegate } func getPreviewProviders(for location: CGPoint, sourceViewController: UIViewController) -> PreviewProviders? { + guard let mastodonController = mastodonController else { return nil } if avatarImageView.frame.contains(location) { return ( - content: { ProfileTableViewController(accountID: self.accountID)}, + content: { ProfileTableViewController(accountID: self.accountID, mastodonController: mastodonController) }, actions: { self.actionsForProfile(accountID: self.accountID, sourceView: self.avatarImageView) } ) } else if attachmentsView.frame.contains(location) { diff --git a/Tusker/Views/Status/ConversationMainStatusTableViewCell.swift b/Tusker/Views/Status/ConversationMainStatusTableViewCell.swift index 6432741f..0621a423 100644 --- a/Tusker/Views/Status/ConversationMainStatusTableViewCell.swift +++ b/Tusker/Views/Status/ConversationMainStatusTableViewCell.swift @@ -40,7 +40,7 @@ class ConversationMainStatusTableViewCell: BaseStatusTableViewCell { override func updateUI(statusID: String, state: StatusState) { super.updateUI(statusID: statusID, state: state) - guard let status = MastodonCache.status(for: statusID) else { fatalError() } + guard let status = mastodonController.cache.status(for: statusID) else { fatalError() } var timestampAndClientText = ConversationMainStatusTableViewCell.dateFormatter.string(from: status.createdAt) if let application = status.application { diff --git a/Tusker/Views/Status/TimelineStatusTableViewCell.swift b/Tusker/Views/Status/TimelineStatusTableViewCell.swift index 5393529d..44a9680b 100644 --- a/Tusker/Views/Status/TimelineStatusTableViewCell.swift +++ b/Tusker/Views/Status/TimelineStatusTableViewCell.swift @@ -34,6 +34,7 @@ class TimelineStatusTableViewCell: BaseStatusTableViewCell { deinit { rebloggerAccountUpdater?.cancel() + updateTimestampWorkItem?.cancel() } override func awakeFromNib() { @@ -41,19 +42,25 @@ class TimelineStatusTableViewCell: BaseStatusTableViewCell { reblogLabel.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(reblogLabelPressed))) accessibilityElements!.insert(reblogLabel!, at: 0) - - rebloggerAccountUpdater = MastodonCache.accountSubject - .filter { $0.id == self.rebloggerID } - .receive(on: DispatchQueue.main) - .sink(receiveValue: updateRebloggerLabel(reblogger:)) } - + + override func createObserversIfNecessary() { + super.createObserversIfNecessary() + + if rebloggerAccountUpdater == nil { + rebloggerAccountUpdater = mastodonController.cache.accountSubject + .filter { [unowned self] in $0.id == self.rebloggerID } + .receive(on: DispatchQueue.main) + .sink { [unowned self] in self.updateRebloggerLabel(reblogger: $0) } + } + } + override func updateUI(statusID: String, state: StatusState) { - guard var status = MastodonCache.status(for: statusID) else { fatalError("Missing cached status \(statusID)") } + guard var status = mastodonController.cache.status(for: statusID) else { fatalError("Missing cached status \(statusID)") } let realStatusID: String if let rebloggedStatusID = status.reblog?.id, - let rebloggedStatus = MastodonCache.status(for: rebloggedStatusID) { + let rebloggedStatus = mastodonController.cache.status(for: rebloggedStatusID) { reblogStatusID = statusID rebloggerID = status.account.id status = rebloggedStatus @@ -78,7 +85,7 @@ class TimelineStatusTableViewCell: BaseStatusTableViewCell { @objc override func updateUIForPreferences() { super.updateUIForPreferences() if let rebloggerID = rebloggerID, - let reblogger = MastodonCache.account(for: rebloggerID) { + let reblogger = mastodonController.cache.account(for: rebloggerID) { updateRebloggerLabel(reblogger: reblogger) } } @@ -88,7 +95,7 @@ class TimelineStatusTableViewCell: BaseStatusTableViewCell { } func updateTimestamp() { - guard let status = MastodonCache.status(for: statusID) else { fatalError("Missing cached status \(statusID!)") } + guard let status = mastodonController.cache.status(for: statusID) else { fatalError("Missing cached status \(statusID!)") } timestampLabel.text = status.createdAt.timeAgoString() timestampLabel.accessibilityLabel = TimelineStatusTableViewCell.relativeDateFormatter.localizedString(for: status.createdAt, relativeTo: Date()) @@ -103,7 +110,7 @@ class TimelineStatusTableViewCell: BaseStatusTableViewCell { delay = nil } if let delay = delay { - updateTimestampWorkItem = DispatchWorkItem { + updateTimestampWorkItem = DispatchWorkItem { [unowned self] in self.updateTimestamp() } DispatchQueue.main.asyncAfter(deadline: .now() + delay, execute: updateTimestampWorkItem!) @@ -115,7 +122,7 @@ class TimelineStatusTableViewCell: BaseStatusTableViewCell { func reply() { if Preferences.shared.mentionReblogger, let rebloggerID = rebloggerID, - let rebloggerAccount = MastodonCache.account(for: rebloggerID) { + let rebloggerAccount = mastodonController.cache.account(for: rebloggerID) { delegate?.reply(to: statusID, mentioningAcct: rebloggerAccount.acct) } else { delegate?.reply(to: statusID) @@ -139,8 +146,9 @@ class TimelineStatusTableViewCell: BaseStatusTableViewCell { } override func getStatusCellPreviewProviders(for location: CGPoint, sourceViewController: UIViewController) -> BaseStatusTableViewCell.PreviewProviders? { + guard let mastodonController = mastodonController else { return nil } return ( - content: { ConversationTableViewController(for: self.statusID, state: self.statusState.copy()) }, + content: { ConversationTableViewController(for: self.statusID, state: self.statusState.copy(), mastodonController: mastodonController) }, actions: { self.actionsForStatus(statusID: self.statusID, sourceView: self) } ) } @@ -156,7 +164,8 @@ extension TimelineStatusTableViewCell: SelectableTableViewCell { extension TimelineStatusTableViewCell: TableViewSwipeActionProvider { func leadingSwipeActionsConfiguration() -> UISwipeActionsConfiguration? { - guard let status = MastodonCache.status(for: statusID) else { fatalError("Missing cached status \(statusID!)") } + guard let mastodonController = mastodonController else { return nil } + guard let status = mastodonController.cache.status(for: statusID) else { fatalError("Missing cached status \(statusID!)") } let favoriteTitle: String let favoriteRequest: Request @@ -172,14 +181,14 @@ extension TimelineStatusTableViewCell: TableViewSwipeActionProvider { favoriteColor = UIColor(displayP3Red: 1, green: 204/255, blue: 0, alpha: 1) } let favorite = UIContextualAction(style: .normal, title: favoriteTitle) { (action, view, completion) in - MastodonController.client.run(favoriteRequest, completion: { response in + mastodonController.run(favoriteRequest, completion: { response in DispatchQueue.main.async { guard case let .success(status, _) = response else { completion(false) return } completion(true) - MastodonCache.add(status: status) + mastodonController.cache.add(status: status) } }) } @@ -199,14 +208,14 @@ extension TimelineStatusTableViewCell: TableViewSwipeActionProvider { reblogColor = tintColor } let reblog = UIContextualAction(style: .normal, title: reblogTitle) { (action, view, completion) in - MastodonController.client.run(reblogRequest, completion: { response in + mastodonController.run(reblogRequest, completion: { response in DispatchQueue.main.async { guard case let .success(status, _) = response else { completion(false) return } completion(true) - MastodonCache.add(status: status) + mastodonController.cache.add(status: status) } }) } diff --git a/Tusker/Views/StatusContentTextView.swift b/Tusker/Views/StatusContentTextView.swift index 242014fa..c10d838b 100644 --- a/Tusker/Views/StatusContentTextView.swift +++ b/Tusker/Views/StatusContentTextView.swift @@ -14,7 +14,8 @@ class StatusContentTextView: ContentTextView { var statusID: String? { didSet { guard let statusID = statusID else { return } - guard let status = MastodonCache.status(for: statusID) else { + guard let mastodonController = mastodonController, + let status = mastodonController.cache.status(for: statusID) else { fatalError("Can't set StatusContentTextView text without cached status for \(statusID)") } setTextFromHtml(status.content) @@ -25,7 +26,8 @@ class StatusContentTextView: ContentTextView { override func getMention(for url: URL, text: String) -> Mention? { let mention: Mention? if let statusID = statusID, - let status = MastodonCache.status(for: statusID) { + let mastodonController = mastodonController, + let status = mastodonController.cache.status(for: statusID) { mention = status.mentions.first { (mention) in // Mastodon and Pleroma include the @ in the text, GNU Social does not (text.dropFirst() == mention.username || text == mention.username) && url.host == mention.url.host @@ -39,7 +41,8 @@ class StatusContentTextView: ContentTextView { override func getHashtag(for url: URL, text: String) -> Hashtag? { let hashtag: Hashtag? if let statusID = statusID, - let status = MastodonCache.status(for: statusID) { + let mastodonController = mastodonController, + let status = mastodonController.cache.status(for: statusID) { hashtag = status.hashtags.first { (hashtag) in hashtag.url == url } diff --git a/Tusker/XCallbackURL/XCBActions.swift b/Tusker/XCallbackURL/XCBActions.swift index 15b42eab..0731fd06 100644 --- a/Tusker/XCallbackURL/XCBActions.swift +++ b/Tusker/XCallbackURL/XCBActions.swift @@ -13,8 +13,15 @@ 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 { - return (UIApplication.shared.delegate as! AppDelegate).window!.rootViewController as! 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) { @@ -31,7 +38,7 @@ struct XCBActions { private static func getStatus(from request: XCBRequest, session: XCBSession, completion: @escaping (Status) -> Void) { if let id = request.arguments["statusID"] { - MastodonCache.status(for: id) { (status) in + mastodonController.cache.status(for: id) { (status) in if let status = status { completion(status) } else { @@ -41,11 +48,11 @@ struct XCBActions { } } } else if let searchQuery = request.arguments["statusURL"] { - let request = MastodonController.client.search(query: searchQuery) - MastodonController.client.run(request) { (response) in + let request = Client.search(query: searchQuery) + mastodonController.run(request) { (response) in if case let .success(results, _) = response, let status = results.statuses.first { - MastodonCache.add(status: status) + mastodonController.cache.add(status: status) completion(status) } else { session.complete(with: .error, additionalData: [ @@ -62,7 +69,7 @@ struct XCBActions { private static func getAccount(from request: XCBRequest, session: XCBSession, completion: @escaping (Account) -> Void) { if let id = request.arguments["accountID"] { - MastodonCache.account(for: id) { (account) in + mastodonController.cache.account(for: id) { (account) in if let account = account { completion(account) } else { @@ -72,11 +79,11 @@ struct XCBActions { } } } else if let searchQuery = request.arguments["accountURL"] { - let request = MastodonController.client.search(query: searchQuery) - MastodonController.client.run(request) { (response) in + let request = Client.search(query: searchQuery) + mastodonController.run(request) { (response) in if case let .success(results, _) = response { if let account = results.accounts.first { - MastodonCache.add(account: account) + mastodonController.cache.add(account: account) completion(account) } else { session.complete(with: .error, additionalData: [ @@ -90,11 +97,11 @@ struct XCBActions { } } } else if let acct = request.arguments["acct"] { - let request = MastodonController.client.searchForAccount(query: acct) - MastodonController.client.run(request) { (response) in + let request = Client.searchForAccount(query: acct) + mastodonController.run(request) { (response) in if case let .success(accounts, _) = response { if let account = accounts.first { - MastodonCache.add(account: account) + mastodonController.cache.add(account: account) completion(account) } else { session.complete(with: .error, additionalData: [ @@ -118,7 +125,7 @@ struct XCBActions { 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) + let vc = ConversationTableViewController(for: status.id, mastodonController: mastodonController) show(vc) } } @@ -132,14 +139,14 @@ struct XCBActions { var status = "" if let mentioning = mentioning { status += mentioning } if let text = text { status += text } - guard CharacterCounter.count(text: status) <= MastodonController.instance.maxStatusCharacters ?? 500 else { + 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)" + "error": "Too many characters. Instance maximum is \(mastodonController.instance.maxStatusCharacters ?? 500)" ]) return } - let request = MastodonController.client.createStatus(text: status, visibility: Preferences.shared.defaultPostVisibility) - MastodonController.client.run(request) { response in + 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, @@ -152,7 +159,7 @@ struct XCBActions { } } } else { - let compose = ComposeViewController(mentioningAcct: mentioning, text: text) + let compose = ComposeViewController(mentioningAcct: mentioning, text: text, mastodonController: mastodonController) compose.xcbSession = session let vc = UINavigationController(rootViewController: compose) present(vc) @@ -199,9 +206,9 @@ struct XCBActions { static func statusAction(request: @escaping (Status) -> Request, alertTitle: String, _ url: XCBRequest, _ session: XCBSession, _ silent: Bool?) { func performAction(status: Status, completion: ((Status) -> Void)?) { - MastodonController.client.run(request(status)) { (response) in + mastodonController.run(request(status)) { (response) in if case let .success(status, _) = response { - MastodonCache.add(status: status) + mastodonController.cache.add(status: status) completion?(status) session.complete(with: .success, additionalData: [ "statusURL": status.url?.absoluteString, @@ -219,7 +226,7 @@ struct XCBActions { if silent ?? false { performAction(status: status, completion: nil) } else { - let vc = ConversationTableViewController(for: status.id) + let vc = ConversationTableViewController(for: status.id, mastodonController: mastodonController) DispatchQueue.main.async { show(vc) } @@ -247,7 +254,7 @@ struct XCBActions { static func showAccount(_ request: XCBRequest, _ session: XCBSession, _ silent: Bool?) { getAccount(from: request, session: session) { (account) in DispatchQueue.main.async { - let vc = ProfileTableViewController(accountID: account.id) + let vc = ProfileTableViewController(accountID: account.id, mastodonController: mastodonController) show(vc) } } @@ -269,7 +276,7 @@ struct XCBActions { } static func getCurrentUser(_ request: XCBRequest, _ session: XCBSession, _ silent: Bool?) { - let account = MastodonController.account! + let account = mastodonController.account! session.complete(with: .success, additionalData: [ "username": account.acct, "displayName": account.displayName, @@ -285,9 +292,9 @@ struct XCBActions { static func followUser(_ request: XCBRequest, _ session: XCBSession, _ silent: Bool?) { func performAction(_ account: Account) { let request = Account.follow(account.id) - MastodonController.client.run(request) { (response) in + mastodonController.run(request) { (response) in if case let .success(relationship, _) = response { - MastodonCache.add(relationship: relationship) + mastodonController.cache.add(relationship: relationship) session.complete(with: .success, additionalData: [ "url": account.url.absoluteString ]) @@ -303,7 +310,7 @@ struct XCBActions { if silent ?? false { performAction(account) } else { - let vc = ProfileTableViewController(accountID: account.id) + let vc = ProfileTableViewController(accountID: account.id, mastodonController: mastodonController) DispatchQueue.main.async { show(vc) }