Merge branch 'multiple-accounts'

This commit is contained in:
Shadowfacts 2020-01-23 22:36:42 -05:00
commit 3220436893
Signed by: shadowfacts
GPG Key ID: 94A5AB95422746E5
69 changed files with 1295 additions and 793 deletions

View File

@ -130,32 +130,32 @@ public class Client {
} }
// MARK: - Self // MARK: - Self
public func getSelfAccount() -> Request<Account> { public static func getSelfAccount() -> Request<Account> {
return Request<Account>(method: .get, path: "/api/v1/accounts/verify_credentials") return Request<Account>(method: .get, path: "/api/v1/accounts/verify_credentials")
} }
public func getFavourites() -> Request<[Status]> { public static func getFavourites() -> Request<[Status]> {
return Request<[Status]>(method: .get, path: "/api/v1/favourites") return Request<[Status]>(method: .get, path: "/api/v1/favourites")
} }
public func getRelationships(accounts: [String]? = nil) -> Request<[Relationship]> { public static func getRelationships(accounts: [String]? = nil) -> Request<[Relationship]> {
return Request<[Relationship]>(method: .get, path: "/api/v1/accounts/relationships", queryParameters: "id" => accounts) return Request<[Relationship]>(method: .get, path: "/api/v1/accounts/relationships", queryParameters: "id" => accounts)
} }
public func getInstance() -> Request<Instance> { public static func getInstance() -> Request<Instance> {
return Request<Instance>(method: .get, path: "/api/v1/instance") return Request<Instance>(method: .get, path: "/api/v1/instance")
} }
public func getCustomEmoji() -> Request<[Emoji]> { public static func getCustomEmoji() -> Request<[Emoji]> {
return Request<[Emoji]>(method: .get, path: "/api/v1/custom_emojis") return Request<[Emoji]>(method: .get, path: "/api/v1/custom_emojis")
} }
// MARK: - Accounts // MARK: - Accounts
public func getAccount(id: String) -> Request<Account> { public static func getAccount(id: String) -> Request<Account> {
return Request<Account>(method: .get, path: "/api/v1/accounts/\(id)") return Request<Account>(method: .get, path: "/api/v1/accounts/\(id)")
} }
public func searchForAccount(query: String, limit: Int? = nil, following: Bool? = nil) -> Request<[Account]> { public static func searchForAccount(query: String, limit: Int? = nil, following: Bool? = nil) -> Request<[Account]> {
return Request<[Account]>(method: .get, path: "/api/v1/accounts/search", queryParameters: [ return Request<[Account]>(method: .get, path: "/api/v1/accounts/search", queryParameters: [
"q" => query, "q" => query,
"limit" => limit, "limit" => limit,
@ -164,32 +164,32 @@ public class Client {
} }
// MARK: - Blocks // MARK: - Blocks
public func getBlocks() -> Request<[Account]> { public static func getBlocks() -> Request<[Account]> {
return Request<[Account]>(method: .get, path: "/api/v1/blocks") return Request<[Account]>(method: .get, path: "/api/v1/blocks")
} }
public func getDomainBlocks() -> Request<[String]> { public static func getDomainBlocks() -> Request<[String]> {
return Request<[String]>(method: .get, path: "api/v1/domain_blocks") return Request<[String]>(method: .get, path: "api/v1/domain_blocks")
} }
public func block(domain: String) -> Request<Empty> { public static func block(domain: String) -> Request<Empty> {
return Request<Empty>(method: .post, path: "/api/v1/domain_blocks", body: .parameters([ return Request<Empty>(method: .post, path: "/api/v1/domain_blocks", body: .parameters([
"domain" => domain "domain" => domain
])) ]))
} }
public func unblock(domain: String) -> Request<Empty> { public static func unblock(domain: String) -> Request<Empty> {
return Request<Empty>(method: .delete, path: "/api/v1/domain_blocks", body: .parameters([ return Request<Empty>(method: .delete, path: "/api/v1/domain_blocks", body: .parameters([
"domain" => domain "domain" => domain
])) ]))
} }
// MARK: - Filters // MARK: - Filters
public func getFilters() -> Request<[Filter]> { public static func getFilters() -> Request<[Filter]> {
return Request<[Filter]>(method: .get, path: "/api/v1/filters") return Request<[Filter]>(method: .get, path: "/api/v1/filters")
} }
public func createFilter(phrase: String, context: [Filter.Context], irreversible: Bool? = nil, wholeWord: Bool? = nil, expiresAt: Date? = nil) -> Request<Filter> { public static func createFilter(phrase: String, context: [Filter.Context], irreversible: Bool? = nil, wholeWord: Bool? = nil, expiresAt: Date? = nil) -> Request<Filter> {
return Request<Filter>(method: .post, path: "/api/v1/filters", body: .parameters([ return Request<Filter>(method: .post, path: "/api/v1/filters", body: .parameters([
"phrase" => phrase, "phrase" => phrase,
"irreversible" => irreversible, "irreversible" => irreversible,
@ -198,40 +198,40 @@ public class Client {
] + "context" => context.contextStrings)) ] + "context" => context.contextStrings))
} }
public func getFilter(id: String) -> Request<Filter> { public static func getFilter(id: String) -> Request<Filter> {
return Request<Filter>(method: .get, path: "/api/v1/filters/\(id)") return Request<Filter>(method: .get, path: "/api/v1/filters/\(id)")
} }
// MARK: - Follows // MARK: - Follows
public func getFollowRequests(range: RequestRange = .default) -> Request<[Account]> { public static func getFollowRequests(range: RequestRange = .default) -> Request<[Account]> {
var request = Request<[Account]>(method: .get, path: "/api/v1/follow_requests") var request = Request<[Account]>(method: .get, path: "/api/v1/follow_requests")
request.range = range request.range = range
return request return request
} }
public func getFollowSuggestions() -> Request<[Account]> { public static func getFollowSuggestions() -> Request<[Account]> {
return Request<[Account]>(method: .get, path: "/api/v1/suggestions") return Request<[Account]>(method: .get, path: "/api/v1/suggestions")
} }
public func followRemote(acct: String) -> Request<Account> { public static func followRemote(acct: String) -> Request<Account> {
return Request<Account>(method: .post, path: "/api/v1/follows", body: .parameters(["uri" => acct])) return Request<Account>(method: .post, path: "/api/v1/follows", body: .parameters(["uri" => acct]))
} }
// MARK: - Lists // MARK: - Lists
public func getLists() -> Request<[List]> { public static func getLists() -> Request<[List]> {
return Request<[List]>(method: .get, path: "/api/v1/lists") return Request<[List]>(method: .get, path: "/api/v1/lists")
} }
public func getList(id: String) -> Request<List> { public static func getList(id: String) -> Request<List> {
return Request<List>(method: .get, path: "/api/v1/lists/\(id)") return Request<List>(method: .get, path: "/api/v1/lists/\(id)")
} }
public func createList(title: String) -> Request<List> { public static func createList(title: String) -> Request<List> {
return Request<List>(method: .post, path: "/api/v1/lists", body: .parameters(["title" => title])) return Request<List>(method: .post, path: "/api/v1/lists", body: .parameters(["title" => title]))
} }
// MARK: - Media // MARK: - Media
public func upload(attachment: FormAttachment, description: String? = nil, focus: (Float, Float)? = nil) -> Request<Attachment> { public static func upload(attachment: FormAttachment, description: String? = nil, focus: (Float, Float)? = nil) -> Request<Attachment> {
return Request<Attachment>(method: .post, path: "/api/v1/media", body: .formData([ return Request<Attachment>(method: .post, path: "/api/v1/media", body: .formData([
"description" => description, "description" => description,
"focus" => focus "focus" => focus
@ -239,14 +239,14 @@ public class Client {
} }
// MARK: - Mutes // MARK: - Mutes
public func getMutes(range: RequestRange) -> Request<[Account]> { public static func getMutes(range: RequestRange) -> Request<[Account]> {
var request = Request<[Account]>(method: .get, path: "/api/v1/mutes") var request = Request<[Account]>(method: .get, path: "/api/v1/mutes")
request.range = range request.range = range
return request return request
} }
// MARK: - Notifications // MARK: - Notifications
public func getNotifications(excludeTypes: [Notification.Kind], range: RequestRange = .default) -> Request<[Notification]> { public static func getNotifications(excludeTypes: [Notification.Kind], range: RequestRange = .default) -> Request<[Notification]> {
var request = Request<[Notification]>(method: .get, path: "/api/v1/notifications", queryParameters: var request = Request<[Notification]>(method: .get, path: "/api/v1/notifications", queryParameters:
"exclude_types" => excludeTypes.map { $0.rawValue } "exclude_types" => excludeTypes.map { $0.rawValue }
) )
@ -254,16 +254,16 @@ public class Client {
return request return request
} }
public func clearNotifications() -> Request<Empty> { public static func clearNotifications() -> Request<Empty> {
return Request<Empty>(method: .post, path: "/api/v1/notifications/clear") return Request<Empty>(method: .post, path: "/api/v1/notifications/clear")
} }
// MARK: - Reports // MARK: - Reports
public func getReports() -> Request<[Report]> { public static func getReports() -> Request<[Report]> {
return Request<[Report]>(method: .get, path: "/api/v1/reports") return Request<[Report]>(method: .get, path: "/api/v1/reports")
} }
public func report(account: Account, statuses: [Status], comment: String) -> Request<Report> { public static func report(account: Account, statuses: [Status], comment: String) -> Request<Report> {
return Request<Report>(method: .post, path: "/api/v1/reports", body: .parameters([ return Request<Report>(method: .post, path: "/api/v1/reports", body: .parameters([
"account_id" => account.id, "account_id" => account.id,
"comment" => comment "comment" => comment
@ -271,7 +271,7 @@ public class Client {
} }
// MARK: - Search // MARK: - Search
public func search(query: String, resolve: Bool? = nil, limit: Int? = nil) -> Request<SearchResults> { public static func search(query: String, resolve: Bool? = nil, limit: Int? = nil) -> Request<SearchResults> {
return Request<SearchResults>(method: .get, path: "/api/v2/search", queryParameters: [ return Request<SearchResults>(method: .get, path: "/api/v2/search", queryParameters: [
"q" => query, "q" => query,
"resolve" => resolve, "resolve" => resolve,
@ -280,18 +280,18 @@ public class Client {
} }
// MARK: - Statuses // MARK: - Statuses
public func getStatus(id: String) -> Request<Status> { public static func getStatus(id: String) -> Request<Status> {
return Request<Status>(method: .get, path: "/api/v1/statuses/\(id)") return Request<Status>(method: .get, path: "/api/v1/statuses/\(id)")
} }
public func createStatus(text: String, public static func createStatus(text: String,
contentType: StatusContentType = .plain, contentType: StatusContentType = .plain,
inReplyTo: String? = nil, inReplyTo: String? = nil,
media: [Attachment]? = nil, media: [Attachment]? = nil,
sensitive: Bool? = nil, sensitive: Bool? = nil,
spoilerText: String? = nil, spoilerText: String? = nil,
visibility: Status.Visibility? = nil, visibility: Status.Visibility? = nil,
language: String? = nil) -> Request<Status> { language: String? = nil) -> Request<Status> {
return Request<Status>(method: .post, path: "/api/v1/statuses", body: .parameters([ return Request<Status>(method: .post, path: "/api/v1/statuses", body: .parameters([
"status" => text, "status" => text,
"content_type" => contentType.mimeType, "content_type" => contentType.mimeType,
@ -304,13 +304,13 @@ public class Client {
} }
// MARK: - Timelines // MARK: - Timelines
public func getStatuses(timeline: Timeline, range: RequestRange = .default) -> Request<[Status]> { public static func getStatuses(timeline: Timeline, range: RequestRange = .default) -> Request<[Status]> {
return timeline.request(range: range) return timeline.request(range: range)
} }
// MARK: Bookmarks // MARK: Bookmarks
public func getBookmarks(range: RequestRange = .default) -> Request<[Status]> { public static func getBookmarks(range: RequestRange = .default) -> Request<[Status]> {
var request = Request<[Status]>(method: .get, path: "/api/v1/bookmarks") var request = Request<[Status]>(method: .get, path: "/api/v1/bookmarks")
request.range = range request.range = range
return request return request

View File

@ -122,6 +122,7 @@
D64BC18A23C16487000D0238 /* UnpinStatusActivity.swift in Sources */ = {isa = PBXBuildFile; fileRef = D64BC18923C16487000D0238 /* UnpinStatusActivity.swift */; }; D64BC18A23C16487000D0238 /* UnpinStatusActivity.swift in Sources */ = {isa = PBXBuildFile; fileRef = D64BC18923C16487000D0238 /* UnpinStatusActivity.swift */; };
D64BC18F23C18B9D000D0238 /* FollowRequestNotificationTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D64BC18D23C18B9D000D0238 /* FollowRequestNotificationTableViewCell.swift */; }; D64BC18F23C18B9D000D0238 /* FollowRequestNotificationTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D64BC18D23C18B9D000D0238 /* FollowRequestNotificationTableViewCell.swift */; };
D64BC19023C18B9D000D0238 /* FollowRequestNotificationTableViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = D64BC18E23C18B9D000D0238 /* FollowRequestNotificationTableViewCell.xib */; }; D64BC19023C18B9D000D0238 /* FollowRequestNotificationTableViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = D64BC18E23C18B9D000D0238 /* FollowRequestNotificationTableViewCell.xib */; };
D64BC19223C271D9000D0238 /* MastodonActivity.swift in Sources */ = {isa = PBXBuildFile; fileRef = D64BC19123C271D9000D0238 /* MastodonActivity.swift */; };
D64D0AAD2128D88B005A6F37 /* LocalData.swift in Sources */ = {isa = PBXBuildFile; fileRef = D64D0AAC2128D88B005A6F37 /* LocalData.swift */; }; D64D0AAD2128D88B005A6F37 /* LocalData.swift in Sources */ = {isa = PBXBuildFile; fileRef = D64D0AAC2128D88B005A6F37 /* LocalData.swift */; };
D64D0AB12128D9AE005A6F37 /* OnboardingViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D64D0AB02128D9AE005A6F37 /* OnboardingViewController.swift */; }; D64D0AB12128D9AE005A6F37 /* OnboardingViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D64D0AB02128D9AE005A6F37 /* OnboardingViewController.swift */; };
D64F80E2215875CC00BEF393 /* XCBActionType.swift in Sources */ = {isa = PBXBuildFile; fileRef = D64F80E1215875CC00BEF393 /* XCBActionType.swift */; }; D64F80E2215875CC00BEF393 /* XCBActionType.swift in Sources */ = {isa = PBXBuildFile; fileRef = D64F80E1215875CC00BEF393 /* XCBActionType.swift */; };
@ -156,10 +157,9 @@
D67C57B221E28FAD00C3118B /* ComposeStatusReplyView.xib in Resources */ = {isa = PBXBuildFile; fileRef = D67C57B121E28FAD00C3118B /* ComposeStatusReplyView.xib */; }; D67C57B221E28FAD00C3118B /* ComposeStatusReplyView.xib in Resources */ = {isa = PBXBuildFile; fileRef = D67C57B121E28FAD00C3118B /* ComposeStatusReplyView.xib */; };
D67C57B421E2910700C3118B /* ComposeStatusReplyView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D67C57B321E2910700C3118B /* ComposeStatusReplyView.swift */; }; D67C57B421E2910700C3118B /* ComposeStatusReplyView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D67C57B321E2910700C3118B /* ComposeStatusReplyView.swift */; };
D68FEC4F232C5BC300C84F23 /* SegmentedPageViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D68FEC4E232C5BC300C84F23 /* SegmentedPageViewController.swift */; }; D68FEC4F232C5BC300C84F23 /* SegmentedPageViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D68FEC4E232C5BC300C84F23 /* SegmentedPageViewController.swift */; };
D6945C2F23AC47C3005C403C /* SavedHashtagsManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6945C2E23AC47C3005C403C /* SavedHashtagsManager.swift */; }; D6945C2F23AC47C3005C403C /* SavedDataManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6945C2E23AC47C3005C403C /* SavedDataManager.swift */; };
D6945C3223AC4D36005C403C /* HashtagTimelineViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6945C3123AC4D36005C403C /* HashtagTimelineViewController.swift */; }; D6945C3223AC4D36005C403C /* HashtagTimelineViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6945C3123AC4D36005C403C /* HashtagTimelineViewController.swift */; };
D6945C3423AC6431005C403C /* AddSavedHashtagViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6945C3323AC6431005C403C /* AddSavedHashtagViewController.swift */; }; D6945C3423AC6431005C403C /* AddSavedHashtagViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6945C3323AC6431005C403C /* AddSavedHashtagViewController.swift */; };
D6945C3623AC6C09005C403C /* SavedInstancesManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6945C3523AC6C09005C403C /* SavedInstancesManager.swift */; };
D6945C3823AC739F005C403C /* InstanceTimelineViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6945C3723AC739F005C403C /* InstanceTimelineViewController.swift */; }; D6945C3823AC739F005C403C /* InstanceTimelineViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6945C3723AC739F005C403C /* InstanceTimelineViewController.swift */; };
D6945C3A23AC75E2005C403C /* FindInstanceViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6945C3923AC75E2005C403C /* FindInstanceViewController.swift */; }; D6945C3A23AC75E2005C403C /* FindInstanceViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6945C3923AC75E2005C403C /* FindInstanceViewController.swift */; };
D6A3BC7723218E1300FD64D5 /* TimelineSegment.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6A3BC7323218C6E00FD64D5 /* TimelineSegment.swift */; }; D6A3BC7723218E1300FD64D5 /* TimelineSegment.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6A3BC7323218C6E00FD64D5 /* TimelineSegment.swift */; };
@ -177,6 +177,7 @@
D6A5BB2F23BBAC97003BF21D /* DelegatingResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6A5BB2E23BBAC97003BF21D /* DelegatingResponse.swift */; }; D6A5BB2F23BBAC97003BF21D /* DelegatingResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6A5BB2E23BBAC97003BF21D /* DelegatingResponse.swift */; };
D6A5BB3123BBAD87003BF21D /* JSONResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6A5BB3023BBAD87003BF21D /* JSONResponse.swift */; }; D6A5BB3123BBAD87003BF21D /* JSONResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6A5BB3023BBAD87003BF21D /* JSONResponse.swift */; };
D6A5FAF1217B7E05003DB2D9 /* ComposeViewController.xib in Resources */ = {isa = PBXBuildFile; fileRef = D6A5FAF0217B7E05003DB2D9 /* ComposeViewController.xib */; }; D6A5FAF1217B7E05003DB2D9 /* ComposeViewController.xib in Resources */ = {isa = PBXBuildFile; fileRef = D6A5FAF0217B7E05003DB2D9 /* ComposeViewController.xib */; };
D6AC956723C4347E008C9946 /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6AC956623C4347E008C9946 /* SceneDelegate.swift */; };
D6AEBB3E2321638100E5038B /* UIActivity+Types.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6AEBB3D2321638100E5038B /* UIActivity+Types.swift */; }; D6AEBB3E2321638100E5038B /* UIActivity+Types.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6AEBB3D2321638100E5038B /* UIActivity+Types.swift */; };
D6AEBB412321642700E5038B /* SendMesasgeActivity.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6AEBB402321642700E5038B /* SendMesasgeActivity.swift */; }; D6AEBB412321642700E5038B /* SendMesasgeActivity.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6AEBB402321642700E5038B /* SendMesasgeActivity.swift */; };
D6AEBB432321685E00E5038B /* OpenInSafariActivity.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6AEBB422321685E00E5038B /* OpenInSafariActivity.swift */; }; D6AEBB432321685E00E5038B /* OpenInSafariActivity.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6AEBB422321685E00E5038B /* OpenInSafariActivity.swift */; };
@ -217,6 +218,8 @@
D6E0DC8E216EDF1E00369478 /* Previewing.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6E0DC8D216EDF1E00369478 /* Previewing.swift */; }; D6E0DC8E216EDF1E00369478 /* Previewing.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6E0DC8D216EDF1E00369478 /* Previewing.swift */; };
D6E6F26321603F8B006A8599 /* CharacterCounter.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6E6F26221603F8B006A8599 /* CharacterCounter.swift */; }; D6E6F26321603F8B006A8599 /* CharacterCounter.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6E6F26221603F8B006A8599 /* CharacterCounter.swift */; };
D6E6F26521604242006A8599 /* CharacterCounterTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6E6F26421604242006A8599 /* CharacterCounterTests.swift */; }; D6E6F26521604242006A8599 /* CharacterCounterTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6E6F26421604242006A8599 /* CharacterCounterTests.swift */; };
D6EBF01523C55C0900AE061B /* UIApplication+Scenes.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6EBF01423C55C0900AE061B /* UIApplication+Scenes.swift */; };
D6EBF01723C55E0D00AE061B /* UISceneSession+MastodonController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6EBF01623C55E0D00AE061B /* UISceneSession+MastodonController.swift */; };
D6F1F84D2193B56E00F5FE67 /* Cache.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6F1F84C2193B56E00F5FE67 /* Cache.swift */; }; D6F1F84D2193B56E00F5FE67 /* Cache.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6F1F84C2193B56E00F5FE67 /* Cache.swift */; };
D6F953EC212519E700CF0F2B /* TimelineTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6F953EB212519E700CF0F2B /* TimelineTableViewController.swift */; }; D6F953EC212519E700CF0F2B /* TimelineTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6F953EB212519E700CF0F2B /* TimelineTableViewController.swift */; };
D6F953F021251A2900CF0F2B /* MastodonController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6F953EF21251A2900CF0F2B /* MastodonController.swift */; }; D6F953F021251A2900CF0F2B /* MastodonController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6F953EF21251A2900CF0F2B /* MastodonController.swift */; };
@ -394,6 +397,7 @@
D64BC18923C16487000D0238 /* UnpinStatusActivity.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UnpinStatusActivity.swift; sourceTree = "<group>"; }; D64BC18923C16487000D0238 /* UnpinStatusActivity.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UnpinStatusActivity.swift; sourceTree = "<group>"; };
D64BC18D23C18B9D000D0238 /* FollowRequestNotificationTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FollowRequestNotificationTableViewCell.swift; sourceTree = "<group>"; }; D64BC18D23C18B9D000D0238 /* FollowRequestNotificationTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FollowRequestNotificationTableViewCell.swift; sourceTree = "<group>"; };
D64BC18E23C18B9D000D0238 /* FollowRequestNotificationTableViewCell.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = FollowRequestNotificationTableViewCell.xib; sourceTree = "<group>"; }; D64BC18E23C18B9D000D0238 /* FollowRequestNotificationTableViewCell.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = FollowRequestNotificationTableViewCell.xib; sourceTree = "<group>"; };
D64BC19123C271D9000D0238 /* MastodonActivity.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonActivity.swift; sourceTree = "<group>"; };
D64D0AAC2128D88B005A6F37 /* LocalData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalData.swift; sourceTree = "<group>"; }; D64D0AAC2128D88B005A6F37 /* LocalData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalData.swift; sourceTree = "<group>"; };
D64D0AB02128D9AE005A6F37 /* OnboardingViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingViewController.swift; sourceTree = "<group>"; }; D64D0AB02128D9AE005A6F37 /* OnboardingViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingViewController.swift; sourceTree = "<group>"; };
D64F80E1215875CC00BEF393 /* XCBActionType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XCBActionType.swift; sourceTree = "<group>"; }; D64F80E1215875CC00BEF393 /* XCBActionType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XCBActionType.swift; sourceTree = "<group>"; };
@ -429,10 +433,9 @@
D67C57B121E28FAD00C3118B /* ComposeStatusReplyView.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = ComposeStatusReplyView.xib; sourceTree = "<group>"; }; D67C57B121E28FAD00C3118B /* ComposeStatusReplyView.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = ComposeStatusReplyView.xib; sourceTree = "<group>"; };
D67C57B321E2910700C3118B /* ComposeStatusReplyView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeStatusReplyView.swift; sourceTree = "<group>"; }; D67C57B321E2910700C3118B /* ComposeStatusReplyView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeStatusReplyView.swift; sourceTree = "<group>"; };
D68FEC4E232C5BC300C84F23 /* SegmentedPageViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SegmentedPageViewController.swift; sourceTree = "<group>"; }; D68FEC4E232C5BC300C84F23 /* SegmentedPageViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SegmentedPageViewController.swift; sourceTree = "<group>"; };
D6945C2E23AC47C3005C403C /* SavedHashtagsManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SavedHashtagsManager.swift; sourceTree = "<group>"; }; D6945C2E23AC47C3005C403C /* SavedDataManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SavedDataManager.swift; sourceTree = "<group>"; };
D6945C3123AC4D36005C403C /* HashtagTimelineViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HashtagTimelineViewController.swift; sourceTree = "<group>"; }; D6945C3123AC4D36005C403C /* HashtagTimelineViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HashtagTimelineViewController.swift; sourceTree = "<group>"; };
D6945C3323AC6431005C403C /* AddSavedHashtagViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddSavedHashtagViewController.swift; sourceTree = "<group>"; }; D6945C3323AC6431005C403C /* AddSavedHashtagViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddSavedHashtagViewController.swift; sourceTree = "<group>"; };
D6945C3523AC6C09005C403C /* SavedInstancesManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SavedInstancesManager.swift; sourceTree = "<group>"; };
D6945C3723AC739F005C403C /* InstanceTimelineViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InstanceTimelineViewController.swift; sourceTree = "<group>"; }; D6945C3723AC739F005C403C /* InstanceTimelineViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InstanceTimelineViewController.swift; sourceTree = "<group>"; };
D6945C3923AC75E2005C403C /* FindInstanceViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = FindInstanceViewController.swift; path = Tusker/Screens/FindInstanceViewController.swift; sourceTree = SOURCE_ROOT; }; D6945C3923AC75E2005C403C /* FindInstanceViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = FindInstanceViewController.swift; path = Tusker/Screens/FindInstanceViewController.swift; sourceTree = SOURCE_ROOT; };
D6A3BC7323218C6E00FD64D5 /* TimelineSegment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineSegment.swift; sourceTree = "<group>"; }; D6A3BC7323218C6E00FD64D5 /* TimelineSegment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineSegment.swift; sourceTree = "<group>"; };
@ -450,6 +453,7 @@
D6A5BB2E23BBAC97003BF21D /* DelegatingResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DelegatingResponse.swift; sourceTree = "<group>"; }; D6A5BB2E23BBAC97003BF21D /* DelegatingResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DelegatingResponse.swift; sourceTree = "<group>"; };
D6A5BB3023BBAD87003BF21D /* JSONResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JSONResponse.swift; sourceTree = "<group>"; }; D6A5BB3023BBAD87003BF21D /* JSONResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JSONResponse.swift; sourceTree = "<group>"; };
D6A5FAF0217B7E05003DB2D9 /* ComposeViewController.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = ComposeViewController.xib; sourceTree = "<group>"; }; D6A5FAF0217B7E05003DB2D9 /* ComposeViewController.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = ComposeViewController.xib; sourceTree = "<group>"; };
D6AC956623C4347E008C9946 /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = "<group>"; };
D6AEBB3D2321638100E5038B /* UIActivity+Types.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIActivity+Types.swift"; sourceTree = "<group>"; }; D6AEBB3D2321638100E5038B /* UIActivity+Types.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIActivity+Types.swift"; sourceTree = "<group>"; };
D6AEBB402321642700E5038B /* SendMesasgeActivity.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SendMesasgeActivity.swift; sourceTree = "<group>"; }; D6AEBB402321642700E5038B /* SendMesasgeActivity.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SendMesasgeActivity.swift; sourceTree = "<group>"; };
D6AEBB422321685E00E5038B /* OpenInSafariActivity.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OpenInSafariActivity.swift; sourceTree = "<group>"; }; D6AEBB422321685E00E5038B /* OpenInSafariActivity.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OpenInSafariActivity.swift; sourceTree = "<group>"; };
@ -494,6 +498,8 @@
D6E0DC8D216EDF1E00369478 /* Previewing.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Previewing.swift; sourceTree = "<group>"; }; D6E0DC8D216EDF1E00369478 /* Previewing.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Previewing.swift; sourceTree = "<group>"; };
D6E6F26221603F8B006A8599 /* CharacterCounter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CharacterCounter.swift; sourceTree = "<group>"; }; D6E6F26221603F8B006A8599 /* CharacterCounter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CharacterCounter.swift; sourceTree = "<group>"; };
D6E6F26421604242006A8599 /* CharacterCounterTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CharacterCounterTests.swift; sourceTree = "<group>"; }; D6E6F26421604242006A8599 /* CharacterCounterTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CharacterCounterTests.swift; sourceTree = "<group>"; };
D6EBF01423C55C0900AE061B /* UIApplication+Scenes.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIApplication+Scenes.swift"; sourceTree = "<group>"; };
D6EBF01623C55E0D00AE061B /* UISceneSession+MastodonController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UISceneSession+MastodonController.swift"; sourceTree = "<group>"; };
D6F1F84C2193B56E00F5FE67 /* Cache.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Cache.swift; sourceTree = "<group>"; }; D6F1F84C2193B56E00F5FE67 /* Cache.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Cache.swift; sourceTree = "<group>"; };
D6F953EB212519E700CF0F2B /* TimelineTableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineTableViewController.swift; sourceTree = "<group>"; }; D6F953EB212519E700CF0F2B /* TimelineTableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineTableViewController.swift; sourceTree = "<group>"; };
D6F953EF21251A2900CF0F2B /* MastodonController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonController.swift; sourceTree = "<group>"; }; D6F953EF21251A2900CF0F2B /* MastodonController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonController.swift; sourceTree = "<group>"; };
@ -967,6 +973,8 @@
D67C57AE21E28EAD00C3118B /* Array+Uniques.swift */, D67C57AE21E28EAD00C3118B /* Array+Uniques.swift */,
0450531E22B0097E00100BA2 /* Timline+UI.swift */, 0450531E22B0097E00100BA2 /* Timline+UI.swift */,
D66A77BA233838DC0058F1EC /* UIFont+Traits.swift */, D66A77BA233838DC0058F1EC /* UIFont+Traits.swift */,
D6EBF01423C55C0900AE061B /* UIApplication+Scenes.swift */,
D6EBF01623C55E0D00AE061B /* UISceneSession+MastodonController.swift */,
); );
path = Extensions; path = Extensions;
sourceTree = "<group>"; sourceTree = "<group>";
@ -1061,6 +1069,7 @@
children = ( children = (
D6AEBB3D2321638100E5038B /* UIActivity+Types.swift */, D6AEBB3D2321638100E5038B /* UIActivity+Types.swift */,
D6AEBB422321685E00E5038B /* OpenInSafariActivity.swift */, D6AEBB422321685E00E5038B /* OpenInSafariActivity.swift */,
D64BC19123C271D9000D0238 /* MastodonActivity.swift */,
D6AEBB4623216B0C00E5038B /* Account Activities */, D6AEBB4623216B0C00E5038B /* Account Activities */,
D627943323A5523800D38C68 /* Status Activities */, D627943323A5523800D38C68 /* Status Activities */,
); );
@ -1181,10 +1190,10 @@
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
D6D4DDCF212518A000E1C4BB /* AppDelegate.swift */, D6D4DDCF212518A000E1C4BB /* AppDelegate.swift */,
D6AC956623C4347E008C9946 /* SceneDelegate.swift */,
D64D0AAC2128D88B005A6F37 /* LocalData.swift */, D64D0AAC2128D88B005A6F37 /* LocalData.swift */,
D627FF75217E923E00CC0648 /* DraftsManager.swift */, D627FF75217E923E00CC0648 /* DraftsManager.swift */,
D6945C2E23AC47C3005C403C /* SavedHashtagsManager.swift */, D6945C2E23AC47C3005C403C /* SavedDataManager.swift */,
D6945C3523AC6C09005C403C /* SavedInstancesManager.swift */,
D6028B9A2150811100F223B9 /* MastodonCache.swift */, D6028B9A2150811100F223B9 /* MastodonCache.swift */,
D6C693EE216192C2007D6A6D /* TuskerNavigationDelegate.swift */, D6C693EE216192C2007D6A6D /* TuskerNavigationDelegate.swift */,
D6F1F84E2193B9BE00F5FE67 /* Caching */, D6F1F84E2193B9BE00F5FE67 /* Caching */,
@ -1620,6 +1629,7 @@
0427033A22B31269000D31B6 /* AdvancedPrefsView.swift in Sources */, 0427033A22B31269000D31B6 /* AdvancedPrefsView.swift in Sources */,
D626493C23C1000300612E6E /* AlbumTableViewCell.swift in Sources */, D626493C23C1000300612E6E /* AlbumTableViewCell.swift in Sources */,
D6757A822157E8FA00721E32 /* XCBSession.swift in Sources */, D6757A822157E8FA00721E32 /* XCBSession.swift in Sources */,
D6EBF01723C55E0D00AE061B /* UISceneSession+MastodonController.swift in Sources */,
04DACE8C212CB14B009840C4 /* MainTabBarViewController.swift in Sources */, 04DACE8C212CB14B009840C4 /* MainTabBarViewController.swift in Sources */,
D6AEBB412321642700E5038B /* SendMesasgeActivity.swift in Sources */, D6AEBB412321642700E5038B /* SendMesasgeActivity.swift in Sources */,
D6BC9DB1232C61BC002CA326 /* NotificationsPageViewController.swift in Sources */, D6BC9DB1232C61BC002CA326 /* NotificationsPageViewController.swift in Sources */,
@ -1631,7 +1641,6 @@
D68FEC4F232C5BC300C84F23 /* SegmentedPageViewController.swift in Sources */, D68FEC4F232C5BC300C84F23 /* SegmentedPageViewController.swift in Sources */,
0450531F22B0097E00100BA2 /* Timline+UI.swift in Sources */, 0450531F22B0097E00100BA2 /* Timline+UI.swift in Sources */,
D667E5F52135BCD50057A976 /* ConversationTableViewController.swift in Sources */, D667E5F52135BCD50057A976 /* ConversationTableViewController.swift in Sources */,
D6945C3623AC6C09005C403C /* SavedInstancesManager.swift in Sources */,
D6C7D27D22B6EBF800071952 /* AttachmentsContainerView.swift in Sources */, D6C7D27D22B6EBF800071952 /* AttachmentsContainerView.swift in Sources */,
D620483823D38190008A63EF /* StatusContentTextView.swift in Sources */, D620483823D38190008A63EF /* StatusContentTextView.swift in Sources */,
D6F953F021251A2900CF0F2B /* MastodonController.swift in Sources */, D6F953F021251A2900CF0F2B /* MastodonController.swift in Sources */,
@ -1642,6 +1651,7 @@
D627943723A552C200D38C68 /* BookmarkStatusActivity.swift in Sources */, D627943723A552C200D38C68 /* BookmarkStatusActivity.swift in Sources */,
D62D2426217ABF63005076CC /* UserActivityType.swift in Sources */, D62D2426217ABF63005076CC /* UserActivityType.swift in Sources */,
D66362712136338600C9CBA2 /* ComposeViewController.swift in Sources */, D66362712136338600C9CBA2 /* ComposeViewController.swift in Sources */,
D6AC956723C4347E008C9946 /* SceneDelegate.swift in Sources */,
D6BC9DD7232D7811002CA326 /* TimelinesPageViewController.swift in Sources */, D6BC9DD7232D7811002CA326 /* TimelinesPageViewController.swift in Sources */,
D620483623D38075008A63EF /* ContentTextView.swift in Sources */, D620483623D38075008A63EF /* ContentTextView.swift in Sources */,
D6028B9B2150811100F223B9 /* MastodonCache.swift in Sources */, D6028B9B2150811100F223B9 /* MastodonCache.swift in Sources */,
@ -1676,15 +1686,17 @@
D63661C02381C144004B9E16 /* PreferencesNavigationController.swift in Sources */, D63661C02381C144004B9E16 /* PreferencesNavigationController.swift in Sources */,
D6B053A223BD2C0600A066FA /* AssetPickerViewController.swift in Sources */, D6B053A223BD2C0600A066FA /* AssetPickerViewController.swift in Sources */,
D627944A23A6AD6100D38C68 /* BookmarksTableViewController.swift in Sources */, D627944A23A6AD6100D38C68 /* BookmarksTableViewController.swift in Sources */,
D64BC19223C271D9000D0238 /* MastodonActivity.swift in Sources */,
D6945C3A23AC75E2005C403C /* FindInstanceViewController.swift in Sources */, D6945C3A23AC75E2005C403C /* FindInstanceViewController.swift in Sources */,
D6AEBB4523216AF800E5038B /* FollowAccountActivity.swift in Sources */, D6AEBB4523216AF800E5038B /* FollowAccountActivity.swift in Sources */,
D6EBF01523C55C0900AE061B /* UIApplication+Scenes.swift in Sources */,
D6538945214D6D7500E3CEFC /* TableViewSwipeActionProvider.swift in Sources */, D6538945214D6D7500E3CEFC /* TableViewSwipeActionProvider.swift in Sources */,
D6E0DC8E216EDF1E00369478 /* Previewing.swift in Sources */, D6E0DC8E216EDF1E00369478 /* Previewing.swift in Sources */,
D6BED174212667E900F02DA0 /* TimelineStatusTableViewCell.swift in Sources */, D6BED174212667E900F02DA0 /* TimelineStatusTableViewCell.swift in Sources */,
0427033822B30F5F000D31B6 /* BehaviorPrefsView.swift in Sources */, 0427033822B30F5F000D31B6 /* BehaviorPrefsView.swift in Sources */,
D627943923A553B600D38C68 /* UnbookmarkStatusActivity.swift in Sources */, D627943923A553B600D38C68 /* UnbookmarkStatusActivity.swift in Sources */,
D64D0AAD2128D88B005A6F37 /* LocalData.swift in Sources */, D64D0AAD2128D88B005A6F37 /* LocalData.swift in Sources */,
D6945C2F23AC47C3005C403C /* SavedHashtagsManager.swift in Sources */, D6945C2F23AC47C3005C403C /* SavedDataManager.swift in Sources */,
D6C94D892139E6EC00CB5196 /* AttachmentView.swift in Sources */, D6C94D892139E6EC00CB5196 /* AttachmentView.swift in Sources */,
D6B053AE23BD322B00A066FA /* AssetPickerSheetContainerViewController.swift in Sources */, D6B053AE23BD322B00A066FA /* AssetPickerSheetContainerViewController.swift in Sources */,
D6C693EF216192C2007D6A6D /* TuskerNavigationDelegate.swift in Sources */, D6C693EF216192C2007D6A6D /* TuskerNavigationDelegate.swift in Sources */,

