diff options
author | Samuel Newman <mozzius@protonmail.com> | 2025-04-17 19:11:46 +0300 |
---|---|---|
committer | GitHub <noreply@github.com> | 2025-04-17 11:11:46 -0500 |
commit | 719d7b7a57c96663292d886adb6f19e283e309e0 (patch) | |
tree | 5fe3ddb22ae7ac7ff7f962c7269a59eaf08dc172 /src/view | |
parent | 4f316538fb16cd86252569f5ededb34e759a4659 (diff) | |
download | voidsky-719d7b7a57c96663292d886adb6f19e283e309e0.tar.zst |
Use `SearchablePeopleList` for add user to list dialog, replace old modal (#8212)
* move to dialogs dir * make searchable people list more generic * new list-add-remove-users dialog * update header text * fix header on android * delete old modal * reduce spacing on items
Diffstat (limited to 'src/view')
-rw-r--r-- | src/view/com/modals/ListAddRemoveUsers.tsx | 316 | ||||
-rw-r--r-- | src/view/com/modals/Modal.tsx | 4 | ||||
-rw-r--r-- | src/view/com/modals/Modal.web.tsx | 5 | ||||
-rw-r--r-- | src/view/screens/ProfileList.tsx | 52 |
4 files changed, 31 insertions, 346 deletions
diff --git a/src/view/com/modals/ListAddRemoveUsers.tsx b/src/view/com/modals/ListAddRemoveUsers.tsx deleted file mode 100644 index 5285d4a15..000000000 --- a/src/view/com/modals/ListAddRemoveUsers.tsx +++ /dev/null @@ -1,316 +0,0 @@ -import React, {useCallback, useState} from 'react' -import { - ActivityIndicator, - Pressable, - SafeAreaView, - StyleSheet, - View, -} from 'react-native' -import {AppBskyActorDefs, AppBskyGraphDefs} from '@atproto/api' -import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' -import {msg, Trans} from '@lingui/macro' -import {useLingui} from '@lingui/react' - -import {HITSLOP_20} from '#/lib/constants' -import {useIsKeyboardVisible} from '#/lib/hooks/useIsKeyboardVisible' -import {usePalette} from '#/lib/hooks/usePalette' -import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries' -import {sanitizeDisplayName} from '#/lib/strings/display-names' -import {cleanError} from '#/lib/strings/errors' -import {sanitizeHandle} from '#/lib/strings/handles' -import {colors, s} from '#/lib/styles' -import {isWeb} from '#/platform/detection' -import {useModalControls} from '#/state/modals' -import {useActorAutocompleteQuery} from '#/state/queries/actor-autocomplete' -import { - getMembership, - ListMembersip, - useDangerousListMembershipsQuery, - useListMembershipAddMutation, - useListMembershipRemoveMutation, -} from '#/state/queries/list-memberships' -import {Button} from '../util/forms/Button' -import {Text} from '../util/text/Text' -import * as Toast from '../util/Toast' -import {UserAvatar} from '../util/UserAvatar' -import {ScrollView, TextInput} from './util' - -export const snapPoints = ['90%'] - -export function Component({ - list, - onChange, -}: { - list: AppBskyGraphDefs.ListView - onChange?: ( - type: 'add' | 'remove', - profile: AppBskyActorDefs.ProfileViewBasic, - ) => void -}) { - const pal = usePalette('default') - const {_} = useLingui() - const {closeModal} = useModalControls() - const {isMobile} = useWebMediaQueries() - const [query, setQuery] = useState('') - const autocomplete = useActorAutocompleteQuery(query) - const {data: memberships} = useDangerousListMembershipsQuery() - const [isKeyboardVisible] = useIsKeyboardVisible() - - const onPressCancelSearch = useCallback(() => setQuery(''), [setQuery]) - - return ( - <SafeAreaView - testID="listAddUserModal" - style={[pal.view, isWeb ? styles.fixedHeight : s.flex1]}> - <View style={[s.flex1, isMobile && {paddingHorizontal: 18}]}> - <View style={[styles.searchContainer, pal.border]}> - <FontAwesomeIcon icon="search" size={16} /> - <TextInput - testID="searchInput" - style={[styles.searchInput, pal.border, pal.text]} - placeholder={_(msg`Search for users`)} - placeholderTextColor={pal.colors.textLight} - value={query} - onChangeText={setQuery} - accessible={true} - accessibilityLabel={_(msg`Search`)} - accessibilityHint="" - autoFocus - autoCapitalize="none" - autoComplete="off" - autoCorrect={false} - selectTextOnFocus - /> - {query ? ( - <Pressable - onPress={onPressCancelSearch} - accessibilityRole="button" - accessibilityLabel={_(msg`Cancel search`)} - accessibilityHint={_(msg`Exits inputting search query`)} - onAccessibilityEscape={onPressCancelSearch} - hitSlop={HITSLOP_20}> - <FontAwesomeIcon - icon="xmark" - size={16} - color={pal.colors.textLight} - /> - </Pressable> - ) : undefined} - </View> - <ScrollView - style={[s.flex1]} - keyboardDismissMode="none" - keyboardShouldPersistTaps="always"> - {autocomplete.isLoading ? ( - <View style={{marginVertical: 20}}> - <ActivityIndicator /> - </View> - ) : autocomplete.data?.length ? ( - <> - {autocomplete.data.slice(0, 40).map((item, i) => ( - <UserResult - key={item.did} - list={list} - profile={item} - memberships={memberships} - noBorder={i === 0} - onChange={onChange} - /> - ))} - </> - ) : ( - <Text - type="xl" - style={[ - pal.textLight, - {paddingHorizontal: 12, paddingVertical: 16}, - ]}> - <Trans>No results found for {query}</Trans> - </Text> - )} - </ScrollView> - <View - style={[ - styles.btnContainer, - {paddingBottom: isKeyboardVisible ? 10 : 20}, - ]}> - <Button - testID="doneBtn" - type="default" - onPress={() => { - closeModal() - }} - accessibilityLabel={_(msg`Done`)} - accessibilityHint="" - label={_(msg({message: 'Done', context: 'action'}))} - labelContainerStyle={{justifyContent: 'center', padding: 4}} - labelStyle={[s.f18]} - /> - </View> - </View> - </SafeAreaView> - ) -} - -function UserResult({ - profile, - list, - memberships, - noBorder, - onChange, -}: { - profile: AppBskyActorDefs.ProfileViewBasic - list: AppBskyGraphDefs.ListView - memberships: ListMembersip[] | undefined - noBorder: boolean - onChange?: ( - type: 'add' | 'remove', - profile: AppBskyActorDefs.ProfileViewBasic, - ) => void | undefined -}) { - const pal = usePalette('default') - const {_} = useLingui() - const [isProcessing, setIsProcessing] = useState(false) - const membership = React.useMemo( - () => getMembership(memberships, list.uri, profile.did), - [memberships, list.uri, profile.did], - ) - 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: profile.did, - }) - Toast.show(_(msg`Added to list`)) - onChange?.('add', profile) - } else { - await listMembershipRemoveMutation.mutateAsync({ - listUri: list.uri, - actorDid: profile.did, - membershipUri: membership, - }) - Toast.show(_(msg`Removed from list`)) - onChange?.('remove', profile) - } - } catch (e) { - Toast.show(cleanError(e), 'xmark') - } finally { - setIsProcessing(false) - } - }, [ - _, - list, - profile, - membership, - setIsProcessing, - onChange, - listMembershipAddMutation, - listMembershipRemoveMutation, - ]) - - return ( - <View - style={[ - pal.border, - { - flexDirection: 'row', - alignItems: 'center', - borderTopWidth: noBorder ? 0 : 1, - paddingHorizontal: 8, - }, - ]}> - <View - style={{ - width: 54, - paddingLeft: 4, - }}> - <UserAvatar - size={40} - avatar={profile.avatar} - type={profile.associated?.labeler ? 'labeler' : 'user'} - /> - </View> - <View - style={{ - flex: 1, - paddingRight: 10, - paddingTop: 10, - paddingBottom: 10, - }}> - <Text - type="lg" - style={[s.bold, pal.text]} - numberOfLines={1} - lineHeight={1.2}> - {sanitizeDisplayName( - profile.displayName || sanitizeHandle(profile.handle), - )} - </Text> - <Text type="md" style={[pal.textLight]} numberOfLines={1}> - {sanitizeHandle(profile.handle, '@')} - </Text> - {!!profile.viewer?.followedBy && <View style={s.flexRow} />} - </View> - <View> - {isProcessing || typeof membership === 'undefined' ? ( - <ActivityIndicator /> - ) : ( - <Button - testID={`user-${profile.handle}-addBtn`} - type="default" - label={membership === false ? _(msg`Add`) : _(msg`Remove`)} - onPress={onToggleMembership} - /> - )} - </View> - </View> - ) -} - -const styles = StyleSheet.create({ - fixedHeight: { - // @ts-ignore web only -prf - height: '80vh', - }, - titleSection: { - paddingTop: isWeb ? 0 : 4, - paddingBottom: isWeb ? 14 : 10, - }, - title: { - textAlign: 'center', - fontWeight: '600', - marginBottom: 5, - }, - searchContainer: { - flexDirection: 'row', - alignItems: 'center', - gap: 8, - borderWidth: 1, - borderRadius: 24, - paddingHorizontal: 16, - paddingVertical: 10, - }, - searchInput: { - fontSize: 16, - flex: 1, - }, - btn: { - flexDirection: 'row', - alignItems: 'center', - justifyContent: 'center', - borderRadius: 32, - padding: 14, - backgroundColor: colors.blue3, - }, - btnContainer: { - paddingTop: 10, - }, -}) diff --git a/src/view/com/modals/Modal.tsx b/src/view/com/modals/Modal.tsx index 9ad651b4f..b4572172c 100644 --- a/src/view/com/modals/Modal.tsx +++ b/src/view/com/modals/Modal.tsx @@ -17,7 +17,6 @@ import * as InviteCodesModal from './InviteCodes' import * as ContentLanguagesSettingsModal from './lang-settings/ContentLanguagesSettings' import * as PostLanguagesSettingsModal from './lang-settings/PostLanguagesSettings' import * as LinkWarningModal from './LinkWarning' -import * as ListAddUserModal from './ListAddRemoveUsers' import * as UserAddRemoveListsModal from './UserAddRemoveLists' import * as VerifyEmailModal from './VerifyEmail' @@ -61,9 +60,6 @@ export function ModalsContainer() { } else if (activeModal?.name === 'user-add-remove-lists') { snapPoints = UserAddRemoveListsModal.snapPoints element = <UserAddRemoveListsModal.Component {...activeModal} /> - } else if (activeModal?.name === 'list-add-remove-users') { - snapPoints = ListAddUserModal.snapPoints - element = <ListAddUserModal.Component {...activeModal} /> } else if (activeModal?.name === 'delete-account') { snapPoints = DeleteAccountModal.snapPoints element = <DeleteAccountModal.Component /> diff --git a/src/view/com/modals/Modal.web.tsx b/src/view/com/modals/Modal.web.tsx index 0c49c8771..74ee7c210 100644 --- a/src/view/com/modals/Modal.web.tsx +++ b/src/view/com/modals/Modal.web.tsx @@ -4,7 +4,7 @@ import {RemoveScrollBar} from 'react-remove-scroll-bar' import {usePalette} from '#/lib/hooks/usePalette' 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 * as ChangeEmailModal from './ChangeEmail' import * as ChangePasswordModal from './ChangePassword' @@ -16,7 +16,6 @@ import * as InviteCodesModal from './InviteCodes' import * as ContentLanguagesSettingsModal from './lang-settings/ContentLanguagesSettings' import * as PostLanguagesSettingsModal from './lang-settings/PostLanguagesSettings' import * as LinkWarningModal from './LinkWarning' -import * as ListAddUserModal from './ListAddRemoveUsers' import * as UserAddRemoveLists from './UserAddRemoveLists' import * as VerifyEmailModal from './VerifyEmail' @@ -65,8 +64,6 @@ function Modal({modal}: {modal: ModalIface}) { element = <CreateOrEditListModal.Component {...modal} /> } else if (modal.name === 'user-add-remove-lists') { element = <UserAddRemoveLists.Component {...modal} /> - } else if (modal.name === 'list-add-remove-users') { - element = <ListAddUserModal.Component {...modal} /> } else if (modal.name === 'crop-image') { element = <CropImageModal.Component {...modal} /> } else if (modal.name === 'delete-account') { diff --git a/src/view/screens/ProfileList.tsx b/src/view/screens/ProfileList.tsx index 966534d97..61f1eb745 100644 --- a/src/view/screens/ProfileList.tsx +++ b/src/view/screens/ProfileList.tsx @@ -5,7 +5,7 @@ import { AppBskyGraphDefs, AtUri, moderateUserList, - ModerationOpts, + type ModerationOpts, RichText as RichTextAPI, } from '@atproto/api' import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' @@ -21,8 +21,11 @@ import {useSetTitle} from '#/lib/hooks/useSetTitle' import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries' import {ComposeIcon2} from '#/lib/icons' import {makeListLink} from '#/lib/routes/links' -import {CommonNavigatorParams, NativeStackScreenProps} from '#/lib/routes/types' -import {NavigationProp} from '#/lib/routes/types' +import { + type CommonNavigatorParams, + type NativeStackScreenProps, +} from '#/lib/routes/types' +import {type NavigationProp} from '#/lib/routes/types' import {shareUrl} from '#/lib/sharing' import {cleanError} from '#/lib/strings/errors' import {toShareUrl} from '#/lib/strings/url-helpers' @@ -38,12 +41,12 @@ import { useListMuteMutation, useListQuery, } from '#/state/queries/list' -import {FeedDescriptor} from '#/state/queries/post-feed' +import {type FeedDescriptor} from '#/state/queries/post-feed' import {RQKEY as FEED_RQKEY} from '#/state/queries/post-feed' import { useAddSavedFeedsMutation, usePreferencesQuery, - UsePreferencesQueryResponse, + type UsePreferencesQueryResponse, useRemoveFeedMutation, useUpdateSavedFeedsMutation, } from '#/state/queries/preferences' @@ -60,10 +63,10 @@ import {EmptyState} from '#/view/com/util/EmptyState' import {FAB} from '#/view/com/util/fab/FAB' import {Button} from '#/view/com/util/forms/Button' import { - DropdownItem, + type DropdownItem, NativeDropdown, } from '#/view/com/util/forms/NativeDropdown' -import {ListRef} from '#/view/com/util/List' +import {type ListRef} from '#/view/com/util/List' import {LoadLatestBtn} from '#/view/com/util/load-latest/LoadLatestBtn' import {LoadingScreen} from '#/view/com/util/LoadingScreen' import {Text} from '#/view/com/util/text/Text' @@ -72,6 +75,7 @@ import {ListHiddenScreen} from '#/screens/List/ListHiddenScreen' import {atoms as a} from '#/alf' import {Button as NewButton, ButtonIcon, ButtonText} from '#/components/Button' import {useDialogControl} from '#/components/Dialog' +import {ListAddRemoveUsersDialog} from '#/components/dialogs/lists/ListAddRemoveUsersDialog' import {PersonPlus_Stroke2_Corner0_Rounded as PersonPlusIcon} from '#/components/icons/Person' import * as Layout from '#/components/Layout' import * as Hider from '#/components/moderation/Hider' @@ -157,12 +161,12 @@ function ProfileListScreenLoaded({ const {rkey} = route.params const feedSectionRef = React.useRef<SectionRef>(null) const aboutSectionRef = React.useRef<SectionRef>(null) - const {openModal} = useModalControls() const isCurateList = list.purpose === AppBskyGraphDefs.CURATELIST const isScreenFocused = useIsFocused() const isHidden = list.labels?.findIndex(l => l.val === '!hide') !== -1 const isOwner = currentAccount?.did === list.creator.did const scrollElRef = useAnimatedRef() + const addUserDialogControl = useDialogControl() const sectionTitlesCurate = [_(msg`Posts`), _(msg`People`)] const moderation = React.useMemo(() => { @@ -177,17 +181,11 @@ function ProfileListScreenLoaded({ }, [setMinimalShellMode]), ) - const onPressAddUser = useCallback(() => { - openModal({ - name: 'list-add-remove-users', - list, - onChange() { - if (isCurateList) { - truncateAndInvalidate(queryClient, FEED_RQKEY(`list|${list.uri}`)) - } - }, - }) - }, [openModal, list, isCurateList, queryClient]) + const onChangeMembers = useCallback(() => { + if (isCurateList) { + truncateAndInvalidate(queryClient, FEED_RQKEY(`list|${list.uri}`)) + } + }, [list.uri, isCurateList, queryClient]) const onCurrentPageSelected = React.useCallback( (index: number) => { @@ -225,7 +223,7 @@ function ProfileListScreenLoaded({ headerHeight={headerHeight} isFocused={isScreenFocused && isFocused} isOwner={isOwner} - onPressAddUser={onPressAddUser} + onPressAddUser={addUserDialogControl.open} /> )} {({headerHeight, scrollElRef}) => ( @@ -233,7 +231,7 @@ function ProfileListScreenLoaded({ ref={aboutSectionRef} scrollElRef={scrollElRef as ListRef} list={list} - onPressAddUser={onPressAddUser} + onPressAddUser={addUserDialogControl.open} headerHeight={headerHeight} /> )} @@ -253,6 +251,11 @@ function ProfileListScreenLoaded({ accessibilityHint="" /> </View> + <ListAddRemoveUsersDialog + control={addUserDialogControl} + list={list} + onChange={onChangeMembers} + /> </Hider.Content> </Hider.Outer> ) @@ -268,7 +271,7 @@ function ProfileListScreenLoaded({ <AboutSection list={list} scrollElRef={scrollElRef as ListRef} - onPressAddUser={onPressAddUser} + onPressAddUser={addUserDialogControl.open} headerHeight={0} /> <FAB @@ -286,6 +289,11 @@ function ProfileListScreenLoaded({ accessibilityHint="" /> </View> + <ListAddRemoveUsersDialog + control={addUserDialogControl} + list={list} + onChange={onChangeMembers} + /> </Hider.Content> </Hider.Outer> ) |