forked from shadowfacts/Tusker
Add ability to save and view instance public timelines
This commit is contained in:
parent
f92a2acc97
commit
377b5f5c85
|
@ -81,7 +81,7 @@ public class Client {
|
||||||
}
|
}
|
||||||
|
|
||||||
func createURLRequest<Result>(request: Request<Result>) -> URLRequest? {
|
func createURLRequest<Result>(request: Request<Result>) -> URLRequest? {
|
||||||
guard var components = URLComponents(url: baseURL, resolvingAgainstBaseURL: true) else { return nil }
|
guard var components = URLComponents(url: request.baseURL ?? baseURL, resolvingAgainstBaseURL: true) else { return nil }
|
||||||
components.path = request.path
|
components.path = request.path
|
||||||
components.queryItems = request.queryParameters.queryItems
|
components.queryItems = request.queryParameters.queryItems
|
||||||
guard let url = components.url else { return nil }
|
guard let url = components.url else { return nil }
|
||||||
|
|
|
@ -11,6 +11,7 @@ import Foundation
|
||||||
public enum Timeline {
|
public enum Timeline {
|
||||||
case home
|
case home
|
||||||
case `public`(local: Bool)
|
case `public`(local: Bool)
|
||||||
|
case instance(instanceURL: URL)
|
||||||
case tag(hashtag: String)
|
case tag(hashtag: String)
|
||||||
case list(id: String)
|
case list(id: String)
|
||||||
case direct
|
case direct
|
||||||
|
@ -21,7 +22,7 @@ extension Timeline {
|
||||||
switch self {
|
switch self {
|
||||||
case .home:
|
case .home:
|
||||||
return "/api/v1/timelines/home"
|
return "/api/v1/timelines/home"
|
||||||
case .public:
|
case .public, .instance(_):
|
||||||
return "/api/v1/timelines/public"
|
return "/api/v1/timelines/public"
|
||||||
case let .tag(hashtag):
|
case let .tag(hashtag):
|
||||||
return "/api/v1/timelines/tag/\(hashtag)"
|
return "/api/v1/timelines/tag/\(hashtag)"
|
||||||
|
@ -33,7 +34,12 @@ extension Timeline {
|
||||||
}
|
}
|
||||||
|
|
||||||
func request(range: RequestRange) -> Request<[Status]> {
|
func request(range: RequestRange) -> Request<[Status]> {
|
||||||
var request = Request<[Status]>(method: .get, path: endpoint)
|
var request: Request<[Status]>
|
||||||
|
if case let .instance(instanceURL) = self {
|
||||||
|
request = Request<[Status]>(method: .get, baseURL: instanceURL, path: endpoint)
|
||||||
|
} else {
|
||||||
|
request = Request<[Status]>(method: .get, path: endpoint)
|
||||||
|
}
|
||||||
if case .public(true) = self {
|
if case .public(true) = self {
|
||||||
request.queryParameters.append("local" => true)
|
request.queryParameters.append("local" => true)
|
||||||
}
|
}
|
||||||
|
@ -51,6 +57,8 @@ extension Timeline: Codable {
|
||||||
self = .home
|
self = .home
|
||||||
case "public":
|
case "public":
|
||||||
self = .public(local: try container.decode(Bool.self, forKey: .local))
|
self = .public(local: try container.decode(Bool.self, forKey: .local))
|
||||||
|
case "instanceURL":
|
||||||
|
self = .instance(instanceURL: try container.decode(URL.self, forKey: .instanceURL))
|
||||||
case "tag":
|
case "tag":
|
||||||
self = .tag(hashtag: try container.decode(String.self, forKey: .hashtag))
|
self = .tag(hashtag: try container.decode(String.self, forKey: .hashtag))
|
||||||
case "list":
|
case "list":
|
||||||
|
@ -70,6 +78,9 @@ extension Timeline: Codable {
|
||||||
case let .public(local):
|
case let .public(local):
|
||||||
try container.encode("public", forKey: .type)
|
try container.encode("public", forKey: .type)
|
||||||
try container.encode(local, forKey: .local)
|
try container.encode(local, forKey: .local)
|
||||||
|
case let .instance(instanceURL):
|
||||||
|
try container.encode("instanceURL", forKey: .type)
|
||||||
|
try container.encode(instanceURL, forKey: .instanceURL)
|
||||||
case let .tag(hashtag):
|
case let .tag(hashtag):
|
||||||
try container.encode("tag", forKey: .type)
|
try container.encode("tag", forKey: .type)
|
||||||
try container.encode(hashtag, forKey: .hashtag)
|
try container.encode(hashtag, forKey: .hashtag)
|
||||||
|
@ -84,6 +95,7 @@ extension Timeline: Codable {
|
||||||
enum CodingKeys: String, CodingKey {
|
enum CodingKeys: String, CodingKey {
|
||||||
case type
|
case type
|
||||||
case local
|
case local
|
||||||
|
case instanceURL
|
||||||
case hashtag
|
case hashtag
|
||||||
case listID
|
case listID
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,12 +10,14 @@ import Foundation
|
||||||
|
|
||||||
public struct Request<ResultType: Decodable> {
|
public struct Request<ResultType: Decodable> {
|
||||||
let method: Method
|
let method: Method
|
||||||
|
let baseURL: URL?
|
||||||
let path: String
|
let path: String
|
||||||
let body: Body
|
let body: Body
|
||||||
var queryParameters: [Parameter]
|
var queryParameters: [Parameter]
|
||||||
|
|
||||||
init(method: Method, path: String, body: Body = .empty, queryParameters: [Parameter] = []) {
|
init(method: Method, baseURL: URL? = nil, path: String, body: Body = .empty, queryParameters: [Parameter] = []) {
|
||||||
self.method = method
|
self.method = method
|
||||||
|
self.baseURL = baseURL
|
||||||
self.path = path
|
self.path = path
|
||||||
self.body = body
|
self.body = body
|
||||||
self.queryParameters = queryParameters
|
self.queryParameters = queryParameters
|
||||||
|
|
|
@ -167,6 +167,9 @@
|
||||||
D6945C2F23AC47C3005C403C /* SavedHashtagsManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6945C2E23AC47C3005C403C /* SavedHashtagsManager.swift */; };
|
D6945C2F23AC47C3005C403C /* SavedHashtagsManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6945C2E23AC47C3005C403C /* SavedHashtagsManager.swift */; };
|
||||||
D6945C3223AC4D36005C403C /* HashtagTimelineViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6945C3123AC4D36005C403C /* HashtagTimelineViewController.swift */; };
|
D6945C3223AC4D36005C403C /* HashtagTimelineViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6945C3123AC4D36005C403C /* HashtagTimelineViewController.swift */; };
|
||||||
D6945C3423AC6431005C403C /* AddSavedHashtagViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6945C3323AC6431005C403C /* AddSavedHashtagViewController.swift */; };
|
D6945C3423AC6431005C403C /* AddSavedHashtagViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6945C3323AC6431005C403C /* AddSavedHashtagViewController.swift */; };
|
||||||
|
D6945C3623AC6C09005C403C /* SavedInstancesManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6945C3523AC6C09005C403C /* SavedInstancesManager.swift */; };
|
||||||
|
D6945C3823AC739F005C403C /* InstanceTimelineViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6945C3723AC739F005C403C /* InstanceTimelineViewController.swift */; };
|
||||||
|
D6945C3A23AC75E2005C403C /* FindInstanceViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6945C3923AC75E2005C403C /* FindInstanceViewController.swift */; };
|
||||||
D6A3BC7723218E1300FD64D5 /* TimelineSegment.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6A3BC7323218C6E00FD64D5 /* TimelineSegment.swift */; };
|
D6A3BC7723218E1300FD64D5 /* TimelineSegment.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6A3BC7323218C6E00FD64D5 /* TimelineSegment.swift */; };
|
||||||
D6A3BC7923218E9200FD64D5 /* NotificationGroup.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6A3BC7823218E9200FD64D5 /* NotificationGroup.swift */; };
|
D6A3BC7923218E9200FD64D5 /* NotificationGroup.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6A3BC7823218E9200FD64D5 /* NotificationGroup.swift */; };
|
||||||
D6A3BC7C232195C600FD64D5 /* ActionNotificationGroupTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6A3BC7A232195C600FD64D5 /* ActionNotificationGroupTableViewCell.swift */; };
|
D6A3BC7C232195C600FD64D5 /* ActionNotificationGroupTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6A3BC7A232195C600FD64D5 /* ActionNotificationGroupTableViewCell.swift */; };
|
||||||
|
@ -441,6 +444,9 @@
|
||||||
D6945C2E23AC47C3005C403C /* SavedHashtagsManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SavedHashtagsManager.swift; sourceTree = "<group>"; };
|
D6945C2E23AC47C3005C403C /* SavedHashtagsManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SavedHashtagsManager.swift; sourceTree = "<group>"; };
|
||||||
D6945C3123AC4D36005C403C /* HashtagTimelineViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HashtagTimelineViewController.swift; sourceTree = "<group>"; };
|
D6945C3123AC4D36005C403C /* HashtagTimelineViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HashtagTimelineViewController.swift; sourceTree = "<group>"; };
|
||||||
D6945C3323AC6431005C403C /* AddSavedHashtagViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddSavedHashtagViewController.swift; sourceTree = "<group>"; };
|
D6945C3323AC6431005C403C /* AddSavedHashtagViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddSavedHashtagViewController.swift; sourceTree = "<group>"; };
|
||||||
|
D6945C3523AC6C09005C403C /* SavedInstancesManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SavedInstancesManager.swift; sourceTree = "<group>"; };
|
||||||
|
D6945C3723AC739F005C403C /* InstanceTimelineViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InstanceTimelineViewController.swift; sourceTree = "<group>"; };
|
||||||
|
D6945C3923AC75E2005C403C /* FindInstanceViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = FindInstanceViewController.swift; path = Tusker/Screens/FindInstanceViewController.swift; sourceTree = SOURCE_ROOT; };
|
||||||
D6A3BC7323218C6E00FD64D5 /* TimelineSegment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineSegment.swift; sourceTree = "<group>"; };
|
D6A3BC7323218C6E00FD64D5 /* TimelineSegment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineSegment.swift; sourceTree = "<group>"; };
|
||||||
D6A3BC7823218E9200FD64D5 /* NotificationGroup.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationGroup.swift; sourceTree = "<group>"; };
|
D6A3BC7823218E9200FD64D5 /* NotificationGroup.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationGroup.swift; sourceTree = "<group>"; };
|
||||||
D6A3BC7A232195C600FD64D5 /* ActionNotificationGroupTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActionNotificationGroupTableViewCell.swift; sourceTree = "<group>"; };
|
D6A3BC7A232195C600FD64D5 /* ActionNotificationGroupTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActionNotificationGroupTableViewCell.swift; sourceTree = "<group>"; };
|
||||||
|
@ -738,6 +744,7 @@
|
||||||
children = (
|
children = (
|
||||||
D627943D23A564D400D38C68 /* ExploreViewController.swift */,
|
D627943D23A564D400D38C68 /* ExploreViewController.swift */,
|
||||||
D6945C3323AC6431005C403C /* AddSavedHashtagViewController.swift */,
|
D6945C3323AC6431005C403C /* AddSavedHashtagViewController.swift */,
|
||||||
|
D6945C3923AC75E2005C403C /* FindInstanceViewController.swift */,
|
||||||
);
|
);
|
||||||
path = Explore;
|
path = Explore;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
|
@ -788,7 +795,6 @@
|
||||||
D641C782213DD7F0004B4513 /* Main */,
|
D641C782213DD7F0004B4513 /* Main */,
|
||||||
D641C783213DD7FE004B4513 /* Onboarding */,
|
D641C783213DD7FE004B4513 /* Onboarding */,
|
||||||
D641C781213DD7DD004B4513 /* Timeline */,
|
D641C781213DD7DD004B4513 /* Timeline */,
|
||||||
D6945C3023AC4D21005C403C /* Hashtag Timeline */,
|
|
||||||
D641C784213DD819004B4513 /* Profile */,
|
D641C784213DD819004B4513 /* Profile */,
|
||||||
D641C785213DD83B004B4513 /* Conversation */,
|
D641C785213DD83B004B4513 /* Conversation */,
|
||||||
D641C786213DD852004B4513 /* Notifications */,
|
D641C786213DD852004B4513 /* Notifications */,
|
||||||
|
@ -812,6 +818,8 @@
|
||||||
children = (
|
children = (
|
||||||
D6BC9DD6232D7811002CA326 /* TimelinesPageViewController.swift */,
|
D6BC9DD6232D7811002CA326 /* TimelinesPageViewController.swift */,
|
||||||
D6F953EB212519E700CF0F2B /* TimelineTableViewController.swift */,
|
D6F953EB212519E700CF0F2B /* TimelineTableViewController.swift */,
|
||||||
|
D6945C3123AC4D36005C403C /* HashtagTimelineViewController.swift */,
|
||||||
|
D6945C3723AC739F005C403C /* InstanceTimelineViewController.swift */,
|
||||||
);
|
);
|
||||||
path = Timeline;
|
path = Timeline;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
|
@ -1065,14 +1073,6 @@
|
||||||
path = de.lproj;
|
path = de.lproj;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
};
|
};
|
||||||
D6945C3023AC4D21005C403C /* Hashtag Timeline */ = {
|
|
||||||
isa = PBXGroup;
|
|
||||||
children = (
|
|
||||||
D6945C3123AC4D36005C403C /* HashtagTimelineViewController.swift */,
|
|
||||||
);
|
|
||||||
path = "Hashtag Timeline";
|
|
||||||
sourceTree = "<group>";
|
|
||||||
};
|
|
||||||
D6A3BC7223218C6E00FD64D5 /* Utilities */ = {
|
D6A3BC7223218C6E00FD64D5 /* Utilities */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
|
@ -1226,6 +1226,7 @@
|
||||||
D64D0AAC2128D88B005A6F37 /* LocalData.swift */,
|
D64D0AAC2128D88B005A6F37 /* LocalData.swift */,
|
||||||
D627FF75217E923E00CC0648 /* DraftsManager.swift */,
|
D627FF75217E923E00CC0648 /* DraftsManager.swift */,
|
||||||
D6945C2E23AC47C3005C403C /* SavedHashtagsManager.swift */,
|
D6945C2E23AC47C3005C403C /* SavedHashtagsManager.swift */,
|
||||||
|
D6945C3523AC6C09005C403C /* SavedInstancesManager.swift */,
|
||||||
D6028B9A2150811100F223B9 /* MastodonCache.swift */,
|
D6028B9A2150811100F223B9 /* MastodonCache.swift */,
|
||||||
D6C693EE216192C2007D6A6D /* TuskerNavigationDelegate.swift */,
|
D6C693EE216192C2007D6A6D /* TuskerNavigationDelegate.swift */,
|
||||||
D6F1F84E2193B9BE00F5FE67 /* Caching */,
|
D6F1F84E2193B9BE00F5FE67 /* Caching */,
|
||||||
|
@ -1653,6 +1654,7 @@
|
||||||
D68FEC4F232C5BC300C84F23 /* SegmentedPageViewController.swift in Sources */,
|
D68FEC4F232C5BC300C84F23 /* SegmentedPageViewController.swift in Sources */,
|
||||||
0450531F22B0097E00100BA2 /* Timline+UI.swift in Sources */,
|
0450531F22B0097E00100BA2 /* Timline+UI.swift in Sources */,
|
||||||
D667E5F52135BCD50057A976 /* ConversationTableViewController.swift in Sources */,
|
D667E5F52135BCD50057A976 /* ConversationTableViewController.swift in Sources */,
|
||||||
|
D6945C3623AC6C09005C403C /* SavedInstancesManager.swift in Sources */,
|
||||||
D6C7D27D22B6EBF800071952 /* AttachmentsContainerView.swift in Sources */,
|
D6C7D27D22B6EBF800071952 /* AttachmentsContainerView.swift in Sources */,
|
||||||
D6F953F021251A2900CF0F2B /* MastodonController.swift in Sources */,
|
D6F953F021251A2900CF0F2B /* MastodonController.swift in Sources */,
|
||||||
0411610022B442870030A9B7 /* AttachmentViewController.swift in Sources */,
|
0411610022B442870030A9B7 /* AttachmentViewController.swift in Sources */,
|
||||||
|
@ -1666,6 +1668,7 @@
|
||||||
D6028B9B2150811100F223B9 /* MastodonCache.swift in Sources */,
|
D6028B9B2150811100F223B9 /* MastodonCache.swift in Sources */,
|
||||||
D6A3BC802321B7E600FD64D5 /* FollowNotificationGroupTableViewCell.swift in Sources */,
|
D6A3BC802321B7E600FD64D5 /* FollowNotificationGroupTableViewCell.swift in Sources */,
|
||||||
D627944D23A9A03D00D38C68 /* ListTimelineViewController.swift in Sources */,
|
D627944D23A9A03D00D38C68 /* ListTimelineViewController.swift in Sources */,
|
||||||
|
D6945C3823AC739F005C403C /* InstanceTimelineViewController.swift in Sources */,
|
||||||
D62D2422217AA7E1005076CC /* UserActivityManager.swift in Sources */,
|
D62D2422217AA7E1005076CC /* UserActivityManager.swift in Sources */,
|
||||||
D60D2B8223844C71001B87A3 /* BaseStatusTableViewCell.swift in Sources */,
|
D60D2B8223844C71001B87A3 /* BaseStatusTableViewCell.swift in Sources */,
|
||||||
D62D2424217ABF3F005076CC /* NSUserActivity+Extensions.swift in Sources */,
|
D62D2424217ABF3F005076CC /* NSUserActivity+Extensions.swift in Sources */,
|
||||||
|
@ -1691,6 +1694,7 @@
|
||||||
D6DD353F22F502EC00A9563A /* Preferences+Notification.swift in Sources */,
|
D6DD353F22F502EC00A9563A /* Preferences+Notification.swift in Sources */,
|
||||||
D63661C02381C144004B9E16 /* PreferencesNavigationController.swift in Sources */,
|
D63661C02381C144004B9E16 /* PreferencesNavigationController.swift in Sources */,
|
||||||
D627944A23A6AD6100D38C68 /* BookmarksTableViewController.swift in Sources */,
|
D627944A23A6AD6100D38C68 /* BookmarksTableViewController.swift in Sources */,
|
||||||
|
D6945C3A23AC75E2005C403C /* FindInstanceViewController.swift in Sources */,
|
||||||
D6AEBB4523216AF800E5038B /* FollowAccountActivity.swift in Sources */,
|
D6AEBB4523216AF800E5038B /* FollowAccountActivity.swift in Sources */,
|
||||||
D6538945214D6D7500E3CEFC /* TableViewSwipeActionProvider.swift in Sources */,
|
D6538945214D6D7500E3CEFC /* TableViewSwipeActionProvider.swift in Sources */,
|
||||||
D6E0DC8E216EDF1E00369478 /* Previewing.swift in Sources */,
|
D6E0DC8E216EDF1E00369478 /* Previewing.swift in Sources */,
|
||||||
|
|
|
@ -16,6 +16,8 @@ extension Timeline {
|
||||||
return "Home"
|
return "Home"
|
||||||
case let .public(local):
|
case let .public(local):
|
||||||
return local ? "Local" : "Federated"
|
return local ? "Local" : "Federated"
|
||||||
|
case let .instance(instance):
|
||||||
|
return instance.host!
|
||||||
case let .tag(hashtag):
|
case let .tag(hashtag):
|
||||||
return "#\(hashtag)"
|
return "#\(hashtag)"
|
||||||
case .list:
|
case .list:
|
||||||
|
@ -35,6 +37,8 @@ extension Timeline {
|
||||||
} else {
|
} else {
|
||||||
return UIImage(systemName: "globe")
|
return UIImage(systemName: "globe")
|
||||||
}
|
}
|
||||||
|
case .instance(_):
|
||||||
|
return UIImage(systemName: "globe")
|
||||||
default:
|
default:
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,7 +16,7 @@ class SavedHashtagsManager: Codable {
|
||||||
private static var archiveURL = SavedHashtagsManager.documentsDirectory.appendingPathComponent("saved_hashtags").appendingPathExtension("plist")
|
private static var archiveURL = SavedHashtagsManager.documentsDirectory.appendingPathComponent("saved_hashtags").appendingPathExtension("plist")
|
||||||
|
|
||||||
static func save() {
|
static func save() {
|
||||||
DispatchQueue.global(qos: .userInitiated).async {
|
DispatchQueue.global(qos: .utility).async {
|
||||||
let encoder = PropertyListEncoder()
|
let encoder = PropertyListEncoder()
|
||||||
let data = try? encoder.encode(shared)
|
let data = try? encoder.encode(shared)
|
||||||
try? data?.write(to: archiveURL, options: .noFileProtection)
|
try? data?.write(to: archiveURL, options: .noFileProtection)
|
||||||
|
@ -36,7 +36,7 @@ class SavedHashtagsManager: Codable {
|
||||||
|
|
||||||
private var savedHashtags: [Hashtag] = []
|
private var savedHashtags: [Hashtag] = []
|
||||||
var sorted: [Hashtag] {
|
var sorted: [Hashtag] {
|
||||||
return savedHashtags.sorted(by: { $0.name > $1.name })
|
return savedHashtags.sorted(by: { $0.name < $1.name })
|
||||||
}
|
}
|
||||||
|
|
||||||
func isSaved(_ hashtag: Hashtag) -> Bool {
|
func isSaved(_ hashtag: Hashtag) -> Bool {
|
||||||
|
|
|
@ -0,0 +1,61 @@
|
||||||
|
//
|
||||||
|
// 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")
|
||||||
|
}
|
|
@ -61,6 +61,16 @@ class ExploreViewController: EnhancedTableViewController {
|
||||||
cell.imageView!.image = UIImage(systemName: "plus")
|
cell.imageView!.image = UIImage(systemName: "plus")
|
||||||
cell.textLabel!.text = NSLocalizedString("Save Hashtag...", comment: "save hashtag nav item title")
|
cell.textLabel!.text = NSLocalizedString("Save Hashtag...", comment: "save hashtag nav item title")
|
||||||
cell.accessoryType = .none
|
cell.accessoryType = .none
|
||||||
|
|
||||||
|
case let .savedInstance(url):
|
||||||
|
cell.imageView!.image = UIImage(systemName: "globe")
|
||||||
|
cell.textLabel!.text = url.host!
|
||||||
|
cell.accessoryType = .disclosureIndicator
|
||||||
|
|
||||||
|
case .findInstance:
|
||||||
|
cell.imageView!.image = UIImage(systemName: "magnifyingglass")
|
||||||
|
cell.textLabel!.text = NSLocalizedString("Find An Instance...", comment: "find instance nav item title")
|
||||||
|
cell.accessoryType = .none
|
||||||
}
|
}
|
||||||
|
|
||||||
return cell
|
return cell
|
||||||
|
@ -68,10 +78,11 @@ class ExploreViewController: EnhancedTableViewController {
|
||||||
dataSource.exploreController = self
|
dataSource.exploreController = self
|
||||||
|
|
||||||
var snapshot = NSDiffableDataSourceSnapshot<Section, Item>()
|
var snapshot = NSDiffableDataSourceSnapshot<Section, Item>()
|
||||||
snapshot.appendSections([.bookmarks, .lists, .savedHashtags])
|
snapshot.appendSections([.bookmarks, .lists, .savedHashtags, .savedInstances])
|
||||||
snapshot.appendItems([.bookmarks], toSection: .bookmarks)
|
snapshot.appendItems([.bookmarks], toSection: .bookmarks)
|
||||||
snapshot.appendItems([.addList], toSection: .lists)
|
snapshot.appendItems([.addList], toSection: .lists)
|
||||||
snapshot.appendItems(SavedHashtagsManager.shared.sorted.map { .savedHashtag($0) } + [.addSavedHashtag], toSection: .savedHashtags)
|
snapshot.appendItems(SavedHashtagsManager.shared.sorted.map { .savedHashtag($0) } + [.addSavedHashtag], toSection: .savedHashtags)
|
||||||
|
snapshot.appendItems(SavedInstanceManager.shared.savedInstances.map { .savedInstance($0) } + [.findInstance], toSection: .savedInstances)
|
||||||
// the initial, static items should not be displayed with an animation
|
// the initial, static items should not be displayed with an animation
|
||||||
UIView.performWithoutAnimation {
|
UIView.performWithoutAnimation {
|
||||||
dataSource.apply(snapshot)
|
dataSource.apply(snapshot)
|
||||||
|
@ -89,6 +100,7 @@ class ExploreViewController: EnhancedTableViewController {
|
||||||
navigationItem.hidesSearchBarWhenScrolling = false
|
navigationItem.hidesSearchBarWhenScrolling = false
|
||||||
|
|
||||||
NotificationCenter.default.addObserver(self, selector: #selector(savedHashtagsChanged), name: .savedHashtagsChanged, object: nil)
|
NotificationCenter.default.addObserver(self, selector: #selector(savedHashtagsChanged), name: .savedHashtagsChanged, object: nil)
|
||||||
|
NotificationCenter.default.addObserver(self, selector: #selector(savedInstancesChanged), name: .savedInstancesChanged, object: nil)
|
||||||
|
|
||||||
reloadLists()
|
reloadLists()
|
||||||
}
|
}
|
||||||
|
@ -117,6 +129,13 @@ class ExploreViewController: EnhancedTableViewController {
|
||||||
dataSource.apply(snapshot)
|
dataSource.apply(snapshot)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@objc func savedInstancesChanged() {
|
||||||
|
var snapshot = dataSource.snapshot()
|
||||||
|
snapshot.deleteItems(snapshot.itemIdentifiers(inSection: .savedInstances))
|
||||||
|
snapshot.appendItems(SavedInstanceManager.shared.savedInstances.map { .savedInstance($0) } + [.findInstance], toSection: .savedInstances)
|
||||||
|
dataSource.apply(snapshot)
|
||||||
|
}
|
||||||
|
|
||||||
func deleteList(_ list: List) {
|
func deleteList(_ list: List) {
|
||||||
let title = String(format: NSLocalizedString("Are you sure want to delete the '%@' list?", comment: "delete list alert title"), list.title)
|
let title = String(format: NSLocalizedString("Are you sure want to delete the '%@' list?", comment: "delete list alert title"), list.title)
|
||||||
let alert = UIAlertController(title: title, message: nil, preferredStyle: .alert)
|
let alert = UIAlertController(title: title, message: nil, preferredStyle: .alert)
|
||||||
|
@ -143,6 +162,10 @@ class ExploreViewController: EnhancedTableViewController {
|
||||||
SavedHashtagsManager.shared.remove(hashtag)
|
SavedHashtagsManager.shared.remove(hashtag)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func removeSavedInstance(_ instanceURL: URL) {
|
||||||
|
SavedInstanceManager.shared.remove(instanceURL)
|
||||||
|
}
|
||||||
|
|
||||||
// MARK: - Table view delegate
|
// MARK: - Table view delegate
|
||||||
|
|
||||||
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
|
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
|
||||||
|
@ -188,6 +211,16 @@ class ExploreViewController: EnhancedTableViewController {
|
||||||
tableView.selectRow(at: nil, animated: true, scrollPosition: .none)
|
tableView.selectRow(at: nil, animated: true, scrollPosition: .none)
|
||||||
let navController = UINavigationController(rootViewController: AddSavedHashtagViewController())
|
let navController = UINavigationController(rootViewController: AddSavedHashtagViewController())
|
||||||
present(navController, animated: true)
|
present(navController, animated: true)
|
||||||
|
|
||||||
|
case let .savedInstance(url):
|
||||||
|
show(InstanceTimelineViewController(for: url), sender: nil)
|
||||||
|
|
||||||
|
case .findInstance:
|
||||||
|
tableView.selectRow(at: nil, animated: true, scrollPosition: .none)
|
||||||
|
let findController = FindInstanceViewController()
|
||||||
|
findController.instanceTimelineDelegate = self
|
||||||
|
let navController = UINavigationController(rootViewController: findController)
|
||||||
|
present(navController, animated: true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -202,6 +235,7 @@ extension ExploreViewController {
|
||||||
case bookmarks
|
case bookmarks
|
||||||
case lists
|
case lists
|
||||||
case savedHashtags
|
case savedHashtags
|
||||||
|
case savedInstances
|
||||||
}
|
}
|
||||||
enum Item: Hashable {
|
enum Item: Hashable {
|
||||||
case bookmarks
|
case bookmarks
|
||||||
|
@ -209,6 +243,8 @@ extension ExploreViewController {
|
||||||
case addList
|
case addList
|
||||||
case savedHashtag(Hashtag)
|
case savedHashtag(Hashtag)
|
||||||
case addSavedHashtag
|
case addSavedHashtag
|
||||||
|
case savedInstance(URL)
|
||||||
|
case findInstance
|
||||||
|
|
||||||
static func == (lhs: ExploreViewController.Item, rhs: ExploreViewController.Item) -> Bool {
|
static func == (lhs: ExploreViewController.Item, rhs: ExploreViewController.Item) -> Bool {
|
||||||
switch (lhs, rhs) {
|
switch (lhs, rhs) {
|
||||||
|
@ -222,6 +258,10 @@ extension ExploreViewController {
|
||||||
return a == b
|
return a == b
|
||||||
case (.addSavedHashtag, .addSavedHashtag):
|
case (.addSavedHashtag, .addSavedHashtag):
|
||||||
return true
|
return true
|
||||||
|
case let (.savedInstance(a), .savedInstance(b)):
|
||||||
|
return a == b
|
||||||
|
case (.findInstance, .findInstance):
|
||||||
|
return true
|
||||||
default:
|
default:
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
@ -240,6 +280,11 @@ extension ExploreViewController {
|
||||||
hasher.combine(hashtag.name)
|
hasher.combine(hashtag.name)
|
||||||
case .addSavedHashtag:
|
case .addSavedHashtag:
|
||||||
hasher.combine("addSavedHashtag")
|
hasher.combine("addSavedHashtag")
|
||||||
|
case let .savedInstance(url):
|
||||||
|
hasher.combine("savedInstance")
|
||||||
|
hasher.combine(url)
|
||||||
|
case .findInstance:
|
||||||
|
hasher.combine("findInstance")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -254,6 +299,8 @@ extension ExploreViewController {
|
||||||
return NSLocalizedString("Lists", comment: "explore lists section title")
|
return NSLocalizedString("Lists", comment: "explore lists section title")
|
||||||
case 2:
|
case 2:
|
||||||
return NSLocalizedString("Saved Hashtags", comment: "explore saved hashtags section title")
|
return NSLocalizedString("Saved Hashtags", comment: "explore saved hashtags section title")
|
||||||
|
case 3:
|
||||||
|
return NSLocalizedString("Instance Timelines", comment: "explore instance timelines section title")
|
||||||
default:
|
default:
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
@ -265,6 +312,8 @@ extension ExploreViewController {
|
||||||
return true
|
return true
|
||||||
case .savedHashtag(_):
|
case .savedHashtag(_):
|
||||||
return true
|
return true
|
||||||
|
case .savedInstance(_):
|
||||||
|
return true
|
||||||
default:
|
default:
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
@ -281,6 +330,8 @@ extension ExploreViewController {
|
||||||
exploreController.deleteList(list)
|
exploreController.deleteList(list)
|
||||||
case let .savedHashtag(hashtag):
|
case let .savedHashtag(hashtag):
|
||||||
exploreController.removeSavedHashtag(hashtag)
|
exploreController.removeSavedHashtag(hashtag)
|
||||||
|
case let .savedInstance(url):
|
||||||
|
exploreController.removeSavedInstance(url)
|
||||||
default:
|
default:
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -289,3 +340,15 @@ extension ExploreViewController {
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
extension ExploreViewController: InstanceTimelineViewControllerDelegate {
|
||||||
|
func didSaveInstance(url: URL) {
|
||||||
|
dismiss(animated: true) {
|
||||||
|
self.show(InstanceTimelineViewController(for: url), sender: nil)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func didUnsaveInstance(url: URL) {
|
||||||
|
dismiss(animated: true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,39 @@
|
||||||
|
//
|
||||||
|
// FindInstanceViewController.swift
|
||||||
|
// Tusker
|
||||||
|
//
|
||||||
|
// Created by Shadowfacts on 12/19/19.
|
||||||
|
// Copyright © 2019 Shadowfacts. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
class FindInstanceViewController: InstanceSelectorTableViewController {
|
||||||
|
|
||||||
|
var instanceTimelineDelegate: InstanceTimelineViewControllerDelegate?
|
||||||
|
|
||||||
|
override func viewDidLoad() {
|
||||||
|
super.viewDidLoad()
|
||||||
|
|
||||||
|
delegate = self
|
||||||
|
|
||||||
|
searchController.hidesNavigationBarDuringPresentation = false
|
||||||
|
|
||||||
|
navigationItem.leftBarButtonItem = UIBarButtonItem(barButtonSystemItem: .cancel, target: self, action: #selector(cancelButtonPressed))
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Interaction
|
||||||
|
|
||||||
|
@objc func cancelButtonPressed() {
|
||||||
|
dismiss(animated: true)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
extension FindInstanceViewController: InstanceSelectorTableViewControllerDelegate {
|
||||||
|
func didSelectInstance(url: URL) {
|
||||||
|
let instanceTimelineController = InstanceTimelineViewController(for: url)
|
||||||
|
instanceTimelineController.delegate = instanceTimelineDelegate
|
||||||
|
show(instanceTimelineController, sender: self)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,71 @@
|
||||||
|
//
|
||||||
|
// InstanceTimelineViewController.swift
|
||||||
|
// Tusker
|
||||||
|
//
|
||||||
|
// Created by Shadowfacts on 12/19/19.
|
||||||
|
// Copyright © 2019 Shadowfacts. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
protocol InstanceTimelineViewControllerDelegate {
|
||||||
|
func didSaveInstance(url: URL)
|
||||||
|
func didUnsaveInstance(url: URL)
|
||||||
|
}
|
||||||
|
|
||||||
|
class InstanceTimelineViewController: TimelineTableViewController {
|
||||||
|
|
||||||
|
var delegate: InstanceTimelineViewControllerDelegate?
|
||||||
|
|
||||||
|
let instanceURL: URL
|
||||||
|
|
||||||
|
var toggleSaveButton: UIBarButtonItem!
|
||||||
|
var toggleSaveButtonTitle: String {
|
||||||
|
if SavedInstanceManager.shared.isSaved(instanceURL) {
|
||||||
|
return NSLocalizedString("Unsave", comment: "unsave instance button")
|
||||||
|
} else {
|
||||||
|
return NSLocalizedString("Save", comment: "save instance button")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
init(for url: URL) {
|
||||||
|
self.instanceURL = url
|
||||||
|
|
||||||
|
super.init(for: .instance(instanceURL: url))
|
||||||
|
}
|
||||||
|
|
||||||
|
required init?(coder aDecoder: NSCoder) {
|
||||||
|
fatalError("init(coder:) has not been implemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
override func viewDidLoad() {
|
||||||
|
super.viewDidLoad()
|
||||||
|
|
||||||
|
toggleSaveButton = UIBarButtonItem(title: toggleSaveButtonTitle, style: .plain, target: self, action: #selector(toggleSaveButtonPressed))
|
||||||
|
navigationItem.rightBarButtonItem = toggleSaveButton
|
||||||
|
|
||||||
|
NotificationCenter.default.addObserver(self, selector: #selector(savedInstancesChanged), name: .savedInstancesChanged, object: nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc func savedInstancesChanged() {
|
||||||
|
toggleSaveButton.title = toggleSaveButtonTitle
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Table view delegate
|
||||||
|
|
||||||
|
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
|
||||||
|
// no-op, we don't currently support viewing whole conversations from other instances
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Interaction
|
||||||
|
@objc func toggleSaveButtonPressed() {
|
||||||
|
if SavedInstanceManager.shared.isSaved(instanceURL) {
|
||||||
|
SavedInstanceManager.shared.remove(instanceURL)
|
||||||
|
delegate?.didUnsaveInstance(url: instanceURL)
|
||||||
|
} else {
|
||||||
|
SavedInstanceManager.shared.add(instanceURL)
|
||||||
|
delegate?.didSaveInstance(url: instanceURL)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -100,6 +100,9 @@ class UserActivityManager {
|
||||||
case .public(local: false):
|
case .public(local: false):
|
||||||
activity.title = NSLocalizedString("Show Federated Timeline", comment: "federated timeline shortcut title")
|
activity.title = NSLocalizedString("Show Federated Timeline", comment: "federated timeline shortcut title")
|
||||||
activity.suggestedInvocationPhrase = NSLocalizedString("Show my federated timeline", comment: "federated timeline invocation phrase")
|
activity.suggestedInvocationPhrase = NSLocalizedString("Show my federated timeline", comment: "federated timeline invocation phrase")
|
||||||
|
case let .instance(instance):
|
||||||
|
activity.title = String(format: NSLocalizedString("Show %@", comment: "show instance timeline shortcut title"), instance.host!)
|
||||||
|
activity.suggestedInvocationPhrase = String(format: NSLocalizedString("Show the instance %@", comment: "instance timeline shortcut invocation phrase"), instance.host!)
|
||||||
case let .tag(hashtag):
|
case let .tag(hashtag):
|
||||||
activity.title = String(format: NSLocalizedString("Show #%@", comment: "show hashtag shortcut title"), hashtag)
|
activity.title = String(format: NSLocalizedString("Show #%@", comment: "show hashtag shortcut title"), hashtag)
|
||||||
activity.suggestedInvocationPhrase = String(format: NSLocalizedString("Show the %@ hashtag", comment: "hashtag shortcut invocation phrase"), hashtag)
|
activity.suggestedInvocationPhrase = String(format: NSLocalizedString("Show the %@ hashtag", comment: "hashtag shortcut invocation phrase"), hashtag)
|
||||||
|
|
Loading…
Reference in New Issue