MastoSearch/MastoSearchMobile/ViewController.swift

243 lines
9.7 KiB
Swift
Raw Normal View History

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!
}
}