diff options
Diffstat (limited to 'src')
-rw-r--r-- | src/state/models/ui/create-account.ts | 1 | ||||
-rw-r--r-- | src/state/models/ui/preferences.ts | 15 | ||||
-rw-r--r-- | src/state/models/ui/shell.ts | 5 | ||||
-rw-r--r-- | src/view/com/modals/BirthDateSettings.tsx | 132 | ||||
-rw-r--r-- | src/view/com/modals/ContentFilteringSettings.tsx | 116 | ||||
-rw-r--r-- | src/view/com/modals/Modal.tsx | 4 | ||||
-rw-r--r-- | src/view/com/modals/Modal.web.tsx | 3 | ||||
-rw-r--r-- | src/view/screens/Settings.tsx | 81 | ||||
-rw-r--r-- | src/view/shell/desktop/LeftNav.tsx | 2 |
9 files changed, 297 insertions, 62 deletions
diff --git a/src/state/models/ui/create-account.ts b/src/state/models/ui/create-account.ts index c5d9f6d9b..d595b200b 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 3b03cdca1..64ab4ecba 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 33fdd5710..647513563 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 000000000..6927ba8d2 --- /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<Date>( + store.preferences.birthDate || new Date(), + ) + const [isProcessing, setIsProcessing] = useState<boolean>(false) + const [error, setError] = useState<string>('') + 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 ( + <View + testID="birthDateSettingsModal" + style={[pal.view, styles.container, isMobile && {paddingHorizontal: 18}]}> + <View style={styles.titleSection}> + <Text type="title-lg" style={[pal.text, styles.title]}> + My Birthday + </Text> + </View> + + <Text type="lg" style={[pal.textLight, {marginBottom: 10}]}> + This information is not shared with other users. + </Text> + + <View> + <DateInput + testID="birthdayInput" + value={date} + onChange={setDate} + buttonType="default-light" + buttonStyle={[pal.border, styles.dateInputButton]} + buttonLabelType="lg" + accessibilityLabel="Birthday" + accessibilityHint="Enter your birth date" + accessibilityLabelledBy="birthDate" + /> + </View> + + {error ? ( + <ErrorMessage message={error} style={styles.error} /> + ) : undefined} + + <View style={[styles.btnContainer, pal.borderDark]}> + {isProcessing ? ( + <View style={styles.btn}> + <ActivityIndicator color="#fff" /> + </View> + ) : ( + <TouchableOpacity + testID="confirmBtn" + onPress={onSave} + style={styles.btn} + accessibilityRole="button" + accessibilityLabel="Save" + accessibilityHint=""> + <Text style={[s.white, s.bold, s.f18]}>Save</Text> + </TouchableOpacity> + )} + </View> + </View> + ) +}) + +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 d2bf278f5..aa0674d7a 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( <View testID="contentFilteringModal" style={[pal.view, styles.container]}> <Text style={[pal.text, styles.title]}>Content Filtering</Text> <ScrollView style={styles.scrollContainer}> - <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> - ) - ) : ( - <ToggleButton - type="default-light" - label="Enable Adult Content" - isSelected={store.preferences.adultContentEnabled} - onPress={onToggleAdultContent} - style={styles.toggleBtn} - /> - )} - </View> + <AdultContentEnabledPref /> <ContentLabelPref group="nsfw" disabled={!store.preferences.adultContentEnabled} @@ -121,6 +84,71 @@ export const Component = observer( }, ) +const AdultContentEnabledPref = observer( + function AdultContentEnabledPrefImpl() { + const store = useStores() + const pal = usePalette('default') + + const onSetAge = () => 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 ( + <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> + ) + }, +) + // TODO: Refactor this component to pass labels down to each tab const ContentLabelPref = observer(function ContentLabelPrefImpl({ group, @@ -277,6 +305,16 @@ const styles = StyleSheet.create({ borderTopWidth: 1, }, + agePrompt: { + flexDirection: 'row', + justifyContent: 'space-between', + alignItems: 'center', + paddingLeft: 14, + paddingRight: 10, + paddingVertical: 8, + borderRadius: 8, + }, + contentLabelPref: { flexDirection: 'row', justifyContent: 'space-between', diff --git a/src/view/com/modals/Modal.tsx b/src/view/com/modals/Modal.tsx index 4a5a7c504..d79c77db3 100644 --- a/src/view/com/modals/Modal.tsx +++ b/src/view/com/modals/Modal.tsx @@ -29,6 +29,7 @@ import * as ContentFilteringSettingsModal from './ContentFilteringSettings' import * as ContentLanguagesSettingsModal from './lang-settings/ContentLanguagesSettings' import * as PostLanguagesSettingsModal from './lang-settings/PostLanguagesSettings' import * as ModerationDetailsModal from './ModerationDetails' +import * as BirthDateSettingsModal from './BirthDateSettings' const DEFAULT_SNAPPOINTS = ['90%'] @@ -132,6 +133,9 @@ export const ModalsContainer = observer(function ModalsContainer() { } else if (activeModal?.name === 'moderation-details') { snapPoints = ModerationDetailsModal.snapPoints element = <ModerationDetailsModal.Component {...activeModal} /> + } else if (activeModal?.name === 'birth-date-settings') { + snapPoints = BirthDateSettingsModal.snapPoints + element = <BirthDateSettingsModal.Component /> } else { return null } diff --git a/src/view/com/modals/Modal.web.tsx b/src/view/com/modals/Modal.web.tsx index 86fa28faf..3e87e0e3c 100644 --- a/src/view/com/modals/Modal.web.tsx +++ b/src/view/com/modals/Modal.web.tsx @@ -27,6 +27,7 @@ import * as ContentFilteringSettingsModal from './ContentFilteringSettings' import * as ContentLanguagesSettingsModal from './lang-settings/ContentLanguagesSettings' import * as PostLanguagesSettingsModal from './lang-settings/PostLanguagesSettings' import * as ModerationDetailsModal from './ModerationDetails' +import * as BirthDateSettingsModal from './BirthDateSettings' export const ModalsContainer = observer(function ModalsContainer() { const store = useStores() @@ -107,6 +108,8 @@ function Modal({modal}: {modal: ModalIface}) { element = <EditImageModal.Component {...modal} /> } else if (modal.name === 'moderation-details') { element = <ModerationDetailsModal.Component {...modal} /> + } else if (modal.name === 'birth-date-settings') { + element = <BirthDateSettingsModal.Component /> } else { return null } diff --git a/src/view/screens/Settings.tsx b/src/view/screens/Settings.tsx index a416bad45..8a543fa4c 100644 --- a/src/view/screens/Settings.tsx +++ b/src/view/screens/Settings.tsx @@ -40,6 +40,7 @@ import {AccountData} from 'state/models/session' import {useAnalytics} from 'lib/analytics/analytics' import {NavigationProp} from 'lib/routes/types' import {pluralize} from 'lib/strings/helpers' +import {HandIcon} from 'lib/icons' import {formatCount} from 'view/com/util/numeric/format' import Clipboard from '@react-native-clipboard/clipboard' import {reset as resetNavigation} from '../../Navigation' @@ -175,7 +176,7 @@ export const SettingsScreen = withAuthRequired( Toast.show('Copied build version to clipboard') }, []) - const openPreferencesModal = React.useCallback(() => { + const openHomeFeedPreferences = React.useCallback(() => { navigation.navigate('PreferencesHomeFeed') }, [navigation]) @@ -220,6 +221,19 @@ export const SettingsScreen = withAuthRequired( </Text> </Text> </View> + <View style={[styles.infoLine]}> + <Text type="lg-medium" style={pal.text}> + Birthday:{' '} + </Text> + <Link + onPress={() => + store.shell.openModal({name: 'birth-date-settings'}) + }> + <Text type="lg" style={pal.link}> + Show + </Text> + </Link> + </View> <View style={styles.spacer20} /> </> ) : null} @@ -387,15 +401,15 @@ export const SettingsScreen = withAuthRequired( <View style={styles.spacer20} /> <Text type="xl-bold" style={[pal.text, styles.heading]}> - Advanced + Basics </Text> <TouchableOpacity testID="preferencesHomeFeedButton" style={[styles.linkCard, pal.view, isSwitching && styles.dimmed]} - onPress={openPreferencesModal} + onPress={openHomeFeedPreferences} accessibilityRole="button" - accessibilityHint="Open home feed preferences modal" - accessibilityLabel="Opens the home feed preferences modal"> + accessibilityHint="" + accessibilityLabel="Opens the home feed preferences"> <View style={[styles.iconContainer, pal.btn]}> <FontAwesomeIcon icon="sliders" @@ -407,23 +421,6 @@ export const SettingsScreen = withAuthRequired( </Text> </TouchableOpacity> <TouchableOpacity - testID="appPasswordBtn" - style={[styles.linkCard, pal.view, isSwitching && styles.dimmed]} - onPress={onPressAppPasswords} - accessibilityRole="button" - accessibilityHint="Open app password settings" - accessibilityLabel="Opens the app password settings page"> - <View style={[styles.iconContainer, pal.btn]}> - <FontAwesomeIcon - icon="lock" - style={pal.text as FontAwesomeIconStyle} - /> - </View> - <Text type="lg" style={pal.text}> - App passwords - </Text> - </TouchableOpacity> - <TouchableOpacity testID="savedFeedsBtn" style={[styles.linkCard, pal.view, isSwitching && styles.dimmed]} accessibilityHint="Saved Feeds" @@ -457,6 +454,44 @@ export const SettingsScreen = withAuthRequired( </Text> </TouchableOpacity> <TouchableOpacity + testID="moderationBtn" + style={[styles.linkCard, pal.view, isSwitching && styles.dimmed]} + onPress={ + isSwitching ? undefined : () => navigation.navigate('Moderation') + } + accessibilityRole="button" + accessibilityHint="" + accessibilityLabel="Opens moderation settings"> + <View style={[styles.iconContainer, pal.btn]}> + <HandIcon style={pal.text} size={18} strokeWidth={6} /> + </View> + <Text type="lg" style={pal.text}> + Moderation + </Text> + </TouchableOpacity> + <View style={styles.spacer20} /> + + <Text type="xl-bold" style={[pal.text, styles.heading]}> + Advanced + </Text> + <TouchableOpacity + testID="appPasswordBtn" + style={[styles.linkCard, pal.view, isSwitching && styles.dimmed]} + onPress={onPressAppPasswords} + accessibilityRole="button" + accessibilityHint="Open app password settings" + accessibilityLabel="Opens the app password settings page"> + <View style={[styles.iconContainer, pal.btn]}> + <FontAwesomeIcon + icon="lock" + style={pal.text as FontAwesomeIconStyle} + /> + </View> + <Text type="lg" style={pal.text}> + App passwords + </Text> + </TouchableOpacity> + <TouchableOpacity testID="changeHandleBtn" style={[styles.linkCard, pal.view, isSwitching && styles.dimmed]} onPress={isSwitching ? undefined : onPressChangeHandle} @@ -620,6 +655,8 @@ const styles = StyleSheet.create({ paddingBottom: 6, }, infoLine: { + flexDirection: 'row', + alignItems: 'center', paddingHorizontal: 18, paddingBottom: 6, }, diff --git a/src/view/shell/desktop/LeftNav.tsx b/src/view/shell/desktop/LeftNav.tsx index 852731950..8c1a33245 100644 --- a/src/view/shell/desktop/LeftNav.tsx +++ b/src/view/shell/desktop/LeftNav.tsx @@ -311,7 +311,7 @@ export const DesktopLeftNav = observer(function DesktopLeftNav() { icon={ <HandIcon strokeWidth={5.5} - style={pal.text as FontAwesomeIconStyle} + style={pal.text} size={isDesktop ? 24 : 27} /> } |