2024-01-06 17:43:26 +00:00
|
|
|
import AppAccount
|
|
|
|
import DesignSystem
|
|
|
|
import Env
|
|
|
|
import Models
|
|
|
|
import Network
|
|
|
|
import SwiftUI
|
|
|
|
|
|
|
|
extension StatusEditor {
|
|
|
|
@MainActor
|
|
|
|
struct EditorView: View {
|
|
|
|
@Environment(Theme.self) private var theme
|
|
|
|
@Environment(UserPreferences.self) private var preferences
|
|
|
|
@Environment(CurrentAccount.self) private var currentAccount
|
|
|
|
@Environment(CurrentInstance.self) private var currentInstance
|
|
|
|
@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
|
2024-01-06 21:26:12 +00:00
|
|
|
|
|
|
|
@Bindable var viewModel: ViewModel
|
|
|
|
@Binding var followUpSEVMs: [ViewModel]
|
|
|
|
@Binding var editingMediaContainer: MediaContainer?
|
|
|
|
|
|
|
|
@FocusState<UUID?>.Binding var isSpoilerTextFocused: UUID?
|
|
|
|
@FocusState<EditorFocusState?>.Binding var editorFocusState: EditorFocusState?
|
|
|
|
let assignedFocusState: EditorFocusState
|
|
|
|
let isMain: Bool
|
2024-01-06 17:43:26 +00:00
|
|
|
|
|
|
|
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
|
2024-01-07 06:03:39 +00:00
|
|
|
pollView
|
2024-01-06 21:26:12 +00:00
|
|
|
characterCountAndLangView
|
2024-01-06 17:43:26 +00:00
|
|
|
MediaView(viewModel: viewModel, editingMediaContainer: $editingMediaContainer)
|
|
|
|
embeddedStatus
|
|
|
|
}
|
|
|
|
.padding(.vertical)
|
|
|
|
|
|
|
|
Divider()
|
|
|
|
}
|
|
|
|
.opacity(editorFocusState == assignedFocusState ? 1 : 0.6)
|
|
|
|
}
|
|
|
|
#if !os(visionOS)
|
|
|
|
.background(theme.primaryBackgroundColor)
|
|
|
|
#endif
|
|
|
|
.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) {
|
|
|
|
PrivacyMenu(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 {
|
2024-01-07 06:03:39 +00:00
|
|
|
if let status = viewModel.replyToStatus {
|
|
|
|
Divider().padding(.vertical, .statusComponentSpacing)
|
|
|
|
StatusRowView(viewModel: .init(status: status,
|
|
|
|
client: client,
|
|
|
|
routerPath: RouterPath(),
|
|
|
|
showActions: false))
|
|
|
|
.accessibilityLabel(status.content.asRawText)
|
2024-01-07 08:37:18 +00:00
|
|
|
.environment(RouterPath())
|
2024-01-07 06:03:39 +00:00
|
|
|
.allowsHitTesting(false)
|
|
|
|
.environment(\.isStatusFocused, false)
|
|
|
|
.padding(.horizontal, .layoutPadding)
|
|
|
|
.padding(.vertical, .statusComponentSpacing)
|
|
|
|
|
|
|
|
} else if let status = viewModel.embeddedStatus {
|
2024-01-06 17:43:26 +00:00
|
|
|
StatusEmbeddedView(status: status, client: client, routerPath: RouterPath())
|
|
|
|
.padding(.horizontal, .layoutPadding)
|
|
|
|
.disabled(true)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
@ViewBuilder
|
|
|
|
private var pollView: some View {
|
|
|
|
if viewModel.showPoll {
|
|
|
|
PollView(viewModel: viewModel, showPoll: $viewModel.showPoll)
|
|
|
|
.padding(.horizontal)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
@ViewBuilder
|
2024-01-06 21:26:12 +00:00
|
|
|
private var characterCountAndLangView: some View {
|
2024-01-06 17:43:26 +00:00
|
|
|
let value = (currentInstance.instance?.configuration?.statuses.maxCharacters ?? 500) + viewModel.statusTextCharacterLength
|
2024-01-06 21:26:12 +00:00
|
|
|
HStack(alignment: .center) {
|
2024-01-07 06:03:39 +00:00
|
|
|
LangButton(viewModel: viewModel)
|
2024-01-06 19:02:16 +00:00
|
|
|
.padding(.leading, .layoutPadding)
|
2024-01-06 21:26:12 +00:00
|
|
|
|
|
|
|
Button {
|
2024-01-07 06:03:39 +00:00
|
|
|
withAnimation {
|
|
|
|
viewModel.showPoll.toggle()
|
|
|
|
viewModel.resetPollDefaults()
|
2024-01-06 21:26:12 +00:00
|
|
|
}
|
2024-01-07 06:03:39 +00:00
|
|
|
} label: {
|
|
|
|
Image(systemName: viewModel.showPoll ? "chart.bar.fill" : "chart.bar")
|
2024-01-06 21:26:12 +00:00
|
|
|
}
|
|
|
|
.buttonStyle(.bordered)
|
2024-01-07 06:03:39 +00:00
|
|
|
.accessibilityLabel("accessibility.editor.button.poll")
|
|
|
|
.disabled(viewModel.shouldDisablePollButton)
|
|
|
|
|
|
|
|
Button {
|
|
|
|
withAnimation {
|
|
|
|
viewModel.spoilerOn.toggle()
|
2024-01-06 21:26:12 +00:00
|
|
|
}
|
2024-01-07 06:03:39 +00:00
|
|
|
isSpoilerTextFocused = viewModel.id
|
|
|
|
} label: {
|
|
|
|
Image(systemName: viewModel.spoilerOn ? "exclamationmark.triangle.fill" : "exclamationmark.triangle")
|
2024-01-06 21:26:12 +00:00
|
|
|
}
|
2024-01-07 06:03:39 +00:00
|
|
|
.buttonStyle(.bordered)
|
|
|
|
.accessibilityLabel("accessibility.editor.button.spoiler")
|
2024-01-06 21:26:12 +00:00
|
|
|
|
2024-01-06 19:02:16 +00:00
|
|
|
Spacer()
|
2024-01-07 06:03:39 +00:00
|
|
|
|
|
|
|
Text("\(value)")
|
|
|
|
.foregroundColor(value < 0 ? .red : .secondary)
|
|
|
|
.font(.callout.monospacedDigit())
|
|
|
|
.contentTransition(.numericText(value: Double(value)))
|
|
|
|
.animation(.default, value: value)
|
|
|
|
.accessibilityLabel("accessibility.editor.button.characters-remaining")
|
|
|
|
.accessibilityValue("\(value)")
|
|
|
|
.accessibilityRemoveTraits(.isStaticText)
|
|
|
|
.accessibilityAddTraits(.updatesFrequently)
|
|
|
|
.accessibilityRespondsToUserInteraction(false)
|
|
|
|
.padding(.trailing, .layoutPadding)
|
2024-01-06 17:43:26 +00:00
|
|
|
}
|
2024-01-07 06:03:39 +00:00
|
|
|
.padding(.vertical, 8)
|
2024-01-06 21:26:12 +00:00
|
|
|
}
|
|
|
|
|
2024-01-06 17:43:26 +00:00
|
|
|
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() }
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|