[APP-690] better handling of post languages language filtering (#893)

* add SelectLangBtn

* memoized objects that are created to reduce re-creation on re-render

* add langs when uploading post

* only send the top 3 languages otherwise backend will throw error

* mv ContentLanguagesSettings to folder

* add post languages settings modal and state

* fix typos

* modify feed manip to also check langs label on post

* Fix tests

* Remove log

* Update feed-manip.ts

* Fix syntax errors

* UI tuneups

* Show the currently selected languages in the composer

* fix linting

* Use a bcp-47 matching function

* Fix a duplicate language issue

* Fix web

* Dont include lang in prompt

* Make select language btn an observer

* Keep device languages on top of language selection UIs

* Fix android build settings

* Enforce a max of 3 languages in posts

* Fix tests

* Fix types

---------

Co-authored-by: Paul Frazee <pfrazee@gmail.com>
This commit is contained in:
Ansh 2023-06-23 10:48:52 -07:00 committed by GitHub
parent 9b19a95e63
commit 08804f265e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
19 changed files with 525 additions and 176 deletions

View file

@ -1,143 +0,0 @@
import React from 'react'
import {StyleSheet, Pressable, View} from 'react-native'
import LinearGradient from 'react-native-linear-gradient'
import {observer} from 'mobx-react-lite'
import {ScrollView} from './util'
import {useStores} from 'state/index'
import {ToggleButton} from '../util/forms/ToggleButton'
import {s, colors, gradients} from 'lib/styles'
import {Text} from '../util/text/Text'
import {usePalette} from 'lib/hooks/usePalette'
import {isDesktopWeb} from 'platform/detection'
import {LANGUAGES, LANGUAGES_MAP_CODE2} from '../../../locale/languages'
export const snapPoints = ['100%']
export function Component({}: {}) {
const store = useStores()
const pal = usePalette('default')
const onPressDone = React.useCallback(() => {
store.shell.closeModal()
}, [store])
const languages = React.useMemo(() => {
const langs = LANGUAGES.filter(
lang =>
!!lang.code2.trim() &&
LANGUAGES_MAP_CODE2[lang.code2].code3 === lang.code3,
)
// sort so that selected languages are on top, then alphabetically
langs.sort((a, b) => {
const hasA = store.preferences.hasContentLanguage(a.code2)
const hasB = store.preferences.hasContentLanguage(b.code2)
if (hasA === hasB) return a.name.localeCompare(b.name)
if (hasA) return -1
return 1
})
return langs
}, [store])
return (
<View testID="contentLanguagesModal" style={[pal.view, styles.container]}>
<Text style={[pal.text, styles.title]}>Content Languages</Text>
<Text style={[pal.text, styles.description]}>
Which languages would you like to see in the your feed? (Leave them all
unchecked to see any language.)
</Text>
<ScrollView style={styles.scrollContainer}>
{languages.map(lang => (
<LanguageToggle
key={lang.code2}
code2={lang.code2}
name={lang.name}
/>
))}
<View style={styles.bottomSpacer} />
</ScrollView>
<View style={[styles.btnContainer, pal.borderDark]}>
<Pressable
testID="sendReportBtn"
onPress={onPressDone}
accessibilityRole="button"
accessibilityLabel="Confirm content language settings"
accessibilityHint="">
<LinearGradient
colors={[gradients.blueLight.start, gradients.blueLight.end]}
start={{x: 0, y: 0}}
end={{x: 1, y: 1}}
style={[styles.btn]}>
<Text style={[s.white, s.bold, s.f18]}>Done</Text>
</LinearGradient>
</Pressable>
</View>
</View>
)
}
const LanguageToggle = observer(
({code2, name}: {code2: string; name: string}) => {
const store = useStores()
const pal = usePalette('default')
const onPress = React.useCallback(() => {
store.preferences.toggleContentLanguage(code2)
}, [store, code2])
return (
<ToggleButton
label={name}
isSelected={store.preferences.contentLanguages.includes(code2)}
onPress={onPress}
style={[pal.border, styles.languageToggle]}
/>
)
},
)
const styles = StyleSheet.create({
container: {
flex: 1,
paddingTop: 20,
},
title: {
textAlign: 'center',
fontWeight: 'bold',
fontSize: 24,
marginBottom: 12,
},
description: {
textAlign: 'center',
paddingHorizontal: 16,
marginBottom: 10,
},
scrollContainer: {
flex: 1,
paddingHorizontal: 10,
},
bottomSpacer: {
height: isDesktopWeb ? 0 : 60,
},
btnContainer: {
paddingTop: 10,
paddingHorizontal: 10,
paddingBottom: isDesktopWeb ? 0 : 40,
borderTopWidth: isDesktopWeb ? 0 : 1,
},
languageToggle: {
borderTopWidth: 1,
borderRadius: 0,
paddingHorizontal: 0,
paddingVertical: 12,
},
btn: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center',
width: '100%',
borderRadius: 32,
padding: 14,
backgroundColor: colors.gray1,
},
})

