[🐴] Settings screen (#3830)
* create settings screen + api * update api package * use putrecord API with validate false * create new RadioGroup componentzio/stable
parent
9861494e34
commit
5af61ca4e4
|
@ -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>
|
||||||
|
)
|
||||||
|
}
|
|
@ -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>
|
||||||
|
)
|
||||||
|
}
|
|
@ -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>
|
|
||||||
)
|
|
||||||
}
|
|
|
@ -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)
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
|
@ -1,9 +1,10 @@
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
import {StyleProp, StyleSheet, TextStyle, View, ViewStyle} from 'react-native'
|
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 {Text} from '../text/Text'
|
||||||
import {Button, ButtonType} from './Button'
|
import {Button, ButtonType} from './Button'
|
||||||
import {useTheme} from 'lib/ThemeContext'
|
|
||||||
import {choose} from 'lib/functions'
|
|
||||||
|
|
||||||
export function RadioButton({
|
export function RadioButton({
|
||||||
testID,
|
testID,
|
||||||
|
|
|
@ -1,8 +1,9 @@
|
||||||
import React, {useState} from 'react'
|
import React, {useState} from 'react'
|
||||||
import {View} from 'react-native'
|
import {View} from 'react-native'
|
||||||
import {RadioButton} from './RadioButton'
|
|
||||||
import {ButtonType} from './Button'
|
|
||||||
import {s} from 'lib/styles'
|
import {s} from 'lib/styles'
|
||||||
|
import {ButtonType} from './Button'
|
||||||
|
import {RadioButton} from './RadioButton'
|
||||||
|
|
||||||
export interface RadioGroupItem {
|
export interface RadioGroupItem {
|
||||||
label: string | JSX.Element
|
label: string | JSX.Element
|
||||||
|
|
Loading…
Reference in New Issue