diff --git a/lib/shared/community-utils.js b/lib/shared/community-utils.js index 7f37396ee..85edffbd9 100644 --- a/lib/shared/community-utils.js +++ b/lib/shared/community-utils.js @@ -1,68 +1,124 @@ // @flow import * as React from 'react'; import { + createOrUpdateFarcasterChannelTagActionTypes, + useCreateOrUpdateFarcasterChannelTag, deleteFarcasterChannelTagActionTypes, useDeleteFarcasterChannelTag, } from '../actions/community-actions.js'; import { createLoadingStatusSelector } from '../selectors/loading-selectors.js'; import type { SetState } from '../types/hook-types.js'; import { useDispatchActionPromise } from '../utils/redux-promise-utils.js'; import { useSelector } from '../utils/redux-utils.js'; function farcasterChannelTagBlobHash(farcasterChannelID: string): string { return `farcaster_channel_tag_${farcasterChannelID}`; } +const createOrUpdateFarcasterChannelTagStatusSelector = + createLoadingStatusSelector(createOrUpdateFarcasterChannelTagActionTypes); +function useCreateFarcasterChannelTag( + commCommunityID: string, + setError: SetState, +): { + +createTag: (farcasterChannelID: string) => mixed, + +isLoading: boolean, +} { + const dispatchActionPromise = useDispatchActionPromise(); + + const createOrUpdateFarcasterChannelTag = + useCreateOrUpdateFarcasterChannelTag(); + + const createCreateOrUpdateActionPromise = React.useCallback( + async (farcasterChannelID: string) => { + try { + return await createOrUpdateFarcasterChannelTag({ + commCommunityID, + farcasterChannelID, + }); + } catch (e) { + setError(e.message); + throw e; + } + }, + [commCommunityID, createOrUpdateFarcasterChannelTag, setError], + ); + + const createTag = React.useCallback( + (farcasterChannelID: string) => { + void dispatchActionPromise( + createOrUpdateFarcasterChannelTagActionTypes, + createCreateOrUpdateActionPromise(farcasterChannelID), + ); + }, + [createCreateOrUpdateActionPromise, dispatchActionPromise], + ); + + const createOrUpdateFarcasterChannelTagStatus = useSelector( + createOrUpdateFarcasterChannelTagStatusSelector, + ); + const isLoading = createOrUpdateFarcasterChannelTagStatus === 'loading'; + + return { + createTag, + isLoading, + }; +} + const deleteFarcasterChannelTagStatusSelector = createLoadingStatusSelector( deleteFarcasterChannelTagActionTypes, ); function useRemoveFarcasterChannelTag( commCommunityID: string, farcasterChannelID: string, setError: SetState, ): { +removeTag: () => mixed, +isLoading: boolean, } { const dispatchActionPromise = useDispatchActionPromise(); const deleteFarcasterChannelTag = useDeleteFarcasterChannelTag(); const createDeleteActionPromise = React.useCallback(async () => { try { return await deleteFarcasterChannelTag({ commCommunityID, farcasterChannelID, }); } catch (e) { setError(e.message); throw e; } }, [ commCommunityID, deleteFarcasterChannelTag, farcasterChannelID, setError, ]); const removeTag = React.useCallback(() => { void dispatchActionPromise( deleteFarcasterChannelTagActionTypes, createDeleteActionPromise(), ); }, [createDeleteActionPromise, dispatchActionPromise]); const deleteFarcasterChannelTagStatus = useSelector( deleteFarcasterChannelTagStatusSelector, ); const isLoading = deleteFarcasterChannelTagStatus === 'loading'; return { removeTag, isLoading, }; } -export { farcasterChannelTagBlobHash, useRemoveFarcasterChannelTag }; +export { + farcasterChannelTagBlobHash, + useCreateFarcasterChannelTag, + useRemoveFarcasterChannelTag, +}; diff --git a/native/community-settings/tag-farcaster-channel/tag-channel-button.react.js b/native/community-settings/tag-farcaster-channel/tag-channel-button.react.js index fddc3ee70..61213271c 100644 --- a/native/community-settings/tag-farcaster-channel/tag-channel-button.react.js +++ b/native/community-settings/tag-farcaster-channel/tag-channel-button.react.js @@ -1,211 +1,164 @@ // @flow import { useActionSheet } from '@expo/react-native-action-sheet'; import { useNavigation } from '@react-navigation/native'; import invariant from 'invariant'; import * as React from 'react'; import { Text, View, Platform, ActivityIndicator } from 'react-native'; import { useSafeAreaInsets } from 'react-native-safe-area-context'; -import { - createOrUpdateFarcasterChannelTagActionTypes, - useCreateOrUpdateFarcasterChannelTag, -} from 'lib/actions/community-actions.js'; import { NeynarClientContext } from 'lib/components/neynar-client-provider.react.js'; -import { createLoadingStatusSelector } from 'lib/selectors/loading-selectors.js'; +import { useCreateFarcasterChannelTag } from 'lib/shared/community-utils.js'; import type { NeynarChannel } from 'lib/types/farcaster-types.js'; import type { SetState } from 'lib/types/hook-types.js'; import { useCurrentUserFID } from 'lib/utils/farcaster-utils.js'; -import { useDispatchActionPromise } from 'lib/utils/redux-promise-utils.js'; import Button from '../../components/button.react.js'; import { TagFarcasterChannelByNameRouteName } from '../../navigation/route-names.js'; import { useSelector } from '../../redux/redux-utils.js'; import { useStyles, useColors } from '../../themes/colors.js'; -const createOrUpdateFarcasterChannelTagStatusSelector = - createLoadingStatusSelector(createOrUpdateFarcasterChannelTagActionTypes); - type Props = { +communityID: string, +setError: SetState, }; function TagChannelButton(props: Props): React.Node { const { communityID, setError } = props; const { navigate } = useNavigation(); const colors = useColors(); const styles = useStyles(unboundStyles); const fid = useCurrentUserFID(); invariant(fid, 'FID should be set'); const [channelOptions, setChannelOptions] = React.useState< $ReadOnlyArray, >([]); const neynarClientContext = React.useContext(NeynarClientContext); invariant(neynarClientContext, 'NeynarClientContext is missing'); const { client } = neynarClientContext; React.useEffect(() => { void (async () => { const channels = await client.fetchFollowedFarcasterChannels(fid); const sortedChannels = channels.sort((a, b) => a.id.localeCompare(b.id)); setChannelOptions(sortedChannels); })(); }, [client, fid]); const activeTheme = useSelector(state => state.globalThemeInfo.activeTheme); const { showActionSheetWithOptions } = useActionSheet(); const insets = useSafeAreaInsets(); - const dispatchActionPromise = useDispatchActionPromise(); - - const createOrUpdateFarcasterChannelTag = - useCreateOrUpdateFarcasterChannelTag(); - - const createCreateOrUpdateActionPromise = React.useCallback( - async (channelID: string) => { - try { - return await createOrUpdateFarcasterChannelTag({ - commCommunityID: communityID, - farcasterChannelID: channelID, - }); - } catch (e) { - setError(e.message); - throw e; - } - }, - [communityID, createOrUpdateFarcasterChannelTag, setError], + const { createTag, isLoading } = useCreateFarcasterChannelTag( + communityID, + setError, ); const onOptionSelected = React.useCallback( (selectedIndex: ?number) => { if ( selectedIndex === undefined || selectedIndex === null || selectedIndex > channelOptions.length ) { return; } setError(null); // This is the "Other" option if (selectedIndex === channelOptions.length) { navigate<'TagFarcasterChannelByName'>({ name: TagFarcasterChannelByNameRouteName, params: { communityID }, }); return; } const channel = channelOptions[selectedIndex]; - void dispatchActionPromise( - createOrUpdateFarcasterChannelTagActionTypes, - createCreateOrUpdateActionPromise(channel.id), - ); + createTag(channel.id); }, - [ - channelOptions, - communityID, - createCreateOrUpdateActionPromise, - dispatchActionPromise, - navigate, - setError, - ], + [channelOptions, communityID, createTag, navigate, setError], ); const onPressTag = React.useCallback(() => { const channelNames = [ 'Other', ...channelOptions.map(channel => `/${channel.id}`), ]; const options = Platform.OS === 'ios' ? [...channelNames, 'Cancel'] : channelNames; const cancelButtonIndex = Platform.OS === 'ios' ? options.length - 1 : -1; const containerStyle = { paddingBottom: insets.bottom, }; showActionSheetWithOptions( { options, cancelButtonIndex, containerStyle, userInterfaceStyle: activeTheme ?? 'dark', }, onOptionSelected, ); }, [ activeTheme, channelOptions, insets.bottom, onOptionSelected, showActionSheetWithOptions, ]); - const createOrUpdateFarcasterChannelTagStatus = useSelector( - createOrUpdateFarcasterChannelTagStatusSelector, - ); - const isLoadingCreateOrUpdateFarcasterChannelTag = - createOrUpdateFarcasterChannelTagStatus === 'loading'; - const buttonContent = React.useMemo(() => { - if (isLoadingCreateOrUpdateFarcasterChannelTag) { + if (isLoading) { return ( ); } return Tag channel; - }, [ - colors.panelForegroundLabel, - isLoadingCreateOrUpdateFarcasterChannelTag, - styles.buttonText, - ]); + }, [colors.panelForegroundLabel, isLoading, styles.buttonText]); return ( - ); } const unboundStyles = { button: { borderRadius: 8, paddingVertical: 12, paddingHorizontal: 24, backgroundColor: 'purpleButton', }, buttonText: { color: 'whiteText', textAlign: 'center', fontWeight: '500', fontSize: 16, lineHeight: 24, }, buttonContainer: { height: 24, }, }; export default TagChannelButton;