From 9e8b14f71008e11889bf0107938850433bcd2cd2 Mon Sep 17 00:00:00 2001 From: Paul Frazee Date: Mon, 11 Sep 2023 18:04:09 -0700 Subject: [PATCH] Add birth date gating to moderation settings (#1435) * Add birth date preference, modal to set, link in settings, and age gate in moderation * Styling fixes for android * Fix types --- src/state/models/ui/create-account.ts | 1 + src/state/models/ui/preferences.ts | 15 ++ src/state/models/ui/shell.ts | 5 + src/view/com/modals/BirthDateSettings.tsx | 132 ++++++++++++++++++ .../com/modals/ContentFilteringSettings.tsx | 116 +++++++++------ src/view/com/modals/Modal.tsx | 4 + src/view/com/modals/Modal.web.tsx | 3 + src/view/screens/Settings.tsx | 81 ++++++++--- src/view/shell/desktop/LeftNav.tsx | 2 +- 9 files changed, 297 insertions(+), 62 deletions(-) create mode 100644 src/view/com/modals/BirthDateSettings.tsx diff --git a/src/state/models/ui/create-account.ts b/src/state/models/ui/create-account.ts index c5d9f6d9..d595b200 100644 --- a/src/state/models/ui/create-account.ts +++ b/src/state/models/ui/create-account.ts @@ -118,6 +118,7 @@ export class CreateAccountModel { password: this.password, inviteCode: this.inviteCode, }) + /* dont await */ this.rootStore.preferences.setBirthDate(this.birthDate) track('Create Account') } catch (e: any) { this.rootStore.onboarding.skip() // undo starting the onboard diff --git a/src/state/models/ui/preferences.ts b/src/state/models/ui/preferences.ts index 3b03cdca..64ab4ecb 100644 --- a/src/state/models/ui/preferences.ts +++ b/src/state/models/ui/preferences.ts @@ -7,6 +7,7 @@ import {RootStoreModel} from '../root-store' import {ModerationOpts} from '@atproto/api' import {DEFAULT_FEEDS} from 'lib/constants' import {deviceLocales} from 'platform/detection' +import {getAge} from 'lib/strings/time' import {LANGUAGES} from '../../../locale/languages' // TEMP we need to permanently convert 'show' to 'ignore', for now we manually convert -prf @@ -47,6 +48,7 @@ export class PreferencesModel { contentLabels = new LabelPreferencesModel() savedFeeds: string[] = [] pinnedFeeds: string[] = [] + birthDate: Date | undefined = undefined homeFeedRepliesEnabled: boolean = true homeFeedRepliesThreshold: number = 2 homeFeedRepostsEnabled: boolean = true @@ -60,6 +62,13 @@ export class PreferencesModel { makeAutoObservable(this, {lock: false}, {autoBind: true}) } + get userAge(): number | undefined { + if (!this.birthDate) { + return undefined + } + return getAge(this.birthDate) + } + serialize() { return { contentLanguages: this.contentLanguages, @@ -199,6 +208,7 @@ export class PreferencesModel { ) { this.pinnedFeeds = prefs.feeds.pinned } + this.birthDate = prefs.birthDate }) // set defaults on missing items @@ -430,6 +440,11 @@ export class PreferencesModel { ) } + async setBirthDate(birthDate: Date) { + this.birthDate = birthDate + await this.rootStore.agent.setPersonalDetails({birthDate}) + } + toggleHomeFeedRepliesEnabled() { this.homeFeedRepliesEnabled = !this.homeFeedRepliesEnabled } diff --git a/src/state/models/ui/shell.ts b/src/state/models/ui/shell.ts index 33fdd571..64751356 100644 --- a/src/state/models/ui/shell.ts +++ b/src/state/models/ui/shell.ts @@ -136,6 +136,10 @@ export interface PostLanguagesSettingsModal { name: 'post-languages-settings' } +export interface BirthDateSettingsModal { + name: 'birth-date-settings' +} + export type Modal = // Account | AddAppPasswordModal @@ -143,6 +147,7 @@ export type Modal = | DeleteAccountModal | EditProfileModal | ProfilePreviewModal + | BirthDateSettingsModal // Curation | ContentFilteringSettingsModal diff --git a/src/view/com/modals/BirthDateSettings.tsx b/src/view/com/modals/BirthDateSettings.tsx new file mode 100644 index 00000000..6927ba8d --- /dev/null +++ b/src/view/com/modals/BirthDateSettings.tsx @@ -0,0 +1,132 @@ +import React, {useState} from 'react' +import { + ActivityIndicator, + StyleSheet, + TouchableOpacity, + View, +} from 'react-native' +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' +import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries' +import {cleanError} from 'lib/strings/errors' + +export const snapPoints = ['50%'] + +export const Component = observer(function Component({}: {}) { + const pal = usePalette('default') + const store = useStores() + const [date, setDate] = useState( + store.preferences.birthDate || new Date(), + ) + const [isProcessing, setIsProcessing] = useState(false) + const [error, setError] = useState('') + const {isMobile} = useWebMediaQueries() + + const onSave = async () => { + setError('') + setIsProcessing(true) + try { + await store.preferences.setBirthDate(date) + store.shell.closeModal() + } catch (e) { + setError(cleanError(String(e))) + } finally { + setIsProcessing(false) + } + } + + return ( + + + + My Birthday + + + + + This information is not shared with other users. + + + + + + + {error ? ( + + ) : undefined} + + + {isProcessing ? ( + + + + ) : ( + + Save + + )} + + + ) +}) + +const styles = StyleSheet.create({ + container: { + flex: 1, + paddingBottom: isWeb ? 0 : 40, + }, + titleSection: { + paddingTop: isWeb ? 0 : 4, + paddingBottom: isWeb ? 14 : 10, + }, + title: { + textAlign: 'center', + fontWeight: '600', + marginBottom: 5, + }, + error: { + borderRadius: 6, + marginTop: 10, + }, + dateInputButton: { + borderWidth: 1, + borderRadius: 6, + paddingVertical: 14, + }, + btn: { + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'center', + borderRadius: 32, + padding: 14, + backgroundColor: colors.blue3, + }, + btnContainer: { + paddingTop: 20, + paddingHorizontal: 20, + }, +}) diff --git a/src/view/com/modals/ContentFilteringSettings.tsx b/src/view/com/modals/ContentFilteringSettings.tsx index d2bf278f..aa0674d7 100644 --- a/src/view/com/modals/ContentFilteringSettings.tsx +++ b/src/view/com/modals/ContentFilteringSettings.tsx @@ -9,6 +9,7 @@ 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' @@ -27,22 +28,6 @@ export const Component = observer( store.preferences.sync() }, [store]) - const onToggleAdultContent = React.useCallback(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', - ) - store.log.error('Failed to update preferences with server', {e}) - } - }, [store]) - const onPressDone = React.useCallback(() => { store.shell.closeModal() }, [store]) @@ -51,29 +36,7 @@ export const Component = observer( Content Filtering - - {isIOS ? ( - store.preferences.adultContentEnabled ? null : ( - - Adult content can only be enabled via the Web at{' '} - - . - - ) - ) : ( - - )} - + store.shell.openModal({name: 'birth-date-settings'}) + + 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', + ) + store.log.error('Failed to update preferences with server', {e}) + } + } + + return ( + + {isIOS ? ( + store.preferences.adultContentEnabled ? null : ( + + Adult content can only be enabled via the Web at{' '} + + . + + ) + ) : typeof store.preferences.birthDate === 'undefined' ? ( + + + Confirm your age to enable adult content. + +