Autocomplete custom emojis in CW field

This commit is contained in:
Shadowfacts 2020-10-12 19:17:57 -04:00
parent 1a4517c43a
commit ae272582ac
Signed by untrusted user: shadowfacts
GPG Key ID: 94A5AB95422746E5
4 changed files with 158 additions and 5 deletions

View File

@ -238,6 +238,7 @@
D6BC9DD7232D7811002CA326 /* TimelinesPageViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6BC9DD6232D7811002CA326 /* TimelinesPageViewController.swift */; }; D6BC9DD7232D7811002CA326 /* TimelinesPageViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6BC9DD6232D7811002CA326 /* TimelinesPageViewController.swift */; };
D6BC9DDA232D8BE5002CA326 /* SearchResultsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6BC9DD9232D8BE5002CA326 /* SearchResultsViewController.swift */; }; D6BC9DDA232D8BE5002CA326 /* SearchResultsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6BC9DD9232D8BE5002CA326 /* SearchResultsViewController.swift */; };
D6BED174212667E900F02DA0 /* TimelineStatusTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6BED173212667E900F02DA0 /* TimelineStatusTableViewCell.swift */; }; D6BED174212667E900F02DA0 /* TimelineStatusTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6BED173212667E900F02DA0 /* TimelineStatusTableViewCell.swift */; };
D6C143DA253510F4007DC240 /* ComposeContentWarningTextField.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6C143D9253510F4007DC240 /* ComposeContentWarningTextField.swift */; };
D6C693EF216192C2007D6A6D /* TuskerNavigationDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6C693EE216192C2007D6A6D /* TuskerNavigationDelegate.swift */; }; D6C693EF216192C2007D6A6D /* TuskerNavigationDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6C693EE216192C2007D6A6D /* TuskerNavigationDelegate.swift */; };
D6C693FC2162FE6F007D6A6D /* LoadingViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6C693FB2162FE6F007D6A6D /* LoadingViewController.swift */; }; D6C693FC2162FE6F007D6A6D /* LoadingViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6C693FB2162FE6F007D6A6D /* LoadingViewController.swift */; };
D6C693FE2162FEEA007D6A6D /* UIViewController+Children.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6C693FD2162FEEA007D6A6D /* UIViewController+Children.swift */; }; D6C693FE2162FEEA007D6A6D /* UIViewController+Children.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6C693FD2162FEEA007D6A6D /* UIViewController+Children.swift */; };
@ -567,6 +568,7 @@
D6BC9DD6232D7811002CA326 /* TimelinesPageViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelinesPageViewController.swift; sourceTree = "<group>"; }; D6BC9DD6232D7811002CA326 /* TimelinesPageViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelinesPageViewController.swift; sourceTree = "<group>"; };
D6BC9DD9232D8BE5002CA326 /* SearchResultsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchResultsViewController.swift; sourceTree = "<group>"; }; D6BC9DD9232D8BE5002CA326 /* SearchResultsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchResultsViewController.swift; sourceTree = "<group>"; };
D6BED173212667E900F02DA0 /* TimelineStatusTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineStatusTableViewCell.swift; sourceTree = "<group>"; }; D6BED173212667E900F02DA0 /* TimelineStatusTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineStatusTableViewCell.swift; sourceTree = "<group>"; };
D6C143D9253510F4007DC240 /* ComposeContentWarningTextField.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeContentWarningTextField.swift; sourceTree = "<group>"; };
D6C693EE216192C2007D6A6D /* TuskerNavigationDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TuskerNavigationDelegate.swift; sourceTree = "<group>"; }; D6C693EE216192C2007D6A6D /* TuskerNavigationDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TuskerNavigationDelegate.swift; sourceTree = "<group>"; };
D6C693FB2162FE6F007D6A6D /* LoadingViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoadingViewController.swift; sourceTree = "<group>"; }; D6C693FB2162FE6F007D6A6D /* LoadingViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoadingViewController.swift; sourceTree = "<group>"; };
D6C693FD2162FEEA007D6A6D /* UIViewController+Children.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIViewController+Children.swift"; sourceTree = "<group>"; }; D6C693FD2162FEEA007D6A6D /* UIViewController+Children.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIViewController+Children.swift"; sourceTree = "<group>"; };
@ -1004,6 +1006,7 @@
D62275A524F1C81800B82A16 /* ComposeReplyView.swift */, D62275A524F1C81800B82A16 /* ComposeReplyView.swift */,
D62275A724F1CA2800B82A16 /* ComposeReplyContentView.swift */, D62275A724F1CA2800B82A16 /* ComposeReplyContentView.swift */,
D6E4267625327FB400C02E1C /* ComposeAutocompleteView.swift */, D6E4267625327FB400C02E1C /* ComposeAutocompleteView.swift */,
D6C143D9253510F4007DC240 /* ComposeContentWarningTextField.swift */,
); );
path = Compose; path = Compose;
sourceTree = "<group>"; sourceTree = "<group>";
@ -1860,6 +1863,7 @@
D622757424EDF1CD00B82A16 /* ComposeAttachmentsList.swift in Sources */, D622757424EDF1CD00B82A16 /* ComposeAttachmentsList.swift in Sources */,
D6E0DC8E216EDF1E00369478 /* Previewing.swift in Sources */, D6E0DC8E216EDF1E00369478 /* Previewing.swift in Sources */,
D6BED174212667E900F02DA0 /* TimelineStatusTableViewCell.swift in Sources */, D6BED174212667E900F02DA0 /* TimelineStatusTableViewCell.swift in Sources */,
D6C143DA253510F4007DC240 /* ComposeContentWarningTextField.swift in Sources */,
0427033822B30F5F000D31B6 /* BehaviorPrefsView.swift in Sources */, 0427033822B30F5F000D31B6 /* BehaviorPrefsView.swift in Sources */,
D627943923A553B600D38C68 /* UnbookmarkStatusActivity.swift in Sources */, D627943923A553B600D38C68 /* UnbookmarkStatusActivity.swift in Sources */,
D64D0AAD2128D88B005A6F37 /* LocalData.swift in Sources */, D64D0AAD2128D88B005A6F37 /* LocalData.swift in Sources */,

