Add local saved hashtags

Closes #66
This commit is contained in:
Shadowfacts 2019-12-19 21:20:29 -05:00
parent 6831ab5385
commit ae6a0513e4
Signed by: shadowfacts
GPG Key ID: 94A5AB95422746E5
8 changed files with 296 additions and 34 deletions

View File

@ -8,7 +8,7 @@
import Foundation
public class Hashtag: Decodable {
public class Hashtag: Codable {
public let name: String
public let url: URL
public let history: [History]?
@ -27,7 +27,7 @@ public class Hashtag: Decodable {
}
extension Hashtag {
public class History: Decodable {
public class History: Codable {
public let day: Date
public let uses: Int
public let accounts: Int
@ -42,7 +42,7 @@ extension Hashtag {
extension Hashtag: Equatable, Hashable {
public static func ==(lhs: Hashtag, rhs: Hashtag) -> Bool {
return lhs.url == rhs.url
return lhs.name == rhs.name
}
public func hash(into hasher: inout Hasher) {

View File

@ -164,6 +164,9 @@
D68632AB21ED8319008C716E /* GMImagePickerController.h in Headers */ = {isa = PBXBuildFile; fileRef = D686329121ED8319008C716E /* GMImagePickerController.h */; settings = {ATTRIBUTES = (Public, ); }; };
D68632AC21ED8319008C716E /* GMImagePicker.strings in Resources */ = {isa = PBXBuildFile; fileRef = D686329321ED8319008C716E /* GMImagePicker.strings */; };
D68FEC4F232C5BC300C84F23 /* SegmentedPageViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D68FEC4E232C5BC300C84F23 /* SegmentedPageViewController.swift */; };
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 */; };
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 */; };
@ -435,6 +438,9 @@
D686329121ED8319008C716E /* GMImagePickerController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = GMImagePickerController.h; sourceTree = "<group>"; };
D686329421ED8319008C716E /* de */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = de; path = GMImagePicker.strings; 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>"; };
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>"; };
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>"; };
@ -731,6 +737,7 @@
isa = PBXGroup;
children = (
D627943D23A564D400D38C68 /* ExploreViewController.swift */,
D6945C3323AC6431005C403C /* AddSavedHashtagViewController.swift */,
);
path = Explore;
sourceTree = "<group>";
@ -781,6 +788,7 @@
D641C782213DD7F0004B4513 /* Main */,
D641C783213DD7FE004B4513 /* Onboarding */,
D641C781213DD7DD004B4513 /* Timeline */,
D6945C3023AC4D21005C403C /* Hashtag Timeline */,
D641C784213DD819004B4513 /* Profile */,
D641C785213DD83B004B4513 /* Conversation */,
D641C786213DD852004B4513 /* Notifications */,
@ -1057,6 +1065,14 @@
path = de.lproj;
sourceTree = "<group>";
};
D6945C3023AC4D21005C403C /* Hashtag Timeline */ = {
isa = PBXGroup;
children = (
D6945C3123AC4D36005C403C /* HashtagTimelineViewController.swift */,
);
path = "Hashtag Timeline";
sourceTree = "<group>";
};
D6A3BC7223218C6E00FD64D5 /* Utilities */ = {
isa = PBXGroup;
children = (
@ -1209,6 +1225,7 @@
D6D4DDCF212518A000E1C4BB /* AppDelegate.swift */,
D64D0AAC2128D88B005A6F37 /* LocalData.swift */,
D627FF75217E923E00CC0648 /* DraftsManager.swift */,
D6945C2E23AC47C3005C403C /* SavedHashtagsManager.swift */,
D6028B9A2150811100F223B9 /* MastodonCache.swift */,
D6C693EE216192C2007D6A6D /* TuskerNavigationDelegate.swift */,
D6F1F84E2193B9BE00F5FE67 /* Caching */,
@ -1641,6 +1658,7 @@
0411610022B442870030A9B7 /* AttachmentViewController.swift in Sources */,
D611C2CF232DC61100C86A49 /* HashtagTableViewCell.swift in Sources */,
D627944F23A9C99800D38C68 /* EditListAccountsViewController.swift in Sources */,
D6945C3423AC6431005C403C /* AddSavedHashtagViewController.swift in Sources */,
D627943723A552C200D38C68 /* BookmarkStatusActivity.swift in Sources */,
D62D2426217ABF63005076CC /* UserActivityType.swift in Sources */,
D66362712136338600C9CBA2 /* ComposeViewController.swift in Sources */,
@ -1680,6 +1698,7 @@
0427033822B30F5F000D31B6 /* BehaviorPrefsView.swift in Sources */,
D627943923A553B600D38C68 /* UnbookmarkStatusActivity.swift in Sources */,
D64D0AAD2128D88B005A6F37 /* LocalData.swift in Sources */,
D6945C2F23AC47C3005C403C /* SavedHashtagsManager.swift in Sources */,
D6C94D892139E6EC00CB5196 /* AttachmentView.swift in Sources */,
D6C693EF216192C2007D6A6D /* TuskerNavigationDelegate.swift in Sources */,
D6C94D872139E62700CB5196 /* LargeImageViewController.swift in Sources */,
@ -1703,6 +1722,7 @@
D64F80E2215875CC00BEF393 /* XCBActionType.swift in Sources */,
04586B4322B301470021BD04 /* AppearancePrefsView.swift in Sources */,
D67C57AF21E28EAD00C3118B /* Array+Uniques.swift in Sources */,
D6945C3223AC4D36005C403C /* HashtagTimelineViewController.swift in Sources */,
D66362752137068A00C9CBA2 /* Visibility+Helpers.swift in Sources */,
D6C693FC2162FE6F007D6A6D /* LoadingViewController.swift in Sources */,
D646C95A213B5D0500269FB5 /* LargeImageInteractionController.swift in Sources */,

View File

@ -0,0 +1,65 @@
//
// 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: .userInitiated).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

@ -0,0 +1,58 @@
//
// AddSavedHashtagViewController.swift
// Tusker
//
// Created by Shadowfacts on 12/19/19.
// Copyright © 2019 Shadowfacts. All rights reserved.
//
import UIKit
import Pachyderm
class AddSavedHashtagViewController: SearchResultsViewController {
var searchController: UISearchController!
override func viewDidLoad() {
super.viewDidLoad()
delegate = self
onlySections = [.hashtags]
searchController = UISearchController(searchResultsController: nil)
searchController.obscuresBackgroundDuringPresentation = false
searchController.hidesNavigationBarDuringPresentation = false
searchController.searchBar.autocapitalizationType = .none
searchController.searchBar.placeholder = NSLocalizedString("Search for hashtags to save", comment: "add saved hashtag search field placeholder")
searchController.searchBar.delegate = self
definesPresentationContext = true
navigationItem.searchController = searchController
navigationItem.hidesSearchBarWhenScrolling = false
navigationItem.leftBarButtonItem = UIBarButtonItem(barButtonSystemItem: .cancel, target: self, action: #selector(cancelButtonPressed))
}
override func performSearch(query: String?) {
if let query = query, !query.starts(with: "#") {
super.performSearch(query: "#\(query)")
} else {
super.performSearch(query: query)
}
}
// MARK: - Interaction
@objc func cancelButtonPressed() {
dismiss(animated: true)
}
}
extension AddSavedHashtagViewController: SearchResultsViewControllerDelegate {
func selectedSearchResult(hashtag: Hashtag) {
SavedHashtagsManager.shared.add(hashtag)
dismiss(animated: true)
}
}

View File

@ -43,7 +43,7 @@ class ExploreViewController: EnhancedTableViewController {
cell.accessoryType = .disclosureIndicator
case let .list(list):
cell.imageView!.image = nil
cell.imageView!.image = UIImage(systemName: "list.bullet")
cell.textLabel!.text = list.title
cell.accessoryType = .disclosureIndicator
@ -51,6 +51,16 @@ class ExploreViewController: EnhancedTableViewController {
cell.imageView!.image = UIImage(systemName: "plus")
cell.textLabel!.text = NSLocalizedString("New List...", comment: "new list nav item title")
cell.accessoryType = .none
case let .savedHashtag(hashtag):
cell.imageView!.image = UIImage(systemName: "number")
cell.textLabel!.text = hashtag.name
cell.accessoryType = .disclosureIndicator
case .addSavedHashtag:
cell.imageView!.image = UIImage(systemName: "plus")
cell.textLabel!.text = NSLocalizedString("Save Hashtag...", comment: "save hashtag nav item title")
cell.accessoryType = .none
}
return cell
@ -58,9 +68,10 @@ class ExploreViewController: EnhancedTableViewController {
dataSource.exploreController = self
var snapshot = NSDiffableDataSourceSnapshot<Section, Item>()
snapshot.appendSections([.bookmarks, .lists])
snapshot.appendSections([.bookmarks, .lists, .savedHashtags])
snapshot.appendItems([.bookmarks], toSection: .bookmarks)
snapshot.appendItems([.addList], toSection: .lists)
snapshot.appendItems(SavedHashtagsManager.shared.sorted.map { .savedHashtag($0) } + [.addSavedHashtag], toSection: .savedHashtags)
// the initial, static items should not be displayed with an animation
UIView.performWithoutAnimation {
dataSource.apply(snapshot)
@ -77,6 +88,8 @@ class ExploreViewController: EnhancedTableViewController {
navigationItem.searchController = searchController
navigationItem.hidesSearchBarWhenScrolling = false
NotificationCenter.default.addObserver(self, selector: #selector(savedHashtagsChanged), name: .savedHashtagsChanged, object: nil)
reloadLists()
}
@ -88,16 +101,48 @@ class ExploreViewController: EnhancedTableViewController {
}
var snapshot = self.dataSource.snapshot()
snapshot.deleteSections([.lists])
snapshot.appendSections([.lists])
snapshot.deleteItems(snapshot.itemIdentifiers(inSection: .lists))
snapshot.appendItems(lists.map { .list($0) } + [.addList], toSection: .lists)
DispatchQueue.main.async {
self.dataSource.apply(snapshot)
}
}
}
@objc func savedHashtagsChanged() {
var snapshot = dataSource.snapshot()
snapshot.deleteItems(snapshot.itemIdentifiers(inSection: .savedHashtags))
snapshot.appendItems(SavedHashtagsManager.shared.sorted.map { .savedHashtag($0) } + [.addSavedHashtag], toSection: .savedHashtags)
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)
alert.addAction(UIAlertAction(title: NSLocalizedString("Cancel", comment: "delete list alert cancel button"), style: .cancel, handler: nil))
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
guard case .success(_, _) = response else {
fatalError()
}
var snapshot = self.dataSource.snapshot()
snapshot.deleteItems([.list(list)])
DispatchQueue.main.async {
self.dataSource.apply(snapshot)
}
}
}))
present(alert, animated: true)
}
func removeSavedHashtag(_ hashtag: Hashtag) {
SavedHashtagsManager.shared.remove(hashtag)
}
// MARK: - Table view delegate
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
@ -133,6 +178,14 @@ class ExploreViewController: EnhancedTableViewController {
}
}))
present(alert, animated: true)
case let .savedHashtag(hashtag):
show(HashtagTimelineViewController(for: hashtag), sender: nil)
case .addSavedHashtag:
tableView.selectRow(at: nil, animated: true, scrollPosition: .none)
let navController = UINavigationController(rootViewController: AddSavedHashtagViewController())
present(navController, animated: true)
}
}
@ -146,12 +199,15 @@ extension ExploreViewController {
enum Section: CaseIterable {
case bookmarks
case lists
case savedHashtags
}
enum Item: Hashable {
case bookmarks
case list(List)
case addList
case savedHashtag(Hashtag)
case addSavedHashtag
static func == (lhs: ExploreViewController.Item, rhs: ExploreViewController.Item) -> Bool {
switch (lhs, rhs) {
case (.bookmarks, .bookmarks):
@ -160,6 +216,10 @@ extension ExploreViewController {
return a.id == b.id
case (.addList, .addList):
return true
case let (.savedHashtag(a), .savedHashtag(b)):
return a == b
case (.addSavedHashtag, .addSavedHashtag):
return true
default:
return false
}
@ -173,6 +233,11 @@ extension ExploreViewController {
hasher.combine(list.id)
case .addList:
hasher.combine("addList")
case let .savedHashtag(hashtag):
hasher.combine("savedHashtag")
hasher.combine(hashtag.name)
case .addSavedHashtag:
hasher.combine("addSavedHashtag")
}
}
}
@ -185,43 +250,39 @@ extension ExploreViewController {
switch section {
case 1:
return NSLocalizedString("Lists", comment: "explore lists section title")
case 2:
return NSLocalizedString("Saved Hashtags", comment: "explore saved hashtags section title")
default:
return nil
}
}
override func tableView(_ tableView: UITableView, canEditRowAt indexPath: IndexPath) -> Bool {
if case .list(_) = itemIdentifier(for: indexPath) {
switch itemIdentifier(for: indexPath) {
case .list(_):
return true
case .savedHashtag(_):
return true
default:
return false
}
return false
}
override func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCell.EditingStyle, forRowAt indexPath: IndexPath) {
guard editingStyle == .delete,
case let .list(list) = itemIdentifier(for: indexPath) else {
return
let exploreController = exploreController else {
return
}
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)
alert.addAction(UIAlertAction(title: NSLocalizedString("Cancel", comment: "delete list alert cancel button"), style: .cancel, handler: nil))
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
guard case .success(_, _) = response else {
fatalError()
}
var snapshot = self.snapshot()
snapshot.deleteItems([.list(list)])
DispatchQueue.main.async {
self.apply(snapshot)
}
}
}))
self.exploreController?.present(alert, animated: true)
switch itemIdentifier(for: indexPath) {
case let .list(list):
exploreController.deleteList(list)
case let .savedHashtag(hashtag):
exploreController.removeSavedHashtag(hashtag)
default:
return
}
}
}

