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 89c9a573..e0de6b61 100644 --- a/Pachyderm/Client.swift +++ b/Pachyderm/Client.swift @@ -130,32 +130,32 @@ public class Client { } // MARK: - Self - public func getSelfAccount() -> Request { + public static func getSelfAccount() -> Request { return Request(method: .get, path: "/api/v1/accounts/verify_credentials") } - public func getFavourites() -> Request<[Status]> { + public static func getFavourites() -> Request<[Status]> { return Request<[Status]>(method: .get, path: "/api/v1/favourites") } - public func getRelationships(accounts: [String]? = nil) -> Request<[Relationship]> { + public static func getRelationships(accounts: [String]? = nil) -> Request<[Relationship]> { return Request<[Relationship]>(method: .get, path: "/api/v1/accounts/relationships", queryParameters: "id" => accounts) } - public func getInstance() -> Request { + public static func getInstance() -> Request { return Request(method: .get, path: "/api/v1/instance") } - public func getCustomEmoji() -> Request<[Emoji]> { + public static func getCustomEmoji() -> Request<[Emoji]> { return Request<[Emoji]>(method: .get, path: "/api/v1/custom_emojis") } // MARK: - Accounts - public func getAccount(id: String) -> Request { + public static func getAccount(id: String) -> Request { return Request(method: .get, path: "/api/v1/accounts/\(id)") } - public func searchForAccount(query: String, limit: Int? = nil, following: Bool? = nil) -> Request<[Account]> { + public static func searchForAccount(query: String, limit: Int? = nil, following: Bool? = nil) -> Request<[Account]> { return Request<[Account]>(method: .get, path: "/api/v1/accounts/search", queryParameters: [ "q" => query, "limit" => limit, @@ -164,32 +164,32 @@ public class Client { } // MARK: - Blocks - public func getBlocks() -> Request<[Account]> { + public static func getBlocks() -> Request<[Account]> { return Request<[Account]>(method: .get, path: "/api/v1/blocks") } - public func getDomainBlocks() -> Request<[String]> { + public static func getDomainBlocks() -> Request<[String]> { return Request<[String]>(method: .get, path: "api/v1/domain_blocks") } - public func block(domain: String) -> Request { + public static func block(domain: String) -> Request { return Request(method: .post, path: "/api/v1/domain_blocks", body: .parameters([ "domain" => domain ])) } - public func unblock(domain: String) -> Request { + public static func unblock(domain: String) -> Request { return Request(method: .delete, path: "/api/v1/domain_blocks", body: .parameters([ "domain" => domain ])) } // MARK: - Filters - public func getFilters() -> Request<[Filter]> { + public static func getFilters() -> Request<[Filter]> { return Request<[Filter]>(method: .get, path: "/api/v1/filters") } - public func createFilter(phrase: String, context: [Filter.Context], irreversible: Bool? = nil, wholeWord: Bool? = nil, expiresAt: Date? = nil) -> Request { + public static func createFilter(phrase: String, context: [Filter.Context], irreversible: Bool? = nil, wholeWord: Bool? = nil, expiresAt: Date? = nil) -> Request { return Request(method: .post, path: "/api/v1/filters", body: .parameters([ "phrase" => phrase, "irreversible" => irreversible, @@ -198,40 +198,40 @@ public class Client { ] + "context" => context.contextStrings)) } - public func getFilter(id: String) -> Request { + public static func getFilter(id: String) -> Request { return Request(method: .get, path: "/api/v1/filters/\(id)") } // MARK: - Follows - public func getFollowRequests(range: RequestRange = .default) -> Request<[Account]> { + public static func getFollowRequests(range: RequestRange = .default) -> Request<[Account]> { var request = Request<[Account]>(method: .get, path: "/api/v1/follow_requests") request.range = range return request } - public func getFollowSuggestions() -> Request<[Account]> { + public static func getFollowSuggestions() -> Request<[Account]> { return Request<[Account]>(method: .get, path: "/api/v1/suggestions") } - public func followRemote(acct: String) -> Request { + public static func followRemote(acct: String) -> Request { return Request(method: .post, path: "/api/v1/follows", body: .parameters(["uri" => acct])) } // MARK: - Lists - public func getLists() -> Request<[List]> { + public static func getLists() -> Request<[List]> { return Request<[List]>(method: .get, path: "/api/v1/lists") } - public func getList(id: String) -> Request { + public static func getList(id: String) -> Request { return Request(method: .get, path: "/api/v1/lists/\(id)") } - public func createList(title: String) -> Request { + public static func createList(title: String) -> Request { return Request(method: .post, path: "/api/v1/lists", body: .parameters(["title" => title])) } // MARK: - Media - public func upload(attachment: FormAttachment, description: String? = nil, focus: (Float, Float)? = nil) -> Request { + public static func upload(attachment: FormAttachment, description: String? = nil, focus: (Float, Float)? = nil) -> Request { return Request(method: .post, path: "/api/v1/media", body: .formData([ "description" => description, "focus" => focus @@ -239,14 +239,14 @@ public class Client { } // MARK: - Mutes - public func getMutes(range: RequestRange) -> Request<[Account]> { + public static func getMutes(range: RequestRange) -> Request<[Account]> { var request = Request<[Account]>(method: .get, path: "/api/v1/mutes") request.range = range return request } // MARK: - Notifications - public func getNotifications(excludeTypes: [Notification.Kind], range: RequestRange = .default) -> Request<[Notification]> { + public static func getNotifications(excludeTypes: [Notification.Kind], range: RequestRange = .default) -> Request<[Notification]> { var request = Request<[Notification]>(method: .get, path: "/api/v1/notifications", queryParameters: "exclude_types" => excludeTypes.map { $0.rawValue } ) @@ -254,16 +254,16 @@ public class Client { return request } - public func clearNotifications() -> Request { + public static func clearNotifications() -> Request { return Request(method: .post, path: "/api/v1/notifications/clear") } // MARK: - Reports - public func getReports() -> Request<[Report]> { + public static func getReports() -> Request<[Report]> { return Request<[Report]>(method: .get, path: "/api/v1/reports") } - public func report(account: Account, statuses: [Status], comment: String) -> Request { + public static func report(account: Account, statuses: [Status], comment: String) -> Request { return Request(method: .post, path: "/api/v1/reports", body: .parameters([ "account_id" => account.id, "comment" => comment @@ -271,7 +271,7 @@ public class Client { } // MARK: - Search - public func search(query: String, resolve: Bool? = nil, limit: Int? = nil) -> Request { + public static func search(query: String, resolve: Bool? = nil, limit: Int? = nil) -> Request { return Request(method: .get, path: "/api/v2/search", queryParameters: [ "q" => query, "resolve" => resolve, @@ -280,18 +280,18 @@ public class Client { } // MARK: - Statuses - public func getStatus(id: String) -> Request { + public static func getStatus(id: String) -> Request { return Request(method: .get, path: "/api/v1/statuses/\(id)") } - public func createStatus(text: String, - contentType: StatusContentType = .plain, - inReplyTo: String? = nil, - media: [Attachment]? = nil, - sensitive: Bool? = nil, - spoilerText: String? = nil, - visibility: Status.Visibility? = nil, - language: String? = nil) -> Request { + public static func createStatus(text: String, + contentType: StatusContentType = .plain, + inReplyTo: String? = nil, + media: [Attachment]? = nil, + sensitive: Bool? = nil, + spoilerText: String? = nil, + visibility: Status.Visibility? = nil, + language: String? = nil) -> Request { return Request(method: .post, path: "/api/v1/statuses", body: .parameters([ "status" => text, "content_type" => contentType.mimeType, @@ -304,13 +304,13 @@ public class Client { } // MARK: - Timelines - public func getStatuses(timeline: Timeline, range: RequestRange = .default) -> Request<[Status]> { + public static func getStatuses(timeline: Timeline, range: RequestRange = .default) -> Request<[Status]> { return timeline.request(range: range) } // MARK: Bookmarks - public func getBookmarks(range: RequestRange = .default) -> Request<[Status]> { + public static func getBookmarks(range: RequestRange = .default) -> Request<[Status]> { var request = Request<[Status]>(method: .get, path: "/api/v1/bookmarks") request.range = range return request diff --git a/Tusker/Activities/Account Activities/FollowAccountActivity.swift b/Tusker/Activities/Account Activities/FollowAccountActivity.swift index 6747ec0f..f0dc74f2 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 303042a5..94daf756 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 b553af06..df1fcfc9 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 66c56c10..f73d0474 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 0857348d..e88a3549 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 2fc2fa85..134e852d 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 bf376e9b..6c841c49 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 5d888f28..70b5482d 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 ec478fb9..142213af 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 4caace12..6dbabc22 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 1807dcf2..92410f58 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 02d487e7..a7f6fc12 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 ff7e2c1d..7484d1dd 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 c68ac440..ae6ccf35 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 9d48ca6f..8cad403c 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 ecf12bfd..45b520e0 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 e1388f42..ad77eed3 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 c5ccc6d2..9ccf9234 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 c5096da3..f38cacd2 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 d2587f43..8a5ca23f 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 f17fe0ac..f6b062e1 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 15be357b..a80e4254 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 15b42eab..d90d1920 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 9dd15d8b..44d85d1e 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 e12e2233..1b91a98e 100644 --- a/Tusker/Activities/Account Activities/AccountActivity.swift +++ b/Tusker/Activities/Account Activities/AccountActivity.swift @@ -9,7 +9,7 @@ import UIKit import Pachyderm -class AccountActivity: UIActivity { +class AccountActivity: MastodonActivity { override class var activityCategory: UIActivity.Category { return .action diff --git a/Tusker/Activities/Account Activities/FollowAccountActivity.swift b/Tusker/Activities/Account Activities/FollowAccountActivity.swift index f0dc74f2..7beb3523 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 0238b0ab..ea67c0b0 100644 --- a/Tusker/Activities/Account Activities/SendMesasgeActivity.swift +++ b/Tusker/Activities/Account Activities/SendMesasgeActivity.swift @@ -28,7 +28,7 @@ class SendMessageActivity: AccountActivity { override var activityViewController: UIViewController? { guard let account = account else { return nil } - return UINavigationController(rootViewController: ComposeViewController(mentioningAcct: account.acct)) + return UINavigationController(rootViewController: ComposeViewController(mentioningAcct: account.acct, mastodonController: mastodonController)) } } diff --git a/Tusker/Activities/Account Activities/UnfollowAccountActivity.swift b/Tusker/Activities/Account Activities/UnfollowAccountActivity.swift index 94daf756..344387d7 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 00000000..32275951 --- /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 df1fcfc9..8c64f007 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 f73d0474..3714a453 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 715b0a6e..3469b789 100644 --- a/Tusker/Activities/Status Activities/StatusActivity.swift +++ b/Tusker/Activities/Status Activities/StatusActivity.swift @@ -9,7 +9,7 @@ import UIKit import Pachyderm -class StatusActivity: UIActivity { +class StatusActivity: MastodonActivity { override class var activityCategory: UIActivity.Category { return .action diff --git a/Tusker/Activities/Status Activities/UnbookmarkStatusActivity.swift b/Tusker/Activities/Status Activities/UnbookmarkStatusActivity.swift index e88a3549..be22c5e1 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 134e852d..cbb717cb 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 68e6c4a8..6f7521aa 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 6c841c49..a916d777 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 70b5482d..1ac5eddf 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 8635a017..36fe72e2 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 142213af..a4370c05 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 6dbabc22..07a68169 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 92410f58..31bbc061 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 a7f6fc12..c278c2b4 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 7484d1dd..796c58f4 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 664d65ee..bf8dbac3 100644 --- a/Tusker/Screens/Lists/ListTimelineViewController.swift +++ b/Tusker/Screens/Lists/ListTimelineViewController.swift @@ -15,10 +15,10 @@ class ListTimelineViewController: TimelineTableViewController { var presentEditOnAppear = false - init(for list: List) { + init(for list: List, mastodonController: MastodonController) { self.list = list - super.init(for: .list(id: list.id)) + super.init(for: .list(id: list.id), mastodonController: mastodonController) title = list.title } @@ -42,7 +42,7 @@ class ListTimelineViewController: TimelineTableViewController { } func presentEdit(animated: Bool) { - let editListAccountsController = EditListAccountsViewController(list: list) + let editListAccountsController = EditListAccountsViewController(list: list, mastodonController: mastodonController) editListAccountsController.navigationItem.leftBarButtonItem = UIBarButtonItem(barButtonSystemItem: .done, target: self, action: #selector(doneButtonPressed)) let navController = UINavigationController(rootViewController: editListAccountsController) present(navController, animated: animated) diff --git a/Tusker/Screens/Main/MainTabBarViewController.swift b/Tusker/Screens/Main/MainTabBarViewController.swift index 02590765..d0d01655 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 3e503e1b..ac66701a 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 ae6ccf35..4bee64b2 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 7df60929..21daac5e 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 3e58ac59..7fef8706 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 45b520e0..e6b1d4b3 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 ad77eed3..45e97c4b 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 9ccf9234..e0d9195c 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 0313a11e..3dcd8787 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 405c998f..b5ee418d 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 f38cacd2..f110638f 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 7fd95c3a..a01e74db 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 b53373af..5701e1f9 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 a62072b2..2530ed17 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 e933d335..cd82f5f4 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 1cdced23..b53e2d02 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 387c7652..bd364d16 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 cafb5c13..f45025dc 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 8a5ca23f..b8865507 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 a3517792..c948f666 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 f6b062e1..f9b985c5 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 a80e4254..a42ddeb2 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 d90d1920..ead66b93 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 b5ee418d..df598ab8 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 f110638f..93362925 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 cd82f5f4..861b6b8a 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 f45025dc..b77cb152 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 b8865507..c83524ff 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 c948f666..c5dce34e 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 f9b985c5..6d1071e3 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 7beb3523..ae89aa7e 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 344387d7..493923d7 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 8c64f007..585f3471 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 3714a453..40ef6cfd 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 be22c5e1..8cce299b 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 cbb717cb..a22df120 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 a916d777..59aa82a6 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 1ac5eddf..0c22b4e2 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 36fe72e2..c001a316 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 a4370c05..ce83ee9a 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 07a68169..a6fc85d0 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 31bbc061..b225840d 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 796c58f4..be46daf8 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 4bee64b2..ce87c5a5 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 e6b1d4b3..2e58f49e 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 45e97c4b..9c973461 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 e0d9195c..dd60518c 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 93362925..c94b45d2 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 2c20e4e7..346d78a3 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 2530ed17..f7b04f20 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 861b6b8a..00d92346 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 015f069e..5ca7deb9 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 33483d06..cedde5f6 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 b77cb152..0c2e62cc 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 c83524ff..1674c639 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 c5dce34e..2b246c2f 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 6d1071e3..be586211 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 184fdb5a..6052ee95 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 a42ddeb2..b2f926dd 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 5c113e74..17bb3c69 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 ead66b93..426c4f55 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 44d85d1e..adf1defc 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 6f7521aa..1854af62 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 937a979e..48f051d6 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 00000000..5e3d7ac2 --- /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 0e9dea80..d0efdffa 100644 --- a/Tusker/Shortcuts/AppShortcutItems.swift +++ b/Tusker/Shortcuts/AppShortcutItems.swift @@ -45,7 +45,9 @@ enum AppShortcutItem: String, CaseIterable { case .composePost: tab = .compose } - let controller = (UIApplication.shared.delegate!.window!!.rootViewController as! MainTabBarViewController) + let scene = UIApplication.shared.connectedScenes.compactMap { $0 as? UIWindowScene }.first! + let window = scene.windows.first { $0.isKeyWindow }! + let controller = window.rootViewController as! MainTabBarViewController controller.select(tab: tab) } } diff --git a/Tusker/Shortcuts/UserActivityManager.swift b/Tusker/Shortcuts/UserActivityManager.swift index 5701e1f9..4b745f40 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 426c4f55..ad2feb5f 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 adf1defc..7fe41973 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 32275951..25fd5cc7 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 59aa82a6..5e2ae194 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 00000000..ce5ab857 --- /dev/null +++ b/Tusker/Extensions/UIApplication+Scenes.swift @@ -0,0 +1,25 @@ +// +// UIApplication+Scenes.swift +// Tusker +// +// Created by Shadowfacts on 1/7/20. +// Copyright © 2020 Shadowfacts. All rights reserved. +// + +import UIKit + +extension UIApplication { + + var activeScene: UIScene? { + connectedScenes.first { $0.activationState == .foregroundActive } + } + + var backgroundScene: UIScene? { + connectedScenes.first { $0.activationState == .background } + } + + var activeOrBackgroundScene: UIScene? { + activeScene ?? backgroundScene + } + +} diff --git a/Tusker/Extensions/UISceneSession+MastodonController.swift b/Tusker/Extensions/UISceneSession+MastodonController.swift new file mode 100644 index 00000000..efa4e638 --- /dev/null +++ b/Tusker/Extensions/UISceneSession+MastodonController.swift @@ -0,0 +1,32 @@ +// +// UISceneSession+MastodonController.swift +// Tusker +// +// Created by Shadowfacts on 1/7/20. +// Copyright © 2020 Shadowfacts. All rights reserved. +// + +import UIKit + +extension UISceneSession { + + var mastodonController: MastodonController? { + get { + return userInfo?["mastodonController"] as? MastodonController + } + set { + if let newValue = newValue { + if userInfo == nil { + userInfo = ["mastodonController": newValue] + } else { + userInfo!["mastodonController"] = newValue + } + } else { + if userInfo != nil { + userInfo?.removeValue(forKey: "mastodonController") + } + } + } + } + +} diff --git a/Tusker/LocalData.swift b/Tusker/LocalData.swift index f8ce0b10..6dad3f5b 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 5e3d7ac2..67c8c152 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 21daac5e..e799e2a6 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 0ac49c04..db3a9349 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 9df96be2..8f2078fa 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 7fef8706..09ac7884 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 df598ab8..ff809ba7 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 4b745f40..4c181663 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 ad2feb5f..0731fd06 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 e0341f45..dd2ce064 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 920e068f..161015b3 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 6dad3f5b..f3954fdd 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 67c8c152..d4b939fb 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 db3a9349..c4a6c12e 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 8f2078fa..38cbf993 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 8ddb9739..8ff37b65 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 f3954fdd..7aafa461 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 d4b939fb..b2a172a2 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 e799e2a6..9bb2af60 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 c4a6c12e..5b93c231 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 38cbf993..b30daa52 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 3f71e3f2..40f90faa 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 dd49bcae..932a257a 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 ac66701a..6a23f5c6 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 0be76809..11681d23 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 8ff37b65..ee7a9cac 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 ff809ba7..d5c19430 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 12022b00..9966b510 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 a01e74db..19fb6878 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 8915fe16..41f39603 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 cb93b6bd..cf01f6ef 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 e65b2d8d..41bfad86 100644 --- a/Tusker/Views/Attachments/AttachmentView.swift +++ b/Tusker/Views/Attachments/AttachmentView.swift @@ -11,13 +11,13 @@ import Pachyderm import Gifu import AVFoundation -protocol AttachmentViewDelegate { +protocol AttachmentViewDelegate: class { func showAttachmentsGallery(startingAt index: Int) } class AttachmentView: UIImageView, GIFAnimatable { - var delegate: AttachmentViewDelegate? + weak var delegate: AttachmentViewDelegate? var playImageView: UIImageView! @@ -71,8 +71,8 @@ class AttachmentView: UIImageView, GIFAnimatable { } func loadImage() { - ImageCache.attachments.get(attachment.url) { (data) in - guard let data = data else { return } + ImageCache.attachments.get(attachment.url) { [weak self] (data) in + guard let self = self, let data = data else { return } DispatchQueue.main.async { if self.attachment.url.pathExtension == "gif" { self.animate(withGIFData: data) diff --git a/Tusker/Views/Attachments/AttachmentsContainerView.swift b/Tusker/Views/Attachments/AttachmentsContainerView.swift index 5ca7deb9..d8e18f9c 100644 --- a/Tusker/Views/Attachments/AttachmentsContainerView.swift +++ b/Tusker/Views/Attachments/AttachmentsContainerView.swift @@ -11,7 +11,7 @@ import Pachyderm class AttachmentsContainerView: UIView { - var delegate: AttachmentViewDelegate? + weak var delegate: AttachmentViewDelegate? var statusID: String! var attachments: [Attachment]! diff --git a/Tusker/Views/Compose Media/ComposeMediaView.swift b/Tusker/Views/Compose Media/ComposeMediaView.swift index 54c1efd7..cd7ba31f 100644 --- a/Tusker/Views/Compose Media/ComposeMediaView.swift +++ b/Tusker/Views/Compose Media/ComposeMediaView.swift @@ -10,14 +10,14 @@ import UIKit import Photos import AVFoundation -protocol ComposeMediaViewDelegate { +protocol ComposeMediaViewDelegate: class { func didRemoveMedia(_ mediaView: ComposeMediaView) func descriptionTextViewDidChange(_ mediaView: ComposeMediaView) } class ComposeMediaView: UIView { - var delegate: ComposeMediaViewDelegate? + weak var delegate: ComposeMediaViewDelegate? @IBOutlet weak var imageView: UIImageView! @IBOutlet weak var descriptionTextView: UITextView! diff --git a/Tusker/Views/ContentTextView.swift b/Tusker/Views/ContentTextView.swift index ea700587..b36076dc 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 bd364d16..3a9008cf 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 cedde5f6..62256c38 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 0c2e62cc..0113d78e 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 1674c639..f62f71b1 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 a3df7918..bf740fa5 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 0b7f778e..a34f8d09 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 62256c38..d4ea85cb 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 0113d78e..90d982fd 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 f62f71b1..f31fe7a1 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 d4aa74ed..6e115f6c 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 0c22b4e2..9aec17c6 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 a34f8d09..6d43c28e 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 6e115f6c..6ffa7b64 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 5b93c231..3b4d7b4f 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 d5c19430..bd249a80 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 615568e9..1bca044d 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 00000000..93ef0959 --- /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 cf91aa9e..00000000 --- a/Tusker/SavedHashtagsManager.swift +++ /dev/null @@ -1,65 +0,0 @@ -// -// SavedHashtagsManager.swift -// Tusker -// -// Created by Shadowfacts on 12/19/19. -// Copyright © 2019 Shadowfacts. All rights reserved. -// - -import Foundation -import Pachyderm - -class SavedHashtagsManager: Codable { - private(set) static var shared: SavedHashtagsManager = load() - - private static var documentsDirectory = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first! - private static var archiveURL = SavedHashtagsManager.documentsDirectory.appendingPathComponent("saved_hashtags").appendingPathExtension("plist") - - static func save() { - DispatchQueue.global(qos: .utility).async { - let encoder = PropertyListEncoder() - let data = try? encoder.encode(shared) - try? data?.write(to: archiveURL, options: .noFileProtection) - } - } - - static func load() -> SavedHashtagsManager { - let decoder = PropertyListDecoder() - if let data = try? Data(contentsOf: archiveURL), - let savedHashtagsManager = try? decoder.decode(Self.self, from: data) { - return savedHashtagsManager - } - return SavedHashtagsManager() - } - - private init() {} - - private var savedHashtags: [Hashtag] = [] - var sorted: [Hashtag] { - return savedHashtags.sorted(by: { $0.name < $1.name }) - } - - func isSaved(_ hashtag: Hashtag) -> Bool { - return savedHashtags.contains(hashtag) - } - - func add(_ hashtag: Hashtag) { - if isSaved(hashtag) { - return - } - savedHashtags.append(hashtag) - SavedHashtagsManager.save() - NotificationCenter.default.post(name: .savedHashtagsChanged, object: nil) - } - - func remove(_ hashtag: Hashtag) { - guard isSaved(hashtag) else { return } - savedHashtags.removeAll(where: { $0.name == hashtag.name }) - SavedHashtagsManager.save() - NotificationCenter.default.post(name: .savedHashtagsChanged, object: nil) - } -} - -extension Foundation.Notification.Name { - static let savedHashtagsChanged = Notification.Name("savedHashtagsChanged") -} diff --git a/Tusker/SavedInstancesManager.swift b/Tusker/SavedInstancesManager.swift deleted file mode 100644 index 7bc466c1..00000000 --- a/Tusker/SavedInstancesManager.swift +++ /dev/null @@ -1,61 +0,0 @@ -// -// SavedInstancesManager.swift -// Tusker -// -// Created by Shadowfacts on 12/19/19. -// Copyright © 2019 Shadowfacts. All rights reserved. -// - -import Foundation - -class SavedInstanceManager: Codable { - private(set) static var shared: SavedInstanceManager = load() - - private static var documentsDirectory = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first! - private static var archiveURL = SavedInstanceManager.documentsDirectory.appendingPathComponent("saved_instances").appendingPathExtension("plist") - - static func save() { - DispatchQueue.global(qos: .utility).async { - let encoder = PropertyListEncoder() - let data = try? encoder.encode(shared) - try? data?.write(to: archiveURL, options: .noFileProtection) - } - } - - static func load() -> SavedInstanceManager { - let decoder = PropertyListDecoder() - if let data = try? Data(contentsOf: archiveURL), - let savedInstanceManager = try? decoder.decode(Self.self, from: data) { - return savedInstanceManager - } - return SavedInstanceManager() - } - - private init() {} - - private(set) var savedInstances: [URL] = [] - - func isSaved(_ url: URL) -> Bool { - return savedInstances.contains(url) - } - - func add(_ url: URL) { - if isSaved(url) { - return - } - savedInstances.append(url) - SavedInstanceManager.save() - NotificationCenter.default.post(name: .savedInstancesChanged, object: nil) - } - - func remove(_ url: URL) { - guard isSaved(url) else { return } - savedInstances.removeAll(where: { $0 == url }) - SavedInstanceManager.save() - NotificationCenter.default.post(name: .savedInstancesChanged, object: nil) - } -} - -extension Notification.Name { - static let savedInstancesChanged = Notification.Name("savedInstancesChanged") -} diff --git a/Tusker/Screens/Explore/AddSavedHashtagViewController.swift b/Tusker/Screens/Explore/AddSavedHashtagViewController.swift index f45eb025..d3b5c491 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 c278c2b4..fe820728 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 11681d23..b77e8bc9 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 3dcd8787..bf3f5a9b 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 bd249a80..0c99ae1d 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 93ef0959..6d4696f9 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 d3b5c491..65585f67 100644 --- a/Tusker/Screens/Explore/AddSavedHashtagViewController.swift +++ b/Tusker/Screens/Explore/AddSavedHashtagViewController.swift @@ -52,7 +52,7 @@ class AddSavedHashtagViewController: SearchResultsViewController { extension AddSavedHashtagViewController: SearchResultsViewControllerDelegate { func selectedSearchResult(hashtag: Hashtag) { - 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 fe820728..0254ddec 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 70a3efbf..abe07e43 100644 --- a/Tusker/Screens/FindInstanceViewController.swift +++ b/Tusker/Screens/FindInstanceViewController.swift @@ -10,8 +10,20 @@ import UIKit class FindInstanceViewController: InstanceSelectorTableViewController { + weak var parentMastodonController: MastodonController? + var instanceTimelineDelegate: InstanceTimelineViewControllerDelegate? + init(parentMastodonController: MastodonController) { + self.parentMastodonController = parentMastodonController + + super.init() + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + override func viewDidLoad() { super.viewDidLoad() @@ -32,7 +44,7 @@ class FindInstanceViewController: InstanceSelectorTableViewController { extension FindInstanceViewController: InstanceSelectorTableViewControllerDelegate { func didSelectInstance(url: URL) { - let instanceTimelineController = InstanceTimelineViewController(for: url) + let instanceTimelineController = InstanceTimelineViewController(for: url, parentMastodonController: parentMastodonController!) instanceTimelineController.delegate = instanceTimelineDelegate show(instanceTimelineController, sender: self) } diff --git a/Tusker/Screens/Timeline/HashtagTimelineViewController.swift b/Tusker/Screens/Timeline/HashtagTimelineViewController.swift index bf3f5a9b..ab8d5b6c 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 0c99ae1d..2694bf6f 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 d0abd874..7efb15cc 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 1242ae4b..12f24cd5 100644 --- a/Tusker/Views/Compose Status Reply/ComposeStatusReplyView.swift +++ b/Tusker/Views/Compose Status Reply/ComposeStatusReplyView.swift @@ -10,6 +10,8 @@ import UIKit import Pachyderm class ComposeStatusReplyView: UIView { + + weak var mastodonController: MastodonController? @IBOutlet weak var avatarImageView: UIImageView! @IBOutlet weak var displayNameLabel: UILabel! @@ -34,6 +36,7 @@ class ComposeStatusReplyView: UIView { func updateUI(for status: Status) { displayNameLabel.text = status.account.realDisplayName usernameLabel.text = "@\(status.account.acct)" + statusContentTextView.overrideMastodonController = mastodonController statusContentTextView.statusID = status.id ImageCache.avatars.get(status.account.avatar) { (data) in diff --git a/Tusker/Views/ContentTextView.swift b/Tusker/Views/ContentTextView.swift index b36076dc..3cd7a4ed 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 23e9a5eb..39d182ca 100644 --- a/Tusker/DraftsManager.swift +++ b/Tusker/DraftsManager.swift @@ -39,8 +39,8 @@ class DraftsManager: Codable { return drafts.sorted(by: { $0.lastModified > $1.lastModified }) } - func create(text: String, contentWarning: String?, inReplyToID: String?, attachments: [DraftAttachment]) -> Draft { - let draft = Draft(text: text, contentWarning: contentWarning, inReplyToID: inReplyToID, attachments: attachments) + func create(accountID: String, text: String, contentWarning: String?, inReplyToID: String?, attachments: [DraftAttachment]) -> Draft { + let draft = Draft(accountID: accountID, text: text, contentWarning: contentWarning, inReplyToID: inReplyToID, attachments: attachments) drafts.append(draft) return draft } @@ -55,14 +55,16 @@ class DraftsManager: Codable { extension DraftsManager { class Draft: Codable, Equatable { let id: UUID + private(set) var accountID: String private(set) var text: String private(set) var contentWarning: String? private(set) var attachments: [DraftAttachment] private(set) var inReplyToID: String? private(set) var lastModified: Date - init(text: String, contentWarning: String?, inReplyToID: String?, attachments: [DraftAttachment], lastModified: Date = Date()) { + init(accountID: String, text: String, contentWarning: String?, inReplyToID: String?, attachments: [DraftAttachment], lastModified: Date = Date()) { self.id = UUID() + self.accountID = accountID self.text = text self.contentWarning = contentWarning self.inReplyToID = inReplyToID @@ -70,7 +72,8 @@ extension DraftsManager { self.lastModified = lastModified } - func update(text: String, contentWarning: String?, attachments: [DraftAttachment]) { + func update(accountID: String, text: String, contentWarning: String?, attachments: [DraftAttachment]) { + self.accountID = accountID self.text = text self.contentWarning = contentWarning self.lastModified = Date() diff --git a/Tusker/LocalData.swift b/Tusker/LocalData.swift index 7aafa461..f1fbfbd3 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 7efb15cc..5e96dc22 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 5e96dc22..d4382ef9 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 40f90faa..6273d584 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 863b37bb..44a9680b 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()