From 34a482f01fec3d8355c17a245fdd99fde0cdf71b Mon Sep 17 00:00:00 2001 From: Thomas Ricouard Date: Sun, 7 Jan 2024 07:03:39 +0100 Subject: [PATCH] More layout rework for the composer --- .../Env/Sources/Env/UserPreferences.swift | 2 +- .../Editor/Components/AccessoryView.swift | 50 ++---- .../Editor/Components/LangButton.swift | 129 ++++++++++++++ .../Sources/StatusKit/Editor/EditorView.swift | 168 +++++------------- .../Sources/StatusKit/Editor/ViewModel.swift | 2 +- 5 files changed, 193 insertions(+), 158 deletions(-) create mode 100644 Packages/StatusKit/Sources/StatusKit/Editor/Components/LangButton.swift diff --git a/Packages/Env/Sources/Env/UserPreferences.swift b/Packages/Env/Sources/Env/UserPreferences.swift index a0636cc4..f4176616 100644 --- a/Packages/Env/Sources/Env/UserPreferences.swift +++ b/Packages/Env/Sources/Env/UserPreferences.swift @@ -14,7 +14,7 @@ import SwiftUI @AppStorage("is_open_ai_enabled") public var isOpenAIEnabled: Bool = true @AppStorage("recently_used_languages") public var recentlyUsedLanguages: [String] = [] - @AppStorage("social_keyboard_composer") public var isSocialKeyboardEnabled: Bool = true + @AppStorage("social_keyboard_composer") public var isSocialKeyboardEnabled: Bool = false @AppStorage("use_instance_content_settings") public var useInstanceContentSettings: Bool = true @AppStorage("app_auto_expand_spoilers") public var appAutoExpandSpoilers = false diff --git a/Packages/StatusKit/Sources/StatusKit/Editor/Components/AccessoryView.swift b/Packages/StatusKit/Sources/StatusKit/Editor/Components/AccessoryView.swift index 8fef356d..147216bb 100644 --- a/Packages/StatusKit/Sources/StatusKit/Editor/Components/AccessoryView.swift +++ b/Packages/StatusKit/Sources/StatusKit/Editor/Components/AccessoryView.swift @@ -57,13 +57,10 @@ extension StatusEditor { actionsView } #else - ScrollView(.horizontal, showsIndicators: false) { - HStack(alignment: .center, spacing: 16) { - actionsView - } - .padding(.horizontal, .layoutPadding) + HStack(alignment: .center, spacing: 16) { + actionsView } - Spacer() + .padding(.horizontal, .layoutPadding) #endif } @@ -153,6 +150,7 @@ extension StatusEditor { .accessibilityLabel("accessibility.editor.button.attach-photo") .disabled(viewModel.showPoll) + Button { // all SEVM have the same visibility value followUpSEVMs.append(ViewModel(mode: .new(visibility: focusedSEVM.visibility))) @@ -160,28 +158,7 @@ extension StatusEditor { Image(systemName: "arrowshape.turn.up.left.circle.fill") } .disabled(!canAddNewSEVM) - - Button { - withAnimation { - viewModel.showPoll.toggle() - viewModel.resetPollDefaults() - } - } label: { - Image(systemName: "chart.bar") - } - .accessibilityLabel("accessibility.editor.button.poll") - .disabled(viewModel.shouldDisablePollButton) - - Button { - withAnimation { - viewModel.spoilerOn.toggle() - } - isSpoilerTextFocused = viewModel.id - } label: { - Image(systemName: viewModel.spoilerOn ? "exclamationmark.triangle.fill" : "exclamationmark.triangle") - } - .accessibilityLabel("accessibility.editor.button.spoiler") - + if !viewModel.customEmojiContainer.isEmpty { Button { isCustomEmojisSheetDisplay = true @@ -202,20 +179,23 @@ extension StatusEditor { } } - Button { - viewModel.insertStatusText(text: "#") - } label: { - Image(systemName: "number") + + if preferences.isOpenAIEnabled { + AIMenu.disabled(!viewModel.canPost) } + Spacer() + Button { viewModel.insertStatusText(text: "@") } label: { Image(systemName: "at") } - - if preferences.isOpenAIEnabled { - AIMenu.disabled(!viewModel.canPost) + + Button { + viewModel.insertStatusText(text: "#") + } label: { + Image(systemName: "number") } } diff --git a/Packages/StatusKit/Sources/StatusKit/Editor/Components/LangButton.swift b/Packages/StatusKit/Sources/StatusKit/Editor/Components/LangButton.swift new file mode 100644 index 00000000..d728244a --- /dev/null +++ b/Packages/StatusKit/Sources/StatusKit/Editor/Components/LangButton.swift @@ -0,0 +1,129 @@ +import DesignSystem +import Env +import SwiftUI +import Models + +extension StatusEditor { + + @MainActor + struct LangButton: View { + @Environment(Theme.self) private var theme + @Environment(CurrentInstance.self) private var currentInstance + @Environment(UserPreferences.self) private var preferences + + @State private var isLanguageSheetDisplayed: Bool = false + @State private var languageSearch: String = "" + + var viewModel: ViewModel + + var body: some View { + Button { + isLanguageSheetDisplayed.toggle() + } label: { + HStack(alignment: .center) { + Image(systemName: "text.bubble") + if let language = viewModel.selectedLanguage { + Text(language.uppercased()) + } else { + Image(systemName: "globe") + } + } + .font(.footnote) + } + .buttonStyle(.bordered) + .onAppear { + viewModel.setInitialLanguageSelection(preference: preferences.recentlyUsedLanguages.first ?? preferences.serverPreferences?.postLanguage) + } + .accessibilityLabel("accessibility.editor.button.language") + .popover(isPresented: $isLanguageSheetDisplayed) { + if UIDevice.current.userInterfaceIdiom == .phone { + languageSheetView + } else { + languageSheetView + .frame(width: 400, height: 500) + } + } + } + + private var languageSheetView: some View { + NavigationStack { + List { + if languageSearch.isEmpty { + if !recentlyUsedLanguages.isEmpty { + Section("status.editor.language-select.recently-used") { + languageSheetSection(languages: recentlyUsedLanguages) + } + } + Section { + languageSheetSection(languages: otherLanguages) + } + } else { + languageSheetSection(languages: languageSearchResult(query: languageSearch)) + } + } + .searchable(text: $languageSearch) + .toolbar { + ToolbarItem(placement: .navigationBarLeading) { + Button("action.cancel", action: { isLanguageSheetDisplayed = false }) + } + } + .navigationTitle("status.editor.language-select.navigation-title") + .navigationBarTitleDisplayMode(.inline) + .scrollContentBackground(.hidden) + .background(theme.secondaryBackgroundColor) + } + } + + @ViewBuilder + private func languageTextView(isoCode: String, nativeName: String?, name: String?) -> some View { + if let nativeName, let name { + Text("\(nativeName) (\(name))") + } else { + Text(isoCode.uppercased()) + } + } + + private func languageSheetSection(languages: [Language]) -> some View { + ForEach(languages) { language in + HStack { + languageTextView( + isoCode: language.isoCode, + nativeName: language.nativeName, + name: language.localizedName + ).tag(language.isoCode) + Spacer() + if language.isoCode == viewModel.selectedLanguage { + Image(systemName: "checkmark") + } + } + .listRowBackground(theme.primaryBackgroundColor) + .contentShape(Rectangle()) + .onTapGesture { + viewModel.selectedLanguage = language.isoCode + viewModel.hasExplicitlySelectedLanguage = true + isLanguageSheetDisplayed = false + } + } + } + + private var recentlyUsedLanguages: [Language] { + preferences.recentlyUsedLanguages.compactMap { isoCode in + Language.allAvailableLanguages.first { $0.isoCode == isoCode } + } + } + + private var otherLanguages: [Language] { + Language.allAvailableLanguages.filter { !preferences.recentlyUsedLanguages.contains($0.isoCode) } + } + + private func languageSearchResult(query: String) -> [Language] { + Language.allAvailableLanguages.filter { language in + guard !languageSearch.isEmpty else { + return true + } + return language.nativeName?.lowercased().hasPrefix(query.lowercased()) == true + || language.localizedName?.lowercased().hasPrefix(query.lowercased()) == true + } + } + } +} diff --git a/Packages/StatusKit/Sources/StatusKit/Editor/EditorView.swift b/Packages/StatusKit/Sources/StatusKit/Editor/EditorView.swift index ac47c6bc..37fca722 100644 --- a/Packages/StatusKit/Sources/StatusKit/Editor/EditorView.swift +++ b/Packages/StatusKit/Sources/StatusKit/Editor/EditorView.swift @@ -23,9 +23,6 @@ extension StatusEditor { @Bindable var viewModel: ViewModel @Binding var followUpSEVMs: [ViewModel] @Binding var editingMediaContainer: MediaContainer? - - @State private var isLanguageSheetDisplayed: Bool = false - @State private var languageSearch: String = "" @FocusState.Binding var isSpoilerTextFocused: UUID? @FocusState.Binding var editorFocusState: EditorFocusState? @@ -47,10 +44,10 @@ extension StatusEditor { VStack(spacing: 0) { accountHeaderView textInput + pollView characterCountAndLangView MediaView(viewModel: viewModel, editingMediaContainer: $editingMediaContainer) embeddedStatus - pollView } .padding(.vertical) @@ -128,9 +125,19 @@ extension StatusEditor { @ViewBuilder private var embeddedStatus: some View { - if viewModel.replyToStatus != nil { Divider().padding(.top, 20) } - - if let status = viewModel.embeddedStatus ?? viewModel.replyToStatus { + if let status = viewModel.replyToStatus { + Divider().padding(.vertical, .statusComponentSpacing) + StatusRowView(viewModel: .init(status: status, + client: client, + routerPath: RouterPath(), + showActions: false)) + .accessibilityLabel(status.content.asRawText) + .allowsHitTesting(false) + .environment(\.isStatusFocused, false) + .padding(.horizontal, .layoutPadding) + .padding(.vertical, .statusComponentSpacing) + + } else if let status = viewModel.embeddedStatus { StatusEmbeddedView(status: status, client: client, routerPath: RouterPath()) .padding(.horizontal, .layoutPadding) .disabled(true) @@ -150,130 +157,49 @@ extension StatusEditor { private var characterCountAndLangView: some View { let value = (currentInstance.instance?.configuration?.statuses.maxCharacters ?? 500) + viewModel.statusTextCharacterLength HStack(alignment: .center) { + LangButton(viewModel: viewModel) + .padding(.leading, .layoutPadding) + + Button { + withAnimation { + viewModel.showPoll.toggle() + viewModel.resetPollDefaults() + } + } label: { + Image(systemName: viewModel.showPoll ? "chart.bar.fill" : "chart.bar") + } + .buttonStyle(.bordered) + .accessibilityLabel("accessibility.editor.button.poll") + .disabled(viewModel.shouldDisablePollButton) + + Button { + withAnimation { + viewModel.spoilerOn.toggle() + } + isSpoilerTextFocused = viewModel.id + } label: { + Image(systemName: viewModel.spoilerOn ? "exclamationmark.triangle.fill" : "exclamationmark.triangle") + } + .buttonStyle(.bordered) + .accessibilityLabel("accessibility.editor.button.spoiler") + + Spacer() + Text("\(value)") .foregroundColor(value < 0 ? .red : .secondary) - .font(.scaledCallout) + .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(.leading, .layoutPadding) - - Button { - isLanguageSheetDisplayed.toggle() - } label: { - HStack(alignment: .center) { - if let language = viewModel.selectedLanguage { - Image(systemName: "text.bubble") - Text(language.uppercased()) - } else { - Image(systemName: "globe") - } - } - .font(.footnote) - } - .buttonStyle(.bordered) - .onAppear { - viewModel.setInitialLanguageSelection(preference: preferences.recentlyUsedLanguages.first ?? preferences.serverPreferences?.postLanguage) - } - .accessibilityLabel("accessibility.editor.button.language") - .popover(isPresented: $isLanguageSheetDisplayed) { - if UIDevice.current.userInterfaceIdiom == .phone { - languageSheetView - } else { - languageSheetView - .frame(width: 400, height: 500) - } - } - - Spacer() + .padding(.trailing, .layoutPadding) } - .padding(.bottom, 8) + .padding(.vertical, 8) } - private var languageSheetView: some View { - NavigationStack { - List { - if languageSearch.isEmpty { - if !recentlyUsedLanguages.isEmpty { - Section("status.editor.language-select.recently-used") { - languageSheetSection(languages: recentlyUsedLanguages) - } - } - Section { - languageSheetSection(languages: otherLanguages) - } - } else { - languageSheetSection(languages: languageSearchResult(query: languageSearch)) - } - } - .searchable(text: $languageSearch) - .toolbar { - ToolbarItem(placement: .navigationBarLeading) { - Button("action.cancel", action: { isLanguageSheetDisplayed = false }) - } - } - .navigationTitle("status.editor.language-select.navigation-title") - .navigationBarTitleDisplayMode(.inline) - .scrollContentBackground(.hidden) - .background(theme.secondaryBackgroundColor) - } - } - - - @ViewBuilder - private func languageTextView(isoCode: String, nativeName: String?, name: String?) -> some View { - if let nativeName, let name { - Text("\(nativeName) (\(name))") - } else { - Text(isoCode.uppercased()) - } - } - - private func languageSheetSection(languages: [Language]) -> some View { - ForEach(languages) { language in - HStack { - languageTextView( - isoCode: language.isoCode, - nativeName: language.nativeName, - name: language.localizedName - ).tag(language.isoCode) - Spacer() - if language.isoCode == viewModel.selectedLanguage { - Image(systemName: "checkmark") - } - } - .listRowBackground(theme.primaryBackgroundColor) - .contentShape(Rectangle()) - .onTapGesture { - viewModel.selectedLanguage = language.isoCode - viewModel.hasExplicitlySelectedLanguage = true - isLanguageSheetDisplayed = false - } - } - } - - private var recentlyUsedLanguages: [Language] { - preferences.recentlyUsedLanguages.compactMap { isoCode in - Language.allAvailableLanguages.first { $0.isoCode == isoCode } - } - } - - private var otherLanguages: [Language] { - Language.allAvailableLanguages.filter { !preferences.recentlyUsedLanguages.contains($0.isoCode) } - } - - private func languageSearchResult(query: String) -> [Language] { - Language.allAvailableLanguages.filter { language in - guard !languageSearch.isEmpty else { - return true - } - return language.nativeName?.lowercased().hasPrefix(query.lowercased()) == true - || language.localizedName?.lowercased().hasPrefix(query.lowercased()) == true - } - } - private func setupViewModel() { viewModel.client = client viewModel.currentAccount = currentAccount.account diff --git a/Packages/StatusKit/Sources/StatusKit/Editor/ViewModel.swift b/Packages/StatusKit/Sources/StatusKit/Editor/ViewModel.swift index 85dec99a..76f42230 100644 --- a/Packages/StatusKit/Sources/StatusKit/Editor/ViewModel.swift +++ b/Packages/StatusKit/Sources/StatusKit/Editor/ViewModel.swift @@ -125,7 +125,7 @@ extension StatusEditor { } var shouldDisablePollButton: Bool { - !mediaPickers.isEmpty + !mediaContainers.isEmpty } var shouldDisplayDismissWarning: Bool {