View File

@ -9,7 +9,7 @@
import UIKit import UIKit
import Pachyderm import Pachyderm
class AccountActivity: UIActivity { class AccountActivity: MastodonActivity {
override class var activityCategory: UIActivity.Category { override class var activityCategory: UIActivity.Category {
return .action return .action

View File

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

View File

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

View File

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

View File

@ -0,0 +1,16 @@
//
// MastodonActivity.swift
// Tusker
//
// Created by Shadowfacts on 1/5/20.
// Copyright © 2020 Shadowfacts. All rights reserved.
//
import UIKit
class MastodonActivity: UIActivity {
var mastodonController: MastodonController {
let scene = UIApplication.shared.activeOrBackgroundScene!
return scene.session.mastodonController!
}
}

View File

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

View File

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

View File

@ -9,7 +9,7 @@
import UIKit import UIKit
import Pachyderm import Pachyderm
class StatusActivity: UIActivity { class StatusActivity: MastodonActivity {
override class var activityCategory: UIActivity.Category { override class var activityCategory: UIActivity.Category {
return .action return .action

View File

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

View File

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

View File

@ -11,117 +11,9 @@ import UIKit
@UIApplicationMain @UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate { class AppDelegate: UIResponder, UIApplicationDelegate {
var window: UIWindow?
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
AppShortcutItem.createItems(for: application) AppShortcutItem.createItems(for: application)
window = UIWindow(frame: UIScreen.main.bounds)
if LocalData.shared.onboardingComplete {
showAppUI()
} else {
showOnboardingUI()
}
NotificationCenter.default.addObserver(self, selector: #selector(onUserLoggedOut), name: .userLoggedOut, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(themePrefChanged), name: .themePreferenceChanged, object: nil)
themePrefChanged()
window!.makeKeyAndVisible()
if let shortcutItem = launchOptions?[.shortcutItem] as? UIApplicationShortcutItem {
_ = AppShortcutItem.handle(shortcutItem)
}
return true return true
} }
func application(_ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey : Any] = [:]) -> Bool {
if url.host == "x-callback-url" {
return XCBManager.handle(url: url)
} else if var components = URLComponents(url: url, resolvingAgainstBaseURL: false),
let tabBarController = window!.rootViewController as? MainTabBarViewController,
let exploreNavController = tabBarController.getTabController(tab: .explore) as? UINavigationController,
let exploreController = exploreNavController.viewControllers.first as? ExploreViewController {
tabBarController.select(tab: .explore)
exploreNavController.popToRootViewController(animated: false)
exploreController.loadViewIfNeeded()
exploreController.searchController.isActive = true
components.scheme = "https"
let query = components.url!.absoluteString
exploreController.searchController.searchBar.text = query
exploreController.resultsController.performSearch(query: query)
return true
}
return false
}
func application(_ application: UIApplication, continue userActivity: NSUserActivity, restorationHandler: @escaping ([UIUserActivityRestoring]?) -> Void) -> Bool {
return userActivity.handleResume()
}
func application(_ application: UIApplication, performActionFor shortcutItem: UIApplicationShortcutItem, completionHandler: @escaping (Bool) -> Void) {
completionHandler(AppShortcutItem.handle(shortcutItem))
}
func applicationWillResignActive(_ application: UIApplication) {
// Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state.
// Use this method to pause ongoing tasks, disable timers, and invalidate graphics rendering callbacks. Games should use this method to pause the game.
}
func applicationDidEnterBackground(_ application: UIApplication) {
// Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later.
// If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits.
Preferences.save()
DraftsManager.save()
}
func applicationWillEnterForeground(_ application: UIApplication) {
// Called as part of the transition from the background to the active state; here you can undo many of the changes made on entering the background.
}
func applicationDidBecomeActive(_ application: UIApplication) {
// Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface.
}
func applicationWillTerminate(_ application: UIApplication) {
// Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:.
}
func showAppUI() {
MastodonController.createClient()
MastodonController.getOwnAccount()
MastodonController.getOwnInstance()
let tabBarController = MainTabBarViewController()
window!.rootViewController = tabBarController
}
func showOnboardingUI() {
let onboarding = OnboardingViewController()
onboarding.onboardingDelegate = self
window!.rootViewController = onboarding
}
@objc func onUserLoggedOut() {
showOnboardingUI()
}
@objc func themePrefChanged() {
window?.overrideUserInterfaceStyle = Preferences.shared.theme
}
}
extension AppDelegate: OnboardingViewControllerDelegate {
func didFinishOnboarding() {
LocalData.shared.onboardingComplete = true
showAppUI()
}
} }

View File

@ -10,64 +10,87 @@ import Foundation
import Pachyderm import Pachyderm
class MastodonController { class MastodonController {
static private(set) var all = [LocalData.UserAccountInfo: MastodonController]()
static var client: Client! @available(*, message: "do something less dumb")
static var first: MastodonController { all.first!.value }
static var account: Account! static func getForAccount(_ account: LocalData.UserAccountInfo) -> MastodonController {
static var instance: Instance! if let controller = all[account] {
return controller
private init() {} } else {
let controller = MastodonController(instanceURL: account.instanceURL)
static func createClient() { controller.accountInfo = account
guard let url = LocalData.shared.instanceURL else { fatalError("Can't connect without instance URL") } controller.client.clientID = account.clientID
controller.client.clientSecret = account.clientSecret
client = Client(baseURL: url) controller.client.accessToken = account.accessToken
all[account] = controller
client.clientID = LocalData.shared.clientID return controller
client.clientSecret = LocalData.shared.clientSecret }
client.accessToken = LocalData.shared.accessToken
} }
static func registerApp(completion: @escaping () -> Void) { private(set) lazy var cache = MastodonCache(mastodonController: self)
guard LocalData.shared.clientID == nil,
LocalData.shared.clientSecret == nil else { let instanceURL: URL
completion() private(set) var accountInfo: LocalData.UserAccountInfo?
let client: Client!
var account: Account!
var instance: Instance!
init(instanceURL: URL) {
self.instanceURL = instanceURL
self.accountInfo = nil
self.client = Client(baseURL: instanceURL)
}
func run<Result>(_ request: Request<Result>, completion: @escaping Client.Callback<Result>) {
client.run(request, completion: completion)
}
func registerApp(completion: @escaping (_ clientID: String, _ clientSecret: String) -> Void) {
guard client.clientID == nil,
client.clientSecret == nil else {
completion(client.clientID!, client.clientSecret!)
return return
} }
client.registerApp(name: "Tusker", redirectURI: "tusker://oauth", scopes: [.read, .write, .follow]) { response in client.registerApp(name: "Tusker", redirectURI: "tusker://oauth", scopes: [.read, .write, .follow]) { response in
guard case let .success(app, _) = response else { fatalError() } guard case let .success(app, _) = response else { fatalError() }
LocalData.shared.clientID = app.clientID self.client.clientID = app.clientID
LocalData.shared.clientSecret = app.clientSecret self.client.clientSecret = app.clientSecret
completion() completion(app.clientID, app.clientSecret)
} }
} }
static func authorize(authorizationCode: String, completion: @escaping () -> Void) { func authorize(authorizationCode: String, completion: @escaping (_ accessToken: String) -> Void) {
client.getAccessToken(authorizationCode: authorizationCode, redirectURI: "tusker://oauth") { response in client.getAccessToken(authorizationCode: authorizationCode, redirectURI: "tusker://oauth") { response in
guard case let .success(settings, _) = response else { fatalError() } guard case let .success(settings, _) = response else { fatalError() }
LocalData.shared.accessToken = settings.accessToken self.client.accessToken = settings.accessToken
completion() completion(settings.accessToken)
} }
} }
static func getOwnAccount(completion: ((Account) -> Void)? = nil) { func getOwnAccount(completion: ((Account) -> Void)? = nil) {
if account != nil { if account != nil {
completion?(account) completion?(account)
} else { } else {
let request = client.getSelfAccount() let request = Client.getSelfAccount()
client.run(request) { response in run(request) { response in
guard case let .success(account, _) = response else { fatalError() } guard case let .success(account, _) = response else { fatalError() }
self.account = account self.account = account
MastodonCache.add(account: account) self.cache.add(account: account)
completion?(account) completion?(account)
} }
} }
} }
static func getOwnInstance() { func getOwnInstance() {
let request = client.getInstance() let request = Client.getInstance()
client.run(request) { (response) in run(request) { (response) in
guard case let .success(instance, _) = response else { fatalError() } guard case let .success(instance, _) = response else { fatalError() }
self.instance = instance self.instance = instance
} }

View File

@ -39,8 +39,8 @@ class DraftsManager: Codable {
return drafts.sorted(by: { $0.lastModified > $1.lastModified }) return drafts.sorted(by: { $0.lastModified > $1.lastModified })
} }
func create(text: String, contentWarning: String?, inReplyToID: String?, attachments: [DraftAttachment]) -> Draft { func create(accountID: String, text: String, contentWarning: String?, inReplyToID: String?, attachments: [DraftAttachment]) -> Draft {
let draft = Draft(text: text, contentWarning: contentWarning, inReplyToID: inReplyToID, attachments: attachments) let draft = Draft(accountID: accountID, text: text, contentWarning: contentWarning, inReplyToID: inReplyToID, attachments: attachments)
drafts.append(draft) drafts.append(draft)
return draft return draft
} }
@ -55,14 +55,16 @@ class DraftsManager: Codable {
extension DraftsManager { extension DraftsManager {
class Draft: Codable, Equatable { class Draft: Codable, Equatable {
let id: UUID let id: UUID
private(set) var accountID: String
private(set) var text: String private(set) var text: String
private(set) var contentWarning: String? private(set) var contentWarning: String?
private(set) var attachments: [DraftAttachment] private(set) var attachments: [DraftAttachment]
private(set) var inReplyToID: String? private(set) var inReplyToID: String?
private(set) var lastModified: Date private(set) var lastModified: Date
init(text: String, contentWarning: String?, inReplyToID: String?, attachments: [DraftAttachment], lastModified: Date = Date()) { init(accountID: String, text: String, contentWarning: String?, inReplyToID: String?, attachments: [DraftAttachment], lastModified: Date = Date()) {
self.id = UUID() self.id = UUID()
self.accountID = accountID
self.text = text self.text = text
self.contentWarning = contentWarning self.contentWarning = contentWarning
self.inReplyToID = inReplyToID self.inReplyToID = inReplyToID
@ -70,7 +72,8 @@ extension DraftsManager {
self.lastModified = lastModified self.lastModified = lastModified
} }
func update(text: String, contentWarning: String?, attachments: [DraftAttachment]) { func update(accountID: String, text: String, contentWarning: String?, attachments: [DraftAttachment]) {
self.accountID = accountID
self.text = text self.text = text
self.contentWarning = contentWarning self.contentWarning = contentWarning
self.lastModified = Date() self.lastModified = Date()

View File

@ -0,0 +1,25 @@
//
// UIApplication+Scenes.swift
// Tusker
//
// Created by Shadowfacts on 1/7/20.
// Copyright © 2020 Shadowfacts. All rights reserved.
//
import UIKit
extension UIApplication {
var activeScene: UIScene? {
connectedScenes.first { $0.activationState == .foregroundActive }
}
var backgroundScene: UIScene? {
connectedScenes.first { $0.activationState == .background }
}
var activeOrBackgroundScene: UIScene? {
activeScene ?? backgroundScene
}
}

View File

@ -0,0 +1,32 @@
//
// UISceneSession+MastodonController.swift
// Tusker
//
// Created by Shadowfacts on 1/7/20.
// Copyright © 2020 Shadowfacts. All rights reserved.
//
import UIKit
extension UISceneSession {
var mastodonController: MastodonController? {
get {
return userInfo?["mastodonController"] as? MastodonController
}
set {
if let newValue = newValue {
if userInfo == nil {
userInfo = ["mastodonController": newValue]
} else {
userInfo!["mastodonController"] = newValue
}
} else {
if userInfo != nil {
userInfo?.removeValue(forKey: "mastodonController")
}
}
}
}
}

View File

@ -2,25 +2,6 @@
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0"> <plist version="1.0">
<dict> <dict>
<key>NSAppTransportSecurity</key>
<dict>
<key>NSExceptionDomains</key>
<dict>
<key>localhost</key>
<dict>
<key>NSExceptionAllowsInsecureHTTPLoads</key>
<true/>
</dict>
</dict>
</dict>
<key>NSUserActivityTypes</key>
<array>
<string>$(PRODUCT_BUNDLE_IDENTIFIER).activity.show-timeline</string>
<string>$(PRODUCT_BUNDLE_IDENTIFIER).activity.check-notifications</string>
<string>$(PRODUCT_BUNDLE_IDENTIFIER).activity.check-mentions</string>
<string>$(PRODUCT_BUNDLE_IDENTIFIER).activity.new-post</string>
<string>$(PRODUCT_BUNDLE_IDENTIFIER).activity.search</string>
</array>
<key>CFBundleDevelopmentRegion</key> <key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string> <string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleExecutable</key> <key>CFBundleExecutable</key>
@ -52,14 +33,52 @@
<string>1</string> <string>1</string>
<key>LSRequiresIPhoneOS</key> <key>LSRequiresIPhoneOS</key>
<true/> <true/>
<key>NSMicrophoneUsageDescription</key> <key>NSAppTransportSecurity</key>
<string>Post videos from the camera.</string> <dict>
<key>NSExceptionDomains</key>
<dict>
<key>localhost</key>
<dict>
<key>NSExceptionAllowsInsecureHTTPLoads</key>
<true/>
</dict>
</dict>
</dict>
<key>NSCameraUsageDescription</key> <key>NSCameraUsageDescription</key>
<string>Post photos and videos from the camera.</string> <string>Post photos and videos from the camera.</string>
<key>NSMicrophoneUsageDescription</key>
<string>Post videos from the camera.</string>
<key>NSPhotoLibraryAddUsageDescription</key> <key>NSPhotoLibraryAddUsageDescription</key>
<string>Save photos directly from other people&apos;s posts.</string> <string>Save photos directly from other people&apos;s posts.</string>
<key>NSPhotoLibraryUsageDescription</key> <key>NSPhotoLibraryUsageDescription</key>
<string>Post photos from the photo library.</string> <string>Post photos from the photo library.</string>
<key>NSUserActivityTypes</key>
<array>
<string>$(PRODUCT_BUNDLE_IDENTIFIER).activity.show-timeline</string>
<string>$(PRODUCT_BUNDLE_IDENTIFIER).activity.check-notifications</string>
<string>$(PRODUCT_BUNDLE_IDENTIFIER).activity.check-mentions</string>
<string>$(PRODUCT_BUNDLE_IDENTIFIER).activity.new-post</string>
<string>$(PRODUCT_BUNDLE_IDENTIFIER).activity.search</string>
</array>
<key>UIApplicationSceneManifest</key>
<dict>
<key>UISceneConfigurations</key>
<dict>
<key>UIWindowSceneSessionRoleApplication</key>
<array>
<dict>
<key>UISceneClassName</key>
<string>UIWindowScene</string>
<key>UISceneDelegateClassName</key>
<string>$(PRODUCT_MODULE_NAME).SceneDelegate</string>
<key>UISceneConfigurationName</key>
<string>main-scene</string>
</dict>
</array>
</dict>
<key>UIApplicationSupportsMultipleScenes</key>
<false/>
</dict>
<key>UILaunchStoryboardName</key> <key>UILaunchStoryboardName</key>
<string>LaunchScreen</string> <string>LaunchScreen</string>
<key>UIRequiredDeviceCapabilities</key> <key>UIRequiredDeviceCapabilities</key>

View File

@ -7,8 +7,9 @@
// //
import Foundation import Foundation
import Combine
class LocalData { class LocalData: ObservableObject {
static let shared = LocalData() static let shared = LocalData()
@ -18,68 +19,130 @@ class LocalData {
if ProcessInfo.processInfo.environment.keys.contains("UI_TESTING") { if ProcessInfo.processInfo.environment.keys.contains("UI_TESTING") {
defaults = UserDefaults(suiteName: "\(Bundle.main.bundleIdentifier!).uitesting")! defaults = UserDefaults(suiteName: "\(Bundle.main.bundleIdentifier!).uitesting")!
if ProcessInfo.processInfo.environment.keys.contains("UI_TESTING_LOGIN") { if ProcessInfo.processInfo.environment.keys.contains("UI_TESTING_LOGIN") {
defaults.set(true, forKey: onboardingCompleteKey) accounts = [
defaults.set(URL(string: "http://localhost:8080")!, forKey: instanceURLKey) UserAccountInfo(
defaults.set("client_id", forKey: clientIDKey) id: UUID().uuidString,
defaults.set("client_secret", forKey: clientSecretKey) instanceURL: URL(string: "http://localhost:8080")!,
defaults.set("access_token", forKey: accessTokenKey) clientID: "client_id",
clientSecret: "client_secret",
username: "admin",
accessToken: "access_token")
]
} }
} else { } else {
defaults = UserDefaults() defaults = UserDefaults()
} }
} }
private let onboardingCompleteKey = "onboardingComplete" private let accountsKey = "accounts"
var accounts: [UserAccountInfo] {
get {
if let array = defaults.array(forKey: accountsKey) as? [[String: String]] {
return array.compactMap { (info) in
guard let id = info["id"],
let instanceURL = info["instanceURL"],
let url = URL(string: instanceURL),
let clientId = info["clientID"],
let secret = info["clientSecret"],
let username = info["username"],
let accessToken = info["accessToken"] else {
return nil
}
return UserAccountInfo(id: id, instanceURL: url, clientID: clientId, clientSecret: secret, username: username, accessToken: accessToken)
}
} else {
return []
}
}
set {
objectWillChange.send()
let array = newValue.map { (info) in
return [
"id": info.id,
"instanceURL": info.instanceURL.absoluteString,
"clientID": info.clientID,
"clientSecret": info.clientSecret,
"username": info.username,
"accessToken": info.accessToken
]
}
defaults.set(array, forKey: accountsKey)
}
}
private let mostRecentAccountKey = "mostRecentAccount"
private var mostRecentAccount: String? {
get {
return defaults.string(forKey: mostRecentAccountKey)
}
set {
objectWillChange.send()
defaults.set(newValue, forKey: mostRecentAccountKey)
}
}
var onboardingComplete: Bool { var onboardingComplete: Bool {
get { return !accounts.isEmpty
return defaults.bool(forKey: onboardingCompleteKey)
}
set {
defaults.set(newValue, forKey: onboardingCompleteKey)
}
} }
private let instanceURLKey = "instanceURL" func addAccount(instanceURL url: URL, clientID: String, clientSecret secret: String, username: String, accessToken: String) -> UserAccountInfo {
var instanceURL: URL? { var accounts = self.accounts
get { if let index = accounts.firstIndex(where: { $0.instanceURL == url && $0.username == username }) {
return defaults.url(forKey: instanceURLKey) accounts.remove(at: index)
}
set {
defaults.set(newValue, forKey: instanceURLKey)
} }
let id = UUID().uuidString
let info = UserAccountInfo(id: id, instanceURL: url, clientID: clientID, clientSecret: secret, username: username, accessToken: accessToken)
accounts.append(info)
self.accounts = accounts
return info
} }
private let clientIDKey = "clientID" func removeAccount(_ info: UserAccountInfo) {
var clientID: String? { accounts.removeAll(where: { $0.id == info.id })
get {
return defaults.string(forKey: clientIDKey)
}
set {
defaults.set(newValue, forKey: clientIDKey)
}
} }
private let clientSecretKey = "clientSecret" func getAccount(id: String) -> UserAccountInfo? {
var clientSecret: String? { return accounts.first(where: { $0.id == id })
get {
return defaults.string(forKey: clientSecretKey)
}
set {
defaults.set(newValue, forKey: clientSecretKey)
}
} }
private let accessTokenKey = "accessToken" func getMostRecentAccount() -> UserAccountInfo? {
var accessToken: String? { guard onboardingComplete else { return nil }
get { let mostRecent: UserAccountInfo?
return defaults.string(forKey: accessTokenKey) if let id = mostRecentAccount {
mostRecent = accounts.first { $0.id == id }
} else {
mostRecent = nil
} }
set { return mostRecent ?? accounts.first!
defaults.set(newValue, forKey: accessTokenKey) }
func setMostRecentAccount(_ account: UserAccountInfo?) {
mostRecentAccount = account?.id
}
}
extension LocalData {
struct UserAccountInfo: Equatable, Hashable {
let id: String
let instanceURL: URL
let clientID: String
let clientSecret: String
let username: String
let accessToken: String
func hash(into hasher: inout Hasher) {
hasher.combine(id)
}
static func ==(lhs: UserAccountInfo, rhs: UserAccountInfo) -> Bool {
return lhs.id == rhs.id
} }
} }
} }
extension Notification.Name { extension Notification.Name {
static let userLoggedOut = Notification.Name("userLoggedOut") static let userLoggedOut = Notification.Name("Tusker.userLoggedOut")
static let addAccount = Notification.Name("Tusker.addAccount")
static let activateAccount = Notification.Name("Tusker.activateAccount")
} }

View File

@ -12,20 +12,26 @@ import Pachyderm
class MastodonCache { class MastodonCache {
private static var statuses = CachedDictionary<Status>(name: "Statuses") private var statuses = CachedDictionary<Status>(name: "Statuses")
private static var accounts = CachedDictionary<Account>(name: "Accounts") private var accounts = CachedDictionary<Account>(name: "Accounts")
private static var relationships = CachedDictionary<Relationship>(name: "Relationships") private var relationships = CachedDictionary<Relationship>(name: "Relationships")
private static var notifications = CachedDictionary<Pachyderm.Notification>(name: "Notifications") private var notifications = CachedDictionary<Pachyderm.Notification>(name: "Notifications")
static let statusSubject = PassthroughSubject<Status, Never>() let statusSubject = PassthroughSubject<Status, Never>()
static let accountSubject = PassthroughSubject<Account, Never>() let accountSubject = PassthroughSubject<Account, Never>()
weak var mastodonController: MastodonController?
init(mastodonController: MastodonController) {
self.mastodonController = mastodonController
}
// MARK: - Statuses // MARK: - Statuses
static func status(for id: String) -> Status? { func status(for id: String) -> Status? {
return statuses[id] return statuses[id]
} }
static func set(status: Status, for id: String) { func set(status: Status, for id: String) {
statuses[id] = status statuses[id] = status
add(account: status.account) add(account: status.account)
if let reblog = status.reblog { if let reblog = status.reblog {
@ -36,100 +42,109 @@ class MastodonCache {
statusSubject.send(status) statusSubject.send(status)
} }
static func status(for id: String, completion: @escaping (Status?) -> Void) { func status(for id: String, completion: @escaping (Status?) -> Void) {
let request = MastodonController.client.getStatus(id: id) guard let mastodonController = mastodonController else {
MastodonController.client.run(request) { response in fatalError("The MastodonController for this cache has been deinitialized, so this cache should no longer exist. Are you storing a strong reference to it?")
}
let request = Client.getStatus(id: id)
mastodonController.run(request) { response in
guard case let .success(status, _) = response else { guard case let .success(status, _) = response else {
completion(nil) completion(nil)
return return
} }
set(status: status, for: id) self.set(status: status, for: id)
completion(status) completion(status)
} }
} }
static func add(status: Status) { func add(status: Status) {
set(status: status, for: status.id) set(status: status, for: status.id)
} }
static func addAll(statuses: [Status]) { func addAll(statuses: [Status]) {
statuses.forEach(add) statuses.forEach(add)
} }
// MARK: - Accounts // MARK: - Accounts
static func account(for id: String) -> Account? { func account(for id: String) -> Account? {
return accounts[id] return accounts[id]
} }
static func set(account: Account, for id: String) { func set(account: Account, for id: String) {
accounts[id] = account accounts[id] = account
accountSubject.send(account) accountSubject.send(account)
} }
static func account(for id: String, completion: @escaping (Account?) -> Void) { func account(for id: String, completion: @escaping (Account?) -> Void) {
let request = MastodonController.client.getAccount(id: id) guard let mastodonController = mastodonController else {
MastodonController.client.run(request) { response in fatalError("The MastodonController for this cache has been deinitialized, so this cache should no longer exist. Are you storing a strong reference to it?")
}
let request = Client.getAccount(id: id)
mastodonController.run(request) { response in
guard case let .success(account, _) = response else { guard case let .success(account, _) = response else {
completion(nil) completion(nil)
return return
} }
set(account: account, for: account.id) self.set(account: account, for: account.id)
completion(account) completion(account)
} }
} }
static func add(account: Account) { func add(account: Account) {
set(account: account, for: account.id) set(account: account, for: account.id)
} }
static func addAll(accounts: [Account]) { func addAll(accounts: [Account]) {
accounts.forEach(add) accounts.forEach(add)
} }
// MARK: - Relationships // MARK: - Relationships
static func relationship(for id: String) -> Relationship? { func relationship(for id: String) -> Relationship? {
return relationships[id] return relationships[id]
} }
static func set(relationship: Relationship, id: String) { func set(relationship: Relationship, id: String) {
relationships[id] = relationship relationships[id] = relationship
} }
static func relationship(for id: String, completion: @escaping (Relationship?) -> Void) { func relationship(for id: String, completion: @escaping (Relationship?) -> Void) {
let request = MastodonController.client.getRelationships(accounts: [id]) guard let mastodonController = mastodonController else {
MastodonController.client.run(request) { response in fatalError("The MastodonController for this cache has been deinitialized, so this cache should no longer exist. Are you storing a strong reference to it?")
}
let request = Client.getRelationships(accounts: [id])
mastodonController.run(request) { response in
guard case let .success(relationships, _) = response, guard case let .success(relationships, _) = response,
let relationship = relationships.first else { let relationship = relationships.first else {
completion(nil) completion(nil)
return return
} }
set(relationship: relationship, id: relationship.id) self.set(relationship: relationship, id: relationship.id)
completion(relationship) completion(relationship)
} }
} }
static func add(relationship: Relationship) { func add(relationship: Relationship) {
set(relationship: relationship, id: relationship.id) set(relationship: relationship, id: relationship.id)
} }
static func addAll(relationships: [Relationship]) { func addAll(relationships: [Relationship]) {
relationships.forEach(add) relationships.forEach(add)
} }
// MARK: - Notifications // MARK: - Notifications
static func notification(for id: String) -> Pachyderm.Notification? { func notification(for id: String) -> Pachyderm.Notification? {
return notifications[id] return notifications[id]
} }
static func set(notification: Pachyderm.Notification, id: String) { func set(notification: Pachyderm.Notification, id: String) {
notifications[id] = notification notifications[id] = notification
} }
static func add(notification: Pachyderm.Notification) { func add(notification: Pachyderm.Notification) {
set(notification: notification, id: notification.id) set(notification: notification, id: notification.id)
} }
static func addAll(notifications: [Pachyderm.Notification]) { func addAll(notifications: [Pachyderm.Notification]) {
notifications.forEach(add) notifications.forEach(add)
} }

View File

@ -0,0 +1,113 @@
//
// SavedDataManager.swift
// Tusker
//
// Created by Shadowfacts on 12/19/19.
// Copyright © 2019 Shadowfacts. All rights reserved.
//
import Foundation
import Pachyderm
class SavedDataManager: Codable {
private(set) static var shared: SavedDataManager = load()
private static var documentsDirectory = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first!
private static var archiveURL = SavedDataManager.documentsDirectory.appendingPathComponent("saved_data").appendingPathExtension("plist")
static func save() {
DispatchQueue.global(qos: .utility).async {
let encoder = PropertyListEncoder()
let data = try? encoder.encode(shared)
try? data?.write(to: archiveURL, options: .noFileProtection)
}
}
static func load() -> SavedDataManager {
let decoder = PropertyListDecoder()
if let data = try? Data(contentsOf: archiveURL),
let savedHashtagsManager = try? decoder.decode(Self.self, from: data) {
return savedHashtagsManager
}
return SavedDataManager()
}
private init() {}
private var savedHashtags: [String: [Hashtag]] = [:] {
didSet {
SavedDataManager.save()
NotificationCenter.default.post(name: .savedHashtagsChanged, object: nil)
}
}
private var savedInstances: [String: [URL]] = [:] {
didSet {
SavedDataManager.save()
NotificationCenter.default.post(name: .savedInstancesChanged, object: nil)
}
}
func sortedHashtags(for account: LocalData.UserAccountInfo) -> [Hashtag] {
if let hashtags = savedHashtags[account.id] {
return hashtags.sorted(by: { $0.name < $1.name })
} else {
return []
}
}
func isSaved(hashtag: Hashtag, for account: LocalData.UserAccountInfo) -> Bool {
return savedHashtags[account.id]?.contains(hashtag) ?? false
}
func add(hashtag: Hashtag, for account: LocalData.UserAccountInfo) {
if isSaved(hashtag: hashtag, for: account) {
return
}
if var saved = savedHashtags[account.id] {
saved.append(hashtag)
savedHashtags[account.id] = saved
} else {
savedHashtags[account.id] = [hashtag]
}
}
func remove(hashtag: Hashtag, for account: LocalData.UserAccountInfo) {
guard isSaved(hashtag: hashtag, for: account) else { return }
if var saved = savedHashtags[account.id] {
saved.removeAll(where: { $0.name == hashtag.name })
savedHashtags[account.id] = saved
}
}
func savedInstances(for account: LocalData.UserAccountInfo) -> [URL] {
return savedInstances[account.id] ?? []
}
func isSaved(instance url: URL, for account: LocalData.UserAccountInfo) -> Bool {
return savedInstances[account.id]?.contains(url) ?? false
}
func add(instance url: URL, for account: LocalData.UserAccountInfo) {
if isSaved(instance: url, for: account) { return }
if var saved = savedInstances[account.id] {
saved.append(url)
savedInstances[account.id] = saved
} else {
savedInstances[account.id] = [url]
}
}
func remove(instance url: URL, for account: LocalData.UserAccountInfo) {
guard isSaved(instance: url, for: account) else { return }
if var saved = savedInstances[account.id] {
saved.removeAll(where: { $0 == url })
savedInstances[account.id] = saved
}
}
}
extension Foundation.Notification.Name {
static let savedHashtagsChanged = Notification.Name("savedHashtagsChanged")
static let savedInstancesChanged = Notification.Name("savedInstancesChanged")
}

View File

@ -1,65 +0,0 @@
//
// SavedHashtagsManager.swift
// Tusker
//
// Created by Shadowfacts on 12/19/19.
// Copyright © 2019 Shadowfacts. All rights reserved.
//
import Foundation
import Pachyderm
class SavedHashtagsManager: Codable {
private(set) static var shared: SavedHashtagsManager = load()
private static var documentsDirectory = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first!
private static var archiveURL = SavedHashtagsManager.documentsDirectory.appendingPathComponent("saved_hashtags").appendingPathExtension("plist")
static func save() {
DispatchQueue.global(qos: .utility).async {
let encoder = PropertyListEncoder()
let data = try? encoder.encode(shared)
try? data?.write(to: archiveURL, options: .noFileProtection)
}
}
static func load() -> SavedHashtagsManager {
let decoder = PropertyListDecoder()
if let data = try? Data(contentsOf: archiveURL),
let savedHashtagsManager = try? decoder.decode(Self.self, from: data) {
return savedHashtagsManager
}
return SavedHashtagsManager()
}
private init() {}
private var savedHashtags: [Hashtag] = []
var sorted: [Hashtag] {
return savedHashtags.sorted(by: { $0.name < $1.name })
}
func isSaved(_ hashtag: Hashtag) -> Bool {
return savedHashtags.contains(hashtag)
}
func add(_ hashtag: Hashtag) {
if isSaved(hashtag) {
return
}
savedHashtags.append(hashtag)
SavedHashtagsManager.save()
NotificationCenter.default.post(name: .savedHashtagsChanged, object: nil)
}
func remove(_ hashtag: Hashtag) {
guard isSaved(hashtag) else { return }
savedHashtags.removeAll(where: { $0.name == hashtag.name })
SavedHashtagsManager.save()
NotificationCenter.default.post(name: .savedHashtagsChanged, object: nil)
}
}
extension Foundation.Notification.Name {
static let savedHashtagsChanged = Notification.Name("savedHashtagsChanged")
}

View File

@ -1,61 +0,0 @@
//
// SavedInstancesManager.swift
// Tusker
//
// Created by Shadowfacts on 12/19/19.
// Copyright © 2019 Shadowfacts. All rights reserved.
//
import Foundation
class SavedInstanceManager: Codable {
private(set) static var shared: SavedInstanceManager = load()
private static var documentsDirectory = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first!
private static var archiveURL = SavedInstanceManager.documentsDirectory.appendingPathComponent("saved_instances").appendingPathExtension("plist")
static func save() {
DispatchQueue.global(qos: .utility).async {
let encoder = PropertyListEncoder()
let data = try? encoder.encode(shared)
try? data?.write(to: archiveURL, options: .noFileProtection)
}
}
static func load() -> SavedInstanceManager {
let decoder = PropertyListDecoder()
if let data = try? Data(contentsOf: archiveURL),
let savedInstanceManager = try? decoder.decode(Self.self, from: data) {
return savedInstanceManager
}
return SavedInstanceManager()
}
private init() {}
private(set) var savedInstances: [URL] = []
func isSaved(_ url: URL) -> Bool {
return savedInstances.contains(url)
}
func add(_ url: URL) {
if isSaved(url) {
return
}
savedInstances.append(url)
SavedInstanceManager.save()
NotificationCenter.default.post(name: .savedInstancesChanged, object: nil)
}
func remove(_ url: URL) {
guard isSaved(url) else { return }
savedInstances.removeAll(where: { $0 == url })
SavedInstanceManager.save()
NotificationCenter.default.post(name: .savedInstancesChanged, object: nil)
}
}
extension Notification.Name {
static let savedInstancesChanged = Notification.Name("savedInstancesChanged")
}

151
Tusker/SceneDelegate.swift Normal file
View File

@ -0,0 +1,151 @@
//
// SceneDelegate.swift
// Tusker
//
// Created by Shadowfacts on 1/6/20.
// Copyright © 2020 Shadowfacts. All rights reserved.
//
import UIKit
import Pachyderm
class SceneDelegate: UIResponder, UIWindowSceneDelegate {
var window: UIWindow?
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
// Use this method to optionally configure and attach the UIWindow `window` to the provided UIWindowScene `scene`.
// If using a storyboard, the `window` property will automatically be initialized and attached to the scene.
// This delegate does not imply the connecting scene or session are new (see `application:configurationForConnectingSceneSession` instead).
guard let windowScene = scene as? UIWindowScene else { return }
window = UIWindow(windowScene: windowScene)
if LocalData.shared.onboardingComplete {
if session.mastodonController == nil {
let account = LocalData.shared.getMostRecentAccount()!
session.mastodonController = MastodonController.getForAccount(account)
}
showAppUI()
} else {
showOnboardingUI()
}
window!.makeKeyAndVisible()
NotificationCenter.default.addObserver(self, selector: #selector(themePrefChanged), name: .themePreferenceChanged, object: nil)
themePrefChanged()
if let shortcutItem = connectionOptions.shortcutItem {
_ = AppShortcutItem.handle(shortcutItem)
}
}
func scene(_ scene: UIScene, openURLContexts URLContexts: Set<UIOpenURLContext>) {
if URLContexts.count > 1 {
fatalError("Cannot open more than 1 URL")
}
let url = URLContexts.first!.url
if url.host == "x-callback-url" {
_ = XCBManager.handle(url: url)
} else if var components = URLComponents(url: url, resolvingAgainstBaseURL: false),
let tabBarController = window!.rootViewController as? MainTabBarViewController,
let exploreNavController = tabBarController.getTabController(tab: .explore) as? UINavigationController,
let exploreController = exploreNavController.viewControllers.first as? ExploreViewController {
tabBarController.select(tab: .explore)
exploreNavController.popToRootViewController(animated: false)
exploreController.loadViewIfNeeded()
exploreController.searchController.isActive = true
components.scheme = "https"
let query = url.absoluteString
exploreController.searchController.searchBar.text = query
exploreController.resultsController.performSearch(query: query)
}
}
func scene(_ scene: UIScene, continue userActivity: NSUserActivity) {
_ = userActivity.handleResume()
}
func windowScene(_ windowScene: UIWindowScene, performActionFor shortcutItem: UIApplicationShortcutItem, completionHandler: @escaping (Bool) -> Void) {
completionHandler(AppShortcutItem.handle(shortcutItem))
}
func sceneDidDisconnect(_ scene: UIScene) {
// Called as the scene is being released by the system.
// This occurs shortly after the scene enters the background, or when its session is discarded.
// Release any resources associated with this scene that can be re-created the next time the scene connects.
// The scene may re-connect later, as its session was not neccessarily discarded (see `application:didDiscardSceneSessions` instead).
}
func sceneDidBecomeActive(_ scene: UIScene) {
// Called when the scene has moved from an inactive state to an active state.
// Use this method to restart any tasks that were paused (or not yet started) when the scene was inactive.
}
func sceneWillResignActive(_ scene: UIScene) {
// Called when the scene will move from an active state to an inactive state.
// This may occur due to temporary interruptions (ex. an incoming phone call).
}
func sceneWillEnterForeground(_ scene: UIScene) {
// Called as the scene transitions from the background to the foreground.
// Use this method to undo the changes made on entering the background.
}
func sceneDidEnterBackground(_ scene: UIScene) {
// Called as the scene transitions from the foreground to the background.
// Use this method to save data, release shared resources, and store enough scene-specific state information
// to restore the scene back to its current state.
Preferences.save()
DraftsManager.save()
}
func activateAccount(_ account: LocalData.UserAccountInfo) {
LocalData.shared.setMostRecentAccount(account)
window!.windowScene!.session.mastodonController = MastodonController.getForAccount(account)
showAppUI()
}
func logoutCurrent() {
LocalData.shared.removeAccount(LocalData.shared.getMostRecentAccount()!)
if LocalData.shared.onboardingComplete {
activateAccount(LocalData.shared.accounts.first!)
} else {
showOnboardingUI()
}
}
func showAppUI() {
let mastodonController = window!.windowScene!.session.mastodonController!
mastodonController.getOwnAccount()
mastodonController.getOwnInstance()
let tabBarController = MainTabBarViewController(mastodonController: mastodonController)
window!.rootViewController = tabBarController
}
func showOnboardingUI() {
let onboarding = OnboardingViewController()
onboarding.onboardingDelegate = self
window!.rootViewController = onboarding
}
@objc func themePrefChanged() {
window?.overrideUserInterfaceStyle = Preferences.shared.theme
}
}
extension SceneDelegate: OnboardingViewControllerDelegate {
func didFinishOnboarding(account: LocalData.UserAccountInfo) {
activateAccount(account)
}
}

View File

@ -12,10 +12,13 @@ class AccountListTableViewController: EnhancedTableViewController {
private let accountCell = "accountCell" private let accountCell = "accountCell"
let mastodonController: MastodonController
let accountIDs: [String] let accountIDs: [String]
init(accountIDs: [String]) { init(accountIDs: [String], mastodonController: MastodonController) {
self.accountIDs = accountIDs self.accountIDs = accountIDs
self.mastodonController = mastodonController
super.init(style: .grouped) super.init(style: .grouped)
} }
@ -50,12 +53,14 @@ class AccountListTableViewController: EnhancedTableViewController {
guard let cell = tableView.dequeueReusableCell(withIdentifier: accountCell, for: indexPath) as? AccountTableViewCell else { fatalError() } guard let cell = tableView.dequeueReusableCell(withIdentifier: accountCell, for: indexPath) as? AccountTableViewCell else { fatalError() }
let id = accountIDs[indexPath.row] let id = accountIDs[indexPath.row]
cell.updateUI(accountID: id)
cell.delegate = self cell.delegate = self
cell.updateUI(accountID: id)
return cell return cell
} }
} }
extension AccountListTableViewController: TuskerNavigationDelegate {} extension AccountListTableViewController: TuskerNavigationDelegate {
var apiController: MastodonController { mastodonController }
}

View File

@ -13,6 +13,8 @@ class BookmarksTableViewController: EnhancedTableViewController {
private let statusCell = "statusCell" private let statusCell = "statusCell"
let mastodonController: MastodonController
var statuses: [(id: String, state: StatusState)] = [] { var statuses: [(id: String, state: StatusState)] = [] {
didSet { didSet {
DispatchQueue.main.async { DispatchQueue.main.async {
@ -24,7 +26,9 @@ class BookmarksTableViewController: EnhancedTableViewController {
var newer: RequestRange? var newer: RequestRange?
var older: RequestRange? var older: RequestRange?
init() { init(mastodonController: MastodonController) {
self.mastodonController = mastodonController
super.init(style: .plain) super.init(style: .plain)
title = NSLocalizedString("Bookmarks", comment: "bookmarks screen title") title = NSLocalizedString("Bookmarks", comment: "bookmarks screen title")
@ -44,10 +48,10 @@ class BookmarksTableViewController: EnhancedTableViewController {
tableView.prefetchDataSource = self tableView.prefetchDataSource = self
let request = MastodonController.client.getBookmarks() let request = Client.getBookmarks()
MastodonController.client.run(request) { (response) in mastodonController.run(request) { (response) in
guard case let .success(statuses, pagination) = response else { fatalError() } guard case let .success(statuses, pagination) = response else { fatalError() }
MastodonCache.addAll(statuses: statuses) self.mastodonController.cache.addAll(statuses: statuses)
self.statuses.append(contentsOf: statuses.map { ($0.id, .unknown) }) self.statuses.append(contentsOf: statuses.map { ($0.id, .unknown) })
self.newer = pagination?.newer self.newer = pagination?.newer
self.older = pagination?.older self.older = pagination?.older
@ -81,11 +85,11 @@ class BookmarksTableViewController: EnhancedTableViewController {
return return
} }
let request = MastodonController.client.getBookmarks(range: older) let request = Client.getBookmarks(range: older)
MastodonController.client.run(request) { (response) in mastodonController.run(request) { (response) in
guard case let .success(newStatuses, pagination) = response else { fatalError() } guard case let .success(newStatuses, pagination) = response else { fatalError() }
self.older = pagination?.older self.older = pagination?.older
MastodonCache.addAll(statuses: newStatuses) self.mastodonController.cache.addAll(statuses: newStatuses)
self.statuses.append(contentsOf: newStatuses.map { ($0.id, .unknown) }) self.statuses.append(contentsOf: newStatuses.map { ($0.id, .unknown) })
} }
} }
@ -101,15 +105,15 @@ class BookmarksTableViewController: EnhancedTableViewController {
override func tableView(_ tableView: UITableView, trailingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? { override func tableView(_ tableView: UITableView, trailingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? {
let cellConfig = (tableView.cellForRow(at: indexPath) as? TableViewSwipeActionProvider)?.trailingSwipeActionsConfiguration() let cellConfig = (tableView.cellForRow(at: indexPath) as? TableViewSwipeActionProvider)?.trailingSwipeActionsConfiguration()
guard let status = MastodonCache.status(for: statuses[indexPath.row].id) else { guard let status = mastodonController.cache.status(for: statuses[indexPath.row].id) else {
return cellConfig return cellConfig
} }
let unbookmarkAction = UIContextualAction(style: .destructive, title: NSLocalizedString("Unbookmark", comment: "unbookmark action title")) { (action, view, completion) in let unbookmarkAction = UIContextualAction(style: .destructive, title: NSLocalizedString("Unbookmark", comment: "unbookmark action title")) { (action, view, completion) in
let request = Status.unbookmark(status) let request = Status.unbookmark(status)
MastodonController.client.run(request) { (response) in self.mastodonController.run(request) { (response) in
guard case let .success(newStatus, _) = response else { fatalError() } guard case let .success(newStatus, _) = response else { fatalError() }
MastodonCache.add(status: newStatus) self.mastodonController.cache.add(status: newStatus)
self.statuses.remove(at: indexPath.row) self.statuses.remove(at: indexPath.row)
} }
} }
@ -127,13 +131,13 @@ class BookmarksTableViewController: EnhancedTableViewController {
} }
override func getSuggestedContextMenuActions(tableView: UITableView, indexPath: IndexPath, point: CGPoint) -> [UIAction] { override func getSuggestedContextMenuActions(tableView: UITableView, indexPath: IndexPath, point: CGPoint) -> [UIAction] {
guard let status = MastodonCache.status(for: statuses[indexPath.row].id) else { return [] } guard let status = mastodonController.cache.status(for: statuses[indexPath.row].id) else { return [] }
return [ return [
UIAction(title: NSLocalizedString("Unbookmark", comment: "unbookmark action title"), image: UIImage(systemName: "bookmark.fill"), identifier: .init("unbookmark"), discoverabilityTitle: nil, attributes: [], state: .off, handler: { (_) in UIAction(title: NSLocalizedString("Unbookmark", comment: "unbookmark action title"), image: UIImage(systemName: "bookmark.fill"), identifier: .init("unbookmark"), discoverabilityTitle: nil, attributes: [], state: .off, handler: { (_) in
let request = Status.unbookmark(status) let request = Status.unbookmark(status)
MastodonController.client.run(request) { (response) in self.mastodonController.run(request) { (response) in
guard case let .success(newStatus, _) = response else { fatalError() } guard case let .success(newStatus, _) = response else { fatalError() }
MastodonCache.add(status: newStatus) self.mastodonController.cache.add(status: newStatus)
self.statuses.remove(at: indexPath.row) self.statuses.remove(at: indexPath.row)
} }
}) })
@ -143,6 +147,8 @@ class BookmarksTableViewController: EnhancedTableViewController {
} }
extension BookmarksTableViewController: StatusTableViewCellDelegate { extension BookmarksTableViewController: StatusTableViewCellDelegate {
var apiController: MastodonController { mastodonController }
func statusCellCollapsedStateChanged(_ cell: BaseStatusTableViewCell) { func statusCellCollapsedStateChanged(_ cell: BaseStatusTableViewCell) {
tableView.beginUpdates() tableView.beginUpdates()
tableView.endUpdates() tableView.endUpdates()
@ -152,7 +158,7 @@ extension BookmarksTableViewController: StatusTableViewCellDelegate {
extension BookmarksTableViewController: UITableViewDataSourcePrefetching { extension BookmarksTableViewController: UITableViewDataSourcePrefetching {
func tableView(_ tableView: UITableView, prefetchRowsAt indexPaths: [IndexPath]) { func tableView(_ tableView: UITableView, prefetchRowsAt indexPaths: [IndexPath]) {
for indexPath in indexPaths { for indexPath in indexPaths {
guard let status = MastodonCache.status(for: statuses[indexPath.row].id) else { continue } guard let status = mastodonController.cache.status(for: statuses[indexPath.row].id) else { continue }
ImageCache.avatars.get(status.account.avatar, completion: nil) ImageCache.avatars.get(status.account.avatar, completion: nil)
for attachment in status.attachments where attachment.kind == .image { for attachment in status.attachments where attachment.kind == .image {
ImageCache.attachments.get(attachment.url, completion: nil) ImageCache.attachments.get(attachment.url, completion: nil)
@ -162,7 +168,7 @@ extension BookmarksTableViewController: UITableViewDataSourcePrefetching {
func tableView(_ tableView: UITableView, cancelPrefetchingForRowsAt indexPaths: [IndexPath]) { func tableView(_ tableView: UITableView, cancelPrefetchingForRowsAt indexPaths: [IndexPath]) {
for indexPath in indexPaths { for indexPath in indexPaths {
guard let status = MastodonCache.status(for: statuses[indexPath.row].id) else { continue } guard let status = mastodonController.cache.status(for: statuses[indexPath.row].id) else { continue }
ImageCache.avatars.cancel(status.account.avatar) ImageCache.avatars.cancel(status.account.avatar)
for attachment in status.attachments where attachment.kind == .image { for attachment in status.attachments where attachment.kind == .image {
ImageCache.attachments.cancel(attachment.url) ImageCache.attachments.cancel(attachment.url)

View File

@ -12,6 +12,8 @@ import Intents
class ComposeViewController: UIViewController { class ComposeViewController: UIViewController {
weak var mastodonController: MastodonController!
var inReplyToID: String? var inReplyToID: String?
var accountsToMention = [String]() var accountsToMention = [String]()
var initialText: String? var initialText: String?
@ -70,9 +72,11 @@ class ComposeViewController: UIViewController {
@IBOutlet weak var postProgressView: SteppedProgressView! @IBOutlet weak var postProgressView: SteppedProgressView!
init(inReplyTo inReplyToID: String? = nil, mentioningAcct: String? = nil, text: String? = nil) { init(inReplyTo inReplyToID: String? = nil, mentioningAcct: String? = nil, text: String? = nil, mastodonController: MastodonController) {
self.mastodonController = mastodonController
self.inReplyToID = inReplyToID self.inReplyToID = inReplyToID
if let inReplyToID = inReplyToID, let inReplyTo = MastodonCache.status(for: inReplyToID) { if let inReplyToID = inReplyToID, let inReplyTo = mastodonController.cache.status(for: inReplyToID) {
accountsToMention = [inReplyTo.account.acct] + inReplyTo.mentions.map { $0.acct } accountsToMention = [inReplyTo.account.acct] + inReplyTo.mentions.map { $0.acct }
} else { } else {
accountsToMention = [] accountsToMention = []
@ -80,7 +84,7 @@ class ComposeViewController: UIViewController {
if let mentioningAcct = mentioningAcct { if let mentioningAcct = mentioningAcct {
accountsToMention.append(mentioningAcct) accountsToMention.append(mentioningAcct)
} }
if let ownAccount = MastodonController.account { if let ownAccount = mastodonController.account {
accountsToMention.removeAll(where: { acct in ownAccount.acct == acct }) accountsToMention.removeAll(where: { acct in ownAccount.acct == acct })
} }
accountsToMention = accountsToMention.uniques() accountsToMention = accountsToMention.uniques()
@ -127,7 +131,7 @@ class ComposeViewController: UIViewController {
statusTextView.text = accountsToMention.map({ acct in "@\(acct) " }).joined() statusTextView.text = accountsToMention.map({ acct in "@\(acct) " }).joined()
initialText = statusTextView.text initialText = statusTextView.text
MastodonController.getOwnAccount { (account) in mastodonController.getOwnAccount { (account) in
DispatchQueue.main.async { DispatchQueue.main.async {
self.selfDetailView.update(account: account) self.selfDetailView.update(account: account)
} }
@ -150,13 +154,13 @@ class ComposeViewController: UIViewController {
} }
if let inReplyToID = inReplyToID { if let inReplyToID = inReplyToID {
if let status = MastodonCache.status(for: inReplyToID) { if let status = mastodonController.cache.status(for: inReplyToID) {
updateInReplyTo(inReplyTo: status) updateInReplyTo(inReplyTo: status)
} else { } else {
let loadingVC = LoadingViewController() let loadingVC = LoadingViewController()
embedChild(loadingVC) embedChild(loadingVC)
MastodonCache.status(for: inReplyToID) { (status) in mastodonController.cache.status(for: inReplyToID) { (status) in
guard let status = status else { return } guard let status = status else { return }
DispatchQueue.main.async { DispatchQueue.main.async {
self.updateInReplyTo(inReplyTo: status) self.updateInReplyTo(inReplyTo: status)
@ -189,6 +193,7 @@ class ComposeViewController: UIViewController {
} }
let replyView = ComposeStatusReplyView.create() let replyView = ComposeStatusReplyView.create()
replyView.mastodonController = mastodonController
replyView.updateUI(for: inReplyTo) replyView.updateUI(for: inReplyTo)
stackView.insertArrangedSubview(replyView, at: 0) stackView.insertArrangedSubview(replyView, at: 0)
@ -290,7 +295,7 @@ class ComposeViewController: UIViewController {
func updateCharactersRemaining() { func updateCharactersRemaining() {
let count = CharacterCounter.count(text: statusTextView.text) let count = CharacterCounter.count(text: statusTextView.text)
let cwCount = contentWarningEnabled ? (contentWarningTextField.text?.count ?? 0) : 0 let cwCount = contentWarningEnabled ? (contentWarningTextField.text?.count ?? 0) : 0
let remaining = (MastodonController.instance.maxStatusCharacters ?? 500) - count - cwCount let remaining = (mastodonController.instance.maxStatusCharacters ?? 500) - count - cwCount
if remaining < 0 { if remaining < 0 {
charactersRemainingLabel.textColor = .red charactersRemainingLabel.textColor = .red
compositionState.formUnion(.tooManyCharacters) compositionState.formUnion(.tooManyCharacters)
@ -316,7 +321,7 @@ class ComposeViewController: UIViewController {
} }
func updateAddAttachmentButton() { func updateAddAttachmentButton() {
switch MastodonController.instance.instanceType { switch mastodonController.instance.instanceType {
case .pleroma: case .pleroma:
addAttachmentButton.isEnabled = true addAttachmentButton.isEnabled = true
case .mastodon: case .mastodon:
@ -363,10 +368,11 @@ class ComposeViewController: UIViewController {
attachments.append(.init(attachment: attachment, description: description)) attachments.append(.init(attachment: attachment, description: description))
} }
let cw = contentWarningEnabled ? contentWarningTextField.text : nil let cw = contentWarningEnabled ? contentWarningTextField.text : nil
let account = mastodonController.accountInfo!
if let currentDraft = self.currentDraft { if let currentDraft = self.currentDraft {
currentDraft.update(text: self.statusTextView.text, contentWarning: cw, attachments: attachments) currentDraft.update(accountID: account.id, text: self.statusTextView.text, contentWarning: cw, attachments: attachments)
} else { } else {
self.currentDraft = DraftsManager.shared.create(text: self.statusTextView.text, contentWarning: cw, inReplyToID: inReplyToID, attachments: attachments) self.currentDraft = DraftsManager.shared.create(accountID: account.id, text: self.statusTextView.text, contentWarning: cw, inReplyToID: inReplyToID, attachments: attachments)
} }
DraftsManager.save() DraftsManager.save()
} }
@ -451,7 +457,7 @@ class ComposeViewController: UIViewController {
} }
@objc func draftsButtonPressed() { @objc func draftsButtonPressed() {
let draftsVC = DraftsTableViewController() let draftsVC = DraftsTableViewController(account: mastodonController.accountInfo!)
draftsVC.delegate = self draftsVC.delegate = self
present(UINavigationController(rootViewController: draftsVC), animated: true) present(UINavigationController(rootViewController: draftsVC), animated: true)
} }
@ -500,8 +506,8 @@ class ComposeViewController: UIViewController {
compAttachment.getData { (data, mimeType) in compAttachment.getData { (data, mimeType) in
self.postProgressView.step() self.postProgressView.step()
let request = MastodonController.client.upload(attachment: FormAttachment(mimeType: mimeType, data: data, fileName: "file"), description: description) let request = Client.upload(attachment: FormAttachment(mimeType: mimeType, data: data, fileName: "file"), description: description)
MastodonController.client.run(request) { (response) in self.mastodonController.run(request) { (response) in
guard case let .success(attachment, _) = response else { fatalError() } guard case let .success(attachment, _) = response else { fatalError() }
attachments[index] = attachment attachments[index] = attachment
@ -519,7 +525,7 @@ class ComposeViewController: UIViewController {
group.notify(queue: .main) { group.notify(queue: .main) {
let attachments = attachments.compactMap { $0 } let attachments = attachments.compactMap { $0 }
let request = MastodonController.client.createStatus(text: text, let request = Client.createStatus(text: text,
contentType: Preferences.shared.statusContentType, contentType: Preferences.shared.statusContentType,
inReplyTo: self.inReplyToID, inReplyTo: self.inReplyToID,
media: attachments, media: attachments,
@ -527,10 +533,10 @@ class ComposeViewController: UIViewController {
spoilerText: contentWarning, spoilerText: contentWarning,
visibility: visibility, visibility: visibility,
language: nil) language: nil)
MastodonController.client.run(request) { (response) in self.mastodonController.run(request) { (response) in
guard case let .success(status, _) = response else { fatalError() } guard case let .success(status, _) = response else { fatalError() }
self.postedStatus = status self.postedStatus = status
MastodonCache.add(status: status) self.mastodonController.cache.add(status: status)
if let draft = self.currentDraft { if let draft = self.currentDraft {
DraftsManager.shared.remove(draft) DraftsManager.shared.remove(draft)
@ -540,7 +546,7 @@ class ComposeViewController: UIViewController {
self.postProgressView.step() self.postProgressView.step()
self.dismiss(animated: true) self.dismiss(animated: true)
let conversationVC = ConversationTableViewController(for: status.id) let conversationVC = ConversationTableViewController(for: status.id, mastodonController: self.mastodonController)
self.show(conversationVC, sender: self) self.show(conversationVC, sender: self)
self.xcbSession?.complete(with: .success, additionalData: [ self.xcbSession?.complete(with: .success, additionalData: [
@ -581,7 +587,7 @@ extension ComposeViewController: UITextViewDelegate {
extension ComposeViewController: AssetPickerViewControllerDelegate { extension ComposeViewController: AssetPickerViewControllerDelegate {
func assetPicker(_ assetPicker: AssetPickerViewController, shouldAllowAssetOfType type: CompositionAttachment.AttachmentType) -> Bool { func assetPicker(_ assetPicker: AssetPickerViewController, shouldAllowAssetOfType type: CompositionAttachment.AttachmentType) -> Bool {
switch MastodonController.instance.instanceType { switch mastodonController.instance.instanceType {
case .pleroma: case .pleroma:
return true return true
case .mastodon: case .mastodon:
@ -618,7 +624,6 @@ extension ComposeViewController: DraftsTableViewControllerDelegate {
func shouldSelectDraft(_ draft: DraftsManager.Draft, completion: @escaping (Bool) -> Void) { func shouldSelectDraft(_ draft: DraftsManager.Draft, completion: @escaping (Bool) -> Void) {
if draft.inReplyToID != self.inReplyToID { if draft.inReplyToID != self.inReplyToID {
// todo: better text for this
let alertController = UIAlertController(title: "Different Reply", message: "The selected draft is a reply to a different status, do you wish to use it?", preferredStyle: .alert) let alertController = UIAlertController(title: "Different Reply", message: "The selected draft is a reply to a different status, do you wish to use it?", preferredStyle: .alert)
alertController.addAction(UIAlertAction(title: "Cancel", style: .cancel, handler: { (_) in alertController.addAction(UIAlertAction(title: "Cancel", style: .cancel, handler: { (_) in
completion(false) completion(false)

View File

@ -8,7 +8,7 @@
import UIKit import UIKit
protocol DraftsTableViewControllerDelegate { protocol DraftsTableViewControllerDelegate: class {
func draftSelectionCanceled() func draftSelectionCanceled()
func shouldSelectDraft(_ draft: DraftsManager.Draft, completion: @escaping (Bool) -> Void) func shouldSelectDraft(_ draft: DraftsManager.Draft, completion: @escaping (Bool) -> Void)
func draftSelected(_ draft: DraftsManager.Draft) func draftSelected(_ draft: DraftsManager.Draft)
@ -17,9 +17,14 @@ protocol DraftsTableViewControllerDelegate {
class DraftsTableViewController: UITableViewController { class DraftsTableViewController: UITableViewController {
var delegate: DraftsTableViewControllerDelegate? let account: LocalData.UserAccountInfo
weak var delegate: DraftsTableViewControllerDelegate?
init() { var drafts = [DraftsManager.Draft]()
init(account: LocalData.UserAccountInfo) {
self.account = account
super.init(nibName: "DraftsTableViewController", bundle: nil) super.init(nibName: "DraftsTableViewController", bundle: nil)
title = "Drafts" title = "Drafts"
@ -37,10 +42,14 @@ class DraftsTableViewController: UITableViewController {
tableView.estimatedRowHeight = 140 tableView.estimatedRowHeight = 140
tableView.register(UINib(nibName: "DraftTableViewCell", bundle: nil), forCellReuseIdentifier: "draftCell") tableView.register(UINib(nibName: "DraftTableViewCell", bundle: nil), forCellReuseIdentifier: "draftCell")
drafts = DraftsManager.shared.sorted.filter { (draft) in
draft.accountID == account.id
}
} }
func draft(for indexPath: IndexPath) -> DraftsManager.Draft { func draft(for indexPath: IndexPath) -> DraftsManager.Draft {
return DraftsManager.shared.sorted[indexPath.row] return drafts[indexPath.row]
} }
// MARK: - Table View Data Source // MARK: - Table View Data Source
@ -50,7 +59,7 @@ class DraftsTableViewController: UITableViewController {
} }
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return DraftsManager.shared.drafts.count return drafts.count
} }
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {

View File

@ -15,6 +15,8 @@ class ConversationTableViewController: EnhancedTableViewController {
static let showPostsImage = UIImage(systemName: "eye.fill")! static let showPostsImage = UIImage(systemName: "eye.fill")!
static let hidePostsImage = UIImage(systemName: "eye.slash.fill")! static let hidePostsImage = UIImage(systemName: "eye.slash.fill")!
let mastodonController: MastodonController
let mainStatusID: String let mainStatusID: String
let mainStatusState: StatusState let mainStatusState: StatusState
var statuses: [(id: String, state: StatusState)] = [] { var statuses: [(id: String, state: StatusState)] = [] {
@ -28,9 +30,10 @@ class ConversationTableViewController: EnhancedTableViewController {
var showStatusesAutomatically = false var showStatusesAutomatically = false
var visibilityBarButtonItem: UIBarButtonItem! var visibilityBarButtonItem: UIBarButtonItem!
init(for mainStatusID: String, state: StatusState = .unknown) { init(for mainStatusID: String, state: StatusState = .unknown, mastodonController: MastodonController) {
self.mainStatusID = mainStatusID self.mainStatusID = mainStatusID
self.mainStatusState = state self.mainStatusState = state
self.mastodonController = mastodonController
super.init(style: .plain) super.init(style: .plain)
} }
@ -55,14 +58,14 @@ class ConversationTableViewController: EnhancedTableViewController {
statuses = [(mainStatusID, mainStatusState)] statuses = [(mainStatusID, mainStatusState)]
guard let mainStatus = MastodonCache.status(for: mainStatusID) else { fatalError("Missing cached status \(mainStatusID)") } guard let mainStatus = mastodonController.cache.status(for: mainStatusID) else { fatalError("Missing cached status \(mainStatusID)") }
let request = Status.getContext(mainStatus) let request = Status.getContext(mainStatus)
MastodonController.client.run(request) { response in mastodonController.run(request) { response in
guard case let .success(context, _) = response else { fatalError() } guard case let .success(context, _) = response else { fatalError() }
let parents = self.getDirectParents(of: mainStatus, from: context.ancestors) let parents = self.getDirectParents(of: mainStatus, from: context.ancestors)
MastodonCache.addAll(statuses: parents) self.mastodonController.cache.addAll(statuses: parents)
MastodonCache.addAll(statuses: context.descendants) self.mastodonController.cache.addAll(statuses: context.descendants)
self.statuses = parents.map { ($0.id, .unknown) } + self.statuses + context.descendants.map { ($0.id, .unknown) } self.statuses = parents.map { ($0.id, .unknown) } + self.statuses + context.descendants.map { ($0.id, .unknown) }
let indexPath = IndexPath(row: parents.count, section: 0) let indexPath = IndexPath(row: parents.count, section: 0)
DispatchQueue.main.async { DispatchQueue.main.async {
@ -101,14 +104,14 @@ class ConversationTableViewController: EnhancedTableViewController {
guard let cell = tableView.dequeueReusableCell(withIdentifier: "mainStatusCell", for: indexPath) as? ConversationMainStatusTableViewCell else { fatalError() } guard let cell = tableView.dequeueReusableCell(withIdentifier: "mainStatusCell", for: indexPath) as? ConversationMainStatusTableViewCell else { fatalError() }
cell.selectionStyle = .none cell.selectionStyle = .none
cell.showStatusAutomatically = showStatusesAutomatically cell.showStatusAutomatically = showStatusesAutomatically
cell.updateUI(statusID: id, state: state)
cell.delegate = self cell.delegate = self
cell.updateUI(statusID: id, state: state)
return cell return cell
} else { } else {
guard let cell = tableView.dequeueReusableCell(withIdentifier: "statusCell", for: indexPath) as? TimelineStatusTableViewCell else { fatalError() } guard let cell = tableView.dequeueReusableCell(withIdentifier: "statusCell", for: indexPath) as? TimelineStatusTableViewCell else { fatalError() }
cell.showStatusAutomatically = showStatusesAutomatically cell.showStatusAutomatically = showStatusesAutomatically
cell.updateUI(statusID: id, state: state)
cell.delegate = self cell.delegate = self
cell.updateUI(statusID: id, state: state)
return cell return cell
} }
} }
@ -155,6 +158,7 @@ class ConversationTableViewController: EnhancedTableViewController {
} }
extension ConversationTableViewController: StatusTableViewCellDelegate { extension ConversationTableViewController: StatusTableViewCellDelegate {
var apiController: MastodonController { mastodonController }
func statusCellCollapsedStateChanged(_ cell: BaseStatusTableViewCell) { func statusCellCollapsedStateChanged(_ cell: BaseStatusTableViewCell) {
// causes the table view to recalculate the cell heights // causes the table view to recalculate the cell heights
tableView.beginUpdates() tableView.beginUpdates()
@ -165,7 +169,7 @@ extension ConversationTableViewController: StatusTableViewCellDelegate {
extension ConversationTableViewController: UITableViewDataSourcePrefetching { extension ConversationTableViewController: UITableViewDataSourcePrefetching {
func tableView(_ tableView: UITableView, prefetchRowsAt indexPaths: [IndexPath]) { func tableView(_ tableView: UITableView, prefetchRowsAt indexPaths: [IndexPath]) {
for indexPath in indexPaths { for indexPath in indexPaths {
guard let status = MastodonCache.status(for: statuses[indexPath.row].id) else { continue } guard let status = mastodonController.cache.status(for: statuses[indexPath.row].id) else { continue }
ImageCache.avatars.get(status.account.avatar, completion: nil) ImageCache.avatars.get(status.account.avatar, completion: nil)
for attachment in status.attachments { for attachment in status.attachments {
ImageCache.attachments.get(attachment.url, completion: nil) ImageCache.attachments.get(attachment.url, completion: nil)
@ -175,7 +179,7 @@ extension ConversationTableViewController: UITableViewDataSourcePrefetching {
func tableView(_ tableView: UITableView, cancelPrefetchingForRowsAt indexPaths: [IndexPath]) { func tableView(_ tableView: UITableView, cancelPrefetchingForRowsAt indexPaths: [IndexPath]) {
for indexPath in indexPaths { for indexPath in indexPaths {
guard let status = MastodonCache.status(for: statuses[indexPath.row].id) else { continue } guard let status = mastodonController.cache.status(for: statuses[indexPath.row].id) else { continue }
ImageCache.avatars.cancel(status.account.avatar) ImageCache.avatars.cancel(status.account.avatar)
for attachment in status.attachments { for attachment in status.attachments {
ImageCache.attachments.cancel(attachment.url) ImageCache.attachments.cancel(attachment.url)

View File

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

View File

@ -12,12 +12,16 @@ import Pachyderm
class ExploreViewController: EnhancedTableViewController { class ExploreViewController: EnhancedTableViewController {
let mastodonController: MastodonController
var dataSource: DataSource! var dataSource: DataSource!
var resultsController: SearchResultsViewController! var resultsController: SearchResultsViewController!
var searchController: UISearchController! var searchController: UISearchController!
init() { init(mastodonController: MastodonController) {
self.mastodonController = mastodonController
super.init(style: .insetGrouped) super.init(style: .insetGrouped)
title = NSLocalizedString("Explore", comment: "explore tab title") title = NSLocalizedString("Explore", comment: "explore tab title")
@ -77,18 +81,20 @@ class ExploreViewController: EnhancedTableViewController {
}) })
dataSource.exploreController = self dataSource.exploreController = self
let account = mastodonController.accountInfo!
var snapshot = NSDiffableDataSourceSnapshot<Section, Item>() var snapshot = NSDiffableDataSourceSnapshot<Section, Item>()
snapshot.appendSections([.bookmarks, .lists, .savedHashtags, .savedInstances]) snapshot.appendSections([.bookmarks, .lists, .savedHashtags, .savedInstances])
snapshot.appendItems([.bookmarks], toSection: .bookmarks) snapshot.appendItems([.bookmarks], toSection: .bookmarks)
snapshot.appendItems([.addList], toSection: .lists) snapshot.appendItems([.addList], toSection: .lists)
snapshot.appendItems(SavedHashtagsManager.shared.sorted.map { .savedHashtag($0) } + [.addSavedHashtag], toSection: .savedHashtags) snapshot.appendItems(SavedDataManager.shared.sortedHashtags(for: account).map { .savedHashtag($0) } + [.addSavedHashtag], toSection: .savedHashtags)
snapshot.appendItems(SavedInstanceManager.shared.savedInstances.map { .savedInstance($0) } + [.findInstance], toSection: .savedInstances) snapshot.appendItems(SavedDataManager.shared.savedInstances(for: account).map { .savedInstance($0) } + [.findInstance], toSection: .savedInstances)
// the initial, static items should not be displayed with an animation // the initial, static items should not be displayed with an animation
UIView.performWithoutAnimation { UIView.performWithoutAnimation {
dataSource.apply(snapshot) dataSource.apply(snapshot)
} }
resultsController = SearchResultsViewController() resultsController = SearchResultsViewController(mastodonController: mastodonController)
resultsController.exploreNavigationController = self.navigationController! resultsController.exploreNavigationController = self.navigationController!
searchController = UISearchController(searchResultsController: resultsController) searchController = UISearchController(searchResultsController: resultsController)
searchController.searchResultsUpdater = resultsController searchController.searchResultsUpdater = resultsController
@ -106,8 +112,8 @@ class ExploreViewController: EnhancedTableViewController {
} }
func reloadLists() { func reloadLists() {
let request = MastodonController.client.getLists() let request = Client.getLists()
MastodonController.client.run(request) { (response) in mastodonController.run(request) { (response) in
guard case let .success(lists, _) = response else { guard case let .success(lists, _) = response else {
fatalError() fatalError()
} }
@ -123,16 +129,18 @@ class ExploreViewController: EnhancedTableViewController {
} }
@objc func savedHashtagsChanged() { @objc func savedHashtagsChanged() {
let account = mastodonController.accountInfo!
var snapshot = dataSource.snapshot() var snapshot = dataSource.snapshot()
snapshot.deleteItems(snapshot.itemIdentifiers(inSection: .savedHashtags)) snapshot.deleteItems(snapshot.itemIdentifiers(inSection: .savedHashtags))
snapshot.appendItems(SavedHashtagsManager.shared.sorted.map { .savedHashtag($0) } + [.addSavedHashtag], toSection: .savedHashtags) snapshot.appendItems(SavedDataManager.shared.sortedHashtags(for: account).map { .savedHashtag($0) } + [.addSavedHashtag], toSection: .savedHashtags)
dataSource.apply(snapshot) dataSource.apply(snapshot)
} }
@objc func savedInstancesChanged() { @objc func savedInstancesChanged() {
let account = mastodonController.accountInfo!
var snapshot = dataSource.snapshot() var snapshot = dataSource.snapshot()
snapshot.deleteItems(snapshot.itemIdentifiers(inSection: .savedInstances)) snapshot.deleteItems(snapshot.itemIdentifiers(inSection: .savedInstances))
snapshot.appendItems(SavedInstanceManager.shared.savedInstances.map { .savedInstance($0) } + [.findInstance], toSection: .savedInstances) snapshot.appendItems(SavedDataManager.shared.savedInstances(for: account).map { .savedInstance($0) } + [.findInstance], toSection: .savedInstances)
dataSource.apply(snapshot) dataSource.apply(snapshot)
} }
@ -143,7 +151,7 @@ class ExploreViewController: EnhancedTableViewController {
alert.addAction(UIAlertAction(title: NSLocalizedString("Delete List", comment: "delete list alert confirm button"), style: .destructive, handler: { (_) in alert.addAction(UIAlertAction(title: NSLocalizedString("Delete List", comment: "delete list alert confirm button"), style: .destructive, handler: { (_) in
let request = List.delete(list) let request = List.delete(list)
MastodonController.client.run(request) { (response) in self.mastodonController.run(request) { (response) in
guard case .success(_, _) = response else { guard case .success(_, _) = response else {
fatalError() fatalError()
} }
@ -159,11 +167,13 @@ class ExploreViewController: EnhancedTableViewController {
} }
func removeSavedHashtag(_ hashtag: Hashtag) { func removeSavedHashtag(_ hashtag: Hashtag) {
SavedHashtagsManager.shared.remove(hashtag) let account = mastodonController.accountInfo!
SavedDataManager.shared.remove(hashtag: hashtag, for: account)
} }
func removeSavedInstance(_ instanceURL: URL) { func removeSavedInstance(_ instanceURL: URL) {
SavedInstanceManager.shared.remove(instanceURL) let account = mastodonController.accountInfo!
SavedDataManager.shared.remove(instance: instanceURL, for: account)
} }
// MARK: - Table view delegate // MARK: - Table view delegate
@ -174,10 +184,10 @@ class ExploreViewController: EnhancedTableViewController {
return return
case .bookmarks: case .bookmarks:
show(BookmarksTableViewController(), sender: nil) show(BookmarksTableViewController(mastodonController: mastodonController), sender: nil)
case let .list(list): case let .list(list):
show(ListTimelineViewController(for: list), sender: nil) show(ListTimelineViewController(for: list, mastodonController: mastodonController), sender: nil)
case .addList: case .addList:
tableView.selectRow(at: nil, animated: true, scrollPosition: .none) tableView.selectRow(at: nil, animated: true, scrollPosition: .none)
@ -189,14 +199,14 @@ class ExploreViewController: EnhancedTableViewController {
fatalError() fatalError()
} }
let request = MastodonController.client.createList(title: title) let request = Client.createList(title: title)
MastodonController.client.run(request) { (response) in self.mastodonController.run(request) { (response) in
guard case let .success(list, _) = response else { fatalError() } guard case let .success(list, _) = response else { fatalError() }
self.reloadLists() self.reloadLists()
DispatchQueue.main.async { DispatchQueue.main.async {
let listTimelineController = ListTimelineViewController(for: list) let listTimelineController = ListTimelineViewController(for: list, mastodonController: self.mastodonController)
listTimelineController.presentEditOnAppear = true listTimelineController.presentEditOnAppear = true
self.show(listTimelineController, sender: nil) self.show(listTimelineController, sender: nil)
} }
@ -205,19 +215,19 @@ class ExploreViewController: EnhancedTableViewController {
present(alert, animated: true) present(alert, animated: true)
case let .savedHashtag(hashtag): case let .savedHashtag(hashtag):
show(HashtagTimelineViewController(for: hashtag), sender: nil) show(HashtagTimelineViewController(for: hashtag, mastodonController: mastodonController), sender: nil)
case .addSavedHashtag: case .addSavedHashtag:
tableView.selectRow(at: nil, animated: true, scrollPosition: .none) tableView.selectRow(at: nil, animated: true, scrollPosition: .none)
let navController = UINavigationController(rootViewController: AddSavedHashtagViewController()) let navController = UINavigationController(rootViewController: AddSavedHashtagViewController(mastodonController: mastodonController))
present(navController, animated: true) present(navController, animated: true)
case let .savedInstance(url): case let .savedInstance(url):
show(InstanceTimelineViewController(for: url), sender: nil) show(InstanceTimelineViewController(for: url, parentMastodonController: mastodonController), sender: nil)
case .findInstance: case .findInstance:
tableView.selectRow(at: nil, animated: true, scrollPosition: .none) tableView.selectRow(at: nil, animated: true, scrollPosition: .none)
let findController = FindInstanceViewController() let findController = FindInstanceViewController(parentMastodonController: mastodonController)
findController.instanceTimelineDelegate = self findController.instanceTimelineDelegate = self
let navController = UINavigationController(rootViewController: findController) let navController = UINavigationController(rootViewController: findController)
present(navController, animated: true) present(navController, animated: true)
@ -344,7 +354,7 @@ extension ExploreViewController {
extension ExploreViewController: InstanceTimelineViewControllerDelegate { extension ExploreViewController: InstanceTimelineViewControllerDelegate {
func didSaveInstance(url: URL) { func didSaveInstance(url: URL) {
dismiss(animated: true) { dismiss(animated: true) {
self.show(InstanceTimelineViewController(for: url), sender: nil) self.show(InstanceTimelineViewController(for: url, parentMastodonController: self.mastodonController), sender: nil)
} }
} }

View File

@ -10,8 +10,20 @@ import UIKit
class FindInstanceViewController: InstanceSelectorTableViewController { class FindInstanceViewController: InstanceSelectorTableViewController {
weak var parentMastodonController: MastodonController?
var instanceTimelineDelegate: InstanceTimelineViewControllerDelegate? var instanceTimelineDelegate: InstanceTimelineViewControllerDelegate?
init(parentMastodonController: MastodonController) {
self.parentMastodonController = parentMastodonController
super.init()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func viewDidLoad() { override func viewDidLoad() {
super.viewDidLoad() super.viewDidLoad()
@ -32,7 +44,7 @@ class FindInstanceViewController: InstanceSelectorTableViewController {
extension FindInstanceViewController: InstanceSelectorTableViewControllerDelegate { extension FindInstanceViewController: InstanceSelectorTableViewControllerDelegate {
func didSelectInstance(url: URL) { func didSelectInstance(url: URL) {
let instanceTimelineController = InstanceTimelineViewController(for: url) let instanceTimelineController = InstanceTimelineViewController(for: url, parentMastodonController: parentMastodonController!)
instanceTimelineController.delegate = instanceTimelineDelegate instanceTimelineController.delegate = instanceTimelineDelegate
show(instanceTimelineController, sender: self) show(instanceTimelineController, sender: self)
} }

View File

@ -11,6 +11,8 @@ import Pachyderm
class EditListAccountsViewController: EnhancedTableViewController { class EditListAccountsViewController: EnhancedTableViewController {
let mastodonController: MastodonController
let list: List let list: List
var dataSource: DataSource! var dataSource: DataSource!
@ -20,8 +22,9 @@ class EditListAccountsViewController: EnhancedTableViewController {
var searchResultsController: SearchResultsViewController! var searchResultsController: SearchResultsViewController!
var searchController: UISearchController! var searchController: UISearchController!
init(list: List) { init(list: List, mastodonController: MastodonController) {
self.list = list self.list = list
self.mastodonController = mastodonController
super.init(style: .plain) super.init(style: .plain)
@ -49,7 +52,7 @@ class EditListAccountsViewController: EnhancedTableViewController {
}) })
dataSource.editListAccountsController = self dataSource.editListAccountsController = self
searchResultsController = SearchResultsViewController() searchResultsController = SearchResultsViewController(mastodonController: mastodonController)
searchResultsController.delegate = self searchResultsController.delegate = self
searchResultsController.onlySections = [.accounts] searchResultsController.onlySections = [.accounts]
searchController = UISearchController(searchResultsController: searchResultsController) searchController = UISearchController(searchResultsController: searchResultsController)
@ -70,14 +73,14 @@ class EditListAccountsViewController: EnhancedTableViewController {
func loadAccounts() { func loadAccounts() {
let request = List.getAccounts(list) let request = List.getAccounts(list)
MastodonController.client.run(request) { (response) in mastodonController.run(request) { (response) in
guard case let .success(accounts, pagination) = response else { guard case let .success(accounts, pagination) = response else {
fatalError() fatalError()
} }
self.nextRange = pagination?.older self.nextRange = pagination?.older
MastodonCache.addAll(accounts: accounts) self.mastodonController.cache.addAll(accounts: accounts)
var snapshot = self.dataSource.snapshot() var snapshot = self.dataSource.snapshot()
snapshot.deleteSections([.accounts]) snapshot.deleteSections([.accounts])
@ -109,7 +112,7 @@ class EditListAccountsViewController: EnhancedTableViewController {
fatalError() fatalError()
} }
let request = List.update(self.list, title: text) let request = List.update(self.list, title: text)
MastodonController.client.run(request) { (response) in self.mastodonController.run(request) { (response) in
guard case .success(_, _) = response else { guard case .success(_, _) = response else {
fatalError() fatalError()
} }
@ -143,7 +146,7 @@ extension EditListAccountsViewController {
} }
let request = List.remove(editListAccountsController!.list, accounts: [id]) let request = List.remove(editListAccountsController!.list, accounts: [id])
MastodonController.client.run(request) { (response) in editListAccountsController!.mastodonController.run(request) { (response) in
guard case .success(_, _) = response else { guard case .success(_, _) = response else {
fatalError() fatalError()
} }
@ -157,7 +160,7 @@ extension EditListAccountsViewController {
extension EditListAccountsViewController: SearchResultsViewControllerDelegate { extension EditListAccountsViewController: SearchResultsViewControllerDelegate {
func selectedSearchResult(account accountID: String) { func selectedSearchResult(account accountID: String) {
let request = List.add(list, accounts: [accountID]) let request = List.add(list, accounts: [accountID])
MastodonController.client.run(request) { (response) in mastodonController.run(request) { (response) in
guard case .success(_, _) = response else { guard case .success(_, _) = response else {
fatalError() fatalError()
} }

View File

@ -15,10 +15,10 @@ class ListTimelineViewController: TimelineTableViewController {
var presentEditOnAppear = false var presentEditOnAppear = false
init(for list: List) { init(for list: List, mastodonController: MastodonController) {
self.list = list self.list = list
super.init(for: .list(id: list.id)) super.init(for: .list(id: list.id), mastodonController: mastodonController)
title = list.title title = list.title
} }
@ -42,7 +42,7 @@ class ListTimelineViewController: TimelineTableViewController {
} }
func presentEdit(animated: Bool) { func presentEdit(animated: Bool) {
let editListAccountsController = EditListAccountsViewController(list: list) let editListAccountsController = EditListAccountsViewController(list: list, mastodonController: mastodonController)
editListAccountsController.navigationItem.leftBarButtonItem = UIBarButtonItem(barButtonSystemItem: .done, target: self, action: #selector(doneButtonPressed)) editListAccountsController.navigationItem.leftBarButtonItem = UIBarButtonItem(barButtonSystemItem: .done, target: self, action: #selector(doneButtonPressed))
let navController = UINavigationController(rootViewController: editListAccountsController) let navController = UINavigationController(rootViewController: editListAccountsController)
present(navController, animated: animated) present(navController, animated: animated)

View File

@ -9,6 +9,8 @@
import UIKit import UIKit
class MainTabBarViewController: UITabBarController, UITabBarControllerDelegate { class MainTabBarViewController: UITabBarController, UITabBarControllerDelegate {
weak var mastodonController: MastodonController!
override var supportedInterfaceOrientations: UIInterfaceOrientationMask { override var supportedInterfaceOrientations: UIInterfaceOrientationMask {
if UIDevice.current.userInterfaceIdiom == .phone { if UIDevice.current.userInterfaceIdiom == .phone {
@ -18,17 +20,27 @@ class MainTabBarViewController: UITabBarController, UITabBarControllerDelegate {
} }
} }
init(mastodonController: MastodonController) {
self.mastodonController = mastodonController
super.init(nibName: nil, bundle: nil)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func viewDidLoad() { override func viewDidLoad() {
super.viewDidLoad() super.viewDidLoad()
self.delegate = self self.delegate = self
viewControllers = [ viewControllers = [
embedInNavigationController(TimelinesPageViewController()), embedInNavigationController(TimelinesPageViewController(mastodonController: mastodonController)),
embedInNavigationController(NotificationsPageViewController()), embedInNavigationController(NotificationsPageViewController(mastodonController: mastodonController)),
ComposeViewController(), ComposeViewController(mastodonController: mastodonController),
embedInNavigationController(ExploreViewController()), embedInNavigationController(ExploreViewController(mastodonController: mastodonController)),
embedInNavigationController(MyProfileTableViewController()), embedInNavigationController(MyProfileTableViewController(mastodonController: mastodonController)),
] ]
} }
@ -49,7 +61,7 @@ class MainTabBarViewController: UITabBarController, UITabBarControllerDelegate {
} }
func presentCompose() { func presentCompose() {
let compose = ComposeViewController() let compose = ComposeViewController(mastodonController: mastodonController)
let navigationController = embedInNavigationController(compose) let navigationController = embedInNavigationController(compose)
navigationController.presentationController?.delegate = compose navigationController.presentationController?.delegate = compose
present(navigationController, animated: true) present(navigationController, animated: true)

View File

@ -13,13 +13,17 @@ class NotificationsPageViewController: SegmentedPageViewController {
private let notificationsTitle = NSLocalizedString("Notifications", comment: "notifications tab title") private let notificationsTitle = NSLocalizedString("Notifications", comment: "notifications tab title")
private let mentionsTitle = NSLocalizedString("Mentions", comment: "mentions tab title") private let mentionsTitle = NSLocalizedString("Mentions", comment: "mentions tab title")
weak var mastodonController: MastodonController!
init() { init(mastodonController: MastodonController) {
let notifications = NotificationsTableViewController(allowedTypes: Pachyderm.Notification.Kind.allCases) self.mastodonController = mastodonController
let notifications = NotificationsTableViewController(allowedTypes: Pachyderm.Notification.Kind.allCases, mastodonController: mastodonController)
notifications.title = notificationsTitle notifications.title = notificationsTitle
notifications.userActivity = UserActivityManager.checkNotificationsActivity() notifications.userActivity = UserActivityManager.checkNotificationsActivity()
let mentions = NotificationsTableViewController(allowedTypes: [.mention]) let mentions = NotificationsTableViewController(allowedTypes: [.mention], mastodonController: mastodonController)
mentions.title = mentionsTitle mentions.title = mentionsTitle
mentions.userActivity = UserActivityManager.checkMentionsActivity() mentions.userActivity = UserActivityManager.checkMentionsActivity()

View File

@ -16,6 +16,8 @@ class NotificationsTableViewController: EnhancedTableViewController {
private let followGroupCell = "followGroupCell" private let followGroupCell = "followGroupCell"
private let followRequestCell = "followRequestCell" private let followRequestCell = "followRequestCell"
let mastodonController: MastodonController
let excludedTypes: [Pachyderm.Notification.Kind] let excludedTypes: [Pachyderm.Notification.Kind]
let groupTypes = [Notification.Kind.favourite, .reblog, .follow] let groupTypes = [Notification.Kind.favourite, .reblog, .follow]
@ -30,8 +32,9 @@ class NotificationsTableViewController: EnhancedTableViewController {
var newer: RequestRange? var newer: RequestRange?
var older: RequestRange? var older: RequestRange?
init(allowedTypes: [Pachyderm.Notification.Kind]) { init(allowedTypes: [Pachyderm.Notification.Kind], mastodonController: MastodonController) {
self.excludedTypes = Array(Set(Pachyderm.Notification.Kind.allCases).subtracting(allowedTypes)) self.excludedTypes = Array(Set(Pachyderm.Notification.Kind.allCases).subtracting(allowedTypes))
self.mastodonController = mastodonController
super.init(style: .plain) super.init(style: .plain)
@ -56,17 +59,17 @@ class NotificationsTableViewController: EnhancedTableViewController {
tableView.prefetchDataSource = self tableView.prefetchDataSource = self
let request = MastodonController.client.getNotifications(excludeTypes: excludedTypes) let request = Client.getNotifications(excludeTypes: excludedTypes)
MastodonController.client.run(request) { result in mastodonController.run(request) { result in
guard case let .success(notifications, pagination) = result else { fatalError() } guard case let .success(notifications, pagination) = result else { fatalError() }
let groups = NotificationGroup.createGroups(notifications: notifications, only: self.groupTypes) let groups = NotificationGroup.createGroups(notifications: notifications, only: self.groupTypes)
self.groups.append(contentsOf: groups) self.groups.append(contentsOf: groups)
MastodonCache.addAll(notifications: notifications) self.mastodonController.cache.addAll(notifications: notifications)
MastodonCache.addAll(statuses: notifications.compactMap { $0.status }) self.mastodonController.cache.addAll(statuses: notifications.compactMap { $0.status })
MastodonCache.addAll(accounts: notifications.map { $0.account }) self.mastodonController.cache.addAll(accounts: notifications.map { $0.account })
self.newer = pagination?.newer self.newer = pagination?.newer
self.older = pagination?.older self.older = pagination?.older
@ -89,31 +92,31 @@ class NotificationsTableViewController: EnhancedTableViewController {
switch group.kind { switch group.kind {
case .mention: case .mention:
guard let notification = MastodonCache.notification(for: group.notificationIDs.first!), guard let notification = mastodonController.cache.notification(for: group.notificationIDs.first!),
let cell = tableView.dequeueReusableCell(withIdentifier: statusCell, for: indexPath) as? TimelineStatusTableViewCell else { let cell = tableView.dequeueReusableCell(withIdentifier: statusCell, for: indexPath) as? TimelineStatusTableViewCell else {
fatalError() fatalError()
} }
cell.updateUI(statusID: notification.status!.id, state: group.statusState!)
cell.delegate = self cell.delegate = self
cell.updateUI(statusID: notification.status!.id, state: group.statusState!)
return cell return cell
case .favourite, .reblog: case .favourite, .reblog:
guard let cell = tableView.dequeueReusableCell(withIdentifier: actionGroupCell, for: indexPath) as? ActionNotificationGroupTableViewCell else { fatalError() } guard let cell = tableView.dequeueReusableCell(withIdentifier: actionGroupCell, for: indexPath) as? ActionNotificationGroupTableViewCell else { fatalError() }
cell.updateUI(group: group)
cell.delegate = self cell.delegate = self
cell.updateUI(group: group)
return cell return cell
case .follow: case .follow:
guard let cell = tableView.dequeueReusableCell(withIdentifier: followGroupCell, for: indexPath) as? FollowNotificationGroupTableViewCell else { fatalError() } guard let cell = tableView.dequeueReusableCell(withIdentifier: followGroupCell, for: indexPath) as? FollowNotificationGroupTableViewCell else { fatalError() }
cell.updateUI(group: group)
cell.delegate = self cell.delegate = self
cell.updateUI(group: group)
return cell return cell
case .followRequest: case .followRequest:
guard let notification = MastodonCache.notification(for: group.notificationIDs.first!), guard let notification = mastodonController.cache.notification(for: group.notificationIDs.first!),
let cell = tableView.dequeueReusableCell(withIdentifier: followRequestCell, for: indexPath) as? FollowRequestNotificationTableViewCell else { fatalError() } let cell = tableView.dequeueReusableCell(withIdentifier: followRequestCell, for: indexPath) as? FollowRequestNotificationTableViewCell else { fatalError() }
cell.updateUI(notification: notification)
cell.delegate = self cell.delegate = self
cell.updateUI(notification: notification)
return cell return cell
} }
} }
@ -124,17 +127,17 @@ class NotificationsTableViewController: EnhancedTableViewController {
if indexPath.row == groups.count - 1 { if indexPath.row == groups.count - 1 {
guard let older = older else { return } guard let older = older else { return }
let request = MastodonController.client.getNotifications(excludeTypes: excludedTypes, range: older) let request = Client.getNotifications(excludeTypes: excludedTypes, range: older)
MastodonController.client.run(request) { result in mastodonController.run(request) { result in
guard case let .success(newNotifications, pagination) = result else { fatalError() } guard case let .success(newNotifications, pagination) = result else { fatalError() }
let groups = NotificationGroup.createGroups(notifications: newNotifications, only: self.groupTypes) let groups = NotificationGroup.createGroups(notifications: newNotifications, only: self.groupTypes)
self.groups.append(contentsOf: groups) self.groups.append(contentsOf: groups)
MastodonCache.addAll(notifications: newNotifications) self.mastodonController.cache.addAll(notifications: newNotifications)
MastodonCache.addAll(statuses: newNotifications.compactMap { $0.status }) self.mastodonController.cache.addAll(statuses: newNotifications.compactMap { $0.status })
MastodonCache.addAll(accounts: newNotifications.map { $0.account }) self.mastodonController.cache.addAll(accounts: newNotifications.map { $0.account })
self.older = pagination?.older self.older = pagination?.older
} }
@ -182,7 +185,7 @@ class NotificationsTableViewController: EnhancedTableViewController {
.map(Pachyderm.Notification.dismiss(id:)) .map(Pachyderm.Notification.dismiss(id:))
.forEach { (request) in .forEach { (request) in
group.enter() group.enter()
MastodonController.client.run(request) { (response) in mastodonController.run(request) { (response) in
group.leave() group.leave()
} }
} }
@ -196,17 +199,17 @@ class NotificationsTableViewController: EnhancedTableViewController {
@objc func refreshNotifications(_ sender: Any) { @objc func refreshNotifications(_ sender: Any) {
guard let newer = newer else { return } guard let newer = newer else { return }
let request = MastodonController.client.getNotifications(excludeTypes: excludedTypes, range: newer) let request = Client.getNotifications(excludeTypes: excludedTypes, range: newer)
MastodonController.client.run(request) { result in mastodonController.run(request) { result in
guard case let .success(newNotifications, pagination) = result else { fatalError() } guard case let .success(newNotifications, pagination) = result else { fatalError() }
let groups = NotificationGroup.createGroups(notifications: newNotifications, only: self.groupTypes) let groups = NotificationGroup.createGroups(notifications: newNotifications, only: self.groupTypes)
self.groups.insert(contentsOf: groups, at: 0) self.groups.insert(contentsOf: groups, at: 0)
MastodonCache.addAll(notifications: newNotifications) self.mastodonController.cache.addAll(notifications: newNotifications)
MastodonCache.addAll(statuses: newNotifications.compactMap { $0.status }) self.mastodonController.cache.addAll(statuses: newNotifications.compactMap { $0.status })
MastodonCache.addAll(accounts: newNotifications.map { $0.account }) self.mastodonController.cache.addAll(accounts: newNotifications.map { $0.account })
if let newer = pagination?.newer { if let newer = pagination?.newer {
self.newer = newer self.newer = newer
@ -224,6 +227,7 @@ class NotificationsTableViewController: EnhancedTableViewController {
} }
extension NotificationsTableViewController: StatusTableViewCellDelegate { extension NotificationsTableViewController: StatusTableViewCellDelegate {
var apiController: MastodonController { mastodonController }
func statusCellCollapsedStateChanged(_ cell: BaseStatusTableViewCell) { func statusCellCollapsedStateChanged(_ cell: BaseStatusTableViewCell) {
// causes the table view to recalculate the cell heights // causes the table view to recalculate the cell heights
tableView.beginUpdates() tableView.beginUpdates()
@ -235,7 +239,7 @@ extension NotificationsTableViewController: UITableViewDataSourcePrefetching {
func tableView(_ tableView: UITableView, prefetchRowsAt indexPaths: [IndexPath]) { func tableView(_ tableView: UITableView, prefetchRowsAt indexPaths: [IndexPath]) {
for indexPath in indexPaths { for indexPath in indexPaths {
for notificationID in groups[indexPath.row].notificationIDs { for notificationID in groups[indexPath.row].notificationIDs {
guard let notification = MastodonCache.notification(for: notificationID) else { continue } guard let notification = mastodonController.cache.notification(for: notificationID) else { continue }
ImageCache.avatars.get(notification.account.avatar, completion: nil) ImageCache.avatars.get(notification.account.avatar, completion: nil)
} }
} }
@ -244,7 +248,7 @@ extension NotificationsTableViewController: UITableViewDataSourcePrefetching {
func tableView(_ tableView: UITableView, cancelPrefetchingForRowsAt indexPaths: [IndexPath]) { func tableView(_ tableView: UITableView, cancelPrefetchingForRowsAt indexPaths: [IndexPath]) {
for indexPath in indexPaths { for indexPath in indexPaths {
for notificationID in groups[indexPath.row].notificationIDs { for notificationID in groups[indexPath.row].notificationIDs {
guard let notification = MastodonCache.notification(for: notificationID) else { continue } guard let notification = mastodonController.cache.notification(for: notificationID) else { continue }
ImageCache.avatars.cancel(notification.account.avatar) ImageCache.avatars.cancel(notification.account.avatar)
} }
} }

View File

@ -10,7 +10,7 @@ import UIKit
import Combine import Combine
import Pachyderm import Pachyderm
protocol InstanceSelectorTableViewControllerDelegate { protocol InstanceSelectorTableViewControllerDelegate: class {
func didSelectInstance(url: URL) func didSelectInstance(url: URL)
} }
@ -18,7 +18,7 @@ fileprivate let instanceCell = "instanceCell"
class InstanceSelectorTableViewController: UITableViewController { class InstanceSelectorTableViewController: UITableViewController {
var delegate: InstanceSelectorTableViewControllerDelegate? weak var delegate: InstanceSelectorTableViewControllerDelegate?
var dataSource: DataSource! var dataSource: DataSource!
var searchController: UISearchController! var searchController: UISearchController!
@ -115,7 +115,7 @@ class InstanceSelectorTableViewController: UITableViewController {
let components = parseURLComponents(input: domain) let components = parseURLComponents(input: domain)
let client = Client(baseURL: components.url!) let client = Client(baseURL: components.url!)
let request = client.getInstance() let request = Client.getInstance()
client.run(request) { (response) in client.run(request) { (response) in
var snapshot = self.dataSource.snapshot() var snapshot = self.dataSource.snapshot()
snapshot.deleteItems(snapshot.itemIdentifiers(inSection: .selected)) snapshot.deleteItems(snapshot.itemIdentifiers(inSection: .selected))

View File

@ -10,7 +10,7 @@ import UIKit
import AuthenticationServices import AuthenticationServices
protocol OnboardingViewControllerDelegate { protocol OnboardingViewControllerDelegate {
func didFinishOnboarding() func didFinishOnboarding(account: LocalData.UserAccountInfo)
} }
class OnboardingViewController: UINavigationController { class OnboardingViewController: UINavigationController {
@ -44,15 +44,13 @@ class OnboardingViewController: UINavigationController {
} }
extension OnboardingViewController: InstanceSelectorTableViewControllerDelegate { extension OnboardingViewController: InstanceSelectorTableViewControllerDelegate {
func didSelectInstance(url: URL) { func didSelectInstance(url instanceURL: URL) {
LocalData.shared.instanceURL = url let mastodonController = MastodonController(instanceURL: instanceURL)
MastodonController.createClient() mastodonController.registerApp { (clientID, clientSecret) in
MastodonController.registerApp {
let clientID = LocalData.shared.clientID!
let callbackURL = "tusker://oauth" let callbackURL = "tusker://oauth"
var components = URLComponents(url: url, resolvingAgainstBaseURL: false)! var components = URLComponents(url: instanceURL, resolvingAgainstBaseURL: false)!
components.path = "/oauth/authorize" components.path = "/oauth/authorize"
components.queryItems = [ components.queryItems = [
URLQueryItem(name: "client_id", value: clientID), URLQueryItem(name: "client_id", value: clientID),
@ -69,9 +67,13 @@ extension OnboardingViewController: InstanceSelectorTableViewControllerDelegate
let item = components.queryItems?.first(where: { $0.name == "code" }), let item = components.queryItems?.first(where: { $0.name == "code" }),
let authCode = item.value else { return } let authCode = item.value else { return }
MastodonController.authorize(authorizationCode: authCode) { mastodonController.authorize(authorizationCode: authCode) { (accessToken) in
DispatchQueue.main.async { mastodonController.getOwnAccount { (account) in
self.onboardingDelegate?.didFinishOnboarding() DispatchQueue.main.async {
let accountInfo = LocalData.shared.addAccount(instanceURL: instanceURL, clientID: clientID, clientSecret: clientSecret, username: account.username, accessToken: accessToken)
self.onboardingDelegate?.didFinishOnboarding(account: accountInfo)
}
} }
} }
} }

View File

@ -10,9 +10,12 @@ import UIKit
import SwiftUI import SwiftUI
class PreferencesNavigationController: UINavigationController { class PreferencesNavigationController: UINavigationController {
private var isSwitchingAccounts = false
init() { init(mastodonController: MastodonController) {
let hostingController = UIHostingController(rootView: PreferencesView()) let view = PreferencesView()
let hostingController = UIHostingController(rootView: view)
super.init(rootViewController: hostingController) super.init(rootViewController: hostingController)
hostingController.navigationItem.rightBarButtonItem = UIBarButtonItem(barButtonSystemItem: .done, target: self, action: #selector(donePressed)) hostingController.navigationItem.rightBarButtonItem = UIBarButtonItem(barButtonSystemItem: .done, target: self, action: #selector(donePressed))
} }
@ -20,16 +23,67 @@ class PreferencesNavigationController: UINavigationController {
required init?(coder aDecoder: NSCoder) { required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented") fatalError("init(coder:) has not been implemented")
} }
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
NotificationCenter.default.addObserver(self, selector: #selector(showAddAccount), name: .addAccount, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(activateAccount(_:)), name: .activateAccount, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(userLoggedOut), name: .userLoggedOut, object: nil)
}
override func viewWillDisappear(_ animated: Bool) { override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated) super.viewWillDisappear(animated)
// workaround for onDisappear not being called when a modally presented UIHostingController is dismissed if !isSwitchingAccounts {
NotificationCenter.default.post(name: .preferencesChanged, object: nil) // workaround for onDisappear not being called when a modally presented UIHostingController is dismissed
NotificationCenter.default.post(name: .preferencesChanged, object: nil)
}
} }
@objc func donePressed() { @objc func donePressed() {
dismiss(animated: true) dismiss(animated: true)
} }
@objc func showAddAccount() {
let onboardingController = OnboardingViewController()
onboardingController.onboardingDelegate = self
onboardingController.instanceSelector.navigationItem.leftBarButtonItem = UIBarButtonItem(barButtonSystemItem: .cancel, target: self, action: #selector(cancelAddAccount))
show(onboardingController, sender: self)
}
@objc func cancelAddAccount() {
dismiss(animated: true) // dismisses instance selector
}
@objc func activateAccount(_ notification: Notification) {
let account = notification.userInfo!["account"] as! LocalData.UserAccountInfo
let sceneDelegate = self.view.window!.windowScene!.delegate as! SceneDelegate
isSwitchingAccounts = true
dismiss(animated: true) { // dismiss preferences
sceneDelegate.activateAccount(account)
}
}
@objc func userLoggedOut() {
let sceneDelegate = self.view.window!.windowScene!.delegate as! SceneDelegate
isSwitchingAccounts = true
dismiss(animated: true) { // dismiss preferences
sceneDelegate.logoutCurrent()
}
}
} }
extension PreferencesNavigationController: OnboardingViewControllerDelegate {
func didFinishOnboarding(account: LocalData.UserAccountInfo) {
DispatchQueue.main.async {
let sceneDelegate = self.view.window!.windowScene!.delegate as! SceneDelegate
self.dismiss(animated: true) { // dismiss instance selector
self.dismiss(animated: true) { // dismiss preferences
sceneDelegate.activateAccount(account)
}
}
}
}
}

View File

@ -7,20 +7,44 @@
import SwiftUI import SwiftUI
struct PreferencesView : View { struct PreferencesView: View {
@ObservedObject var localData = LocalData.shared
@State private var showingLogoutConfirmation = false @State private var showingLogoutConfirmation = false
var body: some View { var body: some View {
// workaround: the navigation view is provided by MyProfileTableViewController so that it can inject the Done button // workaround: the navigation view is provided by MyProfileTableViewController so that it can inject the Done button
// NavigationView { // NavigationView {
List { List {
Section { Section {
ForEach(localData.accounts, id: \.accessToken) { (account) in
Button(action: {
NotificationCenter.default.post(name: .activateAccount, object: nil, userInfo: ["account": account])
}) {
HStack {
Text(account.username)
.foregroundColor(.primary)
Spacer()
if account == self.localData.getMostRecentAccount() {
Image(systemName: "checkmark")
.renderingMode(.template)
.foregroundColor(.secondary)
}
}
}
}
Button(action: { Button(action: {
self.showingLogoutConfirmation = true NotificationCenter.default.post(name: .addAccount, object: nil)
}) { }) {
Text("Logout") Text("Add Account...")
}.alert(isPresented: $showingLogoutConfirmation) { }
Alert(title: Text("Are you sure you want to logout?"), message: nil, primaryButton: .destructive(Text("Logout"), action: self.logoutPressed), secondaryButton: .cancel()) if localData.getMostRecentAccount() != nil {
Button(action: {
self.showingLogoutConfirmation = true
}) {
Text("Logout from current")
}.alert(isPresented: $showingLogoutConfirmation) {
Alert(title: Text("Are you sure you want to logout?"), message: nil, primaryButton: .destructive(Text("Logout"), action: self.logoutPressed), secondaryButton: .cancel())
}
} }
} }
@ -49,11 +73,6 @@ struct PreferencesView : View {
} }
func logoutPressed() { func logoutPressed() {
LocalData.shared.onboardingComplete = false
LocalData.shared.instanceURL = nil
LocalData.shared.clientID = nil
LocalData.shared.clientSecret = nil
LocalData.shared.accessToken = nil
NotificationCenter.default.post(name: .userLoggedOut, object: nil) NotificationCenter.default.post(name: .userLoggedOut, object: nil)
} }
} }
@ -61,7 +80,7 @@ struct PreferencesView : View {
#if DEBUG #if DEBUG
struct PreferencesView_Previews : PreviewProvider { struct PreferencesView_Previews : PreviewProvider {
static var previews: some View { static var previews: some View {
PreferencesView() return PreferencesView()
} }
} }
#endif #endif

View File

@ -11,14 +11,14 @@ import SwiftUI
class MyProfileTableViewController: ProfileTableViewController { class MyProfileTableViewController: ProfileTableViewController {
init() { init(mastodonController: MastodonController) {
super.init(accountID: nil) super.init(accountID: nil, mastodonController: mastodonController)
title = "My Profile" title = "My Profile"
tabBarItem.image = UIImage(systemName: "person.fill") tabBarItem.image = UIImage(systemName: "person.fill")
MastodonController.getOwnAccount { (account) in mastodonController.getOwnAccount { (account) in
self.accountID = account.id self.accountID = account.id
ImageCache.avatars.get(account.avatar, completion: { (data) in ImageCache.avatars.get(account.avatar, completion: { (data) in
@ -50,7 +50,7 @@ class MyProfileTableViewController: ProfileTableViewController {
} }
@objc func preferencesPressed() { @objc func preferencesPressed() {
present(PreferencesNavigationController(), animated: true) present(PreferencesNavigationController(mastodonController: mastodonController), animated: true)
} }
@objc func closePreferences() { @objc func closePreferences() {

View File

@ -12,6 +12,8 @@ import SafariServices
class ProfileTableViewController: EnhancedTableViewController { class ProfileTableViewController: EnhancedTableViewController {
weak var mastodonController: MastodonController!
var accountID: String! { var accountID: String! {
didSet { didSet {
if shouldLoadOnAccountIDSet { if shouldLoadOnAccountIDSet {
@ -43,7 +45,9 @@ class ProfileTableViewController: EnhancedTableViewController {
var shouldLoadOnAccountIDSet = false var shouldLoadOnAccountIDSet = false
var loadingVC: LoadingViewController? = nil var loadingVC: LoadingViewController? = nil
init(accountID: String?) { init(accountID: String?, mastodonController: MastodonController) {
self.mastodonController = mastodonController
self.accountID = accountID self.accountID = accountID
super.init(style: .plain) super.init(style: .plain)
@ -69,12 +73,12 @@ class ProfileTableViewController: EnhancedTableViewController {
tableView.prefetchDataSource = self tableView.prefetchDataSource = self
if let accountID = accountID { if let accountID = accountID {
if MastodonCache.account(for: accountID) != nil { if mastodonController.cache.account(for: accountID) != nil {
updateAccountUI() updateAccountUI()
} else { } else {
loadingVC = LoadingViewController() loadingVC = LoadingViewController()
embedChild(loadingVC!) embedChild(loadingVC!)
MastodonCache.account(for: accountID) { (account) in mastodonController.cache.account(for: accountID) { (account) in
guard account != nil else { guard account != nil else {
let alert = UIAlertController(title: "Something Went Wrong", message: "Couldn't load the selected account", preferredStyle: .alert) let alert = UIAlertController(title: "Something Went Wrong", message: "Couldn't load the selected account", preferredStyle: .alert)
alert.addAction(UIAlertAction(title: "OK", style: .default, handler: { (_) in alert.addAction(UIAlertAction(title: "OK", style: .default, handler: { (_) in
@ -108,14 +112,14 @@ class ProfileTableViewController: EnhancedTableViewController {
getStatuses(onlyPinned: true) { (response) in getStatuses(onlyPinned: true) { (response) in
guard case let .success(statuses, _) = response else { fatalError() } guard case let .success(statuses, _) = response else { fatalError() }
MastodonCache.addAll(statuses: statuses) self.mastodonController.cache.addAll(statuses: statuses)
self.pinnedStatuses = statuses.map { ($0.id, .unknown) } self.pinnedStatuses = statuses.map { ($0.id, .unknown) }
} }
getStatuses() { response in getStatuses() { response in
guard case let .success(statuses, pagination) = response else { fatalError() } guard case let .success(statuses, pagination) = response else { fatalError() }
MastodonCache.addAll(statuses: statuses) self.mastodonController.cache.addAll(statuses: statuses)
self.timelineSegments.append(statuses.map { ($0.id, .unknown) }) self.timelineSegments.append(statuses.map { ($0.id, .unknown) })
self.older = pagination?.older self.older = pagination?.older
@ -124,18 +128,18 @@ class ProfileTableViewController: EnhancedTableViewController {
} }
@objc func updateUIForPreferences() { @objc func updateUIForPreferences() {
guard let account = MastodonCache.account(for: accountID) else { fatalError("Missing cached account \(accountID!)") } guard let accountID = accountID, let account = mastodonController.cache.account(for: accountID) else { return }
navigationItem.title = account.realDisplayName navigationItem.title = account.realDisplayName
} }
func getStatuses(for range: RequestRange = .default, onlyPinned: Bool = false, completion: @escaping Client.Callback<[Status]>) { func getStatuses(for range: RequestRange = .default, onlyPinned: Bool = false, completion: @escaping Client.Callback<[Status]>) {
let request = Account.getStatuses(accountID, range: range, onlyMedia: false, pinned: onlyPinned, excludeReplies: !Preferences.shared.showRepliesInProfiles) let request = Account.getStatuses(accountID, range: range, onlyMedia: false, pinned: onlyPinned, excludeReplies: !Preferences.shared.showRepliesInProfiles)
MastodonController.client.run(request, completion: completion) mastodonController.run(request, completion: completion)
} }
func sendMessageMentioning() { func sendMessageMentioning() {
guard let account = MastodonCache.account(for: accountID) else { fatalError("Missing cached account \(accountID!)") } guard let account = mastodonController.cache.account(for: accountID) else { fatalError("Missing cached account \(accountID!)") }
let vc = UINavigationController(rootViewController: ComposeViewController(mentioningAcct: account.acct)) let vc = UINavigationController(rootViewController: ComposeViewController(mentioningAcct: account.acct, mastodonController: mastodonController))
present(vc, animated: true) present(vc, animated: true)
} }
@ -148,7 +152,7 @@ class ProfileTableViewController: EnhancedTableViewController {
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
if section == 0 { if section == 0 {
return accountID == nil || MastodonCache.account(for: accountID) == nil ? 0 : 1 return accountID == nil || mastodonController.cache.account(for: accountID) == nil ? 0 : 1
} else if section == 1 { } else if section == 1 {
return pinnedStatuses.count return pinnedStatuses.count
} else { } else {
@ -168,14 +172,14 @@ class ProfileTableViewController: EnhancedTableViewController {
guard let cell = tableView.dequeueReusableCell(withIdentifier: "statusCell", for: indexPath) as? TimelineStatusTableViewCell else { fatalError() } guard let cell = tableView.dequeueReusableCell(withIdentifier: "statusCell", for: indexPath) as? TimelineStatusTableViewCell else { fatalError() }
let (id, state) = pinnedStatuses[indexPath.row] let (id, state) = pinnedStatuses[indexPath.row]
cell.showPinned = true cell.showPinned = true
cell.updateUI(statusID: id, state: state)
cell.delegate = self cell.delegate = self
cell.updateUI(statusID: id, state: state)
return cell return cell
default: default:
guard let cell = tableView.dequeueReusableCell(withIdentifier: "statusCell", for: indexPath) as? TimelineStatusTableViewCell else { fatalError() } guard let cell = tableView.dequeueReusableCell(withIdentifier: "statusCell", for: indexPath) as? TimelineStatusTableViewCell else { fatalError() }
let (id, state) = timelineSegments[indexPath.section - 2][indexPath.row] let (id, state) = timelineSegments[indexPath.section - 2][indexPath.row]
cell.updateUI(statusID: id, state: state)
cell.delegate = self cell.delegate = self
cell.updateUI(statusID: id, state: state)
return cell return cell
} }
} }
@ -189,7 +193,7 @@ class ProfileTableViewController: EnhancedTableViewController {
getStatuses(for: older) { response in getStatuses(for: older) { response in
guard case let .success(newStatuses, pagination) = response else { fatalError() } guard case let .success(newStatuses, pagination) = response else { fatalError() }
MastodonCache.addAll(statuses: newStatuses) self.mastodonController.cache.addAll(statuses: newStatuses)
self.timelineSegments[indexPath.section - 2].append(contentsOf: newStatuses.map { ($0.id, .unknown) }) self.timelineSegments[indexPath.section - 2].append(contentsOf: newStatuses.map { ($0.id, .unknown) })
self.older = pagination?.older self.older = pagination?.older
@ -215,7 +219,7 @@ class ProfileTableViewController: EnhancedTableViewController {
getStatuses(for: newer) { response in getStatuses(for: newer) { response in
guard case let .success(newStatuses, pagination) = response else { fatalError() } guard case let .success(newStatuses, pagination) = response else { fatalError() }
MastodonCache.addAll(statuses: newStatuses) self.mastodonController.cache.addAll(statuses: newStatuses)
self.timelineSegments[0].insert(contentsOf: newStatuses.map { ($0.id, .unknown) }, at: 0) self.timelineSegments[0].insert(contentsOf: newStatuses.map { ($0.id, .unknown) }, at: 0)
if let newer = pagination?.newer { if let newer = pagination?.newer {
@ -229,7 +233,7 @@ class ProfileTableViewController: EnhancedTableViewController {
getStatuses(onlyPinned: true) { (response) in getStatuses(onlyPinned: true) { (response) in
guard case let .success(newPinnedStatuses, _) = response else { fatalError() } guard case let .success(newPinnedStatuses, _) = response else { fatalError() }
MastodonCache.addAll(statuses: newPinnedStatuses) self.mastodonController.cache.addAll(statuses: newPinnedStatuses)
let oldPinnedStatuses = self.pinnedStatuses let oldPinnedStatuses = self.pinnedStatuses
var pinnedStatuses = [(id: String, state: StatusState)]() var pinnedStatuses = [(id: String, state: StatusState)]()
@ -253,6 +257,8 @@ class ProfileTableViewController: EnhancedTableViewController {
} }
extension ProfileTableViewController: StatusTableViewCellDelegate { extension ProfileTableViewController: StatusTableViewCellDelegate {
var apiController: MastodonController { mastodonController }
func statusCellCollapsedStateChanged(_ cell: BaseStatusTableViewCell) { func statusCellCollapsedStateChanged(_ cell: BaseStatusTableViewCell) {
// causes the table view to recalculate the cell heights // causes the table view to recalculate the cell heights
tableView.beginUpdates() tableView.beginUpdates()
@ -262,9 +268,9 @@ extension ProfileTableViewController: StatusTableViewCellDelegate {
extension ProfileTableViewController: ProfileHeaderTableViewCellDelegate { extension ProfileTableViewController: ProfileHeaderTableViewCellDelegate {
func showMoreOptions(cell: ProfileHeaderTableViewCell) { func showMoreOptions(cell: ProfileHeaderTableViewCell) {
let account = MastodonCache.account(for: accountID)! let account = mastodonController.cache.account(for: accountID)!
MastodonCache.relationship(for: account.id) { [weak self] (relationship) in mastodonController.cache.relationship(for: account.id) { [weak self] (relationship) in
guard let self = self else { return } guard let self = self else { return }
var customActivities: [UIActivity] = [OpenInSafariActivity()] var customActivities: [UIActivity] = [OpenInSafariActivity()]
@ -287,7 +293,7 @@ extension ProfileTableViewController: UITableViewDataSourcePrefetching {
func tableView(_ tableView: UITableView, prefetchRowsAt indexPaths: [IndexPath]) { func tableView(_ tableView: UITableView, prefetchRowsAt indexPaths: [IndexPath]) {
for indexPath in indexPaths where indexPath.section > 1 { for indexPath in indexPaths where indexPath.section > 1 {
let statusID = timelineSegments[indexPath.section - 2][indexPath.row].id let statusID = timelineSegments[indexPath.section - 2][indexPath.row].id
guard let status = MastodonCache.status(for: statusID) else { continue } guard let status = mastodonController.cache.status(for: statusID) else { continue }
ImageCache.avatars.get(status.account.avatar, completion: nil) ImageCache.avatars.get(status.account.avatar, completion: nil)
for attachment in status.attachments { for attachment in status.attachments {
ImageCache.attachments.get(attachment.url, completion: nil) ImageCache.attachments.get(attachment.url, completion: nil)
@ -298,7 +304,7 @@ extension ProfileTableViewController: UITableViewDataSourcePrefetching {
func tableView(_ tableView: UITableView, cancelPrefetchingForRowsAt indexPaths: [IndexPath]) { func tableView(_ tableView: UITableView, cancelPrefetchingForRowsAt indexPaths: [IndexPath]) {
for indexPath in indexPaths where indexPath.section > 1 { for indexPath in indexPaths where indexPath.section > 1 {
let statusID = timelineSegments[indexPath.section - 2][indexPath.row].id let statusID = timelineSegments[indexPath.section - 2][indexPath.row].id
guard let status = MastodonCache.status(for: statusID) else { continue } guard let status = mastodonController.cache.status(for: statusID) else { continue }
ImageCache.avatars.cancel(status.account.avatar) ImageCache.avatars.cancel(status.account.avatar)
for attachment in status.attachments { for attachment in status.attachments {
ImageCache.attachments.cancel(attachment.url) ImageCache.attachments.cancel(attachment.url)

View File

@ -28,6 +28,8 @@ extension SearchResultsViewControllerDelegate {
class SearchResultsViewController: EnhancedTableViewController { class SearchResultsViewController: EnhancedTableViewController {
let mastodonController: MastodonController!
weak var exploreNavigationController: UINavigationController? weak var exploreNavigationController: UINavigationController?
weak var delegate: SearchResultsViewControllerDelegate? weak var delegate: SearchResultsViewControllerDelegate?
@ -40,7 +42,9 @@ class SearchResultsViewController: EnhancedTableViewController {
let searchSubject = PassthroughSubject<String?, Never>() let searchSubject = PassthroughSubject<String?, Never>()
var currentQuery: String? var currentQuery: String?
init() { init(mastodonController: MastodonController) {
self.mastodonController = mastodonController
super.init(style: .grouped) super.init(style: .grouped)
title = NSLocalizedString("Search", comment: "search screen title") title = NSLocalizedString("Search", comment: "search screen title")
@ -61,18 +65,18 @@ class SearchResultsViewController: EnhancedTableViewController {
switch item { switch item {
case let .account(id): case let .account(id):
let cell = tableView.dequeueReusableCell(withIdentifier: accountCell, for: indexPath) as! AccountTableViewCell let cell = tableView.dequeueReusableCell(withIdentifier: accountCell, for: indexPath) as! AccountTableViewCell
cell.updateUI(accountID: id)
cell.delegate = self cell.delegate = self
cell.updateUI(accountID: id)
return cell return cell
case let .hashtag(tag): case let .hashtag(tag):
let cell = tableView.dequeueReusableCell(withIdentifier: hashtagCell, for: indexPath) as! HashtagTableViewCell let cell = tableView.dequeueReusableCell(withIdentifier: hashtagCell, for: indexPath) as! HashtagTableViewCell
cell.updateUI(hashtag: tag)
cell.delegate = self cell.delegate = self
cell.updateUI(hashtag: tag)
return cell return cell
case let .status(id, state): case let .status(id, state):
let cell = tableView.dequeueReusableCell(withIdentifier: statusCell, for: indexPath) as! TimelineStatusTableViewCell let cell = tableView.dequeueReusableCell(withIdentifier: statusCell, for: indexPath) as! TimelineStatusTableViewCell
cell.updateUI(statusID: id, state: state)
cell.delegate = self cell.delegate = self
cell.updateUI(statusID: id, state: state)
return cell return cell
} }
}) })
@ -117,8 +121,8 @@ class SearchResultsViewController: EnhancedTableViewController {
activityIndicator.startAnimating() activityIndicator.startAnimating()
} }
let request = MastodonController.client.search(query: query, resolve: true, limit: 10) let request = Client.search(query: query, resolve: true, limit: 10)
MastodonController.client.run(request) { (response) in mastodonController.run(request) { (response) in
guard case let .success(results, _) = response else { fatalError() } guard case let .success(results, _) = response else { fatalError() }
DispatchQueue.main.async { DispatchQueue.main.async {
@ -132,7 +136,7 @@ class SearchResultsViewController: EnhancedTableViewController {
if self.onlySections.contains(.accounts) && !results.accounts.isEmpty { if self.onlySections.contains(.accounts) && !results.accounts.isEmpty {
snapshot.appendSections([.accounts]) snapshot.appendSections([.accounts])
snapshot.appendItems(results.accounts.map { .account($0.id) }, toSection: .accounts) snapshot.appendItems(results.accounts.map { .account($0.id) }, toSection: .accounts)
MastodonCache.addAll(accounts: results.accounts) self.mastodonController.cache.addAll(accounts: results.accounts)
} }
if self.onlySections.contains(.hashtags) && !results.hashtags.isEmpty { if self.onlySections.contains(.hashtags) && !results.hashtags.isEmpty {
snapshot.appendSections([.hashtags]) snapshot.appendSections([.hashtags])
@ -141,8 +145,8 @@ class SearchResultsViewController: EnhancedTableViewController {
if self.onlySections.contains(.statuses) && !results.statuses.isEmpty { if self.onlySections.contains(.statuses) && !results.statuses.isEmpty {
snapshot.appendSections([.statuses]) snapshot.appendSections([.statuses])
snapshot.appendItems(results.statuses.map { .status($0.id, .unknown) }, toSection: .statuses) snapshot.appendItems(results.statuses.map { .status($0.id, .unknown) }, toSection: .statuses)
MastodonCache.addAll(statuses: results.statuses) self.mastodonController.cache.addAll(statuses: results.statuses)
MastodonCache.addAll(accounts: results.statuses.map { $0.account }) self.mastodonController.cache.addAll(accounts: results.statuses.map { $0.account })
} }
self.dataSource.apply(snapshot) self.dataSource.apply(snapshot)
} }
@ -217,6 +221,7 @@ extension SearchResultsViewController: UISearchBarDelegate {
} }
extension SearchResultsViewController: StatusTableViewCellDelegate { extension SearchResultsViewController: StatusTableViewCellDelegate {
var apiController: MastodonController { mastodonController }
func statusCellCollapsedStateChanged(_ cell: BaseStatusTableViewCell) { func statusCellCollapsedStateChanged(_ cell: BaseStatusTableViewCell) {
tableView.beginUpdates() tableView.beginUpdates()
tableView.endUpdates() tableView.endUpdates()

View File

@ -14,6 +14,8 @@ class StatusActionAccountListTableViewController: EnhancedTableViewController {
private let statusCell = "statusCell" private let statusCell = "statusCell"
private let accountCell = "accountCell" private let accountCell = "accountCell"
let mastodonController: MastodonController
let actionType: ActionType let actionType: ActionType
let statusID: String let statusID: String
var statusState: StatusState var statusState: StatusState
@ -32,8 +34,11 @@ class StatusActionAccountListTableViewController: EnhancedTableViewController {
- Parameter actionType The action that this VC is for. - Parameter actionType The action that this VC is for.
- Parameter statusID The ID of the status to show. - Parameter statusID The ID of the status to show.
- Parameter accountIDs The accounts that will be shown. If `nil` is passed, a request will be performed to load the accounts. - Parameter accountIDs The accounts that will be shown. If `nil` is passed, a request will be performed to load the accounts.
- Parameter mastodonController The `MastodonController` instance this view controller uses.
*/ */
init(actionType: ActionType, statusID: String, statusState: StatusState, accountIDs: [String]?) { init(actionType: ActionType, statusID: String, statusState: StatusState, accountIDs: [String]?, mastodonController: MastodonController) {
self.mastodonController = mastodonController
self.actionType = actionType self.actionType = actionType
self.statusID = statusID self.statusID = statusID
self.statusState = statusState self.statusState = statusState
@ -68,16 +73,16 @@ class StatusActionAccountListTableViewController: EnhancedTableViewController {
if accountIDs == nil { if accountIDs == nil {
// account IDs haven't been set, so perform a request to load them // account IDs haven't been set, so perform a request to load them
guard let status = MastodonCache.status(for: statusID) else { guard let status = mastodonController.cache.status(for: statusID) else {
fatalError("Missing cached status \(statusID)") fatalError("Missing cached status \(statusID)")
} }
tableView.tableFooterView = UIActivityIndicatorView(style: .large) tableView.tableFooterView = UIActivityIndicatorView(style: .large)
let request = actionType == .favorite ? Status.getFavourites(status) : Status.getReblogs(status) let request = actionType == .favorite ? Status.getFavourites(status) : Status.getReblogs(status)
MastodonController.client.run(request) { (response) in mastodonController.run(request) { (response) in
guard case let .success(accounts, _) = response else { fatalError() } guard case let .success(accounts, _) = response else { fatalError() }
MastodonCache.addAll(accounts: accounts) self.mastodonController.cache.addAll(accounts: accounts)
DispatchQueue.main.async { DispatchQueue.main.async {
self.accountIDs = accounts.map { $0.id } self.accountIDs = accounts.map { $0.id }
self.tableView.tableFooterView = nil self.tableView.tableFooterView = nil
@ -111,14 +116,14 @@ class StatusActionAccountListTableViewController: EnhancedTableViewController {
switch indexPath.section { switch indexPath.section {
case 0: case 0:
guard let cell = tableView.dequeueReusableCell(withIdentifier: statusCell, for: indexPath) as? TimelineStatusTableViewCell else { fatalError() } guard let cell = tableView.dequeueReusableCell(withIdentifier: statusCell, for: indexPath) as? TimelineStatusTableViewCell else { fatalError() }
cell.updateUI(statusID: statusID, state: statusState)
cell.delegate = self cell.delegate = self
cell.updateUI(statusID: statusID, state: statusState)
return cell return cell
case 1: case 1:
guard let accountIDs = accountIDs, guard let accountIDs = accountIDs,
let cell = tableView.dequeueReusableCell(withIdentifier: accountCell, for: indexPath) as? AccountTableViewCell else { fatalError() } let cell = tableView.dequeueReusableCell(withIdentifier: accountCell, for: indexPath) as? AccountTableViewCell else { fatalError() }
cell.updateUI(accountID: accountIDs[indexPath.row])
cell.delegate = self cell.delegate = self
cell.updateUI(accountID: accountIDs[indexPath.row])
return cell return cell
default: default:
fatalError("Invalid section \(indexPath.section)") fatalError("Invalid section \(indexPath.section)")
@ -137,6 +142,7 @@ class StatusActionAccountListTableViewController: EnhancedTableViewController {
} }
extension StatusActionAccountListTableViewController: StatusTableViewCellDelegate { extension StatusActionAccountListTableViewController: StatusTableViewCellDelegate {
var apiController: MastodonController { mastodonController }
func statusCellCollapsedStateChanged(_ cell: BaseStatusTableViewCell) { func statusCellCollapsedStateChanged(_ cell: BaseStatusTableViewCell) {
// causes the table view to recalculate the cell heights // causes the table view to recalculate the cell heights
tableView.beginUpdates() tableView.beginUpdates()

View File

@ -15,17 +15,17 @@ class HashtagTimelineViewController: TimelineTableViewController {
var toggleSaveButton: UIBarButtonItem! var toggleSaveButton: UIBarButtonItem!
var toggleSaveButtonTitle: String { var toggleSaveButtonTitle: String {
if SavedHashtagsManager.shared.isSaved(hashtag) { if SavedDataManager.shared.isSaved(hashtag: hashtag, for: mastodonController.accountInfo!) {
return NSLocalizedString("Unsave", comment: "unsave hashtag button") return NSLocalizedString("Unsave", comment: "unsave hashtag button")
} else { } else {
return NSLocalizedString("Save", comment: "save hashtag button") return NSLocalizedString("Save", comment: "save hashtag button")
} }
} }
init(for hashtag: Hashtag) { init(for hashtag: Hashtag, mastodonController: MastodonController) {
self.hashtag = hashtag self.hashtag = hashtag
super.init(for: .tag(hashtag: hashtag.name)) super.init(for: .tag(hashtag: hashtag.name), mastodonController: mastodonController)
} }
required init?(coder aDecoder: NSCoder) { required init?(coder aDecoder: NSCoder) {
@ -48,10 +48,10 @@ class HashtagTimelineViewController: TimelineTableViewController {
// MARK: - Interaction // MARK: - Interaction
@objc func toggleSaveButtonPressed() { @objc func toggleSaveButtonPressed() {
if SavedHashtagsManager.shared.isSaved(hashtag) { if SavedDataManager.shared.isSaved(hashtag: hashtag, for: mastodonController.accountInfo!) {
SavedHashtagsManager.shared.remove(hashtag) SavedDataManager.shared.remove(hashtag: hashtag, for: mastodonController.accountInfo!)
} else { } else {
SavedHashtagsManager.shared.add(hashtag) SavedDataManager.shared.add(hashtag: hashtag, for: mastodonController.accountInfo!)
} }
} }

View File

@ -8,30 +8,38 @@
import UIKit import UIKit
protocol InstanceTimelineViewControllerDelegate { protocol InstanceTimelineViewControllerDelegate: class {
func didSaveInstance(url: URL) func didSaveInstance(url: URL)
func didUnsaveInstance(url: URL) func didUnsaveInstance(url: URL)
} }
class InstanceTimelineViewController: TimelineTableViewController { class InstanceTimelineViewController: TimelineTableViewController {
var delegate: InstanceTimelineViewControllerDelegate? weak var delegate: InstanceTimelineViewControllerDelegate?
weak var parentMastodonController: MastodonController?
let instanceURL: URL let instanceURL: URL
let instanceMastodonController: MastodonController
var toggleSaveButton: UIBarButtonItem! var toggleSaveButton: UIBarButtonItem!
var toggleSaveButtonTitle: String { var toggleSaveButtonTitle: String {
if SavedInstanceManager.shared.isSaved(instanceURL) { if SavedDataManager.shared.isSaved(instance: instanceURL, for: parentMastodonController!.accountInfo!) {
return NSLocalizedString("Unsave", comment: "unsave instance button") return NSLocalizedString("Unsave", comment: "unsave instance button")
} else { } else {
return NSLocalizedString("Save", comment: "save instance button") return NSLocalizedString("Save", comment: "save instance button")
} }
} }
init(for url: URL) { init(for url: URL, parentMastodonController: MastodonController) {
self.instanceURL = url self.parentMastodonController = parentMastodonController
super.init(for: .instance(instanceURL: url)) self.instanceURL = url
// the timeline VC only stores a weak reference to it, so we need to store a strong reference to make sure it's not released immediately
instanceMastodonController = MastodonController(instanceURL: url)
super.init(for: .instance(instanceURL: url), mastodonController: instanceMastodonController)
} }
required init?(coder aDecoder: NSCoder) { required init?(coder aDecoder: NSCoder) {
@ -51,6 +59,15 @@ class InstanceTimelineViewController: TimelineTableViewController {
toggleSaveButton.title = toggleSaveButtonTitle toggleSaveButton.title = toggleSaveButtonTitle
} }
// MARK: - Table view data source
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = super.tableView(tableView, cellForRowAt: indexPath) as! TimelineStatusTableViewCell
cell.delegate = nil
cell.overrideMastodonController = mastodonController
return cell
}
// MARK: - Table view delegate // MARK: - Table view delegate
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
@ -59,11 +76,11 @@ class InstanceTimelineViewController: TimelineTableViewController {
// MARK: - Interaction // MARK: - Interaction
@objc func toggleSaveButtonPressed() { @objc func toggleSaveButtonPressed() {
if SavedInstanceManager.shared.isSaved(instanceURL) { if SavedDataManager.shared.isSaved(instance: instanceURL, for: parentMastodonController!.accountInfo!) {
SavedInstanceManager.shared.remove(instanceURL) SavedDataManager.shared.remove(instance: instanceURL, for: parentMastodonController!.accountInfo!)
delegate?.didUnsaveInstance(url: instanceURL) delegate?.didUnsaveInstance(url: instanceURL)
} else { } else {
SavedInstanceManager.shared.add(instanceURL) SavedDataManager.shared.add(instance: instanceURL, for: parentMastodonController!.accountInfo!)
delegate?.didSaveInstance(url: instanceURL) delegate?.didSaveInstance(url: instanceURL)
} }
} }

View File

@ -12,6 +12,7 @@ import Pachyderm
class TimelineTableViewController: EnhancedTableViewController { class TimelineTableViewController: EnhancedTableViewController {
var timeline: Timeline! var timeline: Timeline!
weak var mastodonController: MastodonController!
var timelineSegments: [[(id: String, state: StatusState)]] = [] { var timelineSegments: [[(id: String, state: StatusState)]] = [] {
didSet { didSet {
@ -24,8 +25,9 @@ class TimelineTableViewController: EnhancedTableViewController {
var newer: RequestRange? var newer: RequestRange?
var older: RequestRange? var older: RequestRange?
init(for timeline: Timeline) { init(for timeline: Timeline, mastodonController: MastodonController) {
self.timeline = timeline self.timeline = timeline
self.mastodonController = mastodonController
super.init(style: .plain) super.init(style: .plain)
@ -56,15 +58,14 @@ class TimelineTableViewController: EnhancedTableViewController {
tableView.prefetchDataSource = self tableView.prefetchDataSource = self
guard MastodonController.client?.accessToken != nil else { return }
loadInitialStatuses() loadInitialStatuses()
} }
func loadInitialStatuses() { func loadInitialStatuses() {
let request = MastodonController.client.getStatuses(timeline: timeline) let request = Client.getStatuses(timeline: timeline)
MastodonController.client.run(request) { response in mastodonController.run(request) { response in
guard case let .success(statuses, pagination) = response else { fatalError() } guard case let .success(statuses, pagination) = response else { fatalError() }
MastodonCache.addAll(statuses: statuses) self.mastodonController.cache.addAll(statuses: statuses)
self.timelineSegments.insert(statuses.map { ($0.id, .unknown) }, at: 0) self.timelineSegments.insert(statuses.map { ($0.id, .unknown) }, at: 0)
self.newer = pagination?.newer self.newer = pagination?.newer
self.older = pagination?.older self.older = pagination?.older
@ -86,8 +87,8 @@ class TimelineTableViewController: EnhancedTableViewController {
guard let cell = tableView.dequeueReusableCell(withIdentifier: "statusCell", for: indexPath) as? TimelineStatusTableViewCell else { fatalError() } guard let cell = tableView.dequeueReusableCell(withIdentifier: "statusCell", for: indexPath) as? TimelineStatusTableViewCell else { fatalError() }
let (id, state) = timelineSegments[indexPath.section][indexPath.row] let (id, state) = timelineSegments[indexPath.section][indexPath.row]
cell.updateUI(statusID: id, state: state)
cell.delegate = self cell.delegate = self
cell.updateUI(statusID: id, state: state)
return cell return cell
} }
@ -99,11 +100,11 @@ class TimelineTableViewController: EnhancedTableViewController {
indexPath.row == timelineSegments[indexPath.section].count - 1 { indexPath.row == timelineSegments[indexPath.section].count - 1 {
guard let older = older else { return } guard let older = older else { return }
let request = MastodonController.client.getStatuses(timeline: timeline, range: older) let request = Client.getStatuses(timeline: timeline, range: older)
MastodonController.client.run(request) { response in mastodonController.run(request) { response in
guard case let .success(newStatuses, pagination) = response else { fatalError() } guard case let .success(newStatuses, pagination) = response else { fatalError() }
self.older = pagination?.older self.older = pagination?.older
MastodonCache.addAll(statuses: newStatuses) self.mastodonController.cache.addAll(statuses: newStatuses)
self.timelineSegments[self.timelineSegments.count - 1].append(contentsOf: newStatuses.map { ($0.id, .unknown) }) self.timelineSegments[self.timelineSegments.count - 1].append(contentsOf: newStatuses.map { ($0.id, .unknown) })
} }
} }
@ -124,11 +125,11 @@ class TimelineTableViewController: EnhancedTableViewController {
@objc func refreshStatuses(_ sender: Any) { @objc func refreshStatuses(_ sender: Any) {
guard let newer = newer else { return } guard let newer = newer else { return }
let request = MastodonController.client.getStatuses(timeline: timeline, range: newer) let request = Client.getStatuses(timeline: timeline, range: newer)
MastodonController.client.run(request) { response in mastodonController.run(request) { response in
guard case let .success(newStatuses, pagination) = response else { fatalError() } guard case let .success(newStatuses, pagination) = response else { fatalError() }
self.newer = pagination?.newer
MastodonCache.addAll(statuses: newStatuses) self.mastodonController.cache.addAll(statuses: newStatuses)
self.timelineSegments[0].insert(contentsOf: newStatuses.map { ($0.id, .unknown) }, at: 0) self.timelineSegments[0].insert(contentsOf: newStatuses.map { ($0.id, .unknown) }, at: 0)
if let newer = pagination?.newer { if let newer = pagination?.newer {
@ -151,6 +152,8 @@ class TimelineTableViewController: EnhancedTableViewController {
} }
extension TimelineTableViewController: StatusTableViewCellDelegate { extension TimelineTableViewController: StatusTableViewCellDelegate {
var apiController: MastodonController { mastodonController }
func statusCellCollapsedStateChanged(_ cell: BaseStatusTableViewCell) { func statusCellCollapsedStateChanged(_ cell: BaseStatusTableViewCell) {
// causes the table view to recalculate the cell heights // causes the table view to recalculate the cell heights
tableView.beginUpdates() tableView.beginUpdates()
@ -161,7 +164,7 @@ extension TimelineTableViewController: StatusTableViewCellDelegate {
extension TimelineTableViewController: UITableViewDataSourcePrefetching { extension TimelineTableViewController: UITableViewDataSourcePrefetching {
func tableView(_ tableView: UITableView, prefetchRowsAt indexPaths: [IndexPath]) { func tableView(_ tableView: UITableView, prefetchRowsAt indexPaths: [IndexPath]) {
for indexPath in indexPaths { for indexPath in indexPaths {
guard let status = MastodonCache.status(for: statusID(for: indexPath)) else { continue } guard let status = mastodonController.cache.status(for: statusID(for: indexPath)) else { continue }
ImageCache.avatars.get(status.account.avatar, completion: nil) ImageCache.avatars.get(status.account.avatar, completion: nil)
for attachment in status.attachments { for attachment in status.attachments {
ImageCache.attachments.get(attachment.url, completion: nil) ImageCache.attachments.get(attachment.url, completion: nil)
@ -171,7 +174,7 @@ extension TimelineTableViewController: UITableViewDataSourcePrefetching {
func tableView(_ tableView: UITableView, cancelPrefetchingForRowsAt indexPaths: [IndexPath]) { func tableView(_ tableView: UITableView, cancelPrefetchingForRowsAt indexPaths: [IndexPath]) {
for indexPath in indexPaths { for indexPath in indexPaths {
guard let status = MastodonCache.status(for: statusID(for: indexPath)) else { continue } guard let status = mastodonController.cache.status(for: statusID(for: indexPath)) else { continue }
ImageCache.avatars.cancel(status.account.avatar) ImageCache.avatars.cancel(status.account.avatar)
for attachment in status.attachments { for attachment in status.attachments {
ImageCache.attachments.cancel(attachment.url) ImageCache.attachments.cancel(attachment.url)

View File

@ -14,14 +14,18 @@ class TimelinesPageViewController: SegmentedPageViewController {
private let federatedTitle = NSLocalizedString("Federated", comment: "federated timeline tab title") private let federatedTitle = NSLocalizedString("Federated", comment: "federated timeline tab title")
private let localTitle = NSLocalizedString("Local", comment: "local timeline tab title") private let localTitle = NSLocalizedString("Local", comment: "local timeline tab title")
init() { weak var mastodonController: MastodonController!
let home = TimelineTableViewController(for: .home)
init(mastodonController: MastodonController) {
self.mastodonController = mastodonController
let home = TimelineTableViewController(for: .home, mastodonController: mastodonController)
home.title = homeTitle home.title = homeTitle
let federated = TimelineTableViewController(for: .public(local: false)) let federated = TimelineTableViewController(for: .public(local: false), mastodonController: mastodonController)
federated.title = federatedTitle federated.title = federatedTitle
let local = TimelineTableViewController(for: .public(local: true)) let local = TimelineTableViewController(for: .public(local: true), mastodonController: mastodonController)
local.title = localTitle local.title = localTitle
super.init(titles: [ super.init(titles: [

View File

@ -22,8 +22,11 @@ protocol MenuPreviewProvider {
extension MenuPreviewProvider { extension MenuPreviewProvider {
private var mastodonController: MastodonController? { navigationDelegate?.apiController }
func actionsForProfile(accountID: String, sourceView: UIView?) -> [UIAction] { func actionsForProfile(accountID: String, sourceView: UIView?) -> [UIAction] {
guard let account = MastodonCache.account(for: accountID) else { return [] } guard let mastodonController = mastodonController,
let account = mastodonController.cache.account(for: accountID) else { return [] }
return [ return [
createAction(identifier: "openinsafari", title: "Open in Safari", systemImageName: "safari", handler: { (_) in createAction(identifier: "openinsafari", title: "Open in Safari", systemImageName: "safari", handler: { (_) in
self.navigationDelegate?.selected(url: account.url) self.navigationDelegate?.selected(url: account.url)
@ -53,7 +56,8 @@ extension MenuPreviewProvider {
} }
func actionsForStatus(statusID: String, sourceView: UIView?) -> [UIAction] { func actionsForStatus(statusID: String, sourceView: UIView?) -> [UIAction] {
guard let status = MastodonCache.status(for: statusID) else { return [] } guard let mastodonController = mastodonController,
let status = mastodonController.cache.status(for: statusID) else { return [] }
return [ return [
createAction(identifier: "reply", title: "Reply", systemImageName: "arrowshape.turn.up.left", handler: { (_) in createAction(identifier: "reply", title: "Reply", systemImageName: "arrowshape.turn.up.left", handler: { (_) in
self.navigationDelegate?.reply(to: statusID) self.navigationDelegate?.reply(to: statusID)

View File

@ -45,7 +45,9 @@ enum AppShortcutItem: String, CaseIterable {
case .composePost: case .composePost:
tab = .compose tab = .compose
} }
let controller = (UIApplication.shared.delegate!.window!!.rootViewController as! MainTabBarViewController) let scene = UIApplication.shared.connectedScenes.compactMap { $0 as? UIWindowScene }.first!
let window = scene.windows.first { $0.isKeyWindow }!
let controller = window.rootViewController as! MainTabBarViewController
controller.select(tab: tab) controller.select(tab: tab)
} }
} }

View File

@ -15,8 +15,16 @@ class UserActivityManager {
private static let encoder = PropertyListEncoder() private static let encoder = PropertyListEncoder()
private static let decoder = PropertyListDecoder() private static let decoder = PropertyListDecoder()
private static var mastodonController: MastodonController {
let scene = UIApplication.shared.activeOrBackgroundScene!
return scene.session.mastodonController!
}
private static func getMainTabBarController() -> MainTabBarViewController { private static func getMainTabBarController() -> MainTabBarViewController {
return (UIApplication.shared.delegate! as! AppDelegate).window!.rootViewController as! MainTabBarViewController let scene = UIApplication.shared.connectedScenes.compactMap { $0 as? UIWindowScene }.first!
let window = scene.windows.first { $0.isKeyWindow }!
return window.rootViewController as! MainTabBarViewController
// return (UIApplication.shared.delegate! as! AppDelegate).window!.rootViewController as! MainTabBarViewController
} }
private static func present(_ vc: UIViewController, animated: Bool = true) { private static func present(_ vc: UIViewController, animated: Bool = true) {
@ -42,7 +50,8 @@ class UserActivityManager {
static func handleNewPost(activity: NSUserActivity) { static func handleNewPost(activity: NSUserActivity) {
// TODO: check not currently showing compose screen // TODO: check not currently showing compose screen
let mentioning = activity.userInfo?["mentioning"] as? String let mentioning = activity.userInfo?["mentioning"] as? String
present(UINavigationController(rootViewController: ComposeViewController(mentioningAcct: mentioning))) let composeVC = ComposeViewController(mentioningAcct: mentioning, mastodonController: mastodonController)
present(UINavigationController(rootViewController: composeVC))
} }
// MARK: - Check Notifications // MARK: - Check Notifications
@ -60,6 +69,7 @@ class UserActivityManager {
if let navigationController = tabBarController.getTabController(tab: .notifications) as? UINavigationController, if let navigationController = tabBarController.getTabController(tab: .notifications) as? UINavigationController,
let notificationsPageController = navigationController.viewControllers.first as? NotificationsPageViewController { let notificationsPageController = navigationController.viewControllers.first as? NotificationsPageViewController {
navigationController.popToRootViewController(animated: false) navigationController.popToRootViewController(animated: false)
notificationsPageController.loadViewIfNeeded()
notificationsPageController.selectMode(.allNotifications) notificationsPageController.selectMode(.allNotifications)
} }
} }
@ -79,6 +89,7 @@ class UserActivityManager {
if let navController = tabBarController.getTabController(tab: .notifications) as? UINavigationController, if let navController = tabBarController.getTabController(tab: .notifications) as? UINavigationController,
let notificationsPageController = navController.viewControllers.first as? NotificationsPageViewController { let notificationsPageController = navController.viewControllers.first as? NotificationsPageViewController {
navController.popToRootViewController(animated: false) navController.popToRootViewController(animated: false)
notificationsPageController.loadViewIfNeeded()
notificationsPageController.selectMode(.mentionsOnly) notificationsPageController.selectMode(.mentionsOnly)
} }
} }
@ -144,7 +155,8 @@ class UserActivityManager {
rootController.segmentedControl.selectedSegmentIndex = index rootController.segmentedControl.selectedSegmentIndex = index
rootController.selectPage(at: index, animated: false) rootController.selectPage(at: index, animated: false)
default: default:
navigationController.pushViewController(TimelineTableViewController(for: timeline), animated: false) let timeline = TimelineTableViewController(for: timeline, mastodonController: mastodonController)
navigationController.pushViewController(timeline, animated: false)
} }
} }
@ -182,7 +194,7 @@ class UserActivityManager {
tabBarController.select(tab: .explore) tabBarController.select(tab: .explore)
if let navigationController = tabBarController.getTabController(tab: .explore) as? UINavigationController { if let navigationController = tabBarController.getTabController(tab: .explore) as? UINavigationController {
navigationController.popToRootViewController(animated: false) navigationController.popToRootViewController(animated: false)
navigationController.pushViewController(BookmarksTableViewController(), animated: false) navigationController.pushViewController(BookmarksTableViewController(mastodonController: mastodonController), animated: false)
} }
} }

View File

@ -10,7 +10,9 @@ import UIKit
import SafariServices import SafariServices
import Pachyderm import Pachyderm
protocol TuskerNavigationDelegate { protocol TuskerNavigationDelegate: class {
var apiController: MastodonController { get }
func show(_ vc: UIViewController) func show(_ vc: UIViewController)
@ -74,15 +76,15 @@ extension TuskerNavigationDelegate where Self: UIViewController {
return return
} }
show(ProfileTableViewController(accountID: accountID), sender: self) show(ProfileTableViewController(accountID: accountID, mastodonController: apiController), sender: self)
} }
func selected(mention: Mention) { func selected(mention: Mention) {
show(ProfileTableViewController(accountID: mention.id), sender: self) show(ProfileTableViewController(accountID: mention.id, mastodonController: apiController), sender: self)
} }
func selected(tag: Hashtag) { func selected(tag: Hashtag) {
show(HashtagTimelineViewController(for: tag), sender: self) show(HashtagTimelineViewController(for: tag, mastodonController: apiController), sender: self)
} }
func selected(url: URL) { func selected(url: URL) {
@ -119,7 +121,7 @@ extension TuskerNavigationDelegate where Self: UIViewController {
return return
} }
show(ConversationTableViewController(for: statusID, state: state), sender: self) show(ConversationTableViewController(for: statusID, state: state, mastodonController: apiController), sender: self)
} }
// protocols can't have parameter defaults, so this stub is necessary to fulfill the protocol req // protocols can't have parameter defaults, so this stub is necessary to fulfill the protocol req
@ -128,7 +130,7 @@ extension TuskerNavigationDelegate where Self: UIViewController {
} }
func compose(mentioning: String?) { func compose(mentioning: String?) {
let compose = ComposeViewController(mentioningAcct: mentioning) let compose = ComposeViewController(mentioningAcct: mentioning, mastodonController: apiController)
let vc = UINavigationController(rootViewController: compose) let vc = UINavigationController(rootViewController: compose)
vc.presentationController?.delegate = compose vc.presentationController?.delegate = compose
present(vc, animated: true) present(vc, animated: true)
@ -139,7 +141,7 @@ extension TuskerNavigationDelegate where Self: UIViewController {
} }
func reply(to statusID: String, mentioningAcct: String?) { func reply(to statusID: String, mentioningAcct: String?) {
let compose = ComposeViewController(inReplyTo: statusID, mentioningAcct: mentioningAcct) let compose = ComposeViewController(inReplyTo: statusID, mentioningAcct: mentioningAcct, mastodonController: apiController)
let vc = UINavigationController(rootViewController: compose) let vc = UINavigationController(rootViewController: compose)
vc.presentationController?.delegate = compose vc.presentationController?.delegate = compose
present(vc, animated: true) present(vc, animated: true)
@ -202,7 +204,7 @@ extension TuskerNavigationDelegate where Self: UIViewController {
} }
private func moreOptions(forStatus statusID: String) -> UIActivityViewController { private func moreOptions(forStatus statusID: String) -> UIActivityViewController {
guard let status = MastodonCache.status(for: statusID) else { fatalError("Missing cached status \(statusID)") } guard let status = apiController.cache.status(for: statusID) else { fatalError("Missing cached status \(statusID)") }
guard let url = status.url else { fatalError("Missing url for status \(statusID)") } guard let url = status.url else { fatalError("Missing url for status \(statusID)") }
var customActivites: [UIActivity] = [OpenInSafariActivity()] var customActivites: [UIActivity] = [OpenInSafariActivity()]
@ -210,7 +212,7 @@ extension TuskerNavigationDelegate where Self: UIViewController {
customActivites.insert(bookmarked ? UnbookmarkStatusActivity() : BookmarkStatusActivity(), at: 0) customActivites.insert(bookmarked ? UnbookmarkStatusActivity() : BookmarkStatusActivity(), at: 0)
} }
if status.account == MastodonController.account, if status.account == apiController.account,
let pinned = status.pinned { let pinned = status.pinned {
customActivites.insert(pinned ? UnpinStatusActivity() : PinStatusActivity(), at: 1) customActivites.insert(pinned ? UnpinStatusActivity() : PinStatusActivity(), at: 1)
} }
@ -221,7 +223,7 @@ extension TuskerNavigationDelegate where Self: UIViewController {
} }
private func moreOptions(forAccount accountID: String) -> UIActivityViewController { private func moreOptions(forAccount accountID: String) -> UIActivityViewController {
guard let account = MastodonCache.account(for: accountID) else { fatalError("Missing cached account \(accountID)") } guard let account = apiController.cache.account(for: accountID) else { fatalError("Missing cached account \(accountID)") }
return moreOptions(forURL: account.url) return moreOptions(forURL: account.url)
} }
@ -244,13 +246,13 @@ extension TuskerNavigationDelegate where Self: UIViewController {
} }
func showFollowedByList(accountIDs: [String]) { func showFollowedByList(accountIDs: [String]) {
let vc = AccountListTableViewController(accountIDs: accountIDs) let vc = AccountListTableViewController(accountIDs: accountIDs, mastodonController: apiController)
vc.title = NSLocalizedString("Followed By", comment: "followed by accounts list title") vc.title = NSLocalizedString("Followed By", comment: "followed by accounts list title")
show(vc, sender: self) show(vc, sender: self)
} }
func statusActionAccountList(action: StatusActionAccountListTableViewController.ActionType, statusID: String, statusState state: StatusState, accountIDs: [String]?) -> StatusActionAccountListTableViewController { func statusActionAccountList(action: StatusActionAccountListTableViewController.ActionType, statusID: String, statusState state: StatusState, accountIDs: [String]?) -> StatusActionAccountListTableViewController {
return StatusActionAccountListTableViewController(actionType: action, statusID: statusID, statusState: state, accountIDs: accountIDs) return StatusActionAccountListTableViewController(actionType: action, statusID: statusID, statusState: state, accountIDs: accountIDs, mastodonController: apiController)
} }
} }

View File

@ -10,7 +10,8 @@ import UIKit
class AccountTableViewCell: UITableViewCell { class AccountTableViewCell: UITableViewCell {
var delegate: TuskerNavigationDelegate? weak var delegate: TuskerNavigationDelegate?
var mastodonController: MastodonController! { delegate?.apiController }
@IBOutlet weak var avatarImageView: UIImageView! @IBOutlet weak var avatarImageView: UIImageView!
@IBOutlet weak var displayNameLabel: UILabel! @IBOutlet weak var displayNameLabel: UILabel!
@ -31,7 +32,7 @@ class AccountTableViewCell: UITableViewCell {
@objc func updateUIForPrefrences() { @objc func updateUIForPrefrences() {
avatarImageView.layer.cornerRadius = Preferences.shared.avatarStyle.cornerRadius(for: avatarImageView) avatarImageView.layer.cornerRadius = Preferences.shared.avatarStyle.cornerRadius(for: avatarImageView)
guard let account = MastodonCache.account(for: accountID) else { guard let account = mastodonController.cache.account(for: accountID) else {
fatalError("Missing cached account \(accountID!)") fatalError("Missing cached account \(accountID!)")
} }
displayNameLabel.text = account.realDisplayName displayNameLabel.text = account.realDisplayName
@ -39,7 +40,7 @@ class AccountTableViewCell: UITableViewCell {
func updateUI(accountID: String) { func updateUI(accountID: String) {
self.accountID = accountID self.accountID = accountID
guard let account = MastodonCache.account(for: accountID) else { guard let account = mastodonController.cache.account(for: accountID) else {
fatalError("Missing cached account \(accountID)") fatalError("Missing cached account \(accountID)")
} }
@ -68,9 +69,10 @@ extension AccountTableViewCell: MenuPreviewProvider {
var navigationDelegate: TuskerNavigationDelegate? { return delegate } var navigationDelegate: TuskerNavigationDelegate? { return delegate }
func getPreviewProviders(for location: CGPoint, sourceViewController: UIViewController) -> PreviewProviders? { func getPreviewProviders(for location: CGPoint, sourceViewController: UIViewController) -> PreviewProviders? {
guard let mastodonController = mastodonController else { return nil }
return ( return (
content: { ProfileTableViewController(accountID: self.accountID) }, content: { ProfileTableViewController(accountID: self.accountID, mastodonController: mastodonController) },
actions: { self.actionsForProfile(accountID: self.accountID, sourceView: self.avatarImageView) } actions: { self.actionsForProfile(accountID: self.accountID, sourceView: self.avatarImageView) }
) )
} }
} }

View File

@ -11,13 +11,13 @@ import Pachyderm
import Gifu import Gifu
import AVFoundation import AVFoundation
protocol AttachmentViewDelegate { protocol AttachmentViewDelegate: class {
func showAttachmentsGallery(startingAt index: Int) func showAttachmentsGallery(startingAt index: Int)
} }
class AttachmentView: UIImageView, GIFAnimatable { class AttachmentView: UIImageView, GIFAnimatable {
var delegate: AttachmentViewDelegate? weak var delegate: AttachmentViewDelegate?
var playImageView: UIImageView! var playImageView: UIImageView!
@ -71,8 +71,8 @@ class AttachmentView: UIImageView, GIFAnimatable {
} }
func loadImage() { func loadImage() {
ImageCache.attachments.get(attachment.url) { (data) in ImageCache.attachments.get(attachment.url) { [weak self] (data) in
guard let data = data else { return } guard let self = self, let data = data else { return }
DispatchQueue.main.async { DispatchQueue.main.async {
if self.attachment.url.pathExtension == "gif" { if self.attachment.url.pathExtension == "gif" {
self.animate(withGIFData: data) self.animate(withGIFData: data)

View File

@ -11,7 +11,7 @@ import Pachyderm
class AttachmentsContainerView: UIView { class AttachmentsContainerView: UIView {
var delegate: AttachmentViewDelegate? weak var delegate: AttachmentViewDelegate?
var statusID: String! var statusID: String!
var attachments: [Attachment]! var attachments: [Attachment]!
@ -37,8 +37,6 @@ class AttachmentsContainerView: UIView {
createBlurView() createBlurView()
createHideButton() createHideButton()
NotificationCenter.default.addObserver(self, selector: #selector(updateUIForPreferences), name: .preferencesChanged, object: nil)
} }
func getAttachmentView(for attachment: Attachment) -> AttachmentView? { func getAttachmentView(for attachment: Attachment) -> AttachmentView? {
@ -176,11 +174,7 @@ class AttachmentsContainerView: UIView {
self.isHidden = true self.isHidden = true
} }
updateUIForPreferences() contentHidden = Preferences.shared.blurAllMedia || status.sensitive
}
@objc func updateUIForPreferences() {
contentHidden = Preferences.shared.blurAllMedia || (MastodonCache.status(for: statusID)?.sensitive ?? false)
} }
private func createAttachmentView(index: Int) -> AttachmentView { private func createAttachmentView(index: Int) -> AttachmentView {

View File

@ -10,14 +10,14 @@ import UIKit
import Photos import Photos
import AVFoundation import AVFoundation
protocol ComposeMediaViewDelegate { protocol ComposeMediaViewDelegate: class {
func didRemoveMedia(_ mediaView: ComposeMediaView) func didRemoveMedia(_ mediaView: ComposeMediaView)
func descriptionTextViewDidChange(_ mediaView: ComposeMediaView) func descriptionTextViewDidChange(_ mediaView: ComposeMediaView)
} }
class ComposeMediaView: UIView { class ComposeMediaView: UIView {
var delegate: ComposeMediaViewDelegate? weak var delegate: ComposeMediaViewDelegate?
@IBOutlet weak var imageView: UIImageView! @IBOutlet weak var imageView: UIImageView!
@IBOutlet weak var descriptionTextView: UITextView! @IBOutlet weak var descriptionTextView: UITextView!

View File

@ -10,6 +10,8 @@ import UIKit
import Pachyderm import Pachyderm
class ComposeStatusReplyView: UIView { class ComposeStatusReplyView: UIView {
weak var mastodonController: MastodonController?
@IBOutlet weak var avatarImageView: UIImageView! @IBOutlet weak var avatarImageView: UIImageView!
@IBOutlet weak var displayNameLabel: UILabel! @IBOutlet weak var displayNameLabel: UILabel!
@ -34,6 +36,7 @@ class ComposeStatusReplyView: UIView {
func updateUI(for status: Status) { func updateUI(for status: Status) {
displayNameLabel.text = status.account.realDisplayName displayNameLabel.text = status.account.realDisplayName
usernameLabel.text = "@\(status.account.acct)" usernameLabel.text = "@\(status.account.acct)"
statusContentTextView.overrideMastodonController = mastodonController
statusContentTextView.statusID = status.id statusContentTextView.statusID = status.id
ImageCache.avatars.get(status.account.avatar) { (data) in ImageCache.avatars.get(status.account.avatar) { (data) in

View File

@ -15,9 +15,10 @@ private let emojiRegex = try! NSRegularExpression(pattern: ":(\\w+):", options:
class ContentTextView: LinkTextView { class ContentTextView: LinkTextView {
// todo: should be weak weak var navigationDelegate: TuskerNavigationDelegate?
var navigationDelegate: TuskerNavigationDelegate? weak var overrideMastodonController: MastodonController?
var mastodonController: MastodonController? { overrideMastodonController ?? navigationDelegate?.apiController }
var defaultFont: UIFont = .systemFont(ofSize: 17) var defaultFont: UIFont = .systemFont(ofSize: 17)
var defaultColor: UIColor = .label var defaultColor: UIColor = .label
@ -230,9 +231,9 @@ class ContentTextView: LinkTextView {
let text = (self.text as NSString).substring(with: range) let text = (self.text as NSString).substring(with: range)
if let mention = getMention(for: url, text: text) { if let mention = getMention(for: url, text: text) {
return ProfileTableViewController(accountID: mention.id) return ProfileTableViewController(accountID: mention.id, mastodonController: mastodonController!)
} else if let tag = getHashtag(for: url, text: text) { } else if let tag = getHashtag(for: url, text: text) {
return HashtagTimelineViewController(for: tag) return HashtagTimelineViewController(for: tag, mastodonController: mastodonController!)
} else { } else {
return SFSafariViewController(url: url) return SFSafariViewController(url: url)
} }

View File

@ -11,8 +11,8 @@ import Pachyderm
class HashtagTableViewCell: UITableViewCell { class HashtagTableViewCell: UITableViewCell {
var delegate: TuskerNavigationDelegate? weak var delegate: TuskerNavigationDelegate?
@IBOutlet weak var hashtagLabel: UILabel! @IBOutlet weak var hashtagLabel: UILabel!
var hashtag: Hashtag! var hashtag: Hashtag!

View File

@ -12,7 +12,8 @@ import SwiftSoup
class ActionNotificationGroupTableViewCell: UITableViewCell { class ActionNotificationGroupTableViewCell: UITableViewCell {
var delegate: TuskerNavigationDelegate? weak var delegate: TuskerNavigationDelegate?
var mastodonController: MastodonController! { delegate?.apiController }
@IBOutlet weak var actionImageView: UIImageView! @IBOutlet weak var actionImageView: UIImageView!
@IBOutlet weak var actionAvatarStackView: UIStackView! @IBOutlet weak var actionAvatarStackView: UIStackView!
@ -26,6 +27,10 @@ class ActionNotificationGroupTableViewCell: UITableViewCell {
var authorAvatarURL: URL? var authorAvatarURL: URL?
var updateTimestampWorkItem: DispatchWorkItem? var updateTimestampWorkItem: DispatchWorkItem?
deinit {
updateTimestampWorkItem?.cancel()
}
override func awakeFromNib() { override func awakeFromNib() {
super.awakeFromNib() super.awakeFromNib()
@ -33,7 +38,7 @@ class ActionNotificationGroupTableViewCell: UITableViewCell {
} }
@objc func updateUIForPreferences() { @objc func updateUIForPreferences() {
let people = group.notificationIDs.compactMap(MastodonCache.notification(for:)).map { $0.account } let people = group.notificationIDs.compactMap(mastodonController.cache.notification(for:)).map { $0.account }
updateActionLabel(people: people) updateActionLabel(people: people)
for case let imageView as UIImageView in actionAvatarStackView.arrangedSubviews { for case let imageView as UIImageView in actionAvatarStackView.arrangedSubviews {
@ -47,7 +52,7 @@ class ActionNotificationGroupTableViewCell: UITableViewCell {
} }
self.group = group self.group = group
guard let firstNotification = MastodonCache.notification(for: group.notificationIDs.first!) else { fatalError() } guard let firstNotification = mastodonController.cache.notification(for: group.notificationIDs.first!) else { fatalError() }
let status = firstNotification.status! let status = firstNotification.status!
self.statusID = status.id self.statusID = status.id
@ -62,7 +67,7 @@ class ActionNotificationGroupTableViewCell: UITableViewCell {
fatalError() fatalError()
} }
let people = group.notificationIDs.compactMap(MastodonCache.notification(for:)).map { $0.account } let people = group.notificationIDs.compactMap(mastodonController.cache.notification(for:)).map { $0.account }
actionAvatarStackView.arrangedSubviews.forEach { $0.removeFromSuperview() } actionAvatarStackView.arrangedSubviews.forEach { $0.removeFromSuperview() }
for account in people { for account in people {
@ -93,7 +98,7 @@ class ActionNotificationGroupTableViewCell: UITableViewCell {
func updateTimestamp() { func updateTimestamp() {
guard let id = group.notificationIDs.first, guard let id = group.notificationIDs.first,
let notification = MastodonCache.notification(for: id) else { let notification = mastodonController.cache.notification(for: id) else {
fatalError("Missing cached notification") fatalError("Missing cached notification")
} }
@ -109,7 +114,9 @@ class ActionNotificationGroupTableViewCell: UITableViewCell {
delay = nil delay = nil
} }
if let delay = delay { if let delay = delay {
updateTimestampWorkItem = DispatchWorkItem(block: self.updateTimestamp) updateTimestampWorkItem = DispatchWorkItem { [unowned self] in
self.updateTimestamp()
}
DispatchQueue.main.asyncAfter(deadline: .now() + delay, execute: updateTimestampWorkItem!) DispatchQueue.main.asyncAfter(deadline: .now() + delay, execute: updateTimestampWorkItem!)
} else { } else {
updateTimestampWorkItem = nil updateTimestampWorkItem = nil
@ -155,7 +162,7 @@ class ActionNotificationGroupTableViewCell: UITableViewCell {
extension ActionNotificationGroupTableViewCell: SelectableTableViewCell { extension ActionNotificationGroupTableViewCell: SelectableTableViewCell {
func didSelectCell() { func didSelectCell() {
guard let delegate = delegate else { return } guard let delegate = delegate else { return }
let notifications = group.notificationIDs.compactMap(MastodonCache.notification(for:)) let notifications = group.notificationIDs.compactMap(mastodonController.cache.notification(for:))
let accountIDs = notifications.map { $0.account.id } let accountIDs = notifications.map { $0.account.id }
let action: StatusActionAccountListTableViewController.ActionType let action: StatusActionAccountListTableViewController.ActionType
switch notifications.first!.kind { switch notifications.first!.kind {
@ -176,7 +183,7 @@ extension ActionNotificationGroupTableViewCell: MenuPreviewProvider {
func getPreviewProviders(for location: CGPoint, sourceViewController: UIViewController) -> PreviewProviders? { func getPreviewProviders(for location: CGPoint, sourceViewController: UIViewController) -> PreviewProviders? {
return (content: { return (content: {
let notifications = self.group.notificationIDs.compactMap(MastodonCache.notification(for:)) let notifications = self.group.notificationIDs.compactMap(self.mastodonController.cache.notification(for:))
let accountIDs = notifications.map { $0.account.id } let accountIDs = notifications.map { $0.account.id }
let action: StatusActionAccountListTableViewController.ActionType let action: StatusActionAccountListTableViewController.ActionType
switch notifications.first!.kind { switch notifications.first!.kind {

View File

@ -11,7 +11,8 @@ import Pachyderm
class FollowNotificationGroupTableViewCell: UITableViewCell { class FollowNotificationGroupTableViewCell: UITableViewCell {
var delegate: TuskerNavigationDelegate? weak var delegate: TuskerNavigationDelegate?
var mastodonController: MastodonController! { delegate?.apiController }
@IBOutlet weak var avatarStackView: UIStackView! @IBOutlet weak var avatarStackView: UIStackView!
@IBOutlet weak var timestampLabel: UILabel! @IBOutlet weak var timestampLabel: UILabel!
@ -21,6 +22,10 @@ class FollowNotificationGroupTableViewCell: UITableViewCell {
var updateTimestampWorkItem: DispatchWorkItem? var updateTimestampWorkItem: DispatchWorkItem?
deinit {
updateTimestampWorkItem?.cancel()
}
override func awakeFromNib() { override func awakeFromNib() {
super.awakeFromNib() super.awakeFromNib()
@ -28,7 +33,7 @@ class FollowNotificationGroupTableViewCell: UITableViewCell {
} }
@objc func updateUIForPreferences() { @objc func updateUIForPreferences() {
let people = group.notificationIDs.compactMap(MastodonCache.notification(for:)).map { $0.account } let people = group.notificationIDs.compactMap(mastodonController.cache.notification(for:)).map { $0.account }
updateActionLabel(people: people) updateActionLabel(people: people)
for case let imageView as UIImageView in avatarStackView.arrangedSubviews { for case let imageView as UIImageView in avatarStackView.arrangedSubviews {
@ -39,7 +44,7 @@ class FollowNotificationGroupTableViewCell: UITableViewCell {
func updateUI(group: NotificationGroup) { func updateUI(group: NotificationGroup) {
self.group = group self.group = group
let people = group.notificationIDs.compactMap(MastodonCache.notification(for:)).map { $0.account } let people = group.notificationIDs.compactMap(mastodonController.cache.notification(for:)).map { $0.account }
updateActionLabel(people: people) updateActionLabel(people: people)
updateTimestamp() updateTimestamp()
@ -81,7 +86,7 @@ class FollowNotificationGroupTableViewCell: UITableViewCell {
func updateTimestamp() { func updateTimestamp() {
guard let id = group.notificationIDs.first, guard let id = group.notificationIDs.first,
let notification = MastodonCache.notification(for: id) else { let notification = mastodonController.cache.notification(for: id) else {
fatalError("Missing cached notification") fatalError("Missing cached notification")
} }
@ -97,7 +102,7 @@ class FollowNotificationGroupTableViewCell: UITableViewCell {
delay = nil delay = nil
} }
if let delay = delay { if let delay = delay {
updateTimestampWorkItem = DispatchWorkItem { updateTimestampWorkItem = DispatchWorkItem { [unowned self] in
self.updateTimestamp() self.updateTimestamp()
} }
DispatchQueue.main.asyncAfter(deadline: .now() + delay, execute: updateTimestampWorkItem!) DispatchQueue.main.asyncAfter(deadline: .now() + delay, execute: updateTimestampWorkItem!)
@ -117,7 +122,7 @@ class FollowNotificationGroupTableViewCell: UITableViewCell {
extension FollowNotificationGroupTableViewCell: SelectableTableViewCell { extension FollowNotificationGroupTableViewCell: SelectableTableViewCell {
func didSelectCell() { func didSelectCell() {
let people = group.notificationIDs.compactMap(MastodonCache.notification(for:)).map { $0.account.id } let people = group.notificationIDs.compactMap(mastodonController.cache.notification(for:)).map { $0.account.id }
switch people.count { switch people.count {
case 0: case 0:
return return
@ -133,12 +138,13 @@ extension FollowNotificationGroupTableViewCell: MenuPreviewProvider {
var navigationDelegate: TuskerNavigationDelegate? { return delegate } var navigationDelegate: TuskerNavigationDelegate? { return delegate }
func getPreviewProviders(for location: CGPoint, sourceViewController: UIViewController) -> PreviewProviders? { func getPreviewProviders(for location: CGPoint, sourceViewController: UIViewController) -> PreviewProviders? {
guard let mastodonController = mastodonController else { return nil }
return (content: { return (content: {
let accountIDs = self.group.notificationIDs.compactMap(MastodonCache.notification(for:)).map { $0.account.id } let accountIDs = self.group.notificationIDs.compactMap(mastodonController.cache.notification(for:)).map { $0.account.id }
if accountIDs.count == 1 { if accountIDs.count == 1 {
return ProfileTableViewController(accountID: accountIDs.first!) return ProfileTableViewController(accountID: accountIDs.first!, mastodonController: mastodonController)
} else { } else {
return AccountListTableViewController(accountIDs: accountIDs) return AccountListTableViewController(accountIDs: accountIDs, mastodonController: mastodonController)
} }
}, actions: { }, actions: {
return [] return []

View File

@ -11,7 +11,8 @@ import Pachyderm
class FollowRequestNotificationTableViewCell: UITableViewCell { class FollowRequestNotificationTableViewCell: UITableViewCell {
var delegate: TuskerNavigationDelegate? weak var delegate: TuskerNavigationDelegate?
var mastodonController: MastodonController! { delegate?.apiController }
@IBOutlet weak var stackView: UIStackView! @IBOutlet weak var stackView: UIStackView!
@IBOutlet weak var avatarImageView: UIImageView! @IBOutlet weak var avatarImageView: UIImageView!
@ -26,6 +27,10 @@ class FollowRequestNotificationTableViewCell: UITableViewCell {
var updateTimestampWorkItem: DispatchWorkItem? var updateTimestampWorkItem: DispatchWorkItem?
deinit {
updateTimestampWorkItem?.cancel()
}
override func awakeFromNib() { override func awakeFromNib() {
super.awakeFromNib() super.awakeFromNib()
@ -71,7 +76,9 @@ class FollowRequestNotificationTableViewCell: UITableViewCell {
delay = nil delay = nil
} }
if let delay = delay { if let delay = delay {
updateTimestampWorkItem = DispatchWorkItem(block: self.updateTimestamp) updateTimestampWorkItem = DispatchWorkItem { [unowned self] in
self.updateTimestamp()
}
DispatchQueue.main.asyncAfter(deadline: .now() + delay, execute: updateTimestampWorkItem!) DispatchQueue.main.asyncAfter(deadline: .now() + delay, execute: updateTimestampWorkItem!)
} else { } else {
updateTimestampWorkItem = nil updateTimestampWorkItem = nil
@ -89,9 +96,9 @@ class FollowRequestNotificationTableViewCell: UITableViewCell {
@IBAction func rejectButtonPressed() { @IBAction func rejectButtonPressed() {
let request = Account.rejectFollowRequest(account) let request = Account.rejectFollowRequest(account)
MastodonController.client.run(request) { (response) in mastodonController.run(request) { (response) in
guard case let .success(relationship, _) = response else { fatalError() } guard case let .success(relationship, _) = response else { fatalError() }
MastodonCache.add(relationship: relationship) self.mastodonController.cache.add(relationship: relationship)
DispatchQueue.main.async { DispatchQueue.main.async {
UINotificationFeedbackGenerator().notificationOccurred(.success) UINotificationFeedbackGenerator().notificationOccurred(.success)
self.actionButtonsStackView.isHidden = true self.actionButtonsStackView.isHidden = true
@ -106,9 +113,9 @@ class FollowRequestNotificationTableViewCell: UITableViewCell {
@IBAction func acceptButtonPressed() { @IBAction func acceptButtonPressed() {
let request = Account.authorizeFollowRequest(account) let request = Account.authorizeFollowRequest(account)
MastodonController.client.run(request) { (response) in mastodonController.run(request) { (response) in
guard case let .success(relationship, _) = response else { fatalError() } guard case let .success(relationship, _) = response else { fatalError() }
MastodonCache.add(relationship: relationship) self.mastodonController.cache.add(relationship: relationship)
DispatchQueue.main.async { DispatchQueue.main.async {
UINotificationFeedbackGenerator().notificationOccurred(.success) UINotificationFeedbackGenerator().notificationOccurred(.success)
self.actionButtonsStackView.isHidden = true self.actionButtonsStackView.isHidden = true
@ -133,8 +140,9 @@ extension FollowRequestNotificationTableViewCell: MenuPreviewProvider {
var navigationDelegate: TuskerNavigationDelegate? { return delegate } var navigationDelegate: TuskerNavigationDelegate? { return delegate }
func getPreviewProviders(for location: CGPoint, sourceViewController: UIViewController) -> PreviewProviders? { func getPreviewProviders(for location: CGPoint, sourceViewController: UIViewController) -> PreviewProviders? {
guard let mastodonController = mastodonController else { return nil }
return (content: { return (content: {
return ProfileTableViewController(accountID: self.account.id) return ProfileTableViewController(accountID: self.account.id, mastodonController: mastodonController)
}, actions: { }, actions: {
return [] return []
}) })

View File

@ -15,7 +15,8 @@ protocol ProfileHeaderTableViewCellDelegate: TuskerNavigationDelegate {
class ProfileHeaderTableViewCell: UITableViewCell { class ProfileHeaderTableViewCell: UITableViewCell {
var delegate: ProfileHeaderTableViewCellDelegate? weak var delegate: ProfileHeaderTableViewCellDelegate?
var mastodonController: MastodonController! { delegate?.apiController }
@IBOutlet weak var headerImageView: UIImageView! @IBOutlet weak var headerImageView: UIImageView!
@IBOutlet weak var avatarContainerView: UIView! @IBOutlet weak var avatarContainerView: UIView!
@ -55,7 +56,7 @@ class ProfileHeaderTableViewCell: UITableViewCell {
guard accountID != self.accountID else { return } guard accountID != self.accountID else { return }
self.accountID = accountID self.accountID = accountID
guard let account = MastodonCache.account(for: accountID) else { fatalError("Missing cached account \(accountID)") } guard let account = mastodonController.cache.account(for: accountID) else { fatalError("Missing cached account \(accountID)") }
updateUIForPreferences() updateUIForPreferences()
@ -82,12 +83,12 @@ class ProfileHeaderTableViewCell: UITableViewCell {
noteTextView.setTextFromHtml(account.note) noteTextView.setTextFromHtml(account.note)
noteTextView.setEmojis(account.emojis) noteTextView.setEmojis(account.emojis)
if accountID != MastodonController.account.id { if accountID != mastodonController.account.id {
// don't show relationship label for the user's own account // don't show relationship label for the user's own account
if let relationship = MastodonCache.relationship(for: accountID) { if let relationship = mastodonController.cache.relationship(for: accountID) {
followsYouLabel.isHidden = !relationship.followedBy followsYouLabel.isHidden = !relationship.followedBy
} else { } else {
MastodonCache.relationship(for: accountID) { relationship in mastodonController.cache.relationship(for: accountID) { relationship in
DispatchQueue.main.async { DispatchQueue.main.async {
self.followsYouLabel.isHidden = !(relationship?.followedBy ?? false) self.followsYouLabel.isHidden = !(relationship?.followedBy ?? false)
} }
@ -122,7 +123,7 @@ class ProfileHeaderTableViewCell: UITableViewCell {
} }
@objc func updateUIForPreferences() { @objc func updateUIForPreferences() {
guard let account = MastodonCache.account(for: accountID) else { fatalError("Missing cached account \(accountID!)") } guard let account = mastodonController.cache.account(for: accountID) else { fatalError("Missing cached account \(accountID!)") }
avatarContainerView.layer.cornerRadius = Preferences.shared.avatarStyle.cornerRadius(for: avatarContainerView) avatarContainerView.layer.cornerRadius = Preferences.shared.avatarStyle.cornerRadius(for: avatarContainerView)
avatarImageView.layer.cornerRadius = Preferences.shared.avatarStyle.cornerRadius(for: avatarImageView) avatarImageView.layer.cornerRadius = Preferences.shared.avatarStyle.cornerRadius(for: avatarImageView)

View File

@ -15,11 +15,14 @@ protocol StatusTableViewCellDelegate: TuskerNavigationDelegate {
} }
class BaseStatusTableViewCell: UITableViewCell { class BaseStatusTableViewCell: UITableViewCell {
var delegate: StatusTableViewCellDelegate? {
weak var delegate: StatusTableViewCellDelegate? {
didSet { didSet {
contentTextView.navigationDelegate = delegate contentTextView.navigationDelegate = delegate
} }
} }
var overrideMastodonController: MastodonController?
var mastodonController: MastodonController! { overrideMastodonController ?? delegate?.apiController }
@IBOutlet weak var avatarImageView: UIImageView! @IBOutlet weak var avatarImageView: UIImageView!
@IBOutlet weak var displayNameLabel: UILabel! @IBOutlet weak var displayNameLabel: UILabel!
@ -92,20 +95,28 @@ class BaseStatusTableViewCell: UITableViewCell {
attachmentsView.isAccessibilityElement = true attachmentsView.isAccessibilityElement = true
NotificationCenter.default.addObserver(self, selector: #selector(updateUIForPreferences), name: .preferencesChanged, object: nil) NotificationCenter.default.addObserver(self, selector: #selector(updateUIForPreferences), name: .preferencesChanged, object: nil)
}
open func createObserversIfNecessary() {
if statusUpdater == nil {
statusUpdater = mastodonController.cache.statusSubject
.filter { [unowned self] in $0.id == self.statusID }
.receive(on: DispatchQueue.main)
.sink { [unowned self] in self.updateStatusState(status: $0) }
}
statusUpdater = MastodonCache.statusSubject if accountUpdater == nil {
.filter { $0.id == self.statusID } accountUpdater = mastodonController.cache.accountSubject
.receive(on: DispatchQueue.main) .filter { [unowned self] in $0.id == self.accountID }
.sink(receiveValue: updateStatusState(status:)) .receive(on: DispatchQueue.main)
.sink { [unowned self] in self.updateUI(account: $0) }
accountUpdater = MastodonCache.accountSubject }
.filter { $0.id == self.accountID }
.receive(on: DispatchQueue.main)
.sink(receiveValue: updateUI(account:))
} }
func updateUI(statusID: String, state: StatusState) { func updateUI(statusID: String, state: StatusState) {
guard let status = MastodonCache.status(for: statusID) else { createObserversIfNecessary()
guard let status = mastodonController.cache.status(for: statusID) else {
fatalError("Missing cached status") fatalError("Missing cached status")
} }
self.statusID = statusID self.statusID = statusID
@ -180,9 +191,10 @@ class BaseStatusTableViewCell: UITableViewCell {
} }
@objc func updateUIForPreferences() { @objc func updateUIForPreferences() {
guard let account = MastodonCache.account(for: accountID) else { return } guard let account = mastodonController.cache.account(for: accountID) else { return }
avatarImageView.layer.cornerRadius = Preferences.shared.avatarStyle.cornerRadius(for: avatarImageView) avatarImageView.layer.cornerRadius = Preferences.shared.avatarStyle.cornerRadius(for: avatarImageView)
displayNameLabel.text = account.realDisplayName displayNameLabel.text = account.realDisplayName
attachmentsView.contentHidden = Preferences.shared.blurAllMedia || (mastodonController.cache.status(for: statusID)?.sensitive ?? false)
} }
override func prepareForReuse() { override func prepareForReuse() {
@ -240,18 +252,18 @@ class BaseStatusTableViewCell: UITableViewCell {
} }
@IBAction func favoritePressed() { @IBAction func favoritePressed() {
guard let status = MastodonCache.status(for: statusID) else { fatalError("Missing cached status \(statusID!)") } guard let status = mastodonController.cache.status(for: statusID) else { fatalError("Missing cached status \(statusID!)") }
let oldValue = favorited let oldValue = favorited
favorited = !favorited favorited = !favorited
let realStatus: Status = status.reblog ?? status let realStatus: Status = status.reblog ?? status
let request = (favorited ? Status.favourite : Status.unfavourite)(realStatus) let request = (favorited ? Status.favourite : Status.unfavourite)(realStatus)
MastodonController.client.run(request) { response in mastodonController.run(request) { response in
DispatchQueue.main.async { DispatchQueue.main.async {
if case let .success(newStatus, _) = response { if case let .success(newStatus, _) = response {
self.favorited = newStatus.favourited ?? false self.favorited = newStatus.favourited ?? false
MastodonCache.add(status: newStatus) self.mastodonController.cache.add(status: newStatus)
UIImpactFeedbackGenerator(style: .light).impactOccurred() UIImpactFeedbackGenerator(style: .light).impactOccurred()
} else { } else {
self.favorited = oldValue self.favorited = oldValue
@ -265,18 +277,18 @@ class BaseStatusTableViewCell: UITableViewCell {
} }
@IBAction func reblogPressed() { @IBAction func reblogPressed() {
guard let status = MastodonCache.status(for: statusID) else { fatalError("Missing cached status \(statusID!)") } guard let status = mastodonController.cache.status(for: statusID) else { fatalError("Missing cached status \(statusID!)") }
let oldValue = reblogged let oldValue = reblogged
reblogged = !reblogged reblogged = !reblogged
let realStatus: Status = status.reblog ?? status let realStatus: Status = status.reblog ?? status
let request = (reblogged ? Status.reblog : Status.unreblog)(realStatus) let request = (reblogged ? Status.reblog : Status.unreblog)(realStatus)
MastodonController.client.run(request) { response in mastodonController.run(request) { response in
DispatchQueue.main.async { DispatchQueue.main.async {
if case let .success(newStatus, _) = response { if case let .success(newStatus, _) = response {
self.reblogged = newStatus.reblogged ?? false self.reblogged = newStatus.reblogged ?? false
MastodonCache.add(status: newStatus) self.mastodonController.cache.add(status: newStatus)
UIImpactFeedbackGenerator(style: .light).impactOccurred() UIImpactFeedbackGenerator(style: .light).impactOccurred()
} else { } else {
self.reblogged = oldValue self.reblogged = oldValue
@ -303,7 +315,7 @@ class BaseStatusTableViewCell: UITableViewCell {
extension BaseStatusTableViewCell: AttachmentViewDelegate { extension BaseStatusTableViewCell: AttachmentViewDelegate {
func showAttachmentsGallery(startingAt index: Int) { func showAttachmentsGallery(startingAt index: Int) {
guard let status = MastodonCache.status(for: statusID) else { fatalError("Missing cached status \(statusID!)") } guard let status = mastodonController.cache.status(for: statusID) else { fatalError("Missing cached status \(statusID!)") }
let sourceViews = status.attachments.map(attachmentsView.getAttachmentView(for:)) let sourceViews = status.attachments.map(attachmentsView.getAttachmentView(for:))
delegate?.showGallery(attachments: status.attachments, sourceViews: sourceViews, startIndex: index) delegate?.showGallery(attachments: status.attachments, sourceViews: sourceViews, startIndex: index)
} }
@ -313,9 +325,10 @@ extension BaseStatusTableViewCell: MenuPreviewProvider {
var navigationDelegate: TuskerNavigationDelegate? { return delegate } var navigationDelegate: TuskerNavigationDelegate? { return delegate }
func getPreviewProviders(for location: CGPoint, sourceViewController: UIViewController) -> PreviewProviders? { func getPreviewProviders(for location: CGPoint, sourceViewController: UIViewController) -> PreviewProviders? {
guard let mastodonController = mastodonController else { return nil }
if avatarImageView.frame.contains(location) { if avatarImageView.frame.contains(location) {
return ( return (
content: { ProfileTableViewController(accountID: self.accountID)}, content: { ProfileTableViewController(accountID: self.accountID, mastodonController: mastodonController) },
actions: { self.actionsForProfile(accountID: self.accountID, sourceView: self.avatarImageView) } actions: { self.actionsForProfile(accountID: self.accountID, sourceView: self.avatarImageView) }
) )
} else if attachmentsView.frame.contains(location) { } else if attachmentsView.frame.contains(location) {

View File

@ -40,7 +40,7 @@ class ConversationMainStatusTableViewCell: BaseStatusTableViewCell {
override func updateUI(statusID: String, state: StatusState) { override func updateUI(statusID: String, state: StatusState) {
super.updateUI(statusID: statusID, state: state) super.updateUI(statusID: statusID, state: state)
guard let status = MastodonCache.status(for: statusID) else { fatalError() } guard let status = mastodonController.cache.status(for: statusID) else { fatalError() }
var timestampAndClientText = ConversationMainStatusTableViewCell.dateFormatter.string(from: status.createdAt) var timestampAndClientText = ConversationMainStatusTableViewCell.dateFormatter.string(from: status.createdAt)
if let application = status.application { if let application = status.application {

View File

@ -34,6 +34,7 @@ class TimelineStatusTableViewCell: BaseStatusTableViewCell {
deinit { deinit {
rebloggerAccountUpdater?.cancel() rebloggerAccountUpdater?.cancel()
updateTimestampWorkItem?.cancel()
} }
override func awakeFromNib() { override func awakeFromNib() {
@ -41,19 +42,25 @@ class TimelineStatusTableViewCell: BaseStatusTableViewCell {
reblogLabel.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(reblogLabelPressed))) reblogLabel.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(reblogLabelPressed)))
accessibilityElements!.insert(reblogLabel!, at: 0) accessibilityElements!.insert(reblogLabel!, at: 0)
rebloggerAccountUpdater = MastodonCache.accountSubject
.filter { $0.id == self.rebloggerID }
.receive(on: DispatchQueue.main)
.sink(receiveValue: updateRebloggerLabel(reblogger:))
} }
override func createObserversIfNecessary() {
super.createObserversIfNecessary()
if rebloggerAccountUpdater == nil {
rebloggerAccountUpdater = mastodonController.cache.accountSubject
.filter { [unowned self] in $0.id == self.rebloggerID }
.receive(on: DispatchQueue.main)
.sink { [unowned self] in self.updateRebloggerLabel(reblogger: $0) }
}
}
override func updateUI(statusID: String, state: StatusState) { override func updateUI(statusID: String, state: StatusState) {
guard var status = MastodonCache.status(for: statusID) else { fatalError("Missing cached status \(statusID)") } guard var status = mastodonController.cache.status(for: statusID) else { fatalError("Missing cached status \(statusID)") }
let realStatusID: String let realStatusID: String
if let rebloggedStatusID = status.reblog?.id, if let rebloggedStatusID = status.reblog?.id,
let rebloggedStatus = MastodonCache.status(for: rebloggedStatusID) { let rebloggedStatus = mastodonController.cache.status(for: rebloggedStatusID) {
reblogStatusID = statusID reblogStatusID = statusID
rebloggerID = status.account.id rebloggerID = status.account.id
status = rebloggedStatus status = rebloggedStatus
@ -78,7 +85,7 @@ class TimelineStatusTableViewCell: BaseStatusTableViewCell {
@objc override func updateUIForPreferences() { @objc override func updateUIForPreferences() {
super.updateUIForPreferences() super.updateUIForPreferences()
if let rebloggerID = rebloggerID, if let rebloggerID = rebloggerID,
let reblogger = MastodonCache.account(for: rebloggerID) { let reblogger = mastodonController.cache.account(for: rebloggerID) {
updateRebloggerLabel(reblogger: reblogger) updateRebloggerLabel(reblogger: reblogger)
} }
} }
@ -88,7 +95,7 @@ class TimelineStatusTableViewCell: BaseStatusTableViewCell {
} }
func updateTimestamp() { func updateTimestamp() {
guard let status = MastodonCache.status(for: statusID) else { fatalError("Missing cached status \(statusID!)") } guard let status = mastodonController.cache.status(for: statusID) else { fatalError("Missing cached status \(statusID!)") }
timestampLabel.text = status.createdAt.timeAgoString() timestampLabel.text = status.createdAt.timeAgoString()
timestampLabel.accessibilityLabel = TimelineStatusTableViewCell.relativeDateFormatter.localizedString(for: status.createdAt, relativeTo: Date()) timestampLabel.accessibilityLabel = TimelineStatusTableViewCell.relativeDateFormatter.localizedString(for: status.createdAt, relativeTo: Date())
@ -103,7 +110,7 @@ class TimelineStatusTableViewCell: BaseStatusTableViewCell {
delay = nil delay = nil
} }
if let delay = delay { if let delay = delay {
updateTimestampWorkItem = DispatchWorkItem { updateTimestampWorkItem = DispatchWorkItem { [unowned self] in
self.updateTimestamp() self.updateTimestamp()
} }
DispatchQueue.main.asyncAfter(deadline: .now() + delay, execute: updateTimestampWorkItem!) DispatchQueue.main.asyncAfter(deadline: .now() + delay, execute: updateTimestampWorkItem!)
@ -115,7 +122,7 @@ class TimelineStatusTableViewCell: BaseStatusTableViewCell {
func reply() { func reply() {
if Preferences.shared.mentionReblogger, if Preferences.shared.mentionReblogger,
let rebloggerID = rebloggerID, let rebloggerID = rebloggerID,
let rebloggerAccount = MastodonCache.account(for: rebloggerID) { let rebloggerAccount = mastodonController.cache.account(for: rebloggerID) {
delegate?.reply(to: statusID, mentioningAcct: rebloggerAccount.acct) delegate?.reply(to: statusID, mentioningAcct: rebloggerAccount.acct)
} else { } else {
delegate?.reply(to: statusID) delegate?.reply(to: statusID)
@ -139,8 +146,9 @@ class TimelineStatusTableViewCell: BaseStatusTableViewCell {
} }
override func getStatusCellPreviewProviders(for location: CGPoint, sourceViewController: UIViewController) -> BaseStatusTableViewCell.PreviewProviders? { override func getStatusCellPreviewProviders(for location: CGPoint, sourceViewController: UIViewController) -> BaseStatusTableViewCell.PreviewProviders? {
guard let mastodonController = mastodonController else { return nil }
return ( return (
content: { ConversationTableViewController(for: self.statusID, state: self.statusState.copy()) }, content: { ConversationTableViewController(for: self.statusID, state: self.statusState.copy(), mastodonController: mastodonController) },
actions: { self.actionsForStatus(statusID: self.statusID, sourceView: self) } actions: { self.actionsForStatus(statusID: self.statusID, sourceView: self) }
) )
} }
@ -156,7 +164,8 @@ extension TimelineStatusTableViewCell: SelectableTableViewCell {
extension TimelineStatusTableViewCell: TableViewSwipeActionProvider { extension TimelineStatusTableViewCell: TableViewSwipeActionProvider {
func leadingSwipeActionsConfiguration() -> UISwipeActionsConfiguration? { func leadingSwipeActionsConfiguration() -> UISwipeActionsConfiguration? {
guard let status = MastodonCache.status(for: statusID) else { fatalError("Missing cached status \(statusID!)") } guard let mastodonController = mastodonController else { return nil }
guard let status = mastodonController.cache.status(for: statusID) else { fatalError("Missing cached status \(statusID!)") }
let favoriteTitle: String let favoriteTitle: String
let favoriteRequest: Request<Status> let favoriteRequest: Request<Status>
@ -172,14 +181,14 @@ extension TimelineStatusTableViewCell: TableViewSwipeActionProvider {
favoriteColor = UIColor(displayP3Red: 1, green: 204/255, blue: 0, alpha: 1) favoriteColor = UIColor(displayP3Red: 1, green: 204/255, blue: 0, alpha: 1)
} }
let favorite = UIContextualAction(style: .normal, title: favoriteTitle) { (action, view, completion) in let favorite = UIContextualAction(style: .normal, title: favoriteTitle) { (action, view, completion) in
MastodonController.client.run(favoriteRequest, completion: { response in mastodonController.run(favoriteRequest, completion: { response in
DispatchQueue.main.async { DispatchQueue.main.async {
guard case let .success(status, _) = response else { guard case let .success(status, _) = response else {
completion(false) completion(false)
return return
} }
completion(true) completion(true)
MastodonCache.add(status: status) mastodonController.cache.add(status: status)
} }
}) })
} }
@ -199,14 +208,14 @@ extension TimelineStatusTableViewCell: TableViewSwipeActionProvider {
reblogColor = tintColor reblogColor = tintColor
} }
let reblog = UIContextualAction(style: .normal, title: reblogTitle) { (action, view, completion) in let reblog = UIContextualAction(style: .normal, title: reblogTitle) { (action, view, completion) in
MastodonController.client.run(reblogRequest, completion: { response in mastodonController.run(reblogRequest, completion: { response in
DispatchQueue.main.async { DispatchQueue.main.async {
guard case let .success(status, _) = response else { guard case let .success(status, _) = response else {
completion(false) completion(false)
return return
} }
completion(true) completion(true)
MastodonCache.add(status: status) mastodonController.cache.add(status: status)
} }
}) })
} }

View File

@ -14,7 +14,8 @@ class StatusContentTextView: ContentTextView {
var statusID: String? { var statusID: String? {
didSet { didSet {
guard let statusID = statusID else { return } guard let statusID = statusID else { return }
guard let status = MastodonCache.status(for: statusID) else { guard let mastodonController = mastodonController,
let status = mastodonController.cache.status(for: statusID) else {
fatalError("Can't set StatusContentTextView text without cached status for \(statusID)") fatalError("Can't set StatusContentTextView text without cached status for \(statusID)")
} }
setTextFromHtml(status.content) setTextFromHtml(status.content)
@ -25,7 +26,8 @@ class StatusContentTextView: ContentTextView {
override func getMention(for url: URL, text: String) -> Mention? { override func getMention(for url: URL, text: String) -> Mention? {
let mention: Mention? let mention: Mention?
if let statusID = statusID, if let statusID = statusID,
let status = MastodonCache.status(for: statusID) { let mastodonController = mastodonController,
let status = mastodonController.cache.status(for: statusID) {
mention = status.mentions.first { (mention) in mention = status.mentions.first { (mention) in
// Mastodon and Pleroma include the @ in the <a> text, GNU Social does not // Mastodon and Pleroma include the @ in the <a> text, GNU Social does not
(text.dropFirst() == mention.username || text == mention.username) && url.host == mention.url.host (text.dropFirst() == mention.username || text == mention.username) && url.host == mention.url.host
@ -39,7 +41,8 @@ class StatusContentTextView: ContentTextView {
override func getHashtag(for url: URL, text: String) -> Hashtag? { override func getHashtag(for url: URL, text: String) -> Hashtag? {
let hashtag: Hashtag? let hashtag: Hashtag?
if let statusID = statusID, if let statusID = statusID,
let status = MastodonCache.status(for: statusID) { let mastodonController = mastodonController,
let status = mastodonController.cache.status(for: statusID) {
hashtag = status.hashtags.first { (hashtag) in hashtag = status.hashtags.first { (hashtag) in
hashtag.url == url hashtag.url == url
} }

View File

@ -13,8 +13,15 @@ import SwiftSoup
struct XCBActions { struct XCBActions {
// MARK: - Utils // MARK: - Utils
private static var mastodonController: MastodonController {
let scene = UIApplication.shared.activeOrBackgroundScene!
return scene.session.mastodonController!
}
private static func getMainTabBarController() -> MainTabBarViewController { private static func getMainTabBarController() -> MainTabBarViewController {
return (UIApplication.shared.delegate as! AppDelegate).window!.rootViewController as! MainTabBarViewController let scene = UIApplication.shared.connectedScenes.compactMap { $0 as? UIWindowScene }.first!
let window = scene.windows.first { $0.isKeyWindow }!
return window.rootViewController as! MainTabBarViewController
} }
private static func show(_ vc: UIViewController) { private static func show(_ vc: UIViewController) {
@ -31,7 +38,7 @@ struct XCBActions {
private static func getStatus(from request: XCBRequest, session: XCBSession, completion: @escaping (Status) -> Void) { private static func getStatus(from request: XCBRequest, session: XCBSession, completion: @escaping (Status) -> Void) {
if let id = request.arguments["statusID"] { if let id = request.arguments["statusID"] {
MastodonCache.status(for: id) { (status) in mastodonController.cache.status(for: id) { (status) in
if let status = status { if let status = status {
completion(status) completion(status)
} else { } else {
@ -41,11 +48,11 @@ struct XCBActions {
} }
} }
} else if let searchQuery = request.arguments["statusURL"] { } else if let searchQuery = request.arguments["statusURL"] {
let request = MastodonController.client.search(query: searchQuery) let request = Client.search(query: searchQuery)
MastodonController.client.run(request) { (response) in mastodonController.run(request) { (response) in
if case let .success(results, _) = response, if case let .success(results, _) = response,
let status = results.statuses.first { let status = results.statuses.first {
MastodonCache.add(status: status) mastodonController.cache.add(status: status)
completion(status) completion(status)
} else { } else {
session.complete(with: .error, additionalData: [ session.complete(with: .error, additionalData: [
@ -62,7 +69,7 @@ struct XCBActions {
private static func getAccount(from request: XCBRequest, session: XCBSession, completion: @escaping (Account) -> Void) { private static func getAccount(from request: XCBRequest, session: XCBSession, completion: @escaping (Account) -> Void) {
if let id = request.arguments["accountID"] { if let id = request.arguments["accountID"] {
MastodonCache.account(for: id) { (account) in mastodonController.cache.account(for: id) { (account) in
if let account = account { if let account = account {
completion(account) completion(account)
} else { } else {
@ -72,11 +79,11 @@ struct XCBActions {
} }
} }
} else if let searchQuery = request.arguments["accountURL"] { } else if let searchQuery = request.arguments["accountURL"] {
let request = MastodonController.client.search(query: searchQuery) let request = Client.search(query: searchQuery)
MastodonController.client.run(request) { (response) in mastodonController.run(request) { (response) in
if case let .success(results, _) = response { if case let .success(results, _) = response {
if let account = results.accounts.first { if let account = results.accounts.first {
MastodonCache.add(account: account) mastodonController.cache.add(account: account)
completion(account) completion(account)
} else { } else {
session.complete(with: .error, additionalData: [ session.complete(with: .error, additionalData: [
@ -90,11 +97,11 @@ struct XCBActions {
} }
} }
} else if let acct = request.arguments["acct"] { } else if let acct = request.arguments["acct"] {
let request = MastodonController.client.searchForAccount(query: acct) let request = Client.searchForAccount(query: acct)
MastodonController.client.run(request) { (response) in mastodonController.run(request) { (response) in
if case let .success(accounts, _) = response { if case let .success(accounts, _) = response {
if let account = accounts.first { if let account = accounts.first {
MastodonCache.add(account: account) mastodonController.cache.add(account: account)
completion(account) completion(account)
} else { } else {
session.complete(with: .error, additionalData: [ session.complete(with: .error, additionalData: [
@ -118,7 +125,7 @@ struct XCBActions {
static func showStatus(_ request: XCBRequest, _ session: XCBSession, _ silent: Bool?) { static func showStatus(_ request: XCBRequest, _ session: XCBSession, _ silent: Bool?) {
getStatus(from: request, session: session) { (status) in getStatus(from: request, session: session) { (status) in
DispatchQueue.main.async { DispatchQueue.main.async {
let vc = ConversationTableViewController(for: status.id) let vc = ConversationTableViewController(for: status.id, mastodonController: mastodonController)
show(vc) show(vc)
} }
} }
@ -132,14 +139,14 @@ struct XCBActions {
var status = "" var status = ""
if let mentioning = mentioning { status += mentioning } if let mentioning = mentioning { status += mentioning }
if let text = text { status += text } if let text = text { status += text }
guard CharacterCounter.count(text: status) <= MastodonController.instance.maxStatusCharacters ?? 500 else { guard CharacterCounter.count(text: status) <= mastodonController.instance.maxStatusCharacters ?? 500 else {
session.complete(with: .error, additionalData: [ session.complete(with: .error, additionalData: [
"error": "Too many characters. Instance maximum is \(MastodonController.instance.maxStatusCharacters ?? 500)" "error": "Too many characters. Instance maximum is \(mastodonController.instance.maxStatusCharacters ?? 500)"
]) ])
return return
} }
let request = MastodonController.client.createStatus(text: status, visibility: Preferences.shared.defaultPostVisibility) let request = Client.createStatus(text: status, visibility: Preferences.shared.defaultPostVisibility)
MastodonController.client.run(request) { response in mastodonController.run(request) { response in
if case let .success(status, _) = response { if case let .success(status, _) = response {
session.complete(with: .success, additionalData: [ session.complete(with: .success, additionalData: [
"statusURL": status.url?.absoluteString, "statusURL": status.url?.absoluteString,
@ -152,7 +159,7 @@ struct XCBActions {
} }
} }
} else { } else {
let compose = ComposeViewController(mentioningAcct: mentioning, text: text) let compose = ComposeViewController(mentioningAcct: mentioning, text: text, mastodonController: mastodonController)
compose.xcbSession = session compose.xcbSession = session
let vc = UINavigationController(rootViewController: compose) let vc = UINavigationController(rootViewController: compose)
present(vc) present(vc)
@ -199,9 +206,9 @@ struct XCBActions {
static func statusAction(request: @escaping (Status) -> Request<Status>, alertTitle: String, _ url: XCBRequest, _ session: XCBSession, _ silent: Bool?) { static func statusAction(request: @escaping (Status) -> Request<Status>, alertTitle: String, _ url: XCBRequest, _ session: XCBSession, _ silent: Bool?) {
func performAction(status: Status, completion: ((Status) -> Void)?) { func performAction(status: Status, completion: ((Status) -> Void)?) {
MastodonController.client.run(request(status)) { (response) in mastodonController.run(request(status)) { (response) in
if case let .success(status, _) = response { if case let .success(status, _) = response {
MastodonCache.add(status: status) mastodonController.cache.add(status: status)
completion?(status) completion?(status)
session.complete(with: .success, additionalData: [ session.complete(with: .success, additionalData: [
"statusURL": status.url?.absoluteString, "statusURL": status.url?.absoluteString,
@ -219,7 +226,7 @@ struct XCBActions {
if silent ?? false { if silent ?? false {
performAction(status: status, completion: nil) performAction(status: status, completion: nil)
} else { } else {
let vc = ConversationTableViewController(for: status.id) let vc = ConversationTableViewController(for: status.id, mastodonController: mastodonController)
DispatchQueue.main.async { DispatchQueue.main.async {
show(vc) show(vc)
} }
@ -247,7 +254,7 @@ struct XCBActions {
static func showAccount(_ request: XCBRequest, _ session: XCBSession, _ silent: Bool?) { static func showAccount(_ request: XCBRequest, _ session: XCBSession, _ silent: Bool?) {
getAccount(from: request, session: session) { (account) in getAccount(from: request, session: session) { (account) in
DispatchQueue.main.async { DispatchQueue.main.async {
let vc = ProfileTableViewController(accountID: account.id) let vc = ProfileTableViewController(accountID: account.id, mastodonController: mastodonController)
show(vc) show(vc)
} }
} }
@ -269,7 +276,7 @@ struct XCBActions {
} }
static func getCurrentUser(_ request: XCBRequest, _ session: XCBSession, _ silent: Bool?) { static func getCurrentUser(_ request: XCBRequest, _ session: XCBSession, _ silent: Bool?) {
let account = MastodonController.account! let account = mastodonController.account!
session.complete(with: .success, additionalData: [ session.complete(with: .success, additionalData: [
"username": account.acct, "username": account.acct,
"displayName": account.displayName, "displayName": account.displayName,
@ -285,9 +292,9 @@ struct XCBActions {
static func followUser(_ request: XCBRequest, _ session: XCBSession, _ silent: Bool?) { static func followUser(_ request: XCBRequest, _ session: XCBSession, _ silent: Bool?) {
func performAction(_ account: Account) { func performAction(_ account: Account) {
let request = Account.follow(account.id) let request = Account.follow(account.id)
MastodonController.client.run(request) { (response) in mastodonController.run(request) { (response) in
if case let .success(relationship, _) = response { if case let .success(relationship, _) = response {
MastodonCache.add(relationship: relationship) mastodonController.cache.add(relationship: relationship)
session.complete(with: .success, additionalData: [ session.complete(with: .success, additionalData: [
"url": account.url.absoluteString "url": account.url.absoluteString
]) ])
@ -303,7 +310,7 @@ struct XCBActions {
if silent ?? false { if silent ?? false {
performAction(account) performAction(account)
} else { } else {
let vc = ProfileTableViewController(accountID: account.id) let vc = ProfileTableViewController(accountID: account.id, mastodonController: mastodonController)
DispatchQueue.main.async { DispatchQueue.main.async {
show(vc) show(vc)
} }