diff --git a/native/invite-links/invite-links-navigator.react.js b/native/invite-links/invite-links-navigator.react.js new file mode 100644 --- /dev/null +++ b/native/invite-links/invite-links-navigator.react.js @@ -0,0 +1,83 @@ +// @flow + +import { + createStackNavigator, + type StackNavigationHelpers, + type StackNavigationProp, +} from '@react-navigation/stack'; +import * as React from 'react'; +import { SafeAreaView } from 'react-native-safe-area-context'; + +import ViewInviteLinksHeaderLeftButton from './view-invite-links-header-left-button.react.js'; +import ViewInviteLinksHeaderTitle from './view-invite-links-header-title.react.js'; +import ViewInviteLinksScreen from './view-invite-links-screen.react.js'; +import { defaultStackScreenOptions } from '../navigation/options.js'; +import type { RootNavigationProp } from '../navigation/root-navigator.react.js'; +import { + type InviteLinkParamList, + ViewInviteLinksRouteName, + type ScreenParamList, +} from '../navigation/route-names.js'; +import { useColors, useStyles } from '../themes/colors.js'; + +const safeAreaEdges = ['bottom']; + +export type InviteLinksNavigationProps< + RouteName: $Keys = $Keys, +> = StackNavigationProp; + +const InviteLinksStack = createStackNavigator< + ScreenParamList, + InviteLinkParamList, + StackNavigationHelpers, +>(); + +const viewInviteLinksOptions = ({ route }) => ({ + // eslint-disable-next-line react/display-name + headerTitle: props => ( + + ), + headerLeft: ViewInviteLinksHeaderLeftButton, + headerBackImage: () => null, + headerBackTitleStyle: { marginLeft: 20 }, +}); + +type Props = { + +navigation: RootNavigationProp<'InviteLinkNavigator'>, + ... +}; +// eslint-disable-next-line no-unused-vars +function InviteLinksNavigator(props: Props): React.Node { + const styles = useStyles(unboundStyles); + const colors = useColors(); + const screenOptions = React.useMemo( + () => ({ + ...defaultStackScreenOptions, + headerStyle: { + backgroundColor: colors.modalBackground, + borderBottomWidth: 1, + }, + }), + [colors.modalBackground], + ); + return ( + + + + + + ); +} + +const unboundStyles = { + container: { + flex: 1, + backgroundColor: 'modalBackground', + }, +}; + +export default InviteLinksNavigator; diff --git a/native/invite-links/view-invite-links-header-left-button.react.js b/native/invite-links/view-invite-links-header-left-button.react.js new file mode 100644 --- /dev/null +++ b/native/invite-links/view-invite-links-header-left-button.react.js @@ -0,0 +1,19 @@ +// @flow + +import { HeaderBackButton as BaseHeaderBackButton } from '@react-navigation/elements'; +import * as React from 'react'; + +import { useColors } from '../themes/colors.js'; + +type Props = React.ElementConfig; +function ViewInviteLinksHeaderLeftButton(props: Props): React.Node { + const { headerChevron } = useColors(); + if (!props.canGoBack) { + return null; + } + return ( + + ); +} + +export default ViewInviteLinksHeaderLeftButton; diff --git a/native/invite-links/view-invite-links-header-title.react.js b/native/invite-links/view-invite-links-header-title.react.js new file mode 100644 --- /dev/null +++ b/native/invite-links/view-invite-links-header-title.react.js @@ -0,0 +1,25 @@ +// @flow + +import type { HeaderTitleInputProps } from '@react-navigation/elements'; +import { HeaderTitle } from '@react-navigation/elements'; +import * as React from 'react'; + +import type { ThreadInfo } from 'lib/types/thread-types.js'; +import { useResolvedThreadInfo } from 'lib/utils/entity-helpers.js'; +import { firstLine } from 'lib/utils/string-utils.js'; + +type Props = { + +community: ThreadInfo, + ...HeaderTitleInputProps, +}; +function ViewInviteLinksHeaderTitle(props: Props): React.Node { + const { community, ...rest } = props; + const { uiName } = useResolvedThreadInfo(community); + const title = `Invite people to ${firstLine(uiName)}`; + return {title}; +} + +const MemoizedViewInviteLinksHeaderTitle: React.ComponentType = + React.memo(ViewInviteLinksHeaderTitle); + +export default MemoizedViewInviteLinksHeaderTitle; diff --git a/native/invite-links/view-invite-links-screen.react.js b/native/invite-links/view-invite-links-screen.react.js new file mode 100644 --- /dev/null +++ b/native/invite-links/view-invite-links-screen.react.js @@ -0,0 +1,126 @@ +// @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 { primaryInviteLinksSelector } from 'lib/selectors/invite-links-selectors.js'; +import type { InviteLink } from 'lib/types/link-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 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 onPressCopy = React.useCallback(() => { + Clipboard.setString(linkUrl); + setTimeout(confirmCopy); + }, [linkUrl]); + + let publicLinkSection = null; + if (inviteLink) { + publicLinkSection = ( + <> + PUBLIC LINK + + + {linkUrl} + + + Copy + + + + Use this public link to invite your friends into the community! + + + + ); + } + 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, + 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', + paddingTop: 16, + }, +}; + +export default ViewInviteLinksScreen; diff --git a/native/navigation/root-navigator.react.js b/native/navigation/root-navigator.react.js --- a/native/navigation/root-navigator.react.js +++ b/native/navigation/root-navigator.react.js @@ -41,6 +41,7 @@ TermsAndPrivacyRouteName, RegistrationRouteName, InviteLinkModalRouteName, + InviteLinkNavigatorRouteName, } from './route-names.js'; import LoggedOutModal from '../account/logged-out-modal.react.js'; import RegistrationNavigator from '../account/registration/registration-navigator.react.js'; @@ -53,6 +54,7 @@ import ComposeSubchannelModal from '../chat/settings/compose-subchannel-modal.react.js'; import SidebarListModal from '../chat/sidebar-list-modal.react.js'; import SubchannelsListModal from '../chat/subchannels-list-modal.react.js'; +import InviteLinksNavigator from '../invite-links/invite-links-navigator.react.js'; import CustomServerModal from '../profile/custom-server-modal.react.js'; enableScreens(); @@ -210,6 +212,11 @@ component={InviteLinkModal} options={modalOverlayScreenOptions} /> + > = diff --git a/native/themes/colors.js b/native/themes/colors.js --- a/native/themes/colors.js +++ b/native/themes/colors.js @@ -139,6 +139,8 @@ messageLabel: designSystemColors.shadesBlack100, modalSeparator: designSystemColors.shadesWhite60, secondaryButtonBorder: designSystemColors.shadesWhite100, + inviteLinkLinkColor: designSystemColors.shadesBlack100, + inviteLinkButtonBackground: designSystemColors.shadesWhite60, }); export type Colors = $Exact; @@ -234,6 +236,8 @@ messageLabel: designSystemColors.shadesWhite60, modalSeparator: designSystemColors.shadesBlack80, secondaryButtonBorder: designSystemColors.shadesWhite100, + inviteLinkLinkColor: designSystemColors.shadesWhite80, + inviteLinkButtonBackground: designSystemColors.shadesBlack80, }); const colors = { light, dark };