Improve handling of unselecting languanges in composer language menu (#1093)

* allow toggling off/on multiple from main composer lang menu

* fix dropdown styles for long labels

* udpate model to use new string field

* update language UI

* save langs to history on submit

* remove edit

* clean up use new fields

* default to deviceLocales

* fix default valu

* feedback

* use radio icon
zio/stable
Eric Bailey 2023-08-23 15:40:15 -05:00 committed by GitHub
parent acad8cb455
commit b6317d4ce7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 137 additions and 49 deletions

View File

@ -33,6 +33,9 @@ const LABEL_GROUPS = [
'impersonation', 'impersonation',
] ]
const VISIBILITY_VALUES = ['show', 'warn', 'hide'] const VISIBILITY_VALUES = ['show', 'warn', 'hide']
const DEFAULT_LANG_CODES = (deviceLocales || [])
.concat(['en', 'ja', 'pt', 'de'])
.slice(0, 6)
export class LabelPreferencesModel { export class LabelPreferencesModel {
nsfw: LabelPreference = 'hide' nsfw: LabelPreference = 'hide'
@ -51,7 +54,8 @@ export class LabelPreferencesModel {
export class PreferencesModel { export class PreferencesModel {
adultContentEnabled = !isIOS adultContentEnabled = !isIOS
contentLanguages: string[] = deviceLocales || [] contentLanguages: string[] = deviceLocales || []
postLanguages: string[] = deviceLocales || [] postLanguage: string = deviceLocales[0] || 'en'
postLanguageHistory: string[] = DEFAULT_LANG_CODES
contentLabels = new LabelPreferencesModel() contentLabels = new LabelPreferencesModel()
savedFeeds: string[] = [] savedFeeds: string[] = []
pinnedFeeds: string[] = [] pinnedFeeds: string[] = []
@ -71,7 +75,8 @@ export class PreferencesModel {
serialize() { serialize() {
return { return {
contentLanguages: this.contentLanguages, contentLanguages: this.contentLanguages,
postLanguages: this.postLanguages, postLanguage: this.postLanguage,
postLanguageHistory: this.postLanguageHistory,
contentLabels: this.contentLabels, contentLabels: this.contentLabels,
savedFeeds: this.savedFeeds, savedFeeds: this.savedFeeds,
pinnedFeeds: this.pinnedFeeds, pinnedFeeds: this.pinnedFeeds,
@ -101,16 +106,23 @@ export class PreferencesModel {
// default to the device languages // default to the device languages
this.contentLanguages = deviceLocales this.contentLanguages = deviceLocales
} }
// check if post languages in preferences exist, otherwise default to device languages if (hasProp(v, 'postLanguage') && typeof v.postLanguage === 'string') {
if ( this.postLanguage = v.postLanguage
hasProp(v, 'postLanguages') &&
Array.isArray(v.postLanguages) &&
typeof v.postLanguages.every(item => typeof item === 'string')
) {
this.postLanguages = v.postLanguages
} else { } else {
// default to the device languages // default to the device languages
this.postLanguages = deviceLocales this.postLanguage = deviceLocales[0] || 'en'
}
if (
hasProp(v, 'postLanguageHistory') &&
Array.isArray(v.postLanguageHistory) &&
typeof v.postLanguageHistory.every(item => typeof item === 'string')
) {
this.postLanguageHistory = v.postLanguageHistory
.concat(DEFAULT_LANG_CODES)
.slice(0, 6)
} else {
// default to a starter set
this.postLanguageHistory = DEFAULT_LANG_CODES
} }
// check if content labels in preferences exist, then hydrate // check if content labels in preferences exist, then hydrate
if (hasProp(v, 'contentLabels') && typeof v.contentLabels === 'object') { if (hasProp(v, 'contentLabels') && typeof v.contentLabels === 'object') {
@ -279,7 +291,8 @@ export class PreferencesModel {
runInAction(() => { runInAction(() => {
this.contentLabels = new LabelPreferencesModel() this.contentLabels = new LabelPreferencesModel()
this.contentLanguages = deviceLocales this.contentLanguages = deviceLocales
this.postLanguages = deviceLocales this.postLanguage = deviceLocales ? deviceLocales.join(',') : 'en'
this.postLanguageHistory = DEFAULT_LANG_CODES
this.savedFeeds = [] this.savedFeeds = []
this.pinnedFeeds = [] this.pinnedFeeds = []
}) })
@ -305,20 +318,54 @@ export class PreferencesModel {
} }
} }
/**
* A getter that splits `this.postLanguage` into an array of strings.
*
* This was previously the main field on this model, but now we're
* concatenating lang codes to make multi-selection a little better.
*/
get postLanguages() {
// filter out empty strings if exist
return this.postLanguage.split(',').filter(Boolean)
}
hasPostLanguage(code2: string) { hasPostLanguage(code2: string) {
return this.postLanguages.includes(code2) return this.postLanguages.includes(code2)
} }
togglePostLanguage(code2: string) { togglePostLanguage(code2: string) {
if (this.hasPostLanguage(code2)) { if (this.hasPostLanguage(code2)) {
this.postLanguages = this.postLanguages.filter(lang => lang !== code2) this.postLanguage = this.postLanguages
.filter(lang => lang !== code2)
.join(',')
} else { } else {
this.postLanguages = this.postLanguages.concat([code2]) // sort alphabetically for deterministic comparison in context menu
this.postLanguage = this.postLanguages
.concat([code2])
.sort((a, b) => a.localeCompare(b))
.join(',')
} }
} }
setPostLanguage(code2: string) { setPostLanguage(commaSeparatedLangCodes: string) {
this.postLanguages = [code2] this.postLanguage = commaSeparatedLangCodes
}
/**
* Saves whatever language codes are currently selected into a history array,
* which is then used to populate the language selector menu.
*/
savePostLanguageToHistory() {
// filter out duplicate `this.postLanguage` if exists, and prepend
// value to start of array
this.postLanguageHistory = [this.postLanguage]
.concat(
this.postLanguageHistory.filter(
commaSeparatedLangCodes =>
commaSeparatedLangCodes !== this.postLanguage,
),
)
.slice(0, 6)
} }
getReadablePostLanguages() { getReadablePostLanguages() {

View File

@ -212,6 +212,7 @@ export const ComposePost = observer(function ComposePost({
if (!replyTo) { if (!replyTo) {
store.me.mainFeed.onPostCreated() store.me.mainFeed.onPostCreated()
} }
store.preferences.savePostLanguageToHistory()
onPost?.() onPost?.()
onClose() onClose()
Toast.show(`Your ${replyTo ? 'reply' : 'post'} has been published`) Toast.show(`Your ${replyTo ? 'reply' : 'post'} has been published`)

View File

@ -15,7 +15,6 @@ import {usePalette} from 'lib/hooks/usePalette'
import {useStores} from 'state/index' import {useStores} from 'state/index'
import {isNative} from 'platform/detection' import {isNative} from 'platform/detection'
import {codeToLanguageName} from '../../../../locale/helpers' import {codeToLanguageName} from '../../../../locale/helpers'
import {deviceLocales} from 'platform/detection'
export const SelectLangBtn = observer(function SelectLangBtn() { export const SelectLangBtn = observer(function SelectLangBtn() {
const pal = usePalette('default') const pal = usePalette('default')
@ -31,35 +30,48 @@ export const SelectLangBtn = observer(function SelectLangBtn() {
}, [store]) }, [store])
const postLanguagesPref = store.preferences.postLanguages const postLanguagesPref = store.preferences.postLanguages
const postLanguagePref = store.preferences.postLanguage
const items: DropdownItem[] = useMemo(() => { const items: DropdownItem[] = useMemo(() => {
let arr: DropdownItemButton[] = [] let arr: DropdownItemButton[] = []
const add = (langCode: string) => { function add(commaSeparatedLangCodes: string) {
const langName = codeToLanguageName(langCode) const langCodes = commaSeparatedLangCodes.split(',')
const langName = langCodes
.map(code => codeToLanguageName(code))
.join(' + ')
/*
* Filter out any duplicates
*/
if (arr.find((item: DropdownItemButton) => item.label === langName)) { if (arr.find((item: DropdownItemButton) => item.label === langName)) {
return return
} }
arr.push({ arr.push({
icon: store.preferences.hasPostLanguage(langCode) icon:
? ['fas', 'circle-check'] langCodes.every(code => store.preferences.hasPostLanguage(code)) &&
: ['far', 'circle'], langCodes.length === postLanguagesPref.length
? ['fas', 'circle-dot']
: ['far', 'circle'],
label: langName, label: langName,
onPress() { onPress() {
store.preferences.setPostLanguage(langCode) store.preferences.setPostLanguage(commaSeparatedLangCodes)
}, },
}) })
} }
for (const lang of postLanguagesPref) { if (postLanguagesPref.length) {
/*
* Re-join here after sanitization bc postLanguageHistory is an array of
* comma-separated strings too
*/
add(postLanguagePref)
}
// comma-separted strings of lang codes that have been used in the past
for (const lang of store.preferences.postLanguageHistory) {
add(lang) add(lang)
} }
for (const lang of deviceLocales) {
add(lang)
}
add('en') // english
add('ja') // japanese
add('pt') // portugese
add('de') // german
return [ return [
{heading: true, label: 'Post language'}, {heading: true, label: 'Post language'},
@ -70,7 +82,7 @@ export const SelectLangBtn = observer(function SelectLangBtn() {
onPress: onPressMore, onPress: onPressMore,
}, },
] ]
}, [store.preferences, postLanguagesPref, onPressMore]) }, [store.preferences, onPressMore, postLanguagePref, postLanguagesPref])
return ( return (
<DropdownButton <DropdownButton
@ -81,11 +93,9 @@ export const SelectLangBtn = observer(function SelectLangBtn() {
style={styles.button} style={styles.button}
accessibilityLabel="Language selection" accessibilityLabel="Language selection"
accessibilityHint=""> accessibilityHint="">
{store.preferences.postLanguages.length > 0 ? ( {postLanguagesPref.length > 0 ? (
<Text type="lg-bold" style={[pal.link, styles.label]} numberOfLines={1}> <Text type="lg-bold" style={[pal.link, styles.label]} numberOfLines={1}>
{store.preferences.postLanguages {postLanguagesPref.map(lang => codeToLanguageName(lang)).join(', ')}
.map(lang => codeToLanguageName(lang))
.join(', ')}
</Text> </Text>
) : ( ) : (
<FontAwesomeIcon <FontAwesomeIcon

View File

@ -1,17 +1,18 @@
import React from 'react' import React from 'react'
import {StyleSheet, View} from 'react-native' import {StyleSheet, View} from 'react-native'
import {observer} from 'mobx-react-lite'
import {ScrollView} from '../util' import {ScrollView} from '../util'
import {useStores} from 'state/index' import {useStores} from 'state/index'
import {Text} from '../../util/text/Text' import {Text} from '../../util/text/Text'
import {usePalette} from 'lib/hooks/usePalette' import {usePalette} from 'lib/hooks/usePalette'
import {isDesktopWeb, deviceLocales} from 'platform/detection' import {isDesktopWeb, deviceLocales} from 'platform/detection'
import {LANGUAGES, LANGUAGES_MAP_CODE2} from '../../../../locale/languages' import {LANGUAGES, LANGUAGES_MAP_CODE2} from '../../../../locale/languages'
import {LanguageToggle} from './LanguageToggle'
import {ConfirmLanguagesButton} from './ConfirmLanguagesButton' import {ConfirmLanguagesButton} from './ConfirmLanguagesButton'
import {ToggleButton} from 'view/com/util/forms/ToggleButton'
export const snapPoints = ['100%'] export const snapPoints = ['100%']
export function Component({}: {}) { export const Component = observer(() => {
const store = useStores() const store = useStores()
const pal = usePalette('default') const pal = usePalette('default')
const onPressDone = React.useCallback(() => { const onPressDone = React.useCallback(() => {
@ -53,23 +54,38 @@ export function Component({}: {}) {
Which languages are used in this post? Which languages are used in this post?
</Text> </Text>
<ScrollView style={styles.scrollContainer}> <ScrollView style={styles.scrollContainer}>
{languages.map(lang => ( {languages.map(lang => {
<LanguageToggle const isSelected = store.preferences.hasPostLanguage(lang.code2)
key={lang.code2}
code2={lang.code2} // enforce a max of 3 selections for post languages
langType="postLanguages" let isDisabled = false
name={lang.name} if (
onPress={() => { store.preferences.postLanguage.split(',').length >= 3 &&
onPress(lang.code2) !isSelected
}} ) {
/> isDisabled = true
))} }
return (
<ToggleButton
key={lang.code2}
label={lang.name}
isSelected={isSelected}
onPress={() => (isDisabled ? undefined : onPress(lang.code2))}
style={[
pal.border,
styles.languageToggle,
isDisabled && styles.dimmed,
]}
/>
)
})}
<View style={styles.bottomSpacer} /> <View style={styles.bottomSpacer} />
</ScrollView> </ScrollView>
<ConfirmLanguagesButton onPress={onPressDone} /> <ConfirmLanguagesButton onPress={onPressDone} />
</View> </View>
) )
} })
const styles = StyleSheet.create({ const styles = StyleSheet.create({
container: { container: {
@ -94,4 +110,13 @@ const styles = StyleSheet.create({
bottomSpacer: { bottomSpacer: {
height: isDesktopWeb ? 0 : 60, height: isDesktopWeb ? 0 : 60,
}, },
languageToggle: {
borderTopWidth: 1,
borderRadius: 0,
paddingHorizontal: 6,
paddingVertical: 12,
},
dimmed: {
opacity: 0.5,
},
}) })

View File

@ -319,9 +319,12 @@ const styles = StyleSheet.create({
icon: { icon: {
marginLeft: 2, marginLeft: 2,
marginRight: 8, marginRight: 8,
flexShrink: 0,
}, },
label: { label: {
fontSize: 18, fontSize: 18,
flexShrink: 1,
flexGrow: 1,
}, },
separator: { separator: {
borderTopWidth: 1, borderTopWidth: 1,

View File

@ -29,6 +29,7 @@ import {faCircleCheck as farCircleCheck} from '@fortawesome/free-regular-svg-ico
import {faCircleCheck} from '@fortawesome/free-solid-svg-icons/faCircleCheck' import {faCircleCheck} from '@fortawesome/free-solid-svg-icons/faCircleCheck'
import {faCircleExclamation} from '@fortawesome/free-solid-svg-icons/faCircleExclamation' import {faCircleExclamation} from '@fortawesome/free-solid-svg-icons/faCircleExclamation'
import {faCircleUser} from '@fortawesome/free-regular-svg-icons/faCircleUser' import {faCircleUser} from '@fortawesome/free-regular-svg-icons/faCircleUser'
import {faCircleDot} from '@fortawesome/free-solid-svg-icons/faCircleDot'
import {faClone} from '@fortawesome/free-solid-svg-icons/faClone' import {faClone} from '@fortawesome/free-solid-svg-icons/faClone'
import {faClone as farClone} from '@fortawesome/free-regular-svg-icons/faClone' import {faClone as farClone} from '@fortawesome/free-regular-svg-icons/faClone'
import {faComment} from '@fortawesome/free-regular-svg-icons/faComment' import {faComment} from '@fortawesome/free-regular-svg-icons/faComment'
@ -122,6 +123,7 @@ export function setup() {
farCircleCheck, farCircleCheck,
faCircleExclamation, faCircleExclamation,
faCircleUser, faCircleUser,
faCircleDot,
faClone, faClone,
farClone, farClone,
faComment, faComment,