This commit is contained in:
Justin Mazzocchi 2021-02-13 18:28:30 -08:00
parent f16426877c
commit 2cb8370e68
No known key found for this signature in database
GPG key ID: E223E6937AAFB01C
6 changed files with 87 additions and 5 deletions

View file

@ -0,0 +1,13 @@
// Copyright © 2021 Metabolist. All rights reserved.
import UIKit
extension UITextInput {
var textToSelectedRange: String? {
guard let selectedRange = selectedTextRange,
let range = textRange(from: beginningOfDocument, to: selectedRange.end)
else { return nil }
return text(in: range)
}
}

View file

@ -171,6 +171,8 @@
D0DDA77F25C6058300FA0F91 /* ExploreSectionHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0DDA77E25C6058300FA0F91 /* ExploreSectionHeaderView.swift */; }; D0DDA77F25C6058300FA0F91 /* ExploreSectionHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0DDA77E25C6058300FA0F91 /* ExploreSectionHeaderView.swift */; };
D0E1F583251F13EC00D45315 /* WebfingerIndicatorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0E1F582251F13EC00D45315 /* WebfingerIndicatorView.swift */; }; D0E1F583251F13EC00D45315 /* WebfingerIndicatorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0E1F582251F13EC00D45315 /* WebfingerIndicatorView.swift */; };
D0E2C1D124FD97F000854680 /* ViewModels in Frameworks */ = {isa = PBXBuildFile; productRef = D0E2C1D024FD97F000854680 /* ViewModels */; }; D0E2C1D124FD97F000854680 /* ViewModels in Frameworks */ = {isa = PBXBuildFile; productRef = D0E2C1D024FD97F000854680 /* ViewModels */; };
D0E39AB425D8BF88009C10F8 /* UITextInput+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0E39AB325D8BF88009C10F8 /* UITextInput+Extensions.swift */; };
D0E39ABD25D8C046009C10F8 /* UITextInput+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0E39AB325D8BF88009C10F8 /* UITextInput+Extensions.swift */; };
D0E5361C24E3EB4D00FB1CE1 /* NotificationService.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0E5361B24E3EB4D00FB1CE1 /* NotificationService.swift */; }; D0E5361C24E3EB4D00FB1CE1 /* NotificationService.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0E5361B24E3EB4D00FB1CE1 /* NotificationService.swift */; };
D0E5362024E3EB4D00FB1CE1 /* Notification Service Extension.appex in Embed App Extensions */ = {isa = PBXBuildFile; fileRef = D0E5361924E3EB4D00FB1CE1 /* Notification Service Extension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; D0E5362024E3EB4D00FB1CE1 /* Notification Service Extension.appex in Embed App Extensions */ = {isa = PBXBuildFile; fileRef = D0E5361924E3EB4D00FB1CE1 /* Notification Service Extension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
D0E569DB2529319100FA1D72 /* LoadMoreView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0E569DA2529319100FA1D72 /* LoadMoreView.swift */; }; D0E569DB2529319100FA1D72 /* LoadMoreView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0E569DA2529319100FA1D72 /* LoadMoreView.swift */; };
@ -378,6 +380,7 @@
D0E0F1E424FC49FC002C04BF /* Mastodon */ = {isa = PBXFileReference; lastKnownFileType = folder; path = Mastodon; sourceTree = "<group>"; }; D0E0F1E424FC49FC002C04BF /* Mastodon */ = {isa = PBXFileReference; lastKnownFileType = folder; path = Mastodon; sourceTree = "<group>"; };
D0E1F582251F13EC00D45315 /* WebfingerIndicatorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebfingerIndicatorView.swift; sourceTree = "<group>"; }; D0E1F582251F13EC00D45315 /* WebfingerIndicatorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebfingerIndicatorView.swift; sourceTree = "<group>"; };
D0E2C1CF24FD8BA400854680 /* ViewModels */ = {isa = PBXFileReference; lastKnownFileType = folder; path = ViewModels; sourceTree = "<group>"; }; D0E2C1CF24FD8BA400854680 /* ViewModels */ = {isa = PBXFileReference; lastKnownFileType = folder; path = ViewModels; sourceTree = "<group>"; };
D0E39AB325D8BF88009C10F8 /* UITextInput+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UITextInput+Extensions.swift"; sourceTree = "<group>"; };
D0E5361924E3EB4D00FB1CE1 /* Notification Service Extension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = "Notification Service Extension.appex"; sourceTree = BUILT_PRODUCTS_DIR; }; D0E5361924E3EB4D00FB1CE1 /* Notification Service Extension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = "Notification Service Extension.appex"; sourceTree = BUILT_PRODUCTS_DIR; };
D0E5361B24E3EB4D00FB1CE1 /* NotificationService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationService.swift; sourceTree = "<group>"; }; D0E5361B24E3EB4D00FB1CE1 /* NotificationService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationService.swift; sourceTree = "<group>"; };
D0E5361D24E3EB4D00FB1CE1 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; }; D0E5361D24E3EB4D00FB1CE1 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
@ -760,6 +763,7 @@
D0C7D46C24F76169001EBDBB /* UIColor+Extensions.swift */, D0C7D46C24F76169001EBDBB /* UIColor+Extensions.swift */,
D0BE980D25D241CE0057E161 /* UIImage+Extensions.swift */, D0BE980D25D241CE0057E161 /* UIImage+Extensions.swift */,
D08DFB0025CE228E0005DA98 /* UIScrollView+Extensions.swift */, D08DFB0025CE228E0005DA98 /* UIScrollView+Extensions.swift */,
D0E39AB325D8BF88009C10F8 /* UITextInput+Extensions.swift */,
D05936F325AA66A600754FDF /* UIView+Extensions.swift */, D05936F325AA66A600754FDF /* UIView+Extensions.swift */,
D0E7AD3825870B13005F5E2D /* UIVIewController+Extensions.swift */, D0E7AD3825870B13005F5E2D /* UIVIewController+Extensions.swift */,
D0030981250C6C8500EACB32 /* URL+Extensions.swift */, D0030981250C6C8500EACB32 /* URL+Extensions.swift */,
@ -1010,6 +1014,7 @@
D05936DE25A937EC00754FDF /* EditThumbnailView.swift in Sources */, D05936DE25A937EC00754FDF /* EditThumbnailView.swift in Sources */,
D0F0B136251AA12700942152 /* CollectionItem+Extensions.swift in Sources */, D0F0B136251AA12700942152 /* CollectionItem+Extensions.swift in Sources */,
D007023E25562A2800F38136 /* ConversationAvatarsView.swift in Sources */, D007023E25562A2800F38136 /* ConversationAvatarsView.swift in Sources */,
D0E39AB425D8BF88009C10F8 /* UITextInput+Extensions.swift in Sources */,
D0E7AD3925870B13005F5E2D /* UIVIewController+Extensions.swift in Sources */, D0E7AD3925870B13005F5E2D /* UIVIewController+Extensions.swift in Sources */,
D0625E5D250F0B5C00502611 /* StatusContentConfiguration.swift in Sources */, D0625E5D250F0B5C00502611 /* StatusContentConfiguration.swift in Sources */,
D0BEB1F324F8EE8C001B0F04 /* AttachmentView.swift in Sources */, D0BEB1F324F8EE8C001B0F04 /* AttachmentView.swift in Sources */,
@ -1166,6 +1171,7 @@
D07EC7D025B13921006DF726 /* PickerEmoji+Extensions.swift in Sources */, D07EC7D025B13921006DF726 /* PickerEmoji+Extensions.swift in Sources */,
D0FCC106259C4E62000B67DF /* NewStatusViewController.swift in Sources */, D0FCC106259C4E62000B67DF /* NewStatusViewController.swift in Sources */,
D036EBB3259FE28800EC1CFC /* UIColor+Extensions.swift in Sources */, D036EBB3259FE28800EC1CFC /* UIColor+Extensions.swift in Sources */,
D0E39ABD25D8C046009C10F8 /* UITextInput+Extensions.swift in Sources */,
D088406E25AFBBE200BB749B /* EmojiPickerViewController.swift in Sources */, D088406E25AFBBE200BB749B /* EmojiPickerViewController.swift in Sources */,
D00CB23325C92F2D008EF267 /* Attachment+Extensions.swift in Sources */, D00CB23325C92F2D008EF267 /* Attachment+Extensions.swift in Sources */,
D025B14725C4D26B001C69A8 /* ImageCacheSerializer.swift in Sources */, D025B14725C4D26B001C69A8 /* ImageCacheSerializer.swift in Sources */,

