Implement language selection for new posts (#83) close #76

* Implement language selection in status editor

* Apply the correct language on replies and edits

* Use sheet for language selector

Co-authored-by: Thomas Ricouard <ricouard77@gmail.com>
This commit is contained in:
Thomas 2023-01-17 07:07:26 +01:00 committed by GitHub
parent e0f8c9a3c9
commit 382ebcf8f7
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 106 additions and 21 deletions

View file

@ -50,6 +50,7 @@ public protocol AnyStatus {
var spoilerText: String { get }
var filtered: [Filtered]? { get }
var sensitive: Bool { get }
var language: String? { get }
}
@ -83,6 +84,7 @@ public struct Status: AnyStatus, Codable, Identifiable {
public let spoilerText: String
public let filtered: [Filtered]?
public let sensitive: Bool
public let language: String?
public static func placeholder() -> Status {
.init(id: UUID().uuidString,
@ -109,7 +111,8 @@ public struct Status: AnyStatus, Codable, Identifiable {
poll: nil,
spoilerText: "",
filtered: [],
sensitive: false)
sensitive: false,
language: nil)
}
public static func placeholders() -> [Status] {
@ -146,4 +149,5 @@ public struct ReblogStatus: AnyStatus, Codable, Identifiable {
public let spoilerText: String
public let filtered: [Filtered]?
public let sensitive: Bool
public let language: String?
}

View file

@ -80,7 +80,8 @@ public struct StatusData: Encodable {
public let spoilerText: String?
public let mediaIds: [String]?
public let poll: PollData?
public let language: String?
public struct PollData: Encodable {
public let options: [String]
public let multiple: Bool
@ -98,12 +99,14 @@ public struct StatusData: Encodable {
inReplyToId: String? = nil,
spoilerText: String? = nil,
mediaIds: [String]? = nil,
poll: PollData? = nil) {
poll: PollData? = nil,
language: String? = nil) {
self.status = status
self.visibility = visibility
self.inReplyToId = inReplyToId
self.spoilerText = spoilerText
self.mediaIds = mediaIds
self.poll = poll
self.language = language
}
}

View file

@ -5,15 +5,16 @@ import Models
import Env
struct StatusEditorAccessoryView: View {
@Environment(\.dismiss) private var dismiss
@EnvironmentObject private var preferences: UserPreferences
@EnvironmentObject private var theme: Theme
@EnvironmentObject private var currentInstance: CurrentInstance
@FocusState<Bool>.Binding var isSpoilerTextFocused: Bool
@ObservedObject var viewModel: StatusEditorViewModel
@State private var isDrafsSheetDisplayed: Bool = false
@State private var isLanguageSheetDisplayed: Bool = false
@State private var languageSearch: String = ""
var body: some View {
VStack(spacing: 0) {
@ -25,18 +26,6 @@ struct StatusEditorAccessoryView: View {
}
.disabled(viewModel.showPoll)
Button {
viewModel.insertStatusText(text: " @")
} label: {
Image(systemName: "at")
}
Button {
viewModel.insertStatusText(text: " #")
} label: {
Image(systemName: "number")
}
Button {
withAnimation {
viewModel.showPoll.toggle()
@ -61,9 +50,18 @@ struct StatusEditorAccessoryView: View {
} label: {
Image(systemName: "archivebox")
}
}
Button {
isLanguageSheetDisplayed.toggle()
} label: {
if let language = viewModel.selectedLanguage {
Text(language.uppercased())
} else {
Image(systemName: "globe")
}
}
Spacer()
characterCountView
@ -76,6 +74,54 @@ struct StatusEditorAccessoryView: View {
.sheet(isPresented: $isDrafsSheetDisplayed) {
draftsSheetView
}
.sheet(isPresented: $isLanguageSheetDisplayed, content: {
languageSheetView
})
.onAppear {
viewModel.setInitialLanguageSelection(preference: preferences.serverPreferences?.postLanguage)
}
}
@ViewBuilder
private func languageTextView(isoCode: String, nativeName: String?, name: String?) -> some View {
if let nativeName = nativeName, let name = name {
Text("\(nativeName) (\(name))")
} else {
Text(isoCode.uppercased())
}
}
private var languageSheetView: some View {
NavigationStack {
List {
ForEach(availableLanguages, id: \.0) { (isoCode, nativeName, name) in
HStack {
languageTextView(isoCode: isoCode, nativeName: nativeName, name: name)
.tag(isoCode)
Spacer()
if isoCode == viewModel.selectedLanguage {
Image(systemName: "checkmark")
}
}
.listRowBackground(theme.primaryBackgroundColor)
.contentShape(Rectangle())
.onTapGesture {
viewModel.selectedLanguage = isoCode
isLanguageSheetDisplayed = false
}
}
}
.searchable(text: $languageSearch)
.toolbar {
ToolbarItem(placement: .navigationBarLeading) {
Button("Cancel", action: { isLanguageSheetDisplayed = false })
}
}
.navigationTitle("Select Languages")
.navigationBarTitleDisplayMode(.inline)
.scrollContentBackground(.hidden)
.background(theme.secondaryBackgroundColor)
}
}
private var draftsSheetView: some View {
@ -98,7 +144,7 @@ struct StatusEditorAccessoryView: View {
}
.toolbar {
ToolbarItem(placement: .navigationBarLeading) {
Button("Cancel", action: { dismiss() })
Button("Cancel", action: { isDrafsSheetDisplayed = false })
}
}
.scrollContentBackground(.hidden)
@ -115,4 +161,23 @@ struct StatusEditorAccessoryView: View {
.foregroundColor(.gray)
.font(.callout)
}
private var availableLanguages: [(String, String?, String?)] {
Locale.LanguageCode.isoLanguageCodes
.filter { $0.identifier.count == 2 } // Mastodon only supports ISO 639-1 (two-letter) codes
.map { lang in
let nativeLocale = Locale(languageComponents: Locale.Language.Components(languageCode: lang))
return (
lang.identifier,
nativeLocale.localizedString(forLanguageCode: lang.identifier),
Locale.current.localizedString(forLanguageCode: lang.identifier)
)
}
.filter { (identifier, nativeLocale, locale) in
guard !languageSearch.isEmpty else {
return true
}
return nativeLocale?.lowercased().hasPrefix(languageSearch.lowercased()) == true
}
}
}

View file

@ -63,6 +63,7 @@ public class StatusEditorViewModel: ObservableObject {
@Published var mentionsSuggestions: [Account] = []
@Published var tagsSuggestions: [Tag] = []
@Published var selectedLanguage: String?
private var currentSuggestionRange: NSRange?
private var embededStatusURL: URL? {
@ -94,6 +95,17 @@ public class StatusEditorViewModel: ObservableObject {
statusText = .init(string: text)
selectedRange = .init(location: text.utf16.count, length: 0)
}
func setInitialLanguageSelection(preference: String?) {
switch mode {
case .replyTo(let status), .edit(let status):
selectedLanguage = status.language
default:
break
}
selectedLanguage = selectedLanguage ?? preference ?? currentAccount?.source?.language
}
private func getPollOptionsForAPI() -> [String]? {
let options = pollOptions.filter { !$0.trimmingCharacters(in: .whitespaces).isEmpty }
@ -116,7 +128,8 @@ public class StatusEditorViewModel: ObservableObject {
inReplyToId: mode.replyToStatus?.id,
spoilerText: spoilerOn ? spoilerText : nil,
mediaIds: mediasImages.compactMap{ $0.mediaAttachement?.id },
poll: pollData)
poll: pollData,
language: selectedLanguage)
switch mode {
case .new, .replyTo, .quote, .mention, .shareExtension:
postStatus = try await client.post(endpoint: Statuses.postStatus(json: data))