View File

@ -0,0 +1,148 @@
//
// ComposeContentWarningTextField.swift
// Tusker
//
// Created by Shadowfacts on 10/12/20.
// Copyright © 2020 Shadowfacts. All rights reserved.
//
import SwiftUI
struct ComposeContentWarningTextField: UIViewRepresentable {
typealias UIViewType = UITextField
@Binding var text: String
@EnvironmentObject private var uiState: ComposeUIState
func makeUIView(context: Context) -> UITextField {
let view = UITextField()
view.placeholder = "Write your warning here"
view.borderStyle = .roundedRect
view.delegate = context.coordinator
view.addTarget(context.coordinator, action: #selector(Coordinator.didChange(_:)), for: .editingChanged)
context.coordinator.textField = view
context.coordinator.uiState = uiState
context.coordinator.text = $text
return view
}
func updateUIView(_ uiView: UITextField, context: Context) {
uiView.text = text
}
func makeCoordinator() -> Coordinator {
return Coordinator()
}
class Coordinator: NSObject, UITextFieldDelegate, ComposeAutocompleteHandler {
weak var textField: UITextField?
var text: Binding<String>!
var uiState: ComposeUIState!
@objc func didChange(_ textField: UITextField) {
text.wrappedValue = textField.text ?? ""
}
func textFieldDidBeginEditing(_ textField: UITextField) {
uiState.autocompleteHandler = self
updateAutocompleteState(textField: textField)
}
func textFieldDidEndEditing(_ textField: UITextField) {
uiState.autocompleteHandler = nil
updateAutocompleteState(textField: textField)
}
func textFieldDidChangeSelection(_ textField: UITextField) {
updateAutocompleteState(textField: textField)
}
func autocomplete(with string: String) {
guard let textField = textField,
let text = textField.text,
let selectedRange = textField.selectedTextRange,
let lastWordStartIndex = findAutocompleteLastWord(textField: textField) else {
return
}
let selectedRangeStartUTF16 = textField.offset(from: textField.beginningOfDocument, to: selectedRange.start)
let characterBeforeCursorIndex = text.utf16.index(text.startIndex, offsetBy: selectedRangeStartUTF16)
textField.text!.replaceSubrange(lastWordStartIndex..<characterBeforeCursorIndex, with: string)
didChange(textField)
}
private func updateAutocompleteState(textField: UITextField) {
guard let selectedRange = textField.selectedTextRange,
let text = textField.text,
let lastWordStartIndex = findAutocompleteLastWord(textField: textField) else {
uiState.autocompleteState = nil
return
}
if lastWordStartIndex > text.startIndex {
let c = text[text.index(before: lastWordStartIndex)]
if isPermittedForAutocomplete(c) || c == ":" {
uiState.autocompleteState = nil
return
}
}
let selectedRangeStartUTF16 = textField.offset(from: textField.beginningOfDocument, to: selectedRange.start)
let cursorIndex = text.utf16.index(text.startIndex, offsetBy: selectedRangeStartUTF16)
if lastWordStartIndex >= text.startIndex {
let lastWord = text[lastWordStartIndex..<cursorIndex]
let exceptFirst = lastWord[lastWord.index(after: lastWord.startIndex)...]
if lastWord.first == ":" {
uiState.autocompleteState = .emoji(String(exceptFirst))
} else {
uiState.autocompleteState = nil
}
} else {
uiState.autocompleteState = nil
}
}
private func isPermittedForAutocomplete(_ c: Character) -> Bool {
return (c >= "a" && c <= "z") || (c >= "A" && c <= "Z") || (c >= "0" && c <= "9") || c == "_"
}
private func findAutocompleteLastWord(textField: UITextField) -> String.Index? {
guard textField.isFirstResponder,
let selectedRange = textField.selectedTextRange,
selectedRange.isEmpty,
let text = textField.text,
!text.isEmpty else {
return nil
}
let selectedRangeStartUTF16 = textField.offset(from: textField.beginningOfDocument, to: selectedRange.start)
let cursorIndex = text.utf16.index(text.startIndex, offsetBy: selectedRangeStartUTF16)
var lastWordStartIndex = text.index(before: cursorIndex)
while true {
let c = text[lastWordStartIndex]
if !isPermittedForAutocomplete(c) {
break
}
if lastWordStartIndex > text.startIndex {
lastWordStartIndex = text.index(before: lastWordStartIndex)
} else {
break
}
}
return lastWordStartIndex
}
}
}

View File

@ -114,8 +114,7 @@ struct ComposeView: View {
header header
if draft.contentWarningEnabled { if draft.contentWarningEnabled {
TextField("Write your warning here", text: $draft.contentWarning) ComposeContentWarningTextField(text: $draft.contentWarning)
.textFieldStyle(RoundedBorderTextFieldStyle())
} }
MainComposeTextView( MainComposeTextView(

View File

@ -218,11 +218,13 @@ struct MainComposeWrappedTextView: UIViewRepresentable {
uiState.delegate?.keyboardDidHide(accessoryView: textView!.inputAccessoryView!, notification: notification) uiState.delegate?.keyboardDidHide(accessoryView: textView!.inputAccessoryView!, notification: notification)
} }
func textViewDidEndEditing(_ textView: UITextView) { func textViewDidBeginEditing(_ textView: UITextView) {
uiState.autocompleteHandler = self
updateAutocompleteState() updateAutocompleteState()
} }
func textViewDidBeginEditing(_ textView: UITextView) { func textViewDidEndEditing(_ textView: UITextView) {
uiState.autocompleteHandler = nil
updateAutocompleteState() updateAutocompleteState()
} }
@ -312,7 +314,7 @@ struct MainComposeWrappedTextView: UIViewRepresentable {
var lastWordStartIndex = text.index(before: characterBeforeCursorIndex) var lastWordStartIndex = text.index(before: characterBeforeCursorIndex)
var foundFirstAtSign = false var foundFirstAtSign = false
while true { while true {
let c = textView.text[lastWordStartIndex] let c = text[lastWordStartIndex]
if !isPermittedForAutocomplete(c) { if !isPermittedForAutocomplete(c) {
if foundFirstAtSign { if foundFirstAtSign {