View file

@ -23,7 +23,8 @@ import * as WaitlistModal from './Waitlist'
import * as InviteCodesModal from './InviteCodes'
import * as AddAppPassword from './AddAppPasswords'
import * as ContentFilteringSettingsModal from './ContentFilteringSettings'
import * as ContentLanguagesSettingsModal from './ContentLanguagesSettings'
import * as ContentLanguagesSettingsModal from './lang-settings/ContentLanguagesSettings'
import * as PostLanguagesSettingsModal from './lang-settings/PostLanguagesSettings'
import * as PreferencesHomeFeed from './PreferencesHomeFeed'
const DEFAULT_SNAPPOINTS = ['90%']
@ -106,6 +107,9 @@ export const ModalsContainer = observer(function ModalsContainer() {
} else if (activeModal?.name === 'content-languages-settings') {
snapPoints = ContentLanguagesSettingsModal.snapPoints
element = <ContentLanguagesSettingsModal.Component />
} else if (activeModal?.name === 'post-languages-settings') {
snapPoints = PostLanguagesSettingsModal.snapPoints
element = <PostLanguagesSettingsModal.Component />
} else if (activeModal?.name === 'preferences-home-feed') {
snapPoints = PreferencesHomeFeed.snapPoints
element = <PreferencesHomeFeed.Component />

View file

@ -23,7 +23,9 @@ import * as WaitlistModal from './Waitlist'
import * as InviteCodesModal from './InviteCodes'
import * as AddAppPassword from './AddAppPasswords'
import * as ContentFilteringSettingsModal from './ContentFilteringSettings'
import * as ContentLanguagesSettingsModal from './ContentLanguagesSettings'
import * as ContentLanguagesSettingsModal from './lang-settings/ContentLanguagesSettings'
import * as PostLanguagesSettingsModal from './lang-settings/PostLanguagesSettings'
import * as PreferencesHomeFeed from './PreferencesHomeFeed'
export const ModalsContainer = observer(function ModalsContainer() {
@ -94,6 +96,8 @@ function Modal({modal}: {modal: ModalIface}) {
element = <ContentFilteringSettingsModal.Component />
} else if (modal.name === 'content-languages-settings') {
element = <ContentLanguagesSettingsModal.Component />
} else if (modal.name === 'post-languages-settings') {
element = <PostLanguagesSettingsModal.Component />
} else if (modal.name === 'alt-text-image') {
element = <AltTextImageModal.Component {...modal} />
} else if (modal.name === 'edit-image') {

View file

@ -0,0 +1,52 @@
import React from 'react'
import {StyleSheet, Text, View, Pressable} from 'react-native'
import LinearGradient from 'react-native-linear-gradient'
import {s, colors, gradients} from 'lib/styles'
import {isDesktopWeb} from 'platform/detection'
import {usePalette} from 'lib/hooks/usePalette'
export const ConfirmLanguagesButton = ({
onPress,
extraText,
}: {
onPress: () => void
extraText?: string
}) => {
const pal = usePalette('default')
return (
<View style={[styles.btnContainer, pal.borderDark]}>
<Pressable
testID="confirmContentLanguagesBtn"
onPress={onPress}
accessibilityRole="button"
accessibilityLabel="Confirm content language settings"
accessibilityHint="">
<LinearGradient
colors={[gradients.blueLight.start, gradients.blueLight.end]}
start={{x: 0, y: 0}}
end={{x: 1, y: 1}}
style={[styles.btn]}>
<Text style={[s.white, s.bold, s.f18]}>Done{extraText}</Text>
</LinearGradient>
</Pressable>
</View>
)
}
const styles = StyleSheet.create({
btnContainer: {
paddingTop: 10,
paddingHorizontal: 10,
paddingBottom: isDesktopWeb ? 0 : 40,
borderTopWidth: isDesktopWeb ? 0 : 1,
},
btn: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center',
width: '100%',
borderRadius: 32,
padding: 14,
backgroundColor: colors.gray1,
},
})

View file

@ -0,0 +1,100 @@
import React from 'react'
import {StyleSheet, View} from 'react-native'
import {ScrollView} from '../util'
import {useStores} from 'state/index'
import {Text} from '../../util/text/Text'
import {usePalette} from 'lib/hooks/usePalette'
import {isDesktopWeb, deviceLocales} from 'platform/detection'
import {LANGUAGES, LANGUAGES_MAP_CODE2} from '../../../../locale/languages'
import {LanguageToggle} from './LanguageToggle'
import {ConfirmLanguagesButton} from './ConfirmLanguagesButton'
export const snapPoints = ['100%']
export function Component({}: {}) {
const store = useStores()
const pal = usePalette('default')
const onPressDone = React.useCallback(() => {
store.shell.closeModal()
}, [store])
const languages = React.useMemo(() => {
const langs = LANGUAGES.filter(
lang =>
!!lang.code2.trim() &&
LANGUAGES_MAP_CODE2[lang.code2].code3 === lang.code3,
)
// sort so that device & selected languages are on top, then alphabetically
langs.sort((a, b) => {
const hasA =
store.preferences.hasContentLanguage(a.code2) ||
deviceLocales.includes(a.code2)
const hasB =
store.preferences.hasContentLanguage(b.code2) ||
deviceLocales.includes(b.code2)
if (hasA === hasB) return a.name.localeCompare(b.name)
if (hasA) return -1
return 1
})
return langs
}, [store])
const onPress = React.useCallback(
(code2: string) => {
store.preferences.toggleContentLanguage(code2)
},
[store],
)
return (
<View testID="contentLanguagesModal" style={[pal.view, styles.container]}>
<Text style={[pal.text, styles.title]}>Content Languages</Text>
<Text style={[pal.text, styles.description]}>
Which languages would you like to see in your algorithmic feeds?
</Text>
<Text style={[pal.textLight, styles.description]}>
Leave them all unchecked to see any language.
</Text>
<ScrollView style={styles.scrollContainer}>
{languages.map(lang => (
<LanguageToggle
key={lang.code2}
code2={lang.code2}
langType="contentLanguages"
name={lang.name}
onPress={() => {
onPress(lang.code2)
}}
/>
))}
<View style={styles.bottomSpacer} />
</ScrollView>
<ConfirmLanguagesButton onPress={onPressDone} />
</View>
)
}
const styles = StyleSheet.create({
container: {
flex: 1,
paddingTop: 20,
},
title: {
textAlign: 'center',
fontWeight: 'bold',
fontSize: 24,
marginBottom: 12,
},
description: {
textAlign: 'center',
paddingHorizontal: 16,
marginBottom: 10,
},
scrollContainer: {
flex: 1,
paddingHorizontal: 10,
},
bottomSpacer: {
height: isDesktopWeb ? 0 : 60,
},
})

View file

@ -0,0 +1,56 @@
import React from 'react'
import {StyleSheet} from 'react-native'
import {usePalette} from 'lib/hooks/usePalette'
import {observer} from 'mobx-react-lite'
import {ToggleButton} from 'view/com/util/forms/ToggleButton'
import {useStores} from 'state/index'
export const LanguageToggle = observer(
({
code2,
name,
onPress,
langType,
}: {
code2: string
name: string
onPress: () => void
langType: 'contentLanguages' | 'postLanguages'
}) => {
const pal = usePalette('default')
const store = useStores()
const isSelected = store.preferences[langType].includes(code2)
// enforce a max of 3 selections for post languages
let isDisabled = false
if (
langType === 'postLanguages' &&
store.preferences[langType].length >= 3 &&
!isSelected
) {
isDisabled = true
}
return (
<ToggleButton
label={name}
isSelected={isSelected}
onPress={isDisabled ? undefined : onPress}
style={[pal.border, styles.languageToggle, isDisabled && styles.dimmed]}
/>
)
},
)
const styles = StyleSheet.create({
languageToggle: {
borderTopWidth: 1,
borderRadius: 0,
paddingHorizontal: 6,
paddingVertical: 12,
},
dimmed: {
opacity: 0.5,
},
})

View file

@ -0,0 +1,97 @@
import React from 'react'
import {StyleSheet, View} from 'react-native'
import {ScrollView} from '../util'
import {useStores} from 'state/index'
import {Text} from '../../util/text/Text'
import {usePalette} from 'lib/hooks/usePalette'
import {isDesktopWeb, deviceLocales} from 'platform/detection'
import {LANGUAGES, LANGUAGES_MAP_CODE2} from '../../../../locale/languages'
import {LanguageToggle} from './LanguageToggle'
import {ConfirmLanguagesButton} from './ConfirmLanguagesButton'
export const snapPoints = ['100%']
export function Component({}: {}) {
const store = useStores()
const pal = usePalette('default')
const onPressDone = React.useCallback(() => {
store.shell.closeModal()
}, [store])
const languages = React.useMemo(() => {
const langs = LANGUAGES.filter(
lang =>
!!lang.code2.trim() &&
LANGUAGES_MAP_CODE2[lang.code2].code3 === lang.code3,
)
// sort so that device & selected languages are on top, then alphabetically
langs.sort((a, b) => {
const hasA =
store.preferences.hasPostLanguage(a.code2) ||
deviceLocales.includes(a.code2)
const hasB =
store.preferences.hasPostLanguage(b.code2) ||
deviceLocales.includes(b.code2)
if (hasA === hasB) return a.name.localeCompare(b.name)
if (hasA) return -1
return 1
})
return langs
}, [store])
const onPress = React.useCallback(
(code2: string) => {
store.preferences.togglePostLanguage(code2)
},
[store],
)
return (
<View testID="postLanguagesModal" style={[pal.view, styles.container]}>
<Text style={[pal.text, styles.title]}>Post Languages</Text>
<Text style={[pal.text, styles.description]}>
Which languages are used in this post?
</Text>
<ScrollView style={styles.scrollContainer}>
{languages.map(lang => (
<LanguageToggle
key={lang.code2}
code2={lang.code2}
langType="postLanguages"
name={lang.name}
onPress={() => {
onPress(lang.code2)
}}
/>
))}
<View style={styles.bottomSpacer} />
</ScrollView>
<ConfirmLanguagesButton onPress={onPressDone} />
</View>
)
}
const styles = StyleSheet.create({
container: {
flex: 1,
paddingTop: 20,
},
title: {
textAlign: 'center',
fontWeight: 'bold',
fontSize: 24,
marginBottom: 12,
},
description: {
textAlign: 'center',
paddingHorizontal: 16,
marginBottom: 10,
},
scrollContainer: {
flex: 1,
paddingHorizontal: 10,
},
bottomSpacer: {
height: isDesktopWeb ? 0 : 60,
},
})