From a18bcac8b87eeebf470f7d932f606ea57d5f2cf8 Mon Sep 17 00:00:00 2001 From: Shadowfacts Date: Sun, 5 Jan 2020 14:00:39 -0500 Subject: [PATCH 01/21] Pachyderm: Change Client request methods to be static, like all other models Tusker: Add run method to MastodonController and no-longer expose API client object --- Pachyderm/Client.swift | 76 +++++++++---------- .../FollowAccountActivity.swift | 2 +- .../UnfollowAccountActivity.swift | 2 +- .../BookmarkStatusActivity.swift | 2 +- .../Status Activities/PinStatusActivity.swift | 2 +- .../UnbookmarkStatusActivity.swift | 2 +- .../UnpinStatusActivity.swift | 2 +- Tusker/Controllers/MastodonController.swift | 18 +++-- Tusker/MastodonCache.swift | 12 +-- .../BookmarksTableViewController.swift | 12 +-- .../Compose/ComposeViewController.swift | 8 +- .../ConversationTableViewController.swift | 2 +- .../Explore/ExploreViewController.swift | 10 +-- .../EditListAccountsViewController.swift | 8 +- .../NotificationsTableViewController.swift | 14 ++-- .../InstanceSelectorTableViewController.swift | 2 +- .../Profile/ProfileTableViewController.swift | 2 +- .../Search/SearchResultsViewController.swift | 4 +- ...ActionAccountListTableViewController.swift | 2 +- .../TimelineTableViewController.swift | 14 ++-- ...llowRequestNotificationTableViewCell.swift | 4 +- .../Status/BaseStatusTableViewCell.swift | 4 +- .../Status/TimelineStatusTableViewCell.swift | 4 +- Tusker/XCallbackURL/XCBActions.swift | 20 ++--- 24 files changed, 118 insertions(+), 110 deletions(-) diff --git a/Pachyderm/Client.swift b/Pachyderm/Client.swift index 89c9a573de..e0de6b613a 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/Activities/Account Activities/FollowAccountActivity.swift b/Tusker/Activities/Account Activities/FollowAccountActivity.swift index 6747ec0f9c..f0dc74f20b 100644 --- a/Tusker/Activities/Account Activities/FollowAccountActivity.swift +++ b/Tusker/Activities/Account Activities/FollowAccountActivity.swift @@ -28,7 +28,7 @@ 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) } else { diff --git a/Tusker/Activities/Account Activities/UnfollowAccountActivity.swift b/Tusker/Activities/Account Activities/UnfollowAccountActivity.swift index 303042a522..94daf75653 100644 --- a/Tusker/Activities/Account Activities/UnfollowAccountActivity.swift +++ b/Tusker/Activities/Account Activities/UnfollowAccountActivity.swift @@ -28,7 +28,7 @@ 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) } else { diff --git a/Tusker/Activities/Status Activities/BookmarkStatusActivity.swift b/Tusker/Activities/Status Activities/BookmarkStatusActivity.swift index b553af0620..df1fcfc9cf 100644 --- a/Tusker/Activities/Status Activities/BookmarkStatusActivity.swift +++ b/Tusker/Activities/Status Activities/BookmarkStatusActivity.swift @@ -27,7 +27,7 @@ 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) } else { diff --git a/Tusker/Activities/Status Activities/PinStatusActivity.swift b/Tusker/Activities/Status Activities/PinStatusActivity.swift index 66c56c104f..f73d04749f 100644 --- a/Tusker/Activities/Status Activities/PinStatusActivity.swift +++ b/Tusker/Activities/Status Activities/PinStatusActivity.swift @@ -26,7 +26,7 @@ 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) } else { diff --git a/Tusker/Activities/Status Activities/UnbookmarkStatusActivity.swift b/Tusker/Activities/Status Activities/UnbookmarkStatusActivity.swift index 0857348dbb..e88a3549be 100644 --- a/Tusker/Activities/Status Activities/UnbookmarkStatusActivity.swift +++ b/Tusker/Activities/Status Activities/UnbookmarkStatusActivity.swift @@ -27,7 +27,7 @@ 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) } else { diff --git a/Tusker/Activities/Status Activities/UnpinStatusActivity.swift b/Tusker/Activities/Status Activities/UnpinStatusActivity.swift index 2fc2fa8577..134e852d21 100644 --- a/Tusker/Activities/Status Activities/UnpinStatusActivity.swift +++ b/Tusker/Activities/Status Activities/UnpinStatusActivity.swift @@ -26,7 +26,7 @@ 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) } else { diff --git a/Tusker/Controllers/MastodonController.swift b/Tusker/Controllers/MastodonController.swift index bf376e9bbd..6c841c494e 100644 --- a/Tusker/Controllers/MastodonController.swift +++ b/Tusker/Controllers/MastodonController.swift @@ -11,11 +11,15 @@ import Pachyderm class MastodonController { - static var client: Client! + private static var client: Client! static var account: Account! static var instance: Instance! + static var accessToken: String? { + client?.accessToken + } + private init() {} static func createClient() { @@ -28,6 +32,10 @@ class MastodonController { client.accessToken = LocalData.shared.accessToken } + static func run(_ request: Request, completion: @escaping Client.Callback) { + client.run(request, completion: completion) + } + static func registerApp(completion: @escaping () -> Void) { guard LocalData.shared.clientID == nil, LocalData.shared.clientSecret == nil else { @@ -55,8 +63,8 @@ class MastodonController { 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) @@ -66,8 +74,8 @@ class MastodonController { } static func getOwnInstance() { - let request = client.getInstance() - client.run(request) { (response) in + let request = Client.getInstance() + run(request) { (response) in guard case let .success(instance, _) = response else { fatalError() } self.instance = instance } diff --git a/Tusker/MastodonCache.swift b/Tusker/MastodonCache.swift index 5d888f284d..70b5482d57 100644 --- a/Tusker/MastodonCache.swift +++ b/Tusker/MastodonCache.swift @@ -37,8 +37,8 @@ class MastodonCache { } static func status(for id: String, completion: @escaping (Status?) -> Void) { - let request = MastodonController.client.getStatus(id: id) - MastodonController.client.run(request) { response in + let request = Client.getStatus(id: id) + MastodonController.run(request) { response in guard case let .success(status, _) = response else { completion(nil) return @@ -67,8 +67,8 @@ class MastodonCache { } static func account(for id: String, completion: @escaping (Account?) -> Void) { - let request = MastodonController.client.getAccount(id: id) - MastodonController.client.run(request) { response in + let request = Client.getAccount(id: id) + MastodonController.run(request) { response in guard case let .success(account, _) = response else { completion(nil) return @@ -96,8 +96,8 @@ class MastodonCache { } static func relationship(for id: String, completion: @escaping (Relationship?) -> Void) { - let request = MastodonController.client.getRelationships(accounts: [id]) - MastodonController.client.run(request) { response in + let request = Client.getRelationships(accounts: [id]) + MastodonController.run(request) { response in guard case let .success(relationships, _) = response, let relationship = relationships.first else { completion(nil) diff --git a/Tusker/Screens/Bookmarks/BookmarksTableViewController.swift b/Tusker/Screens/Bookmarks/BookmarksTableViewController.swift index ec478fb929..142213af17 100644 --- a/Tusker/Screens/Bookmarks/BookmarksTableViewController.swift +++ b/Tusker/Screens/Bookmarks/BookmarksTableViewController.swift @@ -44,8 +44,8 @@ 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.statuses.append(contentsOf: statuses.map { ($0.id, .unknown) }) @@ -81,8 +81,8 @@ 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) @@ -107,7 +107,7 @@ class BookmarksTableViewController: EnhancedTableViewController { 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 + MastodonController.run(request) { (response) in guard case let .success(newStatus, _) = response else { fatalError() } MastodonCache.add(status: newStatus) self.statuses.remove(at: indexPath.row) @@ -131,7 +131,7 @@ class BookmarksTableViewController: EnhancedTableViewController { 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 + MastodonController.run(request) { (response) in guard case let .success(newStatus, _) = response else { fatalError() } MastodonCache.add(status: newStatus) self.statuses.remove(at: indexPath.row) diff --git a/Tusker/Screens/Compose/ComposeViewController.swift b/Tusker/Screens/Compose/ComposeViewController.swift index 4caace129e..6dbabc22d5 100644 --- a/Tusker/Screens/Compose/ComposeViewController.swift +++ b/Tusker/Screens/Compose/ComposeViewController.swift @@ -480,8 +480,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) + MastodonController.run(request) { (response) in guard case let .success(attachment, _) = response else { fatalError() } attachments[index] = attachment @@ -499,7 +499,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, @@ -507,7 +507,7 @@ class ComposeViewController: UIViewController { spoilerText: contentWarning, visibility: visibility, language: nil) - MastodonController.client.run(request) { (response) in + MastodonController.run(request) { (response) in guard case let .success(status, _) = response else { fatalError() } self.postedStatus = status MastodonCache.add(status: status) diff --git a/Tusker/Screens/Conversation/ConversationTableViewController.swift b/Tusker/Screens/Conversation/ConversationTableViewController.swift index 1807dcf2b8..92410f587f 100644 --- a/Tusker/Screens/Conversation/ConversationTableViewController.swift +++ b/Tusker/Screens/Conversation/ConversationTableViewController.swift @@ -58,7 +58,7 @@ class ConversationTableViewController: EnhancedTableViewController { guard let mainStatus = MastodonCache.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) diff --git a/Tusker/Screens/Explore/ExploreViewController.swift b/Tusker/Screens/Explore/ExploreViewController.swift index 02d487e7ce..a7f6fc120c 100644 --- a/Tusker/Screens/Explore/ExploreViewController.swift +++ b/Tusker/Screens/Explore/ExploreViewController.swift @@ -106,8 +106,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() } @@ -143,7 +143,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 + MastodonController.run(request) { (response) in guard case .success(_, _) = response else { fatalError() } @@ -189,8 +189,8 @@ class ExploreViewController: EnhancedTableViewController { fatalError() } - let request = MastodonController.client.createList(title: title) - MastodonController.client.run(request) { (response) in + let request = Client.createList(title: title) + MastodonController.run(request) { (response) in guard case let .success(list, _) = response else { fatalError() } self.reloadLists() diff --git a/Tusker/Screens/Lists/EditListAccountsViewController.swift b/Tusker/Screens/Lists/EditListAccountsViewController.swift index ff7e2c1d52..7484d1dd06 100644 --- a/Tusker/Screens/Lists/EditListAccountsViewController.swift +++ b/Tusker/Screens/Lists/EditListAccountsViewController.swift @@ -70,7 +70,7 @@ 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() } @@ -109,7 +109,7 @@ class EditListAccountsViewController: EnhancedTableViewController { fatalError() } let request = List.update(self.list, title: text) - MastodonController.client.run(request) { (response) in + MastodonController.run(request) { (response) in guard case .success(_, _) = response else { fatalError() } @@ -143,7 +143,7 @@ extension EditListAccountsViewController { } let request = List.remove(editListAccountsController!.list, accounts: [id]) - MastodonController.client.run(request) { (response) in + MastodonController.run(request) { (response) in guard case .success(_, _) = response else { fatalError() } @@ -157,7 +157,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/Notifications/NotificationsTableViewController.swift b/Tusker/Screens/Notifications/NotificationsTableViewController.swift index c68ac440c1..ae6ccf3541 100644 --- a/Tusker/Screens/Notifications/NotificationsTableViewController.swift +++ b/Tusker/Screens/Notifications/NotificationsTableViewController.swift @@ -56,8 +56,8 @@ 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) @@ -124,8 +124,8 @@ 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) @@ -182,7 +182,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,8 +196,8 @@ 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) diff --git a/Tusker/Screens/Onboarding/InstanceSelectorTableViewController.swift b/Tusker/Screens/Onboarding/InstanceSelectorTableViewController.swift index 9d48ca6f12..8cad403cbf 100644 --- a/Tusker/Screens/Onboarding/InstanceSelectorTableViewController.swift +++ b/Tusker/Screens/Onboarding/InstanceSelectorTableViewController.swift @@ -107,7 +107,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/Profile/ProfileTableViewController.swift b/Tusker/Screens/Profile/ProfileTableViewController.swift index ecf12bfd71..45b520e035 100644 --- a/Tusker/Screens/Profile/ProfileTableViewController.swift +++ b/Tusker/Screens/Profile/ProfileTableViewController.swift @@ -130,7 +130,7 @@ class ProfileTableViewController: EnhancedTableViewController { 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() { diff --git a/Tusker/Screens/Search/SearchResultsViewController.swift b/Tusker/Screens/Search/SearchResultsViewController.swift index e1388f426c..ad77eed37e 100644 --- a/Tusker/Screens/Search/SearchResultsViewController.swift +++ b/Tusker/Screens/Search/SearchResultsViewController.swift @@ -117,8 +117,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 { diff --git a/Tusker/Screens/Status Action Account List/StatusActionAccountListTableViewController.swift b/Tusker/Screens/Status Action Account List/StatusActionAccountListTableViewController.swift index c5ccc6d21c..9ccf923460 100644 --- a/Tusker/Screens/Status Action Account List/StatusActionAccountListTableViewController.swift +++ b/Tusker/Screens/Status Action Account List/StatusActionAccountListTableViewController.swift @@ -75,7 +75,7 @@ class StatusActionAccountListTableViewController: EnhancedTableViewController { 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) DispatchQueue.main.async { diff --git a/Tusker/Screens/Timeline/TimelineTableViewController.swift b/Tusker/Screens/Timeline/TimelineTableViewController.swift index c5096da3f2..f38cacd2af 100644 --- a/Tusker/Screens/Timeline/TimelineTableViewController.swift +++ b/Tusker/Screens/Timeline/TimelineTableViewController.swift @@ -56,13 +56,13 @@ class TimelineTableViewController: EnhancedTableViewController { tableView.prefetchDataSource = self - guard MastodonController.client?.accessToken != nil else { return } + guard MastodonController.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.timelineSegments.insert(statuses.map { ($0.id, .unknown) }, at: 0) @@ -99,8 +99,8 @@ 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) @@ -124,8 +124,8 @@ 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() } self.newer = pagination?.newer MastodonCache.addAll(statuses: newStatuses) diff --git a/Tusker/Views/Notifications/FollowRequestNotificationTableViewCell.swift b/Tusker/Views/Notifications/FollowRequestNotificationTableViewCell.swift index d2587f43d2..8a5ca23f64 100644 --- a/Tusker/Views/Notifications/FollowRequestNotificationTableViewCell.swift +++ b/Tusker/Views/Notifications/FollowRequestNotificationTableViewCell.swift @@ -89,7 +89,7 @@ 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) DispatchQueue.main.async { @@ -106,7 +106,7 @@ 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) DispatchQueue.main.async { diff --git a/Tusker/Views/Status/BaseStatusTableViewCell.swift b/Tusker/Views/Status/BaseStatusTableViewCell.swift index f17fe0acb1..f6b062e19a 100644 --- a/Tusker/Views/Status/BaseStatusTableViewCell.swift +++ b/Tusker/Views/Status/BaseStatusTableViewCell.swift @@ -247,7 +247,7 @@ class BaseStatusTableViewCell: UITableViewCell { 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 @@ -272,7 +272,7 @@ class BaseStatusTableViewCell: UITableViewCell { 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 diff --git a/Tusker/Views/Status/TimelineStatusTableViewCell.swift b/Tusker/Views/Status/TimelineStatusTableViewCell.swift index 15be357bb0..a80e425476 100644 --- a/Tusker/Views/Status/TimelineStatusTableViewCell.swift +++ b/Tusker/Views/Status/TimelineStatusTableViewCell.swift @@ -158,7 +158,7 @@ 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) @@ -185,7 +185,7 @@ 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) diff --git a/Tusker/XCallbackURL/XCBActions.swift b/Tusker/XCallbackURL/XCBActions.swift index 15b42eabd7..d90d1920aa 100644 --- a/Tusker/XCallbackURL/XCBActions.swift +++ b/Tusker/XCallbackURL/XCBActions.swift @@ -41,8 +41,8 @@ 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) @@ -72,8 +72,8 @@ 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) @@ -90,8 +90,8 @@ 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) @@ -138,8 +138,8 @@ struct XCBActions { ]) 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, @@ -199,7 +199,7 @@ 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) completion?(status) @@ -285,7 +285,7 @@ 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) session.complete(with: .success, additionalData: [ From 2bdcb9b7f8fe4db1a26adaa3b820eca3f6303d52 Mon Sep 17 00:00:00 2001 From: Shadowfacts Date: Sun, 5 Jan 2020 15:25:07 -0500 Subject: [PATCH 02/21] Replace global shared MastodonController instance with (mostly) dependency injection The places still using the .shared property are cases where there is no view controller from which to (easily) get the appropriate instance, such as user activity and X-Callback-URL handling. These uses will need to be revisited once there are multiple MastodonControllers. See #16 --- Tusker.xcodeproj/project.pbxproj | 4 ++ .../Account Activities/AccountActivity.swift | 2 +- .../FollowAccountActivity.swift | 2 +- .../SendMesasgeActivity.swift | 2 +- .../UnfollowAccountActivity.swift | 2 +- Tusker/Activities/MastodonActivity.swift | 15 ++++++++ .../BookmarkStatusActivity.swift | 2 +- .../Status Activities/PinStatusActivity.swift | 2 +- .../Status Activities/StatusActivity.swift | 2 +- .../UnbookmarkStatusActivity.swift | 2 +- .../UnpinStatusActivity.swift | 2 +- Tusker/AppDelegate.swift | 10 +++-- Tusker/Controllers/MastodonController.swift | 37 ++++++++++--------- Tusker/MastodonCache.swift | 8 ++-- .../AccountListTableViewController.swift | 9 ++++- .../BookmarksTableViewController.swift | 16 +++++--- .../Compose/ComposeViewController.swift | 22 ++++++----- .../ConversationTableViewController.swift | 8 +++- .../Explore/ExploreViewController.swift | 24 +++++++----- .../EditListAccountsViewController.swift | 15 +++++--- .../Lists/ListTimelineViewController.swift | 6 +-- .../Main/MainTabBarViewController.swift | 26 +++++++++---- .../NotificationsPageViewController.swift | 10 +++-- .../NotificationsTableViewController.swift | 14 ++++--- .../Onboarding/OnboardingViewController.swift | 7 ++-- .../MyProfileTableViewController.swift | 6 +-- .../Profile/ProfileTableViewController.swift | 12 ++++-- .../Search/SearchResultsViewController.swift | 9 ++++- ...ActionAccountListTableViewController.swift | 10 ++++- .../HashtagTimelineViewController.swift | 4 +- .../InstanceTimelineViewController.swift | 5 ++- .../TimelineTableViewController.swift | 14 ++++--- .../TimelinesPageViewController.swift | 12 ++++-- Tusker/Shortcuts/UserActivityManager.swift | 10 +++-- Tusker/TuskerNavigationDelegate.swift | 20 +++++----- .../Account Cell/AccountTableViewCell.swift | 8 +++- Tusker/Views/ContentLabel.swift | 14 ++++--- .../Hashtag Cell/HashtagTableViewCell.swift | 2 +- ...FollowNotificationGroupTableViewCell.swift | 6 ++- ...llowRequestNotificationTableViewCell.swift | 8 ++-- .../ProfileHeaderTableViewCell.swift | 3 +- .../Status/BaseStatusTableViewCell.swift | 13 +++++-- .../Status/TimelineStatusTableViewCell.swift | 8 ++-- Tusker/XCallbackURL/XCBActions.swift | 30 ++++++++------- 44 files changed, 286 insertions(+), 157 deletions(-) create mode 100644 Tusker/Activities/MastodonActivity.swift diff --git a/Tusker.xcodeproj/project.pbxproj b/Tusker.xcodeproj/project.pbxproj index 9dd15d8ba4..44d85d1e69 100644 --- a/Tusker.xcodeproj/project.pbxproj +++ b/Tusker.xcodeproj/project.pbxproj @@ -118,6 +118,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 */; }; @@ -388,6 +389,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 = ""; }; @@ -1056,6 +1058,7 @@ children = ( D6AEBB3D2321638100E5038B /* UIActivity+Types.swift */, D6AEBB422321685E00E5038B /* OpenInSafariActivity.swift */, + D64BC19123C271D9000D0238 /* MastodonActivity.swift */, D6AEBB4623216B0C00E5038B /* Account Activities */, D627943323A5523800D38C68 /* Status Activities */, ); @@ -1670,6 +1673,7 @@ 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 */, D6538945214D6D7500E3CEFC /* TableViewSwipeActionProvider.swift in Sources */, diff --git a/Tusker/Activities/Account Activities/AccountActivity.swift b/Tusker/Activities/Account Activities/AccountActivity.swift index e12e223390..1b91a98ecc 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 f0dc74f20b..7beb3523ca 100644 --- a/Tusker/Activities/Account Activities/FollowAccountActivity.swift +++ b/Tusker/Activities/Account Activities/FollowAccountActivity.swift @@ -28,7 +28,7 @@ class FollowAccountActivity: AccountActivity { UIImpactFeedbackGenerator(style: .medium).impactOccurred() let request = Account.follow(account.id) - MastodonController.run(request) { (response) in + mastodonController.run(request) { (response) in if case let .success(relationship, _) = response { MastodonCache.add(relationship: relationship) } else { diff --git a/Tusker/Activities/Account Activities/SendMesasgeActivity.swift b/Tusker/Activities/Account Activities/SendMesasgeActivity.swift index 0238b0abb5..ea67c0b02d 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 94daf75653..344387d782 100644 --- a/Tusker/Activities/Account Activities/UnfollowAccountActivity.swift +++ b/Tusker/Activities/Account Activities/UnfollowAccountActivity.swift @@ -28,7 +28,7 @@ class UnfollowAccountActivity: AccountActivity { UIImpactFeedbackGenerator(style: .medium).impactOccurred() let request = Account.unfollow(account.id) - MastodonController.run(request) { (response) in + mastodonController.run(request) { (response) in if case let .success(relationship, _) = response { MastodonCache.add(relationship: relationship) } else { diff --git a/Tusker/Activities/MastodonActivity.swift b/Tusker/Activities/MastodonActivity.swift new file mode 100644 index 0000000000..32275951e6 --- /dev/null +++ b/Tusker/Activities/MastodonActivity.swift @@ -0,0 +1,15 @@ +// +// MastodonActivity.swift +// Tusker +// +// Created by Shadowfacts on 1/5/20. +// Copyright © 2020 Shadowfacts. All rights reserved. +// + +import UIKit + +class MastodonActivity: UIActivity { + var mastodonController: MastodonController { + MastodonController.shared + } +} diff --git a/Tusker/Activities/Status Activities/BookmarkStatusActivity.swift b/Tusker/Activities/Status Activities/BookmarkStatusActivity.swift index df1fcfc9cf..8c64f007c0 100644 --- a/Tusker/Activities/Status Activities/BookmarkStatusActivity.swift +++ b/Tusker/Activities/Status Activities/BookmarkStatusActivity.swift @@ -27,7 +27,7 @@ class BookmarkStatusActivity: StatusActivity { guard let status = status else { return } let request = Status.bookmark(status) - MastodonController.run(request) { (response) in + mastodonController.run(request) { (response) in if case let .success(status, _) = response { MastodonCache.add(status: status) } else { diff --git a/Tusker/Activities/Status Activities/PinStatusActivity.swift b/Tusker/Activities/Status Activities/PinStatusActivity.swift index f73d04749f..3714a453a4 100644 --- a/Tusker/Activities/Status Activities/PinStatusActivity.swift +++ b/Tusker/Activities/Status Activities/PinStatusActivity.swift @@ -26,7 +26,7 @@ class PinStatusActivity: StatusActivity { guard let status = status else { return } let request = Status.pin(status) - MastodonController.run(request) { (response) in + mastodonController.run(request) { (response) in if case let .success(status, _) = response { MastodonCache.add(status: status) } else { diff --git a/Tusker/Activities/Status Activities/StatusActivity.swift b/Tusker/Activities/Status Activities/StatusActivity.swift index 715b0a6e3d..3469b78922 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 e88a3549be..be22c5e130 100644 --- a/Tusker/Activities/Status Activities/UnbookmarkStatusActivity.swift +++ b/Tusker/Activities/Status Activities/UnbookmarkStatusActivity.swift @@ -27,7 +27,7 @@ class UnbookmarkStatusActivity: StatusActivity { guard let status = status else { return } let request = Status.unbookmark(status) - MastodonController.run(request) { (response) in + mastodonController.run(request) { (response) in if case let .success(status, _) = response { MastodonCache.add(status: status) } else { diff --git a/Tusker/Activities/Status Activities/UnpinStatusActivity.swift b/Tusker/Activities/Status Activities/UnpinStatusActivity.swift index 134e852d21..cbb717cbc6 100644 --- a/Tusker/Activities/Status Activities/UnpinStatusActivity.swift +++ b/Tusker/Activities/Status Activities/UnpinStatusActivity.swift @@ -26,7 +26,7 @@ class UnpinStatusActivity: StatusActivity { guard let status = status else { return } let request = Status.unpin(status) - MastodonController.run(request) { (response) in + mastodonController.run(request) { (response) in if case let .success(status, _) = response { MastodonCache.add(status: status) } else { diff --git a/Tusker/AppDelegate.swift b/Tusker/AppDelegate.swift index 68e6c4a830..6f7521aa9b 100644 --- a/Tusker/AppDelegate.swift +++ b/Tusker/AppDelegate.swift @@ -12,6 +12,8 @@ import UIKit class AppDelegate: UIResponder, UIApplicationDelegate { var window: UIWindow? + + let mastodonController = MastodonController.shared func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { AppShortcutItem.createItems(for: application) @@ -95,11 +97,11 @@ class AppDelegate: UIResponder, UIApplicationDelegate { } func showAppUI() { - MastodonController.createClient() - MastodonController.getOwnAccount() - MastodonController.getOwnInstance() + mastodonController.createClient() + mastodonController.getOwnAccount() + mastodonController.getOwnInstance() - let tabBarController = MainTabBarViewController() + let tabBarController = MainTabBarViewController(mastodonController: mastodonController) window!.rootViewController = tabBarController } diff --git a/Tusker/Controllers/MastodonController.swift b/Tusker/Controllers/MastodonController.swift index 6c841c494e..a916d77734 100644 --- a/Tusker/Controllers/MastodonController.swift +++ b/Tusker/Controllers/MastodonController.swift @@ -11,32 +11,33 @@ import Pachyderm class MastodonController { - private static var client: Client! + @available(*, deprecated, message: "Use dependency injection to obtain an instance") + static let shared = MastodonController() - static var account: Account! - static var instance: Instance! + private var client: Client! - static var accessToken: String? { + var account: Account! + var instance: Instance! + + var accessToken: String? { client?.accessToken } - private init() {} - - static func createClient() { - guard let url = LocalData.shared.instanceURL else { fatalError("Can't connect without instance URL") } + func createClient(instanceURL: URL = LocalData.shared.instanceURL!) { + client = Client(baseURL: instanceURL) - client = Client(baseURL: url) - - client.clientID = LocalData.shared.clientID - client.clientSecret = LocalData.shared.clientSecret - client.accessToken = LocalData.shared.accessToken + if instanceURL == LocalData.shared.instanceURL { + client.clientID = LocalData.shared.clientID + client.clientSecret = LocalData.shared.clientSecret + client.accessToken = LocalData.shared.accessToken + } } - static func run(_ request: Request, completion: @escaping Client.Callback) { + func run(_ request: Request, completion: @escaping Client.Callback) { client.run(request, completion: completion) } - static func registerApp(completion: @escaping () -> Void) { + func registerApp(completion: @escaping () -> Void) { guard LocalData.shared.clientID == nil, LocalData.shared.clientSecret == nil else { completion() @@ -51,7 +52,7 @@ class MastodonController { } } - static func authorize(authorizationCode: String, completion: @escaping () -> Void) { + func authorize(authorizationCode: String, completion: @escaping () -> Void) { client.getAccessToken(authorizationCode: authorizationCode, redirectURI: "tusker://oauth") { response in guard case let .success(settings, _) = response else { fatalError() } LocalData.shared.accessToken = settings.accessToken @@ -59,7 +60,7 @@ class MastodonController { } } - static func getOwnAccount(completion: ((Account) -> Void)? = nil) { + func getOwnAccount(completion: ((Account) -> Void)? = nil) { if account != nil { completion?(account) } else { @@ -73,7 +74,7 @@ class MastodonController { } } - static func getOwnInstance() { + func getOwnInstance() { let request = Client.getInstance() run(request) { (response) in guard case let .success(instance, _) = response else { fatalError() } diff --git a/Tusker/MastodonCache.swift b/Tusker/MastodonCache.swift index 70b5482d57..1ac5eddf57 100644 --- a/Tusker/MastodonCache.swift +++ b/Tusker/MastodonCache.swift @@ -20,6 +20,8 @@ class MastodonCache { static let statusSubject = PassthroughSubject() static let accountSubject = PassthroughSubject() + static var mastodonController: MastodonController { .shared } + // MARK: - Statuses static func status(for id: String) -> Status? { return statuses[id] @@ -38,7 +40,7 @@ class MastodonCache { static func status(for id: String, completion: @escaping (Status?) -> Void) { let request = Client.getStatus(id: id) - MastodonController.run(request) { response in + mastodonController.run(request) { response in guard case let .success(status, _) = response else { completion(nil) return @@ -68,7 +70,7 @@ class MastodonCache { static func account(for id: String, completion: @escaping (Account?) -> Void) { let request = Client.getAccount(id: id) - MastodonController.run(request) { response in + mastodonController.run(request) { response in guard case let .success(account, _) = response else { completion(nil) return @@ -97,7 +99,7 @@ class MastodonCache { static func relationship(for id: String, completion: @escaping (Relationship?) -> Void) { let request = Client.getRelationships(accounts: [id]) - MastodonController.run(request) { response in + mastodonController.run(request) { response in guard case let .success(relationships, _) = response, let relationship = relationships.first else { completion(nil) diff --git a/Tusker/Screens/Account List/AccountListTableViewController.swift b/Tusker/Screens/Account List/AccountListTableViewController.swift index 8635a01745..36fe72e25c 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) } @@ -58,4 +61,6 @@ class AccountListTableViewController: EnhancedTableViewController { } -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 142213af17..a4370c0501 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") @@ -45,7 +49,7 @@ class BookmarksTableViewController: EnhancedTableViewController { tableView.prefetchDataSource = self let request = Client.getBookmarks() - MastodonController.run(request) { (response) in + mastodonController.run(request) { (response) in guard case let .success(statuses, pagination) = response else { fatalError() } MastodonCache.addAll(statuses: statuses) self.statuses.append(contentsOf: statuses.map { ($0.id, .unknown) }) @@ -82,7 +86,7 @@ class BookmarksTableViewController: EnhancedTableViewController { } let request = Client.getBookmarks(range: older) - MastodonController.run(request) { (response) in + mastodonController.run(request) { (response) in guard case let .success(newStatuses, pagination) = response else { fatalError() } self.older = pagination?.older MastodonCache.addAll(statuses: newStatuses) @@ -107,7 +111,7 @@ class BookmarksTableViewController: EnhancedTableViewController { let unbookmarkAction = UIContextualAction(style: .destructive, title: NSLocalizedString("Unbookmark", comment: "unbookmark action title")) { (action, view, completion) in let request = Status.unbookmark(status) - MastodonController.run(request) { (response) in + self.mastodonController.run(request) { (response) in guard case let .success(newStatus, _) = response else { fatalError() } MastodonCache.add(status: newStatus) self.statuses.remove(at: indexPath.row) @@ -131,7 +135,7 @@ class BookmarksTableViewController: EnhancedTableViewController { 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.run(request) { (response) in + self.mastodonController.run(request) { (response) in guard case let .success(newStatus, _) = response else { fatalError() } MastodonCache.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() diff --git a/Tusker/Screens/Compose/ComposeViewController.swift b/Tusker/Screens/Compose/ComposeViewController.swift index 6dbabc22d5..07a6816971 100644 --- a/Tusker/Screens/Compose/ComposeViewController.swift +++ b/Tusker/Screens/Compose/ComposeViewController.swift @@ -12,6 +12,8 @@ import Intents class ComposeViewController: UIViewController { + let mastodonController: MastodonController + var inReplyToID: String? var accountsToMention: [String] var initialText: String? @@ -64,7 +66,9 @@ 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) { accountsToMention = [inReplyTo.account.acct] + inReplyTo.mentions.map { $0.acct } @@ -73,7 +77,7 @@ class ComposeViewController: UIViewController { } else { accountsToMention = [] } - if let ownAccount = MastodonController.account { + if let ownAccount = mastodonController.account { accountsToMention.removeAll(where: { acct in ownAccount.acct == acct }) } accountsToMention = accountsToMention.uniques() @@ -120,7 +124,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) } @@ -270,7 +274,7 @@ class ComposeViewController: UIViewController { // TODO: include CW char count 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 postBarButtonItem.isEnabled = false @@ -296,7 +300,7 @@ class ComposeViewController: UIViewController { } func updateAddAttachmentButton() { - switch MastodonController.instance.instanceType { + switch mastodonController.instance.instanceType { case .pleroma: addAttachmentButton.isEnabled = true case .mastodon: @@ -481,7 +485,7 @@ class ComposeViewController: UIViewController { self.postProgressView.step() let request = Client.upload(attachment: FormAttachment(mimeType: mimeType, data: data, fileName: "file"), description: description) - MastodonController.run(request) { (response) in + self.mastodonController.run(request) { (response) in guard case let .success(attachment, _) = response else { fatalError() } attachments[index] = attachment @@ -507,7 +511,7 @@ class ComposeViewController: UIViewController { spoilerText: contentWarning, visibility: visibility, language: nil) - MastodonController.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) @@ -520,7 +524,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: [ @@ -561,7 +565,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: diff --git a/Tusker/Screens/Conversation/ConversationTableViewController.swift b/Tusker/Screens/Conversation/ConversationTableViewController.swift index 92410f587f..31bbc06147 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) } @@ -58,7 +61,7 @@ class ConversationTableViewController: EnhancedTableViewController { guard let mainStatus = MastodonCache.status(for: mainStatusID) else { fatalError("Missing cached status \(mainStatusID)") } let request = Status.getContext(mainStatus) - MastodonController.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) @@ -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() diff --git a/Tusker/Screens/Explore/ExploreViewController.swift b/Tusker/Screens/Explore/ExploreViewController.swift index a7f6fc120c..c278c2b42e 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") @@ -88,7 +92,7 @@ class ExploreViewController: EnhancedTableViewController { dataSource.apply(snapshot) } - resultsController = SearchResultsViewController() + resultsController = SearchResultsViewController(mastodonController: mastodonController) resultsController.exploreNavigationController = self.navigationController! searchController = UISearchController(searchResultsController: resultsController) searchController.searchResultsUpdater = resultsController @@ -107,7 +111,7 @@ class ExploreViewController: EnhancedTableViewController { func reloadLists() { let request = Client.getLists() - MastodonController.run(request) { (response) in + mastodonController.run(request) { (response) in guard case let .success(lists, _) = response else { fatalError() } @@ -143,7 +147,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.run(request) { (response) in + self.mastodonController.run(request) { (response) in guard case .success(_, _) = response else { fatalError() } @@ -174,10 +178,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) @@ -190,13 +194,13 @@ class ExploreViewController: EnhancedTableViewController { } let request = Client.createList(title: title) - MastodonController.run(request) { (response) in + 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,11 +209,11 @@ 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): diff --git a/Tusker/Screens/Lists/EditListAccountsViewController.swift b/Tusker/Screens/Lists/EditListAccountsViewController.swift index 7484d1dd06..796c58f409 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,7 +73,7 @@ class EditListAccountsViewController: EnhancedTableViewController { func loadAccounts() { let request = List.getAccounts(list) - MastodonController.run(request) { (response) in + mastodonController.run(request) { (response) in guard case let .success(accounts, pagination) = response else { fatalError() } @@ -109,7 +112,7 @@ class EditListAccountsViewController: EnhancedTableViewController { fatalError() } let request = List.update(self.list, title: text) - MastodonController.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.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.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 664d65eed9..bf8dbac3f1 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 0259076567..d0d01655d7 100644 --- a/Tusker/Screens/Main/MainTabBarViewController.swift +++ b/Tusker/Screens/Main/MainTabBarViewController.swift @@ -9,18 +9,30 @@ import UIKit class MainTabBarViewController: UITabBarController, UITabBarControllerDelegate { - + + let mastodonController: MastodonController + + 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)), ] } @@ -41,7 +53,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 3e503e1b2d..ac66701a87 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") + + let 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 ae6ccf3541..4bee64b26a 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) @@ -57,7 +60,7 @@ class NotificationsTableViewController: EnhancedTableViewController { tableView.prefetchDataSource = self let request = Client.getNotifications(excludeTypes: excludedTypes) - MastodonController.run(request) { result in + mastodonController.run(request) { result in guard case let .success(notifications, pagination) = result else { fatalError() } let groups = NotificationGroup.createGroups(notifications: notifications, only: self.groupTypes) @@ -125,7 +128,7 @@ class NotificationsTableViewController: EnhancedTableViewController { guard let older = older else { return } let request = Client.getNotifications(excludeTypes: excludedTypes, range: older) - MastodonController.run(request) { result in + mastodonController.run(request) { result in guard case let .success(newNotifications, pagination) = result else { fatalError() } let groups = NotificationGroup.createGroups(notifications: newNotifications, only: self.groupTypes) @@ -182,7 +185,7 @@ class NotificationsTableViewController: EnhancedTableViewController { .map(Pachyderm.Notification.dismiss(id:)) .forEach { (request) in group.enter() - MastodonController.run(request) { (response) in + mastodonController.run(request) { (response) in group.leave() } } @@ -197,7 +200,7 @@ class NotificationsTableViewController: EnhancedTableViewController { guard let newer = newer else { return } let request = Client.getNotifications(excludeTypes: excludedTypes, range: newer) - MastodonController.run(request) { result in + mastodonController.run(request) { result in guard case let .success(newNotifications, pagination) = result else { fatalError() } let groups = NotificationGroup.createGroups(notifications: newNotifications, only: self.groupTypes) @@ -222,6 +225,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() diff --git a/Tusker/Screens/Onboarding/OnboardingViewController.swift b/Tusker/Screens/Onboarding/OnboardingViewController.swift index 7df609294e..21daac5eb9 100644 --- a/Tusker/Screens/Onboarding/OnboardingViewController.swift +++ b/Tusker/Screens/Onboarding/OnboardingViewController.swift @@ -46,8 +46,9 @@ class OnboardingViewController: UINavigationController { extension OnboardingViewController: InstanceSelectorTableViewControllerDelegate { func didSelectInstance(url: URL) { LocalData.shared.instanceURL = url - MastodonController.createClient() - MastodonController.registerApp { + let mastodonController = MastodonController.shared + mastodonController.createClient() + mastodonController.registerApp { let clientID = LocalData.shared.clientID! let callbackURL = "tusker://oauth" @@ -69,7 +70,7 @@ extension OnboardingViewController: InstanceSelectorTableViewControllerDelegate let item = components.queryItems?.first(where: { $0.name == "code" }), let authCode = item.value else { return } - MastodonController.authorize(authorizationCode: authCode) { + mastodonController.authorize(authorizationCode: authCode) { DispatchQueue.main.async { self.onboardingDelegate?.didFinishOnboarding() } diff --git a/Tusker/Screens/Profile/MyProfileTableViewController.swift b/Tusker/Screens/Profile/MyProfileTableViewController.swift index 3e58ac5934..7fef87064a 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 diff --git a/Tusker/Screens/Profile/ProfileTableViewController.swift b/Tusker/Screens/Profile/ProfileTableViewController.swift index 45b520e035..e6b1d4b36b 100644 --- a/Tusker/Screens/Profile/ProfileTableViewController.swift +++ b/Tusker/Screens/Profile/ProfileTableViewController.swift @@ -12,6 +12,8 @@ import SafariServices class ProfileTableViewController: EnhancedTableViewController { + let 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) @@ -130,12 +134,12 @@ class ProfileTableViewController: EnhancedTableViewController { 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.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)) + let vc = UINavigationController(rootViewController: ComposeViewController(mentioningAcct: account.acct, mastodonController: mastodonController)) present(vc, animated: true) } @@ -233,6 +237,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() diff --git a/Tusker/Screens/Search/SearchResultsViewController.swift b/Tusker/Screens/Search/SearchResultsViewController.swift index ad77eed37e..45e97c4bf2 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") @@ -118,7 +122,7 @@ class SearchResultsViewController: EnhancedTableViewController { } let request = Client.search(query: query, resolve: true, limit: 10) - MastodonController.run(request) { (response) in + mastodonController.run(request) { (response) in guard case let .success(results, _) = response else { fatalError() } DispatchQueue.main.async { @@ -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 9ccf923460..e0d9195cea 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 @@ -75,7 +80,7 @@ class StatusActionAccountListTableViewController: EnhancedTableViewController { tableView.tableFooterView = UIActivityIndicatorView(style: .large) let request = actionType == .favorite ? Status.getFavourites(status) : Status.getReblogs(status) - MastodonController.run(request) { (response) in + mastodonController.run(request) { (response) in guard case let .success(accounts, _) = response else { fatalError() } MastodonCache.addAll(accounts: accounts) DispatchQueue.main.async { @@ -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 0313a11e74..3dcd8787c7 100644 --- a/Tusker/Screens/Timeline/HashtagTimelineViewController.swift +++ b/Tusker/Screens/Timeline/HashtagTimelineViewController.swift @@ -22,10 +22,10 @@ class HashtagTimelineViewController: TimelineTableViewController { } } - 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) { diff --git a/Tusker/Screens/Timeline/InstanceTimelineViewController.swift b/Tusker/Screens/Timeline/InstanceTimelineViewController.swift index 405c998fad..b5ee418d31 100644 --- a/Tusker/Screens/Timeline/InstanceTimelineViewController.swift +++ b/Tusker/Screens/Timeline/InstanceTimelineViewController.swift @@ -31,7 +31,10 @@ class InstanceTimelineViewController: TimelineTableViewController { init(for url: URL) { self.instanceURL = url - super.init(for: .instance(instanceURL: url)) + let mastodonController = MastodonController() + mastodonController.createClient(instanceURL: url) + + super.init(for: .instance(instanceURL: url), mastodonController: mastodonController) } required init?(coder aDecoder: NSCoder) { diff --git a/Tusker/Screens/Timeline/TimelineTableViewController.swift b/Tusker/Screens/Timeline/TimelineTableViewController.swift index f38cacd2af..f110638f6e 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! + let 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,13 +58,13 @@ class TimelineTableViewController: EnhancedTableViewController { tableView.prefetchDataSource = self - guard MastodonController.accessToken != nil else { return } + guard mastodonController.accessToken != nil else { return } loadInitialStatuses() } func loadInitialStatuses() { let request = Client.getStatuses(timeline: timeline) - MastodonController.run(request) { response in + mastodonController.run(request) { response in guard case let .success(statuses, pagination) = response else { fatalError() } MastodonCache.addAll(statuses: statuses) self.timelineSegments.insert(statuses.map { ($0.id, .unknown) }, at: 0) @@ -100,7 +102,7 @@ class TimelineTableViewController: EnhancedTableViewController { guard let older = older else { return } let request = Client.getStatuses(timeline: timeline, range: older) - MastodonController.run(request) { response in + mastodonController.run(request) { response in guard case let .success(newStatuses, pagination) = response else { fatalError() } self.older = pagination?.older MastodonCache.addAll(statuses: newStatuses) @@ -125,7 +127,7 @@ class TimelineTableViewController: EnhancedTableViewController { guard let newer = newer else { return } let request = Client.getStatuses(timeline: timeline, range: newer) - MastodonController.run(request) { response in + mastodonController.run(request) { response in guard case let .success(newStatuses, pagination) = response else { fatalError() } self.newer = pagination?.newer MastodonCache.addAll(statuses: newStatuses) @@ -146,6 +148,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() diff --git a/Tusker/Screens/Timeline/TimelinesPageViewController.swift b/Tusker/Screens/Timeline/TimelinesPageViewController.swift index 7fd95c3a46..a01e74dbe9 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) + let 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/Shortcuts/UserActivityManager.swift b/Tusker/Shortcuts/UserActivityManager.swift index b53373afc3..5701e1f9c4 100644 --- a/Tusker/Shortcuts/UserActivityManager.swift +++ b/Tusker/Shortcuts/UserActivityManager.swift @@ -15,6 +15,8 @@ class UserActivityManager { private static let encoder = PropertyListEncoder() private static let decoder = PropertyListDecoder() + private static var mastodonController: MastodonController { .shared } + private static func getMainTabBarController() -> MainTabBarViewController { return (UIApplication.shared.delegate! as! AppDelegate).window!.rootViewController as! MainTabBarViewController } @@ -42,7 +44,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 @@ -144,7 +147,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 +186,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 a62072b2bc..2530ed1709 100644 --- a/Tusker/TuskerNavigationDelegate.swift +++ b/Tusker/TuskerNavigationDelegate.swift @@ -12,6 +12,8 @@ import Pachyderm protocol TuskerNavigationDelegate { + var apiController: MastodonController { get } + func show(_ vc: UIViewController) func selected(account accountID: String) @@ -68,15 +70,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) { @@ -113,7 +115,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 @@ -122,14 +124,14 @@ extension TuskerNavigationDelegate where Self: UIViewController { } func compose(mentioning: String? = nil) { - 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) } func reply(to statusID: String) { - let compose = ComposeViewController(inReplyTo: statusID) + let compose = ComposeViewController(inReplyTo: statusID, mastodonController: apiController) let vc = UINavigationController(rootViewController: compose) vc.presentationController?.delegate = compose present(vc, animated: true) @@ -200,7 +202,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) } @@ -228,13 +230,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 e933d33551..cd82f5f422 100644 --- a/Tusker/Views/Account Cell/AccountTableViewCell.swift +++ b/Tusker/Views/Account Cell/AccountTableViewCell.swift @@ -11,6 +11,7 @@ import UIKit class AccountTableViewCell: UITableViewCell { var delegate: TuskerNavigationDelegate? + var mastodonController: MastodonController? { delegate?.apiController } @IBOutlet weak var avatarImageView: UIImageView! @IBOutlet weak var displayNameLabel: UILabel! @@ -68,6 +69,11 @@ extension AccountTableViewCell: MenuPreviewProvider { var navigationDelegate: TuskerNavigationDelegate? { return delegate } func getPreviewProviders(for location: CGPoint, sourceViewController: UIViewController) -> PreviewProviders? { - return (content: { ProfileTableViewController(accountID: self.accountID) }, actions: { self.actionsForProfile(accountID: self.accountID) }) + guard let mastodonController = mastodonController else { return nil } + return (content: { + ProfileTableViewController(accountID: self.accountID, mastodonController: mastodonController) + }, actions: { + self.actionsForProfile(accountID: self.accountID) + }) } } diff --git a/Tusker/Views/ContentLabel.swift b/Tusker/Views/ContentLabel.swift index 1cdced2372..b53e2d0293 100644 --- a/Tusker/Views/ContentLabel.swift +++ b/Tusker/Views/ContentLabel.swift @@ -178,13 +178,15 @@ class ContentLabel: LinkLabel { func getViewController(forLink url: URL, inRange range: NSRange) -> UIViewController { let text = (self.text! as NSString).substring(with: range) - if let mention = getMention(for: url, text: text) { - return ProfileTableViewController(accountID: mention.id) - } else if let tag = getHashtag(for: url, text: text) { - return HashtagTimelineViewController(for: tag) - } else { - return SFSafariViewController(url: url) + if let navigationDelegate = navigationDelegate { + if let mention = getMention(for: url, text: text) { + return ProfileTableViewController(accountID: mention.id, mastodonController: navigationDelegate.apiController) + } else if let tag = getHashtag(for: url, text: text) { + return HashtagTimelineViewController(for: tag, mastodonController: navigationDelegate.apiController) + } + } + return SFSafariViewController(url: url) } func getViewController(forLinkAt point: CGPoint) -> UIViewController? { diff --git a/Tusker/Views/Hashtag Cell/HashtagTableViewCell.swift b/Tusker/Views/Hashtag Cell/HashtagTableViewCell.swift index 387c7652df..bd364d1668 100644 --- a/Tusker/Views/Hashtag Cell/HashtagTableViewCell.swift +++ b/Tusker/Views/Hashtag Cell/HashtagTableViewCell.swift @@ -12,7 +12,7 @@ import Pachyderm class HashtagTableViewCell: UITableViewCell { var delegate: TuskerNavigationDelegate? - + @IBOutlet weak var hashtagLabel: UILabel! var hashtag: Hashtag! diff --git a/Tusker/Views/Notifications/FollowNotificationGroupTableViewCell.swift b/Tusker/Views/Notifications/FollowNotificationGroupTableViewCell.swift index cafb5c134f..f45025dcd5 100644 --- a/Tusker/Views/Notifications/FollowNotificationGroupTableViewCell.swift +++ b/Tusker/Views/Notifications/FollowNotificationGroupTableViewCell.swift @@ -12,6 +12,7 @@ import Pachyderm class FollowNotificationGroupTableViewCell: UITableViewCell { var delegate: TuskerNavigationDelegate? + var mastodonController: MastodonController? { delegate?.apiController } @IBOutlet weak var avatarStackView: UIStackView! @IBOutlet weak var timestampLabel: UILabel! @@ -133,12 +134,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 } 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 8a5ca23f64..b8865507ac 100644 --- a/Tusker/Views/Notifications/FollowRequestNotificationTableViewCell.swift +++ b/Tusker/Views/Notifications/FollowRequestNotificationTableViewCell.swift @@ -12,6 +12,7 @@ import Pachyderm class FollowRequestNotificationTableViewCell: UITableViewCell { var delegate: TuskerNavigationDelegate? + var mastodonController: MastodonController? { delegate?.apiController } @IBOutlet weak var stackView: UIStackView! @IBOutlet weak var avatarImageView: UIImageView! @@ -89,7 +90,7 @@ class FollowRequestNotificationTableViewCell: UITableViewCell { @IBAction func rejectButtonPressed() { let request = Account.rejectFollowRequest(account) - MastodonController.run(request) { (response) in + mastodonController!.run(request) { (response) in guard case let .success(relationship, _) = response else { fatalError() } MastodonCache.add(relationship: relationship) DispatchQueue.main.async { @@ -106,7 +107,7 @@ class FollowRequestNotificationTableViewCell: UITableViewCell { @IBAction func acceptButtonPressed() { let request = Account.authorizeFollowRequest(account) - MastodonController.run(request) { (response) in + mastodonController!.run(request) { (response) in guard case let .success(relationship, _) = response else { fatalError() } MastodonCache.add(relationship: relationship) DispatchQueue.main.async { @@ -133,8 +134,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 a3517792ed..c948f666de 100644 --- a/Tusker/Views/Profile Header/ProfileHeaderTableViewCell.swift +++ b/Tusker/Views/Profile Header/ProfileHeaderTableViewCell.swift @@ -16,6 +16,7 @@ protocol ProfileHeaderTableViewCellDelegate: TuskerNavigationDelegate { class ProfileHeaderTableViewCell: UITableViewCell { var delegate: ProfileHeaderTableViewCellDelegate? + var mastodonController: MastodonController? { delegate?.apiController } @IBOutlet weak var headerImageView: UIImageView! @IBOutlet weak var avatarContainerView: UIView! @@ -82,7 +83,7 @@ class ProfileHeaderTableViewCell: UITableViewCell { noteLabel.setTextFromHtml(account.note) noteLabel.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) { followsYouLabel.isHidden = !relationship.followedBy diff --git a/Tusker/Views/Status/BaseStatusTableViewCell.swift b/Tusker/Views/Status/BaseStatusTableViewCell.swift index f6b062e19a..f9b985c52b 100644 --- a/Tusker/Views/Status/BaseStatusTableViewCell.swift +++ b/Tusker/Views/Status/BaseStatusTableViewCell.swift @@ -15,11 +15,13 @@ protocol StatusTableViewCellDelegate: TuskerNavigationDelegate { } class BaseStatusTableViewCell: UITableViewCell { + var delegate: StatusTableViewCellDelegate? { didSet { contentLabel.navigationDelegate = delegate } } + var mastodonController: MastodonController? { delegate?.apiController } @IBOutlet weak var avatarImageView: UIImageView! @IBOutlet weak var displayNameLabel: UILabel! @@ -247,7 +249,7 @@ class BaseStatusTableViewCell: UITableViewCell { let realStatus: Status = status.reblog ?? status let request = (favorited ? Status.favourite : Status.unfavourite)(realStatus) - MastodonController.run(request) { response in + mastodonController!.run(request) { response in DispatchQueue.main.async { if case let .success(newStatus, _) = response { self.favorited = newStatus.favourited ?? false @@ -272,7 +274,7 @@ class BaseStatusTableViewCell: UITableViewCell { let realStatus: Status = status.reblog ?? status let request = (reblogged ? Status.reblog : Status.unreblog)(realStatus) - MastodonController.run(request) { response in + mastodonController!.run(request) { response in DispatchQueue.main.async { if case let .success(newStatus, _) = response { self.reblogged = newStatus.reblogged ?? false @@ -313,8 +315,13 @@ 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)}, actions: { self.actionsForProfile(accountID: self.accountID) }) + return (content: { + ProfileTableViewController(accountID: self.accountID, mastodonController: mastodonController) + }, actions: { + self.actionsForProfile(accountID: self.accountID) + }) } else if attachmentsView.frame.contains(location) { let attachmentsViewLocation = attachmentsView.convert(location, from: self) if let attachmentView = attachmentsView.attachmentViews.allObjects.first(where: { $0.frame.contains(attachmentsViewLocation) }), diff --git a/Tusker/Views/Status/TimelineStatusTableViewCell.swift b/Tusker/Views/Status/TimelineStatusTableViewCell.swift index a80e425476..a42ddeb24a 100644 --- a/Tusker/Views/Status/TimelineStatusTableViewCell.swift +++ b/Tusker/Views/Status/TimelineStatusTableViewCell.swift @@ -125,8 +125,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) } ) } @@ -142,6 +143,7 @@ extension TimelineStatusTableViewCell: SelectableTableViewCell { extension TimelineStatusTableViewCell: TableViewSwipeActionProvider { func leadingSwipeActionsConfiguration() -> UISwipeActionsConfiguration? { + guard let mastodonController = mastodonController else { return nil } guard let status = MastodonCache.status(for: statusID) else { fatalError("Missing cached status \(statusID!)") } let favoriteTitle: String @@ -158,7 +160,7 @@ 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.run(favoriteRequest, completion: { response in + mastodonController.run(favoriteRequest, completion: { response in DispatchQueue.main.async { guard case let .success(status, _) = response else { completion(false) @@ -185,7 +187,7 @@ extension TimelineStatusTableViewCell: TableViewSwipeActionProvider { reblogColor = tintColor } let reblog = UIContextualAction(style: .normal, title: reblogTitle) { (action, view, completion) in - MastodonController.run(reblogRequest, completion: { response in + mastodonController.run(reblogRequest, completion: { response in DispatchQueue.main.async { guard case let .success(status, _) = response else { completion(false) diff --git a/Tusker/XCallbackURL/XCBActions.swift b/Tusker/XCallbackURL/XCBActions.swift index d90d1920aa..ead66b9371 100644 --- a/Tusker/XCallbackURL/XCBActions.swift +++ b/Tusker/XCallbackURL/XCBActions.swift @@ -13,6 +13,8 @@ import SwiftSoup struct XCBActions { // MARK: - Utils + private static var mastodonController: MastodonController { .shared } + private static func getMainTabBarController() -> MainTabBarViewController { return (UIApplication.shared.delegate as! AppDelegate).window!.rootViewController as! MainTabBarViewController } @@ -42,7 +44,7 @@ struct XCBActions { } } else if let searchQuery = request.arguments["statusURL"] { let request = Client.search(query: searchQuery) - MastodonController.run(request) { (response) in + mastodonController.run(request) { (response) in if case let .success(results, _) = response, let status = results.statuses.first { MastodonCache.add(status: status) @@ -73,7 +75,7 @@ struct XCBActions { } } else if let searchQuery = request.arguments["accountURL"] { let request = Client.search(query: searchQuery) - MastodonController.run(request) { (response) in + mastodonController.run(request) { (response) in if case let .success(results, _) = response { if let account = results.accounts.first { MastodonCache.add(account: account) @@ -91,7 +93,7 @@ struct XCBActions { } } else if let acct = request.arguments["acct"] { let request = Client.searchForAccount(query: acct) - MastodonController.run(request) { (response) in + mastodonController.run(request) { (response) in if case let .success(accounts, _) = response { if let account = accounts.first { MastodonCache.add(account: account) @@ -118,7 +120,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 +134,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 = Client.createStatus(text: status, visibility: Preferences.shared.defaultPostVisibility) - MastodonController.run(request) { response in + mastodonController.run(request) { response in if case let .success(status, _) = response { session.complete(with: .success, additionalData: [ "statusURL": status.url?.absoluteString, @@ -152,7 +154,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,7 +201,7 @@ 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.run(request(status)) { (response) in + mastodonController.run(request(status)) { (response) in if case let .success(status, _) = response { MastodonCache.add(status: status) completion?(status) @@ -219,7 +221,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 +249,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 +271,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,7 +287,7 @@ struct XCBActions { static func followUser(_ request: XCBRequest, _ session: XCBSession, _ silent: Bool?) { func performAction(_ account: Account) { let request = Account.follow(account.id) - MastodonController.run(request) { (response) in + mastodonController.run(request) { (response) in if case let .success(relationship, _) = response { MastodonCache.add(relationship: relationship) session.complete(with: .success, additionalData: [ @@ -303,7 +305,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) } From e3be424f5a77577c0f8a0d36c65a030f75d0127c Mon Sep 17 00:00:00 2001 From: Shadowfacts Date: Sun, 5 Jan 2020 19:39:37 -0500 Subject: [PATCH 03/21] Fix instance public timelines not loading --- .../Timeline/InstanceTimelineViewController.swift | 9 +++++++++ .../Screens/Timeline/TimelineTableViewController.swift | 1 - Tusker/Views/Account Cell/AccountTableViewCell.swift | 2 +- .../FollowNotificationGroupTableViewCell.swift | 2 +- .../FollowRequestNotificationTableViewCell.swift | 6 +++--- .../Profile Header/ProfileHeaderTableViewCell.swift | 4 ++-- Tusker/Views/Status/BaseStatusTableViewCell.swift | 7 ++++--- 7 files changed, 20 insertions(+), 11 deletions(-) diff --git a/Tusker/Screens/Timeline/InstanceTimelineViewController.swift b/Tusker/Screens/Timeline/InstanceTimelineViewController.swift index b5ee418d31..df598ab8be 100644 --- a/Tusker/Screens/Timeline/InstanceTimelineViewController.swift +++ b/Tusker/Screens/Timeline/InstanceTimelineViewController.swift @@ -54,6 +54,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) { diff --git a/Tusker/Screens/Timeline/TimelineTableViewController.swift b/Tusker/Screens/Timeline/TimelineTableViewController.swift index f110638f6e..93362925c6 100644 --- a/Tusker/Screens/Timeline/TimelineTableViewController.swift +++ b/Tusker/Screens/Timeline/TimelineTableViewController.swift @@ -58,7 +58,6 @@ class TimelineTableViewController: EnhancedTableViewController { tableView.prefetchDataSource = self - guard mastodonController.accessToken != nil else { return } loadInitialStatuses() } diff --git a/Tusker/Views/Account Cell/AccountTableViewCell.swift b/Tusker/Views/Account Cell/AccountTableViewCell.swift index cd82f5f422..861b6b8a49 100644 --- a/Tusker/Views/Account Cell/AccountTableViewCell.swift +++ b/Tusker/Views/Account Cell/AccountTableViewCell.swift @@ -11,7 +11,7 @@ import UIKit class AccountTableViewCell: UITableViewCell { var delegate: TuskerNavigationDelegate? - var mastodonController: MastodonController? { delegate?.apiController } + var mastodonController: MastodonController! { delegate?.apiController } @IBOutlet weak var avatarImageView: UIImageView! @IBOutlet weak var displayNameLabel: UILabel! diff --git a/Tusker/Views/Notifications/FollowNotificationGroupTableViewCell.swift b/Tusker/Views/Notifications/FollowNotificationGroupTableViewCell.swift index f45025dcd5..b77cb15208 100644 --- a/Tusker/Views/Notifications/FollowNotificationGroupTableViewCell.swift +++ b/Tusker/Views/Notifications/FollowNotificationGroupTableViewCell.swift @@ -12,7 +12,7 @@ import Pachyderm class FollowNotificationGroupTableViewCell: UITableViewCell { var delegate: TuskerNavigationDelegate? - var mastodonController: MastodonController? { delegate?.apiController } + var mastodonController: MastodonController! { delegate?.apiController } @IBOutlet weak var avatarStackView: UIStackView! @IBOutlet weak var timestampLabel: UILabel! diff --git a/Tusker/Views/Notifications/FollowRequestNotificationTableViewCell.swift b/Tusker/Views/Notifications/FollowRequestNotificationTableViewCell.swift index b8865507ac..c83524ff37 100644 --- a/Tusker/Views/Notifications/FollowRequestNotificationTableViewCell.swift +++ b/Tusker/Views/Notifications/FollowRequestNotificationTableViewCell.swift @@ -12,7 +12,7 @@ import Pachyderm class FollowRequestNotificationTableViewCell: UITableViewCell { var delegate: TuskerNavigationDelegate? - var mastodonController: MastodonController? { delegate?.apiController } + var mastodonController: MastodonController! { delegate?.apiController } @IBOutlet weak var stackView: UIStackView! @IBOutlet weak var avatarImageView: UIImageView! @@ -90,7 +90,7 @@ class FollowRequestNotificationTableViewCell: UITableViewCell { @IBAction func rejectButtonPressed() { let request = Account.rejectFollowRequest(account) - mastodonController!.run(request) { (response) in + mastodonController.run(request) { (response) in guard case let .success(relationship, _) = response else { fatalError() } MastodonCache.add(relationship: relationship) DispatchQueue.main.async { @@ -107,7 +107,7 @@ class FollowRequestNotificationTableViewCell: UITableViewCell { @IBAction func acceptButtonPressed() { let request = Account.authorizeFollowRequest(account) - mastodonController!.run(request) { (response) in + mastodonController.run(request) { (response) in guard case let .success(relationship, _) = response else { fatalError() } MastodonCache.add(relationship: relationship) DispatchQueue.main.async { diff --git a/Tusker/Views/Profile Header/ProfileHeaderTableViewCell.swift b/Tusker/Views/Profile Header/ProfileHeaderTableViewCell.swift index c948f666de..c5dce34ec2 100644 --- a/Tusker/Views/Profile Header/ProfileHeaderTableViewCell.swift +++ b/Tusker/Views/Profile Header/ProfileHeaderTableViewCell.swift @@ -16,7 +16,7 @@ protocol ProfileHeaderTableViewCellDelegate: TuskerNavigationDelegate { class ProfileHeaderTableViewCell: UITableViewCell { var delegate: ProfileHeaderTableViewCellDelegate? - var mastodonController: MastodonController? { delegate?.apiController } + var mastodonController: MastodonController! { delegate?.apiController } @IBOutlet weak var headerImageView: UIImageView! @IBOutlet weak var avatarContainerView: UIView! @@ -83,7 +83,7 @@ class ProfileHeaderTableViewCell: UITableViewCell { noteLabel.setTextFromHtml(account.note) noteLabel.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) { followsYouLabel.isHidden = !relationship.followedBy diff --git a/Tusker/Views/Status/BaseStatusTableViewCell.swift b/Tusker/Views/Status/BaseStatusTableViewCell.swift index f9b985c52b..6d1071e387 100644 --- a/Tusker/Views/Status/BaseStatusTableViewCell.swift +++ b/Tusker/Views/Status/BaseStatusTableViewCell.swift @@ -21,7 +21,8 @@ class BaseStatusTableViewCell: UITableViewCell { contentLabel.navigationDelegate = delegate } } - var mastodonController: MastodonController? { delegate?.apiController } + var overrideMastodonController: MastodonController? + var mastodonController: MastodonController! { overrideMastodonController ?? delegate?.apiController } @IBOutlet weak var avatarImageView: UIImageView! @IBOutlet weak var displayNameLabel: UILabel! @@ -249,7 +250,7 @@ class BaseStatusTableViewCell: UITableViewCell { let realStatus: Status = status.reblog ?? status let request = (favorited ? Status.favourite : Status.unfavourite)(realStatus) - mastodonController!.run(request) { response in + mastodonController.run(request) { response in DispatchQueue.main.async { if case let .success(newStatus, _) = response { self.favorited = newStatus.favourited ?? false @@ -274,7 +275,7 @@ class BaseStatusTableViewCell: UITableViewCell { let realStatus: Status = status.reblog ?? status let request = (reblogged ? Status.reblog : Status.unreblog)(realStatus) - mastodonController!.run(request) { response in + mastodonController.run(request) { response in DispatchQueue.main.async { if case let .success(newStatus, _) = response { self.reblogged = newStatus.reblogged ?? false From 0255483f976d8c9f494a92f7ca6cdcc7dbfa5883 Mon Sep 17 00:00:00 2001 From: Shadowfacts Date: Sun, 5 Jan 2020 19:54:28 -0500 Subject: [PATCH 04/21] Make MastodonCache specific to each API controller See #16 --- .../FollowAccountActivity.swift | 2 +- .../UnfollowAccountActivity.swift | 2 +- .../BookmarkStatusActivity.swift | 2 +- .../Status Activities/PinStatusActivity.swift | 2 +- .../UnbookmarkStatusActivity.swift | 2 +- .../UnpinStatusActivity.swift | 2 +- Tusker/Controllers/MastodonController.swift | 4 +- Tusker/MastodonCache.swift | 62 ++++++++++--------- .../AccountListTableViewController.swift | 2 +- .../BookmarksTableViewController.swift | 16 ++--- .../Compose/ComposeViewController.swift | 8 +-- .../ConversationTableViewController.swift | 14 ++--- .../EditListAccountsViewController.swift | 2 +- .../NotificationsTableViewController.swift | 34 +++++----- .../Profile/ProfileTableViewController.swift | 30 ++++----- .../Search/SearchResultsViewController.swift | 12 ++-- ...ActionAccountListTableViewController.swift | 8 +-- .../TimelineTableViewController.swift | 12 ++-- Tusker/Screens/Utilities/Previewing.swift | 8 ++- Tusker/TuskerNavigationDelegate.swift | 4 +- .../Account Cell/AccountTableViewCell.swift | 4 +- .../AttachmentsContainerView.swift | 8 +-- ...ActionNotificationGroupTableViewCell.swift | 13 ++-- ...FollowNotificationGroupTableViewCell.swift | 10 +-- ...llowRequestNotificationTableViewCell.swift | 4 +- .../ProfileHeaderTableViewCell.swift | 8 +-- .../Status/BaseStatusTableViewCell.swift | 41 +++++++----- .../ConversationMainStatusTableViewCell.swift | 2 +- .../Status/TimelineStatusTableViewCell.swift | 32 ++++++---- Tusker/Views/StatusContentLabel.swift | 12 ++-- Tusker/XCallbackURL/XCBActions.swift | 14 ++--- 31 files changed, 200 insertions(+), 176 deletions(-) diff --git a/Tusker/Activities/Account Activities/FollowAccountActivity.swift b/Tusker/Activities/Account Activities/FollowAccountActivity.swift index 7beb3523ca..ae89aa7e1a 100644 --- a/Tusker/Activities/Account Activities/FollowAccountActivity.swift +++ b/Tusker/Activities/Account Activities/FollowAccountActivity.swift @@ -30,7 +30,7 @@ class FollowAccountActivity: AccountActivity { let request = Account.follow(account.id) 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/UnfollowAccountActivity.swift b/Tusker/Activities/Account Activities/UnfollowAccountActivity.swift index 344387d782..493923d7f9 100644 --- a/Tusker/Activities/Account Activities/UnfollowAccountActivity.swift +++ b/Tusker/Activities/Account Activities/UnfollowAccountActivity.swift @@ -30,7 +30,7 @@ class UnfollowAccountActivity: AccountActivity { let request = Account.unfollow(account.id) 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/Status Activities/BookmarkStatusActivity.swift b/Tusker/Activities/Status Activities/BookmarkStatusActivity.swift index 8c64f007c0..585f3471be 100644 --- a/Tusker/Activities/Status Activities/BookmarkStatusActivity.swift +++ b/Tusker/Activities/Status Activities/BookmarkStatusActivity.swift @@ -29,7 +29,7 @@ class BookmarkStatusActivity: StatusActivity { let request = Status.bookmark(status) 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 3714a453a4..40ef6cfd65 100644 --- a/Tusker/Activities/Status Activities/PinStatusActivity.swift +++ b/Tusker/Activities/Status Activities/PinStatusActivity.swift @@ -28,7 +28,7 @@ class PinStatusActivity: StatusActivity { let request = Status.pin(status) 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/UnbookmarkStatusActivity.swift b/Tusker/Activities/Status Activities/UnbookmarkStatusActivity.swift index be22c5e130..8cce299b95 100644 --- a/Tusker/Activities/Status Activities/UnbookmarkStatusActivity.swift +++ b/Tusker/Activities/Status Activities/UnbookmarkStatusActivity.swift @@ -29,7 +29,7 @@ class UnbookmarkStatusActivity: StatusActivity { let request = Status.unbookmark(status) 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 cbb717cbc6..a22df120d9 100644 --- a/Tusker/Activities/Status Activities/UnpinStatusActivity.swift +++ b/Tusker/Activities/Status Activities/UnpinStatusActivity.swift @@ -28,7 +28,7 @@ class UnpinStatusActivity: StatusActivity { let request = Status.unpin(status) 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/Controllers/MastodonController.swift b/Tusker/Controllers/MastodonController.swift index a916d77734..59aa82a6c8 100644 --- a/Tusker/Controllers/MastodonController.swift +++ b/Tusker/Controllers/MastodonController.swift @@ -14,6 +14,8 @@ class MastodonController { @available(*, deprecated, message: "Use dependency injection to obtain an instance") static let shared = MastodonController() + private(set) lazy var cache = MastodonCache(mastodonController: self) + private var client: Client! var account: Account! @@ -68,7 +70,7 @@ class MastodonController { 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) } } diff --git a/Tusker/MastodonCache.swift b/Tusker/MastodonCache.swift index 1ac5eddf57..0c22b4e259 100644 --- a/Tusker/MastodonCache.swift +++ b/Tusker/MastodonCache.swift @@ -12,22 +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() - static var mastodonController: MastodonController { .shared } + let 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 { @@ -38,66 +42,66 @@ class MastodonCache { statusSubject.send(status) } - static func status(for id: String, completion: @escaping (Status?) -> Void) { + func status(for id: String, completion: @escaping (Status?) -> Void) { 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) { + func account(for id: String, completion: @escaping (Account?) -> Void) { 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) { + func relationship(for id: String, completion: @escaping (Relationship?) -> Void) { let request = Client.getRelationships(accounts: [id]) mastodonController.run(request) { response in guard case let .success(relationships, _) = response, @@ -105,33 +109,33 @@ class MastodonCache { 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/Screens/Account List/AccountListTableViewController.swift b/Tusker/Screens/Account List/AccountListTableViewController.swift index 36fe72e25c..c001a3167e 100644 --- a/Tusker/Screens/Account List/AccountListTableViewController.swift +++ b/Tusker/Screens/Account List/AccountListTableViewController.swift @@ -53,8 +53,8 @@ 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 } diff --git a/Tusker/Screens/Bookmarks/BookmarksTableViewController.swift b/Tusker/Screens/Bookmarks/BookmarksTableViewController.swift index a4370c0501..ce83ee9af3 100644 --- a/Tusker/Screens/Bookmarks/BookmarksTableViewController.swift +++ b/Tusker/Screens/Bookmarks/BookmarksTableViewController.swift @@ -51,7 +51,7 @@ class BookmarksTableViewController: EnhancedTableViewController { 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 @@ -89,7 +89,7 @@ class BookmarksTableViewController: EnhancedTableViewController { 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) }) } } @@ -105,7 +105,7 @@ 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 } @@ -113,7 +113,7 @@ class BookmarksTableViewController: EnhancedTableViewController { let request = Status.unbookmark(status) 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) } } @@ -131,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) 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) } }) @@ -158,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) @@ -168,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 07a6816971..a6fc85d07a 100644 --- a/Tusker/Screens/Compose/ComposeViewController.swift +++ b/Tusker/Screens/Compose/ComposeViewController.swift @@ -70,7 +70,7 @@ class ComposeViewController: UIViewController { 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 if let mentioningAcct = mentioningAcct { accountsToMention = [mentioningAcct] @@ -146,13 +146,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) @@ -514,7 +514,7 @@ class ComposeViewController: UIViewController { 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) diff --git a/Tusker/Screens/Conversation/ConversationTableViewController.swift b/Tusker/Screens/Conversation/ConversationTableViewController.swift index 31bbc06147..b225840dd6 100644 --- a/Tusker/Screens/Conversation/ConversationTableViewController.swift +++ b/Tusker/Screens/Conversation/ConversationTableViewController.swift @@ -58,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.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 { @@ -104,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 } } @@ -169,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) @@ -179,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/Lists/EditListAccountsViewController.swift b/Tusker/Screens/Lists/EditListAccountsViewController.swift index 796c58f409..be46daf882 100644 --- a/Tusker/Screens/Lists/EditListAccountsViewController.swift +++ b/Tusker/Screens/Lists/EditListAccountsViewController.swift @@ -80,7 +80,7 @@ class EditListAccountsViewController: EnhancedTableViewController { self.nextRange = pagination?.older - MastodonCache.addAll(accounts: accounts) + self.mastodonController.cache.addAll(accounts: accounts) var snapshot = self.dataSource.snapshot() snapshot.deleteSections([.accounts]) diff --git a/Tusker/Screens/Notifications/NotificationsTableViewController.swift b/Tusker/Screens/Notifications/NotificationsTableViewController.swift index 4bee64b26a..ce87c5a5cb 100644 --- a/Tusker/Screens/Notifications/NotificationsTableViewController.swift +++ b/Tusker/Screens/Notifications/NotificationsTableViewController.swift @@ -67,9 +67,9 @@ class NotificationsTableViewController: EnhancedTableViewController { 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 @@ -92,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 } } @@ -135,9 +135,9 @@ class NotificationsTableViewController: EnhancedTableViewController { 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 } @@ -207,9 +207,9 @@ class NotificationsTableViewController: EnhancedTableViewController { 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 }) self.newer = pagination?.newer @@ -237,7 +237,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) } } @@ -246,7 +246,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/Profile/ProfileTableViewController.swift b/Tusker/Screens/Profile/ProfileTableViewController.swift index e6b1d4b36b..2e58f49eb3 100644 --- a/Tusker/Screens/Profile/ProfileTableViewController.swift +++ b/Tusker/Screens/Profile/ProfileTableViewController.swift @@ -73,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 @@ -112,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 @@ -128,7 +128,7 @@ class ProfileTableViewController: EnhancedTableViewController { } @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!)") } navigationItem.title = account.realDisplayName } @@ -138,7 +138,7 @@ class ProfileTableViewController: EnhancedTableViewController { } func sendMessageMentioning() { - 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!)") } let vc = UINavigationController(rootViewController: ComposeViewController(mentioningAcct: account.acct, mastodonController: mastodonController)) present(vc, animated: true) } @@ -152,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 { @@ -172,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 } } @@ -193,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 @@ -219,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) self.newer = pagination?.newer @@ -248,9 +248,9 @@ extension ProfileTableViewController: StatusTableViewCellDelegate { extension ProfileTableViewController: ProfileHeaderTableViewCellDelegate { func showMoreOptions() { - 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()] @@ -272,7 +272,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) @@ -283,7 +283,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 45e97c4bf2..9c973461de 100644 --- a/Tusker/Screens/Search/SearchResultsViewController.swift +++ b/Tusker/Screens/Search/SearchResultsViewController.swift @@ -65,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 } }) @@ -136,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]) @@ -145,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) } diff --git a/Tusker/Screens/Status Action Account List/StatusActionAccountListTableViewController.swift b/Tusker/Screens/Status Action Account List/StatusActionAccountListTableViewController.swift index e0d9195cea..dd60518c96 100644 --- a/Tusker/Screens/Status Action Account List/StatusActionAccountListTableViewController.swift +++ b/Tusker/Screens/Status Action Account List/StatusActionAccountListTableViewController.swift @@ -73,7 +73,7 @@ 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)") } @@ -82,7 +82,7 @@ class StatusActionAccountListTableViewController: EnhancedTableViewController { let request = actionType == .favorite ? Status.getFavourites(status) : Status.getReblogs(status) 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 @@ -116,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)") diff --git a/Tusker/Screens/Timeline/TimelineTableViewController.swift b/Tusker/Screens/Timeline/TimelineTableViewController.swift index 93362925c6..c94b45d2c0 100644 --- a/Tusker/Screens/Timeline/TimelineTableViewController.swift +++ b/Tusker/Screens/Timeline/TimelineTableViewController.swift @@ -65,7 +65,7 @@ class TimelineTableViewController: EnhancedTableViewController { 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 @@ -87,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 } @@ -104,7 +104,7 @@ class TimelineTableViewController: EnhancedTableViewController { 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) }) } } @@ -129,7 +129,7 @@ class TimelineTableViewController: EnhancedTableViewController { mastodonController.run(request) { response in guard case let .success(newStatuses, pagination) = response else { fatalError() } self.newer = pagination?.newer - MastodonCache.addAll(statuses: newStatuses) + self.mastodonController.cache.addAll(statuses: newStatuses) self.timelineSegments[0].insert(contentsOf: newStatuses.map { ($0.id, .unknown) }, at: 0) DispatchQueue.main.async { self.refreshControl?.endRefreshing() @@ -159,7 +159,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) @@ -169,7 +169,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/Utilities/Previewing.swift b/Tusker/Screens/Utilities/Previewing.swift index 2c20e4e7e9..346d78a379 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) -> [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) -> [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/TuskerNavigationDelegate.swift b/Tusker/TuskerNavigationDelegate.swift index 2530ed1709..f7b04f20c2 100644 --- a/Tusker/TuskerNavigationDelegate.swift +++ b/Tusker/TuskerNavigationDelegate.swift @@ -194,7 +194,7 @@ extension TuskerNavigationDelegate where Self: UIViewController { } private func moreOptions(forStatus statusID: String) -> UIViewController { - 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()] @@ -213,7 +213,7 @@ extension TuskerNavigationDelegate where Self: UIViewController { } private func moreOptions(forAccount accountID: String) -> UIViewController { - 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) } diff --git a/Tusker/Views/Account Cell/AccountTableViewCell.swift b/Tusker/Views/Account Cell/AccountTableViewCell.swift index 861b6b8a49..00d9234628 100644 --- a/Tusker/Views/Account Cell/AccountTableViewCell.swift +++ b/Tusker/Views/Account Cell/AccountTableViewCell.swift @@ -32,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 @@ -40,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)") } diff --git a/Tusker/Views/Attachments/AttachmentsContainerView.swift b/Tusker/Views/Attachments/AttachmentsContainerView.swift index 015f069ea7..5ca7deb9bf 100644 --- a/Tusker/Views/Attachments/AttachmentsContainerView.swift +++ b/Tusker/Views/Attachments/AttachmentsContainerView.swift @@ -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/Notifications/ActionNotificationGroupTableViewCell.swift b/Tusker/Views/Notifications/ActionNotificationGroupTableViewCell.swift index 33483d06f5..cedde5f6b3 100644 --- a/Tusker/Views/Notifications/ActionNotificationGroupTableViewCell.swift +++ b/Tusker/Views/Notifications/ActionNotificationGroupTableViewCell.swift @@ -13,6 +13,7 @@ import SwiftSoup class ActionNotificationGroupTableViewCell: UITableViewCell { var delegate: TuskerNavigationDelegate? + var mastodonController: MastodonController! { delegate?.apiController } @IBOutlet weak var actionImageView: UIImageView! @IBOutlet weak var actionAvatarStackView: UIStackView! @@ -33,7 +34,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 +48,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 +63,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 +94,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") } @@ -155,7 +156,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 +177,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 b77cb15208..0c2e62cc23 100644 --- a/Tusker/Views/Notifications/FollowNotificationGroupTableViewCell.swift +++ b/Tusker/Views/Notifications/FollowNotificationGroupTableViewCell.swift @@ -29,7 +29,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 { @@ -40,7 +40,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() @@ -82,7 +82,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") } @@ -118,7 +118,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 @@ -136,7 +136,7 @@ extension FollowNotificationGroupTableViewCell: MenuPreviewProvider { 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!, mastodonController: mastodonController) } else { diff --git a/Tusker/Views/Notifications/FollowRequestNotificationTableViewCell.swift b/Tusker/Views/Notifications/FollowRequestNotificationTableViewCell.swift index c83524ff37..1674c639a5 100644 --- a/Tusker/Views/Notifications/FollowRequestNotificationTableViewCell.swift +++ b/Tusker/Views/Notifications/FollowRequestNotificationTableViewCell.swift @@ -92,7 +92,7 @@ class FollowRequestNotificationTableViewCell: UITableViewCell { let request = Account.rejectFollowRequest(account) 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 @@ -109,7 +109,7 @@ class FollowRequestNotificationTableViewCell: UITableViewCell { let request = Account.authorizeFollowRequest(account) 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 diff --git a/Tusker/Views/Profile Header/ProfileHeaderTableViewCell.swift b/Tusker/Views/Profile Header/ProfileHeaderTableViewCell.swift index c5dce34ec2..2b246c2f79 100644 --- a/Tusker/Views/Profile Header/ProfileHeaderTableViewCell.swift +++ b/Tusker/Views/Profile Header/ProfileHeaderTableViewCell.swift @@ -56,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() @@ -85,10 +85,10 @@ class ProfileHeaderTableViewCell: UITableViewCell { 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) } @@ -121,7 +121,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 6d1071e387..be586211f9 100644 --- a/Tusker/Views/Status/BaseStatusTableViewCell.swift +++ b/Tusker/Views/Status/BaseStatusTableViewCell.swift @@ -95,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 { $0.id == self.statusID } + .receive(on: DispatchQueue.main) + .sink(receiveValue: updateStatusState(status:)) + } - 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 { $0.id == self.accountID } + .receive(on: DispatchQueue.main) + .sink(receiveValue: updateUI(account:)) + } } 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 @@ -183,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() { @@ -243,7 +252,7 @@ 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 @@ -254,7 +263,7 @@ class BaseStatusTableViewCell: UITableViewCell { 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 @@ -268,7 +277,7 @@ 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 @@ -279,7 +288,7 @@ class BaseStatusTableViewCell: UITableViewCell { 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 @@ -306,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) } diff --git a/Tusker/Views/Status/ConversationMainStatusTableViewCell.swift b/Tusker/Views/Status/ConversationMainStatusTableViewCell.swift index 184fdb5a49..6052ee9541 100644 --- a/Tusker/Views/Status/ConversationMainStatusTableViewCell.swift +++ b/Tusker/Views/Status/ConversationMainStatusTableViewCell.swift @@ -38,7 +38,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 a42ddeb24a..b2f926dd86 100644 --- a/Tusker/Views/Status/TimelineStatusTableViewCell.swift +++ b/Tusker/Views/Status/TimelineStatusTableViewCell.swift @@ -41,19 +41,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 { $0.id == self.rebloggerID } + .receive(on: DispatchQueue.main) + .sink(receiveValue: updateRebloggerLabel(reblogger:)) + } + } + 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 +84,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 +94,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()) @@ -144,7 +150,7 @@ extension TimelineStatusTableViewCell: TableViewSwipeActionProvider { func leadingSwipeActionsConfiguration() -> UISwipeActionsConfiguration? { guard let mastodonController = mastodonController else { return nil } - 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 favoriteTitle: String let favoriteRequest: Request @@ -167,7 +173,7 @@ extension TimelineStatusTableViewCell: TableViewSwipeActionProvider { return } completion(true) - MastodonCache.add(status: status) + mastodonController.cache.add(status: status) } }) } @@ -194,7 +200,7 @@ extension TimelineStatusTableViewCell: TableViewSwipeActionProvider { return } completion(true) - MastodonCache.add(status: status) + mastodonController.cache.add(status: status) } }) } diff --git a/Tusker/Views/StatusContentLabel.swift b/Tusker/Views/StatusContentLabel.swift index 5c113e74b8..17bb3c691d 100644 --- a/Tusker/Views/StatusContentLabel.swift +++ b/Tusker/Views/StatusContentLabel.swift @@ -11,10 +11,12 @@ import Pachyderm class StatusContentLabel: ContentLabel { + var mastodonController: MastodonController? { navigationDelegate?.apiController } + var statusID: String? { didSet { - guard let statusID = statusID else { return } - guard let status = MastodonCache.status(for: statusID) else { fatalError("Can't set StatusContentLabel text without cached status \(statusID)") } + guard let statusID = statusID, let mastodonController = mastodonController else { return } + guard let status = mastodonController.cache.status(for: statusID) else { fatalError("Can't set StatusContentLabel text without cached status \(statusID)") } setTextFromHtml(status.content) setEmojis(status.emojis) } @@ -23,7 +25,8 @@ class StatusContentLabel: ContentLabel { 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 @@ -37,7 +40,8 @@ class StatusContentLabel: ContentLabel { 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 ead66b9371..426c4f559b 100644 --- a/Tusker/XCallbackURL/XCBActions.swift +++ b/Tusker/XCallbackURL/XCBActions.swift @@ -33,7 +33,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 { @@ -47,7 +47,7 @@ struct XCBActions { 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: [ @@ -64,7 +64,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 { @@ -78,7 +78,7 @@ struct XCBActions { 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: [ @@ -96,7 +96,7 @@ struct XCBActions { 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: [ @@ -203,7 +203,7 @@ struct XCBActions { func performAction(status: Status, completion: ((Status) -> Void)?) { 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, @@ -289,7 +289,7 @@ struct XCBActions { let request = Account.follow(account.id) 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 ]) From 8dba15ca179b10602ce1cc38afaadfe6f52ba27c Mon Sep 17 00:00:00 2001 From: Shadowfacts Date: Tue, 7 Jan 2020 18:39:19 -0500 Subject: [PATCH 05/21] Switch to scene-based lifecycle events See #16 --- Tusker.xcodeproj/project.pbxproj | 4 + Tusker/AppDelegate.swift | 110 ---------------- Tusker/Info.plist | 61 +++++---- Tusker/SceneDelegate.swift | 138 +++++++++++++++++++++ Tusker/Shortcuts/AppShortcutItems.swift | 4 +- Tusker/Shortcuts/UserActivityManager.swift | 7 +- Tusker/XCallbackURL/XCBActions.swift | 5 +- 7 files changed, 195 insertions(+), 134 deletions(-) create mode 100644 Tusker/SceneDelegate.swift diff --git a/Tusker.xcodeproj/project.pbxproj b/Tusker.xcodeproj/project.pbxproj index 44d85d1e69..adf1defcec 100644 --- a/Tusker.xcodeproj/project.pbxproj +++ b/Tusker.xcodeproj/project.pbxproj @@ -174,6 +174,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 */; }; @@ -446,6 +447,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 = ""; }; @@ -1179,6 +1181,7 @@ isa = PBXGroup; children = ( D6D4DDCF212518A000E1C4BB /* AppDelegate.swift */, + D6AC956623C4347E008C9946 /* SceneDelegate.swift */, D64D0AAC2128D88B005A6F37 /* LocalData.swift */, D627FF75217E923E00CC0648 /* DraftsManager.swift */, D6945C2E23AC47C3005C403C /* SavedHashtagsManager.swift */, @@ -1640,6 +1643,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 */, D6028B9B2150811100F223B9 /* MastodonCache.swift in Sources */, D6A3BC802321B7E600FD64D5 /* FollowNotificationGroupTableViewCell.swift in Sources */, diff --git a/Tusker/AppDelegate.swift b/Tusker/AppDelegate.swift index 6f7521aa9b..1854af623d 100644 --- a/Tusker/AppDelegate.swift +++ b/Tusker/AppDelegate.swift @@ -11,119 +11,9 @@ import UIKit @UIApplicationMain class AppDelegate: UIResponder, UIApplicationDelegate { - var window: UIWindow? - - let mastodonController = MastodonController.shared - 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(mastodonController: mastodonController) - 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/Info.plist b/Tusker/Info.plist index 937a979e62..48f051d6f5 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/SceneDelegate.swift b/Tusker/SceneDelegate.swift new file mode 100644 index 0000000000..5e3d7ac2a4 --- /dev/null +++ b/Tusker/SceneDelegate.swift @@ -0,0 +1,138 @@ +// +// SceneDelegate.swift +// Tusker +// +// Created by Shadowfacts on 1/6/20. +// Copyright © 2020 Shadowfacts. All rights reserved. +// + +import UIKit + +class SceneDelegate: UIResponder, UIWindowSceneDelegate { + + var window: UIWindow? + + let mastodonController = MastodonController.shared + + 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 { + showAppUI() + } else { + showOnboardingUI() + } + + window!.makeKeyAndVisible() + + NotificationCenter.default.addObserver(self, selector: #selector(onUserLoggedOut), name: .userLoggedOut, object: nil) + 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 showAppUI() { + mastodonController.createClient() + 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 onUserLoggedOut() { + showOnboardingUI() + } + + @objc func themePrefChanged() { + window?.overrideUserInterfaceStyle = Preferences.shared.theme + } + +} + +extension SceneDelegate: OnboardingViewControllerDelegate { + func didFinishOnboarding() { + LocalData.shared.onboardingComplete = true + showAppUI() + } +} diff --git a/Tusker/Shortcuts/AppShortcutItems.swift b/Tusker/Shortcuts/AppShortcutItems.swift index 0e9dea8062..d0efdffa8f 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 5701e1f9c4..4b745f404b 100644 --- a/Tusker/Shortcuts/UserActivityManager.swift +++ b/Tusker/Shortcuts/UserActivityManager.swift @@ -18,7 +18,10 @@ class UserActivityManager { private static var mastodonController: MastodonController { .shared } 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) { @@ -63,6 +66,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) } } @@ -82,6 +86,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) } } diff --git a/Tusker/XCallbackURL/XCBActions.swift b/Tusker/XCallbackURL/XCBActions.swift index 426c4f559b..ad2feb5f6a 100644 --- a/Tusker/XCallbackURL/XCBActions.swift +++ b/Tusker/XCallbackURL/XCBActions.swift @@ -16,7 +16,10 @@ struct XCBActions { private static var mastodonController: MastodonController { .shared } 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 show(_ vc: UIViewController) { From 3928b2e88acc3061d3ff0d9e8d6e23047ec7ac81 Mon Sep 17 00:00:00 2001 From: Shadowfacts Date: Tue, 7 Jan 2020 21:29:15 -0500 Subject: [PATCH 06/21] Store an array of logged-in accounts internally, get the active MastodonController from the current UIScene See #16 --- Tusker.xcodeproj/project.pbxproj | 8 ++ Tusker/Activities/MastodonActivity.swift | 3 +- Tusker/Controllers/MastodonController.swift | 66 ++++++---- Tusker/Extensions/UIApplication+Scenes.swift | 25 ++++ .../UISceneSession+MastodonController.swift | 32 +++++ Tusker/LocalData.swift | 120 +++++++++++------- Tusker/SceneDelegate.swift | 15 ++- .../Onboarding/OnboardingViewController.swift | 25 ++-- .../PreferencesNavigationController.swift | 5 +- .../Screens/Preferences/PreferencesView.swift | 10 +- .../MyProfileTableViewController.swift | 2 +- .../InstanceTimelineViewController.swift | 5 +- Tusker/Shortcuts/UserActivityManager.swift | 5 +- Tusker/XCallbackURL/XCBActions.swift | 6 +- 14 files changed, 225 insertions(+), 102 deletions(-) create mode 100644 Tusker/Extensions/UIApplication+Scenes.swift create mode 100644 Tusker/Extensions/UISceneSession+MastodonController.swift diff --git a/Tusker.xcodeproj/project.pbxproj b/Tusker.xcodeproj/project.pbxproj index adf1defcec..7fe4197314 100644 --- a/Tusker.xcodeproj/project.pbxproj +++ b/Tusker.xcodeproj/project.pbxproj @@ -217,6 +217,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 */; }; @@ -494,6 +496,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 = ""; }; @@ -966,6 +970,8 @@ D67C57AE21E28EAD00C3118B /* Array+Uniques.swift */, 0450531E22B0097E00100BA2 /* Timline+UI.swift */, D66A77BA233838DC0058F1EC /* UIFont+Traits.swift */, + D6EBF01423C55C0900AE061B /* UIApplication+Scenes.swift */, + D6EBF01623C55E0D00AE061B /* UISceneSession+MastodonController.swift */, ); path = Extensions; sourceTree = ""; @@ -1620,6 +1626,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 */, @@ -1680,6 +1687,7 @@ 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 */, diff --git a/Tusker/Activities/MastodonActivity.swift b/Tusker/Activities/MastodonActivity.swift index 32275951e6..25fd5cc777 100644 --- a/Tusker/Activities/MastodonActivity.swift +++ b/Tusker/Activities/MastodonActivity.swift @@ -10,6 +10,7 @@ import UIKit class MastodonActivity: UIActivity { var mastodonController: MastodonController { - MastodonController.shared + let scene = UIApplication.shared.activeOrBackgroundScene! + return scene.session.mastodonController! } } diff --git a/Tusker/Controllers/MastodonController.swift b/Tusker/Controllers/MastodonController.swift index 59aa82a6c8..5e2ae194c3 100644 --- a/Tusker/Controllers/MastodonController.swift +++ b/Tusker/Controllers/MastodonController.swift @@ -10,55 +10,67 @@ import Foundation import Pachyderm class MastodonController { + + static private(set) var all = [LocalData.UserAccountInfo: MastodonController]() - @available(*, deprecated, message: "Use dependency injection to obtain an instance") - static let shared = MastodonController() + @available(*, message: "do something less dumb") + static var first: MastodonController { all.first!.value } + + 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 + } + } private(set) lazy var cache = MastodonCache(mastodonController: self) - private var client: Client! + let instanceURL: URL + private(set) var accountInfo: LocalData.UserAccountInfo? + + let client: Client! var account: Account! var instance: Instance! - - var accessToken: String? { - client?.accessToken - } - - func createClient(instanceURL: URL = LocalData.shared.instanceURL!) { - client = Client(baseURL: instanceURL) - - if instanceURL == LocalData.shared.instanceURL { - client.clientID = LocalData.shared.clientID - client.clientSecret = LocalData.shared.clientSecret - client.accessToken = LocalData.shared.accessToken - } + + 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 () -> Void) { - guard LocalData.shared.clientID == nil, - LocalData.shared.clientSecret == nil else { - 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) } } - 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) } } diff --git a/Tusker/Extensions/UIApplication+Scenes.swift b/Tusker/Extensions/UIApplication+Scenes.swift new file mode 100644 index 0000000000..ce5ab857ee --- /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 0000000000..efa4e638a0 --- /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/LocalData.swift b/Tusker/LocalData.swift index f8ce0b1075..6dad3f5b8b 100644 --- a/Tusker/LocalData.swift +++ b/Tusker/LocalData.swift @@ -18,65 +18,99 @@ 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( + 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 instanceURL = info["instanceURL"], + let url = URL(string: instanceURL), + let id = info["clientID"], + let secret = info["clientSecret"], + let username = info["username"], + let accessToken = info["accessToken"] else { + return nil + } + return UserAccountInfo(instanceURL: url, clientID: id, clientSecret: secret, username: username, accessToken: accessToken) + } + } else { + return [] + } + } + set { + let array = newValue.map { (info) in + return [ + "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" + var mostRecentAccount: String? { + get { + return defaults.string(forKey: mostRecentAccountKey) + } + set { + 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 id: 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 info = UserAccountInfo(instanceURL: url, clientID: id, 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) { + } - private let clientSecretKey = "clientSecret" - var clientSecret: String? { - get { - return defaults.string(forKey: clientSecretKey) - } - set { - defaults.set(newValue, forKey: clientSecretKey) + func getMostRecentAccount() -> UserAccountInfo? { + if let accessToken = mostRecentAccount { + return accounts.first { $0.accessToken == accessToken } + } else { + return nil } } - - private let accessTokenKey = "accessToken" - var accessToken: String? { - get { - return defaults.string(forKey: accessTokenKey) - } - set { - defaults.set(newValue, forKey: accessTokenKey) - } + +} + +extension LocalData { + struct UserAccountInfo: Equatable, Hashable { + let instanceURL: URL + let clientID: String + let clientSecret: String + let username: String + let accessToken: String } } diff --git a/Tusker/SceneDelegate.swift b/Tusker/SceneDelegate.swift index 5e3d7ac2a4..67c8c15245 100644 --- a/Tusker/SceneDelegate.swift +++ b/Tusker/SceneDelegate.swift @@ -7,12 +7,13 @@ // import UIKit +import Pachyderm class SceneDelegate: UIResponder, UIWindowSceneDelegate { var window: UIWindow? - let mastodonController = MastodonController.shared +// let mastodonController = MastodonController.shared 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`. @@ -23,6 +24,11 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate { 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() @@ -106,7 +112,7 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate { } func showAppUI() { - mastodonController.createClient() + let mastodonController = window!.windowScene!.session.mastodonController! mastodonController.getOwnAccount() mastodonController.getOwnInstance() @@ -131,8 +137,9 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate { } extension SceneDelegate: OnboardingViewControllerDelegate { - func didFinishOnboarding() { - LocalData.shared.onboardingComplete = true + func didFinishOnboarding(account: LocalData.UserAccountInfo) { + LocalData.shared.mostRecentAccount = account.accessToken + window!.windowScene!.session.mastodonController = MastodonController.getForAccount(account) showAppUI() } } diff --git a/Tusker/Screens/Onboarding/OnboardingViewController.swift b/Tusker/Screens/Onboarding/OnboardingViewController.swift index 21daac5eb9..e799e2a6a6 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,16 +44,13 @@ class OnboardingViewController: UINavigationController { } extension OnboardingViewController: InstanceSelectorTableViewControllerDelegate { - func didSelectInstance(url: URL) { - LocalData.shared.instanceURL = url - let mastodonController = MastodonController.shared - 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), @@ -70,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 + let accountInfo = LocalData.shared.addAccount(instanceURL: instanceURL, clientID: clientID, clientSecret: clientSecret, username: account.username, accessToken: accessToken) + + DispatchQueue.main.async { + self.onboardingDelegate?.didFinishOnboarding(account: accountInfo) + } } } } diff --git a/Tusker/Screens/Preferences/PreferencesNavigationController.swift b/Tusker/Screens/Preferences/PreferencesNavigationController.swift index 0ac49c04c0..db3a934923 100644 --- a/Tusker/Screens/Preferences/PreferencesNavigationController.swift +++ b/Tusker/Screens/Preferences/PreferencesNavigationController.swift @@ -11,8 +11,9 @@ import SwiftUI class PreferencesNavigationController: UINavigationController { - init() { - let hostingController = UIHostingController(rootView: PreferencesView()) + init(mastodonController: MastodonController) { + let view = PreferencesView(currentAccount: mastodonController.accountInfo!) + let hostingController = UIHostingController(rootView: view) super.init(rootViewController: hostingController) hostingController.navigationItem.rightBarButtonItem = UIBarButtonItem(barButtonSystemItem: .done, target: self, action: #selector(donePressed)) } diff --git a/Tusker/Screens/Preferences/PreferencesView.swift b/Tusker/Screens/Preferences/PreferencesView.swift index 9df96be2fc..8f2078faaf 100644 --- a/Tusker/Screens/Preferences/PreferencesView.swift +++ b/Tusker/Screens/Preferences/PreferencesView.swift @@ -8,6 +8,7 @@ import SwiftUI struct PreferencesView : View { + var currentAccount: LocalData.UserAccountInfo @State private var showingLogoutConfirmation = false var body: some View { @@ -49,11 +50,7 @@ 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 + LocalData.shared.removeAccount(currentAccount) NotificationCenter.default.post(name: .userLoggedOut, object: nil) } } @@ -61,7 +58,8 @@ struct PreferencesView : View { #if DEBUG struct PreferencesView_Previews : PreviewProvider { static var previews: some View { - PreferencesView() + let account = LocalData.UserAccountInfo(instanceURL: URL(string: "https://mastodon.social")!, clientID: "clientID", clientSecret: "clientSecret", username: "example", accessToken: "accessToken") + return PreferencesView(currentAccount: account) } } #endif diff --git a/Tusker/Screens/Profile/MyProfileTableViewController.swift b/Tusker/Screens/Profile/MyProfileTableViewController.swift index 7fef87064a..09ac78842c 100644 --- a/Tusker/Screens/Profile/MyProfileTableViewController.swift +++ b/Tusker/Screens/Profile/MyProfileTableViewController.swift @@ -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/Timeline/InstanceTimelineViewController.swift b/Tusker/Screens/Timeline/InstanceTimelineViewController.swift index df598ab8be..ff809ba748 100644 --- a/Tusker/Screens/Timeline/InstanceTimelineViewController.swift +++ b/Tusker/Screens/Timeline/InstanceTimelineViewController.swift @@ -31,9 +31,8 @@ class InstanceTimelineViewController: TimelineTableViewController { init(for url: URL) { self.instanceURL = url - let mastodonController = MastodonController() - mastodonController.createClient(instanceURL: url) - + let mastodonController = MastodonController(instanceURL: url) + super.init(for: .instance(instanceURL: url), mastodonController: mastodonController) } diff --git a/Tusker/Shortcuts/UserActivityManager.swift b/Tusker/Shortcuts/UserActivityManager.swift index 4b745f404b..4c18166321 100644 --- a/Tusker/Shortcuts/UserActivityManager.swift +++ b/Tusker/Shortcuts/UserActivityManager.swift @@ -15,7 +15,10 @@ class UserActivityManager { private static let encoder = PropertyListEncoder() private static let decoder = PropertyListDecoder() - private static var mastodonController: MastodonController { .shared } + private static var mastodonController: MastodonController { + let scene = UIApplication.shared.activeOrBackgroundScene! + return scene.session.mastodonController! + } private static func getMainTabBarController() -> MainTabBarViewController { let scene = UIApplication.shared.connectedScenes.compactMap { $0 as? UIWindowScene }.first! diff --git a/Tusker/XCallbackURL/XCBActions.swift b/Tusker/XCallbackURL/XCBActions.swift index ad2feb5f6a..0731fd061e 100644 --- a/Tusker/XCallbackURL/XCBActions.swift +++ b/Tusker/XCallbackURL/XCBActions.swift @@ -13,13 +13,15 @@ import SwiftSoup struct XCBActions { // MARK: - Utils - private static var mastodonController: MastodonController { .shared } + private static var mastodonController: MastodonController { + let scene = UIApplication.shared.activeOrBackgroundScene! + return scene.session.mastodonController! + } private static func getMainTabBarController() -> MainTabBarViewController { let scene = UIApplication.shared.connectedScenes.compactMap { $0 as? UIWindowScene }.first! let window = scene.windows.first { $0.isKeyWindow }! return window.rootViewController as! MainTabBarViewController -// return (UIApplication.shared.delegate as! AppDelegate).window!.rootViewController as! MainTabBarViewController } private static func show(_ vc: UIViewController) { From 08c84688cffe487edd5cc2674f252bdc417df3aa Mon Sep 17 00:00:00 2001 From: Shadowfacts Date: Sat, 18 Jan 2020 19:49:10 -0500 Subject: [PATCH 07/21] Change recommended instance selector to store categories as strings instead of enum Additional categories can be added, which would cause a crash when decoding. As the category isn't used for anything, storing it as an enum value is not necessary. --- Pachyderm/Utilities/InstanceSelector.swift | 19 +------------------ .../Instance Cell/InstanceTableViewCell.swift | 2 +- 2 files changed, 2 insertions(+), 19 deletions(-) diff --git a/Pachyderm/Utilities/InstanceSelector.swift b/Pachyderm/Utilities/InstanceSelector.swift index e0341f450f..dd2ce0645a 100644 --- a/Pachyderm/Utilities/InstanceSelector.swift +++ b/Pachyderm/Utilities/InstanceSelector.swift @@ -51,7 +51,7 @@ public extension InstanceSelector { public let description: String public let proxiedThumbnailURL: URL public let language: String - public let category: Category + public let category: String enum CodingKeys: String, CodingKey { case domain @@ -62,20 +62,3 @@ public extension InstanceSelector { } } } - -public extension InstanceSelector { - enum Category: String, Codable { - // source: https://source.joinmastodon.org/mastodon/joinmastodon/blob/master/src/Wizard.js#L108 - case general - case regional - case art - case journalism - case activism - case lgbt - case games - case tech - case adult - case furry - case food - } -} diff --git a/Tusker/Views/Instance Cell/InstanceTableViewCell.swift b/Tusker/Views/Instance Cell/InstanceTableViewCell.swift index 920e068f41..161015b3f7 100644 --- a/Tusker/Views/Instance Cell/InstanceTableViewCell.swift +++ b/Tusker/Views/Instance Cell/InstanceTableViewCell.swift @@ -36,7 +36,7 @@ class InstanceTableViewCell: UITableViewCell { self.instance = nil domainLabel.text = instance.domain - adultLabel.isHidden = instance.category != .adult + adultLabel.isHidden = instance.category != "adult" descriptionTextView.setTextFromHtml(instance.description) updateThumbnail(url: instance.proxiedThumbnailURL) } From 863867c5226b650ec140be3dc6b4252ac83be719 Mon Sep 17 00:00:00 2001 From: Shadowfacts Date: Sat, 18 Jan 2020 22:43:10 -0500 Subject: [PATCH 08/21] Add logging in to additional accounts and switching accounts via Preferences See #16 --- Tusker/LocalData.swift | 5 ++- Tusker/SceneDelegate.swift | 12 +++--- .../PreferencesNavigationController.swift | 39 +++++++++++++++++- .../Screens/Preferences/PreferencesView.swift | 41 +++++++++++++++---- .../Profile/ProfileTableViewController.swift | 2 +- 5 files changed, 82 insertions(+), 17 deletions(-) diff --git a/Tusker/LocalData.swift b/Tusker/LocalData.swift index 6dad3f5b8b..f3954fdd04 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() @@ -52,6 +53,7 @@ class LocalData { } } set { + objectWillChange.send() let array = newValue.map { (info) in return [ "instanceURL": info.instanceURL.absoluteString, @@ -71,6 +73,7 @@ class LocalData { return defaults.string(forKey: mostRecentAccountKey) } set { + objectWillChange.send() defaults.set(newValue, forKey: mostRecentAccountKey) } } diff --git a/Tusker/SceneDelegate.swift b/Tusker/SceneDelegate.swift index 67c8c15245..d4b939fb18 100644 --- a/Tusker/SceneDelegate.swift +++ b/Tusker/SceneDelegate.swift @@ -13,8 +13,6 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate { var window: UIWindow? -// let mastodonController = MastodonController.shared - 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. @@ -111,6 +109,12 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate { DraftsManager.save() } + func activateAccount(_ account: LocalData.UserAccountInfo) { + LocalData.shared.mostRecentAccount = account.accessToken + window!.windowScene!.session.mastodonController = MastodonController.getForAccount(account) + showAppUI() + } + func showAppUI() { let mastodonController = window!.windowScene!.session.mastodonController! mastodonController.getOwnAccount() @@ -138,8 +142,6 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate { extension SceneDelegate: OnboardingViewControllerDelegate { func didFinishOnboarding(account: LocalData.UserAccountInfo) { - LocalData.shared.mostRecentAccount = account.accessToken - window!.windowScene!.session.mastodonController = MastodonController.getForAccount(account) - showAppUI() + activateAccount(account) } } diff --git a/Tusker/Screens/Preferences/PreferencesNavigationController.swift b/Tusker/Screens/Preferences/PreferencesNavigationController.swift index db3a934923..c4a6c12ee2 100644 --- a/Tusker/Screens/Preferences/PreferencesNavigationController.swift +++ b/Tusker/Screens/Preferences/PreferencesNavigationController.swift @@ -12,7 +12,7 @@ import SwiftUI class PreferencesNavigationController: UINavigationController { init(mastodonController: MastodonController) { - let view = PreferencesView(currentAccount: mastodonController.accountInfo!) + let view = PreferencesView() let hostingController = UIHostingController(rootView: view) super.init(rootViewController: hostingController) hostingController.navigationItem.rightBarButtonItem = UIBarButtonItem(barButtonSystemItem: .done, target: self, action: #selector(donePressed)) @@ -21,6 +21,13 @@ 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) + } override func viewWillDisappear(_ animated: Bool) { super.viewWillDisappear(animated) @@ -32,5 +39,35 @@ class PreferencesNavigationController: UINavigationController { @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) + } + + @objc func activateAccount(_ notification: Notification) { + let account = notification.userInfo!["account"] as! LocalData.UserAccountInfo + let sceneDelegate = self.view.window!.windowScene!.delegate as! SceneDelegate + dismiss(animated: true) { + sceneDelegate.activateAccount(account) + } + } } + +extension Notification.Name { + static let addAccount = Notification.Name("Tusker.addAccount") + static let activateAccount = Notification.Name("Tusker.activateAccount") +} + +extension PreferencesNavigationController: OnboardingViewControllerDelegate { + func didFinishOnboarding(account: LocalData.UserAccountInfo) { + LocalData.shared.mostRecentAccount = account.accessToken + } +} diff --git a/Tusker/Screens/Preferences/PreferencesView.swift b/Tusker/Screens/Preferences/PreferencesView.swift index 8f2078faaf..38cbf99366 100644 --- a/Tusker/Screens/Preferences/PreferencesView.swift +++ b/Tusker/Screens/Preferences/PreferencesView.swift @@ -7,8 +7,8 @@ import SwiftUI -struct PreferencesView : View { - var currentAccount: LocalData.UserAccountInfo +struct PreferencesView: View { + @ObservedObject var localData = LocalData.shared @State private var showingLogoutConfirmation = false var body: some View { @@ -16,12 +16,35 @@ struct PreferencesView : View { // 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.accessToken == self.localData.mostRecentAccount { + 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.mostRecentAccount != 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()) + } } } @@ -50,7 +73,8 @@ struct PreferencesView : View { } func logoutPressed() { - LocalData.shared.removeAccount(currentAccount) +// LocalData.shared.removeAccount(currentAccount) + localData.removeAccount(localData.getMostRecentAccount()!) NotificationCenter.default.post(name: .userLoggedOut, object: nil) } } @@ -58,8 +82,7 @@ struct PreferencesView : View { #if DEBUG struct PreferencesView_Previews : PreviewProvider { static var previews: some View { - let account = LocalData.UserAccountInfo(instanceURL: URL(string: "https://mastodon.social")!, clientID: "clientID", clientSecret: "clientSecret", username: "example", accessToken: "accessToken") - return PreferencesView(currentAccount: account) + return PreferencesView() } } #endif diff --git a/Tusker/Screens/Profile/ProfileTableViewController.swift b/Tusker/Screens/Profile/ProfileTableViewController.swift index 8ddb9739bc..8ff37b657a 100644 --- a/Tusker/Screens/Profile/ProfileTableViewController.swift +++ b/Tusker/Screens/Profile/ProfileTableViewController.swift @@ -128,7 +128,7 @@ class ProfileTableViewController: EnhancedTableViewController { } @objc func updateUIForPreferences() { - guard let account = mastodonController.cache.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 } From c45dd9908853a74262e86c43638be287a85bba14 Mon Sep 17 00:00:00 2001 From: Shadowfacts Date: Sun, 19 Jan 2020 11:52:06 -0500 Subject: [PATCH 09/21] Clean up account switching code --- Tusker/LocalData.swift | 44 ++++++++++++++----- Tusker/SceneDelegate.swift | 16 ++++--- .../Onboarding/OnboardingViewController.swift | 4 +- .../PreferencesNavigationController.swift | 26 +++++++---- .../Screens/Preferences/PreferencesView.swift | 8 ++-- 5 files changed, 66 insertions(+), 32 deletions(-) diff --git a/Tusker/LocalData.swift b/Tusker/LocalData.swift index f3954fdd04..7aafa46144 100644 --- a/Tusker/LocalData.swift +++ b/Tusker/LocalData.swift @@ -21,6 +21,7 @@ class LocalData: ObservableObject { if ProcessInfo.processInfo.environment.keys.contains("UI_TESTING_LOGIN") { accounts = [ UserAccountInfo( + id: UUID().uuidString, instanceURL: URL(string: "http://localhost:8080")!, clientID: "client_id", clientSecret: "client_secret", @@ -38,15 +39,16 @@ class LocalData: ObservableObject { get { if let array = defaults.array(forKey: accountsKey) as? [[String: String]] { return array.compactMap { (info) in - guard let instanceURL = info["instanceURL"], + guard let id = info["id"], + let instanceURL = info["instanceURL"], let url = URL(string: instanceURL), - let id = info["clientID"], + let clientId = info["clientID"], let secret = info["clientSecret"], let username = info["username"], let accessToken = info["accessToken"] else { return nil } - return UserAccountInfo(instanceURL: url, clientID: id, clientSecret: secret, username: username, accessToken: accessToken) + return UserAccountInfo(id: id, instanceURL: url, clientID: clientId, clientSecret: secret, username: username, accessToken: accessToken) } } else { return [] @@ -56,6 +58,7 @@ class LocalData: ObservableObject { objectWillChange.send() let array = newValue.map { (info) in return [ + "id": info.id, "instanceURL": info.instanceURL.absoluteString, "clientID": info.clientID, "clientSecret": info.clientSecret, @@ -68,7 +71,7 @@ class LocalData: ObservableObject { } private let mostRecentAccountKey = "mostRecentAccount" - var mostRecentAccount: String? { + private var mostRecentAccount: String? { get { return defaults.string(forKey: mostRecentAccountKey) } @@ -82,41 +85,60 @@ class LocalData: ObservableObject { return !accounts.isEmpty } - func addAccount(instanceURL url: URL, clientID id: String, clientSecret secret: String, username: String, accessToken: String) -> UserAccountInfo { + 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 info = UserAccountInfo(instanceURL: url, clientID: id, clientSecret: secret, username: username, accessToken: accessToken) + 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 } func removeAccount(_ info: UserAccountInfo) { - + accounts.removeAll(where: { $0.id == info.id }) } func getMostRecentAccount() -> UserAccountInfo? { - if let accessToken = mostRecentAccount { - return accounts.first { $0.accessToken == accessToken } + guard onboardingComplete else { return nil } + let mostRecent: UserAccountInfo? + if let id = mostRecentAccount { + mostRecent = accounts.first { $0.id == id } } else { - return nil + mostRecent = nil } + 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/SceneDelegate.swift b/Tusker/SceneDelegate.swift index d4b939fb18..b2a172a2f6 100644 --- a/Tusker/SceneDelegate.swift +++ b/Tusker/SceneDelegate.swift @@ -34,7 +34,6 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate { window!.makeKeyAndVisible() - NotificationCenter.default.addObserver(self, selector: #selector(onUserLoggedOut), name: .userLoggedOut, object: nil) NotificationCenter.default.addObserver(self, selector: #selector(themePrefChanged), name: .themePreferenceChanged, object: nil) themePrefChanged() @@ -110,11 +109,20 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate { } func activateAccount(_ account: LocalData.UserAccountInfo) { - LocalData.shared.mostRecentAccount = account.accessToken + 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() @@ -130,10 +138,6 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate { window!.rootViewController = onboarding } - @objc func onUserLoggedOut() { - showOnboardingUI() - } - @objc func themePrefChanged() { window?.overrideUserInterfaceStyle = Preferences.shared.theme } diff --git a/Tusker/Screens/Onboarding/OnboardingViewController.swift b/Tusker/Screens/Onboarding/OnboardingViewController.swift index e799e2a6a6..9bb2af607d 100644 --- a/Tusker/Screens/Onboarding/OnboardingViewController.swift +++ b/Tusker/Screens/Onboarding/OnboardingViewController.swift @@ -69,9 +69,9 @@ extension OnboardingViewController: InstanceSelectorTableViewControllerDelegate mastodonController.authorize(authorizationCode: authCode) { (accessToken) in mastodonController.getOwnAccount { (account) in - let accountInfo = LocalData.shared.addAccount(instanceURL: instanceURL, clientID: clientID, clientSecret: clientSecret, username: account.username, accessToken: accessToken) - 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 c4a6c12ee2..5b93c23194 100644 --- a/Tusker/Screens/Preferences/PreferencesNavigationController.swift +++ b/Tusker/Screens/Preferences/PreferencesNavigationController.swift @@ -27,6 +27,7 @@ class PreferencesNavigationController: UINavigationController { 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) { @@ -48,26 +49,35 @@ class PreferencesNavigationController: UINavigationController { } @objc func cancelAddAccount() { - dismiss(animated: true) + 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 - dismiss(animated: true) { + dismiss(animated: true) { // dismiss preferences sceneDelegate.activateAccount(account) } } + + @objc func userLoggedOut() { + let sceneDelegate = self.view.window!.windowScene!.delegate as! SceneDelegate + dismiss(animated: true) { // dismiss preferences + sceneDelegate.logoutCurrent() + } + } } -extension Notification.Name { - static let addAccount = Notification.Name("Tusker.addAccount") - static let activateAccount = Notification.Name("Tusker.activateAccount") -} - extension PreferencesNavigationController: OnboardingViewControllerDelegate { func didFinishOnboarding(account: LocalData.UserAccountInfo) { - LocalData.shared.mostRecentAccount = account.accessToken + 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 38cbf99366..b30daa523b 100644 --- a/Tusker/Screens/Preferences/PreferencesView.swift +++ b/Tusker/Screens/Preferences/PreferencesView.swift @@ -10,7 +10,7 @@ import SwiftUI 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 { @@ -24,7 +24,7 @@ struct PreferencesView: View { Text(account.username) .foregroundColor(.primary) Spacer() - if account.accessToken == self.localData.mostRecentAccount { + if account == self.localData.getMostRecentAccount() { Image(systemName: "checkmark") .renderingMode(.template) .foregroundColor(.secondary) @@ -37,7 +37,7 @@ struct PreferencesView: View { }) { Text("Add Account...") } - if localData.mostRecentAccount != nil { + if localData.getMostRecentAccount() != nil { Button(action: { self.showingLogoutConfirmation = true }) { @@ -73,8 +73,6 @@ struct PreferencesView: View { } func logoutPressed() { -// LocalData.shared.removeAccount(currentAccount) - localData.removeAccount(localData.getMostRecentAccount()!) NotificationCenter.default.post(name: .userLoggedOut, object: nil) } } From 32e89f2c16ae289879d8ce3c5def4b13ecb266e3 Mon Sep 17 00:00:00 2001 From: Shadowfacts Date: Sun, 19 Jan 2020 23:02:07 -0500 Subject: [PATCH 10/21] Fix retain cycles with TuskerNavigationDelegate TuskerNavigationDelegate is now class-bound and only weak references to it are stored. --- .../Compose/Drafts/DraftsTableViewController.swift | 4 ++-- Tusker/Screens/Main/MainTabBarViewController.swift | 2 +- .../Notifications/NotificationsPageViewController.swift | 2 +- .../Onboarding/InstanceSelectorTableViewController.swift | 6 +++--- Tusker/Screens/Profile/ProfileTableViewController.swift | 2 +- .../Screens/Timeline/InstanceTimelineViewController.swift | 4 ++-- Tusker/Screens/Timeline/TimelineTableViewController.swift | 2 +- Tusker/Screens/Timeline/TimelinesPageViewController.swift | 2 +- Tusker/TuskerNavigationDelegate.swift | 2 +- Tusker/Views/Account Cell/AccountTableViewCell.swift | 2 +- Tusker/Views/Attachments/AttachmentView.swift | 8 ++++---- Tusker/Views/Attachments/AttachmentsContainerView.swift | 2 +- Tusker/Views/Compose Media/ComposeMediaView.swift | 4 ++-- Tusker/Views/ContentTextView.swift | 3 +-- Tusker/Views/Hashtag Cell/HashtagTableViewCell.swift | 2 +- .../ActionNotificationGroupTableViewCell.swift | 2 +- .../FollowNotificationGroupTableViewCell.swift | 2 +- .../FollowRequestNotificationTableViewCell.swift | 2 +- .../Views/Profile Header/ProfileHeaderTableViewCell.swift | 2 +- Tusker/Views/Status/BaseStatusTableViewCell.swift | 2 +- 20 files changed, 28 insertions(+), 29 deletions(-) diff --git a/Tusker/Screens/Compose/Drafts/DraftsTableViewController.swift b/Tusker/Screens/Compose/Drafts/DraftsTableViewController.swift index 3f71e3f21d..40f90faa34 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,7 +17,7 @@ protocol DraftsTableViewControllerDelegate { class DraftsTableViewController: UITableViewController { - var delegate: DraftsTableViewControllerDelegate? + weak var delegate: DraftsTableViewControllerDelegate? init() { super.init(nibName: "DraftsTableViewController", bundle: nil) diff --git a/Tusker/Screens/Main/MainTabBarViewController.swift b/Tusker/Screens/Main/MainTabBarViewController.swift index dd49bcae62..932a257a55 100644 --- a/Tusker/Screens/Main/MainTabBarViewController.swift +++ b/Tusker/Screens/Main/MainTabBarViewController.swift @@ -10,7 +10,7 @@ import UIKit class MainTabBarViewController: UITabBarController, UITabBarControllerDelegate { - let mastodonController: MastodonController + weak var mastodonController: MastodonController! override var supportedInterfaceOrientations: UIInterfaceOrientationMask { if UIDevice.current.userInterfaceIdiom == .phone { diff --git a/Tusker/Screens/Notifications/NotificationsPageViewController.swift b/Tusker/Screens/Notifications/NotificationsPageViewController.swift index ac66701a87..6a23f5c642 100644 --- a/Tusker/Screens/Notifications/NotificationsPageViewController.swift +++ b/Tusker/Screens/Notifications/NotificationsPageViewController.swift @@ -14,7 +14,7 @@ class NotificationsPageViewController: SegmentedPageViewController { private let notificationsTitle = NSLocalizedString("Notifications", comment: "notifications tab title") private let mentionsTitle = NSLocalizedString("Mentions", comment: "mentions tab title") - let mastodonController: MastodonController + weak var mastodonController: MastodonController! init(mastodonController: MastodonController) { self.mastodonController = mastodonController diff --git a/Tusker/Screens/Onboarding/InstanceSelectorTableViewController.swift b/Tusker/Screens/Onboarding/InstanceSelectorTableViewController.swift index 0be768090e..11681d23cd 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! @@ -55,7 +55,7 @@ class InstanceSelectorTableViewController: UITableViewController { tableView.rowHeight = UITableView.automaticDimension tableView.estimatedRowHeight = 120 - dataSource = DataSource(tableView: tableView, cellProvider: { (tableView, indexPath, item) -> UITableViewCell? in + dataSource = DataSource(tableView: tableView, cellProvider: { [weak self] (tableView, indexPath, item) -> UITableViewCell? in switch item { case let .selected(instance): let cell = tableView.dequeueReusableCell(withIdentifier: instanceCell, for: indexPath) as! InstanceTableViewCell diff --git a/Tusker/Screens/Profile/ProfileTableViewController.swift b/Tusker/Screens/Profile/ProfileTableViewController.swift index 8ff37b657a..ee7a9cac79 100644 --- a/Tusker/Screens/Profile/ProfileTableViewController.swift +++ b/Tusker/Screens/Profile/ProfileTableViewController.swift @@ -12,7 +12,7 @@ import SafariServices class ProfileTableViewController: EnhancedTableViewController { - let mastodonController: MastodonController + weak var mastodonController: MastodonController! var accountID: String! { didSet { diff --git a/Tusker/Screens/Timeline/InstanceTimelineViewController.swift b/Tusker/Screens/Timeline/InstanceTimelineViewController.swift index ff809ba748..d5c1943036 100644 --- a/Tusker/Screens/Timeline/InstanceTimelineViewController.swift +++ b/Tusker/Screens/Timeline/InstanceTimelineViewController.swift @@ -8,14 +8,14 @@ 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? let instanceURL: URL diff --git a/Tusker/Screens/Timeline/TimelineTableViewController.swift b/Tusker/Screens/Timeline/TimelineTableViewController.swift index 12022b00e9..9966b510aa 100644 --- a/Tusker/Screens/Timeline/TimelineTableViewController.swift +++ b/Tusker/Screens/Timeline/TimelineTableViewController.swift @@ -12,7 +12,7 @@ import Pachyderm class TimelineTableViewController: EnhancedTableViewController { var timeline: Timeline! - let mastodonController: MastodonController + weak var mastodonController: MastodonController! var timelineSegments: [[(id: String, state: StatusState)]] = [] { didSet { diff --git a/Tusker/Screens/Timeline/TimelinesPageViewController.swift b/Tusker/Screens/Timeline/TimelinesPageViewController.swift index a01e74dbe9..19fb687880 100644 --- a/Tusker/Screens/Timeline/TimelinesPageViewController.swift +++ b/Tusker/Screens/Timeline/TimelinesPageViewController.swift @@ -14,7 +14,7 @@ class TimelinesPageViewController: SegmentedPageViewController { private let federatedTitle = NSLocalizedString("Federated", comment: "federated timeline tab title") private let localTitle = NSLocalizedString("Local", comment: "local timeline tab title") - let mastodonController: MastodonController + weak var mastodonController: MastodonController! init(mastodonController: MastodonController) { self.mastodonController = mastodonController diff --git a/Tusker/TuskerNavigationDelegate.swift b/Tusker/TuskerNavigationDelegate.swift index 8915fe16bd..41f3960344 100644 --- a/Tusker/TuskerNavigationDelegate.swift +++ b/Tusker/TuskerNavigationDelegate.swift @@ -10,7 +10,7 @@ import UIKit import SafariServices import Pachyderm -protocol TuskerNavigationDelegate { +protocol TuskerNavigationDelegate: class { var apiController: MastodonController { get } diff --git a/Tusker/Views/Account Cell/AccountTableViewCell.swift b/Tusker/Views/Account Cell/AccountTableViewCell.swift index cb93b6bd0c..cf01f6ef69 100644 --- a/Tusker/Views/Account Cell/AccountTableViewCell.swift +++ b/Tusker/Views/Account Cell/AccountTableViewCell.swift @@ -10,7 +10,7 @@ import UIKit class AccountTableViewCell: UITableViewCell { - var delegate: TuskerNavigationDelegate? + weak var delegate: TuskerNavigationDelegate? var mastodonController: MastodonController! { delegate?.apiController } @IBOutlet weak var avatarImageView: UIImageView! diff --git a/Tusker/Views/Attachments/AttachmentView.swift b/Tusker/Views/Attachments/AttachmentView.swift index e65b2d8d51..41bfad86b8 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 5ca7deb9bf..d8e18f9c50 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]! diff --git a/Tusker/Views/Compose Media/ComposeMediaView.swift b/Tusker/Views/Compose Media/ComposeMediaView.swift index 54c1efd7b4..cd7ba31f2c 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/ContentTextView.swift b/Tusker/Views/ContentTextView.swift index ea7005879c..b36076dc30 100644 --- a/Tusker/Views/ContentTextView.swift +++ b/Tusker/Views/ContentTextView.swift @@ -15,8 +15,7 @@ private let emojiRegex = try! NSRegularExpression(pattern: ":(\\w+):", options: class ContentTextView: LinkTextView { - // todo: should be weak - var navigationDelegate: TuskerNavigationDelegate? + weak var navigationDelegate: TuskerNavigationDelegate? var mastodonController: MastodonController? { navigationDelegate?.apiController } var defaultFont: UIFont = .systemFont(ofSize: 17) diff --git a/Tusker/Views/Hashtag Cell/HashtagTableViewCell.swift b/Tusker/Views/Hashtag Cell/HashtagTableViewCell.swift index bd364d1668..3a9008cf84 100644 --- a/Tusker/Views/Hashtag Cell/HashtagTableViewCell.swift +++ b/Tusker/Views/Hashtag Cell/HashtagTableViewCell.swift @@ -11,7 +11,7 @@ import Pachyderm class HashtagTableViewCell: UITableViewCell { - var delegate: TuskerNavigationDelegate? + weak var delegate: TuskerNavigationDelegate? @IBOutlet weak var hashtagLabel: UILabel! diff --git a/Tusker/Views/Notifications/ActionNotificationGroupTableViewCell.swift b/Tusker/Views/Notifications/ActionNotificationGroupTableViewCell.swift index cedde5f6b3..62256c38a3 100644 --- a/Tusker/Views/Notifications/ActionNotificationGroupTableViewCell.swift +++ b/Tusker/Views/Notifications/ActionNotificationGroupTableViewCell.swift @@ -12,7 +12,7 @@ import SwiftSoup class ActionNotificationGroupTableViewCell: UITableViewCell { - var delegate: TuskerNavigationDelegate? + weak var delegate: TuskerNavigationDelegate? var mastodonController: MastodonController! { delegate?.apiController } @IBOutlet weak var actionImageView: UIImageView! diff --git a/Tusker/Views/Notifications/FollowNotificationGroupTableViewCell.swift b/Tusker/Views/Notifications/FollowNotificationGroupTableViewCell.swift index 0c2e62cc23..0113d78efd 100644 --- a/Tusker/Views/Notifications/FollowNotificationGroupTableViewCell.swift +++ b/Tusker/Views/Notifications/FollowNotificationGroupTableViewCell.swift @@ -11,7 +11,7 @@ import Pachyderm class FollowNotificationGroupTableViewCell: UITableViewCell { - var delegate: TuskerNavigationDelegate? + weak var delegate: TuskerNavigationDelegate? var mastodonController: MastodonController! { delegate?.apiController } @IBOutlet weak var avatarStackView: UIStackView! diff --git a/Tusker/Views/Notifications/FollowRequestNotificationTableViewCell.swift b/Tusker/Views/Notifications/FollowRequestNotificationTableViewCell.swift index 1674c639a5..f62f71b1bc 100644 --- a/Tusker/Views/Notifications/FollowRequestNotificationTableViewCell.swift +++ b/Tusker/Views/Notifications/FollowRequestNotificationTableViewCell.swift @@ -11,7 +11,7 @@ import Pachyderm class FollowRequestNotificationTableViewCell: UITableViewCell { - var delegate: TuskerNavigationDelegate? + weak var delegate: TuskerNavigationDelegate? var mastodonController: MastodonController! { delegate?.apiController } @IBOutlet weak var stackView: UIStackView! diff --git a/Tusker/Views/Profile Header/ProfileHeaderTableViewCell.swift b/Tusker/Views/Profile Header/ProfileHeaderTableViewCell.swift index a3df7918b0..bf740fa5d5 100644 --- a/Tusker/Views/Profile Header/ProfileHeaderTableViewCell.swift +++ b/Tusker/Views/Profile Header/ProfileHeaderTableViewCell.swift @@ -15,7 +15,7 @@ protocol ProfileHeaderTableViewCellDelegate: TuskerNavigationDelegate { class ProfileHeaderTableViewCell: UITableViewCell { - var delegate: ProfileHeaderTableViewCellDelegate? + weak var delegate: ProfileHeaderTableViewCellDelegate? var mastodonController: MastodonController! { delegate?.apiController } @IBOutlet weak var headerImageView: UIImageView! diff --git a/Tusker/Views/Status/BaseStatusTableViewCell.swift b/Tusker/Views/Status/BaseStatusTableViewCell.swift index 0b7f778e5f..a34f8d0961 100644 --- a/Tusker/Views/Status/BaseStatusTableViewCell.swift +++ b/Tusker/Views/Status/BaseStatusTableViewCell.swift @@ -16,7 +16,7 @@ protocol StatusTableViewCellDelegate: TuskerNavigationDelegate { class BaseStatusTableViewCell: UITableViewCell { - var delegate: StatusTableViewCellDelegate? { + weak var delegate: StatusTableViewCellDelegate? { didSet { contentTextView.navigationDelegate = delegate } From 8eb6f6f57388350decc42cb422cda1a226b97bb4 Mon Sep 17 00:00:00 2001 From: Shadowfacts Date: Sun, 19 Jan 2020 23:10:52 -0500 Subject: [PATCH 11/21] Fix retain cycle in timestamp updating code The timestamp update work item shouldn't retain a reference to the cell. It can be unowned because when the cell is deinit'd, the work item will be cancelled. --- .../ActionNotificationGroupTableViewCell.swift | 8 +++++++- .../FollowNotificationGroupTableViewCell.swift | 6 +++++- .../FollowRequestNotificationTableViewCell.swift | 8 +++++++- Tusker/Views/Status/TimelineStatusTableViewCell.swift | 3 ++- 4 files changed, 21 insertions(+), 4 deletions(-) diff --git a/Tusker/Views/Notifications/ActionNotificationGroupTableViewCell.swift b/Tusker/Views/Notifications/ActionNotificationGroupTableViewCell.swift index 62256c38a3..d4ea85cbe4 100644 --- a/Tusker/Views/Notifications/ActionNotificationGroupTableViewCell.swift +++ b/Tusker/Views/Notifications/ActionNotificationGroupTableViewCell.swift @@ -27,6 +27,10 @@ class ActionNotificationGroupTableViewCell: UITableViewCell { var authorAvatarURL: URL? var updateTimestampWorkItem: DispatchWorkItem? + deinit { + updateTimestampWorkItem?.cancel() + } + override func awakeFromNib() { super.awakeFromNib() @@ -110,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 diff --git a/Tusker/Views/Notifications/FollowNotificationGroupTableViewCell.swift b/Tusker/Views/Notifications/FollowNotificationGroupTableViewCell.swift index 0113d78efd..90d982fdbb 100644 --- a/Tusker/Views/Notifications/FollowNotificationGroupTableViewCell.swift +++ b/Tusker/Views/Notifications/FollowNotificationGroupTableViewCell.swift @@ -22,6 +22,10 @@ class FollowNotificationGroupTableViewCell: UITableViewCell { var updateTimestampWorkItem: DispatchWorkItem? + deinit { + updateTimestampWorkItem?.cancel() + } + override func awakeFromNib() { super.awakeFromNib() @@ -98,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!) diff --git a/Tusker/Views/Notifications/FollowRequestNotificationTableViewCell.swift b/Tusker/Views/Notifications/FollowRequestNotificationTableViewCell.swift index f62f71b1bc..f31fe7a12d 100644 --- a/Tusker/Views/Notifications/FollowRequestNotificationTableViewCell.swift +++ b/Tusker/Views/Notifications/FollowRequestNotificationTableViewCell.swift @@ -27,6 +27,10 @@ class FollowRequestNotificationTableViewCell: UITableViewCell { var updateTimestampWorkItem: DispatchWorkItem? + deinit { + updateTimestampWorkItem?.cancel() + } + override func awakeFromNib() { super.awakeFromNib() @@ -72,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 diff --git a/Tusker/Views/Status/TimelineStatusTableViewCell.swift b/Tusker/Views/Status/TimelineStatusTableViewCell.swift index d4aa74ed2f..6e115f6c0c 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() { @@ -109,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!) From 2f630f2f8fffe548dafbba2c0e83ab36aacf2db9 Mon Sep 17 00:00:00 2001 From: Shadowfacts Date: Sun, 19 Jan 2020 23:11:47 -0500 Subject: [PATCH 12/21] Fix retain cycle between MastodonController/MastodonCache The cache should only store a weak reference to the controller, so that when the controller is deinit'd the cache is as well. --- Tusker/MastodonCache.swift | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/Tusker/MastodonCache.swift b/Tusker/MastodonCache.swift index 0c22b4e259..9aec17c675 100644 --- a/Tusker/MastodonCache.swift +++ b/Tusker/MastodonCache.swift @@ -20,7 +20,7 @@ class MastodonCache { let statusSubject = PassthroughSubject() let accountSubject = PassthroughSubject() - let mastodonController: MastodonController + weak var mastodonController: MastodonController? init(mastodonController: MastodonController) { self.mastodonController = mastodonController @@ -43,6 +43,9 @@ class MastodonCache { } 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 { @@ -73,6 +76,9 @@ class MastodonCache { } 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 { @@ -102,6 +108,9 @@ class MastodonCache { } 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, From ee252c02e948d25ca3238bd66cd9d6d4f3e0e0cd Mon Sep 17 00:00:00 2001 From: Shadowfacts Date: Sun, 19 Jan 2020 23:14:51 -0500 Subject: [PATCH 13/21] Fix retain cycle in timeline cell cache observers The use an unowned reference to self because when the cell is deinit'd, the Combine observers will be cancelled. --- Tusker/Views/Status/BaseStatusTableViewCell.swift | 8 ++++---- Tusker/Views/Status/TimelineStatusTableViewCell.swift | 5 +++-- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/Tusker/Views/Status/BaseStatusTableViewCell.swift b/Tusker/Views/Status/BaseStatusTableViewCell.swift index a34f8d0961..6d43c28e0f 100644 --- a/Tusker/Views/Status/BaseStatusTableViewCell.swift +++ b/Tusker/Views/Status/BaseStatusTableViewCell.swift @@ -100,16 +100,16 @@ class BaseStatusTableViewCell: UITableViewCell { open func createObserversIfNecessary() { if statusUpdater == nil { statusUpdater = mastodonController.cache.statusSubject - .filter { $0.id == self.statusID } + .filter { [unowned self] in $0.id == self.statusID } .receive(on: DispatchQueue.main) - .sink(receiveValue: updateStatusState(status:)) + .sink { [unowned self] in self.updateStatusState(status: $0) } } if accountUpdater == nil { accountUpdater = mastodonController.cache.accountSubject - .filter { $0.id == self.accountID } + .filter { [unowned self] in $0.id == self.accountID } .receive(on: DispatchQueue.main) - .sink(receiveValue: updateUI(account:)) + .sink { [unowned self] in self.updateUI(account: $0) } } } diff --git a/Tusker/Views/Status/TimelineStatusTableViewCell.swift b/Tusker/Views/Status/TimelineStatusTableViewCell.swift index 6e115f6c0c..6ffa7b64d0 100644 --- a/Tusker/Views/Status/TimelineStatusTableViewCell.swift +++ b/Tusker/Views/Status/TimelineStatusTableViewCell.swift @@ -49,9 +49,9 @@ class TimelineStatusTableViewCell: BaseStatusTableViewCell { if rebloggerAccountUpdater == nil { rebloggerAccountUpdater = mastodonController.cache.accountSubject - .filter { $0.id == self.rebloggerID } + .filter { [unowned self] in $0.id == self.rebloggerID } .receive(on: DispatchQueue.main) - .sink(receiveValue: updateRebloggerLabel(reblogger:)) + .sink { [unowned self] in self.updateRebloggerLabel(reblogger: $0) } } } @@ -95,6 +95,7 @@ class TimelineStatusTableViewCell: BaseStatusTableViewCell { } func updateTimestamp() { + guard superview != nil else { return } guard let status = mastodonController.cache.status(for: statusID) else { fatalError("Missing cached status \(statusID!)") } timestampLabel.text = status.createdAt.timeAgoString() From 3aa5aa1bc01782274228cbf9156e6dcb56a50fd1 Mon Sep 17 00:00:00 2001 From: Shadowfacts Date: Sun, 19 Jan 2020 23:16:36 -0500 Subject: [PATCH 14/21] Fix weird crashes when switching accounts --- .../Preferences/PreferencesNavigationController.swift | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/Tusker/Screens/Preferences/PreferencesNavigationController.swift b/Tusker/Screens/Preferences/PreferencesNavigationController.swift index 5b93c23194..3b4d7b4f6b 100644 --- a/Tusker/Screens/Preferences/PreferencesNavigationController.swift +++ b/Tusker/Screens/Preferences/PreferencesNavigationController.swift @@ -10,6 +10,8 @@ import UIKit import SwiftUI class PreferencesNavigationController: UINavigationController { + + private var isSwitchingAccounts = false init(mastodonController: MastodonController) { let view = PreferencesView() @@ -33,8 +35,10 @@ class PreferencesNavigationController: UINavigationController { 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() { @@ -55,6 +59,7 @@ class PreferencesNavigationController: UINavigationController { @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) } @@ -62,6 +67,7 @@ class PreferencesNavigationController: UINavigationController { @objc func userLoggedOut() { let sceneDelegate = self.view.window!.windowScene!.delegate as! SceneDelegate + isSwitchingAccounts = true dismiss(animated: true) { // dismiss preferences sceneDelegate.logoutCurrent() } From c99a724bf3f6dc60fe7669c323d1b3e965a46f60 Mon Sep 17 00:00:00 2001 From: Shadowfacts Date: Mon, 20 Jan 2020 11:18:55 -0500 Subject: [PATCH 15/21] Fix instance public timelines crashing The instance timeline controller needs to store a strong reference to the instance-specific MastodonController since the timeline VC only holds a weak reference, and unlike normal screens, the scene session doesn't hold onto the MastodonController for other instances. --- .../Screens/Timeline/InstanceTimelineViewController.swift | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/Tusker/Screens/Timeline/InstanceTimelineViewController.swift b/Tusker/Screens/Timeline/InstanceTimelineViewController.swift index d5c1943036..bd249a802a 100644 --- a/Tusker/Screens/Timeline/InstanceTimelineViewController.swift +++ b/Tusker/Screens/Timeline/InstanceTimelineViewController.swift @@ -18,6 +18,7 @@ class InstanceTimelineViewController: TimelineTableViewController { weak var delegate: InstanceTimelineViewControllerDelegate? let instanceURL: URL + let instanceMastodonController: MastodonController var toggleSaveButton: UIBarButtonItem! var toggleSaveButtonTitle: String { @@ -31,9 +32,10 @@ class InstanceTimelineViewController: TimelineTableViewController { init(for url: URL) { self.instanceURL = url - let mastodonController = MastodonController(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: mastodonController) + super.init(for: .instance(instanceURL: url), mastodonController: instanceMastodonController) } required init?(coder aDecoder: NSCoder) { From 26f1aafa15efdfb56ed319ae32caa79fa31139b7 Mon Sep 17 00:00:00 2001 From: Shadowfacts Date: Mon, 20 Jan 2020 11:20:39 -0500 Subject: [PATCH 16/21] Unify SavedHashtagsManager and SavedInstancesManager --- Tusker.xcodeproj/project.pbxproj | 12 +-- Tusker/SavedDataManager.swift | 88 +++++++++++++++++++ Tusker/SavedHashtagsManager.swift | 65 -------------- Tusker/SavedInstancesManager.swift | 61 ------------- .../AddSavedHashtagViewController.swift | 2 +- .../Explore/ExploreViewController.swift | 12 +-- .../InstanceSelectorTableViewController.swift | 2 +- .../HashtagTimelineViewController.swift | 8 +- .../InstanceTimelineViewController.swift | 8 +- 9 files changed, 108 insertions(+), 150 deletions(-) create mode 100644 Tusker/SavedDataManager.swift delete mode 100644 Tusker/SavedHashtagsManager.swift delete mode 100644 Tusker/SavedInstancesManager.swift diff --git a/Tusker.xcodeproj/project.pbxproj b/Tusker.xcodeproj/project.pbxproj index 615568e916..1bca044da2 100644 --- a/Tusker.xcodeproj/project.pbxproj +++ b/Tusker.xcodeproj/project.pbxproj @@ -156,10 +156,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 */; }; @@ -432,10 +431,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 = ""; }; @@ -1193,8 +1191,7 @@ 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 */, @@ -1641,7 +1638,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 */, @@ -1697,7 +1693,7 @@ 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/SavedDataManager.swift b/Tusker/SavedDataManager.swift new file mode 100644 index 0000000000..93ef09599b --- /dev/null +++ b/Tusker/SavedDataManager.swift @@ -0,0 +1,88 @@ +// +// 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: [Hashtag] = [] { + didSet { + SavedDataManager.save() + NotificationCenter.default.post(name: .savedHashtagsChanged, object: nil) + } + } + var sortedHashtags: [Hashtag] { + return savedHashtags.sorted(by: { $0.name < $1.name }) + } + + private(set) var savedInstances: [URL] = [] { + didSet { + SavedDataManager.save() + NotificationCenter.default.post(name: .savedInstancesChanged, object: nil) + } + } + + func isSaved(hashtag: Hashtag) -> Bool { + return savedHashtags.contains(hashtag) + } + + func add(hashtag: Hashtag) { + if isSaved(hashtag: hashtag) { + return + } + savedHashtags.append(hashtag) + } + + func remove(hashtag: Hashtag) { + guard isSaved(hashtag: hashtag) else { return } + savedHashtags.removeAll(where: { $0.name == hashtag.name }) + } + + func isSaved(instance url: URL) -> Bool { + return savedInstances.contains(url) + } + + func add(instance url: URL) { + if isSaved(instance: url) { return } + savedInstances.append(url) + } + + func remove(instance url: URL) { + guard isSaved(instance: url) else { return } + savedInstances.removeAll(where: { $0 == url }) + } +} + +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 cf91aa9e26..0000000000 --- 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 7bc466c14a..0000000000 --- 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/Screens/Explore/AddSavedHashtagViewController.swift b/Tusker/Screens/Explore/AddSavedHashtagViewController.swift index f45eb02589..d3b5c491a2 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) dismiss(animated: true) } } diff --git a/Tusker/Screens/Explore/ExploreViewController.swift b/Tusker/Screens/Explore/ExploreViewController.swift index c278c2b42e..fe8207286e 100644 --- a/Tusker/Screens/Explore/ExploreViewController.swift +++ b/Tusker/Screens/Explore/ExploreViewController.swift @@ -85,8 +85,8 @@ class ExploreViewController: EnhancedTableViewController { 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.map { .savedHashtag($0) } + [.addSavedHashtag], toSection: .savedHashtags) + snapshot.appendItems(SavedDataManager.shared.savedInstances.map { .savedInstance($0) } + [.findInstance], toSection: .savedInstances) // the initial, static items should not be displayed with an animation UIView.performWithoutAnimation { dataSource.apply(snapshot) @@ -129,14 +129,14 @@ class ExploreViewController: EnhancedTableViewController { @objc func savedHashtagsChanged() { 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.map { .savedHashtag($0) } + [.addSavedHashtag], toSection: .savedHashtags) dataSource.apply(snapshot) } @objc func savedInstancesChanged() { 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.map { .savedInstance($0) } + [.findInstance], toSection: .savedInstances) dataSource.apply(snapshot) } @@ -163,11 +163,11 @@ class ExploreViewController: EnhancedTableViewController { } func removeSavedHashtag(_ hashtag: Hashtag) { - SavedHashtagsManager.shared.remove(hashtag) + SavedDataManager.shared.remove(hashtag: hashtag) } func removeSavedInstance(_ instanceURL: URL) { - SavedInstanceManager.shared.remove(instanceURL) + SavedDataManager.shared.remove(instance: instanceURL) } // MARK: - Table view delegate diff --git a/Tusker/Screens/Onboarding/InstanceSelectorTableViewController.swift b/Tusker/Screens/Onboarding/InstanceSelectorTableViewController.swift index 11681d23cd..b77e8bc9d1 100644 --- a/Tusker/Screens/Onboarding/InstanceSelectorTableViewController.swift +++ b/Tusker/Screens/Onboarding/InstanceSelectorTableViewController.swift @@ -55,7 +55,7 @@ class InstanceSelectorTableViewController: UITableViewController { tableView.rowHeight = UITableView.automaticDimension tableView.estimatedRowHeight = 120 - dataSource = DataSource(tableView: tableView, cellProvider: { [weak self] (tableView, indexPath, item) -> UITableViewCell? in + dataSource = DataSource(tableView: tableView, cellProvider: { (tableView, indexPath, item) -> UITableViewCell? in switch item { case let .selected(instance): let cell = tableView.dequeueReusableCell(withIdentifier: instanceCell, for: indexPath) as! InstanceTableViewCell diff --git a/Tusker/Screens/Timeline/HashtagTimelineViewController.swift b/Tusker/Screens/Timeline/HashtagTimelineViewController.swift index 3dcd8787c7..bf3f5a9bb6 100644 --- a/Tusker/Screens/Timeline/HashtagTimelineViewController.swift +++ b/Tusker/Screens/Timeline/HashtagTimelineViewController.swift @@ -15,7 +15,7 @@ class HashtagTimelineViewController: TimelineTableViewController { var toggleSaveButton: UIBarButtonItem! var toggleSaveButtonTitle: String { - if SavedHashtagsManager.shared.isSaved(hashtag) { + if SavedDataManager.shared.isSaved(hashtag: hashtag) { return NSLocalizedString("Unsave", comment: "unsave hashtag button") } else { return NSLocalizedString("Save", comment: "save hashtag button") @@ -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) { + SavedDataManager.shared.remove(hashtag: hashtag) } else { - SavedHashtagsManager.shared.add(hashtag) + SavedDataManager.shared.add(hashtag: hashtag) } } diff --git a/Tusker/Screens/Timeline/InstanceTimelineViewController.swift b/Tusker/Screens/Timeline/InstanceTimelineViewController.swift index bd249a802a..0c99ae1d7f 100644 --- a/Tusker/Screens/Timeline/InstanceTimelineViewController.swift +++ b/Tusker/Screens/Timeline/InstanceTimelineViewController.swift @@ -22,7 +22,7 @@ class InstanceTimelineViewController: TimelineTableViewController { var toggleSaveButton: UIBarButtonItem! var toggleSaveButtonTitle: String { - if SavedInstanceManager.shared.isSaved(instanceURL) { + if SavedDataManager.shared.isSaved(instance: instanceURL) { return NSLocalizedString("Unsave", comment: "unsave instance button") } else { return NSLocalizedString("Save", comment: "save instance button") @@ -72,11 +72,11 @@ class InstanceTimelineViewController: TimelineTableViewController { // MARK: - Interaction @objc func toggleSaveButtonPressed() { - if SavedInstanceManager.shared.isSaved(instanceURL) { - SavedInstanceManager.shared.remove(instanceURL) + if SavedDataManager.shared.isSaved(instance: instanceURL) { + SavedDataManager.shared.remove(instance: instanceURL) delegate?.didUnsaveInstance(url: instanceURL) } else { - SavedInstanceManager.shared.add(instanceURL) + SavedDataManager.shared.add(instance: instanceURL) delegate?.didSaveInstance(url: instanceURL) } } From ff97b0f76deb4303d7076731082749961eba68b7 Mon Sep 17 00:00:00 2001 From: Shadowfacts Date: Mon, 20 Jan 2020 11:48:47 -0500 Subject: [PATCH 17/21] Change saved hashtags/instances to be per-account See #16 --- Tusker/SavedDataManager.swift | 69 +++++++++++++------ .../AddSavedHashtagViewController.swift | 2 +- .../Explore/ExploreViewController.swift | 24 ++++--- .../Screens/FindInstanceViewController.swift | 14 +++- .../HashtagTimelineViewController.swift | 8 +-- .../InstanceTimelineViewController.swift | 16 +++-- 6 files changed, 90 insertions(+), 43 deletions(-) diff --git a/Tusker/SavedDataManager.swift b/Tusker/SavedDataManager.swift index 93ef09599b..6d4696f9b8 100644 --- a/Tusker/SavedDataManager.swift +++ b/Tusker/SavedDataManager.swift @@ -34,51 +34,76 @@ class SavedDataManager: Codable { private init() {} - private var savedHashtags: [Hashtag] = [] { + private var savedHashtags: [String: [Hashtag]] = [:] { didSet { SavedDataManager.save() NotificationCenter.default.post(name: .savedHashtagsChanged, object: nil) } } - var sortedHashtags: [Hashtag] { - return savedHashtags.sorted(by: { $0.name < $1.name }) - } - - private(set) var savedInstances: [URL] = [] { + + private var savedInstances: [String: [URL]] = [:] { didSet { SavedDataManager.save() NotificationCenter.default.post(name: .savedInstancesChanged, object: nil) } } - func isSaved(hashtag: Hashtag) -> Bool { - return savedHashtags.contains(hashtag) + func sortedHashtags(for account: LocalData.UserAccountInfo) -> [Hashtag] { + if let hashtags = savedHashtags[account.id] { + return hashtags.sorted(by: { $0.name < $1.name }) + } else { + return [] + } } - func add(hashtag: Hashtag) { - if isSaved(hashtag: hashtag) { + 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 } - savedHashtags.append(hashtag) + if var saved = savedHashtags[account.id] { + saved.append(hashtag) + savedHashtags[account.id] = saved + } else { + savedHashtags[account.id] = [hashtag] + } } - func remove(hashtag: Hashtag) { - guard isSaved(hashtag: hashtag) else { return } - savedHashtags.removeAll(where: { $0.name == hashtag.name }) + 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 isSaved(instance url: URL) -> Bool { - return savedInstances.contains(url) + func savedInstances(for account: LocalData.UserAccountInfo) -> [URL] { + return savedInstances[account.id] ?? [] } - func add(instance url: URL) { - if isSaved(instance: url) { return } - savedInstances.append(url) + func isSaved(instance url: URL, for account: LocalData.UserAccountInfo) -> Bool { + return savedInstances[account.id]?.contains(url) ?? false } - func remove(instance url: URL) { - guard isSaved(instance: url) else { return } - savedInstances.removeAll(where: { $0 == url }) + 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 + } } } diff --git a/Tusker/Screens/Explore/AddSavedHashtagViewController.swift b/Tusker/Screens/Explore/AddSavedHashtagViewController.swift index d3b5c491a2..65585f6728 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) { - SavedDataManager.shared.add(hashtag: 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 fe8207286e..0254ddeced 100644 --- a/Tusker/Screens/Explore/ExploreViewController.swift +++ b/Tusker/Screens/Explore/ExploreViewController.swift @@ -81,12 +81,14 @@ 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(SavedDataManager.shared.sortedHashtags.map { .savedHashtag($0) } + [.addSavedHashtag], toSection: .savedHashtags) - snapshot.appendItems(SavedDataManager.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) @@ -127,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(SavedDataManager.shared.sortedHashtags.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(SavedDataManager.shared.savedInstances.map { .savedInstance($0) } + [.findInstance], toSection: .savedInstances) + snapshot.appendItems(SavedDataManager.shared.savedInstances(for: account).map { .savedInstance($0) } + [.findInstance], toSection: .savedInstances) dataSource.apply(snapshot) } @@ -163,11 +167,13 @@ class ExploreViewController: EnhancedTableViewController { } func removeSavedHashtag(_ hashtag: Hashtag) { - SavedDataManager.shared.remove(hashtag: hashtag) + let account = mastodonController.accountInfo! + SavedDataManager.shared.remove(hashtag: hashtag, for: account) } func removeSavedInstance(_ instanceURL: URL) { - SavedDataManager.shared.remove(instance: instanceURL) + let account = mastodonController.accountInfo! + SavedDataManager.shared.remove(instance: instanceURL, for: account) } // MARK: - Table view delegate @@ -217,11 +223,11 @@ class ExploreViewController: EnhancedTableViewController { 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) @@ -348,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 70a3efbf90..abe07e4309 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/Timeline/HashtagTimelineViewController.swift b/Tusker/Screens/Timeline/HashtagTimelineViewController.swift index bf3f5a9bb6..ab8d5b6c60 100644 --- a/Tusker/Screens/Timeline/HashtagTimelineViewController.swift +++ b/Tusker/Screens/Timeline/HashtagTimelineViewController.swift @@ -15,7 +15,7 @@ class HashtagTimelineViewController: TimelineTableViewController { var toggleSaveButton: UIBarButtonItem! var toggleSaveButtonTitle: String { - if SavedDataManager.shared.isSaved(hashtag: 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") @@ -48,10 +48,10 @@ class HashtagTimelineViewController: TimelineTableViewController { // MARK: - Interaction @objc func toggleSaveButtonPressed() { - if SavedDataManager.shared.isSaved(hashtag: hashtag) { - SavedDataManager.shared.remove(hashtag: hashtag) + if SavedDataManager.shared.isSaved(hashtag: hashtag, for: mastodonController.accountInfo!) { + SavedDataManager.shared.remove(hashtag: hashtag, for: mastodonController.accountInfo!) } else { - SavedDataManager.shared.add(hashtag: hashtag) + SavedDataManager.shared.add(hashtag: hashtag, for: mastodonController.accountInfo!) } } diff --git a/Tusker/Screens/Timeline/InstanceTimelineViewController.swift b/Tusker/Screens/Timeline/InstanceTimelineViewController.swift index 0c99ae1d7f..2694bf6fa6 100644 --- a/Tusker/Screens/Timeline/InstanceTimelineViewController.swift +++ b/Tusker/Screens/Timeline/InstanceTimelineViewController.swift @@ -17,21 +17,25 @@ class InstanceTimelineViewController: TimelineTableViewController { weak var delegate: InstanceTimelineViewControllerDelegate? + weak var parentMastodonController: MastodonController? + let instanceURL: URL let instanceMastodonController: MastodonController var toggleSaveButton: UIBarButtonItem! var toggleSaveButtonTitle: String { - if SavedDataManager.shared.isSaved(instance: 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 + 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) @@ -72,11 +76,11 @@ class InstanceTimelineViewController: TimelineTableViewController { // MARK: - Interaction @objc func toggleSaveButtonPressed() { - if SavedDataManager.shared.isSaved(instance: instanceURL) { - SavedDataManager.shared.remove(instance: instanceURL) + if SavedDataManager.shared.isSaved(instance: instanceURL, for: parentMastodonController!.accountInfo!) { + SavedDataManager.shared.remove(instance: instanceURL, for: parentMastodonController!.accountInfo!) delegate?.didUnsaveInstance(url: instanceURL) } else { - SavedDataManager.shared.add(instance: instanceURL) + SavedDataManager.shared.add(instance: instanceURL, for: parentMastodonController!.accountInfo!) delegate?.didSaveInstance(url: instanceURL) } } From 08086f1b1836b4f68809d08a190fb36c84ef0ff4 Mon Sep 17 00:00:00 2001 From: Shadowfacts Date: Mon, 20 Jan 2020 15:25:23 -0500 Subject: [PATCH 18/21] Fix compose reply view missing MastodonController instance --- Tusker/Screens/Compose/ComposeViewController.swift | 1 + .../Views/Compose Status Reply/ComposeStatusReplyView.swift | 3 +++ Tusker/Views/ContentTextView.swift | 5 +++-- 3 files changed, 7 insertions(+), 2 deletions(-) diff --git a/Tusker/Screens/Compose/ComposeViewController.swift b/Tusker/Screens/Compose/ComposeViewController.swift index d0abd874da..7efb15cc2e 100644 --- a/Tusker/Screens/Compose/ComposeViewController.swift +++ b/Tusker/Screens/Compose/ComposeViewController.swift @@ -193,6 +193,7 @@ class ComposeViewController: UIViewController { } let replyView = ComposeStatusReplyView.create() + replyView.mastodonController = mastodonController replyView.updateUI(for: inReplyTo) stackView.insertArrangedSubview(replyView, at: 0) diff --git a/Tusker/Views/Compose Status Reply/ComposeStatusReplyView.swift b/Tusker/Views/Compose Status Reply/ComposeStatusReplyView.swift index 1242ae4b33..12f24cd5bc 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 b36076dc30..3cd7a4eddc 100644 --- a/Tusker/Views/ContentTextView.swift +++ b/Tusker/Views/ContentTextView.swift @@ -16,8 +16,9 @@ private let emojiRegex = try! NSRegularExpression(pattern: ":(\\w+):", options: class ContentTextView: LinkTextView { weak var navigationDelegate: TuskerNavigationDelegate? - var mastodonController: MastodonController? { navigationDelegate?.apiController } - + weak var overrideMastodonController: MastodonController? + var mastodonController: MastodonController? { overrideMastodonController ?? navigationDelegate?.apiController } + var defaultFont: UIFont = .systemFont(ofSize: 17) var defaultColor: UIColor = .label From 59277ec64f85a584dc46da5ca566331f7a51ddff Mon Sep 17 00:00:00 2001 From: Shadowfacts Date: Mon, 20 Jan 2020 15:26:25 -0500 Subject: [PATCH 19/21] Change drafts to store which accounts was used to create them When loading a draft: If the draft was created from a different account than the current one: If the draft was in reply to a status, don't allow it to be loaded. If the draft was not in reply to a status, prompt the user whether or not to load the draft. If the draft was in reply to a different status than the current one: Prompt the user whether or not to load the draft. Otherwise, load the draft. Draft replies created from other accounts can't be loaded from different accounts because the status for inReplyToID of the draft will have a different instance-local ID if the two accounts are on different instances. See #16 --- Tusker/DraftsManager.swift | 11 +++-- Tusker/LocalData.swift | 4 ++ .../Compose/ComposeViewController.swift | 47 +++++++++++++++++-- 3 files changed, 54 insertions(+), 8 deletions(-) diff --git a/Tusker/DraftsManager.swift b/Tusker/DraftsManager.swift index 23e9a5ebb5..39d182cabe 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/LocalData.swift b/Tusker/LocalData.swift index 7aafa46144..f1fbfbd386 100644 --- a/Tusker/LocalData.swift +++ b/Tusker/LocalData.swift @@ -101,6 +101,10 @@ class LocalData: ObservableObject { accounts.removeAll(where: { $0.id == info.id }) } + func getAccount(id: String) -> UserAccountInfo? { + return accounts.first(where: { $0.id == id }) + } + func getMostRecentAccount() -> UserAccountInfo? { guard onboardingComplete else { return nil } let mostRecent: UserAccountInfo? diff --git a/Tusker/Screens/Compose/ComposeViewController.swift b/Tusker/Screens/Compose/ComposeViewController.swift index 7efb15cc2e..5e96dc2206 100644 --- a/Tusker/Screens/Compose/ComposeViewController.swift +++ b/Tusker/Screens/Compose/ComposeViewController.swift @@ -368,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() } @@ -622,8 +623,46 @@ extension ComposeViewController: DraftsTableViewControllerDelegate { } func shouldSelectDraft(_ draft: DraftsManager.Draft, completion: @escaping (Bool) -> Void) { - if draft.inReplyToID != self.inReplyToID { - // todo: better text for this + if draft.accountID != mastodonController.accountInfo!.id { + let currentAccount = mastodonController.accountInfo! + let currentAcct = "\(currentAccount.username)@\(currentAccount.instanceURL.host!)" + let otherAccount = LocalData.shared.getAccount(id: draft.accountID) + let otherAcct: String! + if let otherAccount = otherAccount { + otherAcct = "\(otherAccount.username)@\(otherAccount.instanceURL.host!)" + } else { + otherAcct = nil + } + + if draft.inReplyToID != nil { + let message: String + if otherAccount != nil { + message = "The selected draft is a reply from a different account, it cannot be loaded from this account. To use it, switch accounts to \(otherAcct!)" + } else { + message = "The selected draft is a reply from an account that has been logged-out of. It cannot be loaded." + } + let alertController = UIAlertController(title: "Reply from Different Account", message: message, preferredStyle: .alert) + alertController.addAction(UIAlertAction(title: "OK", style: .default, handler: { (_) in + completion(false) + })) + presentedViewController!.present(alertController, animated: true) + } else { + let message: String + if otherAccount != nil { + message = "The selected draft is from a different account (\(otherAcct!)) than your currently active account (\(currentAcct)). Do you wish to load it anyway?" + } else { + message = "The selected draft from an account that has been logged-out of." + } + let alertController = UIAlertController(title: "Draft from Different Account", message: message, preferredStyle: .alert) + alertController.addAction(UIAlertAction(title: "Cancel", style: .cancel, handler: { (_) in + completion(false) + })) + alertController.addAction(UIAlertAction(title: "Load Draft", style: .default, handler: { (_) in + completion(true) + })) + presentedViewController!.present(alertController, animated: true) + } + } else if draft.inReplyToID != self.inReplyToID { 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) From 4abda02b76675b8d9322c7482810aa94c8743cde Mon Sep 17 00:00:00 2001 From: Shadowfacts Date: Wed, 22 Jan 2020 22:27:58 -0500 Subject: [PATCH 20/21] Only show drafts from current account --- .../Compose/ComposeViewController.swift | 45 ++----------------- .../Drafts/DraftsTableViewController.swift | 15 +++++-- 2 files changed, 15 insertions(+), 45 deletions(-) diff --git a/Tusker/Screens/Compose/ComposeViewController.swift b/Tusker/Screens/Compose/ComposeViewController.swift index 5e96dc2206..d4382ef954 100644 --- a/Tusker/Screens/Compose/ComposeViewController.swift +++ b/Tusker/Screens/Compose/ComposeViewController.swift @@ -12,7 +12,7 @@ import Intents class ComposeViewController: UIViewController { - let mastodonController: MastodonController + weak var mastodonController: MastodonController! var inReplyToID: String? var accountsToMention = [String]() @@ -457,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) } @@ -623,46 +623,7 @@ extension ComposeViewController: DraftsTableViewControllerDelegate { } func shouldSelectDraft(_ draft: DraftsManager.Draft, completion: @escaping (Bool) -> Void) { - if draft.accountID != mastodonController.accountInfo!.id { - let currentAccount = mastodonController.accountInfo! - let currentAcct = "\(currentAccount.username)@\(currentAccount.instanceURL.host!)" - let otherAccount = LocalData.shared.getAccount(id: draft.accountID) - let otherAcct: String! - if let otherAccount = otherAccount { - otherAcct = "\(otherAccount.username)@\(otherAccount.instanceURL.host!)" - } else { - otherAcct = nil - } - - if draft.inReplyToID != nil { - let message: String - if otherAccount != nil { - message = "The selected draft is a reply from a different account, it cannot be loaded from this account. To use it, switch accounts to \(otherAcct!)" - } else { - message = "The selected draft is a reply from an account that has been logged-out of. It cannot be loaded." - } - let alertController = UIAlertController(title: "Reply from Different Account", message: message, preferredStyle: .alert) - alertController.addAction(UIAlertAction(title: "OK", style: .default, handler: { (_) in - completion(false) - })) - presentedViewController!.present(alertController, animated: true) - } else { - let message: String - if otherAccount != nil { - message = "The selected draft is from a different account (\(otherAcct!)) than your currently active account (\(currentAcct)). Do you wish to load it anyway?" - } else { - message = "The selected draft from an account that has been logged-out of." - } - let alertController = UIAlertController(title: "Draft from Different Account", message: message, preferredStyle: .alert) - alertController.addAction(UIAlertAction(title: "Cancel", style: .cancel, handler: { (_) in - completion(false) - })) - alertController.addAction(UIAlertAction(title: "Load Draft", style: .default, handler: { (_) in - completion(true) - })) - presentedViewController!.present(alertController, animated: true) - } - } else if draft.inReplyToID != self.inReplyToID { + if draft.inReplyToID != self.inReplyToID { 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 40f90faa34..6273d584d8 100644 --- a/Tusker/Screens/Compose/Drafts/DraftsTableViewController.swift +++ b/Tusker/Screens/Compose/Drafts/DraftsTableViewController.swift @@ -17,9 +17,14 @@ protocol DraftsTableViewControllerDelegate: class { class DraftsTableViewController: UITableViewController { + 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 { From 1d169bec6745dd6218b303883195eb3fe057d854 Mon Sep 17 00:00:00 2001 From: Shadowfacts Date: Wed, 22 Jan 2020 22:30:05 -0500 Subject: [PATCH 21/21] Fix statuses showing incorrect timestamps when switching accounts --- Tusker/Views/Status/TimelineStatusTableViewCell.swift | 1 - 1 file changed, 1 deletion(-) diff --git a/Tusker/Views/Status/TimelineStatusTableViewCell.swift b/Tusker/Views/Status/TimelineStatusTableViewCell.swift index 863b37bb5e..44a9680bc5 100644 --- a/Tusker/Views/Status/TimelineStatusTableViewCell.swift +++ b/Tusker/Views/Status/TimelineStatusTableViewCell.swift @@ -95,7 +95,6 @@ class TimelineStatusTableViewCell: BaseStatusTableViewCell { } func updateTimestamp() { - guard superview != nil else { return } guard let status = mastodonController.cache.status(for: statusID) else { fatalError("Missing cached status \(statusID!)") } timestampLabel.text = status.createdAt.timeAgoString()