Add ability to save and view instance public timelines

This commit is contained in:
Shadowfacts 2019-12-19 22:41:23 -05:00
parent f92a2acc97
commit 377b5f5c85
Signed by: shadowfacts
GPG Key ID: 94A5AB95422746E5
12 changed files with 275 additions and 16 deletions

View File

@ -81,7 +81,7 @@ public class Client {
}
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.queryItems = request.queryParameters.queryItems
guard let url = components.url else { return nil }

View File

@ -11,6 +11,7 @@ import Foundation
public enum Timeline {
case home
case `public`(local: Bool)
case instance(instanceURL: URL)
case tag(hashtag: String)
case list(id: String)
case direct
@ -21,7 +22,7 @@ extension Timeline {
switch self {
case .home:
return "/api/v1/timelines/home"
case .public:
case .public, .instance(_):
return "/api/v1/timelines/public"
case let .tag(hashtag):
return "/api/v1/timelines/tag/\(hashtag)"
@ -33,7 +34,12 @@ extension Timeline {
}
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 {
request.queryParameters.append("local" => true)
}
@ -51,6 +57,8 @@ extension Timeline: Codable {
self = .home
case "public":
self = .public(local: try container.decode(Bool.self, forKey: .local))
case "instanceURL":
self = .instance(instanceURL: try container.decode(URL.self, forKey: .instanceURL))
case "tag":
self = .tag(hashtag: try container.decode(String.self, forKey: .hashtag))
case "list":
@ -70,6 +78,9 @@ extension Timeline: Codable {
case let .public(local):
try container.encode("public", forKey: .type)
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):
try container.encode("tag", forKey: .type)
try container.encode(hashtag, forKey: .hashtag)
@ -84,6 +95,7 @@ extension Timeline: Codable {
enum CodingKeys: String, CodingKey {
case type
case local
case instanceURL
case hashtag
case listID
}

View File

@ -10,12 +10,14 @@ import Foundation
public struct Request<ResultType: Decodable> {
let method: Method
let baseURL: URL?
let path: String
let body: Body
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.baseURL = baseURL
self.path = path
self.body = body
self.queryParameters = queryParameters

View File

@ -167,6 +167,9 @@
D6945C2F23AC47C3005C403C /* SavedHashtagsManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6945C2E23AC47C3005C403C /* SavedHashtagsManager.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 */; };
D6A3BC7923218E9200FD64D5 /* NotificationGroup.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6A3BC7823218E9200FD64D5 /* NotificationGroup.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>"; };
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>"; };
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>"; };
@ -738,6 +744,7 @@
children = (
D627943D23A564D400D38C68 /* ExploreViewController.swift */,
D6945C3323AC6431005C403C /* AddSavedHashtagViewController.swift */,
D6945C3923AC75E2005C403C /* FindInstanceViewController.swift */,
);
path = Explore;
sourceTree = "<group>";
@ -788,7 +795,6 @@
D641C782213DD7F0004B4513 /* Main */,
D641C783213DD7FE004B4513 /* Onboarding */,
D641C781213DD7DD004B4513 /* Timeline */,
D6945C3023AC4D21005C403C /* Hashtag Timeline */,
D641C784213DD819004B4513 /* Profile */,
D641C785213DD83B004B4513 /* Conversation */,
D641C786213DD852004B4513 /* Notifications */,
@ -812,6 +818,8 @@
children = (
D6BC9DD6232D7811002CA326 /* TimelinesPageViewController.swift */,
D6F953EB212519E700CF0F2B /* TimelineTableViewController.swift */,
D6945C3123AC4D36005C403C /* HashtagTimelineViewController.swift */,
D6945C3723AC739F005C403C /* InstanceTimelineViewController.swift */,
);
path = Timeline;
sourceTree = "<group>";
@ -1065,14 +1073,6 @@
path = de.lproj;
sourceTree = "<group>";
};
D6945C3023AC4D21005C403C /* Hashtag Timeline */ = {
isa = PBXGroup;
children = (
D6945C3123AC4D36005C403C /* HashtagTimelineViewController.swift */,
);
path = "Hashtag Timeline";
sourceTree = "<group>";
};
D6A3BC7223218C6E00FD64D5 /* Utilities */ = {
isa = PBXGroup;
children = (
@ -1226,6 +1226,7 @@
D64D0AAC2128D88B005A6F37 /* LocalData.swift */,
D627FF75217E923E00CC0648 /* DraftsManager.swift */,
D6945C2E23AC47C3005C403C /* SavedHashtagsManager.swift */,
D6945C3523AC6C09005C403C /* SavedInstancesManager.swift */,
D6028B9A2150811100F223B9 /* MastodonCache.swift */,
D6C693EE216192C2007D6A6D /* TuskerNavigationDelegate.swift */,
D6F1F84E2193B9BE00F5FE67 /* Caching */,
@ -1653,6 +1654,7 @@
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 */,
D6F953F021251A2900CF0F2B /* MastodonController.swift in Sources */,
0411610022B442870030A9B7 /* AttachmentViewController.swift in Sources */,
@ -1666,6 +1668,7 @@
D6028B9B2150811100F223B9 /* MastodonCache.swift in Sources */,
D6A3BC802321B7E600FD64D5 /* FollowNotificationGroupTableViewCell.swift in Sources */,
D627944D23A9A03D00D38C68 /* ListTimelineViewController.swift in Sources */,
D6945C3823AC739F005C403C /* InstanceTimelineViewController.swift in Sources */,
D62D2422217AA7E1005076CC /* UserActivityManager.swift in Sources */,
D60D2B8223844C71001B87A3 /* BaseStatusTableViewCell.swift in Sources */,
D62D2424217ABF3F005076CC /* NSUserActivity+Extensions.swift in Sources */,
@ -1691,6 +1694,7 @@
D6DD353F22F502EC00A9563A /* Preferences+Notification.swift in Sources */,
D63661C02381C144004B9E16 /* PreferencesNavigationController.swift in Sources */,
D627944A23A6AD6100D38C68 /* BookmarksTableViewController.swift in Sources */,
D6945C3A23AC75E2005C403C /* FindInstanceViewController.swift in Sources */,
D6AEBB4523216AF800E5038B /* FollowAccountActivity.swift in Sources */,
D6538945214D6D7500E3CEFC /* TableViewSwipeActionProvider.swift in Sources */,
D6E0DC8E216EDF1E00369478 /* Previewing.swift in Sources */,

View File

@ -16,6 +16,8 @@ extension Timeline {
return "Home"
case let .public(local):
return local ? "Local" : "Federated"
case let .instance(instance):
return instance.host!
case let .tag(hashtag):
return "#\(hashtag)"
case .list:
@ -35,6 +37,8 @@ extension Timeline {
} else {
return UIImage(systemName: "globe")
}
case .instance(_):
return UIImage(systemName: "globe")
default:
return nil
}

View File

@ -16,7 +16,7 @@ class SavedHashtagsManager: Codable {
private static var archiveURL = SavedHashtagsManager.documentsDirectory.appendingPathComponent("saved_hashtags").appendingPathExtension("plist")
static func save() {
DispatchQueue.global(qos: .userInitiated).async {
DispatchQueue.global(qos: .utility).async {
let encoder = PropertyListEncoder()
let data = try? encoder.encode(shared)
try? data?.write(to: archiveURL, options: .noFileProtection)
@ -36,7 +36,7 @@ class SavedHashtagsManager: Codable {
private var savedHashtags: [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 {

View File

@ -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")
}

View File

@ -61,6 +61,16 @@ class ExploreViewController: EnhancedTableViewController {
cell.imageView!.image = UIImage(systemName: "plus")
cell.textLabel!.text = NSLocalizedString("Save Hashtag...", comment: "save hashtag nav item title")
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
@ -68,10 +78,11 @@ class ExploreViewController: EnhancedTableViewController {
dataSource.exploreController = self
var snapshot = NSDiffableDataSourceSnapshot<Section, Item>()
snapshot.appendSections([.bookmarks, .lists, .savedHashtags])
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)
// the initial, static items should not be displayed with an animation
UIView.performWithoutAnimation {
dataSource.apply(snapshot)
@ -89,6 +100,7 @@ class ExploreViewController: EnhancedTableViewController {
navigationItem.hidesSearchBarWhenScrolling = false
NotificationCenter.default.addObserver(self, selector: #selector(savedHashtagsChanged), name: .savedHashtagsChanged, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(savedInstancesChanged), name: .savedInstancesChanged, object: nil)
reloadLists()
}
@ -117,6 +129,13 @@ class ExploreViewController: EnhancedTableViewController {
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) {
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)
@ -143,6 +162,10 @@ class ExploreViewController: EnhancedTableViewController {
SavedHashtagsManager.shared.remove(hashtag)
}
func removeSavedInstance(_ instanceURL: URL) {
SavedInstanceManager.shared.remove(instanceURL)
}
// MARK: - Table view delegate
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
@ -188,6 +211,16 @@ class ExploreViewController: EnhancedTableViewController {
tableView.selectRow(at: nil, animated: true, scrollPosition: .none)
let navController = UINavigationController(rootViewController: AddSavedHashtagViewController())
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 lists
case savedHashtags
case savedInstances
}
enum Item: Hashable {
case bookmarks
@ -209,6 +243,8 @@ extension ExploreViewController {
case addList
case savedHashtag(Hashtag)
case addSavedHashtag
case savedInstance(URL)
case findInstance
static func == (lhs: ExploreViewController.Item, rhs: ExploreViewController.Item) -> Bool {
switch (lhs, rhs) {
@ -222,6 +258,10 @@ extension ExploreViewController {
return a == b
case (.addSavedHashtag, .addSavedHashtag):
return true
case let (.savedInstance(a), .savedInstance(b)):
return a == b
case (.findInstance, .findInstance):
return true
default:
return false
}
@ -240,6 +280,11 @@ extension ExploreViewController {
hasher.combine(hashtag.name)
case .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")
case 2:
return NSLocalizedString("Saved Hashtags", comment: "explore saved hashtags section title")
case 3:
return NSLocalizedString("Instance Timelines", comment: "explore instance timelines section title")
default:
return nil
}
@ -265,6 +312,8 @@ extension ExploreViewController {
return true
case .savedHashtag(_):
return true
case .savedInstance(_):
return true
default:
return false
}
@ -281,6 +330,8 @@ extension ExploreViewController {
exploreController.deleteList(list)
case let .savedHashtag(hashtag):
exploreController.removeSavedHashtag(hashtag)
case let .savedInstance(url):
exploreController.removeSavedInstance(url)
default:
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)
}
}

View File

@ -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)
}
}

View File

@ -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)
}
}
}

View File

@ -100,6 +100,9 @@ class UserActivityManager {
case .public(local: false):
activity.title = NSLocalizedString("Show Federated Timeline", comment: "federated timeline shortcut title")
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):
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)