View File

@ -0,0 +1,58 @@
//
// HashtagTimelineViewController.swift
// Tusker
//
// Created by Shadowfacts on 12/19/19.
// Copyright © 2019 Shadowfacts. All rights reserved.
//
import UIKit
import Pachyderm
class HashtagTimelineViewController: TimelineTableViewController {
let hashtag: Hashtag
var toggleSaveButton: UIBarButtonItem!
var toggleSaveButtonTitle: String {
if SavedHashtagsManager.shared.isSaved(hashtag) {
return NSLocalizedString("Unsave", comment: "unsave hashtag button")
} else {
return NSLocalizedString("Save", comment: "save hashtag button")
}
}
init(for hashtag: Hashtag) {
self.hashtag = hashtag
super.init(for: .tag(hashtag: hashtag.name))
}
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(savedHashtagsChanged), name: .savedHashtagsChanged, object: nil)
}
@objc func savedHashtagsChanged() {
toggleSaveButton.title = toggleSaveButtonTitle
}
// MARK: - Interaction
@objc func toggleSaveButtonPressed() {
if SavedHashtagsManager.shared.isSaved(hashtag) {
SavedHashtagsManager.shared.remove(hashtag)
} else {
SavedHashtagsManager.shared.add(hashtag)
}
}
}

View File

@ -76,7 +76,7 @@ extension TuskerNavigationDelegate where Self: UIViewController {
}
func selected(tag: Hashtag) {
show(TimelineTableViewController(for: .tag(hashtag: tag.name)), sender: self)
show(HashtagTimelineViewController(for: tag), sender: self)
}
func selected(url: URL) {

View File

@ -181,7 +181,7 @@ class ContentLabel: LinkLabel {
if let mention = getMention(for: url, text: text) {
return ProfileTableViewController(accountID: mention.id)
} else if let tag = getHashtag(for: url, text: text) {
return TimelineTableViewController(for: .tag(hashtag: tag.name))
return HashtagTimelineViewController(for: tag)
} else {
return SFSafariViewController(url: url)
}