forked from Ivasoft/mattermost-mobile
MM-42835_Invite People - add email+user invites
This commit is contained in:
@@ -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]);
|
||||
|
||||
|
||||
@@ -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'
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -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 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 (
|
||||
<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>
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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.",
|
||||
|
||||
Reference in New Issue
Block a user