Browse Source

Merge branch 'multiple-accounts'

master
Shadowfacts 1 month ago
parent
commit
3220436893
Signed by: Shadowfacts <me@shadowfacts.net> GPG Key ID: 94A5AB95422746E5
69 changed files with 1285 additions and 783 deletions
  1. 38
    38
      Pachyderm/Client.swift
  2. 20
    8
      Tusker.xcodeproj/project.pbxproj
  3. 1
    1
      Tusker/Activities/Account Activities/AccountActivity.swift
  4. 2
    2
      Tusker/Activities/Account Activities/FollowAccountActivity.swift
  5. 1
    1
      Tusker/Activities/Account Activities/SendMesasgeActivity.swift
  6. 2
    2
      Tusker/Activities/Account Activities/UnfollowAccountActivity.swift
  7. 16
    0
      Tusker/Activities/MastodonActivity.swift
  8. 2
    2
      Tusker/Activities/Status Activities/BookmarkStatusActivity.swift
  9. 2
    2
      Tusker/Activities/Status Activities/PinStatusActivity.swift
  10. 1
    1
      Tusker/Activities/Status Activities/StatusActivity.swift
  11. 2
    2
      Tusker/Activities/Status Activities/UnbookmarkStatusActivity.swift
  12. 2
    2
      Tusker/Activities/Status Activities/UnpinStatusActivity.swift
  13. 0
    108
      Tusker/AppDelegate.swift
  14. 53
    30
      Tusker/Controllers/MastodonController.swift
  15. 7
    4
      Tusker/DraftsManager.swift
  16. 25
    0
      Tusker/Extensions/UIApplication+Scenes.swift
  17. 32
    0
      Tusker/Extensions/UISceneSession+MastodonController.swift
  18. 40
    21
      Tusker/Info.plist
  19. 98
    35
      Tusker/LocalData.swift
  20. 49
    34
      Tusker/MastodonCache.swift
  21. 113
    0
      Tusker/SavedDataManager.swift
  22. 0
    65
      Tusker/SavedHashtagsManager.swift
  23. 0
    61
      Tusker/SavedInstancesManager.swift
  24. 151
    0
      Tusker/SceneDelegate.swift
  25. 8
    3
      Tusker/Screens/Account List/AccountListTableViewController.swift
  26. 21
    15
      Tusker/Screens/Bookmarks/BookmarksTableViewController.swift
  27. 24
    19
      Tusker/Screens/Compose/ComposeViewController.swift
  28. 14
    5
      Tusker/Screens/Compose/Drafts/DraftsTableViewController.swift
  29. 13
    9
      Tusker/Screens/Conversation/ConversationTableViewController.swift
  30. 1
    1
      Tusker/Screens/Explore/AddSavedHashtagViewController.swift
  31. 31
    21
      Tusker/Screens/Explore/ExploreViewController.swift
  32. 13
    1
      Tusker/Screens/FindInstanceViewController.swift
  33. 10
    7
      Tusker/Screens/Lists/EditListAccountsViewController.swift
  34. 3
    3
      Tusker/Screens/Lists/ListTimelineViewController.swift
  35. 18
    6
      Tusker/Screens/Main/MainTabBarViewController.swift
  36. 7
    3
      Tusker/Screens/Notifications/NotificationsPageViewController.swift
  37. 29
    25
      Tusker/Screens/Notifications/NotificationsTableViewController.swift
  38. 3
    3
      Tusker/Screens/Onboarding/InstanceSelectorTableViewController.swift
  39. 13
    11
      Tusker/Screens/Onboarding/OnboardingViewController.swift
  40. 58
    4
      Tusker/Screens/Preferences/PreferencesNavigationController.swift
  41. 31
    12
      Tusker/Screens/Preferences/PreferencesView.swift
  42. 4
    4
      Tusker/Screens/Profile/MyProfileTableViewController.swift
  43. 25
    19
      Tusker/Screens/Profile/ProfileTableViewController.swift
  44. 14
    9
      Tusker/Screens/Search/SearchResultsViewController.swift
  45. 12
    6
      Tusker/Screens/Status Action Account List/StatusActionAccountListTableViewController.swift
  46. 6
    6
      Tusker/Screens/Timeline/HashtagTimelineViewController.swift
  47. 26
    9
      Tusker/Screens/Timeline/InstanceTimelineViewController.swift
  48. 18
    15
      Tusker/Screens/Timeline/TimelineTableViewController.swift
  49. 8
    4
      Tusker/Screens/Timeline/TimelinesPageViewController.swift
  50. 6
    2
      Tusker/Screens/Utilities/Previewing.swift
  51. 3
    1
      Tusker/Shortcuts/AppShortcutItems.swift
  52. 16
    4
      Tusker/Shortcuts/UserActivityManager.swift
  53. 14
    12
      Tusker/TuskerNavigationDelegate.swift
  54. 8
    6
      Tusker/Views/Account Cell/AccountTableViewCell.swift
  55. 4
    4
      Tusker/Views/Attachments/AttachmentView.swift
  56. 2
    8
      Tusker/Views/Attachments/AttachmentsContainerView.swift
  57. 2
    2
      Tusker/Views/Compose Media/ComposeMediaView.swift
  58. 3
    0
      Tusker/Views/Compose Status Reply/ComposeStatusReplyView.swift
  59. 6
    5
      Tusker/Views/ContentTextView.swift
  60. 2
    2
      Tusker/Views/Hashtag Cell/HashtagTableViewCell.swift
  61. 15
    8
      Tusker/Views/Notifications/ActionNotificationGroupTableViewCell.swift
  62. 15
    9
      Tusker/Views/Notifications/FollowNotificationGroupTableViewCell.swift
  63. 15
    7
      Tusker/Views/Notifications/FollowRequestNotificationTableViewCell.swift
  64. 7
    6
      Tusker/Views/Profile Header/ProfileHeaderTableViewCell.swift
  65. 33
    20
      Tusker/Views/Status/BaseStatusTableViewCell.swift
  66. 1
    1
      Tusker/Views/Status/ConversationMainStatusTableViewCell.swift
  67. 27
    18
      Tusker/Views/Status/TimelineStatusTableViewCell.swift
  68. 6
    3
      Tusker/Views/StatusContentTextView.swift
  69. 33
    26
      Tusker/XCallbackURL/XCBActions.swift

+ 38
- 38
Pachyderm/Client.swift View File

@@ -130,32 +130,32 @@ public class Client {
130 130
     }
131 131
     
132 132
     // MARK: - Self
