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 iconzio/stable
parent
acad8cb455
commit
b6317d4ce7
|
@ -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() {
|
||||||
|
|
|
@ -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`)
|
||||||
|
|
|
@ -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)) &&
|
||||||
|
langCodes.length === postLanguagesPref.length
|
||||||
|
? ['fas', 'circle-dot']
|
||||||
: ['far', 'circle'],
|
: ['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
|
||||||
|
|
|
@ -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)
|
||||||
|
|
||||||
|
// enforce a max of 3 selections for post languages
|
||||||
|
let isDisabled = false
|
||||||
|
if (
|
||||||
|
store.preferences.postLanguage.split(',').length >= 3 &&
|
||||||
|
!isSelected
|
||||||
|
) {
|
||||||
|
isDisabled = true
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ToggleButton
|
||||||
key={lang.code2}
|
key={lang.code2}
|
||||||
code2={lang.code2}
|
label={lang.name}
|
||||||
langType="postLanguages"
|
isSelected={isSelected}
|
||||||
name={lang.name}
|
onPress={() => (isDisabled ? undefined : onPress(lang.code2))}
|
||||||
onPress={() => {
|
style={[
|
||||||
onPress(lang.code2)
|
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,
|
||||||
|
},
|
||||||
})
|
})
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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,
|
||||||
|
|
Loading…
Reference in New Issue