diff --git a/lib/shared/community-utils.js b/lib/shared/community-utils.js index b7ba5c4a1..1cdf09e05 100644 --- a/lib/shared/community-utils.js +++ b/lib/shared/community-utils.js @@ -1,148 +1,149 @@ // @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'; const tagFarcasterChannelCopy = { DESCRIPTION: 'Tag a Farcaster channel so followers can find your Comm community!', CHANNEL_NAME_HEADER: 'Selected channel:', NO_CHANNEL_TAGGED: 'No Farcaster channel tagged', + REMOVE_TAG_BUTTON: 'Remove tag', }; const tagFarcasterChannelErrorMessages: { +[string]: string } = { already_in_use: 'This Farcaster channel is already tagged to a community.', channel_not_found: 'Could not find a channel with the provided name.', }; function farcasterChannelTagBlobHash(farcasterChannelID: string): string { return `farcaster_channel_tag_${farcasterChannelID}`; } const createOrUpdateFarcasterChannelTagStatusSelector = createLoadingStatusSelector(createOrUpdateFarcasterChannelTagActionTypes); function useCreateFarcasterChannelTag( commCommunityID: string, setError: SetState, onSuccessCallback?: () => mixed, ): { +createTag: (farcasterChannelID: string) => mixed, +isLoading: boolean, } { const dispatchActionPromise = useDispatchActionPromise(); const createOrUpdateFarcasterChannelTag = useCreateOrUpdateFarcasterChannelTag(); const createCreateOrUpdateActionPromise = React.useCallback( async (farcasterChannelID: string) => { try { const res = await createOrUpdateFarcasterChannelTag({ commCommunityID, farcasterChannelID, }); onSuccessCallback?.(); return res; } catch (e) { setError(e.message); throw e; } }, [ commCommunityID, createOrUpdateFarcasterChannelTag, onSuccessCallback, 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 { tagFarcasterChannelCopy, tagFarcasterChannelErrorMessages, farcasterChannelTagBlobHash, useCreateFarcasterChannelTag, useRemoveFarcasterChannelTag, }; diff --git a/native/community-settings/tag-farcaster-channel/remove-tag-button.react.js b/native/community-settings/tag-farcaster-channel/remove-tag-button.react.js index 840d2cfbe..b2d764d28 100644 --- a/native/community-settings/tag-farcaster-channel/remove-tag-button.react.js +++ b/native/community-settings/tag-farcaster-channel/remove-tag-button.react.js @@ -1,67 +1,74 @@ // @flow import * as React from 'react'; import { View, Text, ActivityIndicator } from 'react-native'; -import { useRemoveFarcasterChannelTag } from 'lib/shared/community-utils.js'; +import { + tagFarcasterChannelCopy, + useRemoveFarcasterChannelTag, +} from 'lib/shared/community-utils.js'; import type { SetState } from 'lib/types/hook-types.js'; import Button from '../../components/button.react.js'; import { useStyles, useColors } from '../../themes/colors.js'; type Props = { +communityID: string, +channelID: string, +setError: SetState, }; function RemoveTagButton(props: Props): React.Node { const { communityID, channelID, setError } = props; const styles = useStyles(unboundStyles); const colors = useColors(); const { removeTag, isLoading } = useRemoveFarcasterChannelTag( communityID, channelID, setError, ); const buttonContent = React.useMemo(() => { if (isLoading) { return ( ); } - return Remove tag; + return ( + + {tagFarcasterChannelCopy.REMOVE_TAG_BUTTON} + + ); }, [colors.panelForegroundLabel, isLoading, styles.buttonText]); return ( ); } const unboundStyles = { button: { borderRadius: 8, paddingVertical: 12, paddingHorizontal: 24, borderWidth: 1, borderColor: 'vibrantRedButton', }, buttonText: { fontSize: 16, fontWeight: '500', lineHeight: 24, color: 'vibrantRedButton', textAlign: 'center', }, buttonContainer: { height: 24, }, }; export default RemoveTagButton; diff --git a/web/tag-farcaster-channel/remove-tag-button.react.js b/web/tag-farcaster-channel/remove-tag-button.react.js new file mode 100644 index 000000000..f74452e89 --- /dev/null +++ b/web/tag-farcaster-channel/remove-tag-button.react.js @@ -0,0 +1,40 @@ +// @flow + +import * as React from 'react'; + +import { + tagFarcasterChannelCopy, + useRemoveFarcasterChannelTag, +} from 'lib/shared/community-utils.js'; +import type { SetState } from 'lib/types/hook-types.js'; + +import Button, { buttonThemes } from '../components/button.react.js'; + +type Props = { + +communityID: string, + +channelID: string, + +setError: SetState, +}; + +function RemoveTagButton(props: Props): React.Node { + const { communityID, channelID, setError } = props; + + const { removeTag, isLoading } = useRemoveFarcasterChannelTag( + communityID, + channelID, + setError, + ); + + return ( + + ); +} + +export default RemoveTagButton; diff --git a/web/tag-farcaster-channel/tag-farcaster-channel-modal.css b/web/tag-farcaster-channel/tag-farcaster-channel-modal.css index d2cf2f785..a70400e9b 100644 --- a/web/tag-farcaster-channel/tag-farcaster-channel-modal.css +++ b/web/tag-farcaster-channel/tag-farcaster-channel-modal.css @@ -1,22 +1,36 @@ .modalDescription { color: var(--text-background-secondary-default); font-size: var(--m-font-16); margin-bottom: 24px; } .farcasterChannelTitle { color: var(--text-background-primary-default); font-size: var(--l-font-18); margin-bottom: 8px; } .farcasterChannelText { color: var(--text-background-secondary-default); font-size: var(--m-font-16); font-weight: var(--bold); } .noChannelText { color: var(--text-background-secondary-default); font-size: var(--m-font-16); } + +.errorMessage { + position: absolute; + color: var(--text-background-danger-default); + font-size: var(--s-font-14); + margin-top: 4px; + visibility: hidden; + left: 50%; + transform: translateX(-50%); +} + +.errorMessageVisible { + visibility: visible; +} diff --git a/web/tag-farcaster-channel/tag-farcaster-channel-modal.react.js b/web/tag-farcaster-channel/tag-farcaster-channel-modal.react.js index 5f8041d37..8e295d41d 100644 --- a/web/tag-farcaster-channel/tag-farcaster-channel-modal.react.js +++ b/web/tag-farcaster-channel/tag-farcaster-channel-modal.react.js @@ -1,60 +1,102 @@ // @flow +import classNames from 'classnames'; import * as React from 'react'; import { useModalContext } from 'lib/components/modal-provider.react.js'; -import { tagFarcasterChannelCopy } from 'lib/shared/community-utils.js'; +import { + tagFarcasterChannelCopy, + tagFarcasterChannelErrorMessages, +} from 'lib/shared/community-utils.js'; import type { CommunityInfo } from 'lib/types/community-types.js'; +import RemoveTagButton from './remove-tag-button.react.js'; import css from './tag-farcaster-channel-modal.css'; import Modal from '../modals/modal.react.js'; import { useSelector } from '../redux/redux-utils.js'; type Props = { +communityID: string, }; function TagFarcasterChannelModal(props: Props): React.Node { const { communityID } = props; const { popModal } = useModalContext(); const communityInfo: ?CommunityInfo = useSelector( state => state.communityStore.communityInfos[communityID], ); + const [removeTagError, setRemoveTagError] = React.useState(); + const channelNameTextContent = React.useMemo(() => { if (!communityInfo?.farcasterChannelID) { return (
{tagFarcasterChannelCopy.NO_CHANNEL_TAGGED}
); } return (
/{communityInfo.farcasterChannelID}
); }, [communityInfo?.farcasterChannelID]); + const primaryButton = React.useMemo(() => { + if (communityInfo?.farcasterChannelID) { + return ( + + ); + } + // TODO: Implement TagChannelButton + return null; + }, [communityID, communityInfo?.farcasterChannelID]); + + const errorMessageClassName = classNames(css.errorMessage, { + [css.errorMessageVisible]: removeTagError, + }); + + const errorMessage = + removeTagError && tagFarcasterChannelErrorMessages[removeTagError] + ? tagFarcasterChannelErrorMessages[removeTagError] + : 'Unknown error.'; + const tagFarcasterChannelModal = React.useMemo( () => ( - +
{tagFarcasterChannelCopy.DESCRIPTION}
{tagFarcasterChannelCopy.CHANNEL_NAME_HEADER}
{channelNameTextContent} +
{errorMessage}
), - [channelNameTextContent, popModal], + [ + channelNameTextContent, + errorMessage, + errorMessageClassName, + popModal, + primaryButton, + ], ); return tagFarcasterChannelModal; } export default TagFarcasterChannelModal;