From cfbacd4aaf0448076b3cb8f3d02cf3aa8e2526ff Mon Sep 17 00:00:00 2001 From: Julian Mondragon Date: Sun, 15 Jan 2023 16:37:52 -0500 Subject: [PATCH] MM-42835_Invite People - add email+user invites --- .../header/plus_menu/index.tsx | 7 +- app/screens/invite/invite.tsx | 106 ++++++++++++------ app/screens/invite/summary.tsx | 83 ++++++++++++-- app/screens/navigation.ts | 14 --- assets/base/i18n/en.json | 4 +- 5 files changed, 145 insertions(+), 69 deletions(-) diff --git a/app/screens/home/channel_list/categories_list/header/plus_menu/index.tsx b/app/screens/home/channel_list/categories_list/header/plus_menu/index.tsx index 9e1ec49bf9..61b8a78aab 100644 --- a/app/screens/home/channel_list/categories_list/header/plus_menu/index.tsx +++ b/app/screens/home/channel_list/categories_list/header/plus_menu/index.tsx @@ -53,14 +53,9 @@ const PlusMenuList = ({canCreateChannels, canJoinChannels, canInvitePeople}: Pro const invitePeopleToTeam = useCallback(async () => { await dismissBottomSheet(); - const title = intl.formatMessage({id: 'invite.title', defaultMessage: 'Invite'}); - const closeButton = CompassIcon.getImageSourceSync('close', 24, theme.sidebarHeaderTextColor); - const closeButtonId = 'close-invite'; - showModal( Screens.INVITE, - title, - {closeButton, closeButtonId}, + intl.formatMessage({id: 'invite.title', defaultMessage: 'Invite'}), ); }, [intl, theme]); diff --git a/app/screens/invite/invite.tsx b/app/screens/invite/invite.tsx index a004fb4a3f..ea8e7423ac 100644 --- a/app/screens/invite/invite.tsx +++ b/app/screens/invite/invite.tsx @@ -3,20 +3,22 @@ import React, {useCallback, useEffect, useState, useRef} from 'react'; import {IntlShape, useIntl} from 'react-intl'; -import {Keyboard, View, LayoutChangeEvent} from 'react-native'; -import {ImageResource, OptionsTopBarButton} from 'react-native-navigation'; +import {Keyboard, View, LayoutChangeEvent, Platform} from 'react-native'; +import {OptionsTopBarButton} from 'react-native-navigation'; import {SafeAreaView} from 'react-native-safe-area-context'; import {getTeamMembersByIds, addUsersToTeam, sendEmailInvitesToTeam} from '@actions/remote/team'; import {searchProfiles} from '@actions/remote/user'; +import CompassIcon from '@components/compass_icon'; import Loading from '@components/loading'; import {ServerErrors} from '@constants'; import {useServerUrl} from '@context/server'; import {useTheme} from '@context/theme'; import {useModalPosition} from '@hooks/device'; import useNavButtonPressed from '@hooks/navigation_button_pressed'; -import {dismissModal, setButtons, setTitle} from '@screens/navigation'; +import {dismissModal, setButtons} from '@screens/navigation'; import {isEmail} from '@utils/helpers'; +import {mergeNavigationOptions} from '@utils/navigation'; import {makeStyleSheetFromTheme, changeOpacity} from '@utils/theme'; import {isGuest} from '@utils/user'; @@ -26,16 +28,33 @@ import Summary from './summary'; import type {NavButtons} from '@typings/screens/navigation'; const CLOSE_BUTTON_ID = 'close-invite'; +const BACK_BUTTON_ID = 'back-invite'; const SEND_BUTTON_ID = 'send-invite'; const SEARCH_TIMEOUT_MILLISECONDS = 200; +const DEFAULT_RESULT = {sent: [], notSent: []}; -const makeLeftButton = (icon: ImageResource): OptionsTopBarButton => { - return { - id: CLOSE_BUTTON_ID, - icon, - testID: 'invite.close.button', - }; -}; +const makeLeftButton = (theme: Theme, type: LeftButtonType): OptionsTopBarButton => ( + (type === LeftButtonType.BACK) ? ( + { + id: BACK_BUTTON_ID, + icon: CompassIcon.getImageSourceSync( + Platform.select({ + ios: 'arrow-back-ios', + default: 'arrow-left', + }), + 24, + theme.sidebarHeaderTextColor, + ), + testID: 'invite.back.button', + } + ) : ( + { + id: CLOSE_BUTTON_ID, + icon: CompassIcon.getImageSourceSync('close', 24, theme.sidebarHeaderTextColor), + testID: 'invite.close.button', + } + ) +); const makeRightButton = (theme: Theme, formatMessage: IntlShape['formatMessage'], enabled: boolean): OptionsTopBarButton => ({ id: SEND_BUTTON_ID, @@ -86,10 +105,13 @@ enum Stage { LOADING = 'loading', } +enum LeftButtonType { + CLOSE = 'close', + BACK = 'back', +} + type InviteProps = { componentId: string; - closeButton: ImageResource; - teamId: string; teamDisplayName: string; teamLastIconUpdate: number; @@ -100,7 +122,6 @@ type InviteProps = { export default function Invite({ componentId, - closeButton, teamId, teamDisplayName, teamLastIconUpdate, @@ -122,7 +143,7 @@ export default function Invite({ const [searchResults, setSearchResults] = useState([]); const [selectedIds, setSelectedIds] = useState<{[id: string]: SearchResult}>({}); const [loading, setLoading] = useState(false); - const [result, setResult] = useState({sent: [], notSent: []}); + const [result, setResult] = useState(DEFAULT_RESULT); const [wrapperHeight, setWrapperHeight] = useState(0); const [stage, setStage] = useState(Stage.SELECTION); const [sendError, setSendError] = useState(''); @@ -133,19 +154,6 @@ export default function Invite({ setWrapperHeight(e.nativeEvent.layout.height); }, []); - const setHeaderButtons = useCallback((right: boolean, rightEnabled: boolean) => { - const buttons: NavButtons = { - leftButtons: [makeLeftButton(closeButton)], - rightButtons: right ? [makeRightButton(theme, formatMessage, rightEnabled)] : [], - }; - - setButtons(componentId, buttons); - }, [closeButton, locale, theme, componentId]); - - const setHeaderTitle = useCallback((title: string) => { - setTitle(componentId, title); - }, [locale, theme, componentId]); - const searchUsers = useCallback(async (searchTerm: string) => { if (searchTerm === '') { handleClearSearch(); @@ -162,6 +170,16 @@ export default function Invite({ setSearchResults(results); }, [serverUrl, teamId]); + const handleReset = () => { + setTerm(''); + setSearchResults([]); + setSelectedIds({}); + setLoading(false); + setResult(DEFAULT_RESULT); + setStage(Stage.SELECTION); + setSendError(''); + }; + const handleClearSearch = useCallback(() => { setTerm(''); setSearchResults([]); @@ -196,8 +214,8 @@ export default function Invite({ }, [selectedIds, handleClearSearch]); const handleSendError = () => { - setSendError(formatMessage({id: 'invite.send_error', defaultMessage: 'Received an unexpected error. Please try again or contact your System Admin for assistance.'})); - setResult({sent: [], notSent: []}); + setSendError(formatMessage({id: 'invite.send_error', defaultMessage: 'Something went wrong while trying to send invitations. Please check your network connection and try again.'})); + setResult(DEFAULT_RESULT); setStage(Stage.RESULT); }; @@ -309,19 +327,32 @@ export default function Invite({ }; useNavButtonPressed(CLOSE_BUTTON_ID, componentId, closeModal, [closeModal]); + useNavButtonPressed(BACK_BUTTON_ID, componentId, handleReset, [handleReset]); useNavButtonPressed(SEND_BUTTON_ID, componentId, handleSend, [handleSend]); useEffect(() => { - // Update header buttons in case anything related to the header changes - setHeaderButtons(stage === Stage.SELECTION, selectedCount > 0); - }, [theme, locale, selectedCount, stage]); + const buttons: NavButtons = { + leftButtons: [makeLeftButton(theme, stage === Stage.RESULT && sendError ? LeftButtonType.BACK : LeftButtonType.CLOSE)], + rightButtons: stage === Stage.SELECTION ? [makeRightButton(theme, formatMessage, selectedCount > 0)] : [], + }; + + setButtons(componentId, buttons); + }, [theme, locale, componentId, selectedCount, stage, sendError]); useEffect(() => { - if (stage === Stage.RESULT) { - // Update header title in case anything related to the header changes - setHeaderTitle(formatMessage({id: 'invite.title.summary', defaultMessage: 'Invite summary'})); - } - }, [locale, stage]); + mergeNavigationOptions(componentId, { + topBar: { + title: { + color: theme.sidebarHeaderTextColor, + text: stage === Stage.RESULT ? ( + formatMessage({id: 'invite.title.summary', defaultMessage: 'Invite summary'}) + ) : ( + formatMessage({id: 'invite.title', defaultMessage: 'Invite'}) + ), + }, + }, + }); + }, [componentId, locale, theme, stage]); const handleRemoveItem = useCallback((id: string) => { const newSelectedIds = Object.assign({}, selectedIds); @@ -348,6 +379,7 @@ export default function Invite({ selectedIds={selectedIds} error={sendError} onClose={closeModal} + onRetry={handleReset} testID='invite.screen.summary' /> ); diff --git a/app/screens/invite/summary.tsx b/app/screens/invite/summary.tsx index 19a0659ff2..630704e143 100644 --- a/app/screens/invite/summary.tsx +++ b/app/screens/invite/summary.tsx @@ -1,11 +1,12 @@ // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // See LICENSE.txt for license information. -import React from 'react'; +import React, {useCallback, useMemo} from 'react'; import {useIntl} from 'react-intl'; import {View, Text, ScrollView} from 'react-native'; import Button from 'react-native-button'; +import CompassIcon from '@components/compass_icon'; import FormattedText from '@components/formatted_text'; import AlertSvg from '@components/illustrations/alert'; import ErrorSvg from '@components/illustrations/error'; @@ -51,6 +52,11 @@ const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => { marginHorizontal: 32, marginBottom: 24, }, + summaryErrorText: { + color: changeOpacity(theme.centerChannelColor, 0.72), + ...typography('Body', 200, 'Regular'), + textAlign: 'center', + }, footer: { display: 'flex', flexDirection: 'row', @@ -63,6 +69,17 @@ const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => { flexGrow: 1, maxWidth: MAX_WIDTH_CONTENT, }, + summaryButtonTextContainer: { + display: 'flex', + flexDirection: 'row', + alignItems: 'center', + height: 24, + }, + summaryButtonIcon: { + marginRight: 7, + color: theme.buttonColor, + + }, }; }); @@ -72,6 +89,7 @@ type SummaryProps = { error?: string; testID: string; onClose: () => void; + onRetry: () => void; } export default function Summary({ @@ -80,6 +98,7 @@ export default function Summary({ error, testID, onClose, + onRetry, }: SummaryProps) { const {formatMessage} = useIntl(); const theme = useTheme(); @@ -91,15 +110,33 @@ export default function Summary({ const styleButtonText = buttonTextStyle(theme, 'lg', 'primary'); const styleButtonBackground = buttonBackgroundStyle(theme, 'lg', 'primary'); + const styleSummaryMessageText = useMemo(() => { + const style = []; + + style.push(styles.summaryMessageText); + + if (error) { + style.push({marginBottom: 8}); + } + + return style; + }, [error, styles]); let svg = <>; let message = ''; if (error) { svg = ; + message = formatMessage( + { + id: 'invite.summary.error', + defaultMessage: '{invitationsCount, plural, one {Invitation} other {Invitations}} could not be sent successfully', + }, + {invitationsCount: sentCount + notSentCount}, + ); } else if (!sentCount && notSentCount) { svg = ; - message = error || formatMessage( + message = formatMessage( { id: 'invite.summary.not_sent', defaultMessage: '{notSentCount, plural, one {Invitation wasn’t} other {Invitations weren’t}} sent', @@ -126,9 +163,14 @@ export default function Summary({ ); } - const handleOnPressButton = () => { + const handleOnPressButton = useCallback(() => { + if (error) { + onRetry(); + return; + } + onClose(); - }; + }, [error, onRetry, onClose]); return ( {svg} - + {message} - {!error && ( + {error ? ( + + {error} + + ) : ( <> - + {error ? ( + + + + + ) : ( + + )} diff --git a/app/screens/navigation.ts b/app/screens/navigation.ts index cc8bde955c..ef732064d0 100644 --- a/app/screens/navigation.ts +++ b/app/screens/navigation.ts @@ -701,20 +701,6 @@ export function setButtons(componentId: string, buttons: NavButtons = {leftButto mergeNavigationOptions(componentId, options); } -export function setTitle(componentId: string, title: string) { - const theme = getThemeFromState(); - const options = { - topBar: { - title: { - color: theme.sidebarHeaderTextColor, - text: title, - }, - }, - }; - - mergeNavigationOptions(componentId, options); -} - export function showOverlay(name: string, passProps = {}, options = {}) { if (!isScreenRegistered(name)) { return; diff --git a/assets/base/i18n/en.json b/assets/base/i18n/en.json index 5cb98c873e..52dc25f63d 100644 --- a/assets/base/i18n/en.json +++ b/assets/base/i18n/en.json @@ -331,12 +331,13 @@ "invite.search.email_invite": "invite", "invite.search.no_results": "No one found matching", "invite.searchPlaceholder": "Type a name or email address…", - "invite.send_error": "Received an unexpected error. Please try again or contact your System Admin for assistance.", + "invite.send_error": "Something went wrong while trying to send invitations. Please check your network connection and try again.", "invite.send_invite": "Send", "invite.sendInvitationsTo": "Send invitations to…", "invite.shareLink": "Share link", "invite.summary.done": "Done", "invite.summary.email_invite": "An invitation email has been sent", + "invite.summary.error": "{invitationsCount, plural, one {Invitation} other {Invitations}} could not be sent successfully", "invite.summary.member_invite": "Invited as a member of {teamDisplayName}", "invite.summary.not_sent": "{notSentCount, plural, one {Invitation wasn’t} other {Invitations weren’t}} sent", "invite.summary.report.notSent": "{count} {count, plural, one {invitation} other {invitations}} not sent", @@ -344,6 +345,7 @@ "invite.summary.sent": "Your {sentCount, plural, one {invitation has} other {invitations have}} been sent", "invite.summary.smtp_failure": "SMTP is not configured in System Console", "invite.summary.some_not_sent": "{notSentCount, plural, one {An invitation was} other {Some invitations were}} not sent", + "invite.summary.try_again": "Try again", "invite.title": "Invite", "invite.title.summary": "Invite summary", "join_team.error.group_error": "You need to be a member of a linked group to join this team.",