IceCubesApp/Packages/Status/Sources/Status/Editor/StatusEditorView.swift
Hidemune Takahashi ae3d190799
Added marked text range support (#345)
* 👍 Added markedTextRangeSupport.

* 👍 ignore when markedTextRange is non-nil.

* Update TextView revision

Co-authored-by: Hidemune Takahashi <h1d3mun3.74k4h45h1@gmail.com>
Co-authored-by: Thomas Ricouard <ricouard77@gmail.com>
2023-01-24 17:26:56 +01:00

251 lines
8.2 KiB
Swift

import Accounts
import AppAccount
import DesignSystem
import EmojiText
import Env
import Models
import Network
import NukeUI
import PhotosUI
import SwiftUI
import TextView
import UIKit
public struct StatusEditorView: View {
@EnvironmentObject private var preferences: UserPreferences
@EnvironmentObject private var theme: Theme
@EnvironmentObject private var client: Client
@EnvironmentObject private var currentAccount: CurrentAccount
@Environment(\.dismiss) private var dismiss
@StateObject private var viewModel: StatusEditorViewModel
@FocusState private var isSpoilerTextFocused: Bool
@State private var isDismissAlertPresented: Bool = false
@State private var isLoadingAIRequest: Bool = false
public init(mode: StatusEditorViewModel.Mode) {
_viewModel = StateObject(wrappedValue: .init(mode: mode))
}
public var body: some View {
NavigationStack {
ZStack(alignment: .bottom) {
ScrollView {
Divider()
spoilerTextView
VStack(spacing: 12) {
accountHeaderView
.padding(.horizontal, .layoutPadding)
TextView($viewModel.statusText, $viewModel.selectedRange, $viewModel.markedTextRange)
.placeholder(String(localized: "status.editor.text.placeholder"))
.font(Font.scaledBodyUIFont)
.keyboardType(.twitter)
.padding(.horizontal, .layoutPadding)
StatusEditorMediaView(viewModel: viewModel)
if let status = viewModel.embeddedStatus {
StatusEmbeddedView(status: status)
.padding(.horizontal, .layoutPadding)
.disabled(true)
} else if let status = viewModel.replyToStatus {
Divider()
.padding(.top, 20)
StatusEmbeddedView(status: status)
.padding(.horizontal, .layoutPadding)
.disabled(true)
}
if viewModel.showPoll {
StatusEditorPollView(viewModel: viewModel, showPoll: $viewModel.showPoll)
.padding(.horizontal)
}
Spacer()
}
.padding(.top, 8)
.padding(.bottom, 40)
}
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.prepareStatusText()
if !client.isAuth {
dismiss()
NotificationCenter.default.post(name: NotificationsName.shareSheetClose,
object: nil)
}
Task {
await viewModel.fetchCustomEmojis()
}
}
.onChange(of: currentAccount.account?.id, perform: { _ in
viewModel.client = client
viewModel.currentAccount = currentAccount.account
})
.background(theme.primaryBackgroundColor)
.navigationTitle(viewModel.mode.title)
.navigationBarTitleDisplayMode(.inline)
.alert("Error while posting",
isPresented: $viewModel.showPostingErrorAlert,
actions: {
Button("Ok") { }
}, message: {
Text(viewModel.postingError ?? "")
})
.toolbar {
if preferences.isOpenAIEnabled {
ToolbarItem(placement: .navigationBarTrailing) {
AIMenu
.disabled(!viewModel.canPost)
}
}
ToolbarItem(placement: .navigationBarTrailing) {
Button {
Task {
let status = await viewModel.postStatus()
if status != nil {
dismiss()
NotificationCenter.default.post(name: NotificationsName.shareSheetClose,
object: nil)
}
}
} label: {
if viewModel.isPosting {
ProgressView()
} else {
Text("status.action.post")
}
}
.disabled(!viewModel.canPost)
.keyboardShortcut(.return, modifiers: .command)
}
ToolbarItem(placement: .navigationBarLeading) {
Button {
if viewModel.shouldDisplayDismissWarning {
isDismissAlertPresented = true
} else {
dismiss()
NotificationCenter.default.post(name: NotificationsName.shareSheetClose,
object: nil)
}
} label: {
Text("action.cancel")
}
.keyboardShortcut(.cancelAction)
.confirmationDialog("",
isPresented: $isDismissAlertPresented,
actions: {
Button("status.draft.delete", role: .destructive) {
dismiss()
NotificationCenter.default.post(name: NotificationsName.shareSheetClose,
object: nil)
}
Button("status.draft.save") {
preferences.draftsPosts.insert(viewModel.statusText.string, at: 0)
dismiss()
NotificationCenter.default.post(name: NotificationsName.shareSheetClose,
object: nil)
}
Button("action.cancel", role: .cancel) {}
})
}
}
}
.interactiveDismissDisabled(!viewModel.statusText.string.isEmpty)
}
@ViewBuilder
private var spoilerTextView: some View {
if viewModel.spoilerOn {
VStack {
TextField("status.editor.spoiler", text: $viewModel.spoilerText)
.focused($isSpoilerTextFocused)
.padding(.horizontal, .layoutPadding)
}
.frame(height: 35)
.background(theme.tintColor.opacity(0.20))
.offset(y: -8)
}
}
@ViewBuilder
private var accountHeaderView: some View {
if let account = currentAccount.account {
HStack {
AppAccountsSelectorView(routerPath: RouterPath(),
accountCreationEnabled: false,
avatarSize: .status)
VStack(alignment: .leading, spacing: 4) {
privacyMenu
Text("@\(account.acct)")
.font(.scaledFootnote)
.foregroundColor(.gray)
}
Spacer()
}
}
}
private var privacyMenu: some View {
Menu {
Section("status.editor.visibility") {
ForEach(Models.Visibility.allCases, id: \.self) { visibility in
Button {
viewModel.visibility = visibility
} label: {
Label(visibility.title, systemImage: visibility.iconName)
}
}
}
} label: {
HStack {
Label(viewModel.visibility.title, systemImage: viewModel.visibility.iconName)
Image(systemName: "chevron.down")
}
.font(.scaledFootnote)
.padding(4)
.overlay(
RoundedRectangle(cornerRadius: 8)
.stroke(theme.tintColor, lineWidth: 1)
)
}
}
private var AIMenu: some View {
Menu {
ForEach(StatusEditorAIPrompts.allCases, id: \.self) { prompt in
Button {
Task {
isLoadingAIRequest = true
await viewModel.runOpenAI(prompt: prompt.toRequestPrompt(text: viewModel.statusText.string))
isLoadingAIRequest = false
}
} label: {
prompt.label
}
}
if let backup = viewModel.backupStatusText {
Button {
viewModel.replaceTextWith(text: backup.string)
viewModel.backupStatusText = nil
} label: {
Label("status.editor.restore-previous", systemImage: "arrow.uturn.right")
}
}
} label: {
if isLoadingAIRequest {
ProgressView()
} else {
Image(systemName: "faxmachine")
}
}
}
}