From 9d40d02f9558c3edbc02b686b08e264fb6dfa130 Mon Sep 17 00:00:00 2001 From: Samuel Newman Date: Thu, 4 Jun 2026 10:16:02 +0300 Subject: [PATCH] strong types for listmembers --- eslint-suppressions.json | 17 - .../lists/UserAddRemoveListsDialog.tsx | 5 +- src/view/com/lists/ListMembers.tsx | 194 +++++++----- src/view/com/modals/UserAddRemoveLists.tsx | 299 ------------------ 4 files changed, 114 insertions(+), 401 deletions(-) delete mode 100644 src/view/com/modals/UserAddRemoveLists.tsx 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} - /> - -