This commit is contained in:
Justin Mazzocchi 2021-01-14 09:49:53 -08:00
parent ef8fd98e4b
commit def0e3fff0
No known key found for this signature in database
GPG key ID: E223E6937AAFB01C
15 changed files with 249 additions and 21 deletions

View file

@ -382,6 +382,18 @@ public extension ContentDatabase {
.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> {
ValueObservation.tracking(
TimelineItemsInfo.request(TimelineRecord.filter(TimelineRecord.Columns.id == timeline.id)).fetchOne)
@ -492,6 +504,13 @@ public extension ContentDatabase {
.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? {
try? databaseWriter.read {
try String.fetchOne(

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

View file

@ -7,4 +7,5 @@ public struct Emoji: Codable, Hashable {
public let staticUrl: URL
public let url: URL
public let visibleInPicker: Bool
public let category: String?
}

View file

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

View file

@ -55,6 +55,8 @@
D0625E5D250F0B5C00502611 /* StatusContentConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0625E5C250F0B5C00502611 /* StatusContentConfiguration.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 */; };
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 */; };
D08B8D42253F92B600B1EBEF /* ImagePageViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D08B8D41253F92B600B1EBEF /* ImagePageViewController.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>"; };
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>"; };
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>"; };
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>"; };
@ -511,6 +514,7 @@
isa = PBXGroup;
children = (
D05936CE25A8D79800754FDF /* EditAttachmentViewController.swift */,
D088406C25AFBBE200BB749B /* EmojiPickerViewController.swift */,
D08B8D49253FC36500B1EBEF /* ImageNavigationController.swift */,
D08B8D41253F92B600B1EBEF /* ImagePageViewController.swift */,
D08B8D3C253F929E00B1EBEF /* ImageViewController.swift */,
@ -866,6 +870,7 @@
D0BEB20524FA1107001B0F04 /* FiltersView.swift in Sources */,
D0FCC110259C4F20000B67DF /* NewStatusView.swift in Sources */,
D0C7D49B24F7616A001EBDBB /* PreferencesView.swift in Sources */,
D088406D25AFBBE200BB749B /* EmojiPickerViewController.swift in Sources */,
D0C7D4D724F7616A001EBDBB /* UIColor+Extensions.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
@ -904,6 +909,7 @@
D0FCC106259C4E62000B67DF /* NewStatusViewController.swift in Sources */,
D036EBBD259FE2A100EC1CFC /* Array+Extensions.swift in Sources */,
D036EBB3259FE28800EC1CFC /* UIColor+Extensions.swift in Sources */,
D088406E25AFBBE200BB749B /* EmojiPickerViewController.swift in Sources */,
D036EBB8259FE29800EC1CFC /* Status+Extensions.swift in Sources */,
D05936DF25A937EC00754FDF /* EditThumbnailView.swift in Sources */,
);

View file

@ -62,6 +62,12 @@ public extension IdentityService {
.eraseToAnyPublisher()
}
func refreshEmojis() -> AnyPublisher<Never, Error> {
mastodonAPIClient.request(EmojisEndpoint.customEmojis)
.flatMap(contentDatabase.update(emojis:))
.eraseToAnyPublisher()
}
func confirmIdentity() -> AnyPublisher<Never, Error> {
identityDatabase.confirmIdentity(id: id)
}
@ -165,6 +171,10 @@ public extension IdentityService {
contentDatabase.expiredFiltersPublisher()
}
func pickerEmojisPublisher() -> AnyPublisher<[Emoji], Error> {
contentDatabase.pickerEmojisPublisher()
}
func updatePreferences(_ preferences: Identity.Preferences) -> AnyPublisher<Never, Error> {
identityDatabase.updatePreferences(preferences, id: id)
.collect()

View 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)
}
}

View file

@ -8,6 +8,7 @@ import SwiftUI
import UniformTypeIdentifiers
import ViewModels
// swiftlint:disable file_length
final class NewStatusViewController: UIViewController {
private let viewModel: NewStatusViewModel
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
extension NewStatusViewController: UINavigationControllerDelegate {}
@ -135,6 +143,8 @@ private extension NewStatusViewController {
#endif
case let .presentDocumentPicker(compositionViewModel):
presentDocumentPicker(compositionViewModel: compositionViewModel)
case let .presentEmojiPicker(tag):
presentEmojiPicker(tag: tag)
case let .editAttachment(attachmentViewModel, compositionViewModel):
presentAttachmentEditor(
attachmentViewModel: attachmentViewModel,
@ -222,7 +232,10 @@ private extension NewStatusViewController {
.store(in: &cancellables)
viewModel.$alertItem
.compactMap { $0 }
.sink { [weak self] in self?.present(alertItem: $0) }
.sink { [weak self] in
self?.dismissEmojiPickerIfPresented()
self?.present(alertItem: $0)
}
.store(in: &cancellables)
}
@ -258,6 +271,7 @@ private extension NewStatusViewController {
picker.modalPresentationStyle = .overFullScreen
picker.delegate = self
dismissEmojiPickerIfPresented()
present(picker, animated: true)
}
@ -280,7 +294,6 @@ private extension NewStatusViewController {
alertController.addAction(openSystemSettingsAction)
alertController.addAction(cancelAction)
present(alertController, animated: true)
return
@ -310,6 +323,7 @@ private extension NewStatusViewController {
picker.mediaTypes = [UTType.image.description]
}
dismissEmojiPickerIfPresented()
present(picker, animated: true)
}
#endif
@ -332,16 +346,49 @@ private extension NewStatusViewController {
documentPickerController.delegate = self
documentPickerController.allowsMultipleSelection = false
documentPickerController.modalPresentationStyle = .overFullScreen
dismissEmojiPickerIfPresented()
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) {
let editAttachmentsView = EditAttachmentView { (attachmentViewModel, compositionViewModel) }
let editAttachmentViewController = UIHostingController(rootView: editAttachmentsView)
let navigationController = UINavigationController(rootViewController: editAttachmentViewController)
navigationController.modalPresentationStyle = .overFullScreen
dismissEmojiPickerIfPresented()
present(navigationController, animated: true)
}
@ -383,3 +430,4 @@ private extension NewStatusViewController {
return changeIdentityButton
}
}
// swiftlint:enable file_length

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

View file

@ -120,6 +120,9 @@ public extension NavigationViewModel {
identification.service.refreshFilters()
.sink { _ in } receiveValue: { _ in }
.store(in: &cancellables)
identification.service.refreshEmojis()
.sink { _ in } receiveValue: { _ in }
.store(in: &cancellables)
if identification.identity.preferences.useServerPostingReadingPreferences {
identification.service.refreshServerPreferences()

View file

@ -66,6 +66,7 @@ public extension NewStatusViewModel {
case presentMediaPicker(CompositionViewModel)
case presentCamera(CompositionViewModel)
case presentDocumentPicker(CompositionViewModel)
case presentEmojiPicker(Int)
case editAttachment(AttachmentViewModel, CompositionViewModel)
}
@ -106,6 +107,10 @@ public extension NewStatusViewModel {
eventsSubject.send(.presentDocumentPicker(viewModel))
}
func presentEmojiPicker(tag: Int) {
eventsSubject.send(.presentEmojiPicker(tag))
}
func remove(viewModel: CompositionViewModel) {
compositionViewModels.removeAll { $0 === viewModel }
}

View file

@ -10,6 +10,7 @@ final class CompositionInputAccessoryView: UIView {
let visibilityButton = UIButton()
let addButton = UIButton()
let contentWarningButton = UIButton(type: .system)
let tagForInputView = UUID().hashValue
private let viewModel: CompositionViewModel
private let parentViewModel: NewStatusViewModel
@ -111,6 +112,19 @@ private extension CompositionInputAccessoryView {
UIAction { [weak self] _ in self?.viewModel.displayContentWarning.toggle() },
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())
let charactersLabel = UILabel()
@ -155,7 +169,7 @@ private extension CompositionInputAccessoryView {
}
.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.widthAnchor.constraint(greaterThanOrEqualToConstant: .minimumButtonDimension).isActive = true
}

View file

@ -8,15 +8,15 @@ final class CompositionPollOptionView: UIView {
let option: CompositionViewModel.PollOption
let removeButton = UIButton(type: .close)
private let viewModel: CompositionViewModel
private let compositionInputAccessoryView: CompositionInputAccessoryView
private let parentViewModel: NewStatusViewModel
private var cancellables = Set<AnyCancellable>()
init(viewModel: CompositionViewModel,
option: CompositionViewModel.PollOption,
inputAccessoryView: CompositionInputAccessoryView) {
parentViewModel: NewStatusViewModel,
option: CompositionViewModel.PollOption) {
self.viewModel = viewModel
self.parentViewModel = parentViewModel
self.option = option
self.compositionInputAccessoryView = inputAccessoryView
super.init(frame: .zero)
@ -44,7 +44,11 @@ private extension CompositionPollOptionView {
textField.borderStyle = .roundedRect
textField.adjustsFontForContentSizeCategory = true
textField.font = .preferredFont(forTextStyle: .body)
textField.inputAccessoryView = compositionInputAccessoryView
let textInputAccessoryView = CompositionInputAccessoryView(
viewModel: viewModel,
parentViewModel: parentViewModel)
textField.inputAccessoryView = textInputAccessoryView
textField.tag = textInputAccessoryView.tagForInputView
textField.addAction(
UIAction { [weak self] _ in
self?.option.text = textField.text ?? "" },

View file

@ -6,13 +6,13 @@ import ViewModels
final class CompositionPollView: UIView {
private let viewModel: CompositionViewModel
private let compositionInputAccessoryView: CompositionInputAccessoryView
private let parentViewModel: NewStatusViewModel
private let stackView = UIStackView()
private var cancellables = Set<AnyCancellable>()
init(viewModel: CompositionViewModel, inputAccessoryView: CompositionInputAccessoryView) {
init(viewModel: CompositionViewModel, parentViewModel: NewStatusViewModel) {
self.viewModel = viewModel
self.compositionInputAccessoryView = inputAccessoryView
self.parentViewModel = parentViewModel
super.init(frame: .zero)
@ -118,8 +118,8 @@ private extension CompositionPollView {
if !self.pollOptionViews.contains(where: { $0.option === option }) {
let optionView = CompositionPollOptionView(
viewModel: self.viewModel,
option: option,
inputAccessoryView: self.compositionInputAccessoryView)
parentViewModel: self.parentViewModel,
option: option)
self.stackView.insertArrangedSubview(optionView, at: index)
}

View file

@ -13,7 +13,6 @@ final class CompositionView: UIView {
let removeButton = UIButton(type: .close)
let inReplyToView = UIView()
let hasReplyFollowingView = UIView()
let compositionInputAccessoryView: CompositionInputAccessoryView
let attachmentsView = AttachmentsView()
let attachmentUploadView: AttachmentUploadView
let pollView: CompositionPollView
@ -27,12 +26,9 @@ final class CompositionView: UIView {
self.viewModel = viewModel
self.parentViewModel = parentViewModel
compositionInputAccessoryView = CompositionInputAccessoryView(
viewModel: viewModel,
parentViewModel: parentViewModel)
attachmentUploadView = AttachmentUploadView(viewModel: viewModel)
markAttachmentsSensitiveView = MarkAttachmentsSensitiveView(viewModel: viewModel)
pollView = CompositionPollView(viewModel: viewModel, inputAccessoryView: compositionInputAccessoryView)
pollView = CompositionPollView(viewModel: viewModel, parentViewModel: parentViewModel)
super.init(frame: .zero)
@ -75,12 +71,17 @@ private extension CompositionView {
stackView.axis = .vertical
stackView.spacing = .defaultSpacing
let spoilerTextinputAccessoryView = CompositionInputAccessoryView(
viewModel: viewModel,
parentViewModel: parentViewModel)
stackView.addArrangedSubview(spoilerTextField)
spoilerTextField.borderStyle = .roundedRect
spoilerTextField.adjustsFontForContentSizeCategory = true
spoilerTextField.font = .preferredFont(forTextStyle: .body)
spoilerTextField.placeholder = NSLocalizedString("status.spoiler-text-placeholder", comment: "")
spoilerTextField.inputAccessoryView = compositionInputAccessoryView
spoilerTextField.inputAccessoryView = spoilerTextinputAccessoryView
spoilerTextField.tag = spoilerTextinputAccessoryView.tagForInputView
spoilerTextField.addAction(
UIAction { [weak self] _ in
guard let self = self, let text = self.spoilerTextField.text else { return }
@ -90,6 +91,9 @@ private extension CompositionView {
for: .editingChanged)
let textViewFont = UIFont.preferredFont(forTextStyle: .body)
let textInputAccessoryView = CompositionInputAccessoryView(
viewModel: viewModel,
parentViewModel: parentViewModel)
stackView.addArrangedSubview(textView)
textView.isScrollEnabled = false
@ -97,7 +101,8 @@ private extension CompositionView {
textView.font = textViewFont
textView.textContainerInset = .zero
textView.textContainer.lineFragmentPadding = 0
textView.inputAccessoryView = compositionInputAccessoryView
textView.inputAccessoryView = textInputAccessoryView
textView.tag = textInputAccessoryView.tagForInputView
textView.inputAccessoryView?.sizeToFit()
textView.delegate = self