Add starter pack reference-list opt-out UI (#11578)

This commit is contained in:
Spence Pope
2026-09-03 03:08:48 -04:00
committed by GitHub
parent bb747c5f26
commit 68e56eaea0
15 changed files with 885 additions and 30 deletions
@@ -12,6 +12,7 @@ import {
useListBlockMutation,
useListDeleteMutation,
useListMuteMutation,
useReferenceListOptOutMutation,
} from '#/state/queries/list'
import {useRemoveFeedMutation} from '#/state/queries/preferences'
import {useSession} from '#/state/session'
@@ -51,6 +52,7 @@ export function MoreOptionsMenu({
const editListDialogControl = useDialogControl()
const deleteListPromptControl = useDialogControl()
const reportDialogControl = useReportDialogControl()
const optOutDialogControl = useDialogControl()
const navigation = useNavigation<NavigationProp>()
const {mutateAsync: removeSavedFeed} = useRemoveFeedMutation()
@@ -60,10 +62,36 @@ export function MoreOptionsMenu({
const isCurateList = list.purpose === app.bsky.graph.defs.curatelist.value
const isModList = list.purpose === app.bsky.graph.defs.modlist.value
const isReferenceList =
list.purpose === app.bsky.graph.defs.referencelist.value
const isBlocking = !!list.viewer?.blocked
const isMuting = !!list.viewer?.muted
const isPinned = Boolean(savedFeedConfig?.pinned)
const isOwner = currentAccount?.did === list.creator.did
const referenceListOptOut = list.viewer?.referenceListOptOut
const {mutate: setReferenceListOptOut, isPending: isOptOutPending} =
useReferenceListOptOutMutation({
list,
onSuccess: action => {
ax.metric('starterPack:optOut', {
starterPack: list.uri,
action,
})
Toast.show(
action === 'optOut'
? _(msg`Opted out of starter pack`)
: _(msg`Opt-out undone`),
)
},
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 onPressShare = () => {
const {rkey} = new AtUri(list.uri)
@@ -206,16 +234,41 @@ export function MoreOptionsMenu({
</Menu.Item>
</Menu.Group>
) : (
<Menu.Group>
<Menu.Item
label={_(msg`Report list`)}
onPress={reportDialogControl.open}>
<Menu.ItemText>
<Trans>Report list</Trans>
</Menu.ItemText>
<Menu.ItemIcon position="right" icon={WarningIcon} />
</Menu.Item>
</Menu.Group>
<>
<Menu.Group>
<Menu.Item
label={_(msg`Report list`)}
onPress={reportDialogControl.open}>
<Menu.ItemText>
<Trans>Report list</Trans>
</Menu.ItemText>
<Menu.ItemIcon position="right" icon={WarningIcon} />
</Menu.Item>
</Menu.Group>
{isReferenceList ? (
<>
<Menu.Divider />
<Menu.Group>
<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.Group>
</>
) : null}
</>
)}
{isModList && isPinned && (
@@ -277,6 +330,46 @@ export function MoreOptionsMenu({
confirmButtonColor="negative"
/>
{isReferenceList ? (
<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 be eligible to appear in this starter pack again.
</Trans>
) : (
<Trans>
You will no longer appear in this starter pack. The creator will
be able to see that you've opted out and remove you if they
wish.
</Trans>
)}
</Prompt.DescriptionText>
<Prompt.Actions>
<Prompt.Action
cta={
referenceListOptOut
? _(msg`Undo opt-out`)
: _(msg`Opt out of starter pack`)
}
color={referenceListOptOut ? 'primary' : 'negative'}
disabled={isOptOutPending}
onPress={() => {
setReferenceListOptOut({referenceListOptOut})
}}
/>
<Prompt.Cancel />
</Prompt.Actions>
</Prompt.Outer>
) : null}
<ReportDialog
control={reportDialogControl}
subject={{
@@ -30,6 +30,7 @@ import {useResolveDidQuery} from '#/state/queries/resolve-uri'
import {useShortenLink} from '#/state/queries/shorten-link'
import {
useDeleteStarterPackMutation,
useReferenceListOptOutMutation,
useStarterPackQuery,
} from '#/state/queries/starter-packs'
import {useAppviewClient, usePdsClient, useSession} from '#/state/session'
@@ -523,6 +524,7 @@ function OverflowMenu({
const reportDialogControl = useReportDialogControl()
const deleteDialogControl = useDialogControl()
const convertToListDialogControl = useDialogControl()
const optOutDialogControl = useDialogControl()
const navigation = useNavigation<NavigationProp>()
const {
@@ -546,6 +548,30 @@ function OverflowMenu({
})
const isOwn = starterPack.creator.did === currentAccount?.did
const referenceListOptOut = starterPack.list?.viewer?.referenceListOptOut
const {mutate: setReferenceListOptOut, isPending: isOptOutPending} =
useReferenceListOptOutMutation({
starterPack,
onSuccess: action => {
ax.metric('starterPack:optOut', {
starterPack: starterPack.uri,
action,
})
Toast.show(
action === 'optOut'
? _(msg`Opted out of starter pack`)
: _(msg`Opt-out undone`),
)
},
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 () => {
if (!starterPack.list) {
@@ -650,6 +676,24 @@ function OverflowMenu({
</Menu.ItemText>
<Menu.ItemIcon icon={CircleInfo} position="right" />
</Menu.Item>
{starterPack.list ? (
<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>
) : null}
</>
)}
</Menu.Outer>
@@ -709,6 +753,58 @@ function OverflowMenu({
</Prompt.Actions>
</Prompt.Outer>
{starterPack.list ? (
<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 be eligible to appear in this starter pack again.
</Trans>
) : (
<Trans>
You will no longer appear in this starter pack. The creator will
be able to see that you've opted out and remove you if they
wish.
</Trans>
)}
</Prompt.DescriptionText>
<Prompt.Actions>
<Button
variant="solid"
color={referenceListOptOut ? 'primary' : 'negative'}
size="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>
) : null}
<CreateListFromStarterPackDialog
control={convertToListDialogControl}
starterPack={starterPack}
+2 -1
View File
@@ -138,7 +138,8 @@ export function Provider({
currentStep: 'Details',
name: starterPack.record.name,
description: starterPack.record.description,
profiles: listItems?.map(i => i.subject) ?? [],
profiles:
listItems?.filter(i => !i.subjectOptedOut).map(i => i.subject) ?? [],
feeds: starterPack.feeds ?? [],
processing: false,
transitionDirection: 'Forward',
@@ -24,8 +24,10 @@ function keyExtractor(item: bsky.profile.AnyProfileView) {
export function StepProfiles({
moderationOpts,
optedOutDids,
}: {
moderationOpts: ModerationOpts
optedOutDids: Set<string>
}) {
const t = useTheme()
const [state, dispatch] = useWizardState()
@@ -59,6 +61,7 @@ export function StepProfiles({
state={state}
dispatch={dispatch}
moderationOpts={moderationOpts}
subjectOptedOut={optedOutDids.has(item.did)}
/>
)
}
+10 -1
View File
@@ -341,7 +341,16 @@ function WizardInner({
{state.currentStep === 'Details' ? (
<StepDetails />
) : state.currentStep === 'Profiles' ? (
<StepProfiles moderationOpts={moderationOpts} />
<StepProfiles
moderationOpts={moderationOpts}
optedOutDids={
new Set(
currentListItems
?.filter(item => item.subjectOptedOut)
.map(item => item.subject.did),
)
}
/>
) : state.currentStep === 'Feeds' ? (
<StepFeeds moderationOpts={moderationOpts} />
) : null}