diff --git a/eslint-suppressions.json b/eslint-suppressions.json
index f2e677dc79..7f710cd4d2 100644
--- a/eslint-suppressions.json
+++ b/eslint-suppressions.json
@@ -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": {
diff --git a/src/components/dialogs/lists/UserAddRemoveListsDialog.tsx b/src/components/dialogs/lists/UserAddRemoveListsDialog.tsx
index d22ff1211e..4c0c550649 100644
--- a/src/components/dialogs/lists/UserAddRemoveListsDialog.tsx
+++ b/src/components/dialogs/lists/UserAddRemoveListsDialog.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={}
@@ -222,7 +221,7 @@ function ListItem({
did: subjectDid,
handle,
displayName,
- } as AppBskyActorDefs.ProfileView,
+ },
})
},
onError: err => {
diff --git a/src/view/com/lists/ListMembers.tsx b/src/view/com/lists/ListMembers.tsx
index 886d105dc1..d91b39bf5e 100644
--- a/src/view/com/lists/ListMembers.tsx
+++ b/src/view/com/lists/ListMembers.tsx
@@ -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 (
-
- )
- } else if (item === LOAD_MORE_ERROR_ITEM) {
- return (
-
- )
- } else if (item === LOADING_ITEM) {
- return
+ ({item}: {item: Item}) => {
+ switch (item.kind) {
+ case 'empty': {
+ return renderEmptyState()
+ }
+ case 'error': {
+ return (
+
+ )
+ }
+ case 'load_more_error': {
+ return (
+
+ )
+ }
+ case 'loading': {
+ return
+ }
+ case 'list_item': {
+ const profile = item.listItem.subject
+ if (!moderationOpts) return null
+
+ return (
+
+
+
+
+
+
+ {isOwner && (
+
+ )}
+
+
+
+
+
+
+
+
+ )
+ }
}
-
- const profile = (item as AppBskyGraphDefs.ListItemView).subject
- if (!moderationOpts) return null
-
- return (
-
-
-
-
-
-
- {isOwner && (
-
- )}
-
-
-
-
-
-
-
-
- )
},
[
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}
diff --git a/src/view/com/modals/UserAddRemoveLists.tsx b/src/view/com/modals/UserAddRemoveLists.tsx
deleted file mode 100644
index 1648b0ff22..0000000000
--- a/src/view/com/modals/UserAddRemoveLists.tsx
+++ /dev/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 (
-
-
-
- Update{' '}
-
- {displayName}
- {' '}
- in Lists
-
-
- (
-
- )}
- style={listStyle}
- />
-
-
-
-
- )
-}
-
-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 (
-
-
-
-
-
-
- {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, '@')}
-
- ))}
-
-
-
- {isProcessing || typeof membership === 'undefined' ? (
-
- ) : (
-
- )}
-
-
- )
-}
-
-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',
- },
-})