mirror of
https://github.com/metabolist/metatext.git
synced 2024-11-22 08:10:59 +00:00
wip
This commit is contained in:
parent
ef8fd98e4b
commit
def0e3fff0
15 changed files with 249 additions and 21 deletions
|
@ -382,6 +382,18 @@ public extension ContentDatabase {
|
||||||
.eraseToAnyPublisher()
|
.eraseToAnyPublisher()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func update(emojis: [Emoji]) -> AnyPublisher<Never, Error> {
|
||||||
|
databaseWriter.writePublisher {
|
||||||
|
for emoji in emojis {
|
||||||
|
try emoji.save($0)
|
||||||
|
}
|
||||||
|
|
||||||
|
try Emoji.filter(!emojis.map(\.shortcode).contains(Emoji.Columns.shortcode)).deleteAll($0)
|
||||||
|
}
|
||||||
|
.ignoreOutput()
|
||||||
|
.eraseToAnyPublisher()
|
||||||
|
}
|
||||||
|
|
||||||
func timelinePublisher(_ timeline: Timeline) -> AnyPublisher<[[CollectionItem]], Error> {
|
func timelinePublisher(_ timeline: Timeline) -> AnyPublisher<[[CollectionItem]], Error> {
|
||||||
ValueObservation.tracking(
|
ValueObservation.tracking(
|
||||||
TimelineItemsInfo.request(TimelineRecord.filter(TimelineRecord.Columns.id == timeline.id)).fetchOne)
|
TimelineItemsInfo.request(TimelineRecord.filter(TimelineRecord.Columns.id == timeline.id)).fetchOne)
|
||||||
|
@ -492,6 +504,13 @@ public extension ContentDatabase {
|
||||||
.eraseToAnyPublisher()
|
.eraseToAnyPublisher()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func pickerEmojisPublisher() -> AnyPublisher<[Emoji], Error> {
|
||||||
|
ValueObservation.tracking(Emoji.filter(Emoji.Columns.visibleInPicker == true).fetchAll)
|
||||||
|
.removeDuplicates()
|
||||||
|
.publisher(in: databaseWriter)
|
||||||
|
.eraseToAnyPublisher()
|
||||||
|
}
|
||||||
|
|
||||||
func lastReadId(_ markerTimeline: Marker.Timeline) -> String? {
|
func lastReadId(_ markerTimeline: Marker.Timeline) -> String? {
|
||||||
try? databaseWriter.read {
|
try? databaseWriter.read {
|
||||||
try String.fetchOne(
|
try String.fetchOne(
|
||||||
|
|
17
DB/Sources/DB/Extensions/Emoji+Extensions.swift
Normal file
17
DB/Sources/DB/Extensions/Emoji+Extensions.swift
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
// Copyright © 2020 Metabolist. All rights reserved.
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
import GRDB
|
||||||
|
import Mastodon
|
||||||
|
|
||||||
|
extension Emoji: ContentDatabaseRecord {}
|
||||||
|
|
||||||
|
extension Emoji {
|
||||||
|
enum Columns: String, ColumnExpression {
|
||||||
|
case shortcode
|
||||||
|
case staticUrl
|
||||||
|
case url
|
||||||
|
case visibleInPicker
|
||||||
|
case category
|
||||||
|
}
|
||||||
|
}
|
|
@ -7,4 +7,5 @@ public struct Emoji: Codable, Hashable {
|
||||||
public let staticUrl: URL
|
public let staticUrl: URL
|
||||||
public let url: URL
|
public let url: URL
|
||||||
public let visibleInPicker: Bool
|
public let visibleInPicker: Bool
|
||||||
|
public let category: String?
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,21 @@
|
||||||
|
// Copyright © 2020 Metabolist. All rights reserved.
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
import HTTP
|
||||||
|
import Mastodon
|
||||||
|
|
||||||
|
public enum EmojisEndpoint {
|
||||||
|
case customEmojis
|
||||||
|
}
|
||||||
|
|
||||||
|
extension EmojisEndpoint: Endpoint {
|
||||||
|
public typealias ResultType = [Emoji]
|
||||||
|
|
||||||
|
public var pathComponentsInContext: [String] {
|
||||||
|
["custom_emojis"]
|
||||||
|
}
|
||||||
|
|
||||||
|
public var method: HTTPMethod {
|
||||||
|
.get
|
||||||
|
}
|
||||||
|
}
|
|
@ -55,6 +55,8 @@
|
||||||
D0625E5D250F0B5C00502611 /* StatusContentConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0625E5C250F0B5C00502611 /* StatusContentConfiguration.swift */; };
|
D0625E5D250F0B5C00502611 /* StatusContentConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0625E5C250F0B5C00502611 /* StatusContentConfiguration.swift */; };
|
||||||
D06BC5E625202AD90079541D /* ProfileViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D06BC5E525202AD90079541D /* ProfileViewController.swift */; };
|
D06BC5E625202AD90079541D /* ProfileViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D06BC5E525202AD90079541D /* ProfileViewController.swift */; };
|
||||||
D0849C7F25903C4900A5EBCC /* Status+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0849C7E25903C4900A5EBCC /* Status+Extensions.swift */; };
|
D0849C7F25903C4900A5EBCC /* Status+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0849C7E25903C4900A5EBCC /* Status+Extensions.swift */; };
|
||||||
|
D088406D25AFBBE200BB749B /* EmojiPickerViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D088406C25AFBBE200BB749B /* EmojiPickerViewController.swift */; };
|
||||||
|
D088406E25AFBBE200BB749B /* EmojiPickerViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D088406C25AFBBE200BB749B /* EmojiPickerViewController.swift */; };
|
||||||
D08B8D3D253F929E00B1EBEF /* ImageViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D08B8D3C253F929E00B1EBEF /* ImageViewController.swift */; };
|
D08B8D3D253F929E00B1EBEF /* ImageViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D08B8D3C253F929E00B1EBEF /* ImageViewController.swift */; };
|
||||||
D08B8D42253F92B600B1EBEF /* ImagePageViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D08B8D41253F92B600B1EBEF /* ImagePageViewController.swift */; };
|
D08B8D42253F92B600B1EBEF /* ImagePageViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D08B8D41253F92B600B1EBEF /* ImagePageViewController.swift */; };
|
||||||
D08B8D4A253FC36500B1EBEF /* ImageNavigationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D08B8D49253FC36500B1EBEF /* ImageNavigationController.swift */; };
|
D08B8D4A253FC36500B1EBEF /* ImageNavigationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D08B8D49253FC36500B1EBEF /* ImageNavigationController.swift */; };
|
||||||
|
@ -211,6 +213,7 @@
|
||||||
D06BC5E525202AD90079541D /* ProfileViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileViewController.swift; sourceTree = "<group>"; };
|
D06BC5E525202AD90079541D /* ProfileViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileViewController.swift; sourceTree = "<group>"; };
|
||||||
D0849C7E25903C4900A5EBCC /* Status+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Status+Extensions.swift"; sourceTree = "<group>"; };
|
D0849C7E25903C4900A5EBCC /* Status+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Status+Extensions.swift"; sourceTree = "<group>"; };
|
||||||
D085C3BB25008DEC008A6C5E /* DB */ = {isa = PBXFileReference; lastKnownFileType = folder; path = DB; sourceTree = "<group>"; };
|
D085C3BB25008DEC008A6C5E /* DB */ = {isa = PBXFileReference; lastKnownFileType = folder; path = DB; sourceTree = "<group>"; };
|
||||||
|
D088406C25AFBBE200BB749B /* EmojiPickerViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmojiPickerViewController.swift; sourceTree = "<group>"; };
|
||||||
D08B8D3C253F929E00B1EBEF /* ImageViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageViewController.swift; sourceTree = "<group>"; };
|
D08B8D3C253F929E00B1EBEF /* ImageViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageViewController.swift; sourceTree = "<group>"; };
|
||||||
D08B8D41253F92B600B1EBEF /* ImagePageViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImagePageViewController.swift; sourceTree = "<group>"; };
|
D08B8D41253F92B600B1EBEF /* ImagePageViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImagePageViewController.swift; sourceTree = "<group>"; };
|
||||||
D08B8D49253FC36500B1EBEF /* ImageNavigationController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageNavigationController.swift; sourceTree = "<group>"; };
|
D08B8D49253FC36500B1EBEF /* ImageNavigationController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageNavigationController.swift; sourceTree = "<group>"; };
|
||||||
|
@ -511,6 +514,7 @@
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
D05936CE25A8D79800754FDF /* EditAttachmentViewController.swift */,
|
D05936CE25A8D79800754FDF /* EditAttachmentViewController.swift */,
|
||||||
|
D088406C25AFBBE200BB749B /* EmojiPickerViewController.swift */,
|
||||||
D08B8D49253FC36500B1EBEF /* ImageNavigationController.swift */,
|
D08B8D49253FC36500B1EBEF /* ImageNavigationController.swift */,
|
||||||
D08B8D41253F92B600B1EBEF /* ImagePageViewController.swift */,
|
D08B8D41253F92B600B1EBEF /* ImagePageViewController.swift */,
|
||||||
D08B8D3C253F929E00B1EBEF /* ImageViewController.swift */,
|
D08B8D3C253F929E00B1EBEF /* ImageViewController.swift */,
|
||||||
|
@ -866,6 +870,7 @@
|
||||||
D0BEB20524FA1107001B0F04 /* FiltersView.swift in Sources */,
|
D0BEB20524FA1107001B0F04 /* FiltersView.swift in Sources */,
|
||||||
D0FCC110259C4F20000B67DF /* NewStatusView.swift in Sources */,
|
D0FCC110259C4F20000B67DF /* NewStatusView.swift in Sources */,
|
||||||
D0C7D49B24F7616A001EBDBB /* PreferencesView.swift in Sources */,
|
D0C7D49B24F7616A001EBDBB /* PreferencesView.swift in Sources */,
|
||||||
|
D088406D25AFBBE200BB749B /* EmojiPickerViewController.swift in Sources */,
|
||||||
D0C7D4D724F7616A001EBDBB /* UIColor+Extensions.swift in Sources */,
|
D0C7D4D724F7616A001EBDBB /* UIColor+Extensions.swift in Sources */,
|
||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
@ -904,6 +909,7 @@
|
||||||
D0FCC106259C4E62000B67DF /* NewStatusViewController.swift in Sources */,
|
D0FCC106259C4E62000B67DF /* NewStatusViewController.swift in Sources */,
|
||||||
D036EBBD259FE2A100EC1CFC /* Array+Extensions.swift in Sources */,
|
D036EBBD259FE2A100EC1CFC /* Array+Extensions.swift in Sources */,
|
||||||
D036EBB3259FE28800EC1CFC /* UIColor+Extensions.swift in Sources */,
|
D036EBB3259FE28800EC1CFC /* UIColor+Extensions.swift in Sources */,
|
||||||
|
D088406E25AFBBE200BB749B /* EmojiPickerViewController.swift in Sources */,
|
||||||
D036EBB8259FE29800EC1CFC /* Status+Extensions.swift in Sources */,
|
D036EBB8259FE29800EC1CFC /* Status+Extensions.swift in Sources */,
|
||||||
D05936DF25A937EC00754FDF /* EditThumbnailView.swift in Sources */,
|
D05936DF25A937EC00754FDF /* EditThumbnailView.swift in Sources */,
|
||||||
);
|
);
|
||||||
|
|
|
@ -62,6 +62,12 @@ public extension IdentityService {
|
||||||
.eraseToAnyPublisher()
|
.eraseToAnyPublisher()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func refreshEmojis() -> AnyPublisher<Never, Error> {
|
||||||
|
mastodonAPIClient.request(EmojisEndpoint.customEmojis)
|
||||||
|
.flatMap(contentDatabase.update(emojis:))
|
||||||
|
.eraseToAnyPublisher()
|
||||||
|
}
|
||||||
|
|
||||||
func confirmIdentity() -> AnyPublisher<Never, Error> {
|
func confirmIdentity() -> AnyPublisher<Never, Error> {
|
||||||
identityDatabase.confirmIdentity(id: id)
|
identityDatabase.confirmIdentity(id: id)
|
||||||
}
|
}
|
||||||
|
@ -165,6 +171,10 @@ public extension IdentityService {
|
||||||
contentDatabase.expiredFiltersPublisher()
|
contentDatabase.expiredFiltersPublisher()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func pickerEmojisPublisher() -> AnyPublisher<[Emoji], Error> {
|
||||||
|
contentDatabase.pickerEmojisPublisher()
|
||||||
|
}
|
||||||
|
|
||||||
func updatePreferences(_ preferences: Identity.Preferences) -> AnyPublisher<Never, Error> {
|
func updatePreferences(_ preferences: Identity.Preferences) -> AnyPublisher<Never, Error> {
|
||||||
identityDatabase.updatePreferences(preferences, id: id)
|
identityDatabase.updatePreferences(preferences, id: id)
|
||||||
.collect()
|
.collect()
|
||||||
|
|
63
View Controllers/EmojiPickerViewController.swift
Normal file
63
View Controllers/EmojiPickerViewController.swift
Normal file
|
@ -0,0 +1,63 @@
|
||||||
|
// Copyright © 2021 Metabolist. All rights reserved.
|
||||||
|
|
||||||
|
import Combine
|
||||||
|
import UIKit
|
||||||
|
import ViewModels
|
||||||
|
|
||||||
|
final class EmojiPickerViewController: UIViewController {
|
||||||
|
let searchBar = UISearchBar()
|
||||||
|
|
||||||
|
private let viewModel: EmojiPickerViewModel
|
||||||
|
private var cancellables = Set<AnyCancellable>()
|
||||||
|
|
||||||
|
init(viewModel: EmojiPickerViewModel) {
|
||||||
|
self.viewModel = viewModel
|
||||||
|
|
||||||
|
super.init(nibName: nil, bundle: nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
@available(*, unavailable)
|
||||||
|
required init?(coder: NSCoder) {
|
||||||
|
fatalError("init(coder:) has not been implemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
override func viewDidLoad() {
|
||||||
|
super.viewDidLoad()
|
||||||
|
|
||||||
|
let searchBar = UISearchBar()
|
||||||
|
|
||||||
|
view.addSubview(searchBar)
|
||||||
|
searchBar.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
searchBar.searchBarStyle = .minimal
|
||||||
|
|
||||||
|
NSLayoutConstraint.activate([
|
||||||
|
searchBar.leadingAnchor.constraint(equalTo: view.leadingAnchor),
|
||||||
|
searchBar.topAnchor.constraint(equalTo: view.layoutMarginsGuide.topAnchor),
|
||||||
|
searchBar.trailingAnchor.constraint(equalTo: view.trailingAnchor)
|
||||||
|
])
|
||||||
|
|
||||||
|
// print(UITextInputMode.activeInputModes.map(\.primaryLanguage))
|
||||||
|
print(Locale.availableIdentifiers)
|
||||||
|
}
|
||||||
|
|
||||||
|
override func viewDidAppear(_ animated: Bool) {
|
||||||
|
super.viewDidAppear(animated)
|
||||||
|
|
||||||
|
guard let containerView = popoverPresentationController?.containerView else { return }
|
||||||
|
|
||||||
|
// gets the popover presentation controller's built-in visual effect view to actually show
|
||||||
|
func setClear(view: UIView) {
|
||||||
|
view.backgroundColor = .clear
|
||||||
|
|
||||||
|
if view == self.view {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for view in view.subviews {
|
||||||
|
setClear(view: view)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setClear(view: containerView)
|
||||||
|
}
|
||||||
|
}
|
|
@ -8,6 +8,7 @@ import SwiftUI
|
||||||
import UniformTypeIdentifiers
|
import UniformTypeIdentifiers
|
||||||
import ViewModels
|
import ViewModels
|
||||||
|
|
||||||
|
// swiftlint:disable file_length
|
||||||
final class NewStatusViewController: UIViewController {
|
final class NewStatusViewController: UIViewController {
|
||||||
private let viewModel: NewStatusViewModel
|
private let viewModel: NewStatusViewModel
|
||||||
private let scrollView = UIScrollView()
|
private let scrollView = UIScrollView()
|
||||||
|
@ -121,6 +122,13 @@ extension NewStatusViewController: UIDocumentPickerDelegate {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
extension NewStatusViewController: UIPopoverPresentationControllerDelegate {
|
||||||
|
func adaptivePresentationStyle(for controller: UIPresentationController,
|
||||||
|
traitCollection: UITraitCollection) -> UIModalPresentationStyle {
|
||||||
|
.none
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Required by UIImagePickerController
|
// Required by UIImagePickerController
|
||||||
extension NewStatusViewController: UINavigationControllerDelegate {}
|
extension NewStatusViewController: UINavigationControllerDelegate {}
|
||||||
|
|
||||||
|
@ -135,6 +143,8 @@ private extension NewStatusViewController {
|
||||||
#endif
|
#endif
|
||||||
case let .presentDocumentPicker(compositionViewModel):
|
case let .presentDocumentPicker(compositionViewModel):
|
||||||
presentDocumentPicker(compositionViewModel: compositionViewModel)
|
presentDocumentPicker(compositionViewModel: compositionViewModel)
|
||||||
|
case let .presentEmojiPicker(tag):
|
||||||
|
presentEmojiPicker(tag: tag)
|
||||||
case let .editAttachment(attachmentViewModel, compositionViewModel):
|
case let .editAttachment(attachmentViewModel, compositionViewModel):
|
||||||
presentAttachmentEditor(
|
presentAttachmentEditor(
|
||||||
attachmentViewModel: attachmentViewModel,
|
attachmentViewModel: attachmentViewModel,
|
||||||
|
@ -222,7 +232,10 @@ private extension NewStatusViewController {
|
||||||
.store(in: &cancellables)
|
.store(in: &cancellables)
|
||||||
viewModel.$alertItem
|
viewModel.$alertItem
|
||||||
.compactMap { $0 }
|
.compactMap { $0 }
|
||||||
.sink { [weak self] in self?.present(alertItem: $0) }
|
.sink { [weak self] in
|
||||||
|
self?.dismissEmojiPickerIfPresented()
|
||||||
|
self?.present(alertItem: $0)
|
||||||
|
}
|
||||||
.store(in: &cancellables)
|
.store(in: &cancellables)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -258,6 +271,7 @@ private extension NewStatusViewController {
|
||||||
|
|
||||||
picker.modalPresentationStyle = .overFullScreen
|
picker.modalPresentationStyle = .overFullScreen
|
||||||
picker.delegate = self
|
picker.delegate = self
|
||||||
|
dismissEmojiPickerIfPresented()
|
||||||
present(picker, animated: true)
|
present(picker, animated: true)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -280,7 +294,6 @@ private extension NewStatusViewController {
|
||||||
|
|
||||||
alertController.addAction(openSystemSettingsAction)
|
alertController.addAction(openSystemSettingsAction)
|
||||||
alertController.addAction(cancelAction)
|
alertController.addAction(cancelAction)
|
||||||
|
|
||||||
present(alertController, animated: true)
|
present(alertController, animated: true)
|
||||||
|
|
||||||
return
|
return
|
||||||
|
@ -310,6 +323,7 @@ private extension NewStatusViewController {
|
||||||
picker.mediaTypes = [UTType.image.description]
|
picker.mediaTypes = [UTType.image.description]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
dismissEmojiPickerIfPresented()
|
||||||
present(picker, animated: true)
|
present(picker, animated: true)
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
@ -332,16 +346,49 @@ private extension NewStatusViewController {
|
||||||
documentPickerController.delegate = self
|
documentPickerController.delegate = self
|
||||||
documentPickerController.allowsMultipleSelection = false
|
documentPickerController.allowsMultipleSelection = false
|
||||||
documentPickerController.modalPresentationStyle = .overFullScreen
|
documentPickerController.modalPresentationStyle = .overFullScreen
|
||||||
|
dismissEmojiPickerIfPresented()
|
||||||
present(documentPickerController, animated: true)
|
present(documentPickerController, animated: true)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func presentEmojiPicker(tag: Int) {
|
||||||
|
if dismissEmojiPickerIfPresented() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
guard let fromView = view.viewWithTag(tag) else { return }
|
||||||
|
|
||||||
|
let emojiPickerController = EmojiPickerViewController(
|
||||||
|
viewModel: .init(identification: viewModel.identification))
|
||||||
|
|
||||||
|
emojiPickerController.searchBar.inputAccessoryView = fromView.inputAccessoryView
|
||||||
|
emojiPickerController.preferredContentSize = view.frame.size
|
||||||
|
emojiPickerController.modalPresentationStyle = .popover
|
||||||
|
emojiPickerController.popoverPresentationController?.delegate = self
|
||||||
|
emojiPickerController.popoverPresentationController?.sourceView = fromView
|
||||||
|
emojiPickerController.popoverPresentationController?.sourceRect = fromView.bounds
|
||||||
|
emojiPickerController.popoverPresentationController?.backgroundColor = .clear
|
||||||
|
|
||||||
|
present(emojiPickerController, animated: true)
|
||||||
|
}
|
||||||
|
|
||||||
|
@discardableResult
|
||||||
|
func dismissEmojiPickerIfPresented() -> Bool {
|
||||||
|
let emojiPickerPresented = presentedViewController is EmojiPickerViewController
|
||||||
|
|
||||||
|
if emojiPickerPresented {
|
||||||
|
dismiss(animated: true)
|
||||||
|
}
|
||||||
|
|
||||||
|
return emojiPickerPresented
|
||||||
|
}
|
||||||
|
|
||||||
func presentAttachmentEditor(attachmentViewModel: AttachmentViewModel, compositionViewModel: CompositionViewModel) {
|
func presentAttachmentEditor(attachmentViewModel: AttachmentViewModel, compositionViewModel: CompositionViewModel) {
|
||||||
let editAttachmentsView = EditAttachmentView { (attachmentViewModel, compositionViewModel) }
|
let editAttachmentsView = EditAttachmentView { (attachmentViewModel, compositionViewModel) }
|
||||||
let editAttachmentViewController = UIHostingController(rootView: editAttachmentsView)
|
let editAttachmentViewController = UIHostingController(rootView: editAttachmentsView)
|
||||||
let navigationController = UINavigationController(rootViewController: editAttachmentViewController)
|
let navigationController = UINavigationController(rootViewController: editAttachmentViewController)
|
||||||
|
|
||||||
navigationController.modalPresentationStyle = .overFullScreen
|
navigationController.modalPresentationStyle = .overFullScreen
|
||||||
|
dismissEmojiPickerIfPresented()
|
||||||
present(navigationController, animated: true)
|
present(navigationController, animated: true)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -383,3 +430,4 @@ private extension NewStatusViewController {
|
||||||
return changeIdentityButton
|
return changeIdentityButton
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// swiftlint:enable file_length
|
||||||
|
|
12
ViewModels/Sources/ViewModels/EmojiPickerViewModel.swift
Normal file
12
ViewModels/Sources/ViewModels/EmojiPickerViewModel.swift
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
// Copyright © 2020 Metabolist. All rights reserved.
|
||||||
|
|
||||||
|
import Combine
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
final public class EmojiPickerViewModel: ObservableObject {
|
||||||
|
private let identification: Identification
|
||||||
|
|
||||||
|
public init(identification: Identification) {
|
||||||
|
self.identification = identification
|
||||||
|
}
|
||||||
|
}
|
|
@ -120,6 +120,9 @@ public extension NavigationViewModel {
|
||||||
identification.service.refreshFilters()
|
identification.service.refreshFilters()
|
||||||
.sink { _ in } receiveValue: { _ in }
|
.sink { _ in } receiveValue: { _ in }
|
||||||
.store(in: &cancellables)
|
.store(in: &cancellables)
|
||||||
|
identification.service.refreshEmojis()
|
||||||
|
.sink { _ in } receiveValue: { _ in }
|
||||||
|
.store(in: &cancellables)
|
||||||
|
|
||||||
if identification.identity.preferences.useServerPostingReadingPreferences {
|
if identification.identity.preferences.useServerPostingReadingPreferences {
|
||||||
identification.service.refreshServerPreferences()
|
identification.service.refreshServerPreferences()
|
||||||
|
|
|
@ -66,6 +66,7 @@ public extension NewStatusViewModel {
|
||||||
case presentMediaPicker(CompositionViewModel)
|
case presentMediaPicker(CompositionViewModel)
|
||||||
case presentCamera(CompositionViewModel)
|
case presentCamera(CompositionViewModel)
|
||||||
case presentDocumentPicker(CompositionViewModel)
|
case presentDocumentPicker(CompositionViewModel)
|
||||||
|
case presentEmojiPicker(Int)
|
||||||
case editAttachment(AttachmentViewModel, CompositionViewModel)
|
case editAttachment(AttachmentViewModel, CompositionViewModel)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -106,6 +107,10 @@ public extension NewStatusViewModel {
|
||||||
eventsSubject.send(.presentDocumentPicker(viewModel))
|
eventsSubject.send(.presentDocumentPicker(viewModel))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func presentEmojiPicker(tag: Int) {
|
||||||
|
eventsSubject.send(.presentEmojiPicker(tag))
|
||||||
|
}
|
||||||
|
|
||||||
func remove(viewModel: CompositionViewModel) {
|
func remove(viewModel: CompositionViewModel) {
|
||||||
compositionViewModels.removeAll { $0 === viewModel }
|
compositionViewModels.removeAll { $0 === viewModel }
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,6 +10,7 @@ final class CompositionInputAccessoryView: UIView {
|
||||||
let visibilityButton = UIButton()
|
let visibilityButton = UIButton()
|
||||||
let addButton = UIButton()
|
let addButton = UIButton()
|
||||||
let contentWarningButton = UIButton(type: .system)
|
let contentWarningButton = UIButton(type: .system)
|
||||||
|
let tagForInputView = UUID().hashValue
|
||||||
|
|
||||||
private let viewModel: CompositionViewModel
|
private let viewModel: CompositionViewModel
|
||||||
private let parentViewModel: NewStatusViewModel
|
private let parentViewModel: NewStatusViewModel
|
||||||
|
@ -111,6 +112,19 @@ private extension CompositionInputAccessoryView {
|
||||||
UIAction { [weak self] _ in self?.viewModel.displayContentWarning.toggle() },
|
UIAction { [weak self] _ in self?.viewModel.displayContentWarning.toggle() },
|
||||||
for: .touchUpInside)
|
for: .touchUpInside)
|
||||||
|
|
||||||
|
let emojiButton = UIButton(primaryAction: UIAction { [weak self] _ in
|
||||||
|
guard let self = self else { return }
|
||||||
|
|
||||||
|
self.parentViewModel.presentEmojiPicker(tag: self.tagForInputView)
|
||||||
|
})
|
||||||
|
|
||||||
|
stackView.addArrangedSubview(emojiButton)
|
||||||
|
emojiButton.setImage(
|
||||||
|
UIImage(
|
||||||
|
systemName: "face.smiling",
|
||||||
|
withConfiguration: UIImage.SymbolConfiguration(scale: .medium)),
|
||||||
|
for: .normal)
|
||||||
|
|
||||||
stackView.addArrangedSubview(UIView())
|
stackView.addArrangedSubview(UIView())
|
||||||
|
|
||||||
let charactersLabel = UILabel()
|
let charactersLabel = UILabel()
|
||||||
|
@ -155,7 +169,7 @@ private extension CompositionInputAccessoryView {
|
||||||
}
|
}
|
||||||
.store(in: &cancellables)
|
.store(in: &cancellables)
|
||||||
|
|
||||||
for button in [attachmentButton, pollButton, visibilityButton, contentWarningButton, addButton] {
|
for button in [attachmentButton, pollButton, visibilityButton, contentWarningButton, emojiButton, addButton] {
|
||||||
button.heightAnchor.constraint(greaterThanOrEqualToConstant: .minimumButtonDimension).isActive = true
|
button.heightAnchor.constraint(greaterThanOrEqualToConstant: .minimumButtonDimension).isActive = true
|
||||||
button.widthAnchor.constraint(greaterThanOrEqualToConstant: .minimumButtonDimension).isActive = true
|
button.widthAnchor.constraint(greaterThanOrEqualToConstant: .minimumButtonDimension).isActive = true
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,15 +8,15 @@ final class CompositionPollOptionView: UIView {
|
||||||
let option: CompositionViewModel.PollOption
|
let option: CompositionViewModel.PollOption
|
||||||
let removeButton = UIButton(type: .close)
|
let removeButton = UIButton(type: .close)
|
||||||
private let viewModel: CompositionViewModel
|
private let viewModel: CompositionViewModel
|
||||||
private let compositionInputAccessoryView: CompositionInputAccessoryView
|
private let parentViewModel: NewStatusViewModel
|
||||||
private var cancellables = Set<AnyCancellable>()
|
private var cancellables = Set<AnyCancellable>()
|
||||||
|
|
||||||
init(viewModel: CompositionViewModel,
|
init(viewModel: CompositionViewModel,
|
||||||
option: CompositionViewModel.PollOption,
|
parentViewModel: NewStatusViewModel,
|
||||||
inputAccessoryView: CompositionInputAccessoryView) {
|
option: CompositionViewModel.PollOption) {
|
||||||
self.viewModel = viewModel
|
self.viewModel = viewModel
|
||||||
|
self.parentViewModel = parentViewModel
|
||||||
self.option = option
|
self.option = option
|
||||||
self.compositionInputAccessoryView = inputAccessoryView
|
|
||||||
|
|
||||||
super.init(frame: .zero)
|
super.init(frame: .zero)
|
||||||
|
|
||||||
|
@ -44,7 +44,11 @@ private extension CompositionPollOptionView {
|
||||||
textField.borderStyle = .roundedRect
|
textField.borderStyle = .roundedRect
|
||||||
textField.adjustsFontForContentSizeCategory = true
|
textField.adjustsFontForContentSizeCategory = true
|
||||||
textField.font = .preferredFont(forTextStyle: .body)
|
textField.font = .preferredFont(forTextStyle: .body)
|
||||||
textField.inputAccessoryView = compositionInputAccessoryView
|
let textInputAccessoryView = CompositionInputAccessoryView(
|
||||||
|
viewModel: viewModel,
|
||||||
|
parentViewModel: parentViewModel)
|
||||||
|
textField.inputAccessoryView = textInputAccessoryView
|
||||||
|
textField.tag = textInputAccessoryView.tagForInputView
|
||||||
textField.addAction(
|
textField.addAction(
|
||||||
UIAction { [weak self] _ in
|
UIAction { [weak self] _ in
|
||||||
self?.option.text = textField.text ?? "" },
|
self?.option.text = textField.text ?? "" },
|
||||||
|
|
|
@ -6,13 +6,13 @@ import ViewModels
|
||||||
|
|
||||||
final class CompositionPollView: UIView {
|
final class CompositionPollView: UIView {
|
||||||
private let viewModel: CompositionViewModel
|
private let viewModel: CompositionViewModel
|
||||||
private let compositionInputAccessoryView: CompositionInputAccessoryView
|
private let parentViewModel: NewStatusViewModel
|
||||||
private let stackView = UIStackView()
|
private let stackView = UIStackView()
|
||||||
private var cancellables = Set<AnyCancellable>()
|
private var cancellables = Set<AnyCancellable>()
|
||||||
|
|
||||||
init(viewModel: CompositionViewModel, inputAccessoryView: CompositionInputAccessoryView) {
|
init(viewModel: CompositionViewModel, parentViewModel: NewStatusViewModel) {
|
||||||
self.viewModel = viewModel
|
self.viewModel = viewModel
|
||||||
self.compositionInputAccessoryView = inputAccessoryView
|
self.parentViewModel = parentViewModel
|
||||||
|
|
||||||
super.init(frame: .zero)
|
super.init(frame: .zero)
|
||||||
|
|
||||||
|
@ -118,8 +118,8 @@ private extension CompositionPollView {
|
||||||
if !self.pollOptionViews.contains(where: { $0.option === option }) {
|
if !self.pollOptionViews.contains(where: { $0.option === option }) {
|
||||||
let optionView = CompositionPollOptionView(
|
let optionView = CompositionPollOptionView(
|
||||||
viewModel: self.viewModel,
|
viewModel: self.viewModel,
|
||||||
option: option,
|
parentViewModel: self.parentViewModel,
|
||||||
inputAccessoryView: self.compositionInputAccessoryView)
|
option: option)
|
||||||
|
|
||||||
self.stackView.insertArrangedSubview(optionView, at: index)
|
self.stackView.insertArrangedSubview(optionView, at: index)
|
||||||
}
|
}
|
||||||
|
|
|
@ -13,7 +13,6 @@ final class CompositionView: UIView {
|
||||||
let removeButton = UIButton(type: .close)
|
let removeButton = UIButton(type: .close)
|
||||||
let inReplyToView = UIView()
|
let inReplyToView = UIView()
|
||||||
let hasReplyFollowingView = UIView()
|
let hasReplyFollowingView = UIView()
|
||||||
let compositionInputAccessoryView: CompositionInputAccessoryView
|
|
||||||
let attachmentsView = AttachmentsView()
|
let attachmentsView = AttachmentsView()
|
||||||
let attachmentUploadView: AttachmentUploadView
|
let attachmentUploadView: AttachmentUploadView
|
||||||
let pollView: CompositionPollView
|
let pollView: CompositionPollView
|
||||||
|
@ -27,12 +26,9 @@ final class CompositionView: UIView {
|
||||||
self.viewModel = viewModel
|
self.viewModel = viewModel
|
||||||
self.parentViewModel = parentViewModel
|
self.parentViewModel = parentViewModel
|
||||||
|
|
||||||
compositionInputAccessoryView = CompositionInputAccessoryView(
|
|
||||||
viewModel: viewModel,
|
|
||||||
parentViewModel: parentViewModel)
|
|
||||||
attachmentUploadView = AttachmentUploadView(viewModel: viewModel)
|
attachmentUploadView = AttachmentUploadView(viewModel: viewModel)
|
||||||
markAttachmentsSensitiveView = MarkAttachmentsSensitiveView(viewModel: viewModel)
|
markAttachmentsSensitiveView = MarkAttachmentsSensitiveView(viewModel: viewModel)
|
||||||
pollView = CompositionPollView(viewModel: viewModel, inputAccessoryView: compositionInputAccessoryView)
|
pollView = CompositionPollView(viewModel: viewModel, parentViewModel: parentViewModel)
|
||||||
|
|
||||||
super.init(frame: .zero)
|
super.init(frame: .zero)
|
||||||
|
|
||||||
|
@ -75,12 +71,17 @@ private extension CompositionView {
|
||||||
stackView.axis = .vertical
|
stackView.axis = .vertical
|
||||||
stackView.spacing = .defaultSpacing
|
stackView.spacing = .defaultSpacing
|
||||||
|
|
||||||
|
let spoilerTextinputAccessoryView = CompositionInputAccessoryView(
|
||||||
|
viewModel: viewModel,
|
||||||
|
parentViewModel: parentViewModel)
|
||||||
|
|
||||||
stackView.addArrangedSubview(spoilerTextField)
|
stackView.addArrangedSubview(spoilerTextField)
|
||||||
spoilerTextField.borderStyle = .roundedRect
|
spoilerTextField.borderStyle = .roundedRect
|
||||||
spoilerTextField.adjustsFontForContentSizeCategory = true
|
spoilerTextField.adjustsFontForContentSizeCategory = true
|
||||||
spoilerTextField.font = .preferredFont(forTextStyle: .body)
|
spoilerTextField.font = .preferredFont(forTextStyle: .body)
|
||||||
spoilerTextField.placeholder = NSLocalizedString("status.spoiler-text-placeholder", comment: "")
|
spoilerTextField.placeholder = NSLocalizedString("status.spoiler-text-placeholder", comment: "")
|
||||||
spoilerTextField.inputAccessoryView = compositionInputAccessoryView
|
spoilerTextField.inputAccessoryView = spoilerTextinputAccessoryView
|
||||||
|
spoilerTextField.tag = spoilerTextinputAccessoryView.tagForInputView
|
||||||
spoilerTextField.addAction(
|
spoilerTextField.addAction(
|
||||||
UIAction { [weak self] _ in
|
UIAction { [weak self] _ in
|
||||||
guard let self = self, let text = self.spoilerTextField.text else { return }
|
guard let self = self, let text = self.spoilerTextField.text else { return }
|
||||||
|
@ -90,6 +91,9 @@ private extension CompositionView {
|
||||||
for: .editingChanged)
|
for: .editingChanged)
|
||||||
|
|
||||||
let textViewFont = UIFont.preferredFont(forTextStyle: .body)
|
let textViewFont = UIFont.preferredFont(forTextStyle: .body)
|
||||||
|
let textInputAccessoryView = CompositionInputAccessoryView(
|
||||||
|
viewModel: viewModel,
|
||||||
|
parentViewModel: parentViewModel)
|
||||||
|
|
||||||
stackView.addArrangedSubview(textView)
|
stackView.addArrangedSubview(textView)
|
||||||
textView.isScrollEnabled = false
|
textView.isScrollEnabled = false
|
||||||
|
@ -97,7 +101,8 @@ private extension CompositionView {
|
||||||
textView.font = textViewFont
|
textView.font = textViewFont
|
||||||
textView.textContainerInset = .zero
|
textView.textContainerInset = .zero
|
||||||
textView.textContainer.lineFragmentPadding = 0
|
textView.textContainer.lineFragmentPadding = 0
|
||||||
textView.inputAccessoryView = compositionInputAccessoryView
|
textView.inputAccessoryView = textInputAccessoryView
|
||||||
|
textView.tag = textInputAccessoryView.tagForInputView
|
||||||
textView.inputAccessoryView?.sizeToFit()
|
textView.inputAccessoryView?.sizeToFit()
|
||||||
textView.delegate = self
|
textView.delegate = self
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue