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:
parent
c8c308e31e
commit
05b728fffc
22 changed files with 1339 additions and 914 deletions
|
|
@ -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({
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue