2022-07-03 23:00:17 +00:00
|
|
|
//
|
|
|
|
// ViewController.swift
|
|
|
|
// MastoSearchMobile
|
|
|
|
//
|
|
|
|
// Created by Shadowfacts on 7/3/22.
|
|
|
|
//
|
|
|
|
|
|
|
|
import UIKit
|
|
|
|
import MastoSearchCore
|
|
|
|
import Combine
|
|
|
|
import SafariServices
|
2022-07-18 20:52:56 +00:00
|
|
|
import AuthenticationServices
|
2022-07-03 23:00:17 +00:00
|
|
|
|
|
|
|
class ViewController: UIViewController {
|
|
|
|
|
|
|
|
private let searchQueue = DispatchQueue(label: "Search", qos: .userInitiated)
|
|
|
|
|
|
|
|
private var collectionView: UICollectionView!
|
|
|
|
private var dataSource: UICollectionViewDiffableDataSource<Section, Item>!
|
|
|
|
|
2022-07-18 20:52:56 +00:00
|
|
|
private var searchQuerySubject = CurrentValueSubject<String, Never>("")
|
2022-07-03 23:00:17 +00:00
|
|
|
private var cancellables = Set<AnyCancellable>()
|
|
|
|
|
|
|
|
override func viewDidLoad() {
|
|
|
|
super.viewDidLoad()
|
|
|
|
|
|
|
|
view.backgroundColor = .systemBackground
|
|
|
|
|
|
|
|
navigationItem.title = "MastoSearch"
|
|
|
|
navigationItem.leadingItemGroups = [
|
|
|
|
UIBarButtonItemGroup(barButtonItems: [
|
|
|
|
UIBarButtonItem(title: "Account", menu: createAccountMenu()),
|
|
|
|
UIBarButtonItem(title: "Import", style: .plain, target: self, action: #selector(importPressed)),
|
|
|
|
], representativeItem: nil)
|
|
|
|
]
|
|
|
|
let searchController = UISearchController(searchResultsController: nil)
|
|
|
|
searchController.searchResultsUpdater = self
|
|
|
|
navigationItem.searchController = searchController
|
|
|
|
|
|
|
|
var config = UICollectionLayoutListConfiguration(appearance: .plain)
|
|
|
|
config.headerMode = .supplementary
|
|
|
|
config.itemSeparatorHandler = { indexPath, config in
|
|
|
|
if indexPath.row == 0 {
|
|
|
|
var config = config
|
|
|
|
config.topSeparatorVisibility = .hidden
|
|
|
|
return config
|
|
|
|
} else {
|
|
|
|
return config
|
|
|
|
}
|
|
|
|
}
|
|
|
|
let layout = UICollectionViewCompositionalLayout.list(using: config)
|
|
|
|
collectionView = UICollectionView(frame: view.bounds, collectionViewLayout: layout)
|
|
|
|
collectionView.translatesAutoresizingMaskIntoConstraints = false
|
|
|
|
collectionView.allowsMultipleSelection = true
|
|
|
|
collectionView.delegate = self
|
|
|
|
view.addSubview(collectionView)
|
|
|
|
NSLayoutConstraint.activate([
|
|
|
|
collectionView.topAnchor.constraint(equalTo: view.topAnchor),
|
|
|
|
collectionView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
|
|
|
|
collectionView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
|
|
|
|
collectionView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
|
|
|
|
])
|
|
|
|
|
|
|
|
let header = UICollectionView.SupplementaryRegistration<StatusTableHeaderView>(elementKind: UICollectionView.elementKindSectionHeader) { supplementaryView, elementKind, indexPath in
|
|
|
|
}
|
|
|
|
let cell = UICollectionView.CellRegistration<StatusTableRowCollectionViewCell, Status> { cell, indexPath, status in
|
|
|
|
cell.updateUI(status: status)
|
|
|
|
cell.backgroundColor = indexPath.row % 2 == 0 ? .alternatingTableRow : .systemBackground
|
|
|
|
}
|
|
|
|
|
|
|
|
dataSource = UICollectionViewDiffableDataSource(collectionView: collectionView) { collectionView, indexPath, item in
|
|
|
|
collectionView.dequeueConfiguredReusableCell(using: cell, for: indexPath, item: item.status)
|
|
|
|
}
|
|
|
|
dataSource.supplementaryViewProvider = { collectionView, elementKind, indexPath in
|
|
|
|
guard elementKind == UICollectionView.elementKindSectionHeader else {
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
return collectionView.dequeueConfiguredReusableSupplementary(using: header, for: indexPath)
|
|
|
|
}
|
|
|
|
|
|
|
|
searchQuerySubject
|
|
|
|
.debounce(for: .milliseconds(100), scheduler: DispatchQueue.main)
|
|
|
|
.sink { [unowned self] query in
|
|
|
|
self.updateStatuses(query: query)
|
|
|
|
}
|
|
|
|
.store(in: &cancellables)
|
|
|
|
|
|
|
|
updateStatuses(query: "")
|
2022-07-18 20:52:56 +00:00
|
|
|
|
|
|
|
SyncController.shared.onSync
|
|
|
|
.sink { [unowned self] _ in
|
|
|
|
self.updateStatuses(query: searchQuerySubject.value)
|
|
|
|
}
|
|
|
|
.store(in: &cancellables)
|
2022-07-03 23:00:17 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
private func updateStatuses(query: String) {
|
|
|
|
searchQueue.async {
|
|
|
|
if query.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
|
|
|
|
DatabaseController.shared.getStatuses(sortDescriptor: NSSortDescriptor(key: "published", ascending: false)) { seq in
|
|
|
|
self.applyUpdate(statuses: seq)
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
DatabaseController.shared.getStatuses(query: query, sortDescriptor: NSSortDescriptor(key: "published", ascending: false)) { seq in
|
|
|
|
self.applyUpdate(statuses: seq)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
private func applyUpdate(statuses: StatusSequence) {
|
|
|
|
var snapshot = NSDiffableDataSourceSnapshot<Section, Item>()
|
|
|
|
snapshot.appendSections([.statuses])
|
|
|
|
snapshot.appendItems(statuses.map { Item(status: $0) })
|
|
|
|
DispatchQueue.main.async {
|
|
|
|
self.dataSource.apply(snapshot, animatingDifferences: false)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
private func createAccountMenu() -> UIMenu {
|
|
|
|
if let account = LocalData.account {
|
|
|
|
return UIMenu(children: [
|
|
|
|
UIAction(title: "Logged in to \(account.instanceURL.host!)", attributes: .disabled, handler: { _ in }),
|
|
|
|
UIAction(title: "Log out", attributes: .destructive, handler: { [unowned self] _ in
|
|
|
|
self.logout()
|
|
|
|
}),
|
|
|
|
])
|
|
|
|
} else {
|
|
|
|
return UIMenu(children: [
|
|
|
|
UIAction(title: "Log in...", handler: { [unowned self] _ in
|
|
|
|
self.login()
|
|
|
|
}),
|
|
|
|
])
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
private func login() {
|
2022-07-18 20:52:56 +00:00
|
|
|
let alert = UIAlertController(title: "Instance URL", message: nil, preferredStyle: .alert)
|
|
|
|
alert.addTextField { textField in
|
|
|
|
textField.placeholder = "https://mastodon.social/"
|
|
|
|
}
|
|
|
|
alert.addAction(UIAlertAction(title: "Ok", style: .default, handler: { _ in
|
|
|
|
guard let text = alert.textFields!.first!.text,
|
|
|
|
let url = URL(string: text) else {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
LoginController.shared.logIn(with: url, presentationContextProvider: self) {
|
|
|
|
self.navigationItem.leadingItemGroups.first!.barButtonItems.first!.menu = self.createAccountMenu()
|
|
|
|
(self.view.window!.windowScene!.delegate as! SceneDelegate).syncStatuses()
|
|
|
|
}
|
|
|
|
}))
|
|
|
|
alert.addAction(UIAlertAction(title: "Cancel", style: .cancel))
|
|
|
|
present(alert, animated: true)
|
2022-07-03 23:00:17 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
private func logout() {
|
2022-07-18 20:52:56 +00:00
|
|
|
LocalData.account = nil
|
|
|
|
self.navigationItem.leadingItemGroups.first!.barButtonItems.first!.menu = self.createAccountMenu()
|
2022-07-03 23:00:17 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
@objc private func importPressed() {
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
extension ViewController {
|
|
|
|
enum Section {
|
|
|
|
case statuses
|
|
|
|
}
|
|
|
|
struct Item: Equatable, Hashable {
|
|
|
|
let status: Status
|
|
|
|
|
|
|
|
static func ==(lhs: Item, rhs: Item) -> Bool {
|
|
|
|
return lhs.status.url == rhs.status.url
|
|
|
|
}
|
|
|
|
|
|
|
|
func hash(into hasher: inout Hasher) {
|
|
|
|
hasher.combine(status.url)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
extension ViewController: UISearchResultsUpdating {
|
|
|
|
func updateSearchResults(for searchController: UISearchController) {
|
|
|
|
searchQuerySubject.send(searchController.searchBar.text ?? "")
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
extension ViewController: UICollectionViewDelegate {
|
|
|
|
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
|
|
|
|
guard let status = dataSource.itemIdentifier(for: indexPath)?.status else {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
present(SFSafariViewController(url: URL(string: status.url)!), animated: true)
|
|
|
|
}
|
|
|
|
|
|
|
|
func collectionView(_ collectionView: UICollectionView, contextMenuConfigurationForItemsAt indexPaths: [IndexPath], point: CGPoint) -> UIContextMenuConfiguration? {
|
|
|
|
let statuses = indexPaths.compactMap { dataSource.itemIdentifier(for: $0)?.status }
|
|
|
|
switch statuses.count {
|
|
|
|
case 0:
|
|
|
|
return nil
|
|
|
|
case 1:
|
|
|
|
let url = URL(string: statuses.first!.url)!
|
|
|
|
return UIContextMenuConfiguration {
|
|
|
|
SFSafariViewController(url: url)
|
|
|
|
} actionProvider: { _ in
|
|
|
|
UIMenu(children: [
|
|
|
|
UIAction(title: "Open in Safari", image: UIImage(systemName: "safari"), handler: { [unowned self] _ in
|
|
|
|
self.present(SFSafariViewController(url: url), animated: true)
|
|
|
|
}),
|
|
|
|
UIAction(title: "Copy URL", image: UIImage(systemName: "list.bullet.clipboard"), handler: { _ in
|
|
|
|
UIPasteboard.general.url = url
|
|
|
|
})
|
|
|
|
])
|
|
|
|
}
|
|
|
|
default:
|
|
|
|
return UIContextMenuConfiguration(actionProvider: { _ in
|
|
|
|
UIMenu(children: [
|
|
|
|
UIAction(title: "Copy URLs", image: UIImage(systemName: "list.bullet.clipboard"), handler: { _ in
|
|
|
|
UIPasteboard.general.urls = statuses.map { URL(string: $0.url)! }
|
|
|
|
})
|
|
|
|
])
|
|
|
|
})
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func collectionView(_ collectionView: UICollectionView, willPerformPreviewActionForMenuWith configuration: UIContextMenuConfiguration, animator: UIContextMenuInteractionCommitAnimating) {
|
|
|
|
if let viewController = animator.previewViewController,
|
|
|
|
viewController is SFSafariViewController {
|
|
|
|
animator.preferredCommitStyle = .pop
|
|
|
|
animator.addCompletion {
|
|
|
|
self.present(viewController, animated: true)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2022-07-18 20:52:56 +00:00
|
|
|
|
|
|
|
extension ViewController: ASWebAuthenticationPresentationContextProviding {
|
|
|
|
func presentationAnchor(for session: ASWebAuthenticationSession) -> ASPresentationAnchor {
|
|
|
|
return view.window!
|
|
|
|
}
|
|
|
|
}
|