More compose screen tweaks
This commit is contained in:
parent
f46150422a
commit
ec3678d90d
@ -72,7 +72,7 @@ private struct PostOrDraftsButton: View {
|
||||
}
|
||||
|
||||
private var draftIsEmpty: Bool {
|
||||
draft.text == draft.initialText && (!draft.contentWarningEnabled || draft.contentWarning == draft.initialContentWarning) && draft.attachments.count == 0 && !draft.pollEnabled
|
||||
draft.text == draft.initialText && (!draft.contentWarningEnabled || draft.contentWarning == draft.initialContentWarning) && draft.attachments.count == 0 && (!draft.pollEnabled || !draft.poll!.hasContent)
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
@ -107,8 +107,12 @@ private struct ContentWarningButton: View {
|
||||
|
||||
private func toggleContentWarning() {
|
||||
enabled.toggle()
|
||||
if enabled {
|
||||
focusedField = .contentWarning
|
||||
if focusedField != nil {
|
||||
if enabled {
|
||||
focusedField = .contentWarning
|
||||
} else if focusedField == .contentWarning {
|
||||
focusedField = .body
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -6,6 +6,7 @@
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import CoreData
|
||||
|
||||
struct ComposeView: View {
|
||||
@ObservedObject var draft: Draft
|
||||
@ -103,7 +104,7 @@ struct ComposeView: View {
|
||||
VStack(spacing: 8) {
|
||||
NewReplyStatusView(draft: draft, mastodonController: mastodonController)
|
||||
|
||||
ComposeDraftView(draft: draft, focusedField: $focusedField)
|
||||
DraftEditor(draft: draft, focusedField: $focusedField)
|
||||
}
|
||||
.padding(8)
|
||||
}
|
||||
@ -151,16 +152,7 @@ public struct NavigationTitlePreferenceKey: PreferenceKey {
|
||||
enum FocusableField: Hashable {
|
||||
case contentWarning
|
||||
case body
|
||||
case attachmentDescription(UUID)
|
||||
|
||||
var nextField: FocusableField? {
|
||||
switch self {
|
||||
case .contentWarning:
|
||||
return .body
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
case pollOption(NSManagedObjectID)
|
||||
}
|
||||
|
||||
#if !os(visionOS) && !targetEnvironment(macCatalyst)
|
||||
|
@ -12,24 +12,22 @@ struct ContentWarningTextField: View {
|
||||
@FocusState.Binding var focusedField: FocusableField?
|
||||
|
||||
var body: some View {
|
||||
if draft.contentWarningEnabled {
|
||||
EmojiTextField(
|
||||
text: $draft.contentWarning,
|
||||
placeholder: "Write your warning here",
|
||||
maxLength: nil,
|
||||
// TODO: completely replace this with FocusState
|
||||
becomeFirstResponder: .constant(false),
|
||||
focusNextView: Binding(get: {
|
||||
false
|
||||
}, set: {
|
||||
if $0 {
|
||||
focusedField = .body
|
||||
}
|
||||
})
|
||||
)
|
||||
.focused($focusedField, equals: .contentWarning)
|
||||
.modifier(FocusedInputModifier())
|
||||
}
|
||||
EmojiTextField(
|
||||
text: $draft.contentWarning,
|
||||
placeholder: "Write your warning here",
|
||||
maxLength: nil,
|
||||
// TODO: completely replace this with FocusState
|
||||
becomeFirstResponder: .constant(false),
|
||||
focusNextView: Binding(get: {
|
||||
false
|
||||
}, set: {
|
||||
if $0 {
|
||||
focusedField = .body
|
||||
}
|
||||
})
|
||||
)
|
||||
.focused($focusedField, equals: .contentWarning)
|
||||
.modifier(FocusedInputModifier())
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -20,7 +20,7 @@ struct DraftContentEditor: View {
|
||||
|
||||
HStack(alignment: .firstTextBaseline) {
|
||||
LanguageButton(draft: draft)
|
||||
TogglePollButton(draft: draft)
|
||||
TogglePollButton(draft: draft, focusedField: $focusedField)
|
||||
Spacer()
|
||||
CharactersRemaining(draft: draft)
|
||||
.padding(.trailing, 6)
|
||||
@ -82,6 +82,7 @@ private struct LanguageButton: View {
|
||||
private struct LanguageButtonStyle: ButtonStyle {
|
||||
func makeBody(configuration: Configuration) -> some View {
|
||||
configuration.label
|
||||
.font(.body.monospaced())
|
||||
.foregroundStyle(.tint.opacity(configuration.isPressed ? 0.8 : 1))
|
||||
.padding(.vertical, 2)
|
||||
.padding(.horizontal, 4)
|
||||
@ -93,8 +94,9 @@ private struct LanguageButtonStyle: ButtonStyle {
|
||||
|
||||
private struct TogglePollButton: View {
|
||||
@ObservedObject var draft: Draft
|
||||
@FocusState.Binding var focusedField: FocusableField?
|
||||
@EnvironmentObject private var instanceFeatures: InstanceFeatures
|
||||
|
||||
|
||||
var body: some View {
|
||||
Button(action: togglePoll) {
|
||||
Image(systemName: draft.pollEnabled ? "chart.bar.doc.horizontal.fill" : "chart.bar.horizontal.page")
|
||||
@ -112,11 +114,24 @@ private struct TogglePollButton: View {
|
||||
private func togglePoll() {
|
||||
if draft.pollEnabled {
|
||||
draft.pollEnabled = false
|
||||
|
||||
if case .pollOption(_) = focusedField {
|
||||
focusedField = .body
|
||||
}
|
||||
} else {
|
||||
if draft.poll == nil {
|
||||
draft.poll = Poll(context: DraftsPersistentContainer.shared.viewContext)
|
||||
let poll: Poll
|
||||
if let p = draft.poll {
|
||||
poll = p
|
||||
} else {
|
||||
poll = Poll(context: DraftsPersistentContainer.shared.viewContext)
|
||||
draft.poll = poll
|
||||
}
|
||||
draft.pollEnabled = true
|
||||
|
||||
if focusedField != nil {
|
||||
let optionToFocus = poll.pollOptions.first(where: { $0.text.isEmpty }) ?? poll.pollOptions.last!
|
||||
focusedField = .pollOption(optionToFocus.id)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,5 +1,5 @@
|
||||
//
|
||||
// ComposeDraftView.swift
|
||||
// DraftEditor.swift
|
||||
// ComposeUI
|
||||
//
|
||||
// Created by Shadowfacts on 11/16/24.
|
||||
@ -9,7 +9,7 @@ import SwiftUI
|
||||
import Pachyderm
|
||||
import TuskerComponents
|
||||
|
||||
struct ComposeDraftView: View {
|
||||
struct DraftEditor: View {
|
||||
@ObservedObject var draft: Draft
|
||||
@FocusState.Binding var focusedField: FocusableField?
|
||||
@Environment(\.currentAccount) private var currentAccount
|
||||
@ -24,13 +24,16 @@ struct ComposeDraftView: View {
|
||||
AccountNameView(account: currentAccount)
|
||||
}
|
||||
|
||||
ContentWarningTextField(draft: draft, focusedField: $focusedField)
|
||||
if draft.contentWarningEnabled {
|
||||
ContentWarningTextField(draft: draft, focusedField: $focusedField)
|
||||
.transition(.opacity)
|
||||
}
|
||||
|
||||
DraftContentEditor(draft: draft, focusedField: $focusedField)
|
||||
|
||||
if let poll = draft.poll,
|
||||
draft.pollEnabled {
|
||||
PollEditor(poll: poll)
|
||||
PollEditor(poll: poll, focusedField: $focusedField)
|
||||
.padding(.bottom, 4)
|
||||
// So that during the appearance transition, it's behind the text view.
|
||||
.zIndex(-1)
|
||||
@ -40,11 +43,12 @@ struct ComposeDraftView: View {
|
||||
AttachmentsSection(draft: draft)
|
||||
// We want the padding between the poll/attachments to be part of the poll, so it animates in/out with the transition.
|
||||
// Otherwise, when the poll is added, its bottom edge is aligned with the top edge of the attachments
|
||||
.padding(.top, draft.poll == nil ? 0 : -4)
|
||||
.padding(.top, draft.pollEnabled ? -4 : 0)
|
||||
}
|
||||
// These animations are here, because the height of the VStack and the positions of the lower views needs to animate too.
|
||||
.animation(.snappy, value: draft.pollEnabled)
|
||||
.modifier(PollAnimatingModifier(poll: draft.poll))
|
||||
.animation(.snappy, value: draft.contentWarningEnabled)
|
||||
}
|
||||
}
|
||||
}
|
@ -142,6 +142,10 @@ private final class WrappedTextViewCoordinator: NSObject {
|
||||
|
||||
private func attributedTextFromPlain(_ text: String) -> NSAttributedString {
|
||||
let str = NSMutableAttributedString(string: text)
|
||||
str.addAttributes([
|
||||
.foregroundColor: UIColor.label,
|
||||
.font: UIFontMetrics.default.scaledFont(for: .systemFont(ofSize: 20)),
|
||||
], range: NSRange(location: 0, length: str.length))
|
||||
let mentionMatches = CharacterCounter.mention.matches(in: text, range: NSRange(location: 0, length: str.length))
|
||||
for match in mentionMatches.reversed() {
|
||||
str.insert(NSAttributedString(attachment: Self.attachment), at: match.range.location)
|
||||
@ -175,7 +179,7 @@ private final class WrappedTextViewCoordinator: NSObject {
|
||||
if mentionRegex.numberOfMatches(in: substr, range: NSRange(location: 0, length: substr.utf16.count)) == 0 {
|
||||
changed = true
|
||||
str.removeAttribute(.mention, range: range)
|
||||
str.removeAttribute(.foregroundColor, range: range)
|
||||
str.addAttribute(.foregroundColor, value: UIColor.label, range: range)
|
||||
if hasTextAttachment {
|
||||
str.deleteCharacters(in: NSRange(location: range.location, length: 1))
|
||||
cursorOffset -= 1
|
||||
|
@ -11,20 +11,19 @@ import TuskerComponents
|
||||
|
||||
struct PollEditor: View {
|
||||
@ObservedObject var poll: Poll
|
||||
|
||||
@FocusState.Binding var focusedField: FocusableField?
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading, spacing: 8) {
|
||||
Text("Poll")
|
||||
.font(.headline)
|
||||
|
||||
ForEach(poll.pollOptions) { option in
|
||||
PollOptionEditor(option: option, index: poll.options.index(of: option)) {
|
||||
self.removeOption(option)
|
||||
}
|
||||
.transition(.opacity)
|
||||
PollOptionEditor(poll: poll, option: option, focusedField: $focusedField)
|
||||
.transition(.opacity)
|
||||
}
|
||||
|
||||
AddOptionButton(poll: poll)
|
||||
AddOptionButton(poll: poll, focusedField: $focusedField)
|
||||
|
||||
Toggle("Multiple choice", isOn: $poll.multiple)
|
||||
.padding(.bottom, -8)
|
||||
@ -41,19 +40,11 @@ struct PollEditor: View {
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
.composePlatterBackground()
|
||||
}
|
||||
|
||||
private func removeOption(_ option: PollOption) {
|
||||
let index = poll.options.index(of: option)
|
||||
if index != NSNotFound {
|
||||
var array = poll.options.array
|
||||
array.remove(at: index)
|
||||
poll.options = NSMutableOrderedSet(array: array)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private struct AddOptionButton: View {
|
||||
@ObservedObject var poll: Poll
|
||||
@FocusState.Binding var focusedField: FocusableField?
|
||||
@EnvironmentObject private var instanceFeatures: InstanceFeatures
|
||||
|
||||
private var canAddOption: Bool {
|
||||
@ -76,6 +67,7 @@ private struct AddOptionButton: View {
|
||||
let option = PollOption(context: DraftsPersistentContainer.shared.viewContext)
|
||||
option.poll = poll
|
||||
poll.options.add(option)
|
||||
focusedField = .pollOption(option.id)
|
||||
}
|
||||
}
|
||||
|
||||
@ -147,16 +139,17 @@ private enum PollDuration: Hashable, Equatable, CaseIterable {
|
||||
}
|
||||
|
||||
private struct PollOptionEditor: View {
|
||||
@ObservedObject var poll: Poll
|
||||
@ObservedObject var option: PollOption
|
||||
let index: Int
|
||||
let removeOption: () -> Void
|
||||
@FocusState.Binding var focusedField: FocusableField?
|
||||
@EnvironmentObject private var instanceFeatures: InstanceFeatures
|
||||
|
||||
var placeholder: String {
|
||||
let index = poll.options.index(of: option)
|
||||
if index != NSNotFound {
|
||||
"Option \(index + 1)"
|
||||
return "Option \(index + 1)"
|
||||
} else {
|
||||
""
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
@ -168,12 +161,33 @@ private struct PollOptionEditor: View {
|
||||
.labelStyle(.iconOnly)
|
||||
.buttonStyle(PollOptionButtonStyle())
|
||||
.accessibilityLabel("Remove option")
|
||||
.disabled(poll.options.count == 1)
|
||||
|
||||
EmojiTextField(text: $option.text, placeholder: placeholder, maxLength: instanceFeatures.maxPollOptionChars)
|
||||
.focused($focusedField, equals: .pollOption(option.id))
|
||||
}
|
||||
}
|
||||
|
||||
private func removeOption() {
|
||||
let index = poll.options.index(of: option)
|
||||
if index != NSNotFound && poll.options.count > 1 {
|
||||
var array = poll.options.array
|
||||
array.remove(at: index)
|
||||
poll.options = NSMutableOrderedSet(array: array)
|
||||
// TODO: does this leave dangling PollOptions in the managed object context?
|
||||
|
||||
if case .pollOption(let id) = focusedField,
|
||||
id == option.id {
|
||||
let indexToFocus = index > 0 ? index - 1 : 0
|
||||
focusedField = .pollOption(poll.pollOptions[indexToFocus].id)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private struct PollOptionButtonStyle: ButtonStyle {
|
||||
@Environment(\.isEnabled) private var isEnabled
|
||||
|
||||
func makeBody(configuration: Configuration) -> some View {
|
||||
configuration.label
|
||||
.foregroundStyle(.white)
|
||||
@ -181,7 +195,15 @@ private struct PollOptionButtonStyle: ButtonStyle {
|
||||
.padding(4)
|
||||
.frame(width: 20, height: 20)
|
||||
.background {
|
||||
let color = configuration.role == .destructive ? Color.red : .green
|
||||
let color = if isEnabled {
|
||||
if configuration.role == .destructive {
|
||||
Color.red
|
||||
} else {
|
||||
Color.green
|
||||
}
|
||||
} else {
|
||||
Color.gray
|
||||
}
|
||||
let opacity = configuration.isPressed ? 0.8 : 1
|
||||
Circle()
|
||||
.fill(color.opacity(opacity))
|
||||
|
Loading…
x
Reference in New Issue
Block a user