[APP-549] Language controls for Whats Hot (#563)
* Add a content-language preference control * Update whats hot to only show the selected languages and to refresh on lang pref changes * Fix lint * Fix tests * Add missing accessibility role
This commit is contained in:
parent
95f8360d19
commit
6f1c4ec9a9
14 changed files with 381 additions and 93 deletions
|
@ -21,7 +21,7 @@ export function Component({}: {}) {
|
|||
}, [store])
|
||||
|
||||
return (
|
||||
<View testID="reportPostModal" style={[pal.view, styles.container]}>
|
||||
<View testID="contentModerationModal" style={[pal.view, styles.container]}>
|
||||
<Text style={[pal.text, styles.title]}>Content Moderation</Text>
|
||||
<ScrollView style={styles.scrollContainer}>
|
||||
<ContentLabelPref group="nsfw" />
|
||||
|
|
143
src/view/com/modals/ContentLanguagesSettings.tsx
Normal file
143
src/view/com/modals/ContentLanguagesSettings.tsx
Normal file
|
@ -0,0 +1,143 @@
|
|||
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 What's Hot 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,
|
||||
},
|
||||
})
|
|
@ -21,6 +21,7 @@ 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'
|
||||
|
||||
const DEFAULT_SNAPPOINTS = ['90%']
|
||||
|
||||
|
@ -93,6 +94,9 @@ export const ModalsContainer = observer(function ModalsContainer() {
|
|||
} else if (activeModal?.name === 'content-filtering-settings') {
|
||||
snapPoints = ContentFilteringSettingsModal.snapPoints
|
||||
element = <ContentFilteringSettingsModal.Component />
|
||||
} else if (activeModal?.name === 'content-languages-settings') {
|
||||
snapPoints = ContentLanguagesSettingsModal.snapPoints
|
||||
element = <ContentLanguagesSettingsModal.Component />
|
||||
} else {
|
||||
return null
|
||||
}
|
||||
|
|
|
@ -21,6 +21,7 @@ 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'
|
||||
|
||||
export const ModalsContainer = observer(function ModalsContainer() {
|
||||
const store = useStores()
|
||||
|
@ -84,6 +85,8 @@ function Modal({modal}: {modal: ModalIface}) {
|
|||
element = <AddAppPassword.Component />
|
||||
} else if (modal.name === 'content-filtering-settings') {
|
||||
element = <ContentFilteringSettingsModal.Component />
|
||||
} else if (modal.name === 'content-languages-settings') {
|
||||
element = <ContentLanguagesSettingsModal.Component />
|
||||
} else if (modal.name === 'alt-text-image') {
|
||||
element = <AltTextImageModal.Component {...modal} />
|
||||
} else if (modal.name === 'alt-text-image-read') {
|
||||
|
|
|
@ -48,7 +48,6 @@ export function FollowingEmptyState() {
|
|||
}
|
||||
const styles = StyleSheet.create({
|
||||
emptyContainer: {
|
||||
// flex: 1,
|
||||
height: '100%',
|
||||
paddingVertical: 40,
|
||||
paddingHorizontal: 30,
|
||||
|
|
76
src/view/com/posts/WhatsHotEmptyState.tsx
Normal file
76
src/view/com/posts/WhatsHotEmptyState.tsx
Normal file
|
@ -0,0 +1,76 @@
|
|||
import React from 'react'
|
||||
import {StyleSheet, View} from 'react-native'
|
||||
import {
|
||||
FontAwesomeIcon,
|
||||
FontAwesomeIconStyle,
|
||||
} from '@fortawesome/react-native-fontawesome'
|
||||
import {Text} from '../util/text/Text'
|
||||
import {Button} from '../util/forms/Button'
|
||||
import {MagnifyingGlassIcon} from 'lib/icons'
|
||||
import {useStores} from 'state/index'
|
||||
import {usePalette} from 'lib/hooks/usePalette'
|
||||
import {s} from 'lib/styles'
|
||||
|
||||
export function WhatsHotEmptyState() {
|
||||
const pal = usePalette('default')
|
||||
const palInverted = usePalette('inverted')
|
||||
const store = useStores()
|
||||
|
||||
const onPressSettings = React.useCallback(() => {
|
||||
store.shell.openModal({name: 'content-languages-settings'})
|
||||
}, [store])
|
||||
|
||||
return (
|
||||
<View style={styles.emptyContainer}>
|
||||
<View style={styles.emptyIconContainer}>
|
||||
<MagnifyingGlassIcon style={[styles.emptyIcon, pal.text]} size={62} />
|
||||
</View>
|
||||
<Text type="xl-medium" style={[s.textCenter, pal.text]}>
|
||||
Your What's Hot feed is empty! This is because there aren't enough users
|
||||
posting in your selected language.
|
||||
</Text>
|
||||
<Button type="inverted" style={styles.emptyBtn} onPress={onPressSettings}>
|
||||
<Text type="lg-medium" style={palInverted.text}>
|
||||
Update my settings
|
||||
</Text>
|
||||
<FontAwesomeIcon
|
||||
icon="angle-right"
|
||||
style={palInverted.text as FontAwesomeIconStyle}
|
||||
size={14}
|
||||
/>
|
||||
</Button>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
const styles = StyleSheet.create({
|
||||
emptyContainer: {
|
||||
height: '100%',
|
||||
paddingVertical: 40,
|
||||
paddingHorizontal: 30,
|
||||
},
|
||||
emptyIconContainer: {
|
||||
marginBottom: 16,
|
||||
},
|
||||
emptyIcon: {
|
||||
marginLeft: 'auto',
|
||||
marginRight: 'auto',
|
||||
},
|
||||
emptyBtn: {
|
||||
marginVertical: 20,
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
paddingVertical: 18,
|
||||
paddingHorizontal: 24,
|
||||
borderRadius: 30,
|
||||
},
|
||||
|
||||
feedsTip: {
|
||||
position: 'absolute',
|
||||
left: 22,
|
||||
},
|
||||
feedsTipArrow: {
|
||||
marginLeft: 32,
|
||||
marginTop: 8,
|
||||
},
|
||||
})
|
Loading…
Add table
Add a link
Reference in a new issue