Eric/preferences (#1873)

* Add initial preferences query, couple mutations

* Remove unused

* Clean up labels, migrate getModerationOpts

* Add birth date handling

* Migrate feed prefs

* Migrate thread view prefs

* Migrate homeFeed to use existing key name

* Fix up saved feeds in response, no impl yet

* Migrate saved feeds to new hooks

* Clean up more of preferences

* Fix PreferencesThreads load state

* Fix modal dismissal

* Small spacing fix

---------

Co-authored-by: Paul Frazee <pfrazee@gmail.com>
This commit is contained in:
Eric Bailey 2023-11-12 13:31:11 -06:00 committed by GitHub
parent c8c308e31e
commit 05b728fffc
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
22 changed files with 1339 additions and 914 deletions

View file

@ -9,7 +9,6 @@ import {observer} from 'mobx-react-lite'
import {Text} from '../util/text/Text'
import {DateInput} from '../util/forms/DateInput'
import {ErrorMessage} from '../util/error/ErrorMessage'
import {useStores} from 'state/index'
import {s, colors} from 'lib/styles'
import {usePalette} from 'lib/hooks/usePalette'
import {isWeb} from 'platform/detection'
@ -18,33 +17,36 @@ import {cleanError} from 'lib/strings/errors'
import {Trans, msg} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {useModalControls} from '#/state/modals'
import {
usePreferencesQuery,
usePreferencesSetBirthDateMutation,
UsePreferencesQueryResponse,
} from '#/state/queries/preferences'
import {logger} from '#/logger'
export const snapPoints = ['50%']
export const Component = observer(function Component({}: {}) {
function Inner({preferences}: {preferences: UsePreferencesQueryResponse}) {
const pal = usePalette('default')
const store = useStores()
const {_} = useLingui()
const {closeModal} = useModalControls()
const [date, setDate] = useState<Date>(
store.preferences.birthDate || new Date(),
)
const [isProcessing, setIsProcessing] = useState<boolean>(false)
const [error, setError] = useState<string>('')
const {isMobile} = useWebMediaQueries()
const {_} = useLingui()
const {
isPending,
isError,
error,
mutateAsync: setBirthDate,
} = usePreferencesSetBirthDateMutation()
const [date, setDate] = useState(preferences.birthDate || new Date())
const {closeModal} = useModalControls()
const onSave = async () => {
setError('')
setIsProcessing(true)
const onSave = React.useCallback(async () => {
try {
await store.preferences.setBirthDate(date)
await setBirthDate({birthDate: date})
closeModal()
} catch (e) {
setError(cleanError(String(e)))
} finally {
setIsProcessing(false)
logger.error(`setBirthDate failed`, {error: e})
}
}
}, [date, setBirthDate, closeModal])
return (
<View
@ -74,12 +76,12 @@ export const Component = observer(function Component({}: {}) {
/>
</View>
{error ? (
<ErrorMessage message={error} style={styles.error} />
{isError ? (
<ErrorMessage message={cleanError(error)} style={styles.error} />
) : undefined}
<View style={[styles.btnContainer, pal.borderDark]}>
{isProcessing ? (
{isPending ? (
<View style={styles.btn}>
<ActivityIndicator color="#fff" />
</View>
@ -99,6 +101,16 @@ export const Component = observer(function Component({}: {}) {
</View>
</View>
)
}
export const Component = observer(function Component({}: {}) {
const {data: preferences} = usePreferencesQuery()
return !preferences ? (
<ActivityIndicator />
) : (
<Inner preferences={preferences} />
)
})
const styles = StyleSheet.create({

View file

@ -1,17 +1,15 @@
import React from 'react'
import {BskyPreferences, LabelPreference} from '@atproto/api'
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 {LabelPreference} from 'state/models/ui/preferences'
import {s, colors, gradients} from 'lib/styles'
import {Text} from '../util/text/Text'
import {TextLink} from '../util/Link'
import {ToggleButton} from '../util/forms/ToggleButton'
import {Button} from '../util/forms/Button'
import {usePalette} from 'lib/hooks/usePalette'
import {CONFIGURABLE_LABEL_GROUPS} from 'lib/labeling/const'
import {isIOS} from 'platform/detection'
import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
import * as Toast from '../util/Toast'
@ -19,20 +17,23 @@ import {logger} from '#/logger'
import {Trans, msg} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {useModalControls} from '#/state/modals'
import {
usePreferencesQuery,
usePreferencesSetContentLabelMutation,
usePreferencesSetAdultContentMutation,
ConfigurableLabelGroup,
CONFIGURABLE_LABEL_GROUPS,
} from '#/state/queries/preferences'
export const snapPoints = ['90%']
export const Component = observer(
function ContentFilteringSettingsImpl({}: {}) {
const store = useStores()
const {isMobile} = useWebMediaQueries()
const pal = usePalette('default')
const {_} = useLingui()
const {closeModal} = useModalControls()
React.useEffect(() => {
store.preferences.sync()
}, [store])
const {data: preferences} = usePreferencesQuery()
const onPressDone = React.useCallback(() => {
closeModal()
@ -43,29 +44,38 @@ export const Component = observer(
<Text style={[pal.text, styles.title]}>
<Trans>Content Filtering</Trans>
</Text>
<ScrollView style={styles.scrollContainer}>
<AdultContentEnabledPref />
<ContentLabelPref
group="nsfw"
disabled={!store.preferences.adultContentEnabled}
preferences={preferences}
labelGroup="nsfw"
disabled={!preferences?.adultContentEnabled}
/>
<ContentLabelPref
group="nudity"
disabled={!store.preferences.adultContentEnabled}
preferences={preferences}
labelGroup="nudity"
disabled={!preferences?.adultContentEnabled}
/>
<ContentLabelPref
group="suggestive"
disabled={!store.preferences.adultContentEnabled}
preferences={preferences}
labelGroup="suggestive"
disabled={!preferences?.adultContentEnabled}
/>
<ContentLabelPref
group="gore"
disabled={!store.preferences.adultContentEnabled}
preferences={preferences}
labelGroup="gore"
disabled={!preferences?.adultContentEnabled}
/>
<ContentLabelPref preferences={preferences} labelGroup="hate" />
<ContentLabelPref preferences={preferences} labelGroup="spam" />
<ContentLabelPref
preferences={preferences}
labelGroup="impersonation"
/>
<ContentLabelPref group="hate" />
<ContentLabelPref group="spam" />
<ContentLabelPref group="impersonation" />
<View style={{height: isMobile ? 60 : 0}} />
</ScrollView>
<View
style={[
styles.btnContainer,
@ -94,118 +104,114 @@ export const Component = observer(
},
)
const AdultContentEnabledPref = observer(
function AdultContentEnabledPrefImpl() {
const store = useStores()
const pal = usePalette('default')
const {openModal} = useModalControls()
function AdultContentEnabledPref() {
const pal = usePalette('default')
const {data: preferences} = usePreferencesQuery()
const {mutate, variables} = usePreferencesSetAdultContentMutation()
const {openModal} = useModalControls()
const onSetAge = () => openModal({name: 'birth-date-settings'})
const onSetAge = React.useCallback(
() => openModal({name: 'birth-date-settings'}),
[openModal],
)
const onToggleAdultContent = async () => {
if (isIOS) {
return
}
try {
await store.preferences.setAdultContentEnabled(
!store.preferences.adultContentEnabled,
)
} catch (e) {
Toast.show(
'There was an issue syncing your preferences with the server',
)
logger.error('Failed to update preferences with server', {error: e})
}
const onToggleAdultContent = React.useCallback(async () => {
if (isIOS) return
try {
mutate({
enabled: !(variables?.enabled ?? preferences?.adultContentEnabled),
})
} catch (e) {
Toast.show('There was an issue syncing your preferences with the server')
logger.error('Failed to update preferences with server', {error: e})
}
}, [variables, preferences, mutate])
return (
<View style={s.mb10}>
{isIOS ? (
store.preferences.adultContentEnabled ? null : (
<Text type="md" style={pal.textLight}>
Adult content can only be enabled via the Web at{' '}
<TextLink
style={pal.link}
href="https://bsky.app"
text="bsky.app"
/>
.
</Text>
)
) : typeof store.preferences.birthDate === 'undefined' ? (
<View style={[pal.viewLight, styles.agePrompt]}>
<Text type="md" style={[pal.text, {flex: 1}]}>
Confirm your age to enable adult content.
</Text>
<Button type="primary" label="Set Age" onPress={onSetAge} />
</View>
) : (store.preferences.userAge || 0) >= 18 ? (
<ToggleButton
type="default-light"
label="Enable Adult Content"
isSelected={store.preferences.adultContentEnabled}
onPress={onToggleAdultContent}
style={styles.toggleBtn}
/>
) : (
<View style={[pal.viewLight, styles.agePrompt]}>
<Text type="md" style={[pal.text, {flex: 1}]}>
You must be 18 or older to enable adult content.
</Text>
<Button type="primary" label="Set Age" onPress={onSetAge} />
</View>
)}
</View>
)
},
)
return (
<View style={s.mb10}>
{isIOS ? (
preferences?.adultContentEnabled ? null : (
<Text type="md" style={pal.textLight}>
Adult content can only be enabled via the Web at{' '}
<TextLink
style={pal.link}
href="https://bsky.app"
text="bsky.app"
/>
.
</Text>
)
) : typeof preferences?.birthDate === 'undefined' ? (
<View style={[pal.viewLight, styles.agePrompt]}>
<Text type="md" style={[pal.text, {flex: 1}]}>
Confirm your age to enable adult content.
</Text>
<Button type="primary" label="Set Age" onPress={onSetAge} />
</View>
) : (preferences.userAge || 0) >= 18 ? (
<ToggleButton
type="default-light"
label="Enable Adult Content"
isSelected={variables?.enabled ?? preferences?.adultContentEnabled}
onPress={onToggleAdultContent}
style={styles.toggleBtn}
/>
) : (
<View style={[pal.viewLight, styles.agePrompt]}>
<Text type="md" style={[pal.text, {flex: 1}]}>
You must be 18 or older to enable adult content.
</Text>
<Button type="primary" label="Set Age" onPress={onSetAge} />
</View>
)}
</View>
)
}
// TODO: Refactor this component to pass labels down to each tab
const ContentLabelPref = observer(function ContentLabelPrefImpl({
group,
preferences,
labelGroup,
disabled,
}: {
group: keyof typeof CONFIGURABLE_LABEL_GROUPS
preferences?: BskyPreferences
labelGroup: ConfigurableLabelGroup
disabled?: boolean
}) {
const store = useStores()
const pal = usePalette('default')
const visibility = preferences?.contentLabels?.[labelGroup]
const {mutate, variables} = usePreferencesSetContentLabelMutation()
const onChange = React.useCallback(
async (v: LabelPreference) => {
try {
await store.preferences.setContentLabelPref(group, v)
} catch (e) {
Toast.show(
'There was an issue syncing your preferences with the server',
)
logger.error('Failed to update preferences with server', {error: e})
}
(vis: LabelPreference) => {
mutate({labelGroup, visibility: vis})
},
[store, group],
[mutate, labelGroup],
)
return (
<View style={[styles.contentLabelPref, pal.border]}>
<View style={s.flex1}>
<Text type="md-medium" style={[pal.text]}>
{CONFIGURABLE_LABEL_GROUPS[group].title}
{CONFIGURABLE_LABEL_GROUPS[labelGroup].title}
</Text>
{typeof CONFIGURABLE_LABEL_GROUPS[group].subtitle === 'string' && (
{typeof CONFIGURABLE_LABEL_GROUPS[labelGroup].subtitle === 'string' && (
<Text type="sm" style={[pal.textLight]}>
{CONFIGURABLE_LABEL_GROUPS[group].subtitle}
{CONFIGURABLE_LABEL_GROUPS[labelGroup].subtitle}
</Text>
)}
</View>
{disabled ? (
{disabled || !visibility ? (
<Text type="sm-bold" style={pal.textLight}>
Hide
</Text>
) : (
<SelectGroup
current={store.preferences.contentLabels[group]}
current={variables?.visibility || visibility}
onChange={onChange}
group={group}
labelGroup={labelGroup}
/>
)}
</View>
@ -215,10 +221,10 @@ const ContentLabelPref = observer(function ContentLabelPrefImpl({
interface SelectGroupProps {
current: LabelPreference
onChange: (v: LabelPreference) => void
group: keyof typeof CONFIGURABLE_LABEL_GROUPS
labelGroup: ConfigurableLabelGroup
}
function SelectGroup({current, onChange, group}: SelectGroupProps) {
function SelectGroup({current, onChange, labelGroup}: SelectGroupProps) {
return (
<View style={styles.selectableBtns}>
<SelectableBtn
@ -227,14 +233,14 @@ function SelectGroup({current, onChange, group}: SelectGroupProps) {
label="Hide"
left
onChange={onChange}
group={group}
labelGroup={labelGroup}
/>
<SelectableBtn
current={current}
value="warn"
label="Warn"
onChange={onChange}
group={group}
labelGroup={labelGroup}
/>
<SelectableBtn
current={current}
@ -242,7 +248,7 @@ function SelectGroup({current, onChange, group}: SelectGroupProps) {
label="Show"
right
onChange={onChange}
group={group}
labelGroup={labelGroup}
/>
</View>
)
@ -255,7 +261,7 @@ interface SelectableBtnProps {
left?: boolean
right?: boolean
onChange: (v: LabelPreference) => void
group: keyof typeof CONFIGURABLE_LABEL_GROUPS
labelGroup: ConfigurableLabelGroup
}
function SelectableBtn({
@ -265,7 +271,7 @@ function SelectableBtn({
left,
right,
onChange,
group,
labelGroup,
}: SelectableBtnProps) {
const pal = usePalette('default')
const palPrimary = usePalette('inverted')
@ -281,7 +287,7 @@ function SelectableBtn({
onPress={() => onChange(value)}
accessibilityRole="button"
accessibilityLabel={value}
accessibilityHint={`Set ${value} for ${group} content moderation policy`}>
accessibilityHint={`Set ${value} for ${labelGroup} content moderation policy`}>
<Text style={current === value ? palPrimary.text : pal.text}>
{label}
</Text>