133
-    public func getSelfAccount() -> Request<Account> {
133
+    public static func getSelfAccount() -> Request<Account> {
134 134
         return Request<Account>(method: .get, path: "/api/v1/accounts/verify_credentials")
135 135
     }
136 136
     
137
-    public func getFavourites() -> Request<[Status]> {
137
+    public static func getFavourites() -> Request<[Status]> {
138 138
         return Request<[Status]>(method: .get, path: "/api/v1/favourites")
139 139
     }
140 140
     
141
-    public func getRelationships(accounts: [String]? = nil) -> Request<[Relationship]> {
141
+    public static func getRelationships(accounts: [String]? = nil) -> Request<[Relationship]> {
142 142
         return Request<[Relationship]>(method: .get, path: "/api/v1/accounts/relationships", queryParameters: "id" => accounts)
143 143
     }
144 144
     
145
-    public func getInstance() -> Request<Instance> {
145
+    public static func getInstance() -> Request<Instance> {
146 146
         return Request<Instance>(method: .get, path: "/api/v1/instance")
147 147
     }
148 148
     
149
-    public func getCustomEmoji() -> Request<[Emoji]> {
149
+    public static func getCustomEmoji() -> Request<[Emoji]> {
150 150
         return Request<[Emoji]>(method: .get, path: "/api/v1/custom_emojis")
151 151
     }
152 152
     
153 153
     // MARK: - Accounts
154
-    public func getAccount(id: String) -> Request<Account> {
154
+    public static func getAccount(id: String) -> Request<Account> {
155 155
         return Request<Account>(method: .get, path: "/api/v1/accounts/\(id)")
156 156
     }
157 157
     
158
-    public func searchForAccount(query: String, limit: Int? = nil, following: Bool? = nil) -> Request<[Account]> {
158
+    public static func searchForAccount(query: String, limit: Int? = nil, following: Bool? = nil) -> Request<[Account]> {
159 159
         return Request<[Account]>(method: .get, path: "/api/v1/accounts/search", queryParameters: [
160 160
                 "q" => query,
161 161
                 "limit" => limit,
@@ -164,32 +164,32 @@ public class Client {
164 164
     }
165 165
     
166 166
     // MARK: - Blocks
167
-    public func getBlocks() -> Request<[Account]> {
167
+    public static func getBlocks() -> Request<[Account]> {
168 168
         return Request<[Account]>(method: .get, path: "/api/v1/blocks")
169 169
     }
170 170
     
171
-    public func getDomainBlocks() -> Request<[String]> {
171
+    public static func getDomainBlocks() -> Request<[String]> {
172 172
         return Request<[String]>(method: .get, path: "api/v1/domain_blocks")
173 173
     }
174 174
     
175
-    public func block(domain: String) -> Request<Empty> {
175
+    public static func block(domain: String) -> Request<Empty> {
176 176
         return Request<Empty>(method: .post, path: "/api/v1/domain_blocks", body: .parameters([
177 177
                 "domain" => domain
178 178
             ]))
179 179
     }
180 180
     
181
-    public func unblock(domain: String) -> Request<Empty> {
181
+    public static func unblock(domain: String) -> Request<Empty> {
182 182
         return Request<Empty>(method: .delete, path: "/api/v1/domain_blocks", body: .parameters([
183 183
                 "domain" => domain
184 184
             ]))
185 185
     }
186 186
     
187 187
     // MARK: - Filters
188
-    public func getFilters() -> Request<[Filter]> {
188
+    public static func getFilters() -> Request<[Filter]> {
189 189
         return Request<[Filter]>(method: .get, path: "/api/v1/filters")
190 190
     }
191 191
     
192
-    public func createFilter(phrase: String, context: [Filter.Context], irreversible: Bool? = nil, wholeWord: Bool? = nil, expiresAt: Date? = nil) -> Request<Filter> {
192
+    public static func createFilter(phrase: String, context: [Filter.Context], irreversible: Bool? = nil, wholeWord: Bool? = nil, expiresAt: Date? = nil) -> Request<Filter> {
193 193
         return Request<Filter>(method: .post, path: "/api/v1/filters", body: .parameters([
194 194
                 "phrase" => phrase,
195 195
                 "irreversible" => irreversible,
@@ -198,40 +198,40 @@ public class Client {
198 198
             ] + "context" => context.contextStrings))
199 199
     }
200 200
     
201
-    public func getFilter(id: String) -> Request<Filter> {
201
+    public static func getFilter(id: String) -> Request<Filter> {
202 202
         return Request<Filter>(method: .get, path: "/api/v1/filters/\(id)")
203 203
     }
204 204
     
205 205
     // MARK: - Follows
206
-    public func getFollowRequests(range: RequestRange = .default) -> Request<[Account]> {
206
+    public static func getFollowRequests(range: RequestRange = .default) -> Request<[Account]> {
207 207
         var request = Request<[Account]>(method: .get, path: "/api/v1/follow_requests")
208 208
         request.range = range
209 209
         return request
210 210
     }
211 211
     
212
-    public func getFollowSuggestions() -> Request<[Account]> {
212
+    public static func getFollowSuggestions() -> Request<[Account]> {
213 213
         return Request<[Account]>(method: .get, path: "/api/v1/suggestions")
214 214
     }
215 215
     
216
-    public func followRemote(acct: String) -> Request<Account> {
216
+    public static func followRemote(acct: String) -> Request<Account> {
217 217
         return Request<Account>(method: .post, path: "/api/v1/follows", body: .parameters(["uri" => acct]))
218 218
     }
219 219
     
220 220
     // MARK: - Lists
221
-    public func getLists() -> Request<[List]> {
221
+    public static func getLists() -> Request<[List]> {
222 222
         return Request<[List]>(method: .get, path: "/api/v1/lists")
223 223
     }
224 224
     
225
-    public func getList(id: String) -> Request<List> {
225
+    public static func getList(id: String) -> Request<List> {
226 226
         return Request<List>(method: .get, path: "/api/v1/lists/\(id)")
227 227
     }
228 228
     
229
-    public func createList(title: String) -> Request<List> {
229
+    public static func createList(title: String) -> Request<List> {
230 230
         return Request<List>(method: .post, path: "/api/v1/lists", body: .parameters(["title" => title]))
231 231
     }
232 232
     
233 233
     // MARK: - Media
234
-    public func upload(attachment: FormAttachment, description: String? = nil, focus: (Float, Float)? = nil) -> Request<Attachment> {
234
+    public static func upload(attachment: FormAttachment, description: String? = nil, focus: (Float, Float)? = nil) -> Request<Attachment> {
235 235
         return Request<Attachment>(method: .post, path: "/api/v1/media", body: .formData([
236 236
                 "description" => description,
237 237
                 "focus" => focus
@@ -239,14 +239,14 @@ public class Client {
239 239
     }
240 240
     
241 241
     // MARK: - Mutes
242
-    public func getMutes(range: RequestRange) -> Request<[Account]> {
242
+    public static func getMutes(range: RequestRange) -> Request<[Account]> {
243 243
         var request = Request<[Account]>(method: .get, path: "/api/v1/mutes")
244 244
         request.range = range
245 245
         return request
246 246
     }
247 247
     
248 248
     // MARK: - Notifications
249
-    public func getNotifications(excludeTypes: [Notification.Kind], range: RequestRange = .default) -> Request<[Notification]> {
249
+    public static func getNotifications(excludeTypes: [Notification.Kind], range: RequestRange = .default) -> Request<[Notification]> {
250 250
         var request = Request<[Notification]>(method: .get, path: "/api/v1/notifications", queryParameters:
251 251
             "exclude_types" => excludeTypes.map { $0.rawValue }
252 252
         )
@@ -254,16 +254,16 @@ public class Client {
254 254
         return request
255 255
     }
256 256
     
257
-    public func clearNotifications() -> Request<Empty> {
257
+    public static func clearNotifications() -> Request<Empty> {
258 258
         return Request<Empty>(method: .post, path: "/api/v1/notifications/clear")
259 259
     }
260 260
     
261 261
     // MARK: - Reports
262
-    public func getReports() -> Request<[Report]> {
262
+    public static func getReports() -> Request<[Report]> {
263 263
         return Request<[Report]>(method: .get, path: "/api/v1/reports")
264 264
     }
265 265
     
266
-    public func report(account: Account, statuses: [Status], comment: String) -> Request<Report> {
266
+    public static func report(account: Account, statuses: [Status], comment: String) -> Request<Report> {
267 267
         return Request<Report>(method: .post, path: "/api/v1/reports", body: .parameters([
268 268
                 "account_id" => account.id,
269 269
                 "comment" => comment
@@ -271,7 +271,7 @@ public class Client {
271 271
     }
272 272
     
273 273
     // MARK: - Search
274
-    public func search(query: String, resolve: Bool? = nil, limit: Int? = nil) -> Request<SearchResults> {
274
+    public static func search(query: String, resolve: Bool? = nil, limit: Int? = nil) -> Request<SearchResults> {
275 275
         return Request<SearchResults>(method: .get, path: "/api/v2/search", queryParameters: [
276 276
                 "q" => query,
277 277
                 "resolve" => resolve,
@@ -280,18 +280,18 @@ public class Client {
280 280
     }
281 281
     
282 282
     // MARK: - Statuses
283
-    public func getStatus(id: String) -> Request<Status> {
283
+    public static func getStatus(id: String) -> Request<Status> {
284 284
         return Request<Status>(method: .get, path: "/api/v1/statuses/\(id)")
285 285
     }
286 286
     
287
-    public func createStatus(text: String,
288
-                             contentType: StatusContentType = .plain,
289
-                             inReplyTo: String? = nil,
290
-                             media: [Attachment]? = nil,
291
-                             sensitive: Bool? = nil,
292
-                             spoilerText: String? = nil,
293
-                             visibility: Status.Visibility? = nil,
294
-                             language: String? = nil) -> Request<Status> {
287
+    public static func createStatus(text: String,
288
+                                    contentType: StatusContentType = .plain,
289
+                                    inReplyTo: String? = nil,
290
+                                    media: [Attachment]? = nil,
291
+                                    sensitive: Bool? = nil,
292
+                                    spoilerText: String? = nil,
293
+                                    visibility: Status.Visibility? = nil,
294
+                                    language: String? = nil) -> Request<Status> {
295 295
         return Request<Status>(method: .post, path: "/api/v1/statuses", body: .parameters([
296 296
                 "status" => text,
297 297
                 "content_type" => contentType.mimeType,
@@ -304,13 +304,13 @@ public class Client {
304 304
     }
305 305
     
306 306
     // MARK: - Timelines
307
-    public func getStatuses(timeline: Timeline, range: RequestRange = .default) -> Request<[Status]> {
307
+    public static func getStatuses(timeline: Timeline, range: RequestRange = .default) -> Request<[Status]> {
308 308
         return timeline.request(range: range)
309 309
     }
310 310
     
311 311
     
312 312
     // MARK: Bookmarks
313
-    public func getBookmarks(range: RequestRange = .default) -> Request<[Status]> {
313
+    public static func getBookmarks(range: RequestRange = .default) -> Request<[Status]> {
314 314
         var request = Request<[Status]>(method: .get, path: "/api/v1/bookmarks")
315 315
         request.range = range
316 316
         return request

+ 20
- 8
Tusker.xcodeproj/project.pbxproj View File

@@ -122,6 +122,7 @@
122 122
 		D64BC18A23C16487000D0238 /* UnpinStatusActivity.swift in Sources */ = {isa = PBXBuildFile; fileRef = D64BC18923C16487000D0238 /* UnpinStatusActivity.swift */; };
123 123
 		D64BC18F23C18B9D000D0238 /* FollowRequestNotificationTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D64BC18D23C18B9D000D0238 /* FollowRequestNotificationTableViewCell.swift */; };
124 124
 		D64BC19023C18B9D000D0238 /* FollowRequestNotificationTableViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = D64BC18E23C18B9D000D0238 /* FollowRequestNotificationTableViewCell.xib */; };
125
+		D64BC19223C271D9000D0238 /* MastodonActivity.swift in Sources */ = {isa = PBXBuildFile; fileRef = D64BC19123C271D9000D0238 /* MastodonActivity.swift */; };
125 126
 		D64D0AAD2128D88B005A6F37 /* LocalData.swift in Sources */ = {isa = PBXBuildFile; fileRef = D64D0AAC2128D88B005A6F37 /* LocalData.swift */; };
126 127
 		D64D0AB12128D9AE005A6F37 /* OnboardingViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D64D0AB02128D9AE005A6F37 /* OnboardingViewController.swift */; };
127 128
 		D64F80E2215875CC00BEF393 /* XCBActionType.swift in Sources */ = {isa = PBXBuildFile; fileRef = D64F80E1215875CC00BEF393 /* XCBActionType.swift */; };
@@ -156,10 +157,9 @@
156 157
 		D67C57B221E28FAD00C3118B /* ComposeStatusReplyView.xib in Resources */ = {isa = PBXBuildFile; fileRef = D67C57B121E28FAD00C3118B /* ComposeStatusReplyView.xib */; };
157 158
 		D67C57B421E2910700C3118B /* ComposeStatusReplyView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D67C57B321E2910700C3118B /* ComposeStatusReplyView.swift */; };
158 159
 		D68FEC4F232C5BC300C84F23 /* SegmentedPageViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D68FEC4E232C5BC300C84F23 /* SegmentedPageViewController.swift */; };
159
-		D6945C2F23AC47C3005C403C /* SavedHashtagsManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6945C2E23AC47C3005C403C /* SavedHashtagsManager.swift */; };
160
+		D6945C2F23AC47C3005C403C /* SavedDataManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6945C2E23AC47C3005C403C /* SavedDataManager.swift */; };
160 161
 		D6945C3223AC4D36005C403C /* HashtagTimelineViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6945C3123AC4D36005C403C /* HashtagTimelineViewController.swift */; };
161 162
 		D6945C3423AC6431005C403C /* AddSavedHashtagViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6945C3323AC6431005C403C /* AddSavedHashtagViewController.swift */; };
162
-		D6945C3623AC6C09005C403C /* SavedInstancesManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6945C3523AC6C09005C403C /* SavedInstancesManager.swift */; };
163 163
 		D6945C3823AC739F005C403C /* InstanceTimelineViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6945C3723AC739F005C403C /* InstanceTimelineViewController.swift */; };
164 164
 		D6945C3A23AC75E2005C403C /* FindInstanceViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6945C3923AC75E2005C403C /* FindInstanceViewController.swift */; };
165 165
 		D6A3BC7723218E1300FD64D5 /* TimelineSegment.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6A3BC7323218C6E00FD64D5 /* TimelineSegment.swift */; };
@@ -177,6 +177,7 @@
177 177
 		D6A5BB2F23BBAC97003BF21D /* DelegatingResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6A5BB2E23BBAC97003BF21D /* DelegatingResponse.swift */; };
178 178
 		D6A5BB3123BBAD87003BF21D /* JSONResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6A5BB3023BBAD87003BF21D /* JSONResponse.swift */; };
179 179
 		D6A5FAF1217B7E05003DB2D9 /* ComposeViewController.xib in Resources */ = {isa = PBXBuildFile; fileRef = D6A5FAF0217B7E05003DB2D9 /* ComposeViewController.xib */; };
180
+		D6AC956723C4347E008C9946 /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6AC956623C4347E008C9946 /* SceneDelegate.swift */; };
180 181
 		D6AEBB3E2321638100E5038B /* UIActivity+Types.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6AEBB3D2321638100E5038B /* UIActivity+Types.swift */; };
181 182
 		D6AEBB412321642700E5038B /* SendMesasgeActivity.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6AEBB402321642700E5038B /* SendMesasgeActivity.swift */; };
182 183
 		D6AEBB432321685E00E5038B /* OpenInSafariActivity.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6AEBB422321685E00E5038B /* OpenInSafariActivity.swift */; };
@@ -217,6 +218,8 @@
217 218
 		D6E0DC8E216EDF1E00369478 /* Previewing.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6E0DC8D216EDF1E00369478 /* Previewing.swift */; };
218 219
 		D6E6F26321603F8B006A8599 /* CharacterCounter.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6E6F26221603F8B006A8599 /* CharacterCounter.swift */; };
219 220
 		D6E6F26521604242006A8599 /* CharacterCounterTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6E6F26421604242006A8599 /* CharacterCounterTests.swift */; };
221
+		D6EBF01523C55C0900AE061B /* UIApplication+Scenes.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6EBF01423C55C0900AE061B /* UIApplication+Scenes.swift */; };
222
+		D6EBF01723C55E0D00AE061B /* UISceneSession+MastodonController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6EBF01623C55E0D00AE061B /* UISceneSession+MastodonController.swift */; };
220 223
 		D6F1F84D2193B56E00F5FE67 /* Cache.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6F1F84C2193B56E00F5FE67 /* Cache.swift */; };
221 224
 		D6F953EC212519E700CF0F2B /* TimelineTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6F953EB212519E700CF0F2B /* TimelineTableViewController.swift */; };
222 225
 		D6F953F021251A2900CF0F2B /* MastodonController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6F953EF21251A2900CF0F2B /* MastodonController.swift */; };
@@ -394,6 +397,7 @@
394 397
 		D64BC18923C16487000D0238 /* UnpinStatusActivity.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UnpinStatusActivity.swift; sourceTree = "<group>"; };
395 398
 		D64BC18D23C18B9D000D0238 /* FollowRequestNotificationTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FollowRequestNotificationTableViewCell.swift; sourceTree = "<group>"; };
396 399
 		D64BC18E23C18B9D000D0238 /* FollowRequestNotificationTableViewCell.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = FollowRequestNotificationTableViewCell.xib; sourceTree = "<group>"; };
400
+		D64BC19123C271D9000D0238 /* MastodonActivity.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonActivity.swift; sourceTree = "<group>"; };
397 401
 		D64D0AAC2128D88B005A6F37 /* LocalData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalData.swift; sourceTree = "<group>"; };
398 402
 		D64D0AB02128D9AE005A6F37 /* OnboardingViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingViewController.swift; sourceTree = "<group>"; };
399 403
 		D64F80E1215875CC00BEF393 /* XCBActionType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XCBActionType.swift; sourceTree = "<group>"; };
@@ -429,10 +433,9 @@
429 433
 		D67C57B121E28FAD00C3118B /* ComposeStatusReplyView.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = ComposeStatusReplyView.xib; sourceTree = "<group>"; };
430 434
 		D67C57B321E2910700C3118B /* ComposeStatusReplyView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeStatusReplyView.swift; sourceTree = "<group>"; };
431 435
 		D68FEC4E232C5BC300C84F23 /* SegmentedPageViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SegmentedPageViewController.swift; sourceTree = "<group>"; };
432
-		D6945C2E23AC47C3005C403C /* SavedHashtagsManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SavedHashtagsManager.swift; sourceTree = "<group>"; };
436
+		D6945C2E23AC47C3005C403C /* SavedDataManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SavedDataManager.swift; sourceTree = "<group>"; };
433 437
 		D6945C3123AC4D36005C403C /* HashtagTimelineViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HashtagTimelineViewController.swift; sourceTree = "<group>"; };
434 438
 		D6945C3323AC6431005C403C /* AddSavedHashtagViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddSavedHashtagViewController.swift; sourceTree = "<group>"; };
435
-		D6945C3523AC6C09005C403C /* SavedInstancesManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SavedInstancesManager.swift; sourceTree = "<group>"; };
436 439
 		D6945C3723AC739F005C403C /* InstanceTimelineViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InstanceTimelineViewController.swift; sourceTree = "<group>"; };
437 440
 		D6945C3923AC75E2005C403C /* FindInstanceViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = FindInstanceViewController.swift; path = Tusker/Screens/FindInstanceViewController.swift; sourceTree = SOURCE_ROOT; };
438 441
 		D6A3BC7323218C6E00FD64D5 /* TimelineSegment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineSegment.swift; sourceTree = "<group>"; };
@@ -450,6 +453,7 @@
450 453
 		D6A5BB2E23BBAC97003BF21D /* DelegatingResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DelegatingResponse.swift; sourceTree = "<group>"; };
451 454
 		D6A5BB3023BBAD87003BF21D /* JSONResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JSONResponse.swift; sourceTree = "<group>"; };
452 455
 		D6A5FAF0217B7E05003DB2D9 /* ComposeViewController.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = ComposeViewController.xib; sourceTree = "<group>"; };
456
+		D6AC956623C4347E008C9946 /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = "<group>"; };
453 457
 		D6AEBB3D2321638100E5038B /* UIActivity+Types.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIActivity+Types.swift"; sourceTree = "<group>"; };
454 458
 		D6AEBB402321642700E5038B /* SendMesasgeActivity.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SendMesasgeActivity.swift; sourceTree = "<group>"; };
455 459
 		D6AEBB422321685E00E5038B /* OpenInSafariActivity.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OpenInSafariActivity.swift; sourceTree = "<group>"; };
@@ -494,6 +498,8 @@
494 498
 		D6E0DC8D216EDF1E00369478 /* Previewing.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Previewing.swift; sourceTree = "<group>"; };
495 499
 		D6E6F26221603F8B006A8599 /* CharacterCounter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CharacterCounter.swift; sourceTree = "<group>"; };
496 500
 		D6E6F26421604242006A8599 /* CharacterCounterTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CharacterCounterTests.swift; sourceTree = "<group>"; };
501
+		D6EBF01423C55C0900AE061B /* UIApplication+Scenes.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIApplication+Scenes.swift"; sourceTree = "<group>"; };
502
+		D6EBF01623C55E0D00AE061B /* UISceneSession+MastodonController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UISceneSession+MastodonController.swift"; sourceTree = "<group>"; };
497 503
 		D6F1F84C2193B56E00F5FE67 /* Cache.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Cache.swift; sourceTree = "<group>"; };
498 504
 		D6F953EB212519E700CF0F2B /* TimelineTableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineTableViewController.swift; sourceTree = "<group>"; };
499 505
 		D6F953EF21251A2900CF0F2B /* MastodonController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonController.swift; sourceTree = "<group>"; };
@@ -967,6 +973,8 @@
967 973
 				D67C57AE21E28EAD00C3118B /* Array+Uniques.swift */,
968 974
 				0450531E22B0097E00100BA2 /* Timline+UI.swift */,
969 975
 				D66A77BA233838DC0058F1EC /* UIFont+Traits.swift */,
976
+				D6EBF01423C55C0900AE061B /* UIApplication+Scenes.swift */,
977
+				D6EBF01623C55E0D00AE061B /* UISceneSession+MastodonController.swift */,
970 978
 			);
971 979
 			path = Extensions;
972 980
 			sourceTree = "<group>";
@@ -1061,6 +1069,7 @@
1061 1069
 			children = (
1062 1070
 				D6AEBB3D2321638100E5038B /* UIActivity+Types.swift */,
1063 1071
 				D6AEBB422321685E00E5038B /* OpenInSafariActivity.swift */,
1072
+				D64BC19123C271D9000D0238 /* MastodonActivity.swift */,
1064 1073
 				D6AEBB4623216B0C00E5038B /* Account Activities */,
1065 1074
 				D627943323A5523800D38C68 /* Status Activities */,
1066 1075
 			);
@@ -1181,10 +1190,10 @@
1181 1190
 			isa = PBXGroup;
1182 1191
 			children = (
1183 1192
 				D6D4DDCF212518A000E1C4BB /* AppDelegate.swift */,
1193
+				D6AC956623C4347E008C9946 /* SceneDelegate.swift */,
1184 1194
 				D64D0AAC2128D88B005A6F37 /* LocalData.swift */,
1185 1195
 				D627FF75217E923E00CC0648 /* DraftsManager.swift */,
1186
-				D6945C2E23AC47C3005C403C /* SavedHashtagsManager.swift */,
1187
-				D6945C3523AC6C09005C403C /* SavedInstancesManager.swift */,
1196
+				D6945C2E23AC47C3005C403C /* SavedDataManager.swift */,
1188 1197
 				D6028B9A2150811100F223B9 /* MastodonCache.swift */,
1189 1198
 				D6C693EE216192C2007D6A6D /* TuskerNavigationDelegate.swift */,
1190 1199
 				D6F1F84E2193B9BE00F5FE67 /* Caching */,
@@ -1620,6 +1629,7 @@
1620 1629
 				0427033A22B31269000D31B6 /* AdvancedPrefsView.swift in Sources */,
1621 1630
 				D626493C23C1000300612E6E /* AlbumTableViewCell.swift in Sources */,
1622 1631
 				D6757A822157E8FA00721E32 /* XCBSession.swift in Sources */,
1632
+				D6EBF01723C55E0D00AE061B /* UISceneSession+MastodonController.swift in Sources */,
1623 1633
 				04DACE8C212CB14B009840C4 /* MainTabBarViewController.swift in Sources */,
1624 1634
 				D6AEBB412321642700E5038B /* SendMesasgeActivity.swift in Sources */,
1625 1635
 				D6BC9DB1232C61BC002CA326 /* NotificationsPageViewController.swift in Sources */,
@@ -1631,7 +1641,6 @@
1631 1641
 				D68FEC4F232C5BC300C84F23 /* SegmentedPageViewController.swift in Sources */,
1632 1642
 				0450531F22B0097E00100BA2 /* Timline+UI.swift in Sources */,
1633 1643
 				D667E5F52135BCD50057A976 /* ConversationTableViewController.swift in Sources */,
1634
-				D6945C3623AC6C09005C403C /* SavedInstancesManager.swift in Sources */,
1635 1644
 				D6C7D27D22B6EBF800071952 /* AttachmentsContainerView.swift in Sources */,
1636 1645
 				D620483823D38190008A63EF /* StatusContentTextView.swift in Sources */,
1637 1646
 				D6F953F021251A2900CF0F2B /* MastodonController.swift in Sources */,
@@ -1642,6 +1651,7 @@
1642 1651
 				D627943723A552C200D38C68 /* BookmarkStatusActivity.swift in Sources */,
1643 1652
 				D62D2426217ABF63005076CC /* UserActivityType.swift in Sources */,
1644 1653
 				D66362712136338600C9CBA2 /* ComposeViewController.swift in Sources */,
1654
+				D6AC956723C4347E008C9946 /* SceneDelegate.swift in Sources */,
1645 1655
 				D6BC9DD7232D7811002CA326 /* TimelinesPageViewController.swift in Sources */,
1646 1656
 				D620483623D38075008A63EF /* ContentTextView.swift in Sources */,
1647 1657
 				D6028B9B2150811100F223B9 /* MastodonCache.swift in Sources */,
@@ -1676,15 +1686,17 @@
1676 1686
 				D63661C02381C144004B9E16 /* PreferencesNavigationController.swift in Sources */,
1677 1687
 				D6B053A223BD2C0600A066FA /* AssetPickerViewController.swift in Sources */,
1678 1688
 				D627944A23A6AD6100D38C68 /* BookmarksTableViewController.swift in Sources */,
1689
+				D64BC19223C271D9000D0238 /* MastodonActivity.swift in Sources */,
1679 1690
 				D6945C3A23AC75E2005C403C /* FindInstanceViewController.swift in Sources */,
1680 1691
 				D6AEBB4523216AF800E5038B /* FollowAccountActivity.swift in Sources */,
1692
+				D6EBF01523C55C0900AE061B /* UIApplication+Scenes.swift in Sources */,
1681 1693
 				D6538945214D6D7500E3CEFC /* TableViewSwipeActionProvider.swift in Sources */,
1682 1694
 				D6E0DC8E216EDF1E00369478 /* Previewing.swift in Sources */,
1683 1695
 				D6BED174212667E900F02DA0 /* TimelineStatusTableViewCell.swift in Sources */,
1684 1696
 				0427033822B30F5F000D31B6 /* BehaviorPrefsView.swift in Sources */,
1685 1697
 				D627943923A553B600D38C68 /* UnbookmarkStatusActivity.swift in Sources */,
1686 1698
 				D64D0AAD2128D88B005A6F37 /* LocalData.swift in Sources */,
1687
-				D6945C2F23AC47C3005C403C /* SavedHashtagsManager.swift in Sources */,
1699
+				D6945C2F23AC47C3005C403C /* SavedDataManager.swift in Sources */,
1688 1700
 				D6C94D892139E6EC00CB5196 /* AttachmentView.swift in Sources */,
1689 1701
 				D6B053AE23BD322B00A066FA /* AssetPickerSheetContainerViewController.swift in Sources */,
1690 1702
 				D6C693EF216192C2007D6A6D /* TuskerNavigationDelegate.swift in Sources */,

+ 1
- 1
Tusker/Activities/Account Activities/AccountActivity.swift View File

@@ -9,7 +9,7 @@
9 9
 import UIKit
10 10
 import Pachyderm
11 11
 
12
-class AccountActivity: UIActivity {
12
+class AccountActivity: MastodonActivity {
13 13
 
14 14
     override class var activityCategory: UIActivity.Category {
15 15
         return .action

+ 2
- 2
Tusker/Activities/Account Activities/FollowAccountActivity.swift View File

@@ -28,9 +28,9 @@ class FollowAccountActivity: AccountActivity {
28 28
         UIImpactFeedbackGenerator(style: .medium).impactOccurred()
29 29
         
30 30
         let request = Account.follow(account.id)
31
-        MastodonController.client.run(request) { (response) in
31
+        mastodonController.run(request) { (response) in
32 32
             if case let .success(relationship, _) = response {
33
-                MastodonCache.add(relationship: relationship)
33
+                self.mastodonController.cache.add(relationship: relationship)
34 34
             } else {
35 35
                 // todo: display error message
36 36
                 UINotificationFeedbackGenerator().notificationOccurred(.error)

+ 1
- 1
Tusker/Activities/Account Activities/SendMesasgeActivity.swift View File

@@ -28,7 +28,7 @@ class SendMessageActivity: AccountActivity {
28 28
     override var activityViewController: UIViewController? {
29 29
         guard let account = account else { return nil }
30 30
         
31
-        return UINavigationController(rootViewController: ComposeViewController(mentioningAcct: account.acct))
31
+        return UINavigationController(rootViewController: ComposeViewController(mentioningAcct: account.acct, mastodonController: mastodonController))
32 32
     }
33 33
 
34 34
 }

+ 2
- 2
Tusker/Activities/Account Activities/UnfollowAccountActivity.swift View File

@@ -28,9 +28,9 @@ class UnfollowAccountActivity: AccountActivity {
28 28
         UIImpactFeedbackGenerator(style: .medium).impactOccurred()
29 29
         
30 30
         let request = Account.unfollow(account.id)
31
-        MastodonController.client.run(request) { (response) in
31
+        mastodonController.run(request) { (response) in
32 32
             if case let .success(relationship, _) = response {
33
-                MastodonCache.add(relationship: relationship)
33
+                self.mastodonController.cache.add(relationship: relationship)
34 34
             } else {
35 35
                 // todo: display error message
36 36
                 UINotificationFeedbackGenerator().notificationOccurred(.error)

+ 16
- 0
Tusker/Activities/MastodonActivity.swift View File

@@ -0,0 +1,16 @@
1
+//
2
+//  MastodonActivity.swift
3
+//  Tusker
4
+//
5
+//  Created by Shadowfacts on 1/5/20.
6
+//  Copyright © 2020 Shadowfacts. All rights reserved.
7
+//
8
+
9
+import UIKit
10
+
11
+class MastodonActivity: UIActivity {
12
+    var mastodonController: MastodonController {
13
+        let scene = UIApplication.shared.activeOrBackgroundScene!
14
+        return scene.session.mastodonController!
15
+    }
16
+}

+ 2
- 2
Tusker/Activities/Status Activities/BookmarkStatusActivity.swift View File

@@ -27,9 +27,9 @@ class BookmarkStatusActivity: StatusActivity {
27 27
         guard let status = status else { return }
28 28
         
29 29
         let request = Status.bookmark(status)
30
-        MastodonController.client.run(request) { (response) in
30
+        mastodonController.run(request) { (response) in
31 31
             if case let .success(status, _) = response {
32
-                MastodonCache.add(status: status)
32
+                self.mastodonController.cache.add(status: status)
33 33
             } else {
34 34
                 // todo: display error message
35 35
                 UINotificationFeedbackGenerator().notificationOccurred(.error)

+ 2
- 2
Tusker/Activities/Status Activities/PinStatusActivity.swift View File

@@ -26,9 +26,9 @@ class PinStatusActivity: StatusActivity {
26 26
         guard let status = status else { return }
27 27
         
28 28
         let request = Status.pin(status)
29
-        MastodonController.client.run(request) { (response) in
29
+        mastodonController.run(request) { (response) in
30 30
             if case let .success(status, _) = response {
31
-                MastodonCache.add(status: status)
31
+                self.mastodonController.cache.add(status: status)
32 32
             } else {
33 33
                 // todo: display error message
34 34
                 UINotificationFeedbackGenerator().notificationOccurred(.error)

+ 1
- 1
Tusker/Activities/Status Activities/StatusActivity.swift View File

@@ -9,7 +9,7 @@
9 9
 import UIKit
10 10
 import Pachyderm
11 11
 
12
-class StatusActivity: UIActivity {
12
+class StatusActivity: MastodonActivity {
13 13
 
14 14
     override class var activityCategory: UIActivity.Category {
15 15
         return .action

+ 2
- 2
Tusker/Activities/Status Activities/UnbookmarkStatusActivity.swift View File

@@ -27,9 +27,9 @@ class UnbookmarkStatusActivity: StatusActivity {
27 27
         guard let status = status else { return }
28 28
         
29 29
         let request = Status.unbookmark(status)
30
-        MastodonController.client.run(request) { (response) in
30
+        mastodonController.run(request) { (response) in
31 31
             if case let .success(status, _) = response {
32
-                MastodonCache.add(status: status)
32
+                self.mastodonController.cache.add(status: status)
33 33
             } else {
34 34
                 // todo: display error message
35 35
                 UINotificationFeedbackGenerator().notificationOccurred(.error)

+ 2
- 2
Tusker/Activities/Status Activities/UnpinStatusActivity.swift View File

@@ -26,9 +26,9 @@ class UnpinStatusActivity: StatusActivity {
26 26
         guard let status = status else { return }
27 27
         
28 28
         let request = Status.unpin(status)
29
-        MastodonController.client.run(request) { (response) in
29
+        mastodonController.run(request) { (response) in
30 30
             if case let .success(status, _) = response {
31
-                MastodonCache.add(status: status)
31
+                self.mastodonController.cache.add(status: status)
32 32
             } else {
33 33
                 // todo: display error message
34 34
                 UINotificationFeedbackGenerator().notificationOccurred(.error)

+ 0
- 108
Tusker/AppDelegate.swift View File

@@ -11,117 +11,9 @@ import UIKit
11 11
 @UIApplicationMain
12 12
 class AppDelegate: UIResponder, UIApplicationDelegate {
13 13
 
14
-    var window: UIWindow?
15
-
16 14
     func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
17 15
         AppShortcutItem.createItems(for: application)
18
-
19
-        window = UIWindow(frame: UIScreen.main.bounds)
20
-        
21
-        if LocalData.shared.onboardingComplete {
22
-            showAppUI()
23
-        } else {
24
-            showOnboardingUI()
25
-        }
26
-        
27
-        NotificationCenter.default.addObserver(self, selector: #selector(onUserLoggedOut), name: .userLoggedOut, object: nil)
28
-        
29
-        NotificationCenter.default.addObserver(self, selector: #selector(themePrefChanged), name: .themePreferenceChanged, object: nil)
30
-        themePrefChanged()
31
-        
32
-        window!.makeKeyAndVisible()
33
-        
34
-        if let shortcutItem = launchOptions?[.shortcutItem] as? UIApplicationShortcutItem {
35
-            _ = AppShortcutItem.handle(shortcutItem)
36
-        }
37
-        
38 16
         return true
39 17
     }
40 18
 
41
-    func application(_ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey : Any] = [:]) -> Bool {
42
-        if url.host == "x-callback-url" {
43
-            return XCBManager.handle(url: url)
44
-        } else if var components = URLComponents(url: url, resolvingAgainstBaseURL: false),
45
-            let tabBarController = window!.rootViewController as? MainTabBarViewController,
46
-            let exploreNavController = tabBarController.getTabController(tab: .explore) as? UINavigationController,
47
-            let exploreController = exploreNavController.viewControllers.first as? ExploreViewController {
48
-            
49
-            tabBarController.select(tab: .explore)
50
-            exploreNavController.popToRootViewController(animated: false)
51
-
52
-            exploreController.loadViewIfNeeded()
53
-            exploreController.searchController.isActive = true
54
-            
55
-            components.scheme = "https"
56
-            let query = components.url!.absoluteString
57
-            exploreController.searchController.searchBar.text = query
58
-            exploreController.resultsController.performSearch(query: query)
59
-            
60
-            return true
61
-        }
62
-        return false
63
-    }
64
-    
65
-    func application(_ application: UIApplication, continue userActivity: NSUserActivity, restorationHandler: @escaping ([UIUserActivityRestoring]?) -> Void) -> Bool {
66
-        return userActivity.handleResume()
67
-    }
68
-    
69
-    func application(_ application: UIApplication, performActionFor shortcutItem: UIApplicationShortcutItem, completionHandler: @escaping (Bool) -> Void) {
70
-        completionHandler(AppShortcutItem.handle(shortcutItem))
71
-    }
72
-    
73
-    func applicationWillResignActive(_ application: UIApplication) {
74
-        // 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.
75
-        // Use this method to pause ongoing tasks, disable timers, and invalidate graphics rendering callbacks. Games should use this method to pause the game.
76
-    }
77
-
78
-    func applicationDidEnterBackground(_ application: UIApplication) {
79
-        // 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.
80
-        // If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits.
81
-        Preferences.save()
82
-        DraftsManager.save()
83
-    }
84
-
85
-    func applicationWillEnterForeground(_ application: UIApplication) {
86
-        // 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.
87
-    }
88
-
89
-    func applicationDidBecomeActive(_ application: UIApplication) {
90
-        // 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.
91
-    }
92
-
93
-    func applicationWillTerminate(_ application: UIApplication) {
94
-        // Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:.
95
-    }
96
-    
97
-    func showAppUI() {
98
-        MastodonController.createClient()
99
-        MastodonController.getOwnAccount()
100
-        MastodonController.getOwnInstance()
101
-        
102
-        let tabBarController = MainTabBarViewController()
103
-        window!.rootViewController = tabBarController
104
-    }
105
-    
106
-    func showOnboardingUI() {
107
-        let onboarding = OnboardingViewController()
108
-        onboarding.onboardingDelegate = self
109
-        window!.rootViewController = onboarding
110
-    }
111
-    
112
-    @objc func onUserLoggedOut() {
113
-        showOnboardingUI()
114
-    }
115
-    
116
-    @objc func themePrefChanged() {
117
-        window?.overrideUserInterfaceStyle = Preferences.shared.theme
118
-    }
119
-
120
-}
121
-
122
-extension AppDelegate: OnboardingViewControllerDelegate {
123
-    func didFinishOnboarding() {
124
-        LocalData.shared.onboardingComplete = true
125
-        showAppUI()
126
-    }
127 19
 }

+ 53
- 30
Tusker/Controllers/MastodonController.swift View File

@@ -10,64 +10,87 @@ import Foundation
10 10
 import Pachyderm
11 11
 
12 12
 class MastodonController {
13
+
14
+    static private(set) var all = [LocalData.UserAccountInfo: MastodonController]()
15
+    
16
+    @available(*, message: "do something less dumb")
17
+    static var first: MastodonController { all.first!.value }
18
+    
19
+    static func getForAccount(_ account: LocalData.UserAccountInfo) -> MastodonController {
20
+        if let controller = all[account] {
21
+            return controller
22
+        } else {
23
+            let controller = MastodonController(instanceURL: account.instanceURL)
24
+            controller.accountInfo = account
25
+            controller.client.clientID = account.clientID
26
+            controller.client.clientSecret = account.clientSecret
27
+            controller.client.accessToken = account.accessToken
28
+            all[account] = controller
29
+            return controller
30
+        }
31
+    }
13 32
     
14
-    static var client: Client!
33
+    private(set) lazy var cache = MastodonCache(mastodonController: self)
15 34
     
16
-    static var account: Account!
17
-    static var instance: Instance!
35
+    let instanceURL: URL
36
+    private(set) var accountInfo: LocalData.UserAccountInfo?
18 37
     
19
-    private init() {}
38
+    let client: Client!
20 39
     
21
-    static func createClient() {
22
-        guard let url = LocalData.shared.instanceURL else { fatalError("Can't connect without instance URL") }
23
-        
24
-        client = Client(baseURL: url)
25
-        
26
-        client.clientID = LocalData.shared.clientID
27
-        client.clientSecret = LocalData.shared.clientSecret
28
-        client.accessToken = LocalData.shared.accessToken
40
+    var account: Account!
41
+    var instance: Instance!
42
+
43
+    init(instanceURL: URL) {
44
+        self.instanceURL = instanceURL
45
+        self.accountInfo = nil
46
+        self.client = Client(baseURL: instanceURL)
29 47
     }
30 48
     
31
-    static func registerApp(completion: @escaping () -> Void) {
32
-        guard LocalData.shared.clientID == nil,
33
-            LocalData.shared.clientSecret == nil else {
34
-                completion()
49
+    func run<Result>(_ request: Request<Result>, completion: @escaping Client.Callback<Result>) {
50
+        client.run(request, completion: completion)
51
+    }
52
+    
53
+    func registerApp(completion: @escaping (_ clientID: String, _ clientSecret: String) -> Void) {
54
+        guard client.clientID == nil,
55
+            client.clientSecret == nil else {
56
+                
57
+                completion(client.clientID!, client.clientSecret!)
35 58
                 return
36 59
         }
37
-        
60
+
38 61
         client.registerApp(name: "Tusker", redirectURI: "tusker://oauth", scopes: [.read, .write, .follow]) { response in
39 62
             guard case let .success(app, _) = response else { fatalError() }
40
-            LocalData.shared.clientID = app.clientID
41
-            LocalData.shared.clientSecret = app.clientSecret
42
-            completion()
63
+            self.client.clientID = app.clientID
64
+            self.client.clientSecret = app.clientSecret
65
+            completion(app.clientID, app.clientSecret)
43 66
         }
44 67
     }
45 68
     
46
-    static func authorize(authorizationCode: String, completion: @escaping () -> Void) {
69
+    func authorize(authorizationCode: String, completion: @escaping (_ accessToken: String) -> Void) {
47 70
         client.getAccessToken(authorizationCode: authorizationCode, redirectURI: "tusker://oauth") { response in
48 71
             guard case let .success(settings, _) = response else { fatalError() }
49
-            LocalData.shared.accessToken = settings.accessToken
50
-            completion()
72
+            self.client.accessToken = settings.accessToken
73
+            completion(settings.accessToken)
51 74
         }
52 75
     }
53 76
     
54
-    static func getOwnAccount(completion: ((Account) -> Void)? = nil) {
77
+    func getOwnAccount(completion: ((Account) -> Void)? = nil) {
55 78
         if account != nil {
56 79
             completion?(account)
57 80
         } else {
58
-            let request = client.getSelfAccount()
59
-            client.run(request) { response in
81
+            let request = Client.getSelfAccount()
82
+            run(request) { response in
60 83
                 guard case let .success(account, _) = response else { fatalError() }
61 84
                 self.account = account
62
-                MastodonCache.add(account: account)
85
+                self.cache.add(account: account)
63 86
                 completion?(account)
64 87
             }
65 88
         }
66 89
     }
67 90
     
68
-    static func getOwnInstance() {
69
-        let request = client.getInstance()
70
-        client.run(request) { (response) in
91
+    func getOwnInstance() {
92
+        let request = Client.getInstance()
93
+        run(request) { (response) in
71 94
             guard case let .success(instance, _) = response else { fatalError() }
72 95
             self.instance = instance
73 96
         }

+ 7
- 4
Tusker/DraftsManager.swift View File

@@ -39,8 +39,8 @@ class DraftsManager: Codable {
39 39
         return drafts.sorted(by: { $0.lastModified > $1.lastModified })
40 40
     }
41 41
     
42
-    func create(text: String, contentWarning: String?, inReplyToID: String?, attachments: [DraftAttachment]) -> Draft {
43
-        let draft = Draft(text: text, contentWarning: contentWarning, inReplyToID: inReplyToID, attachments: attachments)
42
+    func create(accountID: String, text: String, contentWarning: String?, inReplyToID: String?, attachments: [DraftAttachment]) -> Draft {
43
+        let draft = Draft(accountID: accountID, text: text, contentWarning: contentWarning, inReplyToID: inReplyToID, attachments: attachments)
44 44
         drafts.append(draft)
45 45
         return draft
46 46
     }
@@ -55,14 +55,16 @@ class DraftsManager: Codable {
55 55
 extension DraftsManager {
56 56
     class Draft: Codable, Equatable {
57 57
         let id: UUID
58
+        private(set) var accountID: String
58 59
         private(set) var text: String
59 60
         private(set) var contentWarning: String?
60 61
         private(set) var attachments: [DraftAttachment]
61 62
         private(set) var inReplyToID: String?
62 63
         private(set) var lastModified: Date
63 64
         
64
-        init(text: String, contentWarning: String?, inReplyToID: String?, attachments: [DraftAttachment], lastModified: Date = Date()) {
65
+        init(accountID: String, text: String, contentWarning: String?, inReplyToID: String?, attachments: [DraftAttachment], lastModified: Date = Date()) {
65 66
             self.id = UUID()
67
+            self.accountID = accountID
66 68
             self.text = text
67 69
             self.contentWarning = contentWarning
68 70
             self.inReplyToID = inReplyToID
@@ -70,7 +72,8 @@ extension DraftsManager {
70 72
             self.lastModified = lastModified
71 73
         }
72 74
         
73
-        func update(text: String, contentWarning: String?, attachments: [DraftAttachment]) {
75
+        func update(accountID: String, text: String, contentWarning: String?, attachments: [DraftAttachment]) {
76
+            self.accountID = accountID
74 77
             self.text = text
75 78
             self.contentWarning = contentWarning
76 79
             self.lastModified = Date()

+ 25
- 0
Tusker/Extensions/UIApplication+Scenes.swift View File

@@ -0,0 +1,25 @@
1
+//
2
+//  UIApplication+Scenes.swift
3
+//  Tusker
4
+//
5
+//  Created by Shadowfacts on 1/7/20.
6
+//  Copyright © 2020 Shadowfacts. All rights reserved.
7
+//
8
+
9
+import UIKit
10
+
11
+extension UIApplication {
12
+    
13
+    var activeScene: UIScene? {
14
+        connectedScenes.first { $0.activationState == .foregroundActive }
15
+    }
16
+    
17
+    var backgroundScene: UIScene? {
18
+        connectedScenes.first { $0.activationState == .background }
19
+    }
20
+    
21
+    var activeOrBackgroundScene: UIScene? {
22
+        activeScene ?? backgroundScene
23
+    }
24
+    
25
+}

+ 32
- 0
Tusker/Extensions/UISceneSession+MastodonController.swift View File

@@ -0,0 +1,32 @@
1
+//
2
+//  UISceneSession+MastodonController.swift
3
+//  Tusker
4
+//
5
+//  Created by Shadowfacts on 1/7/20.
6
+//  Copyright © 2020 Shadowfacts. All rights reserved.
7
+//
8
+
9
+import UIKit
10
+
11
+extension UISceneSession {
12
+    
13
+    var mastodonController: MastodonController? {
14
+        get {
15
+            return userInfo?["mastodonController"] as? MastodonController
16
+        }
17
+        set {
18
+            if let newValue = newValue {
19
+                if userInfo == nil {
20
+                    userInfo = ["mastodonController": newValue]
21
+                } else {
22
+                    userInfo!["mastodonController"] = newValue
23
+                }
24
+            } else {
25
+                if userInfo != nil {
26
+                    userInfo?.removeValue(forKey: "mastodonController")
27
+                }
28
+            }
29
+        }
30
+    }
31
+    
32
+}

+ 40
- 21
Tusker/Info.plist View File

@@ -2,25 +2,6 @@
2 2
 <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
3 3
 <plist version="1.0">
4 4
 <dict>
5
-	<key>NSAppTransportSecurity</key>
6
-	<dict>
7
-		<key>NSExceptionDomains</key>
8
-		<dict>
9
-			<key>localhost</key>
10
-			<dict>
11
-				<key>NSExceptionAllowsInsecureHTTPLoads</key>
12
-				<true/>
13
-			</dict>
14
-		</dict>
15
-	</dict>
16
-	<key>NSUserActivityTypes</key>
17
-	<array>
18
-		<string>$(PRODUCT_BUNDLE_IDENTIFIER).activity.show-timeline</string>
19
-		<string>$(PRODUCT_BUNDLE_IDENTIFIER).activity.check-notifications</string>
20
-		<string>$(PRODUCT_BUNDLE_IDENTIFIER).activity.check-mentions</string>
21
-		<string>$(PRODUCT_BUNDLE_IDENTIFIER).activity.new-post</string>
22
-		<string>$(PRODUCT_BUNDLE_IDENTIFIER).activity.search</string>
23
-	</array>
24 5
 	<key>CFBundleDevelopmentRegion</key>
25 6
 	<string>$(DEVELOPMENT_LANGUAGE)</string>
26 7
 	<key>CFBundleExecutable</key>
@@ -52,14 +33,52 @@
52 33
 	<string>1</string>
53 34
 	<key>LSRequiresIPhoneOS</key>
54 35
 	<true/>
55
-	<key>NSMicrophoneUsageDescription</key>
56
-	<string>Post videos from the camera.</string>
36
+	<key>NSAppTransportSecurity</key>
37
+	<dict>
38
+		<key>NSExceptionDomains</key>
39
+		<dict>
40
+			<key>localhost</key>
41
+			<dict>
42
+				<key>NSExceptionAllowsInsecureHTTPLoads</key>
43
+				<true/>
44
+			</dict>
45
+		</dict>
46
+	</dict>
57 47
 	<key>NSCameraUsageDescription</key>
58 48
 	<string>Post photos and videos from the camera.</string>
49
+	<key>NSMicrophoneUsageDescription</key>
50
+	<string>Post videos from the camera.</string>
59 51
 	<key>NSPhotoLibraryAddUsageDescription</key>
60 52
 	<string>Save photos directly from other people&apos;s posts.</string>
61 53
 	<key>NSPhotoLibraryUsageDescription</key>
62 54
 	<string>Post photos from the photo library.</string>
55
+	<key>NSUserActivityTypes</key>
56
+	<array>
57
+		<string>$(PRODUCT_BUNDLE_IDENTIFIER).activity.show-timeline</string>
58
+		<string>$(PRODUCT_BUNDLE_IDENTIFIER).activity.check-notifications</string>
59
+		<string>$(PRODUCT_BUNDLE_IDENTIFIER).activity.check-mentions</string>
60
+		<string>$(PRODUCT_BUNDLE_IDENTIFIER).activity.new-post</string>
61
+		<string>$(PRODUCT_BUNDLE_IDENTIFIER).activity.search</string>
62
+	</array>
63
+	<key>UIApplicationSceneManifest</key>
64
+	<dict>
65
+		<key>UISceneConfigurations</key>
66
+		<dict>
67
+			<key>UIWindowSceneSessionRoleApplication</key>
68
+			<array>
69
+				<dict>
70
+					<key>UISceneClassName</key>
71
+					<string>UIWindowScene</string>
72
+					<key>UISceneDelegateClassName</key>
73
+					<string>$(PRODUCT_MODULE_NAME).SceneDelegate</string>
74
+					<key>UISceneConfigurationName</key>
75
+					<string>main-scene</string>
76
+				</dict>
77
+			</array>
78
+		</dict>
79
+		<key>UIApplicationSupportsMultipleScenes</key>
80
+		<false/>
81
+	</dict>
63 82
 	<key>UILaunchStoryboardName</key>
64 83
 	<string>LaunchScreen</string>
65 84
 	<key>UIRequiredDeviceCapabilities</key>

+ 98
- 35
Tusker/LocalData.swift View File

@@ -7,8 +7,9 @@
7 7
 //
8 8
 
9 9
 import Foundation
10
+import Combine
10 11
 
11
-class LocalData {
12
+class LocalData: ObservableObject {
12 13
     
13 14
     static let shared = LocalData()
14 15
     
@@ -18,68 +19,130 @@ class LocalData {
18 19
         if ProcessInfo.processInfo.environment.keys.contains("UI_TESTING") {
19 20
             defaults = UserDefaults(suiteName: "\(Bundle.main.bundleIdentifier!).uitesting")!
20 21
             if ProcessInfo.processInfo.environment.keys.contains("UI_TESTING_LOGIN") {
21
-                defaults.set(true, forKey: onboardingCompleteKey)
22
-                defaults.set(URL(string: "http://localhost:8080")!, forKey: instanceURLKey)
23
-                defaults.set("client_id", forKey: clientIDKey)
24
-                defaults.set("client_secret", forKey: clientSecretKey)
25
-                defaults.set("access_token", forKey: accessTokenKey)
22
+                accounts = [
23
+                    UserAccountInfo(
24
+                        id: UUID().uuidString,
25
+                        instanceURL: URL(string: "http://localhost:8080")!,
26
+                        clientID: "client_id",
27
+                        clientSecret: "client_secret",
28
+                        username: "admin",
29
+                        accessToken: "access_token")
30
+                ]
26 31
             }
27 32
         } else {
28 33
             defaults = UserDefaults()
29 34
         }
30 35
     }
31 36
     
32
-    private let onboardingCompleteKey = "onboardingComplete"
33
-    var onboardingComplete: Bool {
37
+    private let accountsKey = "accounts"
38
+    var accounts: [UserAccountInfo] {
34 39
         get {
35
-            return defaults.bool(forKey: onboardingCompleteKey)
40
+            if let array = defaults.array(forKey: accountsKey) as? [[String: String]] {
41
+                return array.compactMap { (info) in
42
+                    guard let id = info["id"],
43
+                        let instanceURL = info["instanceURL"],
44
+                        let url = URL(string: instanceURL),
45
+                        let clientId = info["clientID"],
46
+                        let secret = info["clientSecret"],
47
+                        let username = info["username"],
48
+                        let accessToken = info["accessToken"] else {
49
+                            return nil
50
+                    }
51
+                    return UserAccountInfo(id: id, instanceURL: url, clientID: clientId, clientSecret: secret, username: username, accessToken: accessToken)
52
+                }
53
+            } else {
54
+                return []
55
+            }
36 56
         }
37 57
         set {
38
-            defaults.set(newValue, forKey: onboardingCompleteKey)
58
+            objectWillChange.send()
59
+            let array = newValue.map { (info) in
60
+                return [
61
+                    "id": info.id,
62
+                    "instanceURL": info.instanceURL.absoluteString,
63
+                    "clientID": info.clientID,
64
+                    "clientSecret": info.clientSecret,
65
+                    "username": info.username,
66
+                    "accessToken": info.accessToken
67
+                ]
68
+            }
69
+            defaults.set(array, forKey: accountsKey)
39 70
         }
40 71
     }
41 72
     
42
-    private let instanceURLKey = "instanceURL"
43
-    var instanceURL: URL? {
73
+    private let mostRecentAccountKey = "mostRecentAccount"
74
+    private var mostRecentAccount: String? {
44 75
         get {
45
-            return defaults.url(forKey: instanceURLKey)
76
+            return defaults.string(forKey: mostRecentAccountKey)
46 77
         }
47 78
         set {
48
-            defaults.set(newValue, forKey: instanceURLKey)
79
+            objectWillChange.send()
80
+            defaults.set(newValue, forKey: mostRecentAccountKey)
49 81
         }
50 82
     }
51 83
     
52
-    private let clientIDKey = "clientID"
53
-    var clientID: String? {
54
-        get {
55
-            return defaults.string(forKey: clientIDKey)
56
-        }
57
-        set {
58
-            defaults.set(newValue, forKey: clientIDKey)
59
-        }
84
+    var onboardingComplete: Bool {
85
+        return !accounts.isEmpty
60 86
     }
61 87
     
62
-    private let clientSecretKey = "clientSecret"
63
-    var clientSecret: String? {
64
-        get {
65
-            return defaults.string(forKey: clientSecretKey)
88
+    func addAccount(instanceURL url: URL, clientID: String, clientSecret secret: String, username: String, accessToken: String) -> UserAccountInfo {
89
+        var accounts = self.accounts
90
+        if let index = accounts.firstIndex(where: { $0.instanceURL == url && $0.username == username }) {
91
+            accounts.remove(at: index)
66 92
         }
67
-        set {
68
-            defaults.set(newValue, forKey: clientSecretKey)
93
+        let id = UUID().uuidString
94
+        let info = UserAccountInfo(id: id, instanceURL: url, clientID: clientID, clientSecret: secret, username: username, accessToken: accessToken)
95
+        accounts.append(info)
96
+        self.accounts = accounts
97
+        return info
98
+    }
99
+    
100
+    func removeAccount(_ info: UserAccountInfo) {
101
+        accounts.removeAll(where: { $0.id == info.id })
102
+    }
103
+    
104
+    func getAccount(id: String) -> UserAccountInfo? {
105
+        return accounts.first(where: { $0.id == id })
106
+    }
107
+    
108
+    func getMostRecentAccount() -> UserAccountInfo? {
109
+        guard onboardingComplete else { return nil }
110
+        let mostRecent: UserAccountInfo?
111
+        if let id = mostRecentAccount {
112
+            mostRecent = accounts.first { $0.id == id }
113
+        } else {
114
+            mostRecent = nil
69 115
         }
116
+        return mostRecent ?? accounts.first!
70 117
     }
71 118
     
72
-    private let accessTokenKey = "accessToken"
73
-    var accessToken: String? {
74
-        get {
75
-            return defaults.string(forKey: accessTokenKey)
119
+    func setMostRecentAccount(_ account: UserAccountInfo?) {
120
+        mostRecentAccount = account?.id
121
+    }
122
+
123
+}
124
+
125
+extension LocalData {
126
+    struct UserAccountInfo: Equatable, Hashable {
127
+        let id: String
128
+        let instanceURL: URL
129
+        let clientID: String
130
+        let clientSecret: String
131
+        let username: String
132
+        let accessToken: String
133
+        
134
+        func hash(into hasher: inout Hasher) {
135
+            hasher.combine(id)
76 136
         }
77
-        set {
78
-            defaults.set(newValue, forKey: accessTokenKey)
137
+        
138
+        static func ==(lhs: UserAccountInfo, rhs: UserAccountInfo) -> Bool {
139
+            return lhs.id == rhs.id
79 140
         }
80 141
     }
81 142
 }
82 143
 
83 144
 extension Notification.Name {
84
-    static let userLoggedOut = Notification.Name("userLoggedOut")
145
+    static let userLoggedOut = Notification.Name("Tusker.userLoggedOut")
146
+    static let addAccount = Notification.Name("Tusker.addAccount")
147
+    static let activateAccount = Notification.Name("Tusker.activateAccount")
85 148
 }

+ 49
- 34
Tusker/MastodonCache.swift View File

@@ -12,20 +12,26 @@ import Pachyderm
12 12
 
13 13
 class MastodonCache {
14 14
     
15
-    private static var statuses = CachedDictionary<Status>(name: "Statuses")
16
-    private static var accounts = CachedDictionary<Account>(name: "Accounts")
17
-    private static var relationships = CachedDictionary<Relationship>(name: "Relationships")
18
-    private static var notifications = CachedDictionary<Pachyderm.Notification>(name: "Notifications")
15
+    private var statuses = CachedDictionary<Status>(name: "Statuses")
16
+    private var accounts = CachedDictionary<Account>(name: "Accounts")
17
+    private var relationships = CachedDictionary<Relationship>(name: "Relationships")
18
+    private var notifications = CachedDictionary<Pachyderm.Notification>(name: "Notifications")
19 19
     
20
-    static let statusSubject = PassthroughSubject<Status, Never>()
21
-    static let accountSubject = PassthroughSubject<Account, Never>()
20
+    let statusSubject = PassthroughSubject<Status, Never>()
21
+    let accountSubject = PassthroughSubject<Account, Never>()
22
+
23
+    weak var mastodonController: MastodonController?
24
+    
25
+    init(mastodonController: MastodonController) {
26
+        self.mastodonController = mastodonController
27
+    }
22 28
 
23 29
     // MARK: - Statuses
24
-    static func status(for id: String) -> Status? {
30
+    func status(for id: String) -> Status? {
25 31
         return statuses[id]
26 32
     }
27 33
     
28
-    static func set(status: Status, for id: String) {
34
+    func set(status: Status, for id: String) {
29 35
         statuses[id] = status
30 36
         add(account: status.account)
31 37
         if let reblog = status.reblog {
@@ -36,100 +42,109 @@ class MastodonCache {
36 42
         statusSubject.send(status)
37 43
     }
38 44
     
39
-    static func status(for id: String, completion: @escaping (Status?) -> Void) {
40
-        let request = MastodonController.client.getStatus(id: id)
41
-        MastodonController.client.run(request) { response in
45
+    func status(for id: String, completion: @escaping (Status?) -> Void) {
46
+        guard let mastodonController = mastodonController else {
47
+            fatalError("The MastodonController for this cache has been deinitialized, so this cache should no longer exist. Are you storing a strong reference to it?")
48
+        }
49
+        let request = Client.getStatus(id: id)
50
+        mastodonController.run(request) { response in
42 51
             guard case let .success(status, _) = response else {
43 52
                 completion(nil)
44 53
                 return
45 54
             }
46
-            set(status: status, for: id)
55
+            self.set(status: status, for: id)
47 56
             completion(status)
48 57
         }
49 58
     }
50 59
     
51
-    static func add(status: Status) {
60
+    func add(status: Status) {
52 61
         set(status: status, for: status.id)
53 62
     }
54 63
     
55
-    static func addAll(statuses: [Status]) {
64
+    func addAll(statuses: [Status]) {
56 65
         statuses.forEach(add)
57 66
     }
58 67
     
59 68
     // MARK: - Accounts
60
-    static func account(for id: String) -> Account? {
69
+    func account(for id: String) -> Account? {
61 70
         return accounts[id]
62 71
     }
63 72
     
64
-    static func set(account: Account, for id: String) {
73
+    func set(account: Account, for id: String) {
65 74
         accounts[id] = account
66 75
         accountSubject.send(account)
67 76
     }
68 77
     
69
-    static func account(for id: String, completion: @escaping (Account?) -> Void) {
70
-        let request = MastodonController.client.getAccount(id: id)
71
-        MastodonController.client.run(request) { response in
78
+    func account(for id: String, completion: @escaping (Account?) -> Void) {
79
+        guard let mastodonController = mastodonController else {
80
+            fatalError("The MastodonController for this cache has been deinitialized, so this cache should no longer exist. Are you storing a strong reference to it?")
81
+        }
82
+        let request = Client.getAccount(id: id)
83
+        mastodonController.run(request) { response in
72 84
             guard case let .success(account, _) = response else {
73 85
                 completion(nil)
74 86
                 return
75 87
             }
76
-            set(account: account, for: account.id)
88
+            self.set(account: account, for: account.id)
77 89
             completion(account)
78 90
         }
79 91
     }
80 92
     
81
-    static func add(account: Account) {
93
+    func add(account: Account) {
82 94
         set(account: account, for: account.id)
83 95
     }
84 96
     
85
-    static func addAll(accounts: [Account]) {
97
+    func addAll(accounts: [Account]) {
86 98
         accounts.forEach(add)
87 99
     }
88 100
     
89 101
     // MARK: - Relationships
90
-    static func relationship(for id: String) -> Relationship? {
102
+    func relationship(for id: String) -> Relationship? {
91 103
         return relationships[id]
92 104
     }
93 105
     
94
-    static func set(relationship: Relationship, id: String) {
106
+    func set(relationship: Relationship, id: String) {
95 107
         relationships[id] = relationship
96 108
     }
97 109
     
98
-    static func relationship(for id: String, completion: @escaping (Relationship?) -> Void) {
99
-        let request = MastodonController.client.getRelationships(accounts: [id])
100
-        MastodonController.client.run(request) { response in
110
+    func relationship(for id: String, completion: @escaping (Relationship?) -> Void) {
111
+        guard let mastodonController = mastodonController else {
112
+            fatalError("The MastodonController for this cache has been deinitialized, so this cache should no longer exist. Are you storing a strong reference to it?")
113
+        }
114
+        let request = Client.getRelationships(accounts: [id])
115
+        mastodonController.run(request) { response in
101 116
             guard case let .success(relationships, _) = response,
102 117
                 let relationship = relationships.first else {
103 118
                 completion(nil)
104 119
                 return
105 120
             }
106
-            set(relationship: relationship, id: relationship.id)
121
+            self.set(relationship: relationship, id: relationship.id)
107 122
             completion(relationship)
108 123
         }
109 124
     }
110 125
     
111
-    static func add(relationship: Relationship) {
126
+    func add(relationship: Relationship) {
112 127
         set(relationship: relationship, id: relationship.id)
113 128
     }
114 129
     
115
-    static func addAll(relationships: [Relationship]) {
130
+    func addAll(relationships: [Relationship]) {
116 131
         relationships.forEach(add)
117 132
     }
118 133
     
119 134
     // MARK: - Notifications
120
-    static func notification(for id: String) -> Pachyderm.Notification? {
135
+    func notification(for id: String) -> Pachyderm.Notification? {
121 136
         return notifications[id]
122 137
     }
123 138
 
124
-    static func set(notification: Pachyderm.Notification, id: String) {
139
+    func set(notification: Pachyderm.Notification, id: String) {
125 140
         notifications[id] = notification
126 141
     }
127 142
     
128
-    static func add(notification: Pachyderm.Notification) {
143
+    func add(notification: Pachyderm.Notification) {
129 144
         set(notification: notification, id: notification.id)
130 145
     }
131 146
     
132
-    static func addAll(notifications: [Pachyderm.Notification]) {
147
+    func addAll(notifications: [Pachyderm.Notification]) {
133 148
         notifications.forEach(add)
134 149
     }
135 150
     

+ 113
- 0
Tusker/SavedDataManager.swift View File

@@ -0,0 +1,113 @@
1
+//
2
+//  SavedDataManager.swift
3
+//  Tusker
4
+//
5
+//  Created by Shadowfacts on 12/19/19.
6
+//  Copyright © 2019 Shadowfacts. All rights reserved.
7
+//
8
+
9
+import Foundation
10
+import Pachyderm
11
+
12
+class SavedDataManager: Codable {
13
+    private(set) static var shared: SavedDataManager = load()
14
+    
15
+    private static var documentsDirectory = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first!
16
+    private static var archiveURL = SavedDataManager.documentsDirectory.appendingPathComponent("saved_data").appendingPathExtension("plist")
17
+    
18
+    static func save() {
19
+        DispatchQueue.global(qos: .utility).async {
20
+            let encoder = PropertyListEncoder()
21
+            let data = try? encoder.encode(shared)
22
+            try? data?.write(to: archiveURL, options: .noFileProtection)
23
+        }
24
+    }
25
+    
26
+    static func load() -> SavedDataManager {
27
+        let decoder = PropertyListDecoder()
28
+        if let data = try? Data(contentsOf: archiveURL),
29
+            let savedHashtagsManager = try? decoder.decode(Self.self, from: data) {
30
+            return savedHashtagsManager
31
+        }
32
+        return SavedDataManager()
33
+    }
34
+    
35
+    private init() {}
36
+    
37
+    private var savedHashtags: [String: [Hashtag]] = [:] {
38
+        didSet {
39
+            SavedDataManager.save()
40
+            NotificationCenter.default.post(name: .savedHashtagsChanged, object: nil)
41
+        }
42
+    }
43
+
44
+    private var savedInstances: [String: [URL]] = [:] {
45
+        didSet {
46
+            SavedDataManager.save()
47
+            NotificationCenter.default.post(name: .savedInstancesChanged, object: nil)
48
+        }
49
+    }
50
+    
51
+    func sortedHashtags(for account: LocalData.UserAccountInfo) -> [Hashtag] {
52
+        if let hashtags = savedHashtags[account.id] {
53
+            return hashtags.sorted(by: { $0.name < $1.name })
54
+        } else {
55
+            return []
56
+        }
57
+    }
58
+    
59
+    func isSaved(hashtag: Hashtag, for account: LocalData.UserAccountInfo) -> Bool {
60
+        return savedHashtags[account.id]?.contains(hashtag) ?? false
61
+    }
62
+    
63
+    func add(hashtag: Hashtag, for account: LocalData.UserAccountInfo) {
64
+        if isSaved(hashtag: hashtag, for: account) {
65
+            return
66
+        }
67
+        if var saved = savedHashtags[account.id] {
68
+            saved.append(hashtag)
69
+            savedHashtags[account.id] = saved
70
+        } else {
71
+            savedHashtags[account.id] = [hashtag]
72
+        }
73
+    }
74
+    
75
+    func remove(hashtag: Hashtag, for account: LocalData.UserAccountInfo) {
76
+        guard isSaved(hashtag: hashtag, for: account) else { return }
77
+        if var saved = savedHashtags[account.id] {
78
+            saved.removeAll(where: { $0.name == hashtag.name })
79
+            savedHashtags[account.id] = saved
80
+        }
81
+    }
82
+    
83
+    func savedInstances(for account: LocalData.UserAccountInfo) -> [URL] {
84
+        return savedInstances[account.id] ?? []
85
+    }
86
+    
87
+    func isSaved(instance url: URL, for account: LocalData.UserAccountInfo) -> Bool {
88
+        return savedInstances[account.id]?.contains(url) ?? false
89
+    }
90
+    
91
+    func add(instance url: URL, for account: LocalData.UserAccountInfo) {
92
+        if isSaved(instance: url, for: account) { return }
93
+        if var saved = savedInstances[account.id] {
94
+            saved.append(url)
95
+            savedInstances[account.id] = saved
96
+        } else {
97
+            savedInstances[account.id] = [url]
98
+        }
99
+    }
100
+    
101
+    func remove(instance url: URL, for account: LocalData.UserAccountInfo) {
102
+        guard isSaved(instance: url, for: account) else { return }
103
+        if var saved = savedInstances[account.id] {
104
+            saved.removeAll(where: { $0 == url })
105
+            savedInstances[account.id] = saved
106
+        }
107
+    }
108
+}
109
+
110
+extension Foundation.Notification.Name {
111
+    static let savedHashtagsChanged = Notification.Name("savedHashtagsChanged")
112
+    static let savedInstancesChanged = Notification.Name("savedInstancesChanged")
113
+}

+ 0
- 65
Tusker/SavedHashtagsManager.swift View File

@@ -1,65 +0,0 @@
1
-//
2
-//  SavedHashtagsManager.swift
3
-//  Tusker
4
-//
5
-//  Created by Shadowfacts on 12/19/19.
6
-//  Copyright © 2019 Shadowfacts. All rights reserved.
7
-//
8
-
9
-import Foundation
10
-import Pachyderm
11
-
12
-class SavedHashtagsManager: Codable {
13
-    private(set) static var shared: SavedHashtagsManager = load()
14
-    
15
-    private static var documentsDirectory = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first!
16
-    private static var archiveURL = SavedHashtagsManager.documentsDirectory.appendingPathComponent("saved_hashtags").appendingPathExtension("plist")
17
-    
18
-    static func save() {
19
-        DispatchQueue.global(qos: .utility).async {
20
-            let encoder = PropertyListEncoder()
21
-            let data = try? encoder.encode(shared)
22
-            try? data?.write(to: archiveURL, options: .noFileProtection)
23
-        }
24
-    }
25
-    
26
-    static func load() -> SavedHashtagsManager {
27
-        let decoder = PropertyListDecoder()
28
-        if let data = try? Data(contentsOf: archiveURL),
29
-            let savedHashtagsManager = try? decoder.decode(Self.self, from: data) {
30
-            return savedHashtagsManager
31
-        }
32
-        return SavedHashtagsManager()
33
-    }
34
-    
35
-    private init() {}
36
-    
37
-    private var savedHashtags: [Hashtag] = []
38
-    var sorted: [Hashtag] {
39
-        return savedHashtags.sorted(by: { $0.name < $1.name })
40
-    }
41
-    
42
-    func isSaved(_ hashtag: Hashtag) -> Bool {
43
-        return savedHashtags.contains(hashtag)
44
-    }
45
-    
46
-    func add(_ hashtag: Hashtag) {
47
-        if isSaved(hashtag) {
48
-            return
49
-        }
50
-        savedHashtags.append(hashtag)
51
-        SavedHashtagsManager.save()
52
-        NotificationCenter.default.post(name: .savedHashtagsChanged, object: nil)
53
-    }
54
-    
55
-    func remove(_ hashtag: Hashtag) {
56
-        guard isSaved(hashtag) else { return }
57
-        savedHashtags.removeAll(where: { $0.name == hashtag.name })
58
-        SavedHashtagsManager.save()
59
-        NotificationCenter.default.post(name: .savedHashtagsChanged, object: nil)
60
-    }
61
-}
62
-
63
-extension Foundation.Notification.Name {
64
-    static let savedHashtagsChanged = Notification.Name("savedHashtagsChanged")
65
-}

+ 0
- 61
Tusker/SavedInstancesManager.swift View File

@@ -1,61 +0,0 @@
1
-//
2
-//  SavedInstancesManager.swift
3
-//  Tusker
4
-//
5
-//  Created by Shadowfacts on 12/19/19.
6
-//  Copyright © 2019 Shadowfacts. All rights reserved.
7
-//
8
-
9
-import Foundation
10
-
11
-class SavedInstanceManager: Codable {
12
-    private(set) static var shared: SavedInstanceManager = load()
13
-    
14
-    private static var documentsDirectory = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first!
15
-    private static var archiveURL = SavedInstanceManager.documentsDirectory.appendingPathComponent("saved_instances").appendingPathExtension("plist")
16
-    
17
-    static func save() {
18
-        DispatchQueue.global(qos: .utility).async {
19
-            let encoder = PropertyListEncoder()
20
-            let data = try? encoder.encode(shared)
21
-            try? data?.write(to: archiveURL, options: .noFileProtection)
22
-        }
23
-    }
24
-    
25
-    static func load() -> SavedInstanceManager {
26
-        let decoder = PropertyListDecoder()
27
-        if let data = try? Data(contentsOf: archiveURL),
28
-            let savedInstanceManager = try? decoder.decode(Self.self, from: data) {
29
-            return savedInstanceManager
30
-        }
31
-        return SavedInstanceManager()
32
-    }
33
-    
34
-    private init() {}
35
-    
36
-    private(set) var savedInstances: [URL] = []
37
-    
38
-    func isSaved(_ url: URL) -> Bool {
39
-        return savedInstances.contains(url)
40
-    }
41
-    
42
-    func add(_ url: URL) {
43
-        if isSaved(url) {
44
-            return
45
-        }
46
-        savedInstances.append(url)
47
-        SavedInstanceManager.save()
48
-        NotificationCenter.default.post(name: .savedInstancesChanged, object: nil)
49
-    }
50
-    
51
-    func remove(_ url: URL) {
52
-        guard isSaved(url) else { return }
53
-        savedInstances.removeAll(where: { $0 == url })
54
-        SavedInstanceManager.save()
55
-        NotificationCenter.default.post(name: .savedInstancesChanged, object: nil)
56
-    }
57
-}
58
-
59
-extension Notification.Name {
60
-    static let savedInstancesChanged = Notification.Name("savedInstancesChanged")
61
-}

+ 151
- 0
Tusker/SceneDelegate.swift View File

@@ -0,0 +1,151 @@
1
+//
2
+//  SceneDelegate.swift
3
+//  Tusker
4
+//
5
+//  Created by Shadowfacts on 1/6/20.
6
+//  Copyright © 2020 Shadowfacts. All rights reserved.
7
+//
8
+
9
+import UIKit
10
+import Pachyderm
11
+
12
+class SceneDelegate: UIResponder, UIWindowSceneDelegate {
13
+    
14
+    var window: UIWindow?
15
+
16
+    func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
17
+        // Use this method to optionally configure and attach the UIWindow `window` to the provided UIWindowScene `scene`.
18
+        // If using a storyboard, the `window` property will automatically be initialized and attached to the scene.
19
+        // This delegate does not imply the connecting scene or session are new (see `application:configurationForConnectingSceneSession` instead).
20
+        guard let windowScene = scene as? UIWindowScene else { return }
21
+        
22
+        window = UIWindow(windowScene: windowScene)
23
+        
24
+        if LocalData.shared.onboardingComplete {
25
+            if session.mastodonController == nil {
26
+                let account = LocalData.shared.getMostRecentAccount()!
27
+                session.mastodonController = MastodonController.getForAccount(account)
28
+            }
29
+            
30
+            showAppUI()
31
+        } else {
32
+            showOnboardingUI()
33
+        }
34
+        
35
+        window!.makeKeyAndVisible()
36
+        
37
+        NotificationCenter.default.addObserver(self, selector: #selector(themePrefChanged), name: .themePreferenceChanged, object: nil)
38
+        themePrefChanged()
39
+        
40
+        if let shortcutItem = connectionOptions.shortcutItem {
41
+            _ = AppShortcutItem.handle(shortcutItem)
42
+        }
43
+    }
44
+
45
+    func scene(_ scene: UIScene, openURLContexts URLContexts: Set<UIOpenURLContext>) {
46
+        if URLContexts.count > 1 {
47
+            fatalError("Cannot open more than 1 URL")
48
+        }
49
+        
50
+        let url = URLContexts.first!.url
51
+        
52
+        if url.host == "x-callback-url" {
53
+            _ = XCBManager.handle(url: url)
54
+        } else if var components = URLComponents(url: url, resolvingAgainstBaseURL: false),
55
+            let tabBarController = window!.rootViewController as? MainTabBarViewController,
56
+            let exploreNavController = tabBarController.getTabController(tab: .explore) as? UINavigationController,
57
+            let exploreController = exploreNavController.viewControllers.first as? ExploreViewController {
58
+            
59
+            tabBarController.select(tab: .explore)
60
+            exploreNavController.popToRootViewController(animated: false)
61
+            
62
+            exploreController.loadViewIfNeeded()
63
+            exploreController.searchController.isActive = true
64
+            
65
+            components.scheme = "https"
66
+            let query = url.absoluteString
67
+            exploreController.searchController.searchBar.text = query
68
+            exploreController.resultsController.performSearch(query: query)
69
+        }
70
+    }
71
+    
72
+    func scene(_ scene: UIScene, continue userActivity: NSUserActivity) {
73
+        _ = userActivity.handleResume()
74
+    }
75
+    
76
+    func windowScene(_ windowScene: UIWindowScene, performActionFor shortcutItem: UIApplicationShortcutItem, completionHandler: @escaping (Bool) -> Void) {
77
+        completionHandler(AppShortcutItem.handle(shortcutItem))
78
+    }
79
+    
80
+    func sceneDidDisconnect(_ scene: UIScene) {
81
+        // Called as the scene is being released by the system.
82
+        // This occurs shortly after the scene enters the background, or when its session is discarded.
83
+        // Release any resources associated with this scene that can be re-created the next time the scene connects.
84
+        // The scene may re-connect later, as its session was not neccessarily discarded (see `application:didDiscardSceneSessions` instead).
85
+    }
86
+    
87
+    func sceneDidBecomeActive(_ scene: UIScene) {
88
+        // Called when the scene has moved from an inactive state to an active state.
89
+        // Use this method to restart any tasks that were paused (or not yet started) when the scene was inactive.
90
+    }
91
+    
92
+    func sceneWillResignActive(_ scene: UIScene) {
93
+        // Called when the scene will move from an active state to an inactive state.
94
+        // This may occur due to temporary interruptions (ex. an incoming phone call).
95
+    }
96
+    
97
+    func sceneWillEnterForeground(_ scene: UIScene) {
98
+        // Called as the scene transitions from the background to the foreground.
99
+        // Use this method to undo the changes made on entering the background.
100
+    }
101
+    
102
+    func sceneDidEnterBackground(_ scene: UIScene) {
103
+        // Called as the scene transitions from the foreground to the background.
104
+        // Use this method to save data, release shared resources, and store enough scene-specific state information
105
+        // to restore the scene back to its current state.
106
+        
107
+        Preferences.save()
108
+        DraftsManager.save()
109
+    }
110
+    
111
+    func activateAccount(_ account: LocalData.UserAccountInfo) {
112
+        LocalData.shared.setMostRecentAccount(account)
113
+        window!.windowScene!.session.mastodonController = MastodonController.getForAccount(account)
114
+        showAppUI()
115
+    }
116
+    
117
+    func logoutCurrent() {
118
+        LocalData.shared.removeAccount(LocalData.shared.getMostRecentAccount()!)
119
+        if LocalData.shared.onboardingComplete {
120
+            activateAccount(LocalData.shared.accounts.first!)
121
+        } else {
122
+            showOnboardingUI()
123
+        }
124
+    }
125
+
126
+    func showAppUI() {
127
+        let mastodonController = window!.windowScene!.session.mastodonController!
128
+        mastodonController.getOwnAccount()
129
+        mastodonController.getOwnInstance()
130
+        
131
+        let tabBarController = MainTabBarViewController(mastodonController: mastodonController)
132
+        window!.rootViewController = tabBarController
133
+    }
134
+    
135
+    func showOnboardingUI() {
136
+        let onboarding = OnboardingViewController()
137
+        onboarding.onboardingDelegate = self
138
+        window!.rootViewController = onboarding
139
+    }
140
+    
141
+    @objc func themePrefChanged() {
142
+        window?.overrideUserInterfaceStyle = Preferences.shared.theme
143
+    }
144
+    
145
+}
146
+
147
+extension SceneDelegate: OnboardingViewControllerDelegate {
148
+    func didFinishOnboarding(account: LocalData.UserAccountInfo) {
149
+        activateAccount(account)
150
+    }
151
+}

+ 8
- 3
Tusker/Screens/Account List/AccountListTableViewController.swift View File

@@ -12,10 +12,13 @@ class AccountListTableViewController: EnhancedTableViewController {
12 12
         
13 13
     private let accountCell = "accountCell"
14 14
     
15
+    let mastodonController: MastodonController
16
+    
15 17
     let accountIDs: [String]
16 18
     
17
-    init(accountIDs: [String]) {
19
+    init(accountIDs: [String], mastodonController: MastodonController) {
18 20
         self.accountIDs = accountIDs
21
+        self.mastodonController = mastodonController
19 22
         
20 23
         super.init(style: .grouped)
21 24
     }
@@ -50,12 +53,14 @@ class AccountListTableViewController: EnhancedTableViewController {
50 53
         guard let cell = tableView.dequeueReusableCell(withIdentifier: accountCell, for: indexPath) as? AccountTableViewCell else { fatalError() }
51 54
         
52 55
         let id = accountIDs[indexPath.row]
53
-        cell.updateUI(accountID: id)
54 56
         cell.delegate = self
57
+        cell.updateUI(accountID: id)
55 58
 
56 59
         return cell
57 60
     }
58 61
 
59 62
 }
60 63
 
61
-extension AccountListTableViewController: TuskerNavigationDelegate {}
64
+extension AccountListTableViewController: TuskerNavigationDelegate {
65
+    var apiController: MastodonController { mastodonController }
66
+}

+ 21
- 15
Tusker/Screens/Bookmarks/BookmarksTableViewController.swift View File

@@ -13,6 +13,8 @@ class BookmarksTableViewController: EnhancedTableViewController {
13 13
     
14 14
     private let statusCell = "statusCell"
15 15
 
16
+    let mastodonController: MastodonController
17
+    
16 18
     var statuses: [(id: String, state: StatusState)] = [] {
17 19
         didSet {
18 20
             DispatchQueue.main.async {
@@ -24,7 +26,9 @@ class BookmarksTableViewController: EnhancedTableViewController {
24 26
     var newer: RequestRange?
25 27
     var older: RequestRange?
26 28
     
27
-    init() {
29
+    init(mastodonController: MastodonController) {
30
+        self.mastodonController = mastodonController
31
+        
28 32
         super.init(style: .plain)
29 33
         
30 34
         title = NSLocalizedString("Bookmarks", comment: "bookmarks screen title")
@@ -44,10 +48,10 @@ class BookmarksTableViewController: EnhancedTableViewController {
44 48
         
45 49
         tableView.prefetchDataSource = self
46 50
         
47
-        let request = MastodonController.client.getBookmarks()
48
-        MastodonController.client.run(request) { (response) in
51
+        let request = Client.getBookmarks()
52
+        mastodonController.run(request) { (response) in
49 53
             guard case let .success(statuses, pagination) = response else { fatalError() }
50
-            MastodonCache.addAll(statuses: statuses)
54
+            self.mastodonController.cache.addAll(statuses: statuses)
51 55
             self.statuses.append(contentsOf: statuses.map { ($0.id, .unknown) })
52 56
             self.newer = pagination?.newer
53 57
             self.older = pagination?.older
@@ -81,11 +85,11 @@ class BookmarksTableViewController: EnhancedTableViewController {
81 85
             return
82 86
         }
83 87
         
84
-        let request = MastodonController.client.getBookmarks(range: older)
85
-        MastodonController.client.run(request) { (response) in
88
+        let request = Client.getBookmarks(range: older)
89
+        mastodonController.run(request) { (response) in
86 90
             guard case let .success(newStatuses, pagination) = response else { fatalError() }
87 91
             self.older = pagination?.older
88
-            MastodonCache.addAll(statuses: newStatuses)
92
+            self.mastodonController.cache.addAll(statuses: newStatuses)
89 93
             self.statuses.append(contentsOf: newStatuses.map { ($0.id, .unknown) })
90 94
         }
91 95
     }
@@ -101,15 +105,15 @@ class BookmarksTableViewController: EnhancedTableViewController {
101 105
     override func tableView(_ tableView: UITableView, trailingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? {
102 106
         let cellConfig = (tableView.cellForRow(at: indexPath) as? TableViewSwipeActionProvider)?.trailingSwipeActionsConfiguration()
103 107
 
104
-        guard let status = MastodonCache.status(for: statuses[indexPath.row].id) else {
108
+        guard let status = mastodonController.cache.status(for: statuses[indexPath.row].id) else {
105 109
             return cellConfig
106 110
         }
107 111
         
108 112
         let unbookmarkAction = UIContextualAction(style: .destructive, title: NSLocalizedString("Unbookmark", comment: "unbookmark action title")) { (action, view, completion) in
109 113
             let request = Status.unbookmark(status)
110
-            MastodonController.client.run(request) { (response) in
114
+            self.mastodonController.run(request) { (response) in
111 115
                 guard case let .success(newStatus, _) = response else { fatalError() }
112
-                MastodonCache.add(status: newStatus)
116
+                self.mastodonController.cache.add(status: newStatus)
113 117
                 self.statuses.remove(at: indexPath.row)
114 118
             }
115 119
         }
@@ -127,13 +131,13 @@ class BookmarksTableViewController: EnhancedTableViewController {
127 131
     }
128 132
     
129 133
     override func getSuggestedContextMenuActions(tableView: UITableView, indexPath: IndexPath, point: CGPoint) -> [UIAction] {
130
-        guard let status = MastodonCache.status(for: statuses[indexPath.row].id) else { return [] }
134
+        guard let status = mastodonController.cache.status(for: statuses[indexPath.row].id) else { return [] }
131 135
         return [
132 136
             UIAction(title: NSLocalizedString("Unbookmark", comment: "unbookmark action title"), image: UIImage(systemName: "bookmark.fill"), identifier: .init("unbookmark"), discoverabilityTitle: nil, attributes: [], state: .off, handler: { (_) in
133 137
                 let request = Status.unbookmark(status)
134
-                MastodonController.client.run(request) { (response) in
138
+                self.mastodonController.run(request) { (response) in
135 139
                     guard case let .success(newStatus, _) = response else { fatalError() }
136
-                    MastodonCache.add(status: newStatus)
140
+                    self.mastodonController.cache.add(status: newStatus)
137 141
                     self.statuses.remove(at: indexPath.row)
138 142
                 }
139 143
             })
@@ -143,6 +147,8 @@ class BookmarksTableViewController: EnhancedTableViewController {
143 147
 }
144 148
 
145 149
 extension BookmarksTableViewController: StatusTableViewCellDelegate {
150
+    var apiController: MastodonController { mastodonController }
151
+    
146 152
     func statusCellCollapsedStateChanged(_ cell: BaseStatusTableViewCell) {
147 153
         tableView.beginUpdates()
148 154
         tableView.endUpdates()
@@ -152,7 +158,7 @@ extension BookmarksTableViewController: StatusTableViewCellDelegate {
152 158
 extension BookmarksTableViewController: UITableViewDataSourcePrefetching {
153 159
     func tableView(_ tableView: UITableView, prefetchRowsAt indexPaths: [IndexPath]) {
154 160
         for indexPath in indexPaths {
155
-            guard let status = MastodonCache.status(for: statuses[indexPath.row].id) else { continue }
161
+            guard let status = mastodonController.cache.status(for: statuses[indexPath.row].id) else { continue }
156 162
             ImageCache.avatars.get(status.account.avatar, completion: nil)
157 163
             for attachment in status.attachments where attachment.kind == .image {
158 164
                 ImageCache.attachments.get(attachment.url, completion: nil)
@@ -162,7 +168,7 @@ extension BookmarksTableViewController: UITableViewDataSourcePrefetching {
162 168
     
163 169
     func tableView(_ tableView: UITableView, cancelPrefetchingForRowsAt indexPaths: [IndexPath]) {
164 170
         for indexPath in indexPaths {
165
-            guard let status = MastodonCache.status(for: statuses[indexPath.row].id) else { continue }
171
+            guard let status = mastodonController.cache.status(for: statuses[indexPath.row].id) else { continue }
166 172
             ImageCache.avatars.cancel(status.account.avatar)
167 173
             for attachment in status.attachments where attachment.kind == .image {
168 174
                 ImageCache.attachments.cancel(attachment.url)

+ 24
- 19
Tusker/Screens/Compose/ComposeViewController.swift View File

@@ -12,6 +12,8 @@ import Intents
12 12
 
13 13
 class ComposeViewController: UIViewController {
14 14
     
15
+    weak var mastodonController: MastodonController!
16
+    
15 17
     var inReplyToID: String?
16 18
     var accountsToMention = [String]()
17 19
     var initialText: String?
@@ -70,9 +72,11 @@ class ComposeViewController: UIViewController {
70 72
     
71 73
     @IBOutlet weak var postProgressView: SteppedProgressView!
72 74
     
73
-    init(inReplyTo inReplyToID: String? = nil, mentioningAcct: String? = nil, text: String? = nil) {
75
+    init(inReplyTo inReplyToID: String? = nil, mentioningAcct: String? = nil, text: String? = nil, mastodonController: MastodonController) {
76
+        self.mastodonController = mastodonController
77
+        
74 78
         self.inReplyToID = inReplyToID
75
-        if let inReplyToID = inReplyToID, let inReplyTo = MastodonCache.status(for: inReplyToID) {
79
+        if let inReplyToID = inReplyToID, let inReplyTo = mastodonController.cache.status(for: inReplyToID) {
76 80
             accountsToMention = [inReplyTo.account.acct] + inReplyTo.mentions.map { $0.acct }
77 81
         } else {
78 82
             accountsToMention = []
@@ -80,7 +84,7 @@ class ComposeViewController: UIViewController {
80 84
         if let mentioningAcct = mentioningAcct {
81 85
             accountsToMention.append(mentioningAcct)
82 86
         }
83
-        if let ownAccount = MastodonController.account {
87
+        if let ownAccount = mastodonController.account {
84 88
             accountsToMention.removeAll(where: { acct in ownAccount.acct == acct })
85 89
         }
86 90
         accountsToMention = accountsToMention.uniques()
@@ -127,7 +131,7 @@ class ComposeViewController: UIViewController {
127 131
         statusTextView.text = accountsToMention.map({ acct in "@\(acct) " }).joined()
128 132
         initialText = statusTextView.text
129 133
         
130
-        MastodonController.getOwnAccount { (account) in
134
+        mastodonController.getOwnAccount { (account) in
131 135
             DispatchQueue.main.async {
132 136
                 self.selfDetailView.update(account: account)
133 137
             }
@@ -150,13 +154,13 @@ class ComposeViewController: UIViewController {
150 154
         }
151 155
         
152 156
         if let inReplyToID = inReplyToID {
153
-            if let status = MastodonCache.status(for: inReplyToID) {
157
+            if let status = mastodonController.cache.status(for: inReplyToID) {
154 158
                 updateInReplyTo(inReplyTo: status)
155 159
             } else {
156 160
                 let loadingVC = LoadingViewController()
157 161
                 embedChild(loadingVC)
158 162
                 
159
-                MastodonCache.status(for: inReplyToID) { (status) in
163
+                mastodonController.cache.status(for: inReplyToID) { (status) in
160 164
                     guard let status = status else { return }
161 165
                     DispatchQueue.main.async {
162 166
                         self.updateInReplyTo(inReplyTo: status)
@@ -189,6 +193,7 @@ class ComposeViewController: UIViewController {
189 193
         }
190 194
         
191 195
         let replyView = ComposeStatusReplyView.create()
196
+        replyView.mastodonController = mastodonController
192 197
         replyView.updateUI(for: inReplyTo)
193 198
         stackView.insertArrangedSubview(replyView, at: 0)
194 199
         
@@ -290,7 +295,7 @@ class ComposeViewController: UIViewController {
290 295
     func updateCharactersRemaining() {
291 296
         let count = CharacterCounter.count(text: statusTextView.text)
292 297
         let cwCount = contentWarningEnabled ? (contentWarningTextField.text?.count ?? 0) : 0
293
-        let remaining = (MastodonController.instance.maxStatusCharacters ?? 500) - count - cwCount
298
+        let remaining = (mastodonController.instance.maxStatusCharacters ?? 500) - count - cwCount
294 299
         if remaining < 0 {
295 300
             charactersRemainingLabel.textColor = .red
296 301
             compositionState.formUnion(.tooManyCharacters)
@@ -316,7 +321,7 @@ class ComposeViewController: UIViewController {
316 321
     }
317 322
     
318 323
     func updateAddAttachmentButton() {
319
-        switch MastodonController.instance.instanceType {
324
+        switch mastodonController.instance.instanceType {
320 325
         case .pleroma:
321 326
             addAttachmentButton.isEnabled = true
322 327
         case .mastodon:
@@ -363,10 +368,11 @@ class ComposeViewController: UIViewController {
363 368
             attachments.append(.init(attachment: attachment, description: description))
364 369
         }
365 370
         let cw = contentWarningEnabled ? contentWarningTextField.text : nil
371
+        let account = mastodonController.accountInfo!
366 372
         if let currentDraft = self.currentDraft {
367
-            currentDraft.update(text: self.statusTextView.text, contentWarning: cw, attachments: attachments)
373
+            currentDraft.update(accountID: account.id, text: self.statusTextView.text, contentWarning: cw, attachments: attachments)
368 374
         } else {
369
-            self.currentDraft = DraftsManager.shared.create(text: self.statusTextView.text, contentWarning: cw, inReplyToID: inReplyToID, attachments: attachments)
375
+            self.currentDraft = DraftsManager.shared.create(accountID: account.id, text: self.statusTextView.text, contentWarning: cw, inReplyToID: inReplyToID, attachments: attachments)
370 376
         }
371 377
         DraftsManager.save()
372 378
     }
@@ -451,7 +457,7 @@ class ComposeViewController: UIViewController {
451 457
     }
452 458
     
453 459
     @objc func draftsButtonPressed() {
454
-        let draftsVC = DraftsTableViewController()
460
+        let draftsVC = DraftsTableViewController(account: mastodonController.accountInfo!)
455 461
         draftsVC.delegate = self
456 462
         present(UINavigationController(rootViewController: draftsVC), animated: true)
457 463
     }
@@ -500,8 +506,8 @@ class ComposeViewController: UIViewController {
500 506
             compAttachment.getData { (data, mimeType) in
501 507
                 self.postProgressView.step()
502 508
                 
503
-                let request = MastodonController.client.upload(attachment: FormAttachment(mimeType: mimeType, data: data, fileName: "file"), description: description)
504
-                MastodonController.client.run(request) { (response) in
509
+                let request = Client.upload(attachment: FormAttachment(mimeType: mimeType, data: data, fileName: "file"), description: description)
510
+                self.mastodonController.run(request) { (response) in
505 511
                     guard case let .success(attachment, _) = response else { fatalError() }
506 512
 
507 513
                     attachments[index] = attachment
@@ -519,7 +525,7 @@ class ComposeViewController: UIViewController {
519 525
         group.notify(queue: .main) {
520 526
             let attachments = attachments.compactMap { $0 }
521 527
             
522
-            let request = MastodonController.client.createStatus(text: text,
528
+            let request = Client.createStatus(text: text,
523 529
                                                                  contentType: Preferences.shared.statusContentType,
524 530
                                                                  inReplyTo: self.inReplyToID,
525 531
                                                                  media: attachments,
@@ -527,10 +533,10 @@ class ComposeViewController: UIViewController {
527 533
                                                                  spoilerText: contentWarning,
528 534
                                                                  visibility: visibility,
529 535
                                                                  language: nil)
530
-            MastodonController.client.run(request) { (response) in
536
+            self.mastodonController.run(request) { (response) in
531 537
                 guard case let .success(status, _) = response else { fatalError() }
532 538
                 self.postedStatus = status
533
-                MastodonCache.add(status: status)
539
+                self.mastodonController.cache.add(status: status)
534 540
                 
535 541
                 if let draft = self.currentDraft {
536 542
                     DraftsManager.shared.remove(draft)
@@ -540,7 +546,7 @@ class ComposeViewController: UIViewController {
540 546
                     self.postProgressView.step()
541 547
                     self.dismiss(animated: true)
542 548
                     
543
-                    let conversationVC = ConversationTableViewController(for: status.id)
549
+                    let conversationVC = ConversationTableViewController(for: status.id, mastodonController: self.mastodonController)
544 550
                     self.show(conversationVC, sender: self)
545 551
                     
546 552
                     self.xcbSession?.complete(with: .success, additionalData: [
@@ -581,7 +587,7 @@ extension ComposeViewController: UITextViewDelegate {
581 587
 
582 588
 extension ComposeViewController: AssetPickerViewControllerDelegate {
583 589
     func assetPicker(_ assetPicker: AssetPickerViewController, shouldAllowAssetOfType type: CompositionAttachment.AttachmentType) -> Bool {
584
-        switch MastodonController.instance.instanceType {
590
+        switch mastodonController.instance.instanceType {
585 591
         case .pleroma:
586 592
             return true
587 593
         case .mastodon:
@@ -618,7 +624,6 @@ extension ComposeViewController: DraftsTableViewControllerDelegate {
618 624
     
619 625
     func shouldSelectDraft(_ draft: DraftsManager.Draft, completion: @escaping (Bool) -> Void) {
620 626
         if draft.inReplyToID != self.inReplyToID {
621
-            // todo: better text for this
622 627
             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)
623 628
             alertController.addAction(UIAlertAction(title: "Cancel", style: .cancel, handler: { (_) in
624 629
                 completion(false)

+ 14
- 5
Tusker/Screens/Compose/Drafts/DraftsTableViewController.swift View File

@@ -8,7 +8,7 @@
8 8
 
9 9
 import UIKit
10 10
 
11
-protocol DraftsTableViewControllerDelegate {
11
+protocol DraftsTableViewControllerDelegate: class {
12 12
     func draftSelectionCanceled()
13 13
     func shouldSelectDraft(_ draft: DraftsManager.Draft, completion: @escaping (Bool) -> Void)
14 14
     func draftSelected(_ draft: DraftsManager.Draft)
@@ -17,9 +17,14 @@ protocol DraftsTableViewControllerDelegate {
17 17
 
18 18
 class DraftsTableViewController: UITableViewController {
19 19
     
20
-    var delegate: DraftsTableViewControllerDelegate?
20
+    let account: LocalData.UserAccountInfo
21
+    weak var delegate: DraftsTableViewControllerDelegate?
21 22
     
22
-    init() {
23
+    var drafts = [DraftsManager.Draft]()
24
+    
25
+    init(account: LocalData.UserAccountInfo) {
26
+        self.account = account
27
+        
23 28
         super.init(nibName: "DraftsTableViewController", bundle: nil)
24 29
         
25 30
         title = "Drafts"
@@ -37,10 +42,14 @@ class DraftsTableViewController: UITableViewController {
37 42
         tableView.estimatedRowHeight = 140
38 43
         
39 44
         tableView.register(UINib(nibName: "DraftTableViewCell", bundle: nil), forCellReuseIdentifier: "draftCell")
45
+        
46
+        drafts = DraftsManager.shared.sorted.filter { (draft) in
47
+            draft.accountID == account.id
48
+        }
40 49
     }
41 50
     
42 51
     func draft(for indexPath: IndexPath) -> DraftsManager.Draft {
43
-        return DraftsManager.shared.sorted[indexPath.row]
52
+        return drafts[indexPath.row]
44 53
     }
45 54
     
46 55
     // MARK: - Table View Data Source
@@ -50,7 +59,7 @@ class DraftsTableViewController: UITableViewController {
50 59
     }
51 60
     
52 61
     override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
53
-        return DraftsManager.shared.drafts.count
62
+        return drafts.count
54 63
     }
55 64
     
56 65
     override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {

+ 13
- 9
Tusker/Screens/Conversation/ConversationTableViewController.swift View File

@@ -15,6 +15,8 @@ class ConversationTableViewController: EnhancedTableViewController {
15 15
     static let showPostsImage = UIImage(systemName: "eye.fill")!
16 16
     static let hidePostsImage = UIImage(systemName: "eye.slash.fill")!
17 17
     
18
+    let mastodonController: MastodonController
19
+    
18 20
     let mainStatusID: String
19 21
     let mainStatusState: StatusState
20 22
     var statuses: [(id: String, state: StatusState)] = [] {
@@ -28,9 +30,10 @@ class ConversationTableViewController: EnhancedTableViewController {
28 30
     var showStatusesAutomatically = false
29 31
     var visibilityBarButtonItem: UIBarButtonItem!
30 32
     
31
-    init(for mainStatusID: String, state: StatusState = .unknown) {
33
+    init(for mainStatusID: String, state: StatusState = .unknown, mastodonController: MastodonController) {
32 34
         self.mainStatusID = mainStatusID
33 35
         self.mainStatusState = state
36
+        self.mastodonController = mastodonController
34 37
         
35 38
         super.init(style: .plain)
36 39
     }
@@ -55,14 +58,14 @@ class ConversationTableViewController: EnhancedTableViewController {
55 58
 
56 59
         statuses = [(mainStatusID, mainStatusState)]
57 60
 
58
-        guard let mainStatus = MastodonCache.status(for: mainStatusID) else { fatalError("Missing cached status \(mainStatusID)") }
61
+        guard let mainStatus = mastodonController.cache.status(for: mainStatusID) else { fatalError("Missing cached status \(mainStatusID)") }
59 62
         
60 63
         let request = Status.getContext(mainStatus)
61
-        MastodonController.client.run(request) { response in
64
+        mastodonController.run(request) { response in
62 65
             guard case let .success(context, _) = response else { fatalError() }
63 66
             let parents = self.getDirectParents(of: mainStatus, from: context.ancestors)
64
-            MastodonCache.addAll(statuses: parents)
65
-            MastodonCache.addAll(statuses: context.descendants)
67
+            self.mastodonController.cache.addAll(statuses: parents)
68
+            self.mastodonController.cache.addAll(statuses: context.descendants)
66 69
             self.statuses = parents.map { ($0.id, .unknown) } + self.statuses + context.descendants.map { ($0.id, .unknown) }
67 70
             let indexPath = IndexPath(row: parents.count, section: 0)
68 71
             DispatchQueue.main.async {
@@ -101,14 +104,14 @@ class ConversationTableViewController: EnhancedTableViewController {
101 104
             guard let cell = tableView.dequeueReusableCell(withIdentifier: "mainStatusCell", for: indexPath) as? ConversationMainStatusTableViewCell else { fatalError() }
102 105
             cell.selectionStyle = .none
103 106
             cell.showStatusAutomatically = showStatusesAutomatically
104
-            cell.updateUI(statusID: id, state: state)
105 107
             cell.delegate = self
108
+            cell.updateUI(statusID: id, state: state)
106 109
             return cell
107 110
         } else {
108 111
             guard let cell = tableView.dequeueReusableCell(withIdentifier: "statusCell", for: indexPath) as? TimelineStatusTableViewCell else { fatalError() }
109 112
             cell.showStatusAutomatically = showStatusesAutomatically
110
-            cell.updateUI(statusID: id, state: state)
111 113
             cell.delegate = self
114
+            cell.updateUI(statusID: id, state: state)
112 115
             return cell
113 116
         }
114 117
     }
@@ -155,6 +158,7 @@ class ConversationTableViewController: EnhancedTableViewController {
155 158
 }
156 159
 
157 160
 extension ConversationTableViewController: StatusTableViewCellDelegate {
161
+    var apiController: MastodonController { mastodonController }
158 162
     func statusCellCollapsedStateChanged(_ cell: BaseStatusTableViewCell) {
159 163
         // causes the table view to recalculate the cell heights
160 164
         tableView.beginUpdates()
@@ -165,7 +169,7 @@ extension ConversationTableViewController: StatusTableViewCellDelegate {
165 169
 extension ConversationTableViewController: UITableViewDataSourcePrefetching {
166 170
     func tableView(_ tableView: UITableView, prefetchRowsAt indexPaths: [IndexPath]) {
167 171
         for indexPath in indexPaths {
168
-            guard let status = MastodonCache.status(for: statuses[indexPath.row].id) else { continue }
172
+            guard let status = mastodonController.cache.status(for: statuses[indexPath.row].id) else { continue }
169 173
             ImageCache.avatars.get(status.account.avatar, completion: nil)
170 174
             for attachment in status.attachments {
171 175
                 ImageCache.attachments.get(attachment.url, completion: nil)
@@ -175,7 +179,7 @@ extension ConversationTableViewController: UITableViewDataSourcePrefetching {
175 179
     
176 180
     func tableView(_ tableView: UITableView, cancelPrefetchingForRowsAt indexPaths: [IndexPath]) {
177 181
         for indexPath in indexPaths {
178
-            guard let status = MastodonCache.status(for: statuses[indexPath.row].id) else { continue }
182
+            guard let status = mastodonController.cache.status(for: statuses[indexPath.row].id) else { continue }
179 183
             ImageCache.avatars.cancel(status.account.avatar)
180 184
             for attachment in status.attachments {
181 185
                 ImageCache.attachments.cancel(attachment.url)

+ 1
- 1
Tusker/Screens/Explore/AddSavedHashtagViewController.swift View File

@@ -52,7 +52,7 @@ class AddSavedHashtagViewController: SearchResultsViewController {
52 52
 
53 53
 extension AddSavedHashtagViewController: SearchResultsViewControllerDelegate {
54 54
     func selectedSearchResult(hashtag: Hashtag) {
55
-        SavedHashtagsManager.shared.add(hashtag)
55
+        SavedDataManager.shared.add(hashtag: hashtag, for: mastodonController.accountInfo!)
56 56
         dismiss(animated: true)
57 57
     }
58 58
 }

+ 31
- 21
Tusker/Screens/Explore/ExploreViewController.swift View File

@@ -12,12 +12,16 @@ import Pachyderm
12 12
 
13 13
 class ExploreViewController: EnhancedTableViewController {
14 14
 
15
+    let mastodonController: MastodonController
16
+    
15 17
     var dataSource: DataSource!
16 18
     
17 19
     var resultsController: SearchResultsViewController!
18 20
     var searchController: UISearchController!
19 21
     
20
-    init() {
22
+    init(mastodonController: MastodonController) {
23
+        self.mastodonController = mastodonController
24
+        
21 25
         super.init(style: .insetGrouped)
22 26
         
23 27
         title = NSLocalizedString("Explore", comment: "explore tab title")
@@ -77,18 +81,20 @@ class ExploreViewController: EnhancedTableViewController {
77 81
         })
78 82
         dataSource.exploreController = self
79 83
         
84
+        let account = mastodonController.accountInfo!
85
+        
80 86
         var snapshot = NSDiffableDataSourceSnapshot<Section, Item>()
81 87
         snapshot.appendSections([.bookmarks, .lists, .savedHashtags, .savedInstances])
82 88
         snapshot.appendItems([.bookmarks], toSection: .bookmarks)
83 89
         snapshot.appendItems([.addList], toSection: .lists)
84
-        snapshot.appendItems(SavedHashtagsManager.shared.sorted.map { .savedHashtag($0) } + [.addSavedHashtag], toSection: .savedHashtags)
85
-        snapshot.appendItems(SavedInstanceManager.shared.savedInstances.map { .savedInstance($0) } + [.findInstance], toSection: .savedInstances)
90
+        snapshot.appendItems(SavedDataManager.shared.sortedHashtags(for: account).map { .savedHashtag($0) } + [.addSavedHashtag], toSection: .savedHashtags)
91
+        snapshot.appendItems(SavedDataManager.shared.savedInstances(for: account).map { .savedInstance($0) } + [.findInstance], toSection: .savedInstances)
86 92
         // the initial, static items should not be displayed with an animation
87 93
         UIView.performWithoutAnimation {
88 94
             dataSource.apply(snapshot)
89 95
         }
90 96
 
91
-        resultsController = SearchResultsViewController()
97
+        resultsController = SearchResultsViewController(mastodonController: mastodonController)
92 98
         resultsController.exploreNavigationController = self.navigationController!
93 99
         searchController = UISearchController(searchResultsController: resultsController)
94 100
         searchController.searchResultsUpdater = resultsController
@@ -106,8 +112,8 @@ class ExploreViewController: EnhancedTableViewController {
106 112
     }
107 113
     
108 114
     func reloadLists() {
109
-        let request = MastodonController.client.getLists()
110
-        MastodonController.client.run(request) { (response) in
115
+        let request = Client.getLists()
116
+        mastodonController.run(request) { (response) in
111 117
             guard case let .success(lists, _) = response else {
112 118
                 fatalError()
113 119
             }
@@ -123,16 +129,18 @@ class ExploreViewController: EnhancedTableViewController {
123 129
     }
124 130
     
125 131
     @objc func savedHashtagsChanged() {
132
+        let account = mastodonController.accountInfo!
126 133
         var snapshot = dataSource.snapshot()
127 134
         snapshot.deleteItems(snapshot.itemIdentifiers(inSection: .savedHashtags))
128
-        snapshot.appendItems(SavedHashtagsManager.shared.sorted.map { .savedHashtag($0) } + [.addSavedHashtag], toSection: .savedHashtags)
135
+        snapshot.appendItems(SavedDataManager.shared.sortedHashtags(for: account).map { .savedHashtag($0) } + [.addSavedHashtag], toSection: .savedHashtags)
129 136
         dataSource.apply(snapshot)
130 137
     }
131 138
     
132 139
     @objc func savedInstancesChanged() {
140
+        let account = mastodonController.accountInfo!
133 141
         var snapshot = dataSource.snapshot()
134 142
         snapshot.deleteItems(snapshot.itemIdentifiers(inSection: .savedInstances))
135
-        snapshot.appendItems(SavedInstanceManager.shared.savedInstances.map { .savedInstance($0) } + [.findInstance], toSection: .savedInstances)
143
+        snapshot.appendItems(SavedDataManager.shared.savedInstances(for: account).map { .savedInstance($0) } + [.findInstance], toSection: .savedInstances)
136 144
         dataSource.apply(snapshot)
137 145
     }
138 146
     
@@ -143,7 +151,7 @@ class ExploreViewController: EnhancedTableViewController {
143 151
         alert.addAction(UIAlertAction(title: NSLocalizedString("Delete List", comment: "delete list alert confirm button"), style: .destructive, handler: { (_) in
144 152
             
145 153
             let request = List.delete(list)
146
-            MastodonController.client.run(request) { (response) in
154
+            self.mastodonController.run(request) { (response) in
147 155
                 guard case .success(_, _) = response else {
148 156
                     fatalError()
149 157
                 }
@@ -159,11 +167,13 @@ class ExploreViewController: EnhancedTableViewController {
159 167
     }
160 168
     
161 169
     func removeSavedHashtag(_ hashtag: Hashtag) {
162
-        SavedHashtagsManager.shared.remove(hashtag)
170
+        let account = mastodonController.accountInfo!
171
+        SavedDataManager.shared.remove(hashtag: hashtag, for: account)
163 172
     }
164 173
     
165 174
     func removeSavedInstance(_ instanceURL: URL) {
166
-        SavedInstanceManager.shared.remove(instanceURL)
175
+        let account = mastodonController.accountInfo!
176
+        SavedDataManager.shared.remove(instance: instanceURL, for: account)
167 177
     }
168 178
     
169 179
     // MARK: - Table view delegate
@@ -174,10 +184,10 @@ class ExploreViewController: EnhancedTableViewController {
174 184
             return
175 185
             
176 186
         case .bookmarks:
177
-            show(BookmarksTableViewController(), sender: nil)
187
+            show(BookmarksTableViewController(mastodonController: mastodonController), sender: nil)
178 188
             
179 189
         case let .list(list):
180
-            show(ListTimelineViewController(for: list), sender: nil)
190
+            show(ListTimelineViewController(for: list, mastodonController: mastodonController), sender: nil)
181 191
             
182 192
         case .addList:
183 193
             tableView.selectRow(at: nil, animated: true, scrollPosition: .none)
@@ -189,14 +199,14 @@ class ExploreViewController: EnhancedTableViewController {
189 199
                     fatalError()
190 200
                 }
191 201
                 
192
-                let request = MastodonController.client.createList(title: title)
193
-                MastodonController.client.run(request) { (response) in
202
+                let request = Client.createList(title: title)
203
+                self.mastodonController.run(request) { (response) in
194 204
                     guard case let .success(list, _) = response else { fatalError() }
195 205
                     
196 206
                     self.reloadLists()
197 207
                     
198 208
                     DispatchQueue.main.async {
199
-                        let listTimelineController = ListTimelineViewController(for: list)
209
+                        let listTimelineController = ListTimelineViewController(for: list, mastodonController: self.mastodonController)
200 210
                         listTimelineController.presentEditOnAppear = true
201 211
                         self.show(listTimelineController, sender: nil)
202 212
                     }
@@ -205,19 +215,19 @@ class ExploreViewController: EnhancedTableViewController {
205 215
             present(alert, animated: true)
206 216
             
207 217
         case let .savedHashtag(hashtag):
208
-            show(HashtagTimelineViewController(for: hashtag), sender: nil)
218
+            show(HashtagTimelineViewController(for: hashtag, mastodonController: mastodonController), sender: nil)
209 219
             
210 220
         case .addSavedHashtag:
211 221
             tableView.selectRow(at: nil, animated: true, scrollPosition: .none)
212
-            let navController = UINavigationController(rootViewController: AddSavedHashtagViewController())
222
+            let navController = UINavigationController(rootViewController: AddSavedHashtagViewController(mastodonController: mastodonController))
213 223
             present(navController, animated: true)
214 224
             
215 225
         case let .savedInstance(url):
216
-            show(InstanceTimelineViewController(for: url), sender: nil)
226
+            show(InstanceTimelineViewController(for: url, parentMastodonController: mastodonController), sender: nil)
217 227
             
218 228
         case .findInstance:
219 229
             tableView.selectRow(at: nil, animated: true, scrollPosition: .none)
220
-            let findController = FindInstanceViewController()
230
+            let findController = FindInstanceViewController(parentMastodonController: mastodonController)
221 231
             findController.instanceTimelineDelegate = self
222 232
             let navController = UINavigationController(rootViewController: findController)
223 233
             present(navController, animated: true)
@@ -344,7 +354,7 @@ extension ExploreViewController {
344 354
 extension ExploreViewController: InstanceTimelineViewControllerDelegate {
345 355
     func didSaveInstance(url: URL) {
346 356
         dismiss(animated: true) {
347
-            self.show(InstanceTimelineViewController(for: url), sender: nil)
357
+            self.show(InstanceTimelineViewController(for: url, parentMastodonController: self.mastodonController), sender: nil)
348 358
         }
349 359
     }
350 360
     

+ 13
- 1
Tusker/Screens/FindInstanceViewController.swift View File

@@ -10,8 +10,20 @@ import UIKit
10 10
 
11 11
 class FindInstanceViewController: InstanceSelectorTableViewController {
12 12
 
13
+    weak var parentMastodonController: MastodonController?
14
+    
13 15
     var instanceTimelineDelegate: InstanceTimelineViewControllerDelegate?
14 16
     
17
+    init(parentMastodonController: MastodonController) {
18
+        self.parentMastodonController = parentMastodonController
19
+        
20
+        super.init()
21
+    }
22
+    
23
+    required init?(coder: NSCoder) {
24
+        fatalError("init(coder:) has not been implemented")
25
+    }
26
+    
15 27
     override func viewDidLoad() {
16 28
         super.viewDidLoad()
17 29
         
@@ -32,7 +44,7 @@ class FindInstanceViewController: InstanceSelectorTableViewController {
32 44
 
33 45
 extension FindInstanceViewController: InstanceSelectorTableViewControllerDelegate {
34 46
     func didSel