Compare commits
5 Commits
d8bf770902
...
e6a5b899be
Author | SHA1 | Date |
---|---|---|
Shadowfacts | e6a5b899be | |
Shadowfacts | 60bf3b2e33 | |
Shadowfacts | b465838b71 | |
Shadowfacts | 21bd716844 | |
Shadowfacts | 523fb91b21 |
|
@ -82,14 +82,14 @@ public final class Account: AccountProtocol, Decodable {
|
||||||
return Request<Empty>(method: .delete, path: "/api/v1/suggestions/\(account.id)")
|
return Request<Empty>(method: .delete, path: "/api/v1/suggestions/\(account.id)")
|
||||||
}
|
}
|
||||||
|
|
||||||
public static func getFollowers(_ account: Account, range: RequestRange = .default) -> Request<[Account]> {
|
public static func getFollowers(_ accountID: String, range: RequestRange = .default) -> Request<[Account]> {
|
||||||
var request = Request<[Account]>(method: .get, path: "/api/v1/accounts/\(account.id)/followers")
|
var request = Request<[Account]>(method: .get, path: "/api/v1/accounts/\(accountID)/followers")
|
||||||
request.range = range
|
request.range = range
|
||||||
return request
|
return request
|
||||||
}
|
}
|
||||||
|
|
||||||
public static func getFollowing(_ account: Account, range: RequestRange = .default) -> Request<[Account]> {
|
public static func getFollowing(_ accountID: String, range: RequestRange = .default) -> Request<[Account]> {
|
||||||
var request = Request<[Account]>(method: .get, path: "/api/v1/accounts/\(account.id)/following")
|
var request = Request<[Account]>(method: .get, path: "/api/v1/accounts/\(accountID)/following")
|
||||||
request.range = range
|
request.range = range
|
||||||
return request
|
return request
|
||||||
}
|
}
|
||||||
|
|
|
@ -310,6 +310,11 @@
|
||||||
D6F0B17524A3A1AA001E48C3 /* MainSidebarViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6F0B17424A3A1AA001E48C3 /* MainSidebarViewController.swift */; };
|
D6F0B17524A3A1AA001E48C3 /* MainSidebarViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6F0B17424A3A1AA001E48C3 /* MainSidebarViewController.swift */; };
|
||||||
D6F2E965249E8BFD005846BB /* IssueReporterViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6F2E963249E8BFD005846BB /* IssueReporterViewController.swift */; };
|
D6F2E965249E8BFD005846BB /* IssueReporterViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6F2E963249E8BFD005846BB /* IssueReporterViewController.swift */; };
|
||||||
D6F2E966249E8BFD005846BB /* IssueReporterViewController.xib in Resources */ = {isa = PBXBuildFile; fileRef = D6F2E964249E8BFD005846BB /* IssueReporterViewController.xib */; };
|
D6F2E966249E8BFD005846BB /* IssueReporterViewController.xib in Resources */ = {isa = PBXBuildFile; fileRef = D6F2E964249E8BFD005846BB /* IssueReporterViewController.xib */; };
|
||||||
|
D6F6A54C291EF6FE00F496A8 /* EditListSearchResultsContainerViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6F6A54B291EF6FE00F496A8 /* EditListSearchResultsContainerViewController.swift */; };
|
||||||
|
D6F6A54E291EF7E100F496A8 /* EditListSearchFollowingViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6F6A54D291EF7E100F496A8 /* EditListSearchFollowingViewController.swift */; };
|
||||||
|
D6F6A550291F058600F496A8 /* CreateListService.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6F6A54F291F058600F496A8 /* CreateListService.swift */; };
|
||||||
|
D6F6A552291F098700F496A8 /* RenameListService.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6F6A551291F098700F496A8 /* RenameListService.swift */; };
|
||||||
|
D6F6A554291F0D9600F496A8 /* DeleteListService.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6F6A553291F0D9600F496A8 /* DeleteListService.swift */; };
|
||||||
D6F953F021251A2900CF0F2B /* MastodonController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6F953EF21251A2900CF0F2B /* MastodonController.swift */; };
|
D6F953F021251A2900CF0F2B /* MastodonController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6F953EF21251A2900CF0F2B /* MastodonController.swift */; };
|
||||||
D6FF9860255C717400845181 /* AccountSwitchingContainerViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6FF985F255C717400845181 /* AccountSwitchingContainerViewController.swift */; };
|
D6FF9860255C717400845181 /* AccountSwitchingContainerViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6FF985F255C717400845181 /* AccountSwitchingContainerViewController.swift */; };
|
||||||
/* End PBXBuildFile section */
|
/* End PBXBuildFile section */
|
||||||
|
@ -675,6 +680,11 @@
|
||||||
D6F0B17424A3A1AA001E48C3 /* MainSidebarViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainSidebarViewController.swift; sourceTree = "<group>"; };
|
D6F0B17424A3A1AA001E48C3 /* MainSidebarViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainSidebarViewController.swift; sourceTree = "<group>"; };
|
||||||
D6F2E963249E8BFD005846BB /* IssueReporterViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IssueReporterViewController.swift; sourceTree = "<group>"; };
|
D6F2E963249E8BFD005846BB /* IssueReporterViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IssueReporterViewController.swift; sourceTree = "<group>"; };
|
||||||
D6F2E964249E8BFD005846BB /* IssueReporterViewController.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = IssueReporterViewController.xib; sourceTree = "<group>"; };
|
D6F2E964249E8BFD005846BB /* IssueReporterViewController.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = IssueReporterViewController.xib; sourceTree = "<group>"; };
|
||||||
|
D6F6A54B291EF6FE00F496A8 /* EditListSearchResultsContainerViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditListSearchResultsContainerViewController.swift; sourceTree = "<group>"; };
|
||||||
|
D6F6A54D291EF7E100F496A8 /* EditListSearchFollowingViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditListSearchFollowingViewController.swift; sourceTree = "<group>"; };
|
||||||
|
D6F6A54F291F058600F496A8 /* CreateListService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreateListService.swift; sourceTree = "<group>"; };
|
||||||
|
D6F6A551291F098700F496A8 /* RenameListService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RenameListService.swift; sourceTree = "<group>"; };
|
||||||
|
D6F6A553291F0D9600F496A8 /* DeleteListService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeleteListService.swift; sourceTree = "<group>"; };
|
||||||
D6F953EF21251A2900CF0F2B /* MastodonController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonController.swift; sourceTree = "<group>"; };
|
D6F953EF21251A2900CF0F2B /* MastodonController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonController.swift; sourceTree = "<group>"; };
|
||||||
D6FF985F255C717400845181 /* AccountSwitchingContainerViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountSwitchingContainerViewController.swift; sourceTree = "<group>"; };
|
D6FF985F255C717400845181 /* AccountSwitchingContainerViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountSwitchingContainerViewController.swift; sourceTree = "<group>"; };
|
||||||
/* End PBXFileReference section */
|
/* End PBXFileReference section */
|
||||||
|
@ -833,6 +843,8 @@
|
||||||
children = (
|
children = (
|
||||||
D627944C23A9A03D00D38C68 /* ListTimelineViewController.swift */,
|
D627944C23A9A03D00D38C68 /* ListTimelineViewController.swift */,
|
||||||
D627944E23A9C99800D38C68 /* EditListAccountsViewController.swift */,
|
D627944E23A9C99800D38C68 /* EditListAccountsViewController.swift */,
|
||||||
|
D6F6A54B291EF6FE00F496A8 /* EditListSearchResultsContainerViewController.swift */,
|
||||||
|
D6F6A54D291EF7E100F496A8 /* EditListSearchFollowingViewController.swift */,
|
||||||
);
|
);
|
||||||
path = Lists;
|
path = Lists;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
|
@ -1482,6 +1494,9 @@
|
||||||
D6E9CDA7281A427800BBC98E /* PostService.swift */,
|
D6E9CDA7281A427800BBC98E /* PostService.swift */,
|
||||||
D61ABEFD28F1C92600B29151 /* FavoriteService.swift */,
|
D61ABEFD28F1C92600B29151 /* FavoriteService.swift */,
|
||||||
D621733228F1D5ED004C7DB1 /* ReblogService.swift */,
|
D621733228F1D5ED004C7DB1 /* ReblogService.swift */,
|
||||||
|
D6F6A54F291F058600F496A8 /* CreateListService.swift */,
|
||||||
|
D6F6A551291F098700F496A8 /* RenameListService.swift */,
|
||||||
|
D6F6A553291F0D9600F496A8 /* DeleteListService.swift */,
|
||||||
);
|
);
|
||||||
path = API;
|
path = API;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
|
@ -1751,6 +1766,7 @@
|
||||||
D6285B5321EA708700FE4B39 /* StatusFormat.swift in Sources */,
|
D6285B5321EA708700FE4B39 /* StatusFormat.swift in Sources */,
|
||||||
D6DD353D22F28CD000A9563A /* ContentWarningCopyMode.swift in Sources */,
|
D6DD353D22F28CD000A9563A /* ContentWarningCopyMode.swift in Sources */,
|
||||||
0427033A22B31269000D31B6 /* AdvancedPrefsView.swift in Sources */,
|
0427033A22B31269000D31B6 /* AdvancedPrefsView.swift in Sources */,
|
||||||
|
D6F6A54C291EF6FE00F496A8 /* EditListSearchResultsContainerViewController.swift in Sources */,
|
||||||
D662AEEF263A3B880082A153 /* PollFinishedTableViewCell.swift in Sources */,
|
D662AEEF263A3B880082A153 /* PollFinishedTableViewCell.swift in Sources */,
|
||||||
D626493C23C1000300612E6E /* AlbumTableViewCell.swift in Sources */,
|
D626493C23C1000300612E6E /* AlbumTableViewCell.swift in Sources */,
|
||||||
D62275A624F1C81800B82A16 /* ComposeReplyView.swift in Sources */,
|
D62275A624F1C81800B82A16 /* ComposeReplyView.swift in Sources */,
|
||||||
|
@ -1791,6 +1807,7 @@
|
||||||
D627944F23A9C99800D38C68 /* EditListAccountsViewController.swift in Sources */,
|
D627944F23A9C99800D38C68 /* EditListAccountsViewController.swift in Sources */,
|
||||||
D6945C3423AC6431005C403C /* AddSavedHashtagViewController.swift in Sources */,
|
D6945C3423AC6431005C403C /* AddSavedHashtagViewController.swift in Sources */,
|
||||||
D627943723A552C200D38C68 /* BookmarkStatusActivity.swift in Sources */,
|
D627943723A552C200D38C68 /* BookmarkStatusActivity.swift in Sources */,
|
||||||
|
D6F6A552291F098700F496A8 /* RenameListService.swift in Sources */,
|
||||||
D68ACE5D279B1ABA001CE8EB /* AssetPickerControlCollectionViewCell.swift in Sources */,
|
D68ACE5D279B1ABA001CE8EB /* AssetPickerControlCollectionViewCell.swift in Sources */,
|
||||||
D62D2426217ABF63005076CC /* UserActivityType.swift in Sources */,
|
D62D2426217ABF63005076CC /* UserActivityType.swift in Sources */,
|
||||||
D6AC956723C4347E008C9946 /* MainSceneDelegate.swift in Sources */,
|
D6AC956723C4347E008C9946 /* MainSceneDelegate.swift in Sources */,
|
||||||
|
@ -1908,6 +1925,7 @@
|
||||||
D681E4D3246E2AFF0053414F /* MuteConversationActivity.swift in Sources */,
|
D681E4D3246E2AFF0053414F /* MuteConversationActivity.swift in Sources */,
|
||||||
D6969EA0240C8384002843CE /* EmojiLabel.swift in Sources */,
|
D6969EA0240C8384002843CE /* EmojiLabel.swift in Sources */,
|
||||||
D64BC18623C1253A000D0238 /* AssetPreviewViewController.swift in Sources */,
|
D64BC18623C1253A000D0238 /* AssetPreviewViewController.swift in Sources */,
|
||||||
|
D6F6A550291F058600F496A8 /* CreateListService.swift in Sources */,
|
||||||
D6BEA24B291C6A2B002F4D01 /* AlertWithData.swift in Sources */,
|
D6BEA24B291C6A2B002F4D01 /* AlertWithData.swift in Sources */,
|
||||||
D61ABEFE28F1C92600B29151 /* FavoriteService.swift in Sources */,
|
D61ABEFE28F1C92600B29151 /* FavoriteService.swift in Sources */,
|
||||||
D663626221360B1900C9CBA2 /* Preferences.swift in Sources */,
|
D663626221360B1900C9CBA2 /* Preferences.swift in Sources */,
|
||||||
|
@ -1979,9 +1997,11 @@
|
||||||
D6C1B2082545D1EC00DAAA66 /* StatusCardView.swift in Sources */,
|
D6C1B2082545D1EC00DAAA66 /* StatusCardView.swift in Sources */,
|
||||||
D64BC18A23C16487000D0238 /* UnpinStatusActivity.swift in Sources */,
|
D64BC18A23C16487000D0238 /* UnpinStatusActivity.swift in Sources */,
|
||||||
D64D8CA92463B494006B0BAA /* MultiThreadDictionary.swift in Sources */,
|
D64D8CA92463B494006B0BAA /* MultiThreadDictionary.swift in Sources */,
|
||||||
|
D6F6A554291F0D9600F496A8 /* DeleteListService.swift in Sources */,
|
||||||
D68E6F5F253C9B2D001A1B4C /* BaseEmojiLabel.swift in Sources */,
|
D68E6F5F253C9B2D001A1B4C /* BaseEmojiLabel.swift in Sources */,
|
||||||
D6F0B12B24A3071C001E48C3 /* MainSplitViewController.swift in Sources */,
|
D6F0B12B24A3071C001E48C3 /* MainSplitViewController.swift in Sources */,
|
||||||
D6AEBB3E2321638100E5038B /* UIActivity+Types.swift in Sources */,
|
D6AEBB3E2321638100E5038B /* UIActivity+Types.swift in Sources */,
|
||||||
|
D6F6A54E291EF7E100F496A8 /* EditListSearchFollowingViewController.swift in Sources */,
|
||||||
D61AC1D5232E9FA600C54D2D /* InstanceSelectorTableViewController.swift in Sources */,
|
D61AC1D5232E9FA600C54D2D /* InstanceSelectorTableViewController.swift in Sources */,
|
||||||
D681E4D9246E346E0053414F /* AccountActivityItemSource.swift in Sources */,
|
D681E4D9246E346E0053414F /* AccountActivityItemSource.swift in Sources */,
|
||||||
D626493F23C101C500612E6E /* AlbumAssetCollectionViewController.swift in Sources */,
|
D626493F23C101C500612E6E /* AlbumAssetCollectionViewController.swift in Sources */,
|
||||||
|
|
|
@ -0,0 +1,70 @@
|
||||||
|
//
|
||||||
|
// CreateListService.swift
|
||||||
|
// Tusker
|
||||||
|
//
|
||||||
|
// Created by Shadowfacts on 11/11/22.
|
||||||
|
// Copyright © 2022 Shadowfacts. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
import Pachyderm
|
||||||
|
|
||||||
|
@MainActor
|
||||||
|
class CreateListService {
|
||||||
|
private let mastodonController: MastodonController
|
||||||
|
private let present: (UIViewController) -> Void
|
||||||
|
private let didCreateList: (@MainActor (List) -> Void)?
|
||||||
|
|
||||||
|
private var createAction: UIAlertAction?
|
||||||
|
|
||||||
|
init(mastodonController: MastodonController, present: @escaping (UIViewController) -> Void, didCreateList: (@MainActor (List) -> Void)?) {
|
||||||
|
self.mastodonController = mastodonController
|
||||||
|
self.present = present
|
||||||
|
self.didCreateList = didCreateList
|
||||||
|
}
|
||||||
|
|
||||||
|
func run() {
|
||||||
|
let alert = UIAlertController(title: NSLocalizedString("New List", comment: "new list alert title"), message: NSLocalizedString("Choose a title for your new list", comment: "new list alert message"), preferredStyle: .alert)
|
||||||
|
alert.addTextField { textField in
|
||||||
|
textField.addTarget(self, action: #selector(self.alertTextFieldValueChanged), for: .editingChanged)
|
||||||
|
}
|
||||||
|
alert.addAction(UIAlertAction(title: NSLocalizedString("Cancel", comment: "new list alert cancel button"), style: .cancel, handler: nil))
|
||||||
|
createAction = UIAlertAction(title: NSLocalizedString("Create List", comment: "new list create button"), style: .default, handler: { (_) in
|
||||||
|
let textField = alert.textFields!.first!
|
||||||
|
let title = textField.text ?? ""
|
||||||
|
Task {
|
||||||
|
await self.createList(with: title)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
createAction!.isEnabled = false
|
||||||
|
alert.addAction(createAction!)
|
||||||
|
present(alert)
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc private func alertTextFieldValueChanged(_ textField: UITextField) {
|
||||||
|
createAction?.isEnabled = textField.text?.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty == false
|
||||||
|
}
|
||||||
|
|
||||||
|
private func createList(with title: String) async {
|
||||||
|
do {
|
||||||
|
let request = Client.createList(title: title)
|
||||||
|
let (list, _) = try await mastodonController.run(request)
|
||||||
|
NotificationCenter.default.post(name: .listsChanged, object: nil)
|
||||||
|
self.didCreateList?(list)
|
||||||
|
} catch {
|
||||||
|
let alert = UIAlertController(title: "Error Creating List", message: error.localizedDescription, preferredStyle: .alert)
|
||||||
|
alert.addAction(UIAlertAction(title: "Cancel", style: .cancel))
|
||||||
|
alert.addAction(UIAlertAction(title: "Retry", style: .default, handler: { _ in
|
||||||
|
Task {
|
||||||
|
await self.createList(with: title)
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
present(alert)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
extension Foundation.Notification.Name {
|
||||||
|
static let listsChanged = Notification.Name("listsChanged")
|
||||||
|
}
|
|
@ -0,0 +1,65 @@
|
||||||
|
//
|
||||||
|
// DeleteListService.swift
|
||||||
|
// Tusker
|
||||||
|
//
|
||||||
|
// Created by Shadowfacts on 11/11/22.
|
||||||
|
// Copyright © 2022 Shadowfacts. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
import Pachyderm
|
||||||
|
|
||||||
|
@MainActor
|
||||||
|
class DeleteListService {
|
||||||
|
private let list: List
|
||||||
|
private let mastodonController: MastodonController
|
||||||
|
private let present: (UIViewController) -> Void
|
||||||
|
|
||||||
|
init(list: List, mastodonController: MastodonController, present: @escaping (UIViewController) -> Void) {
|
||||||
|
self.list = list
|
||||||
|
self.mastodonController = mastodonController
|
||||||
|
self.present = present
|
||||||
|
}
|
||||||
|
|
||||||
|
@discardableResult
|
||||||
|
func run() async -> Bool {
|
||||||
|
if await presentConfirmationAlert() {
|
||||||
|
await deleteList()
|
||||||
|
return true
|
||||||
|
} else {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func presentConfirmationAlert() async -> Bool {
|
||||||
|
await withCheckedContinuation { continuation in
|
||||||
|
let titleFormat = NSLocalizedString("Are you sure you want to delete the '%@' list?", comment: "delete list alert title")
|
||||||
|
let title = String(format: titleFormat, 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: { (_) in
|
||||||
|
continuation.resume(returning: false)
|
||||||
|
}))
|
||||||
|
alert.addAction(UIAlertAction(title: NSLocalizedString("Delete List", comment: "delete list alert confirm button"), style: .destructive, handler: { (_) in
|
||||||
|
continuation.resume(returning: true)
|
||||||
|
}))
|
||||||
|
present(alert)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func deleteList() async {
|
||||||
|
do {
|
||||||
|
let request = List.delete(list)
|
||||||
|
_ = try await mastodonController.run(request)
|
||||||
|
NotificationCenter.default.post(name: .listsChanged, object: nil)
|
||||||
|
} catch {
|
||||||
|
let alert = UIAlertController(title: "Error Deleting List", message: error.localizedDescription, preferredStyle: .alert)
|
||||||
|
alert.addAction(UIAlertAction(title: "Cancel", style: .cancel))
|
||||||
|
alert.addAction(UIAlertAction(title: "Retry", style: .default, handler: { _ in
|
||||||
|
Task {
|
||||||
|
await self.deleteList()
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
present(alert)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,69 @@
|
||||||
|
//
|
||||||
|
// RenameListService.swift
|
||||||
|
// Tusker
|
||||||
|
//
|
||||||
|
// Created by Shadowfacts on 11/11/22.
|
||||||
|
// Copyright © 2022 Shadowfacts. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
import Pachyderm
|
||||||
|
|
||||||
|
@MainActor
|
||||||
|
class RenameListService {
|
||||||
|
private let list: List
|
||||||
|
private let mastodonController: MastodonController
|
||||||
|
private let present: (UIViewController) -> Void
|
||||||
|
|
||||||
|
private var renameAction: UIAlertAction?
|
||||||
|
|
||||||
|
init(list: List, mastodonController: MastodonController, present: @escaping (UIViewController) -> Void) {
|
||||||
|
self.list = list
|
||||||
|
self.mastodonController = mastodonController
|
||||||
|
self.present = present
|
||||||
|
}
|
||||||
|
|
||||||
|
func run() {
|
||||||
|
let alert = UIAlertController(title: NSLocalizedString("Rename List", comment: "rename list alert title"), message: nil, preferredStyle: .alert)
|
||||||
|
alert.addTextField { (textField) in
|
||||||
|
textField.text = self.list.title
|
||||||
|
textField.addTarget(self, action: #selector(self.alertTextFieldValueChanged), for: .editingChanged)
|
||||||
|
}
|
||||||
|
alert.addAction(UIAlertAction(title: NSLocalizedString("Cancel", comment: "rename list alert cancel button"), style: .cancel, handler: nil))
|
||||||
|
renameAction = UIAlertAction(title: NSLocalizedString("Rename", comment: "renaem list alert rename button"), style: .default, handler: { (_) in
|
||||||
|
let textField = alert.textFields!.first!
|
||||||
|
let title = textField.text ?? ""
|
||||||
|
Task {
|
||||||
|
await self.updateList(with: title)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
alert.addAction(renameAction!)
|
||||||
|
present(alert)
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc private func alertTextFieldValueChanged(_ textField: UITextField) {
|
||||||
|
renameAction?.isEnabled = textField.text?.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty == false
|
||||||
|
}
|
||||||
|
|
||||||
|
private func updateList(with title: String) async {
|
||||||
|
do {
|
||||||
|
let req = List.update(list, title: title)
|
||||||
|
let (list, _) = try await mastodonController.run(req)
|
||||||
|
NotificationCenter.default.post(name: .listRenamed, object: list.id, userInfo: ["list": list])
|
||||||
|
} catch {
|
||||||
|
let alert = UIAlertController(title: "Error Updating List", message: error.localizedDescription, preferredStyle: .alert)
|
||||||
|
alert.addAction(UIAlertAction(title: "Cancel", style: .cancel))
|
||||||
|
alert.addAction(UIAlertAction(title: "Retry", style: .default, handler: { _ in
|
||||||
|
Task {
|
||||||
|
await self.updateList(with: title)
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
present(alert)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
extension Foundation.Notification.Name {
|
||||||
|
static let listRenamed = Notification.Name("listRenamed")
|
||||||
|
}
|
|
@ -221,10 +221,11 @@ class MastodonCachePersistentStore: NSPersistentContainer {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func addAll(accounts: [Account], completion: (() -> Void)? = nil) {
|
func addAll(accounts: [Account], in context: NSManagedObjectContext? = nil, completion: (() -> Void)? = nil) {
|
||||||
backgroundContext.perform {
|
let context = context ?? backgroundContext
|
||||||
accounts.forEach { self.upsert(account: $0, in: self.backgroundContext) }
|
context.perform {
|
||||||
self.save(context: self.backgroundContext)
|
accounts.forEach { self.upsert(account: $0, in: context) }
|
||||||
|
self.save(context: context)
|
||||||
completion?()
|
completion?()
|
||||||
accounts.forEach { self.accountSubject.send($0.id) }
|
accounts.forEach { self.accountSubject.send($0.id) }
|
||||||
}
|
}
|
||||||
|
|
|
@ -70,6 +70,8 @@ class ExploreViewController: UIViewController, UICollectionViewDelegate {
|
||||||
|
|
||||||
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)
|
NotificationCenter.default.addObserver(self, selector: #selector(savedInstancesChanged), name: .savedInstancesChanged, object: nil)
|
||||||
|
NotificationCenter.default.addObserver(self, selector: #selector(reloadLists), name: .listsChanged, object: nil)
|
||||||
|
NotificationCenter.default.addObserver(self, selector: #selector(listRenamed(_:)), name: .listRenamed, object: nil)
|
||||||
NotificationCenter.default.addObserver(self, selector: #selector(preferencesChanged), name: .preferencesChanged, object: nil)
|
NotificationCenter.default.addObserver(self, selector: #selector(preferencesChanged), name: .preferencesChanged, object: nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -178,7 +180,7 @@ class ExploreViewController: UIViewController, UICollectionViewDelegate {
|
||||||
self.dataSource.apply(snapshot)
|
self.dataSource.apply(snapshot)
|
||||||
}
|
}
|
||||||
|
|
||||||
private func reloadLists() {
|
@objc private func reloadLists() {
|
||||||
let request = Client.getLists()
|
let request = Client.getLists()
|
||||||
mastodonController.run(request) { (response) in
|
mastodonController.run(request) { (response) in
|
||||||
guard case let .success(lists, _) = response else {
|
guard case let .success(lists, _) = response else {
|
||||||
|
@ -196,6 +198,23 @@ class ExploreViewController: UIViewController, UICollectionViewDelegate {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@objc private func listRenamed(_ notification: Foundation.Notification) {
|
||||||
|
let list = notification.userInfo!["list"] as! List
|
||||||
|
var snapshot = dataSource.snapshot()
|
||||||
|
let existing = snapshot.itemIdentifiers(inSection: .lists).first(where: {
|
||||||
|
if case .list(let existingList) = $0, existingList.id == list.id {
|
||||||
|
return true
|
||||||
|
} else {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
})
|
||||||
|
if let existing {
|
||||||
|
snapshot.insertItems([.list(list)], afterItem: existing)
|
||||||
|
snapshot.deleteItems([existing])
|
||||||
|
dataSource.apply(snapshot)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@MainActor
|
@MainActor
|
||||||
private func fetchSavedHashtags() -> [SavedHashtag] {
|
private func fetchSavedHashtags() -> [SavedHashtag] {
|
||||||
let req = SavedHashtag.fetchRequest()
|
let req = SavedHashtag.fetchRequest()
|
||||||
|
@ -255,29 +274,17 @@ class ExploreViewController: UIViewController, UICollectionViewDelegate {
|
||||||
}
|
}
|
||||||
|
|
||||||
private func deleteList(_ list: List, completion: @escaping (Bool) -> Void) {
|
private func deleteList(_ list: List, completion: @escaping (Bool) -> Void) {
|
||||||
let titleFormat = NSLocalizedString("Are you sure you want to delete the '%@' list?", comment: "delete list alert title")
|
Task { @MainActor in
|
||||||
let title = String(format: titleFormat, list.title)
|
let service = DeleteListService(list: list, mastodonController: mastodonController, present: { self.present($0, animated: true) })
|
||||||
let alert = UIAlertController(title: title, message: nil, preferredStyle: .alert)
|
if await service.run() {
|
||||||
alert.addAction(UIAlertAction(title: NSLocalizedString("Cancel", comment: "delete list alert cancel button"), style: .cancel, handler: { (_) in
|
var snapshot = dataSource.snapshot()
|
||||||
completion(false)
|
|
||||||
}))
|
|
||||||
alert.addAction(UIAlertAction(title: NSLocalizedString("Delete List", comment: "delete list alert confirm button"), style: .destructive, handler: { (_) in
|
|
||||||
|
|
||||||
let request = List.delete(list)
|
|
||||||
self.mastodonController.run(request) { (response) in
|
|
||||||
guard case .success(_, _) = response else {
|
|
||||||
fatalError()
|
|
||||||
}
|
|
||||||
|
|
||||||
var snapshot = self.dataSource.snapshot()
|
|
||||||
snapshot.deleteItems([.list(list)])
|
snapshot.deleteItems([.list(list)])
|
||||||
DispatchQueue.main.async {
|
await dataSource.apply(snapshot)
|
||||||
self.dataSource.apply(snapshot)
|
|
||||||
completion(true)
|
completion(true)
|
||||||
|
} else {
|
||||||
|
completion(false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}))
|
|
||||||
present(alert, animated: true)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func removeSavedHashtag(_ hashtag: Hashtag) {
|
func removeSavedHashtag(_ hashtag: Hashtag) {
|
||||||
|
@ -356,28 +363,12 @@ class ExploreViewController: UIViewController, UICollectionViewDelegate {
|
||||||
|
|
||||||
case .addList:
|
case .addList:
|
||||||
collectionView.deselectItem(at: indexPath, animated: true)
|
collectionView.deselectItem(at: indexPath, animated: true)
|
||||||
let alert = UIAlertController(title: NSLocalizedString("New List", comment: "new list alert title"), message: NSLocalizedString("Choose a title for your new list", comment: "new list alert message"), preferredStyle: .alert)
|
let service = CreateListService(mastodonController: mastodonController, present: { self.present($0, animated: true) }) { list in
|
||||||
alert.addTextField(configurationHandler: nil)
|
|
||||||
alert.addAction(UIAlertAction(title: NSLocalizedString("Cancel", comment: "new list alert cancel button"), style: .cancel, handler: nil))
|
|
||||||
alert.addAction(UIAlertAction(title: NSLocalizedString("Create List", comment: "new list create button"), style: .default, handler: { (_) in
|
|
||||||
guard let title = alert.textFields?.first?.text else {
|
|
||||||
fatalError()
|
|
||||||
}
|
|
||||||
|
|
||||||
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, mastodonController: self.mastodonController)
|
let listTimelineController = ListTimelineViewController(for: list, mastodonController: self.mastodonController)
|
||||||
listTimelineController.presentEditOnAppear = true
|
listTimelineController.presentEditOnAppear = true
|
||||||
self.show(listTimelineController, sender: nil)
|
self.show(listTimelineController, sender: nil)
|
||||||
}
|
}
|
||||||
}
|
service.run()
|
||||||
}))
|
|
||||||
present(alert, animated: true)
|
|
||||||
|
|
||||||
case let .savedHashtag(hashtag):
|
case let .savedHashtag(hashtag):
|
||||||
show(HashtagTimelineViewController(for: hashtag, mastodonController: mastodonController), sender: nil)
|
show(HashtagTimelineViewController(for: hashtag, mastodonController: mastodonController), sender: nil)
|
||||||
|
@ -505,7 +496,7 @@ extension ExploreViewController {
|
||||||
case (.profileDirectory, .profileDirectory):
|
case (.profileDirectory, .profileDirectory):
|
||||||
return true
|
return true
|
||||||
case let (.list(a), .list(b)):
|
case let (.list(a), .list(b)):
|
||||||
return a.id == b.id
|
return a.id == b.id && a.title == b.title
|
||||||
case (.addList, .addList):
|
case (.addList, .addList):
|
||||||
return true
|
return true
|
||||||
case let (.savedHashtag(a), .savedHashtag(b)):
|
case let (.savedHashtag(a), .savedHashtag(b)):
|
||||||
|
@ -536,6 +527,7 @@ extension ExploreViewController {
|
||||||
case let .list(list):
|
case let .list(list):
|
||||||
hasher.combine("list")
|
hasher.combine("list")
|
||||||
hasher.combine(list.id)
|
hasher.combine(list.id)
|
||||||
|
hasher.combine(list.title)
|
||||||
case .addList:
|
case .addList:
|
||||||
hasher.combine("addList")
|
hasher.combine("addList")
|
||||||
case let .savedHashtag(hashtag):
|
case let .savedHashtag(hashtag):
|
||||||
|
|
|
@ -13,13 +13,13 @@ class EditListAccountsViewController: EnhancedTableViewController {
|
||||||
|
|
||||||
let mastodonController: MastodonController
|
let mastodonController: MastodonController
|
||||||
|
|
||||||
let list: List
|
private var list: List
|
||||||
|
|
||||||
var dataSource: DataSource!
|
var dataSource: DataSource!
|
||||||
|
|
||||||
var nextRange: RequestRange?
|
var nextRange: RequestRange?
|
||||||
|
|
||||||
var searchResultsController: SearchResultsViewController!
|
var searchResultsController: EditListSearchResultsContainerViewController!
|
||||||
var searchController: UISearchController!
|
var searchController: UISearchController!
|
||||||
|
|
||||||
init(list: List, mastodonController: MastodonController) {
|
init(list: List, mastodonController: MastodonController) {
|
||||||
|
@ -28,7 +28,9 @@ class EditListAccountsViewController: EnhancedTableViewController {
|
||||||
|
|
||||||
super.init(style: .plain)
|
super.init(style: .plain)
|
||||||
|
|
||||||
title = String(format: NSLocalizedString("Edit %@", comment: "edit list screen title"), list.title)
|
listChanged()
|
||||||
|
|
||||||
|
NotificationCenter.default.addObserver(self, selector: #selector(listRenamed(_:)), name: .listRenamed, object: list.id)
|
||||||
}
|
}
|
||||||
|
|
||||||
required init?(coder: NSCoder) {
|
required init?(coder: NSCoder) {
|
||||||
|
@ -53,14 +55,23 @@ class EditListAccountsViewController: EnhancedTableViewController {
|
||||||
})
|
})
|
||||||
dataSource.editListAccountsController = self
|
dataSource.editListAccountsController = self
|
||||||
|
|
||||||
searchResultsController = SearchResultsViewController(mastodonController: mastodonController, resultTypes: [.accounts])
|
searchResultsController = EditListSearchResultsContainerViewController(mastodonController: mastodonController) { [unowned self] accountID in
|
||||||
searchResultsController.delegate = self
|
Task {
|
||||||
|
await self.addAccount(id: accountID)
|
||||||
|
}
|
||||||
|
}
|
||||||
searchController = UISearchController(searchResultsController: searchResultsController)
|
searchController = UISearchController(searchResultsController: searchResultsController)
|
||||||
searchController.hidesNavigationBarDuringPresentation = false
|
searchController.hidesNavigationBarDuringPresentation = false
|
||||||
searchController.searchResultsUpdater = searchResultsController
|
searchController.searchResultsUpdater = searchResultsController
|
||||||
|
if #available(iOS 16.0, *) {
|
||||||
|
searchController.scopeBarActivation = .onSearchActivation
|
||||||
|
} else {
|
||||||
|
searchController.automaticallyShowsScopeBar = true
|
||||||
|
}
|
||||||
searchController.searchBar.autocapitalizationType = .none
|
searchController.searchBar.autocapitalizationType = .none
|
||||||
searchController.searchBar.placeholder = NSLocalizedString("Search for accounts to add", comment: "edit list search field placeholder")
|
searchController.searchBar.placeholder = NSLocalizedString("Search for accounts to add", comment: "edit list search field placeholder")
|
||||||
searchController.searchBar.delegate = searchResultsController
|
searchController.searchBar.delegate = searchResultsController
|
||||||
|
searchController.searchBar.scopeButtonTitles = ["Everyone", "People You Follow"]
|
||||||
definesPresentationContext = true
|
definesPresentationContext = true
|
||||||
|
|
||||||
navigationItem.searchController = searchController
|
navigationItem.searchController = searchController
|
||||||
|
@ -68,28 +79,76 @@ class EditListAccountsViewController: EnhancedTableViewController {
|
||||||
|
|
||||||
navigationItem.rightBarButtonItem = UIBarButtonItem(title: NSLocalizedString("Rename", comment: "rename list button title"), style: .plain, target: self, action: #selector(renameButtonPressed))
|
navigationItem.rightBarButtonItem = UIBarButtonItem(title: NSLocalizedString("Rename", comment: "rename list button title"), style: .plain, target: self, action: #selector(renameButtonPressed))
|
||||||
|
|
||||||
loadAccounts()
|
Task {
|
||||||
|
await loadAccounts()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func loadAccounts() {
|
private func listChanged() {
|
||||||
|
title = String(format: NSLocalizedString("Edit %@", comment: "edit list screen title"), list.title)
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc private func listRenamed(_ notification: Foundation.Notification) {
|
||||||
|
let list = notification.userInfo!["list"] as! List
|
||||||
|
self.list = list
|
||||||
|
self.listChanged()
|
||||||
|
}
|
||||||
|
|
||||||
|
func loadAccounts() async {
|
||||||
|
do {
|
||||||
let request = List.getAccounts(list)
|
let request = List.getAccounts(list)
|
||||||
mastodonController.run(request) { (response) in
|
let (accounts, pagination) = try await mastodonController.run(request)
|
||||||
guard case let .success(accounts, pagination) = response else {
|
|
||||||
fatalError()
|
|
||||||
}
|
|
||||||
|
|
||||||
self.nextRange = pagination?.older
|
self.nextRange = pagination?.older
|
||||||
|
|
||||||
self.mastodonController.persistentContainer.addAll(accounts: accounts) {
|
await withCheckedContinuation { continuation in
|
||||||
var snapshot = self.dataSource.snapshot()
|
mastodonController.persistentContainer.addAll(accounts: accounts) {
|
||||||
snapshot.deleteSections([.accounts])
|
continuation.resume()
|
||||||
snapshot.appendSections([.accounts])
|
}
|
||||||
snapshot.appendItems(accounts.map { .account(id: $0.id) })
|
}
|
||||||
|
|
||||||
DispatchQueue.main.async {
|
var snapshot = self.dataSource.snapshot()
|
||||||
self.dataSource.apply(snapshot)
|
if snapshot.indexOfSection(.accounts) == nil {
|
||||||
|
snapshot.appendSections([.accounts])
|
||||||
|
} else {
|
||||||
|
snapshot.deleteItems(snapshot.itemIdentifiers(inSection: .accounts))
|
||||||
|
}
|
||||||
|
snapshot.appendItems(accounts.map { .account(id: $0.id) })
|
||||||
|
await dataSource.apply(snapshot)
|
||||||
|
} catch {
|
||||||
|
let config = ToastConfiguration(from: error, with: "Error Loading Accounts", in: self) { [unowned self] toast in
|
||||||
|
toast.dismissToast(animated: true)
|
||||||
|
await self.loadAccounts()
|
||||||
|
}
|
||||||
|
self.showToast(configuration: config, animated: true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func addAccount(id: String) async {
|
||||||
|
do {
|
||||||
|
let req = List.add(list, accounts: [id])
|
||||||
|
_ = try await mastodonController.run(req)
|
||||||
|
self.searchController.isActive = false
|
||||||
|
await self.loadAccounts()
|
||||||
|
} catch {
|
||||||
|
let config = ToastConfiguration(from: error, with: "Error Adding Account", in: self) { [unowned self] toast in
|
||||||
|
toast.dismissToast(animated: true)
|
||||||
|
await self.addAccount(id: id)
|
||||||
|
}
|
||||||
|
self.showToast(configuration: config, animated: true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func removeAccount(id: String) async {
|
||||||
|
do {
|
||||||
|
let request = List.remove(list, accounts: [id])
|
||||||
|
_ = try await mastodonController.run(request)
|
||||||
|
await self.loadAccounts()
|
||||||
|
} catch {
|
||||||
|
let config = ToastConfiguration(from: error, with: "Error Removing Account", in: self) { [unowned self] toast in
|
||||||
|
toast.dismissToast(animated: true)
|
||||||
|
await self.removeAccount(id: id)
|
||||||
|
}
|
||||||
|
self.showToast(configuration: config, animated: true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -102,24 +161,7 @@ class EditListAccountsViewController: EnhancedTableViewController {
|
||||||
// MARK: - Interaction
|
// MARK: - Interaction
|
||||||
|
|
||||||
@objc func renameButtonPressed() {
|
@objc func renameButtonPressed() {
|
||||||
let alert = UIAlertController(title: NSLocalizedString("Rename List", comment: "rename list alert title"), message: nil, preferredStyle: .alert)
|
RenameListService(list: list, mastodonController: mastodonController, present: { self.present($0, animated: true) }).run()
|
||||||
alert.addTextField { (textField) in
|
|
||||||
textField.text = self.list.title
|
|
||||||
}
|
|
||||||
alert.addAction(UIAlertAction(title: NSLocalizedString("Cancel", comment: "rename list alert cancel button"), style: .cancel, handler: nil))
|
|
||||||
alert.addAction(UIAlertAction(title: NSLocalizedString("Rename", comment: "renaem list alert rename button"), style: .default, handler: { (_) in
|
|
||||||
guard let text = alert.textFields?.first?.text else {
|
|
||||||
fatalError()
|
|
||||||
}
|
|
||||||
let request = List.update(self.list, title: text)
|
|
||||||
self.mastodonController.run(request) { (response) in
|
|
||||||
guard case .success(_, _) = response else {
|
|
||||||
fatalError()
|
|
||||||
}
|
|
||||||
// todo: show success message somehow
|
|
||||||
}
|
|
||||||
}))
|
|
||||||
present(alert, animated: true)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -145,29 +187,8 @@ extension EditListAccountsViewController {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
let request = List.remove(editListAccountsController!.list, accounts: [id])
|
Task {
|
||||||
editListAccountsController!.mastodonController.run(request) { (response) in
|
await self.editListAccountsController?.removeAccount(id: id)
|
||||||
guard case .success(_, _) = response else {
|
|
||||||
fatalError()
|
|
||||||
}
|
|
||||||
|
|
||||||
self.editListAccountsController?.loadAccounts()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
extension EditListAccountsViewController: SearchResultsViewControllerDelegate {
|
|
||||||
func selectedSearchResult(account accountID: String) {
|
|
||||||
let request = List.add(list, accounts: [accountID])
|
|
||||||
mastodonController.run(request) { (response) in
|
|
||||||
guard case .success(_, _) = response else {
|
|
||||||
fatalError()
|
|
||||||
}
|
|
||||||
|
|
||||||
self.loadAccounts()
|
|
||||||
DispatchQueue.main.async {
|
|
||||||
self.searchController.isActive = false
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,178 @@
|
||||||
|
//
|
||||||
|
// EditListSearchFollowingViewController.swift
|
||||||
|
// Tusker
|
||||||
|
//
|
||||||
|
// Created by Shadowfacts on 11/11/22.
|
||||||
|
// Copyright © 2022 Shadowfacts. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
import Pachyderm
|
||||||
|
|
||||||
|
class EditListSearchFollowingViewController: EnhancedTableViewController {
|
||||||
|
|
||||||
|
private let mastodonController: MastodonController
|
||||||
|
private let didSelectAccount: (String) -> Void
|
||||||
|
|
||||||
|
private var dataSource: UITableViewDiffableDataSource<Section, String>!
|
||||||
|
|
||||||
|
private var query: String?
|
||||||
|
private var accountIDs: [String] = []
|
||||||
|
private var nextRange: RequestRange?
|
||||||
|
|
||||||
|
init(mastodonController: MastodonController, didSelectAccount: @escaping (String) -> Void) {
|
||||||
|
self.mastodonController = mastodonController
|
||||||
|
self.didSelectAccount = didSelectAccount
|
||||||
|
|
||||||
|
super.init(style: .grouped)
|
||||||
|
}
|
||||||
|
|
||||||
|
required init?(coder: NSCoder) {
|
||||||
|
fatalError("init(coder:) has not been implemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
override func viewDidLoad() {
|
||||||
|
super.viewDidLoad()
|
||||||
|
|
||||||
|
tableView.register(UINib(nibName: "AccountTableViewCell", bundle: .main), forCellReuseIdentifier: "accountCell")
|
||||||
|
dataSource = UITableViewDiffableDataSource(tableView: tableView) { tableView, indexPath, itemIdentifier in
|
||||||
|
let cell = tableView.dequeueReusableCell(withIdentifier: "accountCell", for: indexPath) as! AccountTableViewCell
|
||||||
|
cell.delegate = self
|
||||||
|
cell.updateUI(accountID: itemIdentifier)
|
||||||
|
return cell
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override func viewWillAppear(_ animated: Bool) {
|
||||||
|
super.viewWillAppear(animated)
|
||||||
|
|
||||||
|
if dataSource.snapshot().numberOfItems == 0 {
|
||||||
|
Task {
|
||||||
|
await load()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
|
||||||
|
print("will display: \(indexPath)")
|
||||||
|
if indexPath.row == tableView.numberOfRows(inSection: indexPath.section) - 1 {
|
||||||
|
Task {
|
||||||
|
await load()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
|
||||||
|
guard let id = dataSource.itemIdentifier(for: indexPath) else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
didSelectAccount(id)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func load() async {
|
||||||
|
do {
|
||||||
|
let ownAccount = try await mastodonController.getOwnAccount()
|
||||||
|
let req = Account.getFollowing(ownAccount.id, range: nextRange ?? .default)
|
||||||
|
let (following, pagination) = try await mastodonController.run(req)
|
||||||
|
await withCheckedContinuation { continuation in
|
||||||
|
mastodonController.persistentContainer.addAll(accounts: following) {
|
||||||
|
continuation.resume()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
accountIDs.append(contentsOf: following.lazy.map(\.id))
|
||||||
|
nextRange = pagination?.older
|
||||||
|
updateDataSource(appending: following.map(\.id))
|
||||||
|
} catch {
|
||||||
|
let config = ToastConfiguration(from: error, with: "Error Loading Following", in: self) { toast in
|
||||||
|
toast.dismissToast(animated: true)
|
||||||
|
await self.load()
|
||||||
|
}
|
||||||
|
self.showToast(configuration: config, animated: true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func updateDataSourceForQueryChanged() {
|
||||||
|
guard let query, !query.isEmpty else {
|
||||||
|
let snapshot = NSDiffableDataSourceSnapshot<Section, String>()
|
||||||
|
dataSource.apply(snapshot, animatingDifferences: true)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let ids = filterAccounts(ids: accountIDs, with: query)
|
||||||
|
|
||||||
|
var snapshot = dataSource.snapshot()
|
||||||
|
if snapshot.indexOfSection(.accounts) == nil {
|
||||||
|
snapshot.appendSections([.accounts])
|
||||||
|
} else {
|
||||||
|
snapshot.deleteItems(snapshot.itemIdentifiers(inSection: .accounts))
|
||||||
|
}
|
||||||
|
snapshot.appendItems(ids)
|
||||||
|
dataSource.apply(snapshot, animatingDifferences: true)
|
||||||
|
|
||||||
|
// if there aren't any results for the current query, try to load more
|
||||||
|
if ids.isEmpty {
|
||||||
|
Task {
|
||||||
|
await load()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func updateDataSource(appending ids: [String]) {
|
||||||
|
guard let query, !query.isEmpty else {
|
||||||
|
let snapshot = NSDiffableDataSourceSnapshot<Section, String>()
|
||||||
|
dataSource.apply(snapshot, animatingDifferences: true)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let ids = filterAccounts(ids: ids, with: query)
|
||||||
|
|
||||||
|
var snapshot = dataSource.snapshot()
|
||||||
|
if snapshot.indexOfSection(.accounts) == nil {
|
||||||
|
snapshot.appendSections([.accounts])
|
||||||
|
}
|
||||||
|
let existing = snapshot.itemIdentifiers(inSection: .accounts)
|
||||||
|
snapshot.appendItems(ids.filter { !existing.contains($0) })
|
||||||
|
dataSource.apply(snapshot, animatingDifferences: true)
|
||||||
|
|
||||||
|
// if there aren't any results for the current query, try to load more
|
||||||
|
if ids.isEmpty {
|
||||||
|
Task {
|
||||||
|
await load()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func filterAccounts(ids: [String], with query: String) -> [String] {
|
||||||
|
let req = AccountMO.fetchRequest()
|
||||||
|
req.predicate = NSPredicate(format: "id in %@", ids)
|
||||||
|
let accounts = try! mastodonController.persistentContainer.viewContext.fetch(req)
|
||||||
|
|
||||||
|
return accounts
|
||||||
|
.map { (account) -> (AccountMO, Bool) in
|
||||||
|
let displayNameMatch = FuzzyMatcher.match(pattern: query, str: account.displayNameWithoutCustomEmoji)
|
||||||
|
let usernameMatch = FuzzyMatcher.match(pattern: query, str: account.acct)
|
||||||
|
return (account, displayNameMatch.matched || usernameMatch.matched)
|
||||||
|
}
|
||||||
|
.filter(\.1)
|
||||||
|
.map(\.0.id)
|
||||||
|
}
|
||||||
|
|
||||||
|
func updateQuery(_ query: String) {
|
||||||
|
self.query = query
|
||||||
|
updateDataSourceForQueryChanged()
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
extension EditListSearchFollowingViewController {
|
||||||
|
enum Section {
|
||||||
|
case accounts
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension EditListSearchFollowingViewController: TuskerNavigationDelegate {
|
||||||
|
var apiController: MastodonController! { mastodonController }
|
||||||
|
}
|
||||||
|
|
||||||
|
extension EditListSearchFollowingViewController: MenuActionProvider {
|
||||||
|
}
|
|
@ -0,0 +1,115 @@
|
||||||
|
//
|
||||||
|
// EditListSearchResultsContainerViewController.swift
|
||||||
|
// Tusker
|
||||||
|
//
|
||||||
|
// Created by Shadowfacts on 11/11/22.
|
||||||
|
// Copyright © 2022 Shadowfacts. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
import Combine
|
||||||
|
|
||||||
|
class EditListSearchResultsContainerViewController: UIViewController {
|
||||||
|
|
||||||
|
private let mastodonController: MastodonController
|
||||||
|
private let didSelectAccount: (String) -> Void
|
||||||
|
|
||||||
|
private let searchResultsController: SearchResultsViewController
|
||||||
|
private let searchFollowingController: EditListSearchFollowingViewController
|
||||||
|
|
||||||
|
var mode = Mode.search {
|
||||||
|
willSet {
|
||||||
|
currentViewController.removeViewAndController()
|
||||||
|
}
|
||||||
|
didSet {
|
||||||
|
embedChild(currentViewController)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var currentViewController: UIViewController {
|
||||||
|
switch mode {
|
||||||
|
case .search:
|
||||||
|
return searchResultsController
|
||||||
|
case .following:
|
||||||
|
return searchFollowingController
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private var currentQuery: String?
|
||||||
|
private var searchSubject = PassthroughSubject<String?, Never>()
|
||||||
|
private var cancellables = Set<AnyCancellable>()
|
||||||
|
|
||||||
|
init(mastodonController: MastodonController, didSelectAccount: @escaping (String) -> Void) {
|
||||||
|
self.mastodonController = mastodonController
|
||||||
|
self.didSelectAccount = didSelectAccount
|
||||||
|
|
||||||
|
self.searchResultsController = SearchResultsViewController(mastodonController: mastodonController, resultTypes: [.accounts])
|
||||||
|
self.searchFollowingController = EditListSearchFollowingViewController(mastodonController: mastodonController, didSelectAccount: didSelectAccount)
|
||||||
|
|
||||||
|
super.init(nibName: nil, bundle: nil)
|
||||||
|
|
||||||
|
self.searchResultsController.delegate = self
|
||||||
|
}
|
||||||
|
|
||||||
|
required init?(coder: NSCoder) {
|
||||||
|
fatalError("init(coder:) has not been implemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
override func viewDidLoad() {
|
||||||
|
super.viewDidLoad()
|
||||||
|
|
||||||
|
embedChild(currentViewController)
|
||||||
|
|
||||||
|
searchSubject
|
||||||
|
.debounce(for: .milliseconds(500), scheduler: RunLoop.main)
|
||||||
|
.sink { [unowned self] in self.performSearch(query: $0) }
|
||||||
|
.store(in: &cancellables)
|
||||||
|
}
|
||||||
|
|
||||||
|
func performSearch(query: String?) {
|
||||||
|
guard var query = query?.trimmingCharacters(in: .whitespacesAndNewlines) else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if query.starts(with: "@") {
|
||||||
|
query = String(query.dropFirst())
|
||||||
|
}
|
||||||
|
guard query != self.currentQuery else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
self.currentQuery = query
|
||||||
|
|
||||||
|
switch mode {
|
||||||
|
case .search:
|
||||||
|
searchResultsController.performSearch(query: query)
|
||||||
|
case .following:
|
||||||
|
searchFollowingController.updateQuery(query)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
enum Mode: Equatable {
|
||||||
|
case search, following
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
extension EditListSearchResultsContainerViewController: UISearchResultsUpdating {
|
||||||
|
func updateSearchResults(for searchController: UISearchController) {
|
||||||
|
searchSubject.send(searchController.searchBar.text)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension EditListSearchResultsContainerViewController: UISearchBarDelegate {
|
||||||
|
func searchBarTextDidEndEditing(_ searchBar: UISearchBar) {
|
||||||
|
performSearch(query: searchBar.text)
|
||||||
|
}
|
||||||
|
|
||||||
|
func searchBar(_ searchBar: UISearchBar, selectedScopeButtonIndexDidChange selectedScope: Int) {
|
||||||
|
mode = selectedScope == 0 ? .search : .following
|
||||||
|
performSearch(query: searchBar.text)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension EditListSearchResultsContainerViewController: SearchResultsViewControllerDelegate {
|
||||||
|
func selectedSearchResult(account accountID: String) {
|
||||||
|
didSelectAccount(accountID)
|
||||||
|
}
|
||||||
|
}
|
|
@ -11,7 +11,7 @@ import Pachyderm
|
||||||
|
|
||||||
class ListTimelineViewController: TimelineViewController {
|
class ListTimelineViewController: TimelineViewController {
|
||||||
|
|
||||||
let list: List
|
private(set) var list: List
|
||||||
|
|
||||||
var presentEditOnAppear = false
|
var presentEditOnAppear = false
|
||||||
|
|
||||||
|
@ -20,7 +20,9 @@ class ListTimelineViewController: TimelineViewController {
|
||||||
|
|
||||||
super.init(for: .list(id: list.id), mastodonController: mastodonController)
|
super.init(for: .list(id: list.id), mastodonController: mastodonController)
|
||||||
|
|
||||||
title = list.title
|
listChanged()
|
||||||
|
|
||||||
|
NotificationCenter.default.addObserver(self, selector: #selector(listRenamed(_:)), name: .listRenamed, object: list.id)
|
||||||
}
|
}
|
||||||
|
|
||||||
required init?(coder aDecoder: NSCoder) {
|
required init?(coder aDecoder: NSCoder) {
|
||||||
|
@ -41,6 +43,16 @@ class ListTimelineViewController: TimelineViewController {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func listChanged() {
|
||||||
|
title = list.title
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc private func listRenamed(_ notification: Foundation.Notification) {
|
||||||
|
let list = notification.userInfo!["list"] as! List
|
||||||
|
self.list = list
|
||||||
|
self.listChanged()
|
||||||
|
}
|
||||||
|
|
||||||
func presentEdit(animated: Bool) {
|
func presentEdit(animated: Bool) {
|
||||||
let editListAccountsController = EditListAccountsViewController(list: list, mastodonController: mastodonController)
|
let editListAccountsController = EditListAccountsViewController(list: list, mastodonController: mastodonController)
|
||||||
editListAccountsController.navigationItem.leftBarButtonItem = UIBarButtonItem(barButtonSystemItem: .done, target: self, action: #selector(editListDoneButtonPressed))
|
editListAccountsController.navigationItem.leftBarButtonItem = UIBarButtonItem(barButtonSystemItem: .done, target: self, action: #selector(editListDoneButtonPressed))
|
||||||
|
|
|
@ -99,6 +99,8 @@ class MainSidebarViewController: UIViewController {
|
||||||
|
|
||||||
NotificationCenter.default.addObserver(self, selector: #selector(reloadSavedHashtags), name: .savedHashtagsChanged, object: nil)
|
NotificationCenter.default.addObserver(self, selector: #selector(reloadSavedHashtags), name: .savedHashtagsChanged, object: nil)
|
||||||
NotificationCenter.default.addObserver(self, selector: #selector(reloadSavedInstances), name: .savedInstancesChanged, object: nil)
|
NotificationCenter.default.addObserver(self, selector: #selector(reloadSavedInstances), name: .savedInstancesChanged, object: nil)
|
||||||
|
NotificationCenter.default.addObserver(self, selector: #selector(reloadLists), name: .listsChanged, object: nil)
|
||||||
|
NotificationCenter.default.addObserver(self, selector: #selector(listRenamed(_:)), name: .listRenamed, object: nil)
|
||||||
NotificationCenter.default.addObserver(self, selector: #selector(preferencesChanged), name: .preferencesChanged, object: nil)
|
NotificationCenter.default.addObserver(self, selector: #selector(preferencesChanged), name: .preferencesChanged, object: nil)
|
||||||
|
|
||||||
onViewDidLoad?()
|
onViewDidLoad?()
|
||||||
|
@ -201,7 +203,7 @@ class MainSidebarViewController: UIViewController {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func reloadLists() {
|
@objc private func reloadLists() {
|
||||||
let request = Client.getLists()
|
let request = Client.getLists()
|
||||||
mastodonController.run(request) { [weak self] (response) in
|
mastodonController.run(request) { [weak self] (response) in
|
||||||
guard let self = self, case let .success(lists, _) = response else { return }
|
guard let self = self, case let .success(lists, _) = response else { return }
|
||||||
|
@ -223,6 +225,23 @@ class MainSidebarViewController: UIViewController {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@objc private func listRenamed(_ notification: Foundation.Notification) {
|
||||||
|
let list = notification.userInfo!["list"] as! List
|
||||||
|
var snapshot = dataSource.snapshot()
|
||||||
|
let existing = snapshot.itemIdentifiers(inSection: .lists).first(where: {
|
||||||
|
if case .list(let existingList) = $0, existingList.id == list.id {
|
||||||
|
return true
|
||||||
|
} else {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
})
|
||||||
|
if let existing {
|
||||||
|
snapshot.insertItems([.list(list)], afterItem: existing)
|
||||||
|
snapshot.deleteItems([existing])
|
||||||
|
dataSource.apply(snapshot)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@MainActor
|
@MainActor
|
||||||
private func fetchSavedHashtags() -> [SavedHashtag] {
|
private func fetchSavedHashtags() -> [SavedHashtag] {
|
||||||
let req = SavedHashtag.fetchRequest()
|
let req = SavedHashtag.fetchRequest()
|
||||||
|
@ -297,28 +316,12 @@ class MainSidebarViewController: UIViewController {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// todo: deduplicate with ExploreViewController
|
|
||||||
private func showAddList() {
|
private func showAddList() {
|
||||||
let alert = UIAlertController(title: "New List", message: "Choose a title for your new list", preferredStyle: .alert)
|
let service = CreateListService(mastodonController: mastodonController, present: { self.present($0, animated: true
|
||||||
alert.addTextField(configurationHandler: nil)
|
) }) { list in
|
||||||
alert.addAction(UIAlertAction(title: "Cancel", style: .cancel, handler: nil))
|
|
||||||
alert.addAction(UIAlertAction(title: "Create List", style: .default, handler: { (_) in
|
|
||||||
guard let title = alert.textFields?.first?.text else {
|
|
||||||
fatalError()
|
|
||||||
}
|
|
||||||
|
|
||||||
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 {
|
|
||||||
self.sidebarDelegate?.sidebar(self, didSelectItem: .list(list))
|
self.sidebarDelegate?.sidebar(self, didSelectItem: .list(list))
|
||||||
}
|
}
|
||||||
}
|
service.run()
|
||||||
}))
|
|
||||||
present(alert, animated: true)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// todo: deduplicate with ExploreViewController
|
// todo: deduplicate with ExploreViewController
|
||||||
|
@ -551,11 +554,22 @@ extension MainSidebarViewController: UICollectionViewDelegate {
|
||||||
}
|
}
|
||||||
activity.displaysAuxiliaryScene = true
|
activity.displaysAuxiliaryScene = true
|
||||||
return UIContextMenuConfiguration(identifier: nil, previewProvider: nil) { (_) in
|
return UIContextMenuConfiguration(identifier: nil, previewProvider: nil) { (_) in
|
||||||
return UIMenu(children: [
|
var actions: [UIAction] = [
|
||||||
UIWindowScene.ActivationAction({ action in
|
UIWindowScene.ActivationAction({ action in
|
||||||
return UIWindowScene.ActivationConfiguration(userActivity: activity)
|
return UIWindowScene.ActivationConfiguration(userActivity: activity)
|
||||||
}),
|
}),
|
||||||
])
|
]
|
||||||
|
|
||||||
|
if case .list(let list) = item {
|
||||||
|
actions.append(UIAction(title: "Delete List", image: UIImage(systemName: "trash"), attributes: .destructive, handler: { [unowned self] _ in
|
||||||
|
Task {
|
||||||
|
let service = DeleteListService(list: list, mastodonController: self.mastodonController, present: { self.present($0, animated: true) })
|
||||||
|
await service.run()
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
return UIMenu(children: actions)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -69,6 +69,7 @@ class AccountTableViewCell: UITableViewCell {
|
||||||
|
|
||||||
let accountID = self.accountID
|
let accountID = self.accountID
|
||||||
|
|
||||||
|
avatarImageView.image = nil
|
||||||
if let avatarURL = account.avatar {
|
if let avatarURL = account.avatar {
|
||||||
avatarRequest = ImageCache.avatars.get(avatarURL) { [weak self] (_, image) in
|
avatarRequest = ImageCache.avatars.get(avatarURL) { [weak self] (_, image) in
|
||||||
guard let self = self else { return }
|
guard let self = self else { return }
|
||||||
|
|
Loading…
Reference in New Issue