MM-42835_Invite People - add email+user invites

This commit is contained in:
Julian Mondragon
2023-01-15 16:37:52 -05:00
parent a9a9c00860
commit cfbacd4aaf
5 changed files with 145 additions and 69 deletions

View File

@@ -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]);

View File

@@ -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<SearchResult[]>([]);
const [selectedIds, setSelectedIds] = useState<{[id: string]: SearchResult}>({});
const [loading, setLoading] = useState(false);
const [result, setResult] = useState<Result>({sent: [], notSent: []});
const [result, setResult] = useState<Result>(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'
/>
);

View File

@@ -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 = <ErrorSvg/>;
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 = <ErrorSvg/>;
message = error || formatMessage(
message = formatMessage(
{
id: 'invite.summary.not_sent',
defaultMessage: '{notSentCount, plural, one {Invitation wasnt} other {Invitations werent}} sent',
@@ -126,9 +163,14 @@ export default function Summary({
);
}
const handleOnPressButton = () => {
const handleOnPressButton = useCallback(() => {
if (error) {
onRetry();
return;
}
onClose();
};
}, [error, onRetry, onClose]);
return (
<View
@@ -143,10 +185,14 @@ export default function Summary({
<View style={styles.summarySvg}>
{svg}
</View>
<Text style={styles.summaryMessageText}>
<Text style={styleSummaryMessageText}>
{message}
</Text>
{!error && (
{error ? (
<Text style={styles.summaryErrorText}>
{error}
</Text>
) : (
<>
<SummaryReport
type={SummaryReportType.NOT_SENT}
@@ -170,11 +216,26 @@ export default function Summary({
onPress={handleOnPressButton}
testID='invite.summary_button'
>
<FormattedText
id='invite.summary.done'
defaultMessage='Done'
style={styleButtonText}
/>
{error ? (
<View style={styles.summaryButtonTextContainer}>
<CompassIcon
name='refresh'
size={24}
style={styles.summaryButtonIcon}
/>
<FormattedText
id='invite.summary.try_again'
defaultMessage='Try again'
style={styleButtonText}
/>
</View>
) : (
<FormattedText
id='invite.summary.done'
defaultMessage='Done'
style={styleButtonText}
/>
)}
</Button>
</View>
</View>

View File

@@ -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;

View File

@@ -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 wasnt} other {Invitations werent}} 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.",