diff --git a/lib/shared/community-utils.js b/lib/shared/community-utils.js index 1cbc95d3f..f6f4f79ca 100644 --- a/lib/shared/community-utils.js +++ b/lib/shared/community-utils.js @@ -1,134 +1,142 @@ // @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', +}; + 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, farcasterChannelTagBlobHash, useCreateFarcasterChannelTag, useRemoveFarcasterChannelTag, }; diff --git a/native/community-settings/tag-farcaster-channel/tag-farcaster-channel.react.js b/native/community-settings/tag-farcaster-channel/tag-farcaster-channel.react.js index 55563b97d..68257277e 100644 --- a/native/community-settings/tag-farcaster-channel/tag-farcaster-channel.react.js +++ b/native/community-settings/tag-farcaster-channel/tag-farcaster-channel.react.js @@ -1,164 +1,169 @@ // @flow import * as React from 'react'; import { View, Text } from 'react-native'; +import { tagFarcasterChannelCopy } from 'lib/shared/community-utils.js'; import type { CommunityInfo } from 'lib/types/community-types.js'; import RemoveTagButton from './remove-tag-button.react.js'; import TagChannelButton from './tag-channel-button.react.js'; import type { TagFarcasterChannelNavigationProp } from './tag-farcaster-channel-navigator.react.js'; import { tagFarcasterChannelErrorMessages } from './tag-farcaster-channel-utils.js'; import { type NavigationRoute } from '../../navigation/route-names.js'; import { useSelector } from '../../redux/redux-utils.js'; import { useStyles } from '../../themes/colors.js'; export type TagFarcasterChannelParams = { +communityID: string, }; type Props = { +navigation: TagFarcasterChannelNavigationProp<'TagFarcasterChannel'>, +route: NavigationRoute<'TagFarcasterChannel'>, }; function TagFarcasterChannel(props: Props): React.Node { const { route } = props; const { communityID } = route.params; const communityInfo: ?CommunityInfo = useSelector( state => state.communityStore.communityInfos[communityID], ); const styles = useStyles(unboundStyles); const [error, setError] = React.useState(null); const errorMessage = React.useMemo(() => { if (!error) { return null; } return ( {tagFarcasterChannelErrorMessages[error] ?? 'Unknown error.'} ); }, [error, styles.error]); const channelNameTextContent = React.useMemo(() => { if (!communityInfo?.farcasterChannelID) { return ( - No Farcaster channel tagged + + {tagFarcasterChannelCopy.NO_CHANNEL_TAGGED} + ); } return ( {`/${communityInfo.farcasterChannelID}`} ); }, [ communityInfo?.farcasterChannelID, styles.channelNameText, styles.noChannelText, ]); const sectionButton = React.useMemo( () => communityInfo?.farcasterChannelID ? ( ) : ( ), [communityID, communityInfo?.farcasterChannelID], ); const tagFarcasterChannel = React.useMemo( () => ( - Tag a Farcaster channel so followers can find your Comm community! + {tagFarcasterChannelCopy.DESCRIPTION} FARCASTER CHANNEL - Selected channel: + + {tagFarcasterChannelCopy.CHANNEL_NAME_HEADER} + {channelNameTextContent} {sectionButton} {errorMessage} ), [ styles.panelSectionContainer, styles.sectionText, styles.sectionHeaderText, styles.channelNameContainer, styles.errorContainer, channelNameTextContent, sectionButton, errorMessage, ], ); return tagFarcasterChannel; } const unboundStyles = { panelSectionContainer: { backgroundColor: 'panelForeground', padding: 16, borderBottomColor: 'panelSeparator', borderBottomWidth: 1, borderTopColor: 'panelSeparator', borderTopWidth: 1, }, sectionText: { color: 'panelForegroundLabel', fontSize: 14, }, sectionHeaderText: { fontSize: 14, fontWeight: '400', lineHeight: 20, color: 'panelForegroundLabel', paddingHorizontal: 16, paddingBottom: 4, marginTop: 24, }, channelNameContainer: { marginTop: 8, marginBottom: 24, height: 20, }, channelNameText: { fontSize: 16, fontWeight: '600', color: 'panelForegroundLabel', }, noChannelText: { fontSize: 16, color: 'panelForegroundLabel', }, errorContainer: { height: 18, }, error: { fontSize: 12, fontWeight: '400', lineHeight: 18, textAlign: 'center', color: 'redText', }, }; export default TagFarcasterChannel; diff --git a/web/tag-farcaster-channel/tag-farcaster-channel-modal.css b/web/tag-farcaster-channel/tag-farcaster-channel-modal.css new file mode 100644 index 000000000..d2cf2f785 --- /dev/null +++ b/web/tag-farcaster-channel/tag-farcaster-channel-modal.css @@ -0,0 +1,22 @@ +.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); +} diff --git a/web/tag-farcaster-channel/tag-farcaster-channel-modal.react.js b/web/tag-farcaster-channel/tag-farcaster-channel-modal.react.js new file mode 100644 index 000000000..5f8041d37 --- /dev/null +++ b/web/tag-farcaster-channel/tag-farcaster-channel-modal.react.js @@ -0,0 +1,60 @@ +// @flow + +import * as React from 'react'; + +import { useModalContext } from 'lib/components/modal-provider.react.js'; +import { tagFarcasterChannelCopy } from 'lib/shared/community-utils.js'; +import type { CommunityInfo } from 'lib/types/community-types.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 channelNameTextContent = React.useMemo(() => { + if (!communityInfo?.farcasterChannelID) { + return ( +
+ {tagFarcasterChannelCopy.NO_CHANNEL_TAGGED} +
+ ); + } + + return ( +
+ /{communityInfo.farcasterChannelID} +
+ ); + }, [communityInfo?.farcasterChannelID]); + + const tagFarcasterChannelModal = React.useMemo( + () => ( + +
+ {tagFarcasterChannelCopy.DESCRIPTION} +
+
+ {tagFarcasterChannelCopy.CHANNEL_NAME_HEADER} +
+ {channelNameTextContent} +
+ ), + [channelNameTextContent, popModal], + ); + + return tagFarcasterChannelModal; +} + +export default TagFarcasterChannelModal;