Merge branch 'multiple-accounts'

This commit is contained in:
Shadowfacts 2020-01-23 22:36:42 -05:00
commit 3220436893
Signed by untrusted user: 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
public func getSelfAccount() -> Request<Account> {
public static func getSelfAccount() -> Request<Account> {
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")
}
public func getRelationships(accounts: [String]? = nil) -> Request<[Relationship]> {
public static func getRelationships(accounts: [String]? = nil) -> Request<[Relationship]> {
return Request<[Relationship]>(method: .get, path: "/api/v1/accounts/relationships", queryParameters: "id" => accounts)
}
public func getInstance() -> Request<Instance> {
public static func getInstance() -> Request<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")
}
// 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)")
}
public func searchForAccount(query: String, limit: Int? = nil, following: Bool? = nil) -> Request<[Account]> {
public static func searchForAccount(query: String, limit: Int? = nil, following: Bool? = nil) -> Request<[Account]> {
return Request<[Account]>(method: .get, path: "/api/v1/accounts/search", queryParameters: [
"q" => query,
"limit" => limit,
@ -164,32 +164,32 @@ public class Client {
}
// MARK: - Blocks
public func getBlocks() -> Request<[Account]> {
public static func getBlocks() -> Request<[Account]> {
return Request<[Account]>(method: .get, path: "/api/v1/blocks")
}
public func getDomainBlocks() -> Request<[String]> {
public static func getDomainBlocks() -> Request<[String]> {
return Request<[String]>(method: .get, path: "api/v1/domain_blocks")
}
public func block(domain: String) -> Request<Empty> {
public static func block(domain: String) -> Request<Empty> {
return Request<Empty>(method: .post, path: "/api/v1/domain_blocks", body: .parameters([
"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([
"domain" => domain
]))
}
// MARK: - Filters
public func getFilters() -> Request<[Filter]> {
public static func getFilters() -> Request<[Filter]> {
return Request<[Filter]>(method: .get, path: "/api/v1/filters")
}
public func createFilter(phrase: String, context: [Filter.Context], irreversible: Bool? = nil, wholeWord: Bool? = nil, expiresAt: Date? = nil) -> Request<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([
"phrase" => phrase,
"irreversible" => irreversible,
@ -198,40 +198,40 @@ public class Client {
] + "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)")
}
// MARK: - Follows
public func getFollowRequests(range: RequestRange = .default) -> Request<[Account]> {
public static func getFollowRequests(range: RequestRange = .default) -> Request<[Account]> {
var request = Request<[Account]>(method: .get, path: "/api/v1/follow_requests")
request.range = range
return request
}
public func getFollowSuggestions() -> Request<[Account]> {
public static func getFollowSuggestions() -> Request<[Account]> {
return Request<[Account]>(method: .get, path: "/api/v1/suggestions")
}
public func followRemote(acct: String) -> Request<Account> {
public static func followRemote(acct: String) -> Request<Account> {
return Request<Account>(method: .post, path: "/api/v1/follows", body: .parameters(["uri" => acct]))
}
// MARK: - Lists
public func getLists() -> Request<[List]> {
public static func getLists() -> Request<[List]> {
return Request<[List]>(method: .get, path: "/api/v1/lists")
}
public func getList(id: String) -> Request<List> {
public static func getList(id: String) -> Request<List> {
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]))
}
// 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([
"description" => description,
"focus" => focus
@ -239,14 +239,14 @@ public class Client {
}
// MARK: - Mutes
public func getMutes(range: RequestRange) -> Request<[Account]> {
public static func getMutes(range: RequestRange) -> Request<[Account]> {
var request = Request<[Account]>(method: .get, path: "/api/v1/mutes")
request.range = range
return request
}
// MARK: - Notifications
public func getNotifications(excludeTypes: [Notification.Kind], range: RequestRange = .default) -> Request<[Notification]> {
public static func getNotifications(excludeTypes: [Notification.Kind], range: RequestRange = .default) -> Request<[Notification]> {
var request = Request<[Notification]>(method: .get, path: "/api/v1/notifications", queryParameters:
"exclude_types" => excludeTypes.map { $0.rawValue }
)
@ -254,16 +254,16 @@ public class Client {
return request
}
public func clearNotifications() -> Request<Empty> {
public static func clearNotifications() -> Request<Empty> {
return Request<Empty>(method: .post, path: "/api/v1/notifications/clear")
}
// MARK: - Reports
public func getReports() -> Request<[Report]> {
public static func getReports() -> Request<[Report]> {
return Request<[Report]>(method: .get, path: "/api/v1/reports")
}
public func report(account: Account, statuses: [Status], comment: String) -> Request<Report> {
public static func report(account: Account, statuses: [Status], comment: String) -> Request<Report> {
return Request<Report>(method: .post, path: "/api/v1/reports", body: .parameters([
"account_id" => account.id,
"comment" => comment
@ -271,7 +271,7 @@ public class Client {
}
// MARK: - Search
public func search(query: String, resolve: Bool? = nil, limit: Int? = nil) -> Request<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: [
"q" => query,
"resolve" => resolve,
@ -280,18 +280,18 @@ public class Client {
}
// 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)")
}
public func createStatus(text: String,
contentType: StatusContentType = .plain,
inReplyTo: String? = nil,
media: [Attachment]? = nil,
sensitive: Bool? = nil,
spoilerText: String? = nil,
visibility: Status.Visibility? = nil,
language: String? = nil) -> Request<Status> {
public static func createStatus(text: String,
contentType: StatusContentType = .plain,
inReplyTo: String? = nil,
media: [Attachment]? = nil,
sensitive: Bool? = nil,
spoilerText: String? = nil,
visibility: Status.Visibility? = nil,
language: String? = nil) -> Request<Status> {
return Request<Status>(method: .post, path: "/api/v1/statuses", body: .parameters([
"status" => text,
"content_type" => contentType.mimeType,
@ -304,13 +304,13 @@ public class Client {
}
// MARK: - Timelines
public func getStatuses(timeline: Timeline, range: RequestRange = .default) -> Request<[Status]> {
public static func getStatuses(timeline: Timeline, range: RequestRange = .default) -> Request<[Status]> {
return timeline.request(range: range)
}
// MARK: Bookmarks
public func getBookmarks(range: RequestRange = .default) -> Request<[Status]> {
public static func getBookmarks(range: RequestRange = .default) -> Request<[Status]> {
var request = Request<[Status]>(method: .get, path: "/api/v1/bookmarks")
request.range = range
return request

View File

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

View File

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

View File

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

View File

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

View File

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

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 }
let request = Status.bookmark(status)
MastodonController.client.run(request) { (response) in
mastodonController.run(request) { (response) in
if case let .success(status, _) = response {
MastodonCache.add(status: status)
self.mastodonController.cache.add(status: status)
} else {
// todo: display error message
UINotificationFeedbackGenerator().notificationOccurred(.error)

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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">
<plist version="1.0">
<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>
<string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleExecutable</key>
@ -52,14 +33,52 @@
<string>1</string>
<key>LSRequiresIPhoneOS</key>
<true/>
<key>NSMicrophoneUsageDescription</key>
<string>Post videos from the camera.</string>
<key>NSAppTransportSecurity</key>
<dict>
<key>NSExceptionDomains</key>
<dict>
<key>localhost</key>
<dict>
<key>NSExceptionAllowsInsecureHTTPLoads</key>
<true/>
</dict>
</dict>
</dict>
<key>NSCameraUsageDescription</key>
<string>Post photos and videos from the camera.</string>
<key>NSMicrophoneUsageDescription</key>
<string>Post videos from the camera.</string>
<key>NSPhotoLibraryAddUsageDescription</key>
<string>Save photos directly from other people&apos;s posts.</string>
<key>NSPhotoLibraryUsageDescription</key>
<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>
<string>LaunchScreen</string>
<key>UIRequiredDeviceCapabilities</key>

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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