diff --git a/lib/facts/links.js b/lib/facts/links.js new file mode 100644 index 000000000..09328d86b --- /dev/null +++ b/lib/facts/links.js @@ -0,0 +1,7 @@ +// @flow + +function inviteLinkUrl(secret: string): string { + return `https://comm.app/invite/${secret}`; +} + +export { inviteLinkUrl }; diff --git a/native/invite-links/manage-public-link-screen.react.js b/native/invite-links/manage-public-link-screen.react.js index 647da17f7..0d612607d 100644 --- a/native/invite-links/manage-public-link-screen.react.js +++ b/native/invite-links/manage-public-link-screen.react.js @@ -1,267 +1,268 @@ // @flow import * as React from 'react'; import { Text, View, Alert } from 'react-native'; import { createOrUpdatePublicLink, createOrUpdatePublicLinkActionTypes, disableInviteLink as callDisableInviteLink, disableInviteLinkLinkActionTypes, } from 'lib/actions/link-actions.js'; +import { inviteLinkUrl } from 'lib/facts/links.js'; import { primaryInviteLinksSelector } from 'lib/selectors/invite-links-selectors.js'; import { createLoadingStatusSelector } from 'lib/selectors/loading-selectors.js'; import type { ThreadInfo } from 'lib/types/thread-types.js'; import { useDispatchActionPromise, useServerCall, } from 'lib/utils/action-utils.js'; import Button from '../components/button.react.js'; import TextInput from '../components/text-input.react.js'; import type { RootNavigationProp } from '../navigation/root-navigator.react.js'; import type { NavigationRoute } from '../navigation/route-names.js'; import { useSelector } from '../redux/redux-utils.js'; import { useStyles } from '../themes/colors.js'; export type ManagePublicLinkScreenParams = { +community: ThreadInfo, }; type Props = { +navigation: RootNavigationProp<'ManagePublicLink'>, +route: NavigationRoute<'ManagePublicLink'>, }; function ManagePublicLinkScreen(props: Props): React.Node { const { community } = props.route.params; const inviteLink = useSelector(primaryInviteLinksSelector)[community.id]; const [name, setName] = React.useState( inviteLink?.name ?? Math.random().toString(36).slice(-9), ); const [error, setError] = React.useState(null); const dispatchActionPromise = useDispatchActionPromise(); const callCreateOrUpdatePublicLink = useServerCall(createOrUpdatePublicLink); const createCreateOrUpdateActionPromise = React.useCallback(async () => { setError(null); try { return await callCreateOrUpdatePublicLink({ name, communityID: community.id, }); } catch (e) { setError(e.message); throw e; } }, [callCreateOrUpdatePublicLink, community.id, name]); const createInviteLink = React.useCallback(() => { dispatchActionPromise( createOrUpdatePublicLinkActionTypes, createCreateOrUpdateActionPromise(), ); }, [createCreateOrUpdateActionPromise, dispatchActionPromise]); const createOrUpdatePublicLinkStatus = useSelector( createOrUpdatePublicLinkStatusSelector, ); const styles = useStyles(unboundStyles); let errorComponent = null; if (error) { errorComponent = {error}; } const disableInviteLinkServerCall = useServerCall(callDisableInviteLink); const createDisableLinkActionPromise = React.useCallback(async () => { setError(null); try { return await disableInviteLinkServerCall({ name, communityID: community.id, }); } catch (e) { setError(e.message); throw e; } }, [disableInviteLinkServerCall, community.id, name]); const disableInviteLink = React.useCallback(() => { dispatchActionPromise( disableInviteLinkLinkActionTypes, createDisableLinkActionPromise(), ); }, [createDisableLinkActionPromise, dispatchActionPromise]); const disableInviteLinkStatus = useSelector(disableInviteLinkStatusSelector); const isLoading = createOrUpdatePublicLinkStatus === 'loading' || disableInviteLinkStatus === 'loading'; const onDisableButtonClick = React.useCallback(() => { Alert.alert( 'Disable public link', 'Are you sure you want to disable your public link? Members who have your community’s public link but have not joined will not able to with the disabled link. \n' + '\n' + 'Other communities may also claim your previous public link url.', [ { text: 'Confirm disable', style: 'destructive', onPress: disableInviteLink, }, { text: 'Cancel', }, ], { cancelable: true, }, ); }, [disableInviteLink]); let disablePublicLinkButton = null; if (inviteLink) { disablePublicLinkButton = ( ); } return ( Let your community be more accessible with your own unique public link. By enabling a public link, you are allowing anyone who has your link to join your community.{'\n\n'} Editing your community’s public link allows other communities to claim your previous URL. INVITE URL - https://comm.app/invite/ + {inviteLinkUrl('')} {errorComponent} {disablePublicLinkButton} ); } const createOrUpdatePublicLinkStatusSelector = createLoadingStatusSelector( createOrUpdatePublicLinkActionTypes, ); const disableInviteLinkStatusSelector = createLoadingStatusSelector( disableInviteLinkLinkActionTypes, ); const unboundStyles = { sectionTitle: { fontSize: 14, fontWeight: '400', lineHeight: 20, color: 'modalBackgroundLabel', paddingHorizontal: 16, paddingBottom: 4, marginTop: 24, }, section: { borderBottomColor: 'modalSeparator', borderBottomWidth: 1, borderTopColor: 'modalSeparator', borderTopWidth: 1, backgroundColor: 'modalForeground', padding: 16, }, sectionText: { fontSize: 14, fontWeight: '400', lineHeight: 22, color: 'modalBackgroundLabel', }, inviteLink: { flexDirection: 'row', alignItems: 'center', marginBottom: 8, }, inviteLinkPrefix: { fontSize: 14, fontWeight: '400', lineHeight: 22, color: 'disabledButtonText', marginRight: 2, }, input: { color: 'panelForegroundLabel', borderColor: 'panelSecondaryForegroundBorder', borderWidth: 1, borderRadius: 8, paddingVertical: 13, paddingHorizontal: 16, flex: 1, }, button: { borderRadius: 8, paddingVertical: 12, paddingHorizontal: 24, marginTop: 8, }, buttonPrimary: { backgroundColor: 'purpleButton', }, destructiveButtonContainer: { margin: 16, }, destructiveButton: { borderWidth: 1, borderRadius: 8, borderColor: 'vibrantRedButton', }, destructiveButtonText: { fontSize: 16, fontWeight: '500', lineHeight: 24, color: 'vibrantRedButton', textAlign: 'center', }, buttonText: { color: 'whiteText', textAlign: 'center', fontWeight: '500', fontSize: 16, lineHeight: 24, }, error: { fontSize: 12, fontWeight: '400', lineHeight: 18, textAlign: 'center', color: 'redText', }, }; export default ManagePublicLinkScreen; diff --git a/native/invite-links/view-invite-links-screen.react.js b/native/invite-links/view-invite-links-screen.react.js index fa8af69a1..6a272f3d8 100644 --- a/native/invite-links/view-invite-links-screen.react.js +++ b/native/invite-links/view-invite-links-screen.react.js @@ -1,166 +1,167 @@ // @flow import Clipboard from '@react-native-clipboard/clipboard'; import * as React from 'react'; import { Text, View } from 'react-native'; import { TouchableOpacity } from 'react-native-gesture-handler'; +import { inviteLinkUrl } from 'lib/facts/links.js'; import { primaryInviteLinksSelector } from 'lib/selectors/invite-links-selectors.js'; import { threadHasPermission } from 'lib/shared/thread-utils.js'; import type { InviteLink } from 'lib/types/link-types.js'; import { threadPermissions } from 'lib/types/thread-permission-types.js'; import type { ThreadInfo } from 'lib/types/thread-types.js'; import SWMansionIcon from '../components/swmansion-icon.react.js'; import { displayActionResultModal } from '../navigation/action-result-modal.js'; import type { RootNavigationProp } from '../navigation/root-navigator.react.js'; import { ManagePublicLinkRouteName, type NavigationRoute, } from '../navigation/route-names.js'; import { useSelector } from '../redux/redux-utils.js'; import { useStyles, useColors } from '../themes/colors.js'; export type ViewInviteLinksScreenParams = { +community: ThreadInfo, }; type Props = { +navigation: RootNavigationProp<'ViewInviteLinks'>, +route: NavigationRoute<'ViewInviteLinks'>, }; const confirmCopy = () => displayActionResultModal('copied!'); function ViewInviteLinksScreen(props: Props): React.Node { const { community } = props.route.params; const inviteLink: ?InviteLink = useSelector(primaryInviteLinksSelector)[ community.id ]; const styles = useStyles(unboundStyles); const { modalForegroundLabel } = useColors(); - const linkUrl = `https://comm.app/invite/${inviteLink?.name ?? ''}`; + const linkUrl = inviteLinkUrl(inviteLink?.name ?? ''); const onPressCopy = React.useCallback(() => { Clipboard.setString(linkUrl); setTimeout(confirmCopy); }, [linkUrl]); const { navigate } = props.navigation; const onEditButtonClick = React.useCallback(() => { navigate<'ManagePublicLink'>({ name: ManagePublicLinkRouteName, params: { community, }, }); }, [community, navigate]); const canManageLinks = threadHasPermission( community, threadPermissions.MANAGE_INVITE_LINKS, ); let publicLinkSection = null; if (inviteLink || canManageLinks) { let description; if (canManageLinks) { description = ( <> Public links allow unlimited uses and never expire. Edit public link ); } else { description = ( Use this public link to invite your friends into the community! ); } publicLinkSection = ( <> PUBLIC LINK {linkUrl} Copy {description} ); } return {publicLinkSection}; } const unboundStyles = { container: { flex: 1, paddingTop: 24, }, sectionTitle: { fontSize: 12, fontWeight: '400', lineHeight: 18, color: 'modalBackgroundLabel', paddingHorizontal: 16, paddingBottom: 4, }, section: { borderBottomColor: 'modalSeparator', borderBottomWidth: 1, borderTopColor: 'modalSeparator', borderTopWidth: 1, backgroundColor: 'modalForeground', padding: 16, }, link: { paddingHorizontal: 16, paddingVertical: 9, marginBottom: 16, backgroundColor: 'inviteLinkButtonBackground', borderRadius: 20, flexDirection: 'row', justifyContent: 'space-between', }, linkText: { fontSize: 14, fontWeight: '400', lineHeight: 22, color: 'inviteLinkLinkColor', }, button: { flexDirection: 'row', alignItems: 'center', }, copy: { fontSize: 12, fontWeight: '400', lineHeight: 18, color: 'modalForegroundLabel', paddingLeft: 8, }, details: { fontSize: 12, fontWeight: '400', lineHeight: 18, color: 'modalForegroundLabel', }, editLinkButton: { color: 'purpleLink', }, }; export default ViewInviteLinksScreen;