View file

@ -10,12 +10,16 @@ public final class CompositionViewModel: AttachmentsRenderingViewModel, Observab
public let id = Id() public let id = Id()
public var isPosted = false public var isPosted = false
@Published public var text = "" @Published public var text = ""
@Published public var textToSelectedRange = ""
@Published public var contentWarning = "" @Published public var contentWarning = ""
@Published public var contentWarningTextToSelectedRange = ""
@Published public var displayContentWarning = false @Published public var displayContentWarning = false
@Published public var sensitive = false @Published public var sensitive = false
@Published public var displayPoll = false @Published public var displayPoll = false
@Published public var pollMultipleChoice = false @Published public var pollMultipleChoice = false
@Published public var pollExpiresIn = PollExpiry.oneDay @Published public var pollExpiresIn = PollExpiry.oneDay
@Published public private(set) var autocompleteQuery: String?
@Published public private(set) var contentWarningAutocompleteQuery: String?
@Published public private(set) var pollOptions = [PollOption(text: ""), PollOption(text: "")] @Published public private(set) var pollOptions = [PollOption(text: ""), PollOption(text: "")]
@Published public private(set) var attachmentViewModels = [AttachmentViewModel]() @Published public private(set) var attachmentViewModels = [AttachmentViewModel]()
@Published public private(set) var attachmentUpload: AttachmentUpload? @Published public private(set) var attachmentUpload: AttachmentUpload?
@ -38,11 +42,14 @@ public final class CompositionViewModel: AttachmentsRenderingViewModel, Observab
textPresent || attachmentPresent textPresent || attachmentPresent
} }
.assign(to: &$isPostable) .assign(to: &$isPostable)
$attachmentViewModels $attachmentViewModels
.combineLatest($attachmentUpload, $displayPoll) .combineLatest($attachmentUpload, $displayPoll)
.map { $0.count < Self.maxAttachmentCount && $1 == nil && !$2 } .map { $0.count < Self.maxAttachmentCount && $1 == nil && !$2 }
.assign(to: &$canAddAttachment) .assign(to: &$canAddAttachment)
$attachmentViewModels.map(\.isEmpty).assign(to: &$canAddNonImageAttachment) $attachmentViewModels.map(\.isEmpty).assign(to: &$canAddNonImageAttachment)
$text.map { $text.map {
let tokens = $0.components(separatedBy: " ") let tokens = $0.components(separatedBy: " ")
@ -51,7 +58,18 @@ public final class CompositionViewModel: AttachmentsRenderingViewModel, Observab
.combineLatest($displayContentWarning, $contentWarning) .combineLatest($displayContentWarning, $contentWarning)
.map { Self.maxCharacters - ($0 + ($1 ? $2.count : 0)) } .map { Self.maxCharacters - ($0 + ($1 ? $2.count : 0)) }
.assign(to: &$remainingCharacters) .assign(to: &$remainingCharacters)
$displayContentWarning.filter { $0 }.assign(to: &$sensitive) $displayContentWarning.filter { $0 }.assign(to: &$sensitive)
$textToSelectedRange
.map { Self.extractAutocompleteQuery(textToSelectedRange: $0, emojiOnly: false) }
.removeDuplicates()
.assign(to: &$autocompleteQuery)
$contentWarningTextToSelectedRange
.map { Self.extractAutocompleteQuery(textToSelectedRange: $0, emojiOnly: true) }
.removeDuplicates()
.assign(to: &$contentWarningAutocompleteQuery)
} }
public func attachmentSelected(viewModel: AttachmentViewModel) { public func attachmentSelected(viewModel: AttachmentViewModel) {
@ -86,11 +104,17 @@ public extension CompositionViewModel {
class PollOption: ObservableObject { class PollOption: ObservableObject {
public let id = Id() public let id = Id()
@Published public var text: String @Published public var text: String
@Published public var textToSelectedRange = ""
@Published public private(set) var remainingCharacters = CompositionViewModel.maxCharacters @Published public private(set) var remainingCharacters = CompositionViewModel.maxCharacters
@Published public private(set) var autocompleteQuery: String?
public init(text: String) { public init(text: String) {
self.text = text self.text = text
$text.map { Self.maxCharacters - $0.count }.assign(to: &$remainingCharacters) $text.map { Self.maxCharacters - $0.count }.assign(to: &$remainingCharacters)
$textToSelectedRange
.map { CompositionViewModel.extractAutocompleteQuery(textToSelectedRange: $0, emojiOnly: true) }
.removeDuplicates()
.assign(to: &$autocompleteQuery)
} }
} }
@ -234,6 +258,18 @@ public extension CompositionViewModel.PollOption {
private extension CompositionViewModel { private extension CompositionViewModel {
static let maxAttachmentCount = 4 static let maxAttachmentCount = 4
static let autocompleteQueryRegularExpression = #"([@#:]\S+)\z"#
static let emojiOnlyAutocompleteQueryRegularExpression = #"(:\S+)\z"#
static func extractAutocompleteQuery(textToSelectedRange: String, emojiOnly: Bool) -> String? {
guard let range = textToSelectedRange.range(
of: emojiOnly ? emojiOnlyAutocompleteQueryRegularExpression: autocompleteQueryRegularExpression,
options: .regularExpression,
locale: .current)
else { return nil }
return String(textToSelectedRange[range])
}
} }
private extension String { private extension String {

View file

@ -11,11 +11,15 @@ final class CompositionInputAccessoryView: UIToolbar {
private let viewModel: CompositionViewModel private let viewModel: CompositionViewModel
private let parentViewModel: NewStatusViewModel private let parentViewModel: NewStatusViewModel
private let autocompleteQueryPublisher: AnyPublisher<String?, Never>
private var cancellables = Set<AnyCancellable>() private var cancellables = Set<AnyCancellable>()
init(viewModel: CompositionViewModel, parentViewModel: NewStatusViewModel) { init(viewModel: CompositionViewModel,
parentViewModel: NewStatusViewModel,
autocompleteQueryPublisher: AnyPublisher<String?, Never>) {
self.viewModel = viewModel self.viewModel = viewModel
self.parentViewModel = parentViewModel self.parentViewModel = parentViewModel
self.autocompleteQueryPublisher = autocompleteQueryPublisher
super.init( super.init(
frame: .init( frame: .init(
@ -170,6 +174,11 @@ private extension CompositionInputAccessoryView {
.sink { addButton.isEnabled = $0 } .sink { addButton.isEnabled = $0 }
.store(in: &cancellables) .store(in: &cancellables)
autocompleteQueryPublisher
.print()
.sink { _ in /* TODO */ }
.store(in: &cancellables)
parentViewModel.$visibility parentViewModel.$visibility
.sink { [weak self] in .sink { [weak self] in
visibilityButton.image = UIImage(systemName: $0.systemImageName) visibilityButton.image = UIImage(systemName: $0.systemImageName)

View file

@ -46,12 +46,20 @@ private extension CompositionPollOptionView {
textField.font = .preferredFont(forTextStyle: .body) textField.font = .preferredFont(forTextStyle: .body)
let textInputAccessoryView = CompositionInputAccessoryView( let textInputAccessoryView = CompositionInputAccessoryView(
viewModel: viewModel, viewModel: viewModel,
parentViewModel: parentViewModel) parentViewModel: parentViewModel,
autocompleteQueryPublisher: option.$autocompleteQuery.eraseToAnyPublisher())
textField.inputAccessoryView = textInputAccessoryView textField.inputAccessoryView = textInputAccessoryView
textField.tag = textInputAccessoryView.tagForInputView textField.tag = textInputAccessoryView.tagForInputView
textField.addAction( textField.addAction(
UIAction { [weak self] _ in UIAction { [weak self] _ in
self?.option.text = self?.textField.text ?? "" }, guard let self = self, let text = self.textField.text else { return }
self.option.text = text
if let textToSelectedRange = self.textField.textToSelectedRange {
self.option.textToSelectedRange = textToSelectedRange
}
},
for: .editingChanged) for: .editingChanged)
textField.text = option.text textField.text = option.text

View file

@ -49,6 +49,10 @@ extension CompositionView {
extension CompositionView: UITextViewDelegate { extension CompositionView: UITextViewDelegate {
func textViewDidChange(_ textView: UITextView) { func textViewDidChange(_ textView: UITextView) {
viewModel.text = textView.text viewModel.text = textView.text
if let textToSelectedRange = textView.textToSelectedRange {
viewModel.textToSelectedRange = textToSelectedRange
}
} }
} }
@ -81,7 +85,8 @@ private extension CompositionView {
let spoilerTextinputAccessoryView = CompositionInputAccessoryView( let spoilerTextinputAccessoryView = CompositionInputAccessoryView(
viewModel: viewModel, viewModel: viewModel,
parentViewModel: parentViewModel) parentViewModel: parentViewModel,
autocompleteQueryPublisher: viewModel.$contentWarningAutocompleteQuery.eraseToAnyPublisher())
stackView.addArrangedSubview(spoilerTextField) stackView.addArrangedSubview(spoilerTextField)
spoilerTextField.borderStyle = .roundedRect spoilerTextField.borderStyle = .roundedRect
@ -95,13 +100,18 @@ private extension CompositionView {
guard let self = self, let text = self.spoilerTextField.text else { return } guard let self = self, let text = self.spoilerTextField.text else { return }
self.viewModel.contentWarning = text self.viewModel.contentWarning = text
if let textToSelectedRange = self.spoilerTextField.textToSelectedRange {
self.viewModel.contentWarningTextToSelectedRange = textToSelectedRange
}
}, },
for: .editingChanged) for: .editingChanged)
let textViewFont = UIFont.preferredFont(forTextStyle: .body) let textViewFont = UIFont.preferredFont(forTextStyle: .body)
let textInputAccessoryView = CompositionInputAccessoryView( let textInputAccessoryView = CompositionInputAccessoryView(
viewModel: viewModel, viewModel: viewModel,
parentViewModel: parentViewModel) parentViewModel: parentViewModel,
autocompleteQueryPublisher: viewModel.$autocompleteQuery.eraseToAnyPublisher())
stackView.addArrangedSubview(textView) stackView.addArrangedSubview(textView)
textView.keyboardType = .twitter textView.keyboardType = .twitter