[🐴] Settings screen (#3830)

* create settings screen + api

* update api package

* use putrecord API with validate false

* create new RadioGroup component
zio/stable
Samuel Newman 2024-05-14 18:57:16 +01:00 committed by GitHub
parent 9861494e34
commit 5af61ca4e4
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 216 additions and 28 deletions

View File

@ -0,0 +1,76 @@
import React from 'react'
import {View, ViewProps} from 'react-native'
import {atoms as a, useTheme} from '#/alf'
import {Button} from './Button'
import {Text} from './Typography'
export function RadioGroup<T extends string | number>({
value,
onSelect,
items,
...props
}: ViewProps & {
value: T
onSelect: (value: T) => void
items: Array<{label: string; value: T}>
}) {
return (
<View {...props}>
{items.map(item => (
<Button
label={item.label}
key={item.value}
variant="ghost"
color="secondary"
size="small"
onPress={() => onSelect(item.value)}
style={[a.justify_between, a.px_sm]}>
<Text style={a.text_md}>{item.label}</Text>
<RadioIcon selected={value === item.value} />
</Button>
))}
</View>
)
}
function RadioIcon({selected}: {selected: boolean}) {
const t = useTheme()
return (
<View
style={[
{
width: 30,
height: 30,
borderWidth: 2,
borderColor: selected
? t.palette.primary_500
: t.palette.contrast_200,
},
selected
? {
backgroundColor:
t.name === 'light'
? t.palette.primary_100
: t.palette.primary_900,
}
: t.atoms.bg,
a.align_center,
a.justify_center,
a.rounded_full,
]}>
{selected && (
<View
style={[
{
width: 18,
height: 18,
backgroundColor: t.palette.primary_500,
},
a.rounded_full,
]}
/>
)}
</View>
)
}

View File

@ -0,0 +1,70 @@
import React, {useCallback} from 'react'
import {View} from 'react-native'
import {AppBskyActorDefs} from '@atproto/api'
import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {NativeStackScreenProps} from '@react-navigation/native-stack'
import {UseQueryResult} from '@tanstack/react-query'
import {CommonNavigatorParams} from '#/lib/routes/types'
import {useGate} from '#/lib/statsig/statsig'
import {useUpdateActorDeclaration} from '#/state/queries/messages/actor-declaration'
import {useProfileQuery} from '#/state/queries/profile'
import {useSession} from '#/state/session'
import * as Toast from '#/view/com/util/Toast'
import {ViewHeader} from '#/view/com/util/ViewHeader'
import {CenteredView} from '#/view/com/util/Views'
import {atoms as a} from '#/alf'
import {RadioGroup} from '#/components/RadioGroup'
import {Text} from '#/components/Typography'
import {ClipClopGate} from './gate'
type AllowIncoming = 'all' | 'none' | 'following'
type Props = NativeStackScreenProps<CommonNavigatorParams, 'MessagesSettings'>
export function MessagesSettingsScreen({}: Props) {
const {_} = useLingui()
const {currentAccount} = useSession()
const {data: profile} = useProfileQuery({
did: currentAccount!.did,
}) as UseQueryResult<AppBskyActorDefs.ProfileViewDetailed, Error>
const {mutate: updateDeclaration} = useUpdateActorDeclaration({
onError: () => {
Toast.show(_(msg`Failed to update settings`))
},
})
const onSelectItem = useCallback(
(key: string) => {
updateDeclaration(key as AllowIncoming)
},
[updateDeclaration],
)
const gate = useGate()
if (!gate('dms')) return <ClipClopGate />
return (
<CenteredView sideBorders>
<ViewHeader title={_(msg`Settings`)} showOnDesktop showBorder />
<View style={[a.px_md, a.py_lg, a.gap_md]}>
<Text style={[a.text_xl, a.font_bold, a.px_sm]}>
<Trans>Allow messages from</Trans>
</Text>
<RadioGroup<AllowIncoming>
value={
(profile?.associated?.chat?.allowIncoming as AllowIncoming) ??
'following'
}
items={[
{label: _(msg`Everyone`), value: 'all'},
{label: _(msg`Follows only`), value: 'following'},
{label: _(msg`No one`), value: 'none'},
]}
onSelect={onSelectItem}
/>
</View>
</CenteredView>
)
}

View File

@ -1,24 +0,0 @@
import React from 'react'
import {View} from 'react-native'
import {msg} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {NativeStackScreenProps} from '@react-navigation/native-stack'
import {CommonNavigatorParams} from '#/lib/routes/types'
import {useGate} from '#/lib/statsig/statsig'
import {ViewHeader} from '#/view/com/util/ViewHeader'
import {ClipClopGate} from '../gate'
type Props = NativeStackScreenProps<CommonNavigatorParams, 'MessagesSettings'>
export function MessagesSettingsScreen({}: Props) {
const {_} = useLingui()
const gate = useGate()
if (!gate('dms')) return <ClipClopGate />
return (
<View>
<ViewHeader title={_(msg`Settings`)} showOnDesktop />
</View>
)
}

View File

@ -0,0 +1,64 @@
import {AppBskyActorDefs} from '@atproto/api'
import {useMutation, useQueryClient} from '@tanstack/react-query'
import {logger} from '#/logger'
import {useAgent, useSession} from '#/state/session'
import {RQKEY as PROFILE_RKEY} from '../profile'
export function useUpdateActorDeclaration({
onSuccess,
onError,
}: {
onSuccess?: () => void
onError?: (error: Error) => void
}) {
const queryClient = useQueryClient()
const {currentAccount} = useSession()
const {getAgent} = useAgent()
return useMutation({
mutationFn: async (allowIncoming: 'all' | 'none' | 'following') => {
if (!currentAccount) throw new Error('Not logged in')
// TODO(sam): remove validate: false once PDSes have the new lexicon
const result = await getAgent().api.com.atproto.repo.putRecord({
collection: 'chat.bsky.actor.declaration',
rkey: 'self',
repo: currentAccount.did,
validate: false,
record: {
$type: 'chat.bsky.actor.declaration',
allowIncoming,
},
})
return result
},
onMutate: allowIncoming => {
if (!currentAccount) return
queryClient.setQueryData(
PROFILE_RKEY(currentAccount?.did),
(old?: AppBskyActorDefs.ProfileViewDetailed) => {
if (!old) return old
return {
...old,
associated: {
...old.associated,
chat: {
allowIncoming,
},
},
} satisfies AppBskyActorDefs.ProfileViewDetailed
},
)
},
onSuccess,
onError: error => {
logger.error(error)
if (currentAccount) {
queryClient.invalidateQueries({
queryKey: PROFILE_RKEY(currentAccount.did),
})
}
onError?.(error)
},
})
}

View File

@ -1,9 +1,10 @@
import React from 'react'
import {StyleProp, StyleSheet, TextStyle, View, ViewStyle} from 'react-native'
import {choose} from 'lib/functions'
import {useTheme} from 'lib/ThemeContext'
import {Text} from '../text/Text'
import {Button, ButtonType} from './Button'
import {useTheme} from 'lib/ThemeContext'
import {choose} from 'lib/functions'
export function RadioButton({
testID,

View File

@ -1,8 +1,9 @@
import React, {useState} from 'react'
import {View} from 'react-native'
import {RadioButton} from './RadioButton'
import {ButtonType} from './Button'
import {s} from 'lib/styles'
import {ButtonType} from './Button'
import {RadioButton} from './RadioButton'
export interface RadioGroupItem {
label: string | JSX.Element