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 {
|
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
|
#endif
|
||||||
|
@ -107,8 +107,12 @@ private struct ContentWarningButton: View {
|
|||||||
|
|
||||||
private func toggleContentWarning() {
|
private func toggleContentWarning() {
|
||||||
enabled.toggle()
|
enabled.toggle()
|
||||||
|
if focusedField != nil {
|
||||||
if enabled {
|
if enabled {
|
||||||
focusedField = .contentWarning
|
focusedField = .contentWarning
|
||||||
|
} else if focusedField == .contentWarning {
|
||||||
|
focusedField = .body
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -6,6 +6,7 @@
|
|||||||
//
|
//
|
||||||
|
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
import CoreData
|
||||||
|
|
||||||
struct ComposeView: View {
|
struct ComposeView: View {
|
||||||
@ObservedObject var draft: Draft
|
@ObservedObject var draft: Draft
|
||||||
@ -103,7 +104,7 @@ struct ComposeView: View {
|
|||||||
VStack(spacing: 8) {
|
VStack(spacing: 8) {
|
||||||
NewReplyStatusView(draft: draft, mastodonController: mastodonController)
|
NewReplyStatusView(draft: draft, mastodonController: mastodonController)
|
||||||
|
|
||||||
ComposeDraftView(draft: draft, focusedField: $focusedField)
|
DraftEditor(draft: draft, focusedField: $focusedField)
|
||||||
}
|
}
|
||||||
.padding(8)
|
.padding(8)
|
||||||
}
|
}
|
||||||
@ -151,16 +152,7 @@ public struct NavigationTitlePreferenceKey: PreferenceKey {
|
|||||||
enum FocusableField: Hashable {
|
enum FocusableField: Hashable {
|
||||||
case contentWarning
|
case contentWarning
|
||||||
case body
|
case body
|
||||||
case attachmentDescription(UUID)
|
case pollOption(NSManagedObjectID)
|
||||||
|
|
||||||
var nextField: FocusableField? {
|
|
||||||
switch self {
|
|
||||||
case .contentWarning:
|
|
||||||
return .body
|
|
||||||
default:
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#if !os(visionOS) && !targetEnvironment(macCatalyst)
|
#if !os(visionOS) && !targetEnvironment(macCatalyst)
|
||||||
|
@ -12,7 +12,6 @@ struct ContentWarningTextField: View {
|
|||||||
@FocusState.Binding var focusedField: FocusableField?
|
@FocusState.Binding var focusedField: FocusableField?
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
if draft.contentWarningEnabled {
|
|
||||||
EmojiTextField(
|
EmojiTextField(
|
||||||
text: $draft.contentWarning,
|
text: $draft.contentWarning,
|
||||||
placeholder: "Write your warning here",
|
placeholder: "Write your warning here",
|
||||||
@ -31,7 +30,6 @@ struct ContentWarningTextField: View {
|
|||||||
.modifier(FocusedInputModifier())
|
.modifier(FocusedInputModifier())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
//#Preview {
|
//#Preview {
|
||||||
// ContentWarningTextField()
|
// ContentWarningTextField()
|
||||||
|
@ -20,7 +20,7 @@ struct DraftContentEditor: View {
|
|||||||
|
|
||||||
HStack(alignment: .firstTextBaseline) {
|
HStack(alignment: .firstTextBaseline) {
|
||||||
LanguageButton(draft: draft)
|
LanguageButton(draft: draft)
|
||||||
TogglePollButton(draft: draft)
|
TogglePollButton(draft: draft, focusedField: $focusedField)
|
||||||
Spacer()
|
Spacer()
|
||||||
CharactersRemaining(draft: draft)
|
CharactersRemaining(draft: draft)
|
||||||
.padding(.trailing, 6)
|
.padding(.trailing, 6)
|
||||||
@ -82,6 +82,7 @@ private struct LanguageButton: View {
|
|||||||
private struct LanguageButtonStyle: ButtonStyle {
|
private struct LanguageButtonStyle: ButtonStyle {
|
||||||
func makeBody(configuration: Configuration) -> some View {
|
func makeBody(configuration: Configuration) -> some View {
|
||||||
configuration.label
|
configuration.label
|
||||||
|
.font(.body.monospaced())
|
||||||
.foregroundStyle(.tint.opacity(configuration.isPressed ? 0.8 : 1))
|
.foregroundStyle(.tint.opacity(configuration.isPressed ? 0.8 : 1))
|
||||||
.padding(.vertical, 2)
|
.padding(.vertical, 2)
|
||||||
.padding(.horizontal, 4)
|
.padding(.horizontal, 4)
|
||||||
@ -93,6 +94,7 @@ private struct LanguageButtonStyle: ButtonStyle {
|
|||||||
|
|
||||||
private struct TogglePollButton: View {
|
private struct TogglePollButton: View {
|
||||||
@ObservedObject var draft: Draft
|
@ObservedObject var draft: Draft
|
||||||
|
@FocusState.Binding var focusedField: FocusableField?
|
||||||
@EnvironmentObject private var instanceFeatures: InstanceFeatures
|
@EnvironmentObject private var instanceFeatures: InstanceFeatures
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
@ -112,11 +114,24 @@ private struct TogglePollButton: View {
|
|||||||
private func togglePoll() {
|
private func togglePoll() {
|
||||||
if draft.pollEnabled {
|
if draft.pollEnabled {
|
||||||
draft.pollEnabled = false
|
draft.pollEnabled = false
|
||||||
|
|
||||||
|
if case .pollOption(_) = focusedField {
|
||||||
|
focusedField = .body
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
if draft.poll == nil {
|
let poll: Poll
|
||||||
draft.poll = Poll(context: DraftsPersistentContainer.shared.viewContext)
|
if let p = draft.poll {
|
||||||
|
poll = p
|
||||||
|
} else {
|
||||||
|
poll = Poll(context: DraftsPersistentContainer.shared.viewContext)
|
||||||
|
draft.poll = poll
|
||||||
}
|
}
|
||||||
draft.pollEnabled = true
|
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
|
// ComposeUI
|
||||||
//
|
//
|
||||||
// Created by Shadowfacts on 11/16/24.
|
// Created by Shadowfacts on 11/16/24.
|
||||||
@ -9,7 +9,7 @@ import SwiftUI
|
|||||||
import Pachyderm
|
import Pachyderm
|
||||||
import TuskerComponents
|
import TuskerComponents
|
||||||
|
|
||||||
struct ComposeDraftView: View {
|
struct DraftEditor: View {
|
||||||
@ObservedObject var draft: Draft
|
@ObservedObject var draft: Draft
|
||||||
@FocusState.Binding var focusedField: FocusableField?
|
@FocusState.Binding var focusedField: FocusableField?
|
||||||
@Environment(\.currentAccount) private var currentAccount
|
@Environment(\.currentAccount) private var currentAccount
|
||||||
@ -24,13 +24,16 @@ struct ComposeDraftView: View {
|
|||||||
AccountNameView(account: currentAccount)
|
AccountNameView(account: currentAccount)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if draft.contentWarningEnabled {
|
||||||
ContentWarningTextField(draft: draft, focusedField: $focusedField)
|
ContentWarningTextField(draft: draft, focusedField: $focusedField)
|
||||||
|
.transition(.opacity)
|
||||||
|
}
|
||||||
|
|
||||||
DraftContentEditor(draft: draft, focusedField: $focusedField)
|
DraftContentEditor(draft: draft, focusedField: $focusedField)
|
||||||
|
|
||||||
if let poll = draft.poll,
|
if let poll = draft.poll,
|
||||||
draft.pollEnabled {
|
draft.pollEnabled {
|
||||||
PollEditor(poll: poll)
|
PollEditor(poll: poll, focusedField: $focusedField)
|
||||||
.padding(.bottom, 4)
|
.padding(.bottom, 4)
|
||||||
// So that during the appearance transition, it's behind the text view.
|
// So that during the appearance transition, it's behind the text view.
|
||||||
.zIndex(-1)
|
.zIndex(-1)
|
||||||
@ -40,11 +43,12 @@ struct ComposeDraftView: View {
|
|||||||
AttachmentsSection(draft: draft)
|
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.
|
// 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
|
// 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.
|
// 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)
|
.animation(.snappy, value: draft.pollEnabled)
|
||||||
.modifier(PollAnimatingModifier(poll: draft.poll))
|
.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 {
|
private func attributedTextFromPlain(_ text: String) -> NSAttributedString {
|
||||||
let str = NSMutableAttributedString(string: text)
|
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))
|
let mentionMatches = CharacterCounter.mention.matches(in: text, range: NSRange(location: 0, length: str.length))
|
||||||
for match in mentionMatches.reversed() {
|
for match in mentionMatches.reversed() {
|
||||||
str.insert(NSAttributedString(attachment: Self.attachment), at: match.range.location)
|
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 {
|
if mentionRegex.numberOfMatches(in: substr, range: NSRange(location: 0, length: substr.utf16.count)) == 0 {
|
||||||
changed = true
|
changed = true
|
||||||
str.removeAttribute(.mention, range: range)
|
str.removeAttribute(.mention, range: range)
|
||||||
str.removeAttribute(.foregroundColor, range: range)
|
str.addAttribute(.foregroundColor, value: UIColor.label, range: range)
|
||||||
if hasTextAttachment {
|
if hasTextAttachment {
|
||||||
str.deleteCharacters(in: NSRange(location: range.location, length: 1))
|
str.deleteCharacters(in: NSRange(location: range.location, length: 1))
|
||||||
cursorOffset -= 1
|
cursorOffset -= 1
|
||||||
|
@ -11,6 +11,7 @@ import TuskerComponents
|
|||||||
|
|
||||||
struct PollEditor: View {
|
struct PollEditor: View {
|
||||||
@ObservedObject var poll: Poll
|
@ObservedObject var poll: Poll
|
||||||
|
@FocusState.Binding var focusedField: FocusableField?
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
VStack(alignment: .leading, spacing: 8) {
|
VStack(alignment: .leading, spacing: 8) {
|
||||||
@ -18,13 +19,11 @@ struct PollEditor: View {
|
|||||||
.font(.headline)
|
.font(.headline)
|
||||||
|
|
||||||
ForEach(poll.pollOptions) { option in
|
ForEach(poll.pollOptions) { option in
|
||||||
PollOptionEditor(option: option, index: poll.options.index(of: option)) {
|
PollOptionEditor(poll: poll, option: option, focusedField: $focusedField)
|
||||||
self.removeOption(option)
|
|
||||||
}
|
|
||||||
.transition(.opacity)
|
.transition(.opacity)
|
||||||
}
|
}
|
||||||
|
|
||||||
AddOptionButton(poll: poll)
|
AddOptionButton(poll: poll, focusedField: $focusedField)
|
||||||
|
|
||||||
Toggle("Multiple choice", isOn: $poll.multiple)
|
Toggle("Multiple choice", isOn: $poll.multiple)
|
||||||
.padding(.bottom, -8)
|
.padding(.bottom, -8)
|
||||||
@ -41,19 +40,11 @@ struct PollEditor: View {
|
|||||||
.frame(maxWidth: .infinity, alignment: .leading)
|
.frame(maxWidth: .infinity, alignment: .leading)
|
||||||
.composePlatterBackground()
|
.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 {
|
private struct AddOptionButton: View {
|
||||||
@ObservedObject var poll: Poll
|
@ObservedObject var poll: Poll
|
||||||
|
@FocusState.Binding var focusedField: FocusableField?
|
||||||
@EnvironmentObject private var instanceFeatures: InstanceFeatures
|
@EnvironmentObject private var instanceFeatures: InstanceFeatures
|
||||||
|
|
||||||
private var canAddOption: Bool {
|
private var canAddOption: Bool {
|
||||||
@ -76,6 +67,7 @@ private struct AddOptionButton: View {
|
|||||||
let option = PollOption(context: DraftsPersistentContainer.shared.viewContext)
|
let option = PollOption(context: DraftsPersistentContainer.shared.viewContext)
|
||||||
option.poll = poll
|
option.poll = poll
|
||||||
poll.options.add(option)
|
poll.options.add(option)
|
||||||
|
focusedField = .pollOption(option.id)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -147,16 +139,17 @@ private enum PollDuration: Hashable, Equatable, CaseIterable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private struct PollOptionEditor: View {
|
private struct PollOptionEditor: View {
|
||||||
|
@ObservedObject var poll: Poll
|
||||||
@ObservedObject var option: PollOption
|
@ObservedObject var option: PollOption
|
||||||
let index: Int
|
@FocusState.Binding var focusedField: FocusableField?
|
||||||
let removeOption: () -> Void
|
|
||||||
@EnvironmentObject private var instanceFeatures: InstanceFeatures
|
@EnvironmentObject private var instanceFeatures: InstanceFeatures
|
||||||
|
|
||||||
var placeholder: String {
|
var placeholder: String {
|
||||||
|
let index = poll.options.index(of: option)
|
||||||
if index != NSNotFound {
|
if index != NSNotFound {
|
||||||
"Option \(index + 1)"
|
return "Option \(index + 1)"
|
||||||
} else {
|
} else {
|
||||||
""
|
return ""
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -168,12 +161,33 @@ private struct PollOptionEditor: View {
|
|||||||
.labelStyle(.iconOnly)
|
.labelStyle(.iconOnly)
|
||||||
.buttonStyle(PollOptionButtonStyle())
|
.buttonStyle(PollOptionButtonStyle())
|
||||||
.accessibilityLabel("Remove option")
|
.accessibilityLabel("Remove option")
|
||||||
|
.disabled(poll.options.count == 1)
|
||||||
|
|
||||||
EmojiTextField(text: $option.text, placeholder: placeholder, maxLength: instanceFeatures.maxPollOptionChars)
|
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 {
|
private struct PollOptionButtonStyle: ButtonStyle {
|
||||||
|
@Environment(\.isEnabled) private var isEnabled
|
||||||
|
|
||||||
func makeBody(configuration: Configuration) -> some View {
|
func makeBody(configuration: Configuration) -> some View {
|
||||||
configuration.label
|
configuration.label
|
||||||
.foregroundStyle(.white)
|
.foregroundStyle(.white)
|
||||||
@ -181,7 +195,15 @@ private struct PollOptionButtonStyle: ButtonStyle {
|
|||||||
.padding(4)
|
.padding(4)
|
||||||
.frame(width: 20, height: 20)
|
.frame(width: 20, height: 20)
|
||||||
.background {
|
.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
|
let opacity = configuration.isPressed ? 0.8 : 1
|
||||||
Circle()
|
Circle()
|
||||||
.fill(color.opacity(opacity))
|
.fill(color.opacity(opacity))
|
||||||
|
Loading…
x
Reference in New Issue
Block a user