strong types for listmembers
This commit is contained in:
@@ -2135,20 +2135,6 @@
|
||||
"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": {
|
||||
"@typescript-eslint/no-explicit-any": {
|
||||
"count": 2
|
||||
@@ -2180,9 +2166,6 @@
|
||||
},
|
||||
"@typescript-eslint/require-await": {
|
||||
"count": 1
|
||||
},
|
||||
"react-hooks/refs": {
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"src/view/com/notifications/NotificationFeed.tsx": {
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import {useCallback} from 'react'
|
||||
import {View} from 'react-native'
|
||||
import {type AppBskyActorDefs} from '@atproto/api'
|
||||
import {Trans, useLingui} from '@lingui/react/macro'
|
||||
import {useQueryClient} from '@tanstack/react-query'
|
||||
|
||||
@@ -172,7 +171,7 @@ function ListsContent({
|
||||
? () => 'lists_dialog_loader'
|
||||
: (item: ListWithMembership) => item.list.uri
|
||||
}
|
||||
onEndReached={onEndReached}
|
||||
onEndReached={() => void onEndReached()}
|
||||
onEndReachedThreshold={0.1}
|
||||
ListHeaderComponent={listHeader}
|
||||
ListEmptyComponent={<Empty />}
|
||||
@@ -222,7 +221,7 @@ function ListItem({
|
||||
did: subjectDid,
|
||||
handle,
|
||||
displayName,
|
||||
} as AppBskyActorDefs.ProfileView,
|
||||
},
|
||||
})
|
||||
},
|
||||
onError: err => {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import {type JSX, useCallback, useMemo, useState} from 'react'
|
||||
import {useCallback, useMemo, useState} from 'react'
|
||||
import {
|
||||
Dimensions,
|
||||
type GestureResponderEvent,
|
||||
@@ -28,10 +28,23 @@ import {ListFooter} from '#/components/Lists'
|
||||
import * as ProfileCard from '#/components/ProfileCard'
|
||||
import type * as bsky from '#/types/bsky'
|
||||
|
||||
const LOADING_ITEM = {_reactKey: '__loading__'}
|
||||
const EMPTY_ITEM = {_reactKey: '__empty__'}
|
||||
const ERROR_ITEM = {_reactKey: '__error__'}
|
||||
const LOAD_MORE_ERROR_ITEM = {_reactKey: '__load_more_error__'}
|
||||
const LOADING_ITEM = {kind: 'loading', _reactKey: '__loading__'} as const
|
||||
const EMPTY_ITEM = {kind: 'empty', _reactKey: '__empty__'} as const
|
||||
const ERROR_ITEM = {kind: 'error', _reactKey: '__error__'} as const
|
||||
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({
|
||||
list,
|
||||
@@ -50,8 +63,8 @@ export function ListMembers({
|
||||
scrollElRef?: ListRef
|
||||
onScrolledDownChange: (isScrolledDown: boolean) => void
|
||||
onPressTryAgain?: () => void
|
||||
renderHeader: () => JSX.Element
|
||||
renderEmptyState: () => JSX.Element
|
||||
renderHeader: () => React.ReactElement
|
||||
renderEmptyState: () => React.ReactElement
|
||||
testID?: string
|
||||
headerOffset?: number
|
||||
desktopFixedHeightOffset?: number
|
||||
@@ -62,7 +75,7 @@ export function ListMembers({
|
||||
const {currentAccount} = useSession()
|
||||
const moderationOpts = useModerationOpts()
|
||||
const editListsDialogControl = useDialogControl()
|
||||
const [selectedProfile, setSelectedProfile] = React.useState<
|
||||
const [selectedProfile, setSelectedProfile] = useState<
|
||||
bsky.profile.AnyProfileView | undefined
|
||||
>()
|
||||
|
||||
@@ -82,23 +95,28 @@ export function ListMembers({
|
||||
currentAccount && data?.pages[0].list.creator.did === currentAccount.did
|
||||
|
||||
const items = useMemo(() => {
|
||||
let items: any[] = []
|
||||
const items: Item[] = []
|
||||
if (isFetched) {
|
||||
if (isEmpty && isError) {
|
||||
items = items.concat([ERROR_ITEM])
|
||||
items.push(ERROR_ITEM)
|
||||
}
|
||||
if (isEmpty) {
|
||||
items = items.concat([EMPTY_ITEM])
|
||||
items.push(EMPTY_ITEM)
|
||||
} else if (data) {
|
||||
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) {
|
||||
items = items.concat([LOAD_MORE_ERROR_ITEM])
|
||||
items.push(LOAD_MORE_ERROR_ITEM)
|
||||
}
|
||||
} else if (isFetching) {
|
||||
items = items.concat([LOADING_ITEM])
|
||||
items.push(LOADING_ITEM)
|
||||
}
|
||||
return items
|
||||
}, [isFetched, isEmpty, isError, data, isFetching])
|
||||
@@ -126,7 +144,7 @@ export function ListMembers({
|
||||
}, [isFetching, hasNextPage, isError, fetchNextPage])
|
||||
|
||||
const onPressRetryLoadMore = useCallback(() => {
|
||||
fetchNextPage()
|
||||
void fetchNextPage()
|
||||
}, [fetchNextPage])
|
||||
|
||||
const onPressEditMembership = useCallback(
|
||||
@@ -142,71 +160,81 @@ export function ListMembers({
|
||||
// =
|
||||
|
||||
const renderItem = useCallback(
|
||||
({item}: {item: any}) => {
|
||||
if (item === EMPTY_ITEM) {
|
||||
return renderEmptyState()
|
||||
} else if (item === ERROR_ITEM) {
|
||||
return (
|
||||
<ErrorMessage
|
||||
message={cleanError(error)}
|
||||
onPressTryAgain={onPressTryAgain}
|
||||
/>
|
||||
)
|
||||
} else if (item === LOAD_MORE_ERROR_ITEM) {
|
||||
return (
|
||||
<LoadMoreRetryBtn
|
||||
label={_(
|
||||
msg`There was an issue fetching the list. Tap here to try again.`,
|
||||
)}
|
||||
onPress={onPressRetryLoadMore}
|
||||
/>
|
||||
)
|
||||
} else if (item === LOADING_ITEM) {
|
||||
return <ProfileCardFeedLoadingPlaceholder />
|
||||
({item}: {item: Item}) => {
|
||||
switch (item.kind) {
|
||||
case 'empty': {
|
||||
return renderEmptyState()
|
||||
}
|
||||
case 'error': {
|
||||
return (
|
||||
<ErrorMessage
|
||||
message={cleanError(error)}
|
||||
onPressTryAgain={onPressTryAgain}
|
||||
/>
|
||||
)
|
||||
}
|
||||
case 'load_more_error': {
|
||||
return (
|
||||
<LoadMoreRetryBtn
|
||||
label={_(
|
||||
msg`There was an issue fetching the list. Tap here to try again.`,
|
||||
)}
|
||||
onPress={onPressRetryLoadMore}
|
||||
/>
|
||||
)
|
||||
}
|
||||
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,
|
||||
@@ -247,18 +275,20 @@ export function ListMembers({
|
||||
testID={testID ? `${testID}-flatlist` : undefined}
|
||||
ref={scrollElRef}
|
||||
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}
|
||||
ListHeaderComponent={!isEmpty ? renderHeader : undefined}
|
||||
ListFooterComponent={renderFooter}
|
||||
refreshing={isRefreshing}
|
||||
onRefresh={onRefresh}
|
||||
onRefresh={() => void onRefresh()}
|
||||
headerOffset={headerOffset}
|
||||
contentContainerStyle={{
|
||||
minHeight: Dimensions.get('window').height * 1.5,
|
||||
}}
|
||||
onScrolledDownChange={onScrolledDownChange}
|
||||
onEndReached={onEndReached}
|
||||
onEndReached={() => void onEndReached()}
|
||||
onEndReachedThreshold={0.6}
|
||||
removeClippedSubviews={true}
|
||||
desktopFixedHeight={desktopFixedHeightOffset || true}
|
||||
|
||||
@@ -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',
|
||||
},
|
||||
})
|
||||
Reference in New Issue
Block a user