mirror of
https://github.com/Dimillian/IceCubesApp.git
synced 2025-06-05 13:28:50 +00:00
Feature: Post and Reply by Threads (#1740)
* refactor: `StatusEditorView` * feat: post and reply by threads * Tidy up * Fixes --------- Co-authored-by: Thomas Ricouard <ricouard77@gmail.com>
This commit is contained in:
parent
d8a686be51
commit
1977b1a572
10 changed files with 592 additions and 284 deletions
|
@ -61533,6 +61533,125 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"status.editor.follow-up.text.placeholder" : {
|
||||||
|
"extractionState" : "manual",
|
||||||
|
"localizations" : {
|
||||||
|
"be" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "needs_review",
|
||||||
|
"value" : "Type your follow-up content here."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"ca" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "needs_review",
|
||||||
|
"value" : "Type your follow-up content here."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"de" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "needs_review",
|
||||||
|
"value" : "Type your follow-up content here."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"en" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "Type your follow-up content here."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"en-GB" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "Type your follow-up content here."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"es" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "needs_review",
|
||||||
|
"value" : "Type your follow-up content here."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"eu" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "needs_review",
|
||||||
|
"value" : "Type your follow-up content here."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"fr" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "needs_review",
|
||||||
|
"value" : "Type your follow-up content here."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"it" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "needs_review",
|
||||||
|
"value" : "Type your follow-up content here."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"ja" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "needs_review",
|
||||||
|
"value" : "Type your follow-up content here."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"ko" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "needs_review",
|
||||||
|
"value" : "Type your follow-up content here."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"nb" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "needs_review",
|
||||||
|
"value" : "Type your follow-up content here."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"nl" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "needs_review",
|
||||||
|
"value" : "Type your follow-up content here."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"pl" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "needs_review",
|
||||||
|
"value" : "Type your follow-up content here."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"pt-BR" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "needs_review",
|
||||||
|
"value" : "Type your follow-up content here."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"tr" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "needs_review",
|
||||||
|
"value" : "Type your follow-up content here."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"uk" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "needs_review",
|
||||||
|
"value" : "Type your follow-up content here."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"zh-Hans" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "needs_review",
|
||||||
|
"value" : "Type your follow-up content here."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"zh-Hant" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "needs_review",
|
||||||
|
"value" : "Type your follow-up content here."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"status.editor.language-select.confirmation.detected-%@" : {
|
"status.editor.language-select.confirmation.detected-%@" : {
|
||||||
"extractionState" : "manual",
|
"extractionState" : "manual",
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
|
|
|
@ -12,8 +12,9 @@ struct StatusEditorAccessoryView: View {
|
||||||
@Environment(CurrentInstance.self) private var currentInstance
|
@Environment(CurrentInstance.self) private var currentInstance
|
||||||
@Environment(\.colorScheme) private var colorScheme
|
@Environment(\.colorScheme) private var colorScheme
|
||||||
|
|
||||||
@FocusState<Bool>.Binding var isSpoilerTextFocused: Bool
|
@FocusState<UUID?>.Binding var isSpoilerTextFocused: UUID?
|
||||||
var viewModel: StatusEditorViewModel
|
let focusedSEVM: StatusEditorViewModel
|
||||||
|
@Binding var followUpSEVMs: [StatusEditorViewModel]
|
||||||
|
|
||||||
@State private var isDraftsSheetDisplayed: Bool = false
|
@State private var isDraftsSheetDisplayed: Bool = false
|
||||||
@State private var isLanguageSheetDisplayed: Bool = false
|
@State private var isLanguageSheetDisplayed: Bool = false
|
||||||
|
@ -25,7 +26,8 @@ struct StatusEditorAccessoryView: View {
|
||||||
@State private var isCameraPickerPresented: Bool = false
|
@State private var isCameraPickerPresented: Bool = false
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
@Bindable var viewModel = viewModel
|
@Bindable var viewModel = focusedSEVM
|
||||||
|
|
||||||
VStack(spacing: 0) {
|
VStack(spacing: 0) {
|
||||||
Divider()
|
Divider()
|
||||||
HStack {
|
HStack {
|
||||||
|
@ -83,6 +85,14 @@ struct StatusEditorAccessoryView: View {
|
||||||
.accessibilityLabel("accessibility.editor.button.attach-photo")
|
.accessibilityLabel("accessibility.editor.button.attach-photo")
|
||||||
.disabled(viewModel.showPoll)
|
.disabled(viewModel.showPoll)
|
||||||
|
|
||||||
|
Button {
|
||||||
|
// all SEVM have the same visibility value
|
||||||
|
followUpSEVMs.append(StatusEditorViewModel(mode: .new(visibility: focusedSEVM.visibility)))
|
||||||
|
} label: {
|
||||||
|
Image(systemName: "arrowshape.turn.up.left.circle.fill")
|
||||||
|
}
|
||||||
|
.disabled(!canAddNewSEVM)
|
||||||
|
|
||||||
Button {
|
Button {
|
||||||
withAnimation {
|
withAnimation {
|
||||||
viewModel.showPoll.toggle()
|
viewModel.showPoll.toggle()
|
||||||
|
@ -98,7 +108,7 @@ struct StatusEditorAccessoryView: View {
|
||||||
withAnimation {
|
withAnimation {
|
||||||
viewModel.spoilerOn.toggle()
|
viewModel.spoilerOn.toggle()
|
||||||
}
|
}
|
||||||
isSpoilerTextFocused.toggle()
|
isSpoilerTextFocused = viewModel.id
|
||||||
} label: {
|
} label: {
|
||||||
Image(systemName: viewModel.spoilerOn ? "exclamationmark.triangle.fill" : "exclamationmark.triangle")
|
Image(systemName: viewModel.spoilerOn ? "exclamationmark.triangle.fill" : "exclamationmark.triangle")
|
||||||
}
|
}
|
||||||
|
@ -180,12 +190,26 @@ struct StatusEditorAccessoryView: View {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private var canAddNewSEVM: Bool {
|
||||||
|
guard followUpSEVMs.count < 5 else { return false }
|
||||||
|
|
||||||
|
if followUpSEVMs.isEmpty, // there is only mainSEVM on the editor
|
||||||
|
!focusedSEVM.statusText.string.isEmpty // focusedSEVM is also mainSEVM
|
||||||
|
{ return true }
|
||||||
|
|
||||||
|
if let lastSEVMs = followUpSEVMs.last,
|
||||||
|
!lastSEVMs.statusText.string.isEmpty
|
||||||
|
{ return true }
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
private var draftsListView: some View {
|
private var draftsListView: some View {
|
||||||
DraftsListView(selectedDraft: .init(get: {
|
DraftsListView(selectedDraft: .init(get: {
|
||||||
nil
|
nil
|
||||||
}, set: { draft in
|
}, set: { draft in
|
||||||
if let draft {
|
if let draft {
|
||||||
viewModel.insertStatusText(text: draft.content)
|
focusedSEVM.insertStatusText(text: draft.content)
|
||||||
}
|
}
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
@ -205,17 +229,17 @@ struct StatusEditorAccessoryView: View {
|
||||||
Button {
|
Button {
|
||||||
Task {
|
Task {
|
||||||
isLoadingAIRequest = true
|
isLoadingAIRequest = true
|
||||||
await viewModel.runOpenAI(prompt: prompt.toRequestPrompt(text: viewModel.statusText.string))
|
await focusedSEVM.runOpenAI(prompt: prompt.toRequestPrompt(text: focusedSEVM.statusText.string))
|
||||||
isLoadingAIRequest = false
|
isLoadingAIRequest = false
|
||||||
}
|
}
|
||||||
} label: {
|
} label: {
|
||||||
prompt.label
|
prompt.label
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if let backup = viewModel.backupStatusText {
|
if let backup = focusedSEVM.backupStatusText {
|
||||||
Button {
|
Button {
|
||||||
viewModel.replaceTextWith(text: backup.string)
|
focusedSEVM.replaceTextWith(text: backup.string)
|
||||||
viewModel.backupStatusText = nil
|
focusedSEVM.backupStatusText = nil
|
||||||
} label: {
|
} label: {
|
||||||
Label("status.editor.restore-previous", systemImage: "arrow.uturn.right")
|
Label("status.editor.restore-previous", systemImage: "arrow.uturn.right")
|
||||||
}
|
}
|
||||||
|
@ -268,15 +292,15 @@ struct StatusEditorAccessoryView: View {
|
||||||
name: language.localizedName
|
name: language.localizedName
|
||||||
).tag(language.isoCode)
|
).tag(language.isoCode)
|
||||||
Spacer()
|
Spacer()
|
||||||
if language.isoCode == viewModel.selectedLanguage {
|
if language.isoCode == focusedSEVM.selectedLanguage {
|
||||||
Image(systemName: "checkmark")
|
Image(systemName: "checkmark")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.listRowBackground(theme.primaryBackgroundColor)
|
.listRowBackground(theme.primaryBackgroundColor)
|
||||||
.contentShape(Rectangle())
|
.contentShape(Rectangle())
|
||||||
.onTapGesture {
|
.onTapGesture {
|
||||||
viewModel.selectedLanguage = language.isoCode
|
focusedSEVM.selectedLanguage = language.isoCode
|
||||||
viewModel.hasExplicitlySelectedLanguage = true
|
focusedSEVM.hasExplicitlySelectedLanguage = true
|
||||||
isLanguageSheetDisplayed = false
|
isLanguageSheetDisplayed = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -285,7 +309,7 @@ struct StatusEditorAccessoryView: View {
|
||||||
private var customEmojisSheet: some View {
|
private var customEmojisSheet: some View {
|
||||||
NavigationStack {
|
NavigationStack {
|
||||||
ScrollView {
|
ScrollView {
|
||||||
ForEach(viewModel.customEmojiContainer) { container in
|
ForEach(focusedSEVM.customEmojiContainer) { container in
|
||||||
VStack(alignment: .leading) {
|
VStack(alignment: .leading) {
|
||||||
Text(container.categoryName)
|
Text(container.categoryName)
|
||||||
.font(.scaledFootnote)
|
.font(.scaledFootnote)
|
||||||
|
@ -308,7 +332,7 @@ struct StatusEditorAccessoryView: View {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.onTapGesture {
|
.onTapGesture {
|
||||||
viewModel.insertStatusText(text: " :\(emoji.shortcode): ")
|
focusedSEVM.insertStatusText(text: " :\(emoji.shortcode): ")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -332,7 +356,7 @@ struct StatusEditorAccessoryView: View {
|
||||||
|
|
||||||
@ViewBuilder
|
@ViewBuilder
|
||||||
private var characterCountView: some View {
|
private var characterCountView: some View {
|
||||||
let value = (currentInstance.instance?.configuration?.statuses.maxCharacters ?? 500) + viewModel.statusTextCharacterLength
|
let value = (currentInstance.instance?.configuration?.statuses.maxCharacters ?? 500) + focusedSEVM.statusTextCharacterLength
|
||||||
|
|
||||||
Text("\(value)")
|
Text("\(value)")
|
||||||
.foregroundColor(value < 0 ? .red : .secondary)
|
.foregroundColor(value < 0 ? .red : .secondary)
|
||||||
|
|
|
@ -105,6 +105,7 @@ struct StatusEditorMediaEditView: View {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.preferredColorScheme(theme.selectedScheme == .dark ? .dark : .light)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -10,7 +10,7 @@ struct StatusEditorMediaView: View {
|
||||||
@Environment(Theme.self) private var theme
|
@Environment(Theme.self) private var theme
|
||||||
@Environment(CurrentInstance.self) private var currentInstance
|
@Environment(CurrentInstance.self) private var currentInstance
|
||||||
var viewModel: StatusEditorViewModel
|
var viewModel: StatusEditorViewModel
|
||||||
@Binding var editingContainer: StatusEditorMediaContainer?
|
@Binding var editingMediaContainer: StatusEditorMediaContainer?
|
||||||
|
|
||||||
@State private var isErrorDisplayed: Bool = false
|
@State private var isErrorDisplayed: Bool = false
|
||||||
|
|
||||||
|
@ -56,9 +56,9 @@ struct StatusEditorMediaView: View {
|
||||||
private var scrollBottomPadding : CGFloat? = 0
|
private var scrollBottomPadding : CGFloat? = 0
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
init(viewModel: StatusEditorViewModel, editingContainer: Binding<StatusEditorMediaContainer?>) {
|
init(viewModel: StatusEditorViewModel, editingMediaContainer: Binding<StatusEditorMediaContainer?>) {
|
||||||
self.viewModel = viewModel
|
self.viewModel = viewModel
|
||||||
self._editingContainer = editingContainer
|
self._editingMediaContainer = editingMediaContainer
|
||||||
}
|
}
|
||||||
|
|
||||||
private func pixel(at index: Int) -> some View {
|
private func pixel(at index: Int) -> some View {
|
||||||
|
@ -175,7 +175,7 @@ struct StatusEditorMediaView: View {
|
||||||
if container.mediaAttachment?.url != nil {
|
if container.mediaAttachment?.url != nil {
|
||||||
if currentInstance.isEditAltTextSupported || !viewModel.mode.isEditing {
|
if currentInstance.isEditAltTextSupported || !viewModel.mode.isEditing {
|
||||||
Button {
|
Button {
|
||||||
editingContainer = container
|
editingMediaContainer = container
|
||||||
} label: {
|
} label: {
|
||||||
Label(container.mediaAttachment?.description?.isEmpty == false ?
|
Label(container.mediaAttachment?.description?.isEmpty == false ?
|
||||||
"status.editor.description.edit" : "status.editor.description.add",
|
"status.editor.description.edit" : "status.editor.description.add",
|
||||||
|
@ -211,7 +211,7 @@ struct StatusEditorMediaView: View {
|
||||||
|
|
||||||
private func makeAltMarker(container: StatusEditorMediaContainer) -> some View {
|
private func makeAltMarker(container: StatusEditorMediaContainer) -> some View {
|
||||||
Button {
|
Button {
|
||||||
editingContainer = container
|
editingMediaContainer = container
|
||||||
} label: {
|
} label: {
|
||||||
Text("status.image.alt-text.abbreviation")
|
Text("status.image.alt-text.abbreviation")
|
||||||
.font(.caption2)
|
.font(.caption2)
|
||||||
|
|
158
Packages/Status/Sources/Status/Editor/StatusEditorCoreView.swift
Normal file
158
Packages/Status/Sources/Status/Editor/StatusEditorCoreView.swift
Normal file
|
@ -0,0 +1,158 @@
|
||||||
|
import SwiftUI
|
||||||
|
import Models
|
||||||
|
import Env
|
||||||
|
import DesignSystem
|
||||||
|
import Accounts
|
||||||
|
import AppAccount
|
||||||
|
import Network
|
||||||
|
|
||||||
|
@MainActor
|
||||||
|
struct StatusEditorCoreView: View {
|
||||||
|
@Bindable var viewModel: StatusEditorViewModel
|
||||||
|
@Binding var followUpSEVMs: [StatusEditorViewModel]
|
||||||
|
@Binding var editingMediaContainer: StatusEditorMediaContainer?
|
||||||
|
|
||||||
|
@FocusState<UUID?>.Binding var isSpoilerTextFocused: UUID?
|
||||||
|
@FocusState<StatusEditorFocusState?>.Binding var editorFocusState: StatusEditorFocusState?
|
||||||
|
let assignedFocusState: StatusEditorFocusState
|
||||||
|
let isMain: Bool
|
||||||
|
|
||||||
|
@Environment(Theme.self) private var theme
|
||||||
|
@Environment(UserPreferences.self) private var preferences
|
||||||
|
@Environment(CurrentAccount.self) private var currentAccount
|
||||||
|
@Environment(AppAccountsManager.self) private var appAccounts
|
||||||
|
@Environment(Client.self) private var client
|
||||||
|
#if targetEnvironment(macCatalyst)
|
||||||
|
@Environment(\.dismissWindow) private var dismissWindow
|
||||||
|
#else
|
||||||
|
@Environment(\.dismiss) private var dismiss
|
||||||
|
#endif
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
HStack(spacing: 0) {
|
||||||
|
if !isMain {
|
||||||
|
Rectangle()
|
||||||
|
.fill(theme.tintColor)
|
||||||
|
.frame(width: 2)
|
||||||
|
.accessibilityHidden(true)
|
||||||
|
.padding(.leading, .layoutPadding)
|
||||||
|
}
|
||||||
|
|
||||||
|
VStack(spacing: 0) {
|
||||||
|
spoilerTextView
|
||||||
|
VStack(spacing: 0) {
|
||||||
|
accountHeaderView
|
||||||
|
textInput
|
||||||
|
StatusEditorMediaView(viewModel: viewModel, editingMediaContainer: $editingMediaContainer)
|
||||||
|
embeddedStatus
|
||||||
|
pollView
|
||||||
|
}
|
||||||
|
.padding(.vertical)
|
||||||
|
|
||||||
|
Divider()
|
||||||
|
}
|
||||||
|
.opacity(editorFocusState == assignedFocusState ? 1 : 0.6)
|
||||||
|
}
|
||||||
|
.background(theme.primaryBackgroundColor)
|
||||||
|
.focused($editorFocusState, equals: assignedFocusState)
|
||||||
|
.onAppear { setupViewModel() }
|
||||||
|
}
|
||||||
|
|
||||||
|
@ViewBuilder
|
||||||
|
private var spoilerTextView: some View {
|
||||||
|
if viewModel.spoilerOn {
|
||||||
|
TextField("status.editor.spoiler", text: $viewModel.spoilerText)
|
||||||
|
.focused($isSpoilerTextFocused, equals: viewModel.id)
|
||||||
|
.padding(.horizontal, .layoutPadding)
|
||||||
|
.padding(.vertical)
|
||||||
|
.background(theme.tintColor.opacity(0.20))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@ViewBuilder
|
||||||
|
private var accountHeaderView: some View {
|
||||||
|
if let account = currentAccount.account, !viewModel.mode.isEditing {
|
||||||
|
HStack {
|
||||||
|
if viewModel.mode.isInShareExtension {
|
||||||
|
AppAccountsSelectorView(routerPath: RouterPath(),
|
||||||
|
accountCreationEnabled: false,
|
||||||
|
avatarConfig: .status)
|
||||||
|
} else {
|
||||||
|
AvatarView(account.avatar, config: AvatarView.FrameConfig.status)
|
||||||
|
.environment(theme)
|
||||||
|
.accessibilityHidden(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
VStack(alignment: .leading, spacing: 4) {
|
||||||
|
StatusEditorPrivacyMenu(visibility: $viewModel.visibility, tint: isMain ? theme.tintColor : .secondary)
|
||||||
|
.disabled(!isMain)
|
||||||
|
|
||||||
|
Text("@\(account.acct)@\(appAccounts.currentClient.server)")
|
||||||
|
.font(.scaledFootnote)
|
||||||
|
.foregroundStyle(.secondary)
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer()
|
||||||
|
|
||||||
|
if case let .followUp(id) = assignedFocusState {
|
||||||
|
Button {
|
||||||
|
followUpSEVMs.removeAll { $0.id == id }
|
||||||
|
} label: {
|
||||||
|
HStack {
|
||||||
|
Image(systemName: "minus.circle.fill").foregroundStyle(.red)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.padding(.horizontal, .layoutPadding)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private var textInput: some View {
|
||||||
|
TextView(
|
||||||
|
$viewModel.statusText,
|
||||||
|
getTextView: { textView in viewModel.textView = textView }
|
||||||
|
)
|
||||||
|
.placeholder(String(localized: isMain ? "status.editor.text.placeholder" : "status.editor.follow-up.text.placeholder"))
|
||||||
|
.setKeyboardType(preferences.isSocialKeyboardEnabled ? .twitter : .default)
|
||||||
|
.padding(.horizontal, .layoutPadding)
|
||||||
|
.padding(.vertical)
|
||||||
|
}
|
||||||
|
|
||||||
|
@ViewBuilder
|
||||||
|
private var embeddedStatus: some View {
|
||||||
|
if viewModel.replyToStatus != nil { Divider().padding(.top, 20) }
|
||||||
|
|
||||||
|
if let status = viewModel.embeddedStatus ?? viewModel.replyToStatus {
|
||||||
|
StatusEmbeddedView(status: status, client: client, routerPath: RouterPath())
|
||||||
|
.padding(.horizontal, .layoutPadding)
|
||||||
|
.disabled(true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@ViewBuilder
|
||||||
|
private var pollView: some View {
|
||||||
|
if viewModel.showPoll {
|
||||||
|
StatusEditorPollView(viewModel: viewModel, showPoll: $viewModel.showPoll)
|
||||||
|
.padding(.horizontal)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func setupViewModel() {
|
||||||
|
viewModel.client = client
|
||||||
|
viewModel.currentAccount = currentAccount.account
|
||||||
|
viewModel.theme = theme
|
||||||
|
viewModel.preferences = preferences
|
||||||
|
viewModel.prepareStatusText()
|
||||||
|
if !client.isAuth {
|
||||||
|
#if targetEnvironment(macCatalyst)
|
||||||
|
dismissWindow()
|
||||||
|
#else
|
||||||
|
dismiss()
|
||||||
|
#endif
|
||||||
|
NotificationCenter.default.post(name: .shareSheetClose, object: nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
Task { await viewModel.fetchCustomEmojis() }
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,5 @@
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
enum StatusEditorFocusState: Hashable {
|
||||||
|
case main, followUp(index: UUID)
|
||||||
|
}
|
|
@ -0,0 +1,32 @@
|
||||||
|
import SwiftUI
|
||||||
|
import Models
|
||||||
|
|
||||||
|
struct StatusEditorPrivacyMenu: View {
|
||||||
|
@Binding var visibility: Models.Visibility
|
||||||
|
let tint: Color
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
Menu {
|
||||||
|
ForEach(Models.Visibility.allCases, id: \.self) { vis in
|
||||||
|
Button { self.visibility = vis } label: {
|
||||||
|
Label(vis.title, systemImage: vis.iconName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} label: {
|
||||||
|
HStack {
|
||||||
|
Label(visibility.title, systemImage: visibility.iconName)
|
||||||
|
.accessibilityLabel("accessibility.editor.privacy.label")
|
||||||
|
.accessibilityValue(visibility.title)
|
||||||
|
.accessibilityHint("accessibility.editor.privacy.hint")
|
||||||
|
Image(systemName: "chevron.down")
|
||||||
|
}
|
||||||
|
.font(.scaledFootnote)
|
||||||
|
.padding(4)
|
||||||
|
.overlay(
|
||||||
|
RoundedRectangle(cornerRadius: 8)
|
||||||
|
.stroke(tint, lineWidth: 1)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,136 @@
|
||||||
|
import SwiftUI
|
||||||
|
import Env
|
||||||
|
import Models
|
||||||
|
import StoreKit
|
||||||
|
|
||||||
|
@MainActor
|
||||||
|
struct StatusEditorToolbarItems: ToolbarContent {
|
||||||
|
@State private var isLanguageConfirmPresented = false
|
||||||
|
@State private var isDismissAlertPresented: Bool = false
|
||||||
|
let mainSEVM: StatusEditorViewModel
|
||||||
|
let followUpSEVMs: [StatusEditorViewModel]
|
||||||
|
|
||||||
|
@Environment(\.modelContext) private var context
|
||||||
|
@Environment(UserPreferences.self) private var preferences
|
||||||
|
#if targetEnvironment(macCatalyst)
|
||||||
|
@Environment(\.dismissWindow) private var dismissWindow
|
||||||
|
#else
|
||||||
|
@Environment(\.dismiss) private var dismiss
|
||||||
|
#endif
|
||||||
|
|
||||||
|
var body: some ToolbarContent {
|
||||||
|
ToolbarItem(placement: .navigationBarTrailing) {
|
||||||
|
Button {
|
||||||
|
Task {
|
||||||
|
mainSEVM.evaluateLanguages()
|
||||||
|
if preferences.autoDetectPostLanguage, let _ = mainSEVM.languageConfirmationDialogLanguages {
|
||||||
|
isLanguageConfirmPresented = true
|
||||||
|
} else {
|
||||||
|
await postAllStatus()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} label: {
|
||||||
|
if mainSEVM.isPosting {
|
||||||
|
ProgressView()
|
||||||
|
} else {
|
||||||
|
Text("status.action.post").bold()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.disabled(!mainSEVM.canPost)
|
||||||
|
.keyboardShortcut(.return, modifiers: .command)
|
||||||
|
.confirmationDialog("", isPresented: $isLanguageConfirmPresented, actions: {
|
||||||
|
languageConfirmationDialog
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
ToolbarItem(placement: .navigationBarLeading) {
|
||||||
|
Button {
|
||||||
|
if mainSEVM.shouldDisplayDismissWarning {
|
||||||
|
isDismissAlertPresented = true
|
||||||
|
} else {
|
||||||
|
close()
|
||||||
|
NotificationCenter.default.post(name: .shareSheetClose,
|
||||||
|
object: nil)
|
||||||
|
}
|
||||||
|
} label: {
|
||||||
|
Text("action.cancel")
|
||||||
|
}
|
||||||
|
.keyboardShortcut(.cancelAction)
|
||||||
|
.confirmationDialog(
|
||||||
|
"",
|
||||||
|
isPresented: $isDismissAlertPresented,
|
||||||
|
actions: {
|
||||||
|
Button("status.draft.delete", role: .destructive) {
|
||||||
|
close()
|
||||||
|
NotificationCenter.default.post(name: .shareSheetClose,
|
||||||
|
object: nil)
|
||||||
|
}
|
||||||
|
Button("status.draft.save") {
|
||||||
|
context.insert(Draft(content: mainSEVM.statusText.string))
|
||||||
|
close()
|
||||||
|
NotificationCenter.default.post(name: .shareSheetClose,
|
||||||
|
object: nil)
|
||||||
|
}
|
||||||
|
Button("action.cancel", role: .cancel) {}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@discardableResult
|
||||||
|
private func postStatus(with model: StatusEditorViewModel, isMainPost: Bool) async -> Status? {
|
||||||
|
let status = await model.postStatus()
|
||||||
|
|
||||||
|
if status != nil && isMainPost {
|
||||||
|
close()
|
||||||
|
SoundEffectManager.shared.playSound(.tootSent)
|
||||||
|
NotificationCenter.default.post(name: .shareSheetClose, object: nil)
|
||||||
|
#if !targetEnvironment(macCatalyst)
|
||||||
|
if !mainSEVM.mode.isInShareExtension, !preferences.requestedReview {
|
||||||
|
if let scene = UIApplication.shared.connectedScenes.first(where: { $0.activationState == .foregroundActive }) as? UIWindowScene {
|
||||||
|
SKStoreReviewController.requestReview(in: scene)
|
||||||
|
}
|
||||||
|
preferences.requestedReview = true
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
return status
|
||||||
|
}
|
||||||
|
|
||||||
|
private func postAllStatus() async {
|
||||||
|
guard let openingPost = await postStatus(with: mainSEVM, isMainPost: true) else { return }
|
||||||
|
for p in followUpSEVMs {
|
||||||
|
p.mode = .replyTo(status: openingPost)
|
||||||
|
await postStatus(with: p, isMainPost: false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#if targetEnvironment(macCatalyst)
|
||||||
|
private func close() { dismissWindow() }
|
||||||
|
#else
|
||||||
|
private func close() { dismiss() }
|
||||||
|
#endif
|
||||||
|
|
||||||
|
@ViewBuilder
|
||||||
|
private var languageConfirmationDialog: some View {
|
||||||
|
if let (detected: detected, selected: selected) = mainSEVM.languageConfirmationDialogLanguages,
|
||||||
|
let detectedLong = Locale.current.localizedString(forLanguageCode: detected),
|
||||||
|
let selectedLong = Locale.current.localizedString(forLanguageCode: selected)
|
||||||
|
{
|
||||||
|
Button("status.editor.language-select.confirmation.detected-\(detectedLong)") {
|
||||||
|
mainSEVM.selectedLanguage = detected
|
||||||
|
Task { await postAllStatus() }
|
||||||
|
}
|
||||||
|
Button("status.editor.language-select.confirmation.selected-\(selectedLong)") {
|
||||||
|
mainSEVM.selectedLanguage = selected
|
||||||
|
Task { await postAllStatus() }
|
||||||
|
}
|
||||||
|
Button("action.cancel", role: .cancel) {
|
||||||
|
mainSEVM.languageConfirmationDialogLanguages = nil
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
EmptyView()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -14,290 +14,121 @@ import UIKit
|
||||||
@MainActor
|
@MainActor
|
||||||
public struct StatusEditorView: View {
|
public struct StatusEditorView: View {
|
||||||
@Environment(AppAccountsManager.self) private var appAccounts
|
@Environment(AppAccountsManager.self) private var appAccounts
|
||||||
@Environment(UserPreferences.self) private var preferences
|
|
||||||
@Environment(Theme.self) private var theme
|
|
||||||
@Environment(Client.self) private var client
|
|
||||||
@Environment(CurrentAccount.self) private var currentAccount
|
@Environment(CurrentAccount.self) private var currentAccount
|
||||||
@Environment(\.dismiss) private var dismiss
|
@Environment(Theme.self) private var theme
|
||||||
@Environment(\.dismissWindow) private var dismissWindow
|
|
||||||
@Environment(\.modelContext) private var context
|
|
||||||
|
|
||||||
@State private var viewModel: StatusEditorViewModel
|
@State private var mainSEVM: StatusEditorViewModel
|
||||||
@FocusState private var isSpoilerTextFocused: Bool
|
@State private var followUpSEVMs: [StatusEditorViewModel] = []
|
||||||
|
@FocusState private var isSpoilerTextFocused: UUID? // connect CoreEditor and StatusEditorAccessoryView
|
||||||
|
@State private var editingMediaContainer: StatusEditorMediaContainer?
|
||||||
|
@State private var scrollID: UUID?
|
||||||
|
|
||||||
@State private var isDismissAlertPresented: Bool = false
|
@FocusState private var editorFocusState: StatusEditorFocusState?
|
||||||
@State private var isLanguageConfirmPresented = false
|
|
||||||
|
private var focusedSEVM: StatusEditorViewModel {
|
||||||
|
if case let .followUp(id) = editorFocusState,
|
||||||
|
let sevm = followUpSEVMs.first(where: { $0.id == id })
|
||||||
|
{ return sevm }
|
||||||
|
|
||||||
@State private var editingContainer: StatusEditorMediaContainer?
|
return mainSEVM
|
||||||
|
}
|
||||||
|
|
||||||
public init(mode: StatusEditorViewModel.Mode) {
|
public init(mode: StatusEditorViewModel.Mode) {
|
||||||
_viewModel = .init(initialValue: .init(mode: mode))
|
_mainSEVM = State(initialValue: StatusEditorViewModel(mode: mode))
|
||||||
}
|
}
|
||||||
|
|
||||||
public var body: some View {
|
public var body: some View {
|
||||||
|
@Bindable var focusedSEVM = self.focusedSEVM
|
||||||
|
|
||||||
NavigationStack {
|
NavigationStack {
|
||||||
ZStack(alignment: .bottom) {
|
ScrollView {
|
||||||
ScrollView {
|
VStackLayout(spacing: 0) {
|
||||||
Divider()
|
StatusEditorCoreView(
|
||||||
spoilerTextView
|
viewModel: mainSEVM,
|
||||||
VStack(spacing: 12) {
|
followUpSEVMs: $followUpSEVMs,
|
||||||
accountHeaderView
|
editingMediaContainer: $editingMediaContainer,
|
||||||
.padding(.horizontal, .layoutPadding)
|
isSpoilerTextFocused: $isSpoilerTextFocused,
|
||||||
TextView($viewModel.statusText,
|
editorFocusState: $editorFocusState,
|
||||||
getTextView: { textView in
|
assignedFocusState: .main,
|
||||||
viewModel.textView = textView
|
isMain: true
|
||||||
})
|
|
||||||
.placeholder(String(localized: "status.editor.text.placeholder"))
|
|
||||||
.setKeyboardType(preferences.isSocialKeyboardEnabled ? .twitter : .default)
|
|
||||||
.padding(.horizontal, .layoutPadding)
|
|
||||||
StatusEditorMediaView(viewModel: viewModel,
|
|
||||||
editingContainer: $editingContainer)
|
|
||||||
if let status = viewModel.embeddedStatus {
|
|
||||||
StatusEmbeddedView(status: status, client: client, routerPath: RouterPath())
|
|
||||||
.padding(.horizontal, .layoutPadding)
|
|
||||||
.disabled(true)
|
|
||||||
} else if let status = viewModel.replyToStatus {
|
|
||||||
Divider()
|
|
||||||
.padding(.top, 20)
|
|
||||||
StatusEmbeddedView(status: status, client: client, routerPath: RouterPath())
|
|
||||||
.padding(.horizontal, .layoutPadding)
|
|
||||||
.disabled(true)
|
|
||||||
}
|
|
||||||
if viewModel.showPoll {
|
|
||||||
StatusEditorPollView(viewModel: viewModel, showPoll: $viewModel.showPoll)
|
|
||||||
.padding(.horizontal)
|
|
||||||
}
|
|
||||||
Spacer()
|
|
||||||
}
|
|
||||||
.padding(.top, 8)
|
|
||||||
.padding(.bottom, 40)
|
|
||||||
}
|
|
||||||
.accessibilitySortPriority(1) // Ensure that all elements inside the `ScrollView` occur earlier than the accessory views
|
|
||||||
.padding(.top, 1) // hacky fix for weird SwiftUI scrollView bug when adding padding
|
|
||||||
.padding(.bottom, 48)
|
|
||||||
VStack(alignment: .leading, spacing: 0) {
|
|
||||||
StatusEditorAutoCompleteView(viewModel: viewModel)
|
|
||||||
StatusEditorAccessoryView(isSpoilerTextFocused: $isSpoilerTextFocused,
|
|
||||||
viewModel: viewModel)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.onDrop(of: StatusEditorUTTypeSupported.types(), delegate: viewModel)
|
|
||||||
.onAppear {
|
|
||||||
viewModel.client = client
|
|
||||||
viewModel.currentAccount = currentAccount.account
|
|
||||||
viewModel.theme = theme
|
|
||||||
viewModel.preferences = preferences
|
|
||||||
viewModel.prepareStatusText()
|
|
||||||
if !client.isAuth {
|
|
||||||
close()
|
|
||||||
NotificationCenter.default.post(name: .shareSheetClose,
|
|
||||||
object: nil)
|
|
||||||
}
|
|
||||||
|
|
||||||
Task {
|
|
||||||
await viewModel.fetchCustomEmojis()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.onChange(of: currentAccount.account?.id) {
|
|
||||||
viewModel.currentAccount = currentAccount.account
|
|
||||||
}
|
|
||||||
.background(theme.primaryBackgroundColor)
|
|
||||||
.navigationTitle(viewModel.mode.title)
|
|
||||||
.navigationBarTitleDisplayMode(.inline)
|
|
||||||
.toolbarBackground(.visible, for: .navigationBar)
|
|
||||||
.alert("status.error.posting.title",
|
|
||||||
isPresented: $viewModel.showPostingErrorAlert,
|
|
||||||
actions: {
|
|
||||||
Button("OK") {}
|
|
||||||
}, message: {
|
|
||||||
Text(viewModel.postingError ?? "")
|
|
||||||
})
|
|
||||||
.toolbar {
|
|
||||||
ToolbarItem(placement: .navigationBarTrailing) {
|
|
||||||
Button {
|
|
||||||
Task {
|
|
||||||
viewModel.evaluateLanguages()
|
|
||||||
if preferences.autoDetectPostLanguage, let _ = viewModel.languageConfirmationDialogLanguages {
|
|
||||||
isLanguageConfirmPresented = true
|
|
||||||
} else {
|
|
||||||
await postStatus()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} label: {
|
|
||||||
if viewModel.isPosting {
|
|
||||||
ProgressView()
|
|
||||||
} else {
|
|
||||||
Text("status.action.post").bold()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.disabled(!viewModel.canPost)
|
|
||||||
.keyboardShortcut(.return, modifiers: .command)
|
|
||||||
.confirmationDialog("", isPresented: $isLanguageConfirmPresented, actions: {
|
|
||||||
languageConfirmationDialog
|
|
||||||
})
|
|
||||||
}
|
|
||||||
ToolbarItem(placement: .navigationBarLeading) {
|
|
||||||
Button {
|
|
||||||
if viewModel.shouldDisplayDismissWarning {
|
|
||||||
isDismissAlertPresented = true
|
|
||||||
} else {
|
|
||||||
close()
|
|
||||||
NotificationCenter.default.post(name: .shareSheetClose,
|
|
||||||
object: nil)
|
|
||||||
}
|
|
||||||
} label: {
|
|
||||||
Text("action.cancel")
|
|
||||||
}
|
|
||||||
.keyboardShortcut(.cancelAction)
|
|
||||||
.confirmationDialog(
|
|
||||||
"",
|
|
||||||
isPresented: $isDismissAlertPresented,
|
|
||||||
actions: {
|
|
||||||
Button("status.draft.delete", role: .destructive) {
|
|
||||||
close()
|
|
||||||
NotificationCenter.default.post(name: .shareSheetClose,
|
|
||||||
object: nil)
|
|
||||||
}
|
|
||||||
Button("status.draft.save") {
|
|
||||||
context.insert(Draft(content: viewModel.statusText.string))
|
|
||||||
close()
|
|
||||||
NotificationCenter.default.post(name: .shareSheetClose,
|
|
||||||
object: nil)
|
|
||||||
}
|
|
||||||
Button("action.cancel", role: .cancel) {}
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
}
|
.id(mainSEVM.id)
|
||||||
}
|
|
||||||
}
|
|
||||||
.sheet(item: $editingContainer) { container in
|
|
||||||
StatusEditorMediaEditView(viewModel: viewModel, container: container)
|
|
||||||
.preferredColorScheme(theme.selectedScheme == .dark ? .dark : .light)
|
|
||||||
}
|
|
||||||
.interactiveDismissDisabled(viewModel.shouldDisplayDismissWarning)
|
|
||||||
.onChange(of: appAccounts.currentClient) { _, newValue in
|
|
||||||
if viewModel.mode.isInShareExtension {
|
|
||||||
currentAccount.setClient(client: newValue)
|
|
||||||
viewModel.client = newValue
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@ViewBuilder
|
ForEach(followUpSEVMs) { sevm in
|
||||||
private var languageConfirmationDialog: some View {
|
@Bindable var sevm: StatusEditorViewModel = sevm
|
||||||
if let (detected: detected, selected: selected) = viewModel.languageConfirmationDialogLanguages,
|
|
||||||
let detectedLong = Locale.current.localizedString(forLanguageCode: detected),
|
|
||||||
let selectedLong = Locale.current.localizedString(forLanguageCode: selected)
|
|
||||||
{
|
|
||||||
Button("status.editor.language-select.confirmation.detected-\(detectedLong)") {
|
|
||||||
viewModel.selectedLanguage = detected
|
|
||||||
Task {
|
|
||||||
await postStatus()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Button("status.editor.language-select.confirmation.selected-\(selectedLong)") {
|
|
||||||
viewModel.selectedLanguage = selected
|
|
||||||
Task {
|
|
||||||
await postStatus()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Button("action.cancel", role: .cancel) {
|
|
||||||
viewModel.languageConfirmationDialogLanguages = nil
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
EmptyView()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private func postStatus() async {
|
StatusEditorCoreView(
|
||||||
let status = await viewModel.postStatus()
|
viewModel: sevm,
|
||||||
if status != nil {
|
followUpSEVMs: $followUpSEVMs,
|
||||||
close()
|
editingMediaContainer: $editingMediaContainer,
|
||||||
SoundEffectManager.shared.playSound(.tootSent)
|
isSpoilerTextFocused: $isSpoilerTextFocused,
|
||||||
NotificationCenter.default.post(name: .shareSheetClose,
|
editorFocusState: $editorFocusState,
|
||||||
object: nil)
|
assignedFocusState: .followUp(index: sevm.id),
|
||||||
#if !targetEnvironment(macCatalyst)
|
isMain: false
|
||||||
if !viewModel.mode.isInShareExtension, !preferences.requestedReview {
|
)
|
||||||
if let scene = UIApplication.shared.connectedScenes.first(where: { $0.activationState == .foregroundActive }) as? UIWindowScene {
|
.id(sevm.id)
|
||||||
SKStoreReviewController.requestReview(in: scene)
|
}
|
||||||
}
|
}
|
||||||
preferences.requestedReview = true
|
.scrollTargetLayout()
|
||||||
}
|
}
|
||||||
#endif
|
.scrollPosition(id: $scrollID, anchor: .top)
|
||||||
}
|
.animation(.bouncy(duration: 0.3), value: editorFocusState)
|
||||||
}
|
.animation(.bouncy(duration: 0.3), value: followUpSEVMs)
|
||||||
|
.background(Color.primaryBackground)
|
||||||
@ViewBuilder
|
.safeAreaInset(edge: .bottom) {
|
||||||
private var spoilerTextView: some View {
|
StatusEditorAutoCompleteView(viewModel: focusedSEVM)
|
||||||
if viewModel.spoilerOn {
|
|
||||||
VStack {
|
|
||||||
TextField("status.editor.spoiler", text: $viewModel.spoilerText)
|
|
||||||
.focused($isSpoilerTextFocused)
|
|
||||||
.padding(.horizontal, .layoutPadding)
|
|
||||||
}
|
}
|
||||||
.frame(height: 35)
|
.safeAreaInset(edge: .bottom) {
|
||||||
.background(theme.tintColor.opacity(0.20))
|
StatusEditorAccessoryView(isSpoilerTextFocused: $isSpoilerTextFocused, focusedSEVM: focusedSEVM, followUpSEVMs: $followUpSEVMs)
|
||||||
.offset(y: -8)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@ViewBuilder
|
|
||||||
private var accountHeaderView: some View {
|
|
||||||
if let account = currentAccount.account, !viewModel.mode.isEditing {
|
|
||||||
HStack {
|
|
||||||
if viewModel.mode.isInShareExtension {
|
|
||||||
AppAccountsSelectorView(routerPath: RouterPath(),
|
|
||||||
accountCreationEnabled: false,
|
|
||||||
avatarConfig: .status)
|
|
||||||
} else {
|
|
||||||
AvatarView(account.avatar, config: AvatarView.FrameConfig.status)
|
|
||||||
.environment(theme)
|
|
||||||
.accessibilityHidden(true)
|
|
||||||
}
|
|
||||||
VStack(alignment: .leading, spacing: 4) {
|
|
||||||
privacyMenu
|
|
||||||
Text("@\(account.acct)@\(appAccounts.currentClient.server)")
|
|
||||||
.font(.scaledFootnote)
|
|
||||||
.foregroundStyle(.secondary)
|
|
||||||
}
|
|
||||||
Spacer()
|
|
||||||
}
|
}
|
||||||
}
|
.accessibilitySortPriority(1) // Ensure that all elements inside the `ScrollView` occur earlier than the accessory views
|
||||||
}
|
.navigationTitle(focusedSEVM.mode.title)
|
||||||
|
.navigationBarTitleDisplayMode(.inline)
|
||||||
private var privacyMenu: some View {
|
.toolbar { StatusEditorToolbarItems(mainSEVM: mainSEVM, followUpSEVMs: followUpSEVMs) }
|
||||||
Menu {
|
.toolbarBackground(.visible, for: .navigationBar)
|
||||||
Section("status.editor.visibility") {
|
.alert(
|
||||||
ForEach(Models.Visibility.allCases, id: \.self) { visibility in
|
"status.error.posting.title",
|
||||||
Button {
|
isPresented: $focusedSEVM.showPostingErrorAlert,
|
||||||
viewModel.visibility = visibility
|
actions: {
|
||||||
} label: {
|
Button("OK") {}
|
||||||
Label(visibility.title, systemImage: visibility.iconName)
|
}, message: {
|
||||||
|
Text(mainSEVM.postingError ?? "")
|
||||||
|
})
|
||||||
|
.interactiveDismissDisabled(mainSEVM.shouldDisplayDismissWarning)
|
||||||
|
.onChange(of: appAccounts.currentClient) { _, newValue in
|
||||||
|
if mainSEVM.mode.isInShareExtension {
|
||||||
|
currentAccount.setClient(client: newValue)
|
||||||
|
mainSEVM.client = newValue
|
||||||
|
for post in followUpSEVMs {
|
||||||
|
post.client = newValue
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} label: {
|
.onDrop(of: StatusEditorUTTypeSupported.types(), delegate: focusedSEVM)
|
||||||
HStack {
|
.onChange(of: currentAccount.account?.id) {
|
||||||
Label(viewModel.visibility.title, systemImage: viewModel.visibility.iconName)
|
mainSEVM.currentAccount = currentAccount.account
|
||||||
.accessibilityLabel("accessibility.editor.privacy.label")
|
for p in followUpSEVMs {
|
||||||
.accessibilityValue(viewModel.visibility.title)
|
p.currentAccount = mainSEVM.currentAccount
|
||||||
.accessibilityHint("accessibility.editor.privacy.hint")
|
}
|
||||||
Image(systemName: "chevron.down")
|
}
|
||||||
|
.onChange(of: mainSEVM.visibility) {
|
||||||
|
for p in followUpSEVMs {
|
||||||
|
p.visibility = mainSEVM.visibility
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.onChange(of: followUpSEVMs.count) { oldValue, newValue in
|
||||||
|
if oldValue < newValue {
|
||||||
|
Task {
|
||||||
|
try? await Task.sleep(for: .seconds(0.1))
|
||||||
|
withAnimation(.bouncy(duration: 0.5)) {
|
||||||
|
scrollID = followUpSEVMs.last?.id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
.font(.scaledFootnote)
|
|
||||||
.padding(4)
|
|
||||||
.overlay(
|
|
||||||
RoundedRectangle(cornerRadius: 8)
|
|
||||||
.stroke(theme.tintColor, lineWidth: 1)
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
.sheet(item: $editingMediaContainer) { container in
|
||||||
|
StatusEditorMediaEditView(viewModel: focusedSEVM, container: container)
|
||||||
private func close() {
|
}
|
||||||
#if targetEnvironment(macCatalyst)
|
|
||||||
dismissWindow()
|
|
||||||
#else
|
|
||||||
dismiss()
|
|
||||||
#endif
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,7 +8,9 @@ import PhotosUI
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
@MainActor
|
@MainActor
|
||||||
@Observable public class StatusEditorViewModel: NSObject {
|
@Observable public class StatusEditorViewModel: NSObject, Identifiable {
|
||||||
|
public let id = UUID()
|
||||||
|
|
||||||
var mode: Mode
|
var mode: Mode
|
||||||
|
|
||||||
var client: Client?
|
var client: Client?
|
||||||
|
|
Loading…
Reference in a new issue