forked from shadowfacts/Tusker
Unify compose screen input accessory toolbars
This commit is contained in:
parent
a718721537
commit
abf6ff8115
|
@ -60,7 +60,7 @@ struct ComposeAutocompleteMentionsView: View {
|
||||||
HStack(spacing: 8) {
|
HStack(spacing: 8) {
|
||||||
ForEach(accounts, id: \.id) { (account) in
|
ForEach(accounts, id: \.id) { (account) in
|
||||||
Button {
|
Button {
|
||||||
uiState.autocompleteHandler?.autocomplete(with: "@\(account.acct)")
|
uiState.currentInput?.autocomplete(with: "@\(account.acct)")
|
||||||
} label: {
|
} label: {
|
||||||
HStack(spacing: 4) {
|
HStack(spacing: 4) {
|
||||||
ComposeAvatarImageView(url: account.avatar)
|
ComposeAvatarImageView(url: account.avatar)
|
||||||
|
@ -242,7 +242,7 @@ struct ComposeAutocompleteEmojisView: View {
|
||||||
HStack(spacing: 8) {
|
HStack(spacing: 8) {
|
||||||
ForEach(emojis, id: \.shortcode) { (emoji) in
|
ForEach(emojis, id: \.shortcode) { (emoji) in
|
||||||
Button {
|
Button {
|
||||||
uiState.autocompleteHandler?.autocomplete(with: ":\(emoji.shortcode):")
|
uiState.currentInput?.autocomplete(with: ":\(emoji.shortcode):")
|
||||||
} label: {
|
} label: {
|
||||||
HStack(spacing: 4) {
|
HStack(spacing: 4) {
|
||||||
CustomEmojiImageView(emoji: emoji)
|
CustomEmojiImageView(emoji: emoji)
|
||||||
|
@ -306,7 +306,7 @@ struct ComposeAutocompleteHashtagsView: View {
|
||||||
HStack(spacing: 8) {
|
HStack(spacing: 8) {
|
||||||
ForEach(hashtags, id: \.name) { (hashtag) in
|
ForEach(hashtags, id: \.name) { (hashtag) in
|
||||||
Button {
|
Button {
|
||||||
uiState.autocompleteHandler?.autocomplete(with: "#\(hashtag.name)")
|
uiState.currentInput?.autocomplete(with: "#\(hashtag.name)")
|
||||||
} label: {
|
} label: {
|
||||||
Text(verbatim: "#\(hashtag.name)")
|
Text(verbatim: "#\(hashtag.name)")
|
||||||
.foregroundColor(Color(UIColor.label))
|
.foregroundColor(Color(UIColor.label))
|
||||||
|
|
|
@ -73,22 +73,27 @@ struct ComposeEmojiTextField: UIViewRepresentable {
|
||||||
return Coordinator()
|
return Coordinator()
|
||||||
}
|
}
|
||||||
|
|
||||||
class Coordinator: NSObject, UITextFieldDelegate, ComposeAutocompleteHandler {
|
class Coordinator: NSObject, UITextFieldDelegate, ComposeInput {
|
||||||
weak var textField: UITextField?
|
weak var textField: UITextField?
|
||||||
var text: Binding<String>!
|
var text: Binding<String>!
|
||||||
var uiState: ComposeUIState!
|
// break retained cycle through ComposeUIState.currentInput
|
||||||
|
unowned var uiState: ComposeUIState!
|
||||||
var didChange: ((String) -> Void)?
|
var didChange: ((String) -> Void)?
|
||||||
var didEndEditing: (() -> Void)?
|
var didEndEditing: (() -> Void)?
|
||||||
|
|
||||||
var skipSettingTextOnNextUpdate = false
|
var skipSettingTextOnNextUpdate = false
|
||||||
|
|
||||||
|
var toolbarElements: [ComposeUIState.ToolbarElement] {
|
||||||
|
[]
|
||||||
|
}
|
||||||
|
|
||||||
@objc func didChange(_ textField: UITextField) {
|
@objc func didChange(_ textField: UITextField) {
|
||||||
text.wrappedValue = textField.text ?? ""
|
text.wrappedValue = textField.text ?? ""
|
||||||
didChange?(text.wrappedValue)
|
didChange?(text.wrappedValue)
|
||||||
}
|
}
|
||||||
|
|
||||||
func textFieldDidBeginEditing(_ textField: UITextField) {
|
func textFieldDidBeginEditing(_ textField: UITextField) {
|
||||||
uiState.autocompleteHandler = self
|
uiState.currentInput = self
|
||||||
updateAutocompleteState(textField: textField)
|
updateAutocompleteState(textField: textField)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -103,6 +108,9 @@ struct ComposeEmojiTextField: UIViewRepresentable {
|
||||||
self.updateAutocompleteState(textField: textField)
|
self.updateAutocompleteState(textField: textField)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func applyFormat(_ format: StatusFormat) {
|
||||||
|
}
|
||||||
|
|
||||||
func autocomplete(with string: String) {
|
func autocomplete(with string: String) {
|
||||||
guard let textField = textField,
|
guard let textField = textField,
|
||||||
let text = textField.text,
|
let text = textField.text,
|
||||||
|
|
|
@ -15,6 +15,9 @@ protocol ComposeHostingControllerDelegate: AnyObject {
|
||||||
func dismissCompose(mode: ComposeUIState.DismissMode) -> Bool
|
func dismissCompose(mode: ComposeUIState.DismissMode) -> Bool
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private let VISIBILITY_BAR_BUTTON_TAG = 42001
|
||||||
|
private let LOCAL_ONLY_BAR_BUTTON_TAG = 42002
|
||||||
|
|
||||||
class ComposeHostingController: UIHostingController<ComposeContainerView> {
|
class ComposeHostingController: UIHostingController<ComposeContainerView> {
|
||||||
|
|
||||||
weak var delegate: ComposeHostingControllerDelegate?
|
weak var delegate: ComposeHostingControllerDelegate?
|
||||||
|
@ -31,8 +34,6 @@ class ComposeHostingController: UIHostingController<ComposeContainerView> {
|
||||||
|
|
||||||
private var mainToolbar: UIToolbar!
|
private var mainToolbar: UIToolbar!
|
||||||
private var inputAccessoryToolbar: UIToolbar!
|
private var inputAccessoryToolbar: UIToolbar!
|
||||||
private var visibilityBarButtonItems = [UIBarButtonItem]()
|
|
||||||
private var localOnlyItems = [UIBarButtonItem]()
|
|
||||||
|
|
||||||
override var inputAccessoryView: UIView? { inputAccessoryToolbar }
|
override var inputAccessoryView: UIView? { inputAccessoryToolbar }
|
||||||
|
|
||||||
|
@ -56,8 +57,14 @@ class ComposeHostingController: UIHostingController<ComposeContainerView> {
|
||||||
|
|
||||||
// main toolbar is shown at the bottom of the screen, the input accessory is attached to the keyboard while editing
|
// main toolbar is shown at the bottom of the screen, the input accessory is attached to the keyboard while editing
|
||||||
// (except for MainComposeTextView which has its own accessory to add formatting buttons)
|
// (except for MainComposeTextView which has its own accessory to add formatting buttons)
|
||||||
mainToolbar = createToolbar()
|
mainToolbar = UIToolbar()
|
||||||
inputAccessoryToolbar = createToolbar()
|
mainToolbar.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
mainToolbar.isAccessibilityElement = true
|
||||||
|
setupToolbarItems(toolbar: mainToolbar, input: nil)
|
||||||
|
inputAccessoryToolbar = UIToolbar()
|
||||||
|
inputAccessoryToolbar.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
inputAccessoryToolbar.isAccessibilityElement = true
|
||||||
|
setupToolbarItems(toolbar: inputAccessoryToolbar, input: nil)
|
||||||
|
|
||||||
NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillShow(_:)), name: UIResponder.keyboardWillShowNotification, object: nil)
|
NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillShow(_:)), name: UIResponder.keyboardWillShowNotification, object: nil)
|
||||||
NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillHide(_:)), name: UIResponder.keyboardWillHideNotification, object: nil)
|
NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillHide(_:)), name: UIResponder.keyboardWillHideNotification, object: nil)
|
||||||
|
@ -87,6 +94,12 @@ class ComposeHostingController: UIHostingController<ComposeContainerView> {
|
||||||
DraftsManager.save()
|
DraftsManager.save()
|
||||||
}
|
}
|
||||||
.store(in: &cancellables)
|
.store(in: &cancellables)
|
||||||
|
|
||||||
|
self.uiState.$currentInput
|
||||||
|
.sink { [unowned self] in
|
||||||
|
self.setupToolbarItems(toolbar: self.inputAccessoryToolbar, input: $0)
|
||||||
|
}
|
||||||
|
.store(in: &cancellables)
|
||||||
}
|
}
|
||||||
|
|
||||||
required init?(coder aDecoder: NSCoder) {
|
required init?(coder aDecoder: NSCoder) {
|
||||||
|
@ -116,30 +129,57 @@ class ComposeHostingController: UIHostingController<ComposeContainerView> {
|
||||||
DraftsManager.save()
|
DraftsManager.save()
|
||||||
}
|
}
|
||||||
|
|
||||||
private func createToolbar() -> UIToolbar {
|
private func setupToolbarItems(toolbar: UIToolbar, input: ComposeInput?) {
|
||||||
let toolbar = UIToolbar()
|
var items: [UIBarButtonItem] = []
|
||||||
toolbar.translatesAutoresizingMaskIntoConstraints = false
|
|
||||||
toolbar.isAccessibilityElement = true
|
items.append(UIBarButtonItem(title: "CW", style: .plain, target: self, action: #selector(cwButtonPressed)))
|
||||||
|
|
||||||
let visibilityItem = UIBarButtonItem(image: nil, style: .plain, target: nil, action: nil)
|
let visibilityItem = UIBarButtonItem(image: nil, style: .plain, target: nil, action: nil)
|
||||||
visibilityBarButtonItems.append(visibilityItem)
|
visibilityItem.tag = VISIBILITY_BAR_BUTTON_TAG
|
||||||
visibilityChanged(draft.visibility)
|
items.append(visibilityItem)
|
||||||
|
|
||||||
toolbar.items = [
|
|
||||||
UIBarButtonItem(title: "CW", style: .plain, target: self, action: #selector(cwButtonPressed)),
|
|
||||||
visibilityItem,
|
|
||||||
UIBarButtonItem(barButtonSystemItem: .flexibleSpace, target: nil, action: nil),
|
|
||||||
UIBarButtonItem(title: "Drafts", style: .plain, target: self, action: #selector(draftsButtonPresed))
|
|
||||||
]
|
|
||||||
|
|
||||||
if mastodonController.instanceFeatures.localOnlyPosts {
|
if mastodonController.instanceFeatures.localOnlyPosts {
|
||||||
let item = UIBarButtonItem(image: nil, style: .plain, target: nil, action: nil)
|
let item = UIBarButtonItem(image: nil, style: .plain, target: nil, action: nil)
|
||||||
toolbar.items!.insert(item, at: 2)
|
item.tag = LOCAL_ONLY_BAR_BUTTON_TAG
|
||||||
localOnlyItems.append(item)
|
items.append(item)
|
||||||
localOnlyChanged(draft.localOnly)
|
localOnlyChanged(draft.localOnly)
|
||||||
}
|
}
|
||||||
|
|
||||||
return toolbar
|
items.append(UIBarButtonItem(systemItem: .flexibleSpace))
|
||||||
|
|
||||||
|
if input?.toolbarElements.contains(.formattingButtons) == true,
|
||||||
|
Preferences.shared.statusContentType != .plain {
|
||||||
|
|
||||||
|
for (idx, format) in StatusFormat.allCases.enumerated() {
|
||||||
|
let item: UIBarButtonItem
|
||||||
|
if let image = format.image {
|
||||||
|
item = UIBarButtonItem(image: image, style: .plain, target: self, action: #selector(formatButtonPressed(_:)))
|
||||||
|
} else if let (str, attributes) = format.title {
|
||||||
|
item = UIBarButtonItem(title: str, style: .plain, target: self, action: #selector(formatButtonPressed(_:)))
|
||||||
|
item.setTitleTextAttributes(attributes, for: .normal)
|
||||||
|
item.setTitleTextAttributes(attributes, for: .highlighted)
|
||||||
|
} else {
|
||||||
|
fatalError("StatusFormat must have either image or title")
|
||||||
|
}
|
||||||
|
item.tag = StatusFormat.allCases.firstIndex(of: format)!
|
||||||
|
item.accessibilityLabel = format.accessibilityLabel
|
||||||
|
|
||||||
|
items.append(item)
|
||||||
|
if idx != StatusFormat.allCases.count - 1 {
|
||||||
|
let spacer = UIBarButtonItem(systemItem: .fixedSpace)
|
||||||
|
spacer.width = 8
|
||||||
|
items.append(spacer)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
items.append(UIBarButtonItem(systemItem: .flexibleSpace))
|
||||||
|
}
|
||||||
|
|
||||||
|
items.append(UIBarButtonItem(title: "Drafts", style: .plain, target: self, action: #selector(draftsButtonPresed)))
|
||||||
|
|
||||||
|
toolbar.items = items
|
||||||
|
visibilityChanged(draft.visibility)
|
||||||
|
localOnlyChanged(draft.localOnly)
|
||||||
}
|
}
|
||||||
|
|
||||||
private func updateAdditionalSafeAreaInsets() {
|
private func updateAdditionalSafeAreaInsets() {
|
||||||
|
@ -198,7 +238,10 @@ class ComposeHostingController: UIHostingController<ComposeContainerView> {
|
||||||
}
|
}
|
||||||
|
|
||||||
private func visibilityChanged(_ newVisibility: Status.Visibility) {
|
private func visibilityChanged(_ newVisibility: Status.Visibility) {
|
||||||
for item in visibilityBarButtonItems {
|
for toolbar in [mainToolbar, inputAccessoryToolbar] {
|
||||||
|
guard let item = toolbar?.items?.first(where: { $0.tag == VISIBILITY_BAR_BUTTON_TAG }) else {
|
||||||
|
continue
|
||||||
|
}
|
||||||
item.image = UIImage(systemName: newVisibility.imageName)
|
item.image = UIImage(systemName: newVisibility.imageName)
|
||||||
item.accessibilityLabel = String(format: NSLocalizedString("Visibility: %@", comment: "compose visiblity accessibility label"), draft.visibility.displayName)
|
item.accessibilityLabel = String(format: NSLocalizedString("Visibility: %@", comment: "compose visiblity accessibility label"), draft.visibility.displayName)
|
||||||
let elements = Status.Visibility.allCases.map { (visibility) -> UIMenuElement in
|
let elements = Status.Visibility.allCases.map { (visibility) -> UIMenuElement in
|
||||||
|
@ -212,7 +255,10 @@ class ComposeHostingController: UIHostingController<ComposeContainerView> {
|
||||||
}
|
}
|
||||||
|
|
||||||
private func localOnlyChanged(_ localOnly: Bool) {
|
private func localOnlyChanged(_ localOnly: Bool) {
|
||||||
for item in localOnlyItems {
|
for toolbar in [mainToolbar, inputAccessoryToolbar] {
|
||||||
|
guard let item = toolbar?.items?.first(where: { $0.tag == LOCAL_ONLY_BAR_BUTTON_TAG }) else {
|
||||||
|
continue
|
||||||
|
}
|
||||||
if localOnly {
|
if localOnly {
|
||||||
item.image = UIImage(named: "link.broken")
|
item.image = UIImage(named: "link.broken")
|
||||||
item.accessibilityLabel = "Local-only"
|
item.accessibilityLabel = "Local-only"
|
||||||
|
@ -260,6 +306,11 @@ class ComposeHostingController: UIHostingController<ComposeContainerView> {
|
||||||
draft.contentWarningEnabled = !draft.contentWarningEnabled
|
draft.contentWarningEnabled = !draft.contentWarningEnabled
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@objc func formatButtonPressed(_ sender: UIBarButtonItem) {
|
||||||
|
let format = StatusFormat.allCases[sender.tag]
|
||||||
|
uiState.currentInput?.applyFormat(format)
|
||||||
|
}
|
||||||
|
|
||||||
@objc func draftsButtonPresed() {
|
@objc func draftsButtonPresed() {
|
||||||
let draftsVC = DraftsTableViewController(account: mastodonController.accountInfo!, exclude: draft)
|
let draftsVC = DraftsTableViewController(account: mastodonController.accountInfo!, exclude: draft)
|
||||||
draftsVC.delegate = self
|
draftsVC.delegate = self
|
||||||
|
|
|
@ -31,7 +31,7 @@ class ComposeUIState: ObservableObject {
|
||||||
|
|
||||||
var composeDrawingMode: ComposeDrawingMode?
|
var composeDrawingMode: ComposeDrawingMode?
|
||||||
|
|
||||||
weak var autocompleteHandler: ComposeAutocompleteHandler?
|
@Published var currentInput: ComposeInput?
|
||||||
|
|
||||||
init(draft: Draft) {
|
init(draft: Draft) {
|
||||||
self.draft = draft
|
self.draft = draft
|
||||||
|
@ -60,6 +60,16 @@ extension ComposeUIState {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protocol ComposeAutocompleteHandler: AnyObject {
|
protocol ComposeInput: AnyObject {
|
||||||
|
var toolbarElements: [ComposeUIState.ToolbarElement] { get }
|
||||||
|
|
||||||
func autocomplete(with string: String)
|
func autocomplete(with string: String)
|
||||||
|
|
||||||
|
func applyFormat(_ format: StatusFormat)
|
||||||
|
}
|
||||||
|
|
||||||
|
extension ComposeUIState {
|
||||||
|
enum ToolbarElement {
|
||||||
|
case formattingButtons
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -39,7 +39,7 @@ struct EmojiPickerWrapper: UIViewControllerRepresentable {
|
||||||
}
|
}
|
||||||
|
|
||||||
func selectedEmoji(_ emoji: Emoji) {
|
func selectedEmoji(_ emoji: Emoji) {
|
||||||
uiState.autocompleteHandler?.autocomplete(with: ":\(emoji.shortcode):")
|
uiState.currentInput?.autocomplete(with: ":\(emoji.shortcode):")
|
||||||
uiState.autocompleteState = nil
|
uiState.autocompleteState = nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -57,9 +57,6 @@ struct MainComposeWrappedTextView: UIViewRepresentable {
|
||||||
|
|
||||||
@EnvironmentObject var uiState: ComposeUIState
|
@EnvironmentObject var uiState: ComposeUIState
|
||||||
@EnvironmentObject var mastodonController: MastodonController
|
@EnvironmentObject var mastodonController: MastodonController
|
||||||
// todo: should these be part of the coordinator?
|
|
||||||
@State var visibilityButton: UIBarButtonItem?
|
|
||||||
@State var localOnlyButton: UIBarButtonItem?
|
|
||||||
|
|
||||||
func makeUIView(context: Context) -> UITextView {
|
func makeUIView(context: Context) -> UITextView {
|
||||||
let textView = WrappedTextView()
|
let textView = WrappedTextView()
|
||||||
|
@ -69,101 +66,9 @@ struct MainComposeWrappedTextView: UIViewRepresentable {
|
||||||
textView.font = .systemFont(ofSize: 20)
|
textView.font = .systemFont(ofSize: 20)
|
||||||
textView.textContainer.lineBreakMode = .byWordWrapping
|
textView.textContainer.lineBreakMode = .byWordWrapping
|
||||||
context.coordinator.textView = textView
|
context.coordinator.textView = textView
|
||||||
|
|
||||||
uiState.autocompleteHandler = context.coordinator
|
|
||||||
|
|
||||||
let visibilityButton = UIBarButtonItem(image: UIImage(systemName: visibility.imageName), style: .plain, target: nil, action: nil)
|
|
||||||
updateVisibilityMenu(visibilityButton)
|
|
||||||
let toolbar = UIToolbar()
|
|
||||||
toolbar.translatesAutoresizingMaskIntoConstraints = false
|
|
||||||
toolbar.items = [
|
|
||||||
UIBarButtonItem(title: "CW", style: .plain, target: nil, action: #selector(ComposeHostingController.cwButtonPressed)),
|
|
||||||
visibilityButton,
|
|
||||||
UIBarButtonItem(barButtonSystemItem: .flexibleSpace, target: nil, action: nil),
|
|
||||||
] + createFormattingButtons(coordinator: context.coordinator) + [
|
|
||||||
UIBarButtonItem(barButtonSystemItem: .flexibleSpace, target: nil, action: nil),
|
|
||||||
UIBarButtonItem(title: "Drafts", style: .plain, target: nil, action: #selector(ComposeHostingController.draftsButtonPresed)),
|
|
||||||
]
|
|
||||||
textView.inputAccessoryView = toolbar
|
|
||||||
// can't modify @State during view update
|
|
||||||
DispatchQueue.main.async {
|
|
||||||
self.visibilityButton = visibilityButton
|
|
||||||
}
|
|
||||||
|
|
||||||
if mastodonController.instanceFeatures.localOnlyPosts {
|
|
||||||
let image: UIImage
|
|
||||||
if uiState.draft.localOnly {
|
|
||||||
image = UIImage(named: "link.broken")!
|
|
||||||
} else {
|
|
||||||
image = UIImage(systemName: "link")!
|
|
||||||
}
|
|
||||||
let item = UIBarButtonItem(image: image, style: .plain, target: nil, action: nil)
|
|
||||||
toolbar.items!.insert(item, at: 2)
|
|
||||||
updateLocalOnlyMenu(item)
|
|
||||||
|
|
||||||
DispatchQueue.main.async {
|
|
||||||
self.localOnlyButton = item
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
NotificationCenter.default.addObserver(context.coordinator, selector: #selector(Coordinator.keyboardWillShow(_:)), name: UIResponder.keyboardWillShowNotification, object: nil)
|
|
||||||
NotificationCenter.default.addObserver(context.coordinator, selector: #selector(Coordinator.keyboardWillHide(_:)), name: UIResponder.keyboardWillHideNotification, object: nil)
|
|
||||||
NotificationCenter.default.addObserver(context.coordinator, selector: #selector(Coordinator.keyboardDidHide(_:)), name: UIResponder.keyboardDidHideNotification, object: nil)
|
|
||||||
|
|
||||||
return textView
|
return textView
|
||||||
}
|
}
|
||||||
|
|
||||||
private func createFormattingButtons(coordinator: Coordinator) -> [UIBarButtonItem] {
|
|
||||||
guard Preferences.shared.statusContentType != .plain else {
|
|
||||||
return []
|
|
||||||
}
|
|
||||||
|
|
||||||
var formatButtons = StatusFormat.allCases.map { (format) -> UIBarButtonItem in
|
|
||||||
let item: UIBarButtonItem
|
|
||||||
if let image = format.image {
|
|
||||||
item = UIBarButtonItem(image: image, style: .plain, target: coordinator, action: #selector(Coordinator.formatButtonPressed(_:)))
|
|
||||||
} else if let (str, attributes) = format.title {
|
|
||||||
item = UIBarButtonItem(title: str, style: .plain, target: coordinator, action: #selector(Coordinator.formatButtonPressed(_:)))
|
|
||||||
item.setTitleTextAttributes(attributes, for: .normal)
|
|
||||||
item.setTitleTextAttributes(attributes, for: .highlighted)
|
|
||||||
} else {
|
|
||||||
fatalError("StatusFormat must have either an image or a title")
|
|
||||||
}
|
|
||||||
item.tag = StatusFormat.allCases.firstIndex(of: format)!
|
|
||||||
item.accessibilityLabel = format.accessibilityLabel
|
|
||||||
return item
|
|
||||||
}
|
|
||||||
|
|
||||||
for i in (1..<StatusFormat.allCases.count).reversed() {
|
|
||||||
let spacer = UIBarButtonItem(barButtonSystemItem: .fixedSpace, target: nil, action: nil)
|
|
||||||
spacer.width = 8
|
|
||||||
formatButtons.insert(spacer, at: i)
|
|
||||||
}
|
|
||||||
|
|
||||||
return formatButtons
|
|
||||||
}
|
|
||||||
|
|
||||||
private func updateVisibilityMenu(_ visibilityButton: UIBarButtonItem) {
|
|
||||||
let elements = Status.Visibility.allCases.map { (visibility) -> UIMenuElement in
|
|
||||||
let state = visibility == self.visibility ? UIMenuElement.State.on : .off
|
|
||||||
return UIAction(title: visibility.displayName, image: UIImage(systemName: visibility.unfilledImageName), identifier: nil, discoverabilityTitle: nil, attributes: [], state: state) { (_) in
|
|
||||||
self.uiState.draft.visibility = visibility
|
|
||||||
}
|
|
||||||
}
|
|
||||||
visibilityButton.menu = UIMenu(title: "", image: nil, identifier: nil, options: [], children: elements)
|
|
||||||
}
|
|
||||||
|
|
||||||
private func updateLocalOnlyMenu(_ localOnlyButton: UIBarButtonItem) {
|
|
||||||
localOnlyButton.menu = UIMenu(children: [
|
|
||||||
UIAction(title: "Local-only", image: UIImage(named: "link.broken"), state: uiState.draft.localOnly ? .on : .off) { (_) in
|
|
||||||
self.uiState.draft.localOnly = true
|
|
||||||
},
|
|
||||||
UIAction(title: "Federated", image: UIImage(systemName: "link"), state: uiState.draft.localOnly ? .off : .on) { (_) in
|
|
||||||
self.uiState.draft.localOnly = false
|
|
||||||
},
|
|
||||||
])
|
|
||||||
}
|
|
||||||
|
|
||||||
func updateUIView(_ uiView: UITextView, context: Context) {
|
func updateUIView(_ uiView: UITextView, context: Context) {
|
||||||
if context.coordinator.skipSettingTextOnNextUpdate {
|
if context.coordinator.skipSettingTextOnNextUpdate {
|
||||||
context.coordinator.skipSettingTextOnNextUpdate = false
|
context.coordinator.skipSettingTextOnNextUpdate = false
|
||||||
|
@ -171,18 +76,6 @@ struct MainComposeWrappedTextView: UIViewRepresentable {
|
||||||
uiView.text = text
|
uiView.text = text
|
||||||
}
|
}
|
||||||
|
|
||||||
if let visibilityButton = visibilityButton {
|
|
||||||
visibilityButton.image = UIImage(systemName: visibility.imageName)
|
|
||||||
updateVisibilityMenu(visibilityButton)
|
|
||||||
}
|
|
||||||
if let localOnlyButton = localOnlyButton {
|
|
||||||
if uiState.draft.localOnly {
|
|
||||||
localOnlyButton.image = UIImage(named: "link.broken")
|
|
||||||
} else {
|
|
||||||
localOnlyButton.image = UIImage(systemName: "link")
|
|
||||||
}
|
|
||||||
updateLocalOnlyMenu(localOnlyButton)
|
|
||||||
}
|
|
||||||
context.coordinator.text = $text
|
context.coordinator.text = $text
|
||||||
context.coordinator.didChange = textDidChange
|
context.coordinator.didChange = textDidChange
|
||||||
context.coordinator.uiState = uiState
|
context.coordinator.uiState = uiState
|
||||||
|
@ -232,17 +125,23 @@ struct MainComposeWrappedTextView: UIViewRepresentable {
|
||||||
command.attributes.remove(.disabled)
|
command.attributes.remove(.disabled)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
class Coordinator: NSObject, UITextViewDelegate, ComposeAutocompleteHandler, ComposeTextViewCaretScrolling {
|
class Coordinator: NSObject, UITextViewDelegate, ComposeInput, ComposeTextViewCaretScrolling {
|
||||||
weak var textView: UITextView?
|
weak var textView: UITextView?
|
||||||
var text: Binding<String>
|
var text: Binding<String>
|
||||||
var didChange: (UITextView) -> Void
|
var didChange: (UITextView) -> Void
|
||||||
var uiState: ComposeUIState
|
// break retained cycle through ComposeUIState.currentInput
|
||||||
|
unowned var uiState: ComposeUIState
|
||||||
var caretScrollPositionAnimator: UIViewPropertyAnimator?
|
var caretScrollPositionAnimator: UIViewPropertyAnimator?
|
||||||
|
|
||||||
var skipSettingTextOnNextUpdate = false
|
var skipSettingTextOnNextUpdate = false
|
||||||
|
|
||||||
|
var toolbarElements: [ComposeUIState.ToolbarElement] {
|
||||||
|
[.formattingButtons]
|
||||||
|
}
|
||||||
|
|
||||||
init(text: Binding<String>, uiState: ComposeUIState, didChange: @escaping (UITextView) -> Void) {
|
init(text: Binding<String>, uiState: ComposeUIState, didChange: @escaping (UITextView) -> Void) {
|
||||||
self.text = text
|
self.text = text
|
||||||
self.didChange = didChange
|
self.didChange = didChange
|
||||||
|
@ -256,11 +155,6 @@ struct MainComposeWrappedTextView: UIViewRepresentable {
|
||||||
ensureCursorVisible(textView: textView)
|
ensureCursorVisible(textView: textView)
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc func formatButtonPressed(_ sender: UIBarButtonItem) {
|
|
||||||
let format = StatusFormat.allCases[sender.tag]
|
|
||||||
applyFormat(format)
|
|
||||||
}
|
|
||||||
|
|
||||||
func applyFormat(_ format: StatusFormat) {
|
func applyFormat(_ format: StatusFormat) {
|
||||||
guard let textView = textView,
|
guard let textView = textView,
|
||||||
textView.isFirstResponder,
|
textView.isFirstResponder,
|
||||||
|
@ -281,24 +175,13 @@ struct MainComposeWrappedTextView: UIViewRepresentable {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc func keyboardWillShow(_ notification: Foundation.Notification) {
|
|
||||||
uiState.delegate?.keyboardWillShow(accessoryView: textView!.inputAccessoryView!, notification: notification)
|
|
||||||
}
|
|
||||||
|
|
||||||
@objc func keyboardWillHide(_ notification: Foundation.Notification) {
|
|
||||||
uiState.delegate?.keyboardWillHide(accessoryView: textView!.inputAccessoryView!, notification: notification)
|
|
||||||
}
|
|
||||||
|
|
||||||
@objc func keyboardDidHide(_ notification: Foundation.Notification) {
|
|
||||||
uiState.delegate?.keyboardDidHide(accessoryView: textView!.inputAccessoryView!, notification: notification)
|
|
||||||
}
|
|
||||||
|
|
||||||
func textViewDidBeginEditing(_ textView: UITextView) {
|
func textViewDidBeginEditing(_ textView: UITextView) {
|
||||||
uiState.autocompleteHandler = self
|
uiState.currentInput = self
|
||||||
updateAutocompleteState()
|
updateAutocompleteState()
|
||||||
}
|
}
|
||||||
|
|
||||||
func textViewDidEndEditing(_ textView: UITextView) {
|
func textViewDidEndEditing(_ textView: UITextView) {
|
||||||
|
uiState.currentInput = nil
|
||||||
updateAutocompleteState()
|
updateAutocompleteState()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue