Add starter pack opt-out UI
This commit is contained in:
+6
-1
@@ -92,6 +92,7 @@
|
|||||||
"app.bsky.graph.muteActor",
|
"app.bsky.graph.muteActor",
|
||||||
"app.bsky.graph.muteActorList",
|
"app.bsky.graph.muteActorList",
|
||||||
"app.bsky.graph.muteThread",
|
"app.bsky.graph.muteThread",
|
||||||
|
"app.bsky.graph.referencelistoptout",
|
||||||
"app.bsky.graph.searchStarterPacks",
|
"app.bsky.graph.searchStarterPacks",
|
||||||
"app.bsky.graph.searchStarterPacksV2",
|
"app.bsky.graph.searchStarterPacksV2",
|
||||||
"app.bsky.graph.starterpack",
|
"app.bsky.graph.starterpack",
|
||||||
@@ -538,7 +539,7 @@
|
|||||||
},
|
},
|
||||||
"app.bsky.graph.defs": {
|
"app.bsky.graph.defs": {
|
||||||
"uri": "at://did:plc:4v4y5r3lwsbtmsxhile2ljac/com.atproto.lexicon.schema/app.bsky.graph.defs",
|
"uri": "at://did:plc:4v4y5r3lwsbtmsxhile2ljac/com.atproto.lexicon.schema/app.bsky.graph.defs",
|
||||||
"cid": "bafyreifcipomli7yggtl46xufgxlnrw7se6xmsdxmzgfcz2tiu76ljatxm"
|
"cid": "bafyreief2f7zpllyicjugbn7faohmnzwujeiytfzj76uckxmrqmtdvechy"
|
||||||
},
|
},
|
||||||
"app.bsky.graph.follow": {
|
"app.bsky.graph.follow": {
|
||||||
"uri": "at://did:plc:4v4y5r3lwsbtmsxhile2ljac/com.atproto.lexicon.schema/app.bsky.graph.follow",
|
"uri": "at://did:plc:4v4y5r3lwsbtmsxhile2ljac/com.atproto.lexicon.schema/app.bsky.graph.follow",
|
||||||
@@ -632,6 +633,10 @@
|
|||||||
"uri": "at://did:plc:4v4y5r3lwsbtmsxhile2ljac/com.atproto.lexicon.schema/app.bsky.graph.muteThread",
|
"uri": "at://did:plc:4v4y5r3lwsbtmsxhile2ljac/com.atproto.lexicon.schema/app.bsky.graph.muteThread",
|
||||||
"cid": "bafyreib6ppci3qzye6wktkm4byxtb5mnl2vg22fm7oawcdvof2tfogx4dy"
|
"cid": "bafyreib6ppci3qzye6wktkm4byxtb5mnl2vg22fm7oawcdvof2tfogx4dy"
|
||||||
},
|
},
|
||||||
|
"app.bsky.graph.referencelistoptout": {
|
||||||
|
"uri": "at://did:plc:4v4y5r3lwsbtmsxhile2ljac/com.atproto.lexicon.schema/app.bsky.graph.referencelistoptout",
|
||||||
|
"cid": "bafyreifode2cfu7x7yamiorzg66u46l4zdr2yxmtuoikrup7j2dhwzxf3q"
|
||||||
|
},
|
||||||
"app.bsky.graph.searchStarterPacks": {
|
"app.bsky.graph.searchStarterPacks": {
|
||||||
"uri": "at://did:plc:4v4y5r3lwsbtmsxhile2ljac/com.atproto.lexicon.schema/app.bsky.graph.searchStarterPacks",
|
"uri": "at://did:plc:4v4y5r3lwsbtmsxhile2ljac/com.atproto.lexicon.schema/app.bsky.graph.searchStarterPacks",
|
||||||
"cid": "bafyreia446ip6mbnwpml6hlvxab7jtsud7zczde3u6zmnsa3op4bpxu7um"
|
"cid": "bafyreia446ip6mbnwpml6hlvxab7jtsud7zczde3u6zmnsa3op4bpxu7um"
|
||||||
|
|||||||
@@ -100,6 +100,11 @@
|
|||||||
"subject": {
|
"subject": {
|
||||||
"ref": "app.bsky.actor.defs#profileView",
|
"ref": "app.bsky.actor.defs#profileView",
|
||||||
"type": "ref"
|
"type": "ref"
|
||||||
|
},
|
||||||
|
"subjectOptedOut": {
|
||||||
|
"type": "boolean",
|
||||||
|
"const": true,
|
||||||
|
"description": "Set to true when the subject has opted out of appearing in the reference list. Only set when the viewer owns the list."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -228,6 +233,11 @@
|
|||||||
"blocked": {
|
"blocked": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"format": "at-uri"
|
"format": "at-uri"
|
||||||
|
},
|
||||||
|
"referenceListOptOut": {
|
||||||
|
"type": "string",
|
||||||
|
"format": "at-uri",
|
||||||
|
"description": "The authenticated viewer's app.bsky.graph.referencelistoptout record URI for this reference list. Only set for reference lists. A client can delete this record to undo the opt-out."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -0,0 +1,30 @@
|
|||||||
|
{
|
||||||
|
"id": "app.bsky.graph.referencelistoptout",
|
||||||
|
"defs": {
|
||||||
|
"main": {
|
||||||
|
"key": "tid",
|
||||||
|
"type": "record",
|
||||||
|
"record": {
|
||||||
|
"type": "object",
|
||||||
|
"required": [
|
||||||
|
"subject",
|
||||||
|
"createdAt"
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"subject": {
|
||||||
|
"type": "string",
|
||||||
|
"format": "at-uri",
|
||||||
|
"description": "Canonical, DID-based AT URI of the app.bsky.graph.list record from which the author requests omission."
|
||||||
|
},
|
||||||
|
"createdAt": {
|
||||||
|
"type": "string",
|
||||||
|
"format": "datetime"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"description": "Record requesting that its author be omitted from the public presentation of a reference list. This record is only enforced when the subject list's current purpose is app.bsky.graph.defs#referencelist. AppView indexes at most one record per actor and list pair, and ignores duplicate records."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"$type": "com.atproto.lexicon.schema",
|
||||||
|
"lexicon": 1
|
||||||
|
}
|
||||||
@@ -2,6 +2,7 @@ import {forwardRef, useCallback, useImperativeHandle, useState} from 'react'
|
|||||||
import {type ListRenderItemInfo, View} from 'react-native'
|
import {type ListRenderItemInfo, View} from 'react-native'
|
||||||
import {AtUri} from '@atproto/syntax'
|
import {AtUri} from '@atproto/syntax'
|
||||||
import {type ModerationOpts} from '@bsky/sdk/moderation'
|
import {type ModerationOpts} from '@bsky/sdk/moderation'
|
||||||
|
import {Trans} from '@lingui/react/macro'
|
||||||
|
|
||||||
import {useBottomBarOffset} from '#/lib/hooks/useBottomBarOffset'
|
import {useBottomBarOffset} from '#/lib/hooks/useBottomBarOffset'
|
||||||
import {useInitialNumToRender} from '#/lib/hooks/useInitialNumToRender'
|
import {useInitialNumToRender} from '#/lib/hooks/useInitialNumToRender'
|
||||||
@@ -13,11 +14,12 @@ import {type SectionRef} from '#/screens/Profile/Sections/types'
|
|||||||
import {atoms as a, useTheme} from '#/alf'
|
import {atoms as a, useTheme} from '#/alf'
|
||||||
import {ListFooter, ListMaybePlaceholder} from '#/components/Lists'
|
import {ListFooter, ListMaybePlaceholder} from '#/components/Lists'
|
||||||
import {Default as ProfileCard} from '#/components/ProfileCard'
|
import {Default as ProfileCard} from '#/components/ProfileCard'
|
||||||
|
import {Text} from '#/components/Typography'
|
||||||
import {IS_NATIVE, IS_WEB} from '#/env'
|
import {IS_NATIVE, IS_WEB} from '#/env'
|
||||||
import {type app} from '#/lexicons'
|
import {type app} from '#/lexicons'
|
||||||
|
|
||||||
function keyExtractor(item: app.bsky.actor.defs.ProfileView, index: number) {
|
function keyExtractor(item: app.bsky.graph.defs.ListItemView) {
|
||||||
return `${item.did}-${index}`
|
return item.uri
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ProfilesListProps {
|
interface ProfilesListProps {
|
||||||
@@ -42,26 +44,27 @@ export const ProfilesList = forwardRef<SectionRef, ProfilesListProps>(
|
|||||||
|
|
||||||
// The server returns these sorted by descending creation date, so we want to invert
|
// The server returns these sorted by descending creation date, so we want to invert
|
||||||
|
|
||||||
const profiles = data
|
const listItems = data
|
||||||
?.filter(
|
?.filter(
|
||||||
p => !isBlockedOrBlocking(p.subject) && !p.subject.associated?.labeler,
|
p => !isBlockedOrBlocking(p.subject) && !p.subject.associated?.labeler,
|
||||||
)
|
)
|
||||||
.map(p => p.subject)
|
|
||||||
.reverse()
|
.reverse()
|
||||||
const isOwn = new AtUri(listUri).host === currentAccount?.did
|
const isOwn = new AtUri(listUri).host === currentAccount?.did
|
||||||
|
|
||||||
const getSortedProfiles = () => {
|
const getSortedProfiles = () => {
|
||||||
if (!profiles) return
|
if (!listItems) return
|
||||||
if (!isOwn) return profiles
|
if (!isOwn) return listItems
|
||||||
|
|
||||||
const myIndex = profiles.findIndex(p => p.did === currentAccount?.did)
|
const myIndex = listItems.findIndex(
|
||||||
|
item => item.subject.did === currentAccount?.did,
|
||||||
|
)
|
||||||
return myIndex !== -1
|
return myIndex !== -1
|
||||||
? [
|
? [
|
||||||
profiles[myIndex],
|
listItems[myIndex],
|
||||||
...profiles.slice(0, myIndex),
|
...listItems.slice(0, myIndex),
|
||||||
...profiles.slice(myIndex + 1),
|
...listItems.slice(myIndex + 1),
|
||||||
]
|
]
|
||||||
: profiles
|
: listItems
|
||||||
}
|
}
|
||||||
const onScrollToTop = useCallback(() => {
|
const onScrollToTop = useCallback(() => {
|
||||||
scrollElRef.current?.scrollToOffset({
|
scrollElRef.current?.scrollToOffset({
|
||||||
@@ -77,7 +80,7 @@ export const ProfilesList = forwardRef<SectionRef, ProfilesListProps>(
|
|||||||
const renderItem = ({
|
const renderItem = ({
|
||||||
item,
|
item,
|
||||||
index,
|
index,
|
||||||
}: ListRenderItemInfo<app.bsky.actor.defs.ProfileView>) => {
|
}: ListRenderItemInfo<app.bsky.graph.defs.ListItemView>) => {
|
||||||
return (
|
return (
|
||||||
<View
|
<View
|
||||||
style={[
|
style={[
|
||||||
@@ -86,10 +89,15 @@ export const ProfilesList = forwardRef<SectionRef, ProfilesListProps>(
|
|||||||
(IS_WEB || index !== 0) && a.border_t,
|
(IS_WEB || index !== 0) && a.border_t,
|
||||||
]}>
|
]}>
|
||||||
<ProfileCard
|
<ProfileCard
|
||||||
profile={item}
|
profile={item.subject}
|
||||||
moderationOpts={moderationOpts}
|
moderationOpts={moderationOpts}
|
||||||
logContext="StarterPackProfilesList"
|
logContext="StarterPackProfilesList"
|
||||||
/>
|
/>
|
||||||
|
{item.subjectOptedOut ? (
|
||||||
|
<Text style={[a.text_sm, t.atoms.text_contrast_medium]}>
|
||||||
|
<Trans>Opted out of this starter pack</Trans>
|
||||||
|
</Text>
|
||||||
|
) : null}
|
||||||
</View>
|
</View>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ import {useResolveDidQuery} from '#/state/queries/resolve-uri'
|
|||||||
import {useShortenLink} from '#/state/queries/shorten-link'
|
import {useShortenLink} from '#/state/queries/shorten-link'
|
||||||
import {
|
import {
|
||||||
useDeleteStarterPackMutation,
|
useDeleteStarterPackMutation,
|
||||||
|
useReferenceListOptOutMutation,
|
||||||
useStarterPackQuery,
|
useStarterPackQuery,
|
||||||
} from '#/state/queries/starter-packs'
|
} from '#/state/queries/starter-packs'
|
||||||
import {useAppviewClient, usePdsClient, useSession} from '#/state/session'
|
import {useAppviewClient, usePdsClient, useSession} from '#/state/session'
|
||||||
@@ -523,6 +524,7 @@ function OverflowMenu({
|
|||||||
const reportDialogControl = useReportDialogControl()
|
const reportDialogControl = useReportDialogControl()
|
||||||
const deleteDialogControl = useDialogControl()
|
const deleteDialogControl = useDialogControl()
|
||||||
const convertToListDialogControl = useDialogControl()
|
const convertToListDialogControl = useDialogControl()
|
||||||
|
const optOutDialogControl = useDialogControl()
|
||||||
const navigation = useNavigation<NavigationProp>()
|
const navigation = useNavigation<NavigationProp>()
|
||||||
|
|
||||||
const {
|
const {
|
||||||
@@ -546,6 +548,19 @@ function OverflowMenu({
|
|||||||
})
|
})
|
||||||
|
|
||||||
const isOwn = starterPack.creator.did === currentAccount?.did
|
const isOwn = starterPack.creator.did === currentAccount?.did
|
||||||
|
const referenceListOptOut = starterPack.list?.viewer?.referenceListOptOut
|
||||||
|
const {mutate: setReferenceListOptOut, isPending: isOptOutPending} =
|
||||||
|
useReferenceListOptOutMutation({
|
||||||
|
starterPack,
|
||||||
|
onError: error => {
|
||||||
|
logger.error('Failed to update starter pack opt-out', {
|
||||||
|
safeMessage: error,
|
||||||
|
})
|
||||||
|
Toast.show(_(msg`Failed to update starter pack opt-out`), {
|
||||||
|
type: 'error',
|
||||||
|
})
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
const onDeleteStarterPack = async () => {
|
const onDeleteStarterPack = async () => {
|
||||||
if (!starterPack.list) {
|
if (!starterPack.list) {
|
||||||
@@ -650,6 +665,22 @@ function OverflowMenu({
|
|||||||
</Menu.ItemText>
|
</Menu.ItemText>
|
||||||
<Menu.ItemIcon icon={CircleInfo} position="right" />
|
<Menu.ItemIcon icon={CircleInfo} position="right" />
|
||||||
</Menu.Item>
|
</Menu.Item>
|
||||||
|
<Menu.Item
|
||||||
|
label={
|
||||||
|
referenceListOptOut
|
||||||
|
? _(msg`Undo opt-out from starter pack`)
|
||||||
|
: _(msg`Opt out of starter pack`)
|
||||||
|
}
|
||||||
|
disabled={isOptOutPending}
|
||||||
|
onPress={() => optOutDialogControl.open()}>
|
||||||
|
<Menu.ItemText>
|
||||||
|
{referenceListOptOut ? (
|
||||||
|
<Trans>Undo opt-out</Trans>
|
||||||
|
) : (
|
||||||
|
<Trans>Opt out of starter pack</Trans>
|
||||||
|
)}
|
||||||
|
</Menu.ItemText>
|
||||||
|
</Menu.Item>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</Menu.Outer>
|
</Menu.Outer>
|
||||||
@@ -709,6 +740,52 @@ function OverflowMenu({
|
|||||||
</Prompt.Actions>
|
</Prompt.Actions>
|
||||||
</Prompt.Outer>
|
</Prompt.Outer>
|
||||||
|
|
||||||
|
<Prompt.Outer control={optOutDialogControl}>
|
||||||
|
<Prompt.TitleText>
|
||||||
|
{referenceListOptOut ? (
|
||||||
|
<Trans>Undo opt-out?</Trans>
|
||||||
|
) : (
|
||||||
|
<Trans>Opt out of this starter pack?</Trans>
|
||||||
|
)}
|
||||||
|
</Prompt.TitleText>
|
||||||
|
<Prompt.DescriptionText>
|
||||||
|
{referenceListOptOut ? (
|
||||||
|
<Trans>You will appear in this starter pack again.</Trans>
|
||||||
|
) : (
|
||||||
|
<Trans>
|
||||||
|
You will no longer appear in this starter pack. The creator can
|
||||||
|
still remove you from its member list.
|
||||||
|
</Trans>
|
||||||
|
)}
|
||||||
|
</Prompt.DescriptionText>
|
||||||
|
<Prompt.Actions>
|
||||||
|
<Button
|
||||||
|
variant="solid"
|
||||||
|
color={referenceListOptOut ? 'primary' : 'negative'}
|
||||||
|
size={gtMobile ? 'small' : 'large'}
|
||||||
|
label={
|
||||||
|
referenceListOptOut
|
||||||
|
? _(msg`Undo opt-out`)
|
||||||
|
: _(msg`Opt out of starter pack`)
|
||||||
|
}
|
||||||
|
disabled={isOptOutPending}
|
||||||
|
onPress={() => {
|
||||||
|
optOutDialogControl.close()
|
||||||
|
setReferenceListOptOut({referenceListOptOut})
|
||||||
|
}}>
|
||||||
|
<ButtonText>
|
||||||
|
{referenceListOptOut ? (
|
||||||
|
<Trans>Undo opt-out</Trans>
|
||||||
|
) : (
|
||||||
|
<Trans>Opt out</Trans>
|
||||||
|
)}
|
||||||
|
</ButtonText>
|
||||||
|
{isOptOutPending && <ButtonIcon icon={Loader} />}
|
||||||
|
</Button>
|
||||||
|
<Prompt.Cancel />
|
||||||
|
</Prompt.Actions>
|
||||||
|
</Prompt.Outer>
|
||||||
|
|
||||||
<CreateListFromStarterPackDialog
|
<CreateListFromStarterPackDialog
|
||||||
control={convertToListDialogControl}
|
control={convertToListDialogControl}
|
||||||
starterPack={starterPack}
|
starterPack={starterPack}
|
||||||
|
|||||||
@@ -84,7 +84,10 @@ export async function invalidateListMembersQuery({
|
|||||||
queryClient: QueryClient
|
queryClient: QueryClient
|
||||||
uri: string
|
uri: string
|
||||||
}) {
|
}) {
|
||||||
await queryClient.invalidateQueries({queryKey: RQKEY(uri)})
|
await Promise.all([
|
||||||
|
queryClient.invalidateQueries({queryKey: RQKEY(uri)}),
|
||||||
|
queryClient.invalidateQueries({queryKey: RQKEY_ALL(uri)}),
|
||||||
|
])
|
||||||
}
|
}
|
||||||
|
|
||||||
export function* findAllProfilesInQueryData(
|
export function* findAllProfilesInQueryData(
|
||||||
|
|||||||
@@ -71,6 +71,116 @@ export function useStarterPackQuery({
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function useReferenceListOptOutMutation({
|
||||||
|
starterPack,
|
||||||
|
onError,
|
||||||
|
}: {
|
||||||
|
starterPack: app.bsky.graph.defs.StarterPackView
|
||||||
|
onError: (error: Error) => void
|
||||||
|
}) {
|
||||||
|
const queryClient = useQueryClient()
|
||||||
|
const appviewClient = useAppviewClient()
|
||||||
|
const pdsClient = usePdsClient()
|
||||||
|
const parsed = parseStarterPackUri(starterPack.uri)!
|
||||||
|
const queryKey = RQKEY({did: parsed.name, rkey: parsed.rkey})
|
||||||
|
|
||||||
|
return useMutation<
|
||||||
|
AtUriString | undefined,
|
||||||
|
Error,
|
||||||
|
{referenceListOptOut?: string},
|
||||||
|
{previous?: app.bsky.graph.defs.StarterPackView}
|
||||||
|
>({
|
||||||
|
mutationFn: async ({referenceListOptOut}) => {
|
||||||
|
if (!starterPack.list) {
|
||||||
|
throw new Error('Starter pack does not have a reference list')
|
||||||
|
}
|
||||||
|
|
||||||
|
let nextOptOut: AtUriString | undefined
|
||||||
|
if (referenceListOptOut) {
|
||||||
|
const {rkeySafe: rkey} = new AtUri(referenceListOptOut)
|
||||||
|
await pdsClient.delete(app.bsky.graph.referencelistoptout, {
|
||||||
|
repo: pdsClient.assertDid,
|
||||||
|
rkey,
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
const result = await pdsClient.create(
|
||||||
|
app.bsky.graph.referencelistoptout,
|
||||||
|
{
|
||||||
|
subject: starterPack.list.uri,
|
||||||
|
createdAt: toDatetimeString(new Date()),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
nextOptOut = result.uri
|
||||||
|
}
|
||||||
|
|
||||||
|
await until(
|
||||||
|
5,
|
||||||
|
1e3,
|
||||||
|
(value, error) =>
|
||||||
|
!error &&
|
||||||
|
value.starterPack.list?.viewer?.referenceListOptOut === nextOptOut,
|
||||||
|
async () =>
|
||||||
|
await appviewClient.call(app.bsky.graph.getStarterPack, {
|
||||||
|
starterPack: starterPack.uri,
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
return nextOptOut
|
||||||
|
},
|
||||||
|
onMutate: async ({referenceListOptOut}) => {
|
||||||
|
await queryClient.cancelQueries({queryKey})
|
||||||
|
const previous =
|
||||||
|
queryClient.getQueryData<app.bsky.graph.defs.StarterPackView>(queryKey)
|
||||||
|
queryClient.setQueryData<app.bsky.graph.defs.StarterPackView>(
|
||||||
|
queryKey,
|
||||||
|
current =>
|
||||||
|
current?.list
|
||||||
|
? {
|
||||||
|
...current,
|
||||||
|
list: {
|
||||||
|
...current.list,
|
||||||
|
viewer: {
|
||||||
|
...current.list.viewer,
|
||||||
|
referenceListOptOut: referenceListOptOut
|
||||||
|
? undefined
|
||||||
|
: `at://${pdsClient.assertDid}/app.bsky.graph.referencelistoptout/pending`,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
: current,
|
||||||
|
)
|
||||||
|
return {previous}
|
||||||
|
},
|
||||||
|
onSuccess: referenceListOptOut => {
|
||||||
|
queryClient.setQueryData<app.bsky.graph.defs.StarterPackView>(
|
||||||
|
queryKey,
|
||||||
|
current =>
|
||||||
|
current?.list
|
||||||
|
? {
|
||||||
|
...current,
|
||||||
|
list: {
|
||||||
|
...current.list,
|
||||||
|
viewer: {
|
||||||
|
...current.list.viewer,
|
||||||
|
referenceListOptOut,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
: current,
|
||||||
|
)
|
||||||
|
void invalidateListMembersQuery({
|
||||||
|
queryClient,
|
||||||
|
uri: starterPack.list!.uri,
|
||||||
|
})
|
||||||
|
},
|
||||||
|
onError: (error, _, context) => {
|
||||||
|
if (context?.previous) {
|
||||||
|
queryClient.setQueryData(queryKey, context.previous)
|
||||||
|
}
|
||||||
|
onError(error)
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
export async function invalidateStarterPack({
|
export async function invalidateStarterPack({
|
||||||
queryClient,
|
queryClient,
|
||||||
did,
|
did,
|
||||||
|
|||||||
Reference in New Issue
Block a user