import {View} from 'react-native'
import {Trans, useLingui} from '@lingui/react/macro'
import {useQueryClient} from '@tanstack/react-query'
import {createSanitizedDisplayName} from '#/lib/moderation/create-sanitized-display-name'
import {sanitizeDisplayName} from '#/lib/strings/display-names'
import {isNetworkError} from '#/lib/strings/errors'
import {sanitizeHandle} from '#/lib/strings/handles'
import {logger} from '#/logger'
import {
useListMembershipAddMutation,
useListMembershipRemoveMutation,
} from '#/state/queries/list-memberships'
import {
type ListWithMembership,
removeListMembershipOptimistically,
updateListMembershipOptimistically,
useListsWithMembershipQuery,
} from '#/state/queries/lists-with-membership'
import {useSession} from '#/state/session'
import {UserAvatar} from '#/view/com/util/UserAvatar'
import {atoms as a, platform, useBreakpoints, useTheme, web} from '#/alf'
import {Button, ButtonIcon, ButtonText} from '#/components/Button'
import * as Dialog from '#/components/Dialog'
import {BulletList_Stroke2_Corner0_Rounded as ListIcon} from '#/components/icons/BulletList'
import {TimesLarge_Stroke2_Corner0_Rounded as XIcon} from '#/components/icons/Times'
import {Loader} from '#/components/Loader'
import * as Toast from '#/components/Toast'
import {Text} from '#/components/Typography'
import type * as bsky from '#/types/bsky'
export type UserAddRemoveListsDialogProps = {
control: Dialog.DialogControlProps
profile: bsky.profile.AnyProfileView | undefined
onAdd?: (listUri: string) => void
onRemove?: (listUri: string) => void
}
export function UserAddRemoveListsDialog({
control,
profile,
onAdd,
onRemove,
}: UserAddRemoveListsDialogProps) {
return (
)
}
function Empty() {
const t = useTheme()
return (
You have no lists.
)
}
function ListsContent({
profile,
onAdd,
onRemove,
}: {
profile: bsky.profile.AnyProfileView | undefined
onAdd?: (listUri: string) => void
onRemove?: (listUri: string) => void
}) {
const t = useTheme()
const control = Dialog.useDialogContext()
const {t: l} = useLingui()
const {gtMobile} = useBreakpoints()
const {
data,
isError,
isLoading,
hasNextPage,
isFetchingNextPage,
fetchNextPage,
} = useListsWithMembershipQuery({actor: profile?.did})
const listItems = data?.pages.flatMap(page => page.listsWithMembership) || []
const onEndReached = async () => {
if (isFetchingNextPage || !hasNextPage || isError) return
try {
await fetchNextPage()
} catch (err) {
// Error handling is optional since this is just pagination
}
}
const renderItem = ({item}: {item: ListWithMembership}) =>
profile ? (
) : null
const onClose = () => {
control.close()
}
const listHeader = (
{profile ? (
Update {createSanitizedDisplayName(profile, true)} in Lists
) : (
Update in Lists
)}
)
return (
(
)
: renderItem
}
keyExtractor={
isLoading
? () => 'lists_dialog_loader'
: (item: ListWithMembership) => item.list.uri
}
onEndReached={() => void onEndReached()}
onEndReachedThreshold={0.1}
stickyHeaderIndices={web([0])}
ListHeaderComponent={listHeader}
ListFooterComponent={
isFetchingNextPage ? (
) : undefined
}
ListEmptyComponent={!isLoading && data ? : undefined}
webInnerContentContainerStyle={[a.py_0]}
style={platform({
web: [a.px_2xl, a.pb_md],
native: [a.px_2xl, a.pt_lg],
})}
/>
)
}
function ListItem({
listWithMembership,
profile,
onAdd,
onRemove,
}: {
listWithMembership: ListWithMembership
profile: bsky.profile.AnyProfileView
onAdd?: (listUri: string) => void
onRemove?: (listUri: string) => void
}) {
const {t: l} = useLingui()
const t = useTheme()
const queryClient = useQueryClient()
const {currentAccount} = useSession()
const list = listWithMembership.list
const listItem = listWithMembership.listItem
const isMember = !!listItem
const {mutate: addMembership, isPending: isPendingAdd} =
useListMembershipAddMutation({
subject: profile,
onSuccess: data => {
Toast.show(l`Added to list`)
onAdd?.(list.uri)
updateListMembershipOptimistically({
queryClient,
actor: profile.did,
listUri: list.uri,
membershipUri: data.uri,
subject: {
did: profile.did,
handle: profile.handle,
displayName: profile.displayName,
},
})
},
onError: err => {
if (!isNetworkError(err)) {
logger.error('Failed to add to list', {safeMessage: err})
}
Toast.show(l`Failed to add to list`, {type: 'error'})
},
})
const {mutate: removeMembership, isPending: isPendingRemove} =
useListMembershipRemoveMutation({
onSuccess: () => {
Toast.show(l`Removed from list`)
onRemove?.(list.uri)
removeListMembershipOptimistically({
queryClient,
actor: profile.did,
listUri: list.uri,
})
},
onError: err => {
if (!isNetworkError(err)) {
logger.error('Failed to remove from list', {safeMessage: err})
}
Toast.show(l`Failed to remove from list`, {type: 'error'})
},
})
const isPending = isPendingAdd || isPendingRemove
const handleToggleMembership = () => {
if (isPending) return
if (!isMember) {
addMembership({
listUri: list.uri,
actorDid: profile.did,
})
} else {
if (!listItem?.uri) {
logger.error('Cannot remove: missing membership URI')
return
}
removeMembership({
listUri: list.uri,
actorDid: profile.did,
membershipUri: listItem.uri,
})
}
}
return (
{sanitizeDisplayName(list.name)}
{list.purpose === 'app.bsky.graph.defs#curatelist' &&
(list.creator.did === currentAccount?.did ? (
User list by you
) : (
User list by {sanitizeHandle(list.creator.handle, '@')}
))}
{list.purpose === 'app.bsky.graph.defs#modlist' &&
(list.creator.did === currentAccount?.did ? (
Moderation list by you
) : (
Moderation list by {sanitizeHandle(list.creator.handle, '@')}
))}
)
}