diff --git a/Tusker.xcodeproj/project.pbxproj b/Tusker.xcodeproj/project.pbxproj index da451a96..d2fb8065 100644 --- a/Tusker.xcodeproj/project.pbxproj +++ b/Tusker.xcodeproj/project.pbxproj @@ -238,6 +238,7 @@ D6BC9DD7232D7811002CA326 /* TimelinesPageViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6BC9DD6232D7811002CA326 /* TimelinesPageViewController.swift */; }; D6BC9DDA232D8BE5002CA326 /* SearchResultsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6BC9DD9232D8BE5002CA326 /* SearchResultsViewController.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 */; }; D6C693FC2162FE6F007D6A6D /* LoadingViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6C693FB2162FE6F007D6A6D /* LoadingViewController.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 = ""; }; D6BC9DD9232D8BE5002CA326 /* SearchResultsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchResultsViewController.swift; sourceTree = ""; }; D6BED173212667E900F02DA0 /* TimelineStatusTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineStatusTableViewCell.swift; sourceTree = ""; }; + D6C143D9253510F4007DC240 /* ComposeContentWarningTextField.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeContentWarningTextField.swift; sourceTree = ""; }; D6C693EE216192C2007D6A6D /* TuskerNavigationDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TuskerNavigationDelegate.swift; sourceTree = ""; }; D6C693FB2162FE6F007D6A6D /* LoadingViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoadingViewController.swift; sourceTree = ""; }; D6C693FD2162FEEA007D6A6D /* UIViewController+Children.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIViewController+Children.swift"; sourceTree = ""; }; @@ -1004,6 +1006,7 @@ D62275A524F1C81800B82A16 /* ComposeReplyView.swift */, D62275A724F1CA2800B82A16 /* ComposeReplyContentView.swift */, D6E4267625327FB400C02E1C /* ComposeAutocompleteView.swift */, + D6C143D9253510F4007DC240 /* ComposeContentWarningTextField.swift */, ); path = Compose; sourceTree = ""; @@ -1860,6 +1863,7 @@ D622757424EDF1CD00B82A16 /* ComposeAttachmentsList.swift in Sources */, D6E0DC8E216EDF1E00369478 /* Previewing.swift in Sources */, D6BED174212667E900F02DA0 /* TimelineStatusTableViewCell.swift in Sources */, + D6C143DA253510F4007DC240 /* ComposeContentWarningTextField.swift in Sources */, 0427033822B30F5F000D31B6 /* BehaviorPrefsView.swift in Sources */, D627943923A553B600D38C68 /* UnbookmarkStatusActivity.swift in Sources */, D64D0AAD2128D88B005A6F37 /* LocalData.swift in Sources */, diff --git a/Tusker/Screens/Compose/ComposeContentWarningTextField.swift b/Tusker/Screens/Compose/ComposeContentWarningTextField.swift new file mode 100644 index 00000000..6148e3e3 --- /dev/null +++ b/Tusker/Screens/Compose/ComposeContentWarningTextField.swift @@ -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! + 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.. 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.. 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 + } + } + +} diff --git a/Tusker/Screens/Compose/ComposeView.swift b/Tusker/Screens/Compose/ComposeView.swift index 07bbf63d..ad73c6ef 100644 --- a/Tusker/Screens/Compose/ComposeView.swift +++ b/Tusker/Screens/Compose/ComposeView.swift @@ -114,8 +114,7 @@ struct ComposeView: View { header if draft.contentWarningEnabled { - TextField("Write your warning here", text: $draft.contentWarning) - .textFieldStyle(RoundedBorderTextFieldStyle()) + ComposeContentWarningTextField(text: $draft.contentWarning) } MainComposeTextView( diff --git a/Tusker/Screens/Compose/MainComposeTextView.swift b/Tusker/Screens/Compose/MainComposeTextView.swift index 917a88ec..a893455f 100644 --- a/Tusker/Screens/Compose/MainComposeTextView.swift +++ b/Tusker/Screens/Compose/MainComposeTextView.swift @@ -218,11 +218,13 @@ struct MainComposeWrappedTextView: UIViewRepresentable { uiState.delegate?.keyboardDidHide(accessoryView: textView!.inputAccessoryView!, notification: notification) } - func textViewDidEndEditing(_ textView: UITextView) { + func textViewDidBeginEditing(_ textView: UITextView) { + uiState.autocompleteHandler = self updateAutocompleteState() } - func textViewDidBeginEditing(_ textView: UITextView) { + func textViewDidEndEditing(_ textView: UITextView) { + uiState.autocompleteHandler = nil updateAutocompleteState() } @@ -312,7 +314,7 @@ struct MainComposeWrappedTextView: UIViewRepresentable { var lastWordStartIndex = text.index(before: characterBeforeCursorIndex) var foundFirstAtSign = false while true { - let c = textView.text[lastWordStartIndex] + let c = text[lastWordStartIndex] if !isPermittedForAutocomplete(c) { if foundFirstAtSign {