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

View file

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

View file

@ -5,15 +5,16 @@ import Models
import Env import Env
struct StatusEditorAccessoryView: View { struct StatusEditorAccessoryView: View {
@Environment(\.dismiss) private var dismiss
@EnvironmentObject private var preferences: UserPreferences @EnvironmentObject private var preferences: UserPreferences
@EnvironmentObject private var theme: Theme @EnvironmentObject private var theme: Theme
@EnvironmentObject private var currentInstance: CurrentInstance @EnvironmentObject private var currentInstance: CurrentInstance
@FocusState<Bool>.Binding var isSpoilerTextFocused: Bool @FocusState<Bool>.Binding var isSpoilerTextFocused: Bool
@ObservedObject var viewModel: StatusEditorViewModel @ObservedObject var viewModel: StatusEditorViewModel
@State private var isDrafsSheetDisplayed: Bool = false @State private var isDrafsSheetDisplayed: Bool = false
@State private var isLanguageSheetDisplayed: Bool = false
@State private var languageSearch: String = ""
var body: some View { var body: some View {
VStack(spacing: 0) { VStack(spacing: 0) {
@ -25,18 +26,6 @@ struct StatusEditorAccessoryView: View {
} }
.disabled(viewModel.showPoll) .disabled(viewModel.showPoll)
Button {
viewModel.insertStatusText(text: " @")
} label: {
Image(systemName: "at")
}
Button {
viewModel.insertStatusText(text: " #")
} label: {
Image(systemName: "number")
}
Button { Button {
withAnimation { withAnimation {
viewModel.showPoll.toggle() viewModel.showPoll.toggle()
@ -61,9 +50,18 @@ struct StatusEditorAccessoryView: View {
} label: { } label: {
Image(systemName: "archivebox") Image(systemName: "archivebox")
} }
} }
Button {
isLanguageSheetDisplayed.toggle()
} label: {
if let language = viewModel.selectedLanguage {
Text(language.uppercased())
} else {
Image(systemName: "globe")
}
}
Spacer() Spacer()
characterCountView characterCountView
@ -76,6 +74,54 @@ struct StatusEditorAccessoryView: View {
.sheet(isPresented: $isDrafsSheetDisplayed) { .sheet(isPresented: $isDrafsSheetDisplayed) {
draftsSheetView 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 { private var draftsSheetView: some View {
@ -98,7 +144,7 @@ struct StatusEditorAccessoryView: View {
} }
.toolbar { .toolbar {
ToolbarItem(placement: .navigationBarLeading) { ToolbarItem(placement: .navigationBarLeading) {
Button("Cancel", action: { dismiss() }) Button("Cancel", action: { isDrafsSheetDisplayed = false })
} }
} }
.scrollContentBackground(.hidden) .scrollContentBackground(.hidden)
@ -115,4 +161,23 @@ struct StatusEditorAccessoryView: View {
.foregroundColor(.gray) .foregroundColor(.gray)
.font(.callout) .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 mentionsSuggestions: [Account] = []
@Published var tagsSuggestions: [Tag] = [] @Published var tagsSuggestions: [Tag] = []
@Published var selectedLanguage: String?
private var currentSuggestionRange: NSRange? private var currentSuggestionRange: NSRange?
private var embededStatusURL: URL? { private var embededStatusURL: URL? {
@ -94,6 +95,17 @@ public class StatusEditorViewModel: ObservableObject {
statusText = .init(string: text) statusText = .init(string: text)
selectedRange = .init(location: text.utf16.count, length: 0) 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]? { private func getPollOptionsForAPI() -> [String]? {
let options = pollOptions.filter { !$0.trimmingCharacters(in: .whitespaces).isEmpty } let options = pollOptions.filter { !$0.trimmingCharacters(in: .whitespaces).isEmpty }
@ -116,7 +128,8 @@ public class StatusEditorViewModel: ObservableObject {
inReplyToId: mode.replyToStatus?.id, inReplyToId: mode.replyToStatus?.id,
spoilerText: spoilerOn ? spoilerText : nil, spoilerText: spoilerOn ? spoilerText : nil,
mediaIds: mediasImages.compactMap{ $0.mediaAttachement?.id }, mediaIds: mediasImages.compactMap{ $0.mediaAttachement?.id },
poll: pollData) poll: pollData,
language: selectedLanguage)
switch mode { switch mode {
case .new, .replyTo, .quote, .mention, .shareExtension: case .new, .replyTo, .quote, .mention, .shareExtension:
postStatus = try await client.post(endpoint: Statuses.postStatus(json: data)) postStatus = try await client.post(endpoint: Statuses.postStatus(json: data))