Replace Add User to Lists Modal with modern Dialog (#9677)
Co-authored-by: Claude Haiku 4.5 <noreply@anthropic.com> Co-authored-by: Eric Bailey <git@esb.lol>
This commit is contained in:
@@ -145,21 +145,21 @@ appId: xyz.blueskyweb.app
|
|||||||
id: "profileHeaderDropdownBtn"
|
id: "profileHeaderDropdownBtn"
|
||||||
- tapOn: "Add to Lists"
|
- tapOn: "Add to Lists"
|
||||||
- assertVisible:
|
- assertVisible:
|
||||||
id: "userAddRemoveListsModal"
|
id: "userAddRemoveListsDialog"
|
||||||
- tapOn:
|
- tapOn:
|
||||||
id: "user-bob.test-addBtn"
|
id: "user-bob.test-addBtn"
|
||||||
- tapOn:
|
- tapOn:
|
||||||
id: "doneBtn"
|
id: "doneBtn"
|
||||||
- assertNotVisible:
|
- assertNotVisible:
|
||||||
id: "userAddRemoveListsModal"
|
id: "userAddRemoveListsDialog"
|
||||||
- tapOn:
|
- tapOn:
|
||||||
id: "profileHeaderDropdownBtn"
|
id: "profileHeaderDropdownBtn"
|
||||||
- tapOn: "Add to Lists"
|
- tapOn: "Add to Lists"
|
||||||
- assertVisible:
|
- assertVisible:
|
||||||
id: "userAddRemoveListsModal"
|
id: "userAddRemoveListsDialog"
|
||||||
- tapOn:
|
- tapOn:
|
||||||
id: "user-bob.test-addBtn"
|
id: "user-bob.test-addBtn"
|
||||||
- tapOn:
|
- tapOn:
|
||||||
id: "doneBtn"
|
id: "doneBtn"
|
||||||
- assertNotVisible:
|
- assertNotVisible:
|
||||||
id: "userAddRemoveListsModal"
|
id: "userAddRemoveListsDialog"
|
||||||
|
|||||||
@@ -1836,11 +1836,6 @@
|
|||||||
"count": 1
|
"count": 1
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"src/state/queries/list-memberships.ts": {
|
|
||||||
"@typescript-eslint/no-floating-promises": {
|
|
||||||
"count": 2
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"src/state/queries/list.ts": {
|
"src/state/queries/list.ts": {
|
||||||
"@typescript-eslint/no-floating-promises": {
|
"@typescript-eslint/no-floating-promises": {
|
||||||
"count": 7
|
"count": 7
|
||||||
@@ -2163,20 +2158,6 @@
|
|||||||
"count": 2
|
"count": 2
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"src/view/com/lists/ListMembers.tsx": {
|
|
||||||
"@typescript-eslint/no-explicit-any": {
|
|
||||||
"count": 3
|
|
||||||
},
|
|
||||||
"@typescript-eslint/no-floating-promises": {
|
|
||||||
"count": 1
|
|
||||||
},
|
|
||||||
"@typescript-eslint/no-misused-promises": {
|
|
||||||
"count": 2
|
|
||||||
},
|
|
||||||
"@typescript-eslint/no-unsafe-member-access": {
|
|
||||||
"count": 2
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"src/view/com/lists/MyLists.tsx": {
|
"src/view/com/lists/MyLists.tsx": {
|
||||||
"@typescript-eslint/no-explicit-any": {
|
"@typescript-eslint/no-explicit-any": {
|
||||||
"count": 2
|
"count": 2
|
||||||
@@ -2208,9 +2189,6 @@
|
|||||||
},
|
},
|
||||||
"@typescript-eslint/require-await": {
|
"@typescript-eslint/require-await": {
|
||||||
"count": 1
|
"count": 1
|
||||||
},
|
|
||||||
"react-hooks/refs": {
|
|
||||||
"count": 1
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"src/view/com/notifications/NotificationFeed.tsx": {
|
"src/view/com/notifications/NotificationFeed.tsx": {
|
||||||
|
|||||||
@@ -7,10 +7,8 @@ import {Trans} from '@lingui/react/macro'
|
|||||||
|
|
||||||
import {cleanError} from '#/lib/strings/errors'
|
import {cleanError} from '#/lib/strings/errors'
|
||||||
import {useModerationOpts} from '#/state/preferences/moderation-opts'
|
import {useModerationOpts} from '#/state/preferences/moderation-opts'
|
||||||
|
import {useAllListMembersQuery} from '#/state/queries/list-members'
|
||||||
import {
|
import {
|
||||||
getMembership,
|
|
||||||
type ListMembersip,
|
|
||||||
useDangerousListMembershipsQuery,
|
|
||||||
useListMembershipAddMutation,
|
useListMembershipAddMutation,
|
||||||
useListMembershipRemoveMutation,
|
useListMembershipRemoveMutation,
|
||||||
} from '#/state/queries/list-memberships'
|
} from '#/state/queries/list-memberships'
|
||||||
@@ -61,7 +59,7 @@ function DialogInner({
|
|||||||
}) {
|
}) {
|
||||||
const {_} = useLingui()
|
const {_} = useLingui()
|
||||||
const moderationOpts = useModerationOpts()
|
const moderationOpts = useModerationOpts()
|
||||||
const {data: memberships} = useDangerousListMembershipsQuery()
|
const {data: listMembers} = useAllListMembersQuery(list.uri)
|
||||||
|
|
||||||
const renderProfileCard = useCallback(
|
const renderProfileCard = useCallback(
|
||||||
(item: ProfileItem) => {
|
(item: ProfileItem) => {
|
||||||
@@ -69,13 +67,13 @@ function DialogInner({
|
|||||||
<UserResult
|
<UserResult
|
||||||
profile={item.profile}
|
profile={item.profile}
|
||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
memberships={memberships}
|
listMembers={listMembers}
|
||||||
list={list}
|
list={list}
|
||||||
moderationOpts={moderationOpts}
|
moderationOpts={moderationOpts}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
[onChange, memberships, list, moderationOpts],
|
[onChange, listMembers, list, moderationOpts],
|
||||||
)
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -86,16 +84,30 @@ function DialogInner({
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns undefined for pending, false for not a member, and string for a member (the URI of the membership record)
|
||||||
|
*/
|
||||||
|
function getMembership(
|
||||||
|
listMembers: AppBskyGraphDefs.ListItemView[] | undefined,
|
||||||
|
actorDid: string,
|
||||||
|
): string | false | undefined {
|
||||||
|
if (!listMembers) {
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
const member = listMembers.find(item => item.subject.did === actorDid)
|
||||||
|
return member ? member.uri : false
|
||||||
|
}
|
||||||
|
|
||||||
function UserResult({
|
function UserResult({
|
||||||
profile,
|
profile,
|
||||||
list,
|
list,
|
||||||
memberships,
|
listMembers,
|
||||||
onChange,
|
onChange,
|
||||||
moderationOpts,
|
moderationOpts,
|
||||||
}: {
|
}: {
|
||||||
profile: bsky.profile.AnyProfileView
|
profile: bsky.profile.AnyProfileView
|
||||||
list: AppBskyGraphDefs.ListView
|
list: AppBskyGraphDefs.ListView
|
||||||
memberships: ListMembersip[] | undefined
|
listMembers: AppBskyGraphDefs.ListItemView[] | undefined
|
||||||
onChange?: (
|
onChange?: (
|
||||||
type: 'add' | 'remove',
|
type: 'add' | 'remove',
|
||||||
profile: bsky.profile.AnyProfileView,
|
profile: bsky.profile.AnyProfileView,
|
||||||
@@ -104,8 +116,8 @@ function UserResult({
|
|||||||
}) {
|
}) {
|
||||||
const {_} = useLingui()
|
const {_} = useLingui()
|
||||||
const membership = useMemo(
|
const membership = useMemo(
|
||||||
() => getMembership(memberships, list.uri, profile.did),
|
() => getMembership(listMembers, profile.did),
|
||||||
[memberships, list.uri, profile.did],
|
[listMembers, profile.did],
|
||||||
)
|
)
|
||||||
const {mutate: listMembershipAdd, isPending: isAddingPending} =
|
const {mutate: listMembershipAdd, isPending: isAddingPending} =
|
||||||
useListMembershipAddMutation({
|
useListMembershipAddMutation({
|
||||||
|
|||||||
@@ -0,0 +1,331 @@
|
|||||||
|
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 (
|
||||||
|
<Dialog.Outer
|
||||||
|
control={control}
|
||||||
|
testID="userAddRemoveListsDialog"
|
||||||
|
nativeOptions={{fullHeight: true}}>
|
||||||
|
<Dialog.Handle />
|
||||||
|
<ListsContent profile={profile} onAdd={onAdd} onRemove={onRemove} />
|
||||||
|
</Dialog.Outer>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function Empty() {
|
||||||
|
const t = useTheme()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View
|
||||||
|
style={[
|
||||||
|
a.gap_2xl,
|
||||||
|
platform({web: {paddingTop: 100}, native: {paddingTop: 64}}),
|
||||||
|
]}>
|
||||||
|
<View style={[a.gap_xs, a.align_center]}>
|
||||||
|
<ListIcon
|
||||||
|
size="xl"
|
||||||
|
style={{color: t.atoms.border_contrast_medium.borderColor}}
|
||||||
|
/>
|
||||||
|
<Text style={[a.text_center]}>
|
||||||
|
<Trans>You have no lists.</Trans>
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
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 ? (
|
||||||
|
<ListItem
|
||||||
|
listWithMembership={item}
|
||||||
|
profile={profile}
|
||||||
|
onAdd={onAdd}
|
||||||
|
onRemove={onRemove}
|
||||||
|
/>
|
||||||
|
) : null
|
||||||
|
|
||||||
|
const onClose = () => {
|
||||||
|
control.close()
|
||||||
|
}
|
||||||
|
|
||||||
|
const listHeader = (
|
||||||
|
<View
|
||||||
|
style={[
|
||||||
|
a.justify_between,
|
||||||
|
a.align_center,
|
||||||
|
a.flex_row,
|
||||||
|
a.pb_lg,
|
||||||
|
t.atoms.bg,
|
||||||
|
gtMobile ? a.pt_2xl : a.pt_xl,
|
||||||
|
]}>
|
||||||
|
<Text style={[a.text_lg, a.font_semi_bold]}>
|
||||||
|
{profile ? (
|
||||||
|
<Trans>
|
||||||
|
Update {createSanitizedDisplayName(profile, true)} in Lists
|
||||||
|
</Trans>
|
||||||
|
) : (
|
||||||
|
<Trans>Update in Lists</Trans>
|
||||||
|
)}
|
||||||
|
</Text>
|
||||||
|
<Button
|
||||||
|
testID="doneBtn"
|
||||||
|
label={l`Close`}
|
||||||
|
onPress={onClose}
|
||||||
|
variant="ghost"
|
||||||
|
color="secondary"
|
||||||
|
size="small"
|
||||||
|
shape="round"
|
||||||
|
style={{margin: -8}}>
|
||||||
|
<ButtonIcon icon={XIcon} />
|
||||||
|
</Button>
|
||||||
|
</View>
|
||||||
|
)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog.InnerFlatList
|
||||||
|
data={isLoading ? [{}] : listItems}
|
||||||
|
renderItem={
|
||||||
|
isLoading
|
||||||
|
? () => (
|
||||||
|
<View style={[a.align_center, a.py_2xl]}>
|
||||||
|
<Loader size="xl" />
|
||||||
|
</View>
|
||||||
|
)
|
||||||
|
: renderItem
|
||||||
|
}
|
||||||
|
keyExtractor={
|
||||||
|
isLoading
|
||||||
|
? () => 'lists_dialog_loader'
|
||||||
|
: (item: ListWithMembership) => item.list.uri
|
||||||
|
}
|
||||||
|
onEndReached={() => void onEndReached()}
|
||||||
|
onEndReachedThreshold={0.1}
|
||||||
|
stickyHeaderIndices={web([0])}
|
||||||
|
ListHeaderComponent={listHeader}
|
||||||
|
ListFooterComponent={
|
||||||
|
isFetchingNextPage ? (
|
||||||
|
<View style={[a.align_center, a.py_lg]}>
|
||||||
|
<Loader size="lg" />
|
||||||
|
</View>
|
||||||
|
) : null
|
||||||
|
}
|
||||||
|
ListEmptyComponent={!isLoading && data ? <Empty /> : null}
|
||||||
|
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 (
|
||||||
|
<View
|
||||||
|
testID={`toggleBtn-${list.name}`}
|
||||||
|
style={[a.flex_row, a.align_center, a.py_md, a.gap_md]}>
|
||||||
|
<UserAvatar size={40} avatar={list.avatar} type="list" />
|
||||||
|
<View style={[a.flex_1]}>
|
||||||
|
<Text
|
||||||
|
style={[a.text_md, a.font_semi_bold, a.leading_snug]}
|
||||||
|
numberOfLines={1}>
|
||||||
|
{sanitizeDisplayName(list.name)}
|
||||||
|
</Text>
|
||||||
|
<Text
|
||||||
|
style={[a.text_sm, a.leading_snug, t.atoms.text_contrast_medium]}
|
||||||
|
numberOfLines={1}>
|
||||||
|
{list.purpose === 'app.bsky.graph.defs#curatelist' &&
|
||||||
|
(list.creator.did === currentAccount?.did ? (
|
||||||
|
<Trans>User list by you</Trans>
|
||||||
|
) : (
|
||||||
|
<Trans>
|
||||||
|
User list by {sanitizeHandle(list.creator.handle, '@')}
|
||||||
|
</Trans>
|
||||||
|
))}
|
||||||
|
{list.purpose === 'app.bsky.graph.defs#modlist' &&
|
||||||
|
(list.creator.did === currentAccount?.did ? (
|
||||||
|
<Trans>Moderation list by you</Trans>
|
||||||
|
) : (
|
||||||
|
<Trans>
|
||||||
|
Moderation list by {sanitizeHandle(list.creator.handle, '@')}
|
||||||
|
</Trans>
|
||||||
|
))}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
<Button
|
||||||
|
testID={`user-${profile.handle}-addBtn`}
|
||||||
|
label={isMember ? l`Remove` : l`Add`}
|
||||||
|
onPress={handleToggleMembership}
|
||||||
|
disabled={isPending}
|
||||||
|
size="tiny"
|
||||||
|
color={isMember ? 'secondary' : 'primary_subtle'}>
|
||||||
|
{isPending && <ButtonIcon icon={Loader} />}
|
||||||
|
<ButtonText>
|
||||||
|
{isMember ? <Trans>Remove</Trans> : <Trans>Add</Trans>}
|
||||||
|
</ButtonText>
|
||||||
|
</Button>
|
||||||
|
</View>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import {useCallback} from 'react'
|
import {useCallback} from 'react'
|
||||||
import {type ChatBskyActorDefs, ChatBskyConvoDefs} from '@atproto/api'
|
import {type ChatBskyActorDefs, type ChatBskyConvoDefs} from '@atproto/api'
|
||||||
import {Trans, useLingui} from '@lingui/react/macro'
|
import {Trans, useLingui} from '@lingui/react/macro'
|
||||||
import {StackActions, useNavigation} from '@react-navigation/native'
|
import {StackActions, useNavigation} from '@react-navigation/native'
|
||||||
import {useQueryClient} from '@tanstack/react-query'
|
import {useQueryClient} from '@tanstack/react-query'
|
||||||
|
|||||||
@@ -3,15 +3,6 @@ import {createContext, useContext, useEffect, useMemo, useState} from 'react'
|
|||||||
import {useNonReactiveCallback} from '#/lib/hooks/useNonReactiveCallback'
|
import {useNonReactiveCallback} from '#/lib/hooks/useNonReactiveCallback'
|
||||||
import {useHotkeysContext} from '#/lib/hotkeys'
|
import {useHotkeysContext} from '#/lib/hotkeys'
|
||||||
|
|
||||||
export interface UserAddRemoveListsModal {
|
|
||||||
name: 'user-add-remove-lists'
|
|
||||||
subject: string
|
|
||||||
handle: string
|
|
||||||
displayName: string
|
|
||||||
onAdd?: (listUri: string) => void
|
|
||||||
onRemove?: (listUri: string) => void
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ContentLanguagesSettingsModal {
|
export interface ContentLanguagesSettingsModal {
|
||||||
name: 'content-languages-settings'
|
name: 'content-languages-settings'
|
||||||
}
|
}
|
||||||
@@ -21,10 +12,7 @@ export interface ContentLanguagesSettingsModal {
|
|||||||
*/
|
*/
|
||||||
export type Modal =
|
export type Modal =
|
||||||
// Curation
|
// Curation
|
||||||
| ContentLanguagesSettingsModal
|
ContentLanguagesSettingsModal
|
||||||
|
|
||||||
// Lists
|
|
||||||
| UserAddRemoveListsModal
|
|
||||||
|
|
||||||
const ModalContext = createContext<{
|
const ModalContext = createContext<{
|
||||||
isModalActive: boolean
|
isModalActive: boolean
|
||||||
|
|||||||
@@ -1,19 +1,3 @@
|
|||||||
/**
|
|
||||||
* NOTE
|
|
||||||
*
|
|
||||||
* This query is a temporary solution to our lack of server API for
|
|
||||||
* querying user membership in an API. It is extremely inefficient.
|
|
||||||
*
|
|
||||||
* THIS SHOULD ONLY BE USED IN MODALS FOR MODIFYING A USER'S LIST MEMBERSHIP!
|
|
||||||
* Use the list-members query for rendering a list's members.
|
|
||||||
*
|
|
||||||
* It works by fetching *all* of the user's list item records and querying
|
|
||||||
* or manipulating that cache. For users with large lists, it will fall
|
|
||||||
* down completely, so be very conservative about how you use it.
|
|
||||||
*
|
|
||||||
* -prf
|
|
||||||
*/
|
|
||||||
|
|
||||||
import {
|
import {
|
||||||
type AppBskyActorDefs,
|
type AppBskyActorDefs,
|
||||||
type AppBskyGraphGetStarterPacksWithMembership,
|
type AppBskyGraphGetStarterPacksWithMembership,
|
||||||
@@ -22,85 +6,17 @@ import {
|
|||||||
import {
|
import {
|
||||||
type InfiniteData,
|
type InfiniteData,
|
||||||
useMutation,
|
useMutation,
|
||||||
useQuery,
|
|
||||||
useQueryClient,
|
useQueryClient,
|
||||||
} from '@tanstack/react-query'
|
} from '@tanstack/react-query'
|
||||||
|
|
||||||
import {STALE} from '#/state/queries'
|
import {
|
||||||
import {RQKEY as LIST_MEMBERS_RQKEY} from '#/state/queries/list-members'
|
RQKEY as LIST_MEMBERS_RQKEY,
|
||||||
|
RQKEY_ALL as LIST_MEMBERS_ALL_RQKEY,
|
||||||
|
} from '#/state/queries/list-members'
|
||||||
import {useAgent, useSession} from '#/state/session'
|
import {useAgent, useSession} from '#/state/session'
|
||||||
import type * as bsky from '#/types/bsky'
|
import type * as bsky from '#/types/bsky'
|
||||||
import {RQKEY_WITH_MEMBERSHIP as STARTER_PACKS_WITH_MEMBERSHIPS_RKEY} from './actor-starter-packs'
|
import {RQKEY_WITH_MEMBERSHIP as STARTER_PACKS_WITH_MEMBERSHIPS_RKEY} from './actor-starter-packs'
|
||||||
|
|
||||||
// sanity limit is SANITY_PAGE_LIMIT*PAGE_SIZE total records
|
|
||||||
const SANITY_PAGE_LIMIT = 1000
|
|
||||||
const PAGE_SIZE = 100
|
|
||||||
// ...which comes 100,000k list members
|
|
||||||
|
|
||||||
const RQKEY_ROOT = 'list-memberships'
|
|
||||||
export const RQKEY = () => [RQKEY_ROOT]
|
|
||||||
|
|
||||||
export interface ListMembersip {
|
|
||||||
membershipUri: string
|
|
||||||
listUri: string
|
|
||||||
actorDid: string
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This API is dangerous! Read the note above!
|
|
||||||
*/
|
|
||||||
export function useDangerousListMembershipsQuery() {
|
|
||||||
const {currentAccount} = useSession()
|
|
||||||
const agent = useAgent()
|
|
||||||
return useQuery<ListMembersip[]>({
|
|
||||||
staleTime: STALE.MINUTES.FIVE,
|
|
||||||
queryKey: RQKEY(),
|
|
||||||
async queryFn() {
|
|
||||||
if (!currentAccount) {
|
|
||||||
return []
|
|
||||||
}
|
|
||||||
let cursor
|
|
||||||
let arr: ListMembersip[] = []
|
|
||||||
for (let i = 0; i < SANITY_PAGE_LIMIT; i++) {
|
|
||||||
const res = await agent.app.bsky.graph.listitem.list({
|
|
||||||
repo: currentAccount.did,
|
|
||||||
limit: PAGE_SIZE,
|
|
||||||
cursor,
|
|
||||||
})
|
|
||||||
arr = arr.concat(
|
|
||||||
res.records.map(r => ({
|
|
||||||
membershipUri: r.uri,
|
|
||||||
listUri: r.value.list,
|
|
||||||
actorDid: r.value.subject,
|
|
||||||
})),
|
|
||||||
)
|
|
||||||
cursor = res.cursor
|
|
||||||
if (!cursor) {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return arr
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns undefined for pending, false for not a member, and string for a member (the URI of the membership record)
|
|
||||||
*/
|
|
||||||
export function getMembership(
|
|
||||||
memberships: ListMembersip[] | undefined,
|
|
||||||
list: string,
|
|
||||||
actor: string,
|
|
||||||
): string | false | undefined {
|
|
||||||
if (!memberships) {
|
|
||||||
return undefined
|
|
||||||
}
|
|
||||||
const membership = memberships.find(
|
|
||||||
m => m.listUri === list && m.actorDid === actor,
|
|
||||||
)
|
|
||||||
return membership ? membership.membershipUri : false
|
|
||||||
}
|
|
||||||
|
|
||||||
export function useListMembershipAddMutation({
|
export function useListMembershipAddMutation({
|
||||||
subject,
|
subject,
|
||||||
onSuccess,
|
onSuccess,
|
||||||
@@ -133,43 +49,21 @@ export function useListMembershipAddMutation({
|
|||||||
createdAt: new Date().toISOString(),
|
createdAt: new Date().toISOString(),
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
// TODO
|
|
||||||
// we need to wait for appview to update, but there's not an efficient
|
|
||||||
// query for that, so we use a timeout below
|
|
||||||
// -prf
|
|
||||||
return res
|
return res
|
||||||
},
|
},
|
||||||
onSuccess: (data, variables) => {
|
onSuccess: (data, variables) => {
|
||||||
// manually update the cache; a refetch is too expensive
|
|
||||||
let memberships = queryClient.getQueryData<ListMembersip[]>(RQKEY())
|
|
||||||
if (memberships) {
|
|
||||||
memberships = memberships
|
|
||||||
// avoid dups
|
|
||||||
.filter(
|
|
||||||
m =>
|
|
||||||
!(
|
|
||||||
m.actorDid === variables.actorDid &&
|
|
||||||
m.listUri === variables.listUri
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.concat([
|
|
||||||
{
|
|
||||||
...variables,
|
|
||||||
membershipUri: data.uri,
|
|
||||||
},
|
|
||||||
])
|
|
||||||
queryClient.setQueryData(RQKEY(), memberships)
|
|
||||||
}
|
|
||||||
// invalidate the members queries (used for rendering the listings)
|
// invalidate the members queries (used for rendering the listings)
|
||||||
// use a timeout to wait for the appview (see above)
|
// use a timeout to wait for the appview
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
queryClient.invalidateQueries({
|
void queryClient.invalidateQueries({
|
||||||
queryKey: LIST_MEMBERS_RQKEY(variables.listUri),
|
queryKey: LIST_MEMBERS_RQKEY(variables.listUri),
|
||||||
})
|
})
|
||||||
|
void queryClient.invalidateQueries({
|
||||||
|
queryKey: LIST_MEMBERS_ALL_RQKEY(variables.listUri),
|
||||||
|
})
|
||||||
}, 1e3)
|
}, 1e3)
|
||||||
|
|
||||||
// update WITH_MEMBERSHIPS query
|
// update WITH_MEMBERSHIPS query for starter packs
|
||||||
|
|
||||||
if (subject) {
|
if (subject) {
|
||||||
queryClient.setQueryData<
|
queryClient.setQueryData<
|
||||||
InfiniteData<AppBskyGraphGetStarterPacksWithMembership.OutputSchema>
|
InfiniteData<AppBskyGraphGetStarterPacksWithMembership.OutputSchema>
|
||||||
@@ -251,32 +145,20 @@ export function useListMembershipRemoveMutation({
|
|||||||
repo: currentAccount.did,
|
repo: currentAccount.did,
|
||||||
rkey: membershipUrip.rkey,
|
rkey: membershipUrip.rkey,
|
||||||
})
|
})
|
||||||
// TODO
|
|
||||||
// we need to wait for appview to update, but there's not an efficient
|
|
||||||
// query for that, so we use a timeout below
|
|
||||||
// -prf
|
|
||||||
},
|
},
|
||||||
onSuccess: (data, variables) => {
|
onSuccess: (data, variables) => {
|
||||||
// manually update the cache; a refetch is too expensive
|
|
||||||
let memberships = queryClient.getQueryData<ListMembersip[]>(RQKEY())
|
|
||||||
if (memberships) {
|
|
||||||
memberships = memberships.filter(
|
|
||||||
m =>
|
|
||||||
!(
|
|
||||||
m.actorDid === variables.actorDid &&
|
|
||||||
m.listUri === variables.listUri
|
|
||||||
),
|
|
||||||
)
|
|
||||||
queryClient.setQueryData(RQKEY(), memberships)
|
|
||||||
}
|
|
||||||
// invalidate the members queries (used for rendering the listings)
|
// invalidate the members queries (used for rendering the listings)
|
||||||
// use a timeout to wait for the appview (see above)
|
// use a timeout to wait for the appview
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
queryClient.invalidateQueries({
|
void queryClient.invalidateQueries({
|
||||||
queryKey: LIST_MEMBERS_RQKEY(variables.listUri),
|
queryKey: LIST_MEMBERS_RQKEY(variables.listUri),
|
||||||
})
|
})
|
||||||
|
void queryClient.invalidateQueries({
|
||||||
|
queryKey: LIST_MEMBERS_ALL_RQKEY(variables.listUri),
|
||||||
|
})
|
||||||
}, 1e3)
|
}, 1e3)
|
||||||
|
|
||||||
|
// update WITH_MEMBERSHIPS query for starter packs
|
||||||
queryClient.setQueryData<
|
queryClient.setQueryData<
|
||||||
InfiniteData<AppBskyGraphGetStarterPacksWithMembership.OutputSchema>
|
InfiniteData<AppBskyGraphGetStarterPacksWithMembership.OutputSchema>
|
||||||
>(STARTER_PACKS_WITH_MEMBERSHIPS_RKEY(variables.actorDid), old => {
|
>(STARTER_PACKS_WITH_MEMBERSHIPS_RKEY(variables.actorDid), old => {
|
||||||
|
|||||||
@@ -0,0 +1,122 @@
|
|||||||
|
import {
|
||||||
|
type AppBskyActorDefs,
|
||||||
|
type AppBskyGraphGetListsWithMembership,
|
||||||
|
} from '@atproto/api'
|
||||||
|
import {
|
||||||
|
type InfiniteData,
|
||||||
|
type QueryClient,
|
||||||
|
type QueryKey,
|
||||||
|
useInfiniteQuery,
|
||||||
|
} from '@tanstack/react-query'
|
||||||
|
|
||||||
|
import {createQueryKey} from '#/state/queries/util'
|
||||||
|
import {useAgent} from '#/state/session'
|
||||||
|
|
||||||
|
export type ListWithMembership =
|
||||||
|
AppBskyGraphGetListsWithMembership.ListWithMembership
|
||||||
|
|
||||||
|
const listsWithMembershipQueryKeyRoot = 'lists-with-membership'
|
||||||
|
export const createListsWithMembershipQueryKey = (args: {actor: string}) =>
|
||||||
|
createQueryKey(listsWithMembershipQueryKeyRoot, args)
|
||||||
|
|
||||||
|
export function useListsWithMembershipQuery({
|
||||||
|
actor,
|
||||||
|
enabled = true,
|
||||||
|
}: {
|
||||||
|
actor: string | undefined
|
||||||
|
enabled?: boolean
|
||||||
|
}) {
|
||||||
|
const agent = useAgent()
|
||||||
|
|
||||||
|
return useInfiniteQuery<
|
||||||
|
AppBskyGraphGetListsWithMembership.OutputSchema,
|
||||||
|
Error,
|
||||||
|
InfiniteData<AppBskyGraphGetListsWithMembership.OutputSchema>,
|
||||||
|
QueryKey,
|
||||||
|
string | undefined
|
||||||
|
>({
|
||||||
|
queryKey: createListsWithMembershipQueryKey({actor: actor ?? ''}),
|
||||||
|
queryFn: async ({pageParam}: {pageParam?: string}) => {
|
||||||
|
const res = await agent.app.bsky.graph.getListsWithMembership({
|
||||||
|
actor: actor!, // the enabled flag prevents this from running until actor is set
|
||||||
|
limit: 50,
|
||||||
|
cursor: pageParam,
|
||||||
|
})
|
||||||
|
return res.data
|
||||||
|
},
|
||||||
|
enabled: Boolean(actor) && enabled,
|
||||||
|
initialPageParam: undefined,
|
||||||
|
getNextPageParam: lastPage => lastPage.cursor,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function updateListMembershipOptimistically({
|
||||||
|
queryClient,
|
||||||
|
actor,
|
||||||
|
listUri,
|
||||||
|
membershipUri,
|
||||||
|
subject,
|
||||||
|
}: {
|
||||||
|
queryClient: QueryClient
|
||||||
|
actor: string
|
||||||
|
listUri: string
|
||||||
|
membershipUri: string
|
||||||
|
subject: AppBskyActorDefs.ProfileView
|
||||||
|
}) {
|
||||||
|
queryClient.setQueryData<
|
||||||
|
InfiniteData<AppBskyGraphGetListsWithMembership.OutputSchema>
|
||||||
|
>(createListsWithMembershipQueryKey({actor}), old => {
|
||||||
|
if (!old) return old
|
||||||
|
|
||||||
|
return {
|
||||||
|
...old,
|
||||||
|
pages: old.pages.map(page => ({
|
||||||
|
...page,
|
||||||
|
listsWithMembership: page.listsWithMembership.map(lwm => {
|
||||||
|
if (lwm.list.uri === listUri) {
|
||||||
|
return {
|
||||||
|
...lwm,
|
||||||
|
listItem: {
|
||||||
|
uri: membershipUri,
|
||||||
|
subject,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return lwm
|
||||||
|
}),
|
||||||
|
})),
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function removeListMembershipOptimistically({
|
||||||
|
queryClient,
|
||||||
|
actor,
|
||||||
|
listUri,
|
||||||
|
}: {
|
||||||
|
queryClient: QueryClient
|
||||||
|
actor: string
|
||||||
|
listUri: string
|
||||||
|
}) {
|
||||||
|
queryClient.setQueryData<
|
||||||
|
InfiniteData<AppBskyGraphGetListsWithMembership.OutputSchema>
|
||||||
|
>(createListsWithMembershipQueryKey({actor}), old => {
|
||||||
|
if (!old) return old
|
||||||
|
|
||||||
|
return {
|
||||||
|
...old,
|
||||||
|
pages: old.pages.map(page => ({
|
||||||
|
...page,
|
||||||
|
listsWithMembership: page.listsWithMembership.map(lwm => {
|
||||||
|
if (lwm.list.uri === listUri) {
|
||||||
|
return {
|
||||||
|
...lwm,
|
||||||
|
listItem: undefined,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return lwm
|
||||||
|
}),
|
||||||
|
})),
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import {type JSX, useCallback, useMemo, useState} from 'react'
|
import {useCallback, useMemo, useState} from 'react'
|
||||||
import {
|
import {
|
||||||
Dimensions,
|
Dimensions,
|
||||||
type GestureResponderEvent,
|
type GestureResponderEvent,
|
||||||
@@ -13,7 +13,6 @@ import {Trans} from '@lingui/react/macro'
|
|||||||
|
|
||||||
import {cleanError} from '#/lib/strings/errors'
|
import {cleanError} from '#/lib/strings/errors'
|
||||||
import {logger} from '#/logger'
|
import {logger} from '#/logger'
|
||||||
import {useModalControls} from '#/state/modals'
|
|
||||||
import {useModerationOpts} from '#/state/preferences/moderation-opts'
|
import {useModerationOpts} from '#/state/preferences/moderation-opts'
|
||||||
import {useListMembersQuery} from '#/state/queries/list-members'
|
import {useListMembersQuery} from '#/state/queries/list-members'
|
||||||
import {useSession} from '#/state/session'
|
import {useSession} from '#/state/session'
|
||||||
@@ -23,14 +22,29 @@ import {ProfileCardFeedLoadingPlaceholder} from '#/view/com/util/LoadingPlacehol
|
|||||||
import {LoadMoreRetryBtn} from '#/view/com/util/LoadMoreRetryBtn'
|
import {LoadMoreRetryBtn} from '#/view/com/util/LoadMoreRetryBtn'
|
||||||
import {atoms as a, useTheme} from '#/alf'
|
import {atoms as a, useTheme} from '#/alf'
|
||||||
import {Button, ButtonText} from '#/components/Button'
|
import {Button, ButtonText} from '#/components/Button'
|
||||||
|
import {useDialogControl} from '#/components/Dialog'
|
||||||
|
import {UserAddRemoveListsDialog} from '#/components/dialogs/lists/UserAddRemoveListsDialog'
|
||||||
import {ListFooter} from '#/components/Lists'
|
import {ListFooter} from '#/components/Lists'
|
||||||
import * as ProfileCard from '#/components/ProfileCard'
|
import * as ProfileCard from '#/components/ProfileCard'
|
||||||
import type * as bsky from '#/types/bsky'
|
import type * as bsky from '#/types/bsky'
|
||||||
|
|
||||||
const LOADING_ITEM = {_reactKey: '__loading__'}
|
const LOADING_ITEM = {kind: 'loading', _reactKey: '__loading__'} as const
|
||||||
const EMPTY_ITEM = {_reactKey: '__empty__'}
|
const EMPTY_ITEM = {kind: 'empty', _reactKey: '__empty__'} as const
|
||||||
const ERROR_ITEM = {_reactKey: '__error__'}
|
const ERROR_ITEM = {kind: 'error', _reactKey: '__error__'} as const
|
||||||
const LOAD_MORE_ERROR_ITEM = {_reactKey: '__load_more_error__'}
|
const LOAD_MORE_ERROR_ITEM = {
|
||||||
|
kind: 'load_more_error',
|
||||||
|
_reactKey: '__load_more_error__',
|
||||||
|
} as const
|
||||||
|
|
||||||
|
type Item =
|
||||||
|
| typeof LOADING_ITEM
|
||||||
|
| typeof EMPTY_ITEM
|
||||||
|
| typeof ERROR_ITEM
|
||||||
|
| typeof LOAD_MORE_ERROR_ITEM
|
||||||
|
| {
|
||||||
|
kind: 'list_item'
|
||||||
|
listItem: AppBskyGraphDefs.ListItemView
|
||||||
|
}
|
||||||
|
|
||||||
export function ListMembers({
|
export function ListMembers({
|
||||||
list,
|
list,
|
||||||
@@ -49,8 +63,8 @@ export function ListMembers({
|
|||||||
scrollElRef?: ListRef
|
scrollElRef?: ListRef
|
||||||
onScrolledDownChange: (isScrolledDown: boolean) => void
|
onScrolledDownChange: (isScrolledDown: boolean) => void
|
||||||
onPressTryAgain?: () => void
|
onPressTryAgain?: () => void
|
||||||
renderHeader: () => JSX.Element
|
renderHeader: () => React.ReactElement
|
||||||
renderEmptyState: () => JSX.Element
|
renderEmptyState: () => React.ReactElement
|
||||||
testID?: string
|
testID?: string
|
||||||
headerOffset?: number
|
headerOffset?: number
|
||||||
desktopFixedHeightOffset?: number
|
desktopFixedHeightOffset?: number
|
||||||
@@ -58,9 +72,12 @@ export function ListMembers({
|
|||||||
const t = useTheme()
|
const t = useTheme()
|
||||||
const {_} = useLingui()
|
const {_} = useLingui()
|
||||||
const [isRefreshing, setIsRefreshing] = useState(false)
|
const [isRefreshing, setIsRefreshing] = useState(false)
|
||||||
const {openModal} = useModalControls()
|
|
||||||
const {currentAccount} = useSession()
|
const {currentAccount} = useSession()
|
||||||
const moderationOpts = useModerationOpts()
|
const moderationOpts = useModerationOpts()
|
||||||
|
const editListsDialogControl = useDialogControl()
|
||||||
|
const [selectedProfile, setSelectedProfile] = useState<
|
||||||
|
bsky.profile.AnyProfileView | undefined
|
||||||
|
>()
|
||||||
|
|
||||||
const {
|
const {
|
||||||
data,
|
data,
|
||||||
@@ -78,23 +95,28 @@ export function ListMembers({
|
|||||||
currentAccount && data?.pages[0].list.creator.did === currentAccount.did
|
currentAccount && data?.pages[0].list.creator.did === currentAccount.did
|
||||||
|
|
||||||
const items = useMemo(() => {
|
const items = useMemo(() => {
|
||||||
let items: any[] = []
|
const items: Item[] = []
|
||||||
if (isFetched) {
|
if (isFetched) {
|
||||||
if (isEmpty && isError) {
|
if (isEmpty && isError) {
|
||||||
items = items.concat([ERROR_ITEM])
|
items.push(ERROR_ITEM)
|
||||||
}
|
}
|
||||||
if (isEmpty) {
|
if (isEmpty) {
|
||||||
items = items.concat([EMPTY_ITEM])
|
items.push(EMPTY_ITEM)
|
||||||
} else if (data) {
|
} else if (data) {
|
||||||
for (const page of data.pages) {
|
for (const page of data.pages) {
|
||||||
items = items.concat(page.items)
|
items.push(
|
||||||
|
...page.items.map(item => ({
|
||||||
|
kind: 'list_item' as const,
|
||||||
|
listItem: item,
|
||||||
|
})),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (!isEmpty && isError) {
|
if (!isEmpty && isError) {
|
||||||
items = items.concat([LOAD_MORE_ERROR_ITEM])
|
items.push(LOAD_MORE_ERROR_ITEM)
|
||||||
}
|
}
|
||||||
} else if (isFetching) {
|
} else if (isFetching) {
|
||||||
items = items.concat([LOADING_ITEM])
|
items.push(LOADING_ITEM)
|
||||||
}
|
}
|
||||||
return items
|
return items
|
||||||
}, [isFetched, isEmpty, isError, data, isFetching])
|
}, [isFetched, isEmpty, isError, data, isFetching])
|
||||||
@@ -122,91 +144,97 @@ export function ListMembers({
|
|||||||
}, [isFetching, hasNextPage, isError, fetchNextPage])
|
}, [isFetching, hasNextPage, isError, fetchNextPage])
|
||||||
|
|
||||||
const onPressRetryLoadMore = useCallback(() => {
|
const onPressRetryLoadMore = useCallback(() => {
|
||||||
fetchNextPage()
|
void fetchNextPage()
|
||||||
}, [fetchNextPage])
|
}, [fetchNextPage])
|
||||||
|
|
||||||
const onPressEditMembership = useCallback(
|
const onPressEditMembership = useCallback(
|
||||||
(e: GestureResponderEvent, profile: bsky.profile.AnyProfileView) => {
|
(e: GestureResponderEvent, profile: bsky.profile.AnyProfileView) => {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
openModal({
|
setSelectedProfile(profile)
|
||||||
name: 'user-add-remove-lists',
|
editListsDialogControl.open()
|
||||||
subject: profile.did,
|
|
||||||
displayName: profile.displayName || profile.handle,
|
|
||||||
handle: profile.handle,
|
|
||||||
})
|
|
||||||
},
|
},
|
||||||
[openModal],
|
[editListsDialogControl],
|
||||||
)
|
)
|
||||||
|
|
||||||
// rendering
|
// rendering
|
||||||
// =
|
// =
|
||||||
|
|
||||||
const renderItem = useCallback(
|
const renderItem = useCallback(
|
||||||
({item}: {item: any}) => {
|
({item}: {item: Item}) => {
|
||||||
if (item === EMPTY_ITEM) {
|
switch (item.kind) {
|
||||||
return renderEmptyState()
|
case 'empty': {
|
||||||
} else if (item === ERROR_ITEM) {
|
return renderEmptyState()
|
||||||
return (
|
}
|
||||||
<ErrorMessage
|
case 'error': {
|
||||||
message={cleanError(error)}
|
return (
|
||||||
onPressTryAgain={onPressTryAgain}
|
<ErrorMessage
|
||||||
/>
|
message={cleanError(error)}
|
||||||
)
|
onPressTryAgain={onPressTryAgain}
|
||||||
} else if (item === LOAD_MORE_ERROR_ITEM) {
|
/>
|
||||||
return (
|
)
|
||||||
<LoadMoreRetryBtn
|
}
|
||||||
label={_(
|
case 'load_more_error': {
|
||||||
msg`There was an issue fetching the list. Tap here to try again.`,
|
return (
|
||||||
)}
|
<LoadMoreRetryBtn
|
||||||
onPress={onPressRetryLoadMore}
|
label={_(
|
||||||
/>
|
msg`There was an issue fetching the list. Tap here to try again.`,
|
||||||
)
|
)}
|
||||||
} else if (item === LOADING_ITEM) {
|
onPress={onPressRetryLoadMore}
|
||||||
return <ProfileCardFeedLoadingPlaceholder />
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
case 'loading': {
|
||||||
|
return <ProfileCardFeedLoadingPlaceholder />
|
||||||
|
}
|
||||||
|
case 'list_item': {
|
||||||
|
const profile = item.listItem.subject
|
||||||
|
if (!moderationOpts) return null
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View
|
||||||
|
style={[
|
||||||
|
a.py_md,
|
||||||
|
a.px_xl,
|
||||||
|
a.border_t,
|
||||||
|
t.atoms.border_contrast_low,
|
||||||
|
]}>
|
||||||
|
<ProfileCard.Link profile={profile}>
|
||||||
|
<ProfileCard.Outer>
|
||||||
|
<ProfileCard.Header>
|
||||||
|
<ProfileCard.Avatar
|
||||||
|
profile={profile}
|
||||||
|
moderationOpts={moderationOpts}
|
||||||
|
/>
|
||||||
|
<ProfileCard.NameAndHandle
|
||||||
|
profile={profile}
|
||||||
|
moderationOpts={moderationOpts}
|
||||||
|
/>
|
||||||
|
{isOwner && (
|
||||||
|
<Button
|
||||||
|
testID={`user-${profile.handle}-editBtn`}
|
||||||
|
label={_(msg({message: 'Edit', context: 'action'}))}
|
||||||
|
onPress={e => onPressEditMembership(e, profile)}
|
||||||
|
size="small"
|
||||||
|
color="secondary">
|
||||||
|
<ButtonText>
|
||||||
|
<Trans context="action">Edit</Trans>
|
||||||
|
</ButtonText>
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</ProfileCard.Header>
|
||||||
|
|
||||||
|
<ProfileCard.Labels
|
||||||
|
profile={profile}
|
||||||
|
moderationOpts={moderationOpts}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<ProfileCard.Description profile={profile} />
|
||||||
|
</ProfileCard.Outer>
|
||||||
|
</ProfileCard.Link>
|
||||||
|
</View>
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const profile = (item as AppBskyGraphDefs.ListItemView).subject
|
|
||||||
if (!moderationOpts) return null
|
|
||||||
|
|
||||||
return (
|
|
||||||
<View
|
|
||||||
style={[a.py_md, a.px_xl, a.border_t, t.atoms.border_contrast_low]}>
|
|
||||||
<ProfileCard.Link profile={profile}>
|
|
||||||
<ProfileCard.Outer>
|
|
||||||
<ProfileCard.Header>
|
|
||||||
<ProfileCard.Avatar
|
|
||||||
profile={profile}
|
|
||||||
moderationOpts={moderationOpts}
|
|
||||||
/>
|
|
||||||
<ProfileCard.NameAndHandle
|
|
||||||
profile={profile}
|
|
||||||
moderationOpts={moderationOpts}
|
|
||||||
/>
|
|
||||||
{isOwner && (
|
|
||||||
<Button
|
|
||||||
testID={`user-${profile.handle}-editBtn`}
|
|
||||||
label={_(msg({message: 'Edit', context: 'action'}))}
|
|
||||||
onPress={e => onPressEditMembership(e, profile)}
|
|
||||||
size="small"
|
|
||||||
variant="solid"
|
|
||||||
color="secondary">
|
|
||||||
<ButtonText>
|
|
||||||
<Trans context="action">Edit</Trans>
|
|
||||||
</ButtonText>
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
</ProfileCard.Header>
|
|
||||||
|
|
||||||
<ProfileCard.Labels
|
|
||||||
profile={profile}
|
|
||||||
moderationOpts={moderationOpts}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<ProfileCard.Description profile={profile} />
|
|
||||||
</ProfileCard.Outer>
|
|
||||||
</ProfileCard.Link>
|
|
||||||
</View>
|
|
||||||
)
|
|
||||||
},
|
},
|
||||||
[
|
[
|
||||||
renderEmptyState,
|
renderEmptyState,
|
||||||
@@ -247,22 +275,29 @@ export function ListMembers({
|
|||||||
testID={testID ? `${testID}-flatlist` : undefined}
|
testID={testID ? `${testID}-flatlist` : undefined}
|
||||||
ref={scrollElRef}
|
ref={scrollElRef}
|
||||||
data={items}
|
data={items}
|
||||||
keyExtractor={(item: any) => item.subject?.did || item._reactKey}
|
keyExtractor={(item: Item) =>
|
||||||
|
item.kind === 'list_item' ? item.listItem.subject.did : item._reactKey
|
||||||
|
}
|
||||||
renderItem={renderItem}
|
renderItem={renderItem}
|
||||||
ListHeaderComponent={!isEmpty ? renderHeader : undefined}
|
ListHeaderComponent={!isEmpty ? renderHeader : undefined}
|
||||||
ListFooterComponent={renderFooter}
|
ListFooterComponent={renderFooter}
|
||||||
refreshing={isRefreshing}
|
refreshing={isRefreshing}
|
||||||
onRefresh={onRefresh}
|
onRefresh={() => void onRefresh()}
|
||||||
headerOffset={headerOffset}
|
headerOffset={headerOffset}
|
||||||
contentContainerStyle={{
|
contentContainerStyle={{
|
||||||
minHeight: Dimensions.get('window').height * 1.5,
|
minHeight: Dimensions.get('window').height * 1.5,
|
||||||
}}
|
}}
|
||||||
onScrolledDownChange={onScrolledDownChange}
|
onScrolledDownChange={onScrolledDownChange}
|
||||||
onEndReached={onEndReached}
|
onEndReached={() => void onEndReached()}
|
||||||
onEndReachedThreshold={0.6}
|
onEndReachedThreshold={0.6}
|
||||||
removeClippedSubviews={true}
|
removeClippedSubviews={true}
|
||||||
desktopFixedHeight={desktopFixedHeightOffset || true}
|
desktopFixedHeight={desktopFixedHeightOffset || true}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<UserAddRemoveListsDialog
|
||||||
|
control={editListsDialogControl}
|
||||||
|
profile={selectedProfile}
|
||||||
|
/>
|
||||||
</View>
|
</View>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ import {usePalette} from '#/lib/hooks/usePalette'
|
|||||||
import {useModalControls, useModals} from '#/state/modals'
|
import {useModalControls, useModals} from '#/state/modals'
|
||||||
import {FullWindowOverlay} from '#/components/FullWindowOverlay'
|
import {FullWindowOverlay} from '#/components/FullWindowOverlay'
|
||||||
import {createCustomBackdrop} from '../util/BottomSheetCustomBackdrop'
|
import {createCustomBackdrop} from '../util/BottomSheetCustomBackdrop'
|
||||||
import * as UserAddRemoveListsModal from './UserAddRemoveLists'
|
|
||||||
|
|
||||||
const DEFAULT_SNAPPOINTS = ['90%']
|
const DEFAULT_SNAPPOINTS = ['90%']
|
||||||
const HANDLE_HEIGHT = 24
|
const HANDLE_HEIGHT = 24
|
||||||
@@ -40,10 +39,7 @@ export function ModalsContainer() {
|
|||||||
|
|
||||||
let snapPoints: (string | number)[] = DEFAULT_SNAPPOINTS
|
let snapPoints: (string | number)[] = DEFAULT_SNAPPOINTS
|
||||||
let element
|
let element
|
||||||
if (activeModal?.name === 'user-add-remove-lists') {
|
{
|
||||||
snapPoints = UserAddRemoveListsModal.snapPoints
|
|
||||||
element = <UserAddRemoveListsModal.Component {...activeModal} />
|
|
||||||
} else {
|
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ import {usePalette} from '#/lib/hooks/usePalette'
|
|||||||
import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries'
|
import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries'
|
||||||
import {type Modal as ModalIface} from '#/state/modals'
|
import {type Modal as ModalIface} from '#/state/modals'
|
||||||
import {useModalControls, useModals} from '#/state/modals'
|
import {useModalControls, useModals} from '#/state/modals'
|
||||||
import * as UserAddRemoveLists from './UserAddRemoveLists'
|
|
||||||
|
|
||||||
export function ModalsContainer() {
|
export function ModalsContainer() {
|
||||||
const {isModalActive, activeModals} = useModals()
|
const {isModalActive, activeModals} = useModals()
|
||||||
@@ -25,7 +24,7 @@ export function ModalsContainer() {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function Modal({modal}: {modal: ModalIface}) {
|
function Modal({modal: _modal}: {modal: ModalIface}) {
|
||||||
const {isModalActive} = useModals()
|
const {isModalActive} = useModals()
|
||||||
const {closeModal} = useModalControls()
|
const {closeModal} = useModalControls()
|
||||||
const pal = usePalette('default')
|
const pal = usePalette('default')
|
||||||
@@ -44,9 +43,7 @@ function Modal({modal}: {modal: ModalIface}) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let element
|
let element
|
||||||
if (modal.name === 'user-add-remove-lists') {
|
{
|
||||||
element = <UserAddRemoveLists.Component {...modal} />
|
|
||||||
} else {
|
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,299 +0,0 @@
|
|||||||
import {useCallback, useMemo, useState} from 'react'
|
|
||||||
import {
|
|
||||||
ActivityIndicator,
|
|
||||||
StyleSheet,
|
|
||||||
useWindowDimensions,
|
|
||||||
View,
|
|
||||||
} from 'react-native'
|
|
||||||
import {type AppBskyGraphDefs as GraphDefs} from '@atproto/api'
|
|
||||||
import {msg} from '@lingui/core/macro'
|
|
||||||
import {useLingui} from '@lingui/react'
|
|
||||||
import {Trans} from '@lingui/react/macro'
|
|
||||||
|
|
||||||
import {usePalette} from '#/lib/hooks/usePalette'
|
|
||||||
import {sanitizeDisplayName} from '#/lib/strings/display-names'
|
|
||||||
import {cleanError} from '#/lib/strings/errors'
|
|
||||||
import {sanitizeHandle} from '#/lib/strings/handles'
|
|
||||||
import {s} from '#/lib/styles'
|
|
||||||
import {useModalControls} from '#/state/modals'
|
|
||||||
import {
|
|
||||||
getMembership,
|
|
||||||
type ListMembersip,
|
|
||||||
useDangerousListMembershipsQuery,
|
|
||||||
useListMembershipAddMutation,
|
|
||||||
useListMembershipRemoveMutation,
|
|
||||||
} from '#/state/queries/list-memberships'
|
|
||||||
import {useSession} from '#/state/session'
|
|
||||||
import {IS_ANDROID, IS_WEB, IS_WEB_MOBILE} from '#/env'
|
|
||||||
import {MyLists} from '../lists/MyLists'
|
|
||||||
import {Button} from '../util/forms/Button'
|
|
||||||
import {Text} from '../util/text/Text'
|
|
||||||
import * as Toast from '../util/Toast'
|
|
||||||
import {UserAvatar} from '../util/UserAvatar'
|
|
||||||
|
|
||||||
export const snapPoints = ['fullscreen']
|
|
||||||
|
|
||||||
export function Component({
|
|
||||||
subject,
|
|
||||||
handle,
|
|
||||||
displayName,
|
|
||||||
onAdd,
|
|
||||||
onRemove,
|
|
||||||
}: {
|
|
||||||
subject: string
|
|
||||||
handle: string
|
|
||||||
displayName: string
|
|
||||||
onAdd?: (listUri: string) => void
|
|
||||||
onRemove?: (listUri: string) => void
|
|
||||||
}) {
|
|
||||||
const {closeModal} = useModalControls()
|
|
||||||
const pal = usePalette('default')
|
|
||||||
const {height: screenHeight} = useWindowDimensions()
|
|
||||||
const {_} = useLingui()
|
|
||||||
const {data: memberships} = useDangerousListMembershipsQuery()
|
|
||||||
|
|
||||||
const onPressDone = useCallback(() => {
|
|
||||||
closeModal()
|
|
||||||
}, [closeModal])
|
|
||||||
|
|
||||||
const listStyle = useMemo(() => {
|
|
||||||
if (IS_WEB_MOBILE) {
|
|
||||||
return [pal.border, {height: screenHeight / 2}]
|
|
||||||
} else if (IS_WEB) {
|
|
||||||
return [pal.border, {height: screenHeight / 1.5}]
|
|
||||||
}
|
|
||||||
|
|
||||||
return [pal.border, {flex: 1, borderTopWidth: StyleSheet.hairlineWidth}]
|
|
||||||
}, [pal.border, screenHeight])
|
|
||||||
|
|
||||||
const headerStyles = [
|
|
||||||
{
|
|
||||||
textAlign: 'center',
|
|
||||||
fontWeight: '600',
|
|
||||||
fontSize: 20,
|
|
||||||
marginBottom: 12,
|
|
||||||
paddingHorizontal: 12,
|
|
||||||
} as const,
|
|
||||||
pal.text,
|
|
||||||
]
|
|
||||||
|
|
||||||
return (
|
|
||||||
<View testID="userAddRemoveListsModal" style={s.hContentRegion}>
|
|
||||||
<Text style={headerStyles} numberOfLines={1}>
|
|
||||||
<Trans>
|
|
||||||
Update{' '}
|
|
||||||
<Text style={headerStyles} numberOfLines={1}>
|
|
||||||
{displayName}
|
|
||||||
</Text>{' '}
|
|
||||||
in Lists
|
|
||||||
</Trans>
|
|
||||||
</Text>
|
|
||||||
<MyLists
|
|
||||||
filter="all"
|
|
||||||
inline
|
|
||||||
renderItem={(list, index) => (
|
|
||||||
<ListItem
|
|
||||||
key={list.uri}
|
|
||||||
index={index}
|
|
||||||
list={list}
|
|
||||||
memberships={memberships}
|
|
||||||
subject={subject}
|
|
||||||
handle={handle}
|
|
||||||
onAdd={onAdd}
|
|
||||||
onRemove={onRemove}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
style={listStyle}
|
|
||||||
/>
|
|
||||||
<View style={[styles.btns, pal.border]}>
|
|
||||||
<Button
|
|
||||||
testID="doneBtn"
|
|
||||||
type="default"
|
|
||||||
onPress={onPressDone}
|
|
||||||
style={styles.footerBtn}
|
|
||||||
accessibilityLabel={_(msg({message: `Done`, context: 'action'}))}
|
|
||||||
accessibilityHint=""
|
|
||||||
onAccessibilityEscape={onPressDone}
|
|
||||||
label={_(msg({message: `Done`, context: 'action'}))}
|
|
||||||
/>
|
|
||||||
</View>
|
|
||||||
</View>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function ListItem({
|
|
||||||
index,
|
|
||||||
list,
|
|
||||||
memberships,
|
|
||||||
subject,
|
|
||||||
handle,
|
|
||||||
onAdd,
|
|
||||||
onRemove,
|
|
||||||
}: {
|
|
||||||
index: number
|
|
||||||
list: GraphDefs.ListView
|
|
||||||
memberships: ListMembersip[] | undefined
|
|
||||||
subject: string
|
|
||||||
handle: string
|
|
||||||
onAdd?: (listUri: string) => void
|
|
||||||
onRemove?: (listUri: string) => void
|
|
||||||
}) {
|
|
||||||
const pal = usePalette('default')
|
|
||||||
const {_} = useLingui()
|
|
||||||
const {currentAccount} = useSession()
|
|
||||||
const [isProcessing, setIsProcessing] = useState(false)
|
|
||||||
const membership = useMemo(
|
|
||||||
() => getMembership(memberships, list.uri, subject),
|
|
||||||
[memberships, list.uri, subject],
|
|
||||||
)
|
|
||||||
const listMembershipAddMutation = useListMembershipAddMutation()
|
|
||||||
const listMembershipRemoveMutation = useListMembershipRemoveMutation()
|
|
||||||
|
|
||||||
const onToggleMembership = useCallback(async () => {
|
|
||||||
if (typeof membership === 'undefined') {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
setIsProcessing(true)
|
|
||||||
try {
|
|
||||||
if (membership === false) {
|
|
||||||
await listMembershipAddMutation.mutateAsync({
|
|
||||||
listUri: list.uri,
|
|
||||||
actorDid: subject,
|
|
||||||
})
|
|
||||||
Toast.show(_(msg`Added to list`))
|
|
||||||
onAdd?.(list.uri)
|
|
||||||
} else {
|
|
||||||
await listMembershipRemoveMutation.mutateAsync({
|
|
||||||
listUri: list.uri,
|
|
||||||
actorDid: subject,
|
|
||||||
membershipUri: membership,
|
|
||||||
})
|
|
||||||
Toast.show(_(msg`Removed from list`))
|
|
||||||
onRemove?.(list.uri)
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
Toast.show(cleanError(e), 'xmark')
|
|
||||||
} finally {
|
|
||||||
setIsProcessing(false)
|
|
||||||
}
|
|
||||||
}, [
|
|
||||||
_,
|
|
||||||
list,
|
|
||||||
subject,
|
|
||||||
membership,
|
|
||||||
setIsProcessing,
|
|
||||||
onAdd,
|
|
||||||
onRemove,
|
|
||||||
listMembershipAddMutation,
|
|
||||||
listMembershipRemoveMutation,
|
|
||||||
])
|
|
||||||
|
|
||||||
return (
|
|
||||||
<View
|
|
||||||
testID={`toggleBtn-${list.name}`}
|
|
||||||
style={[
|
|
||||||
styles.listItem,
|
|
||||||
pal.border,
|
|
||||||
index !== 0 && {borderTopWidth: StyleSheet.hairlineWidth},
|
|
||||||
]}>
|
|
||||||
<View style={styles.listItemAvi}>
|
|
||||||
<UserAvatar size={40} avatar={list.avatar} type="list" />
|
|
||||||
</View>
|
|
||||||
<View style={styles.listItemContent}>
|
|
||||||
<Text
|
|
||||||
type="lg"
|
|
||||||
style={[{fontWeight: '600'}, pal.text]}
|
|
||||||
numberOfLines={1}
|
|
||||||
lineHeight={1.2}>
|
|
||||||
{sanitizeDisplayName(list.name)}
|
|
||||||
</Text>
|
|
||||||
<Text type="md" style={[pal.textLight]} numberOfLines={1}>
|
|
||||||
{list.purpose === 'app.bsky.graph.defs#curatelist' &&
|
|
||||||
(list.creator.did === currentAccount?.did ? (
|
|
||||||
<Trans>User list by you</Trans>
|
|
||||||
) : (
|
|
||||||
<Trans>
|
|
||||||
User list by {sanitizeHandle(list.creator.handle, '@')}
|
|
||||||
</Trans>
|
|
||||||
))}
|
|
||||||
{list.purpose === 'app.bsky.graph.defs#modlist' &&
|
|
||||||
(list.creator.did === currentAccount?.did ? (
|
|
||||||
<Trans>Moderation list by you</Trans>
|
|
||||||
) : (
|
|
||||||
<Trans>
|
|
||||||
Moderation list by {sanitizeHandle(list.creator.handle, '@')}
|
|
||||||
</Trans>
|
|
||||||
))}
|
|
||||||
</Text>
|
|
||||||
</View>
|
|
||||||
<View>
|
|
||||||
{isProcessing || typeof membership === 'undefined' ? (
|
|
||||||
<ActivityIndicator />
|
|
||||||
) : (
|
|
||||||
<Button
|
|
||||||
testID={`user-${handle}-addBtn`}
|
|
||||||
type="default"
|
|
||||||
label={membership === false ? _(msg`Add`) : _(msg`Remove`)}
|
|
||||||
onPress={onToggleMembership}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</View>
|
|
||||||
</View>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
|
||||||
container: {
|
|
||||||
paddingHorizontal: IS_WEB ? 0 : 16,
|
|
||||||
},
|
|
||||||
btns: {
|
|
||||||
position: 'relative',
|
|
||||||
flexDirection: 'row',
|
|
||||||
alignItems: 'center',
|
|
||||||
justifyContent: 'center',
|
|
||||||
gap: 10,
|
|
||||||
paddingTop: 10,
|
|
||||||
paddingBottom: IS_ANDROID ? 10 : 0,
|
|
||||||
borderTopWidth: StyleSheet.hairlineWidth,
|
|
||||||
},
|
|
||||||
footerBtn: {
|
|
||||||
paddingHorizontal: 24,
|
|
||||||
paddingVertical: 12,
|
|
||||||
},
|
|
||||||
|
|
||||||
listItem: {
|
|
||||||
flexDirection: 'row',
|
|
||||||
alignItems: 'center',
|
|
||||||
paddingHorizontal: 14,
|
|
||||||
paddingVertical: 10,
|
|
||||||
},
|
|
||||||
listItemAvi: {
|
|
||||||
width: 54,
|
|
||||||
paddingLeft: 4,
|
|
||||||
paddingTop: 8,
|
|
||||||
paddingBottom: 10,
|
|
||||||
},
|
|
||||||
listItemContent: {
|
|
||||||
flex: 1,
|
|
||||||
paddingRight: 10,
|
|
||||||
paddingTop: 10,
|
|
||||||
paddingBottom: 10,
|
|
||||||
},
|
|
||||||
checkbox: {
|
|
||||||
flexDirection: 'row',
|
|
||||||
alignItems: 'center',
|
|
||||||
justifyContent: 'center',
|
|
||||||
borderWidth: 1,
|
|
||||||
width: 24,
|
|
||||||
height: 24,
|
|
||||||
borderRadius: 6,
|
|
||||||
marginRight: 8,
|
|
||||||
},
|
|
||||||
loadingContainer: {
|
|
||||||
position: 'absolute',
|
|
||||||
top: 10,
|
|
||||||
right: 0,
|
|
||||||
bottom: 0,
|
|
||||||
justifyContent: 'center',
|
|
||||||
},
|
|
||||||
})
|
|
||||||
@@ -9,7 +9,6 @@ import {type NavigationProp} from '#/lib/routes/types'
|
|||||||
import {shareText, shareUrl} from '#/lib/sharing'
|
import {shareText, shareUrl} from '#/lib/sharing'
|
||||||
import {toShareUrl} from '#/lib/strings/url-helpers'
|
import {toShareUrl} from '#/lib/strings/url-helpers'
|
||||||
import {type Shadow} from '#/state/cache/types'
|
import {type Shadow} from '#/state/cache/types'
|
||||||
import {useModalControls} from '#/state/modals'
|
|
||||||
import {Nux, useNux, useSaveNux} from '#/state/queries/nuxs'
|
import {Nux, useNux, useSaveNux} from '#/state/queries/nuxs'
|
||||||
import {
|
import {
|
||||||
RQKEY as profileQueryKey,
|
RQKEY as profileQueryKey,
|
||||||
@@ -22,6 +21,7 @@ import {EventStopper} from '#/view/com/util/EventStopper'
|
|||||||
import {atoms as a, useTheme} from '#/alf'
|
import {atoms as a, useTheme} from '#/alf'
|
||||||
import {Button, ButtonIcon} from '#/components/Button'
|
import {Button, ButtonIcon} from '#/components/Button'
|
||||||
import {useDialogControl} from '#/components/Dialog'
|
import {useDialogControl} from '#/components/Dialog'
|
||||||
|
import {UserAddRemoveListsDialog} from '#/components/dialogs/lists/UserAddRemoveListsDialog'
|
||||||
import {StarterPackDialog} from '#/components/dialogs/StarterPackDialog'
|
import {StarterPackDialog} from '#/components/dialogs/StarterPackDialog'
|
||||||
import {ArrowOutOfBoxModified_Stroke2_Corner2_Rounded as ArrowOutOfBoxIcon} from '#/components/icons/ArrowOutOfBox'
|
import {ArrowOutOfBoxModified_Stroke2_Corner2_Rounded as ArrowOutOfBoxIcon} from '#/components/icons/ArrowOutOfBox'
|
||||||
import {ChainLink_Stroke2_Corner0_Rounded as ChainLinkIcon} from '#/components/icons/ChainLink'
|
import {ChainLink_Stroke2_Corner0_Rounded as ChainLinkIcon} from '#/components/icons/ChainLink'
|
||||||
@@ -72,7 +72,6 @@ let ProfileMenu = ({
|
|||||||
const ax = useAnalytics()
|
const ax = useAnalytics()
|
||||||
const {t: l} = useLingui()
|
const {t: l} = useLingui()
|
||||||
const {currentAccount, hasSession} = useSession()
|
const {currentAccount, hasSession} = useSession()
|
||||||
const {openModal} = useModalControls()
|
|
||||||
const reportDialogControl = useReportDialogControl()
|
const reportDialogControl = useReportDialogControl()
|
||||||
const queryClient = useQueryClient()
|
const queryClient = useQueryClient()
|
||||||
const navigation = useNavigation<NavigationProp>()
|
const navigation = useNavigation<NavigationProp>()
|
||||||
@@ -105,6 +104,7 @@ let ProfileMenu = ({
|
|||||||
const goLiveDialogControl = useDialogControl()
|
const goLiveDialogControl = useDialogControl()
|
||||||
const goLiveDisabledDialogControl = useDialogControl()
|
const goLiveDisabledDialogControl = useDialogControl()
|
||||||
const addToStarterPacksDialogControl = useDialogControl()
|
const addToStarterPacksDialogControl = useDialogControl()
|
||||||
|
const addToListsDialogControl = useDialogControl()
|
||||||
|
|
||||||
const showLoggedOutWarning = useMemo(() => {
|
const showLoggedOutWarning = useMemo(() => {
|
||||||
return (
|
return (
|
||||||
@@ -129,15 +129,8 @@ let ProfileMenu = ({
|
|||||||
}, [profile])
|
}, [profile])
|
||||||
|
|
||||||
const onPressAddRemoveLists = useCallback(() => {
|
const onPressAddRemoveLists = useCallback(() => {
|
||||||
openModal({
|
addToListsDialogControl.open()
|
||||||
name: 'user-add-remove-lists',
|
}, [addToListsDialogControl])
|
||||||
subject: profile.did,
|
|
||||||
handle: profile.handle,
|
|
||||||
displayName: profile.displayName || profile.handle,
|
|
||||||
onAdd: invalidateProfileQuery,
|
|
||||||
onRemove: invalidateProfileQuery,
|
|
||||||
})
|
|
||||||
}, [profile, openModal, invalidateProfileQuery])
|
|
||||||
|
|
||||||
const onPressMuteAccount = useCallback(async () => {
|
const onPressMuteAccount = useCallback(async () => {
|
||||||
if (profile.viewer?.muted) {
|
if (profile.viewer?.muted) {
|
||||||
@@ -530,6 +523,12 @@ let ProfileMenu = ({
|
|||||||
control={addToStarterPacksDialogControl}
|
control={addToStarterPacksDialogControl}
|
||||||
targetDid={profile.did}
|
targetDid={profile.did}
|
||||||
/>
|
/>
|
||||||
|
<UserAddRemoveListsDialog
|
||||||
|
control={addToListsDialogControl}
|
||||||
|
profile={profile}
|
||||||
|
onAdd={invalidateProfileQuery}
|
||||||
|
onRemove={invalidateProfileQuery}
|
||||||
|
/>
|
||||||
<ReportDialog
|
<ReportDialog
|
||||||
control={reportDialogControl}
|
control={reportDialogControl}
|
||||||
subject={{
|
subject={{
|
||||||
|
|||||||
Reference in New Issue
Block a user