Address starter pack opt-out review feedback

This commit is contained in:
vineyardbovines
2026-09-02 19:47:24 -04:00
parent d993cc4163
commit 9eaee783e9
7 changed files with 46 additions and 6 deletions
+1
View File
@@ -703,6 +703,7 @@ export type Events = {
} }
'starterPack:removeUser': { 'starterPack:removeUser': {
starterPack?: string starterPack?: string
context?: 'opt-out'
} }
'starterPack:share': { 'starterPack:share': {
starterPack: string starterPack: string
@@ -22,6 +22,7 @@ import {ListFooter, ListMaybePlaceholder} from '#/components/Lists'
import {Loader} from '#/components/Loader' import {Loader} from '#/components/Loader'
import {Default as ProfileCard} from '#/components/ProfileCard' import {Default as ProfileCard} from '#/components/ProfileCard'
import * as Toast from '#/components/Toast' import * as Toast from '#/components/Toast'
import {useAnalytics} from '#/analytics'
import {IS_NATIVE, IS_WEB} from '#/env' import {IS_NATIVE, IS_WEB} from '#/env'
import {type app} from '#/lexicons' import {type app} from '#/lexicons'
@@ -159,9 +160,14 @@ function OptedOutControls({
canRemove: boolean canRemove: boolean
}) { }) {
const {_} = useLingui() const {_} = useLingui()
const ax = useAnalytics()
const [isRemoved, setIsRemoved] = useState(false)
const {mutate: removeMembership, isPending} = useListMembershipRemoveMutation( const {mutate: removeMembership, isPending} = useListMembershipRemoveMutation(
{ {
onSuccess: () => Toast.show(_(msg`Removed from starter pack`)), onSuccess: () => {
setIsRemoved(true)
Toast.show(_(msg`Removed from starter pack`))
},
onError: error => onError: error =>
Toast.show(cleanError(error), { Toast.show(cleanError(error), {
type: 'error', type: 'error',
@@ -169,6 +175,8 @@ function OptedOutControls({
}, },
) )
if (isRemoved) return null
return ( return (
<Admonition.Outer type="info" style={[a.mt_sm]}> <Admonition.Outer type="info" style={[a.mt_sm]}>
<Admonition.Row style={[a.align_center]}> <Admonition.Row style={[a.align_center]}>
@@ -183,13 +191,14 @@ function OptedOutControls({
label={_(msg`Remove user from starter pack`)} label={_(msg`Remove user from starter pack`)}
color="secondary" color="secondary"
disabled={isPending} disabled={isPending}
onPress={() => onPress={() => {
ax.metric('starterPack:removeUser', {context: 'opt-out'})
removeMembership({ removeMembership({
listUri, listUri,
actorDid: item.subject.did, actorDid: item.subject.did,
membershipUri: item.uri, membershipUri: item.uri,
}) })
}> }}>
{isPending ? ( {isPending ? (
<ButtonIcon icon={Loader} /> <ButtonIcon icon={Loader} />
) : ( ) : (
@@ -36,6 +36,7 @@ function WizardListCard({
avatar, avatar,
included, included,
disabled, disabled,
subjectOptedOut,
moderationUi, moderationUi,
}: { }: {
type: 'user' | 'algo' type: 'user' | 'algo'
@@ -48,6 +49,7 @@ function WizardListCard({
avatar?: string avatar?: string
included?: boolean included?: boolean
disabled?: boolean disabled?: boolean
subjectOptedOut?: boolean
moderationUi: ModerationUI moderationUi: ModerationUI
}) { }) {
const t = useTheme() const t = useTheme()
@@ -97,6 +99,11 @@ function WizardListCard({
numberOfLines={1}> numberOfLines={1}>
{subtitle} {subtitle}
</Text> </Text>
{subjectOptedOut ? (
<Text style={[a.text_sm, t.atoms.text_contrast_medium]}>
<Trans>Opted out</Trans>
</Text>
) : null}
</View> </View>
{btnType === 'checkbox' ? ( {btnType === 'checkbox' ? (
<Checkbox /> <Checkbox />
@@ -123,12 +130,14 @@ export function WizardProfileCard({
dispatch, dispatch,
profile, profile,
moderationOpts, moderationOpts,
subjectOptedOut = false,
}: { }: {
btnType: 'checkbox' | 'remove' btnType: 'checkbox' | 'remove'
state: WizardState state: WizardState
dispatch: (action: WizardAction) => void dispatch: (action: WizardAction) => void
profile: bsky.profile.AnyProfileView profile: bsky.profile.AnyProfileView
moderationOpts: ModerationOpts moderationOpts: ModerationOpts
subjectOptedOut?: boolean
}) { }) {
const ax = useAnalytics() const ax = useAnalytics()
const {currentAccount} = useSession() const {currentAccount} = useSession()
@@ -138,7 +147,9 @@ export function WizardProfileCard({
const isTarget = profile.did === targetProfileDid const isTarget = profile.did === targetProfileDid
const included = isTarget || state.profiles.some(p => p.did === profile.did) const included = isTarget || state.profiles.some(p => p.did === profile.did)
const disabled = const disabled =
isTarget || (!included && state.profiles.length >= STARTER_PACK_MAX_SIZE) subjectOptedOut ||
isTarget ||
(!included && state.profiles.length >= STARTER_PACK_MAX_SIZE)
const moderationUi = moderateProfile(profile, moderationOpts).ui('avatar') const moderationUi = moderateProfile(profile, moderationOpts).ui('avatar')
const displayName = profile.displayName const displayName = profile.displayName
? sanitizeDisplayName(profile.displayName) ? sanitizeDisplayName(profile.displayName)
@@ -169,6 +180,7 @@ export function WizardProfileCard({
avatar={profile.avatar} avatar={profile.avatar}
included={included} included={included}
disabled={disabled} disabled={disabled}
subjectOptedOut={subjectOptedOut}
moderationUi={moderationUi} moderationUi={moderationUi}
/> />
) )
@@ -557,6 +557,11 @@ function OverflowMenu({
starterPack: starterPack.uri, starterPack: starterPack.uri,
action, action,
}) })
Toast.show(
action === 'optOut'
? _(msg`Opted out of starter pack`)
: _(msg`Opt-out undone`),
)
}, },
onError: error => { onError: error => {
logger.error('Failed to update starter pack opt-out', { logger.error('Failed to update starter pack opt-out', {
+2 -1
View File
@@ -138,7 +138,8 @@ export function Provider({
currentStep: 'Details', currentStep: 'Details',
name: starterPack.record.name, name: starterPack.record.name,
description: starterPack.record.description, description: starterPack.record.description,
profiles: listItems?.map(i => i.subject) ?? [], profiles:
listItems?.filter(i => !i.subjectOptedOut).map(i => i.subject) ?? [],
feeds: starterPack.feeds ?? [], feeds: starterPack.feeds ?? [],
processing: false, processing: false,
transitionDirection: 'Forward', transitionDirection: 'Forward',
@@ -24,8 +24,10 @@ function keyExtractor(item: bsky.profile.AnyProfileView) {
export function StepProfiles({ export function StepProfiles({
moderationOpts, moderationOpts,
optedOutDids,
}: { }: {
moderationOpts: ModerationOpts moderationOpts: ModerationOpts
optedOutDids: Set<string>
}) { }) {
const t = useTheme() const t = useTheme()
const [state, dispatch] = useWizardState() const [state, dispatch] = useWizardState()
@@ -59,6 +61,7 @@ export function StepProfiles({
state={state} state={state}
dispatch={dispatch} dispatch={dispatch}
moderationOpts={moderationOpts} moderationOpts={moderationOpts}
subjectOptedOut={optedOutDids.has(item.did)}
/> />
) )
} }
+10 -1
View File
@@ -341,7 +341,16 @@ function WizardInner({
{state.currentStep === 'Details' ? ( {state.currentStep === 'Details' ? (
<StepDetails /> <StepDetails />
) : state.currentStep === 'Profiles' ? ( ) : 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' ? ( ) : state.currentStep === 'Feeds' ? (
<StepFeeds moderationOpts={moderationOpts} /> <StepFeeds moderationOpts={moderationOpts} />
) : null} ) : null}