forked from Ivasoft/mattermost-mobile
MM-42835_Invite People - add email+user invites
This commit is contained in:
@@ -114,23 +114,55 @@ export async function addUserToTeam(serverUrl: string, teamId: string, userId: s
|
||||
}
|
||||
}
|
||||
|
||||
export async function addUsersToTeam(serverUrl: string, teamId: string, userIds: string[]) {
|
||||
export async function addUsersToTeam(serverUrl: string, teamId: string, userIds: string[], fetchOnly = false) {
|
||||
let client;
|
||||
try {
|
||||
client = NetworkManager.getClient(serverUrl);
|
||||
} catch (error) {
|
||||
return {error};
|
||||
}
|
||||
|
||||
try {
|
||||
client = NetworkManager.getClient(serverUrl);
|
||||
EphemeralStore.startAddingToTeam(teamId);
|
||||
|
||||
const members = await client.addUsersToTeamGracefully(teamId, userIds);
|
||||
|
||||
if (!fetchOnly) {
|
||||
setTeamLoading(serverUrl, true);
|
||||
|
||||
const teamMemberships: TeamMembership[] = [];
|
||||
const roles: Record<string, boolean> = {};
|
||||
|
||||
for (const {member} of members) {
|
||||
teamMemberships.push(member);
|
||||
member.roles.split(' ').forEach((role) => {
|
||||
if (!roles[role]) {
|
||||
roles[role] = true;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
fetchRolesIfNeeded(serverUrl, Object.getOwnPropertyNames(roles));
|
||||
|
||||
const operator = DatabaseManager.serverDatabases[serverUrl]?.operator;
|
||||
|
||||
if (operator) {
|
||||
const team = await client.getTeam(teamId);
|
||||
|
||||
const models: Model[] = (await Promise.all([
|
||||
operator.handleTeam({teams: [team], prepareRecordsOnly: true}),
|
||||
operator.handleTeamMemberships({teamMemberships, prepareRecordsOnly: true}),
|
||||
])).flat();
|
||||
|
||||
await operator.batchRecords(models);
|
||||
}
|
||||
|
||||
setTeamLoading(serverUrl, false);
|
||||
}
|
||||
|
||||
EphemeralStore.finishAddingToTeam(teamId);
|
||||
return {members};
|
||||
} catch (error) {
|
||||
EphemeralStore.finishAddingToTeam(teamId);
|
||||
if (client) {
|
||||
EphemeralStore.finishAddingToTeam(teamId);
|
||||
}
|
||||
|
||||
forceLogoutIfNecessary(serverUrl, error as ClientError);
|
||||
return {error};
|
||||
}
|
||||
@@ -138,13 +170,10 @@ export async function addUsersToTeam(serverUrl: string, teamId: string, userIds:
|
||||
|
||||
export async function sendEmailInvitesToTeam(serverUrl: string, teamId: string, emails: string[]) {
|
||||
let client;
|
||||
try {
|
||||
client = NetworkManager.getClient(serverUrl);
|
||||
} catch (error) {
|
||||
return {error};
|
||||
}
|
||||
|
||||
try {
|
||||
client = NetworkManager.getClient(serverUrl);
|
||||
|
||||
const members = await client.sendEmailInvitesToTeamGracefully(teamId, emails);
|
||||
|
||||
return {members};
|
||||
@@ -469,17 +498,46 @@ export async function handleKickFromTeam(serverUrl: string, teamId: string) {
|
||||
}
|
||||
}
|
||||
|
||||
export async function getTeamMembersByIds(serverUrl: string, teamId: string, userIds: string[]) {
|
||||
export async function getTeamMembersByIds(serverUrl: string, teamId: string, userIds: string[], fetchOnly?: boolean) {
|
||||
let client;
|
||||
try {
|
||||
client = NetworkManager.getClient(serverUrl);
|
||||
} catch (error) {
|
||||
return {error};
|
||||
}
|
||||
|
||||
try {
|
||||
client = NetworkManager.getClient(serverUrl);
|
||||
const members = await client.getTeamMembersByIds(teamId, userIds);
|
||||
|
||||
if (!fetchOnly) {
|
||||
setTeamLoading(serverUrl, true);
|
||||
|
||||
const teamMemberships: TeamMembership[] = [];
|
||||
const roles: Record<string, boolean> = {};
|
||||
|
||||
for (const member of members) {
|
||||
teamMemberships.push(member);
|
||||
member.roles.split(' ').forEach((role) => {
|
||||
if (!roles[role]) {
|
||||
roles[role] = true;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
fetchRolesIfNeeded(serverUrl, Object.getOwnPropertyNames(roles));
|
||||
|
||||
const operator = DatabaseManager.serverDatabases[serverUrl]?.operator;
|
||||
|
||||
if (operator) {
|
||||
const team = await client.getTeam(teamId);
|
||||
|
||||
const models: Model[] = (await Promise.all([
|
||||
operator.handleTeam({teams: [team], prepareRecordsOnly: true}),
|
||||
operator.handleTeamMemberships({teamMemberships, prepareRecordsOnly: true}),
|
||||
])).flat();
|
||||
|
||||
await operator.batchRecords(models);
|
||||
}
|
||||
|
||||
setTeamLoading(serverUrl, false);
|
||||
}
|
||||
|
||||
return {members};
|
||||
} catch (error) {
|
||||
forceLogoutIfNecessary(serverUrl, error as ClientError);
|
||||
|
||||
@@ -27,7 +27,7 @@ type Props = {
|
||||
/*
|
||||
* The user that this component represents.
|
||||
*/
|
||||
user: UserProfile|string;
|
||||
user: UserProfile;
|
||||
|
||||
/*
|
||||
* A handler function that will deselect a user when clicked on.
|
||||
@@ -61,16 +61,16 @@ const getStyleFromTheme = makeStyleSheetFromTheme((theme) => {
|
||||
justifyContent: 'center',
|
||||
marginLeft: 7,
|
||||
},
|
||||
profileContainer: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
marginRight: 8,
|
||||
color: theme.centerChannelColor,
|
||||
},
|
||||
text: {
|
||||
marginLeft: 8,
|
||||
color: theme.centerChannelColor,
|
||||
...typography('Body', 100, 'SemiBold'),
|
||||
},
|
||||
picture: {
|
||||
width: 20,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
@@ -84,42 +84,36 @@ export default function SelectedUser({
|
||||
const style = getStyleFromTheme(theme);
|
||||
const intl = useIntl();
|
||||
|
||||
const isProfile = typeof user !== 'string';
|
||||
const id = isProfile ? user.id : user;
|
||||
|
||||
const onPress = useCallback(() => {
|
||||
onRemove(id);
|
||||
}, [onRemove, id]);
|
||||
|
||||
const userItemTestID = `${testID}.${id}`;
|
||||
onRemove(user.id);
|
||||
}, [onRemove, user.id]);
|
||||
|
||||
const userItemTestID = `${testID}.${user.id}`;
|
||||
return (
|
||||
<Animated.View
|
||||
entering={FadeIn.duration(FADE_DURATION)}
|
||||
exiting={FadeOut.duration(FADE_DURATION)}
|
||||
style={style.container}
|
||||
testID={`${userItemTestID}`}
|
||||
testID={`${testID}.${user.id}`}
|
||||
>
|
||||
{isProfile && (
|
||||
<View style={style.picture}>
|
||||
<ProfilePicture
|
||||
author={user}
|
||||
size={20}
|
||||
iconSize={20}
|
||||
testID={`${userItemTestID}.profile_picture`}
|
||||
/>
|
||||
</View>
|
||||
)}
|
||||
<View style={style.profileContainer}>
|
||||
<ProfilePicture
|
||||
author={user}
|
||||
size={20}
|
||||
iconSize={20}
|
||||
testID={`${userItemTestID}.profile_picture`}
|
||||
/>
|
||||
</View>
|
||||
<Text
|
||||
style={style.text}
|
||||
testID={`${userItemTestID}.display_name`}
|
||||
testID={`${testID}.${user.id}.display_name`}
|
||||
>
|
||||
{isProfile ? displayUsername(user, intl.locale, teammateNameDisplay) : id}
|
||||
{displayUsername(user, intl.locale, teammateNameDisplay)}
|
||||
</Text>
|
||||
<TouchableOpacity
|
||||
style={style.remove}
|
||||
onPress={onPress}
|
||||
testID={`${userItemTestID}.remove.button`}
|
||||
testID={`${testID}.${user.id}.remove.button`}
|
||||
>
|
||||
<CompassIcon
|
||||
name='close-circle'
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
import React, {useMemo} from 'react';
|
||||
import {IntlShape, useIntl} from 'react-intl';
|
||||
import {StyleProp, Text, View, ViewStyle} from 'react-native';
|
||||
|
||||
@@ -121,6 +121,10 @@ const UserItem = ({
|
||||
}
|
||||
}
|
||||
|
||||
const usernameTextStyle = useMemo(() => {
|
||||
return [style.rowUsername, {flexShrink: rowUsernameFlexShrink}];
|
||||
}, [user, rowUsernameFlexShrink]);
|
||||
|
||||
return (
|
||||
<View
|
||||
style={[style.row, containerStyle]}
|
||||
@@ -158,7 +162,7 @@ const UserItem = ({
|
||||
}
|
||||
{Boolean(user) && (
|
||||
<Text
|
||||
style={{...style.rowUsername, flexShrink: rowUsernameFlexShrink}}
|
||||
style={usernameTextStyle}
|
||||
numberOfLines={1}
|
||||
testID={`${userItemTestId}.username`}
|
||||
>
|
||||
|
||||
@@ -26,7 +26,7 @@ const PlusMenuList = ({canCreateChannels, canJoinChannels, canInvitePeople}: Pro
|
||||
await dismissBottomSheet();
|
||||
|
||||
const title = intl.formatMessage({id: 'browse_channels.title', defaultMessage: 'Browse channels'});
|
||||
const closeButton = await CompassIcon.getImageSource('close', 24, theme.sidebarHeaderTextColor);
|
||||
const closeButton = CompassIcon.getImageSourceSync('close', 24, theme.sidebarHeaderTextColor);
|
||||
|
||||
showModal(Screens.BROWSE_CHANNELS, title, {
|
||||
closeButton,
|
||||
@@ -44,7 +44,7 @@ const PlusMenuList = ({canCreateChannels, canJoinChannels, canInvitePeople}: Pro
|
||||
await dismissBottomSheet();
|
||||
|
||||
const title = intl.formatMessage({id: 'create_direct_message.title', defaultMessage: 'Create Direct Message'});
|
||||
const closeButton = await CompassIcon.getImageSource('close', 24, theme.sidebarHeaderTextColor);
|
||||
const closeButton = CompassIcon.getImageSourceSync('close', 24, theme.sidebarHeaderTextColor);
|
||||
showModal(Screens.CREATE_DIRECT_MESSAGE, title, {
|
||||
closeButton,
|
||||
});
|
||||
@@ -54,12 +54,13 @@ const PlusMenuList = ({canCreateChannels, canJoinChannels, canInvitePeople}: Pro
|
||||
await dismissBottomSheet();
|
||||
|
||||
const title = intl.formatMessage({id: 'invite.title', defaultMessage: 'Invite'});
|
||||
const closeButton = await CompassIcon.getImageSource('close', 24, theme.sidebarHeaderTextColor);
|
||||
const closeButton = CompassIcon.getImageSourceSync('close', 24, theme.sidebarHeaderTextColor);
|
||||
const closeButtonId = 'close-invite';
|
||||
|
||||
showModal(
|
||||
Screens.INVITE,
|
||||
title,
|
||||
{closeButton},
|
||||
{closeButton, closeButtonId},
|
||||
);
|
||||
}, [intl, theme]);
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ import {SafeAreaView} from 'react-native-safe-area-context';
|
||||
import {getTeamMembersByIds, addUsersToTeam, sendEmailInvitesToTeam} from '@actions/remote/team';
|
||||
import {searchProfiles} from '@actions/remote/user';
|
||||
import Loading from '@components/loading';
|
||||
import {General, ServerErrors} from '@constants';
|
||||
import {ServerErrors} from '@constants';
|
||||
import {useServerUrl} from '@context/server';
|
||||
import {useTheme} from '@context/theme';
|
||||
import {useModalPosition} from '@hooks/device';
|
||||
@@ -27,6 +27,7 @@ import type {NavButtons} from '@typings/screens/navigation';
|
||||
|
||||
const CLOSE_BUTTON_ID = 'close-invite';
|
||||
const SEND_BUTTON_ID = 'send-invite';
|
||||
const SEARCH_TIMEOUT_MILLISECONDS = 200;
|
||||
|
||||
const makeLeftButton = (icon: ImageResource): OptionsTopBarButton => {
|
||||
return {
|
||||
@@ -124,6 +125,7 @@ export default function Invite({
|
||||
const [result, setResult] = useState<Result>({sent: [], notSent: []});
|
||||
const [wrapperHeight, setWrapperHeight] = useState(0);
|
||||
const [stage, setStage] = useState(Stage.SELECTION);
|
||||
const [sendError, setSendError] = useState('');
|
||||
|
||||
const selectedCount = Object.keys(selectedIds).length;
|
||||
|
||||
@@ -153,8 +155,8 @@ export default function Invite({
|
||||
const {data} = await searchProfiles(serverUrl, searchTerm.toLowerCase(), {allow_inactive: true});
|
||||
const results: SearchResult[] = data ?? [];
|
||||
|
||||
if (isEmail(searchTerm.trim())) {
|
||||
results.unshift(searchTerm.trim() as EmailInvite);
|
||||
if (!results.length && isEmail(searchTerm.trim())) {
|
||||
results.push(searchTerm.trim() as EmailInvite);
|
||||
}
|
||||
|
||||
setSearchResults(results);
|
||||
@@ -176,7 +178,7 @@ export default function Invite({
|
||||
searchTimeoutId.current = setTimeout(async () => {
|
||||
await searchUsers(text);
|
||||
setLoading(false);
|
||||
}, General.SEARCH_TIMEOUT_MILLISECONDS);
|
||||
}, SEARCH_TIMEOUT_MILLISECONDS);
|
||||
}, [searchUsers]);
|
||||
|
||||
const handleSelectItem = useCallback((item: SearchResult) => {
|
||||
@@ -193,6 +195,12 @@ export default function Invite({
|
||||
handleClearSearch();
|
||||
}, [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: []});
|
||||
setStage(Stage.RESULT);
|
||||
};
|
||||
|
||||
const handleSend = async () => {
|
||||
if (!selectedCount) {
|
||||
return;
|
||||
@@ -211,11 +219,19 @@ export default function Invite({
|
||||
}
|
||||
}
|
||||
|
||||
const {members: currentTeamMembers = []} = await getTeamMembersByIds(serverUrl, teamId, userIds);
|
||||
const currentMemberIds: Record<string, boolean> = {};
|
||||
|
||||
for (const {user_id: currentMemberId} of currentTeamMembers) {
|
||||
currentMemberIds[currentMemberId] = true;
|
||||
if (userIds.length) {
|
||||
const {members: currentTeamMembers = [], error: getTeamMembersByIdsError} = await getTeamMembersByIds(serverUrl, teamId, userIds);
|
||||
|
||||
if (getTeamMembersByIdsError) {
|
||||
handleSendError();
|
||||
return;
|
||||
}
|
||||
|
||||
for (const {user_id: currentMemberId} of currentTeamMembers) {
|
||||
currentMemberIds[currentMemberId] = true;
|
||||
}
|
||||
}
|
||||
|
||||
const sent: InviteResult[] = [];
|
||||
@@ -233,7 +249,12 @@ export default function Invite({
|
||||
}
|
||||
|
||||
if (usersToAdd.length) {
|
||||
const {members} = await addUsersToTeam(serverUrl, teamId, usersToAdd);
|
||||
const {members, error: addUsersToTeamError} = await addUsersToTeam(serverUrl, teamId, usersToAdd);
|
||||
|
||||
if (addUsersToTeamError) {
|
||||
handleSendError();
|
||||
return;
|
||||
}
|
||||
|
||||
if (members) {
|
||||
const membersWithError: Record<string, string> = {};
|
||||
@@ -254,7 +275,12 @@ export default function Invite({
|
||||
}
|
||||
|
||||
if (emails.length) {
|
||||
const {members} = await sendEmailInvitesToTeam(serverUrl, teamId, emails);
|
||||
const {members, error: sendEmailInvitesToTeamError} = await sendEmailInvitesToTeam(serverUrl, teamId, emails);
|
||||
|
||||
if (sendEmailInvitesToTeamError) {
|
||||
handleSendError();
|
||||
return;
|
||||
}
|
||||
|
||||
if (members) {
|
||||
const membersWithError: Record<string, string> = {};
|
||||
@@ -320,6 +346,7 @@ export default function Invite({
|
||||
<Summary
|
||||
result={result}
|
||||
selectedIds={selectedIds}
|
||||
error={sendError}
|
||||
onClose={closeModal}
|
||||
testID='invite.screen.summary'
|
||||
/>
|
||||
|
||||
88
app/screens/invite/selected_email.tsx
Normal file
88
app/screens/invite/selected_email.tsx
Normal file
@@ -0,0 +1,88 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React, {useCallback} from 'react';
|
||||
import {Text, TouchableOpacity} from 'react-native';
|
||||
import Animated, {FadeIn, FadeOut} from 'react-native-reanimated';
|
||||
|
||||
import CompassIcon from '@components/compass_icon';
|
||||
import {useTheme} from '@context/theme';
|
||||
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
|
||||
import {typography} from '@utils/typography';
|
||||
|
||||
type Props = {
|
||||
email: string;
|
||||
onRemove: (id: string) => void;
|
||||
testID?: string;
|
||||
}
|
||||
|
||||
export const USER_CHIP_HEIGHT = 32;
|
||||
export const USER_CHIP_BOTTOM_MARGIN = 8;
|
||||
const FADE_DURATION = 100;
|
||||
|
||||
const getStyleFromTheme = makeStyleSheetFromTheme((theme) => {
|
||||
return {
|
||||
container: {
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
flexDirection: 'row',
|
||||
borderRadius: 16,
|
||||
height: USER_CHIP_HEIGHT,
|
||||
backgroundColor: changeOpacity(theme.centerChannelColor, 0.08),
|
||||
marginBottom: USER_CHIP_BOTTOM_MARGIN,
|
||||
marginRight: 8,
|
||||
paddingHorizontal: 7,
|
||||
},
|
||||
remove: {
|
||||
justifyContent: 'center',
|
||||
marginLeft: 7,
|
||||
},
|
||||
text: {
|
||||
marginLeft: 8,
|
||||
color: theme.centerChannelColor,
|
||||
...typography('Body', 100, 'SemiBold'),
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
export default function SelectedEmail({
|
||||
email,
|
||||
onRemove,
|
||||
testID,
|
||||
}: Props) {
|
||||
const theme = useTheme();
|
||||
const style = getStyleFromTheme(theme);
|
||||
|
||||
const onPress = useCallback(() => {
|
||||
onRemove(email);
|
||||
}, [onRemove, email]);
|
||||
|
||||
const selectedEmailTestID = `${testID}.${email}`;
|
||||
|
||||
return (
|
||||
<Animated.View
|
||||
entering={FadeIn.duration(FADE_DURATION)}
|
||||
exiting={FadeOut.duration(FADE_DURATION)}
|
||||
style={style.container}
|
||||
testID={`${selectedEmailTestID}`}
|
||||
>
|
||||
<Text
|
||||
style={style.text}
|
||||
testID={`${selectedEmailTestID}.display_name`}
|
||||
>
|
||||
{email}
|
||||
</Text>
|
||||
<TouchableOpacity
|
||||
style={style.remove}
|
||||
onPress={onPress}
|
||||
testID={`${selectedEmailTestID}.remove.button`}
|
||||
>
|
||||
<CompassIcon
|
||||
name='close-circle'
|
||||
size={18}
|
||||
color={changeOpacity(theme.centerChannelColor, 0.32)}
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
</Animated.View>
|
||||
);
|
||||
}
|
||||
@@ -2,37 +2,37 @@
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React, {useCallback, useState, useMemo} from 'react';
|
||||
import {useIntl} from 'react-intl';
|
||||
import {Keyboard, Platform, View, Text, TouchableOpacity, LayoutChangeEvent, useWindowDimensions, FlatList, ListRenderItemInfo, ScrollView} from 'react-native';
|
||||
import {
|
||||
Keyboard,
|
||||
Platform,
|
||||
View,
|
||||
LayoutChangeEvent,
|
||||
useWindowDimensions,
|
||||
FlatList,
|
||||
ListRenderItemInfo,
|
||||
ScrollView,
|
||||
} from 'react-native';
|
||||
import Animated, {useDerivedValue} from 'react-native-reanimated';
|
||||
import {useSafeAreaInsets} from 'react-native-safe-area-context';
|
||||
import Share from 'react-native-share';
|
||||
import {ShareOptions} from 'react-native-share/lib/typescript/types';
|
||||
|
||||
import CompassIcon from '@components/compass_icon';
|
||||
import FloatingTextInput from '@components/floating_text_input_label';
|
||||
import FormattedText from '@components/formatted_text';
|
||||
import SelectedUser from '@components/selected_users/selected_user';
|
||||
import TeamIcon from '@components/team_sidebar/team_list/team_item/team_icon';
|
||||
import TouchableWithFeedback from '@components/touchable_with_feedback';
|
||||
import UserItem from '@components/user_item';
|
||||
import {MAX_LIST_HEIGHT, MAX_LIST_TABLET_DIFF} from '@constants/autocomplete';
|
||||
import {useServerDisplayName} from '@context/server';
|
||||
import {useTheme} from '@context/theme';
|
||||
import {useAutocompleteDefaultAnimatedValues} from '@hooks/autocomplete';
|
||||
import {useIsTablet, useKeyboardHeight} from '@hooks/device';
|
||||
import {preventDoubleTap} from '@utils/tap';
|
||||
import {makeStyleSheetFromTheme, changeOpacity, getKeyboardAppearanceFromTheme} from '@utils/theme';
|
||||
import {makeStyleSheetFromTheme, changeOpacity} from '@utils/theme';
|
||||
import {typography} from '@utils/typography';
|
||||
|
||||
import {SearchResult} from './invite';
|
||||
import SelectedEmail from './selected_email';
|
||||
import SelectionSearchBar from './selection_search_bar';
|
||||
import SelectionTeamBar from './selection_team_bar';
|
||||
import TextItem, {TextItemType} from './text_item';
|
||||
|
||||
const AUTOCOMPLETE_ADJUST = 5;
|
||||
const BOTTOM_AUTOCOMPLETE_SEPARATION = 10;
|
||||
const SEARCH_BAR_TITLE_MARGIN_TOP = 24;
|
||||
const SEARCH_BAR_MARGIN_TOP = 16;
|
||||
const SEARCH_BAR_BORDER = 2;
|
||||
const KEYBOARD_HEIGHT_ADJUST = 3;
|
||||
|
||||
const INITIAL_BATCH_TO_RENDER = 15;
|
||||
const SCROLL_EVENT_THROTTLE = 60;
|
||||
@@ -104,16 +104,6 @@ const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => {
|
||||
shareLinkIcon: {
|
||||
color: theme.buttonBg,
|
||||
},
|
||||
searchBarTitleText: {
|
||||
marginHorizontal: 20,
|
||||
marginTop: SEARCH_BAR_TITLE_MARGIN_TOP,
|
||||
color: theme.centerChannelColor,
|
||||
...typography('Heading', 700, 'SemiBold'),
|
||||
},
|
||||
searchBar: {
|
||||
marginHorizontal: 20,
|
||||
marginTop: SEARCH_BAR_MARGIN_TOP,
|
||||
},
|
||||
searchList: {
|
||||
left: 20,
|
||||
right: 20,
|
||||
@@ -123,10 +113,12 @@ const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => {
|
||||
searchListBorder: {
|
||||
borderWidth: 1,
|
||||
borderColor: changeOpacity(theme.centerChannelColor, 0.2),
|
||||
overflow: 'hidden',
|
||||
borderRadius: 4,
|
||||
elevation: 3,
|
||||
},
|
||||
searchListPadding: {
|
||||
paddingVertical: 8,
|
||||
},
|
||||
searchListShadow: {
|
||||
shadowColor: '#000',
|
||||
shadowOpacity: 0.12,
|
||||
@@ -193,98 +185,31 @@ export default function Selection({
|
||||
onRemoveItem,
|
||||
onClose,
|
||||
}: SelectionProps) {
|
||||
const {formatMessage} = useIntl();
|
||||
const theme = useTheme();
|
||||
const styles = getStyleSheet(theme);
|
||||
const serverDisplayName = useServerDisplayName();
|
||||
const dimensions = useWindowDimensions();
|
||||
const insets = useSafeAreaInsets();
|
||||
const isTablet = useIsTablet();
|
||||
const keyboardHeight = useKeyboardHeight();
|
||||
|
||||
const [headerFieldHeight, setHeaderFieldHeight] = useState(0);
|
||||
const [teamBarHeight, setTeamBarHeight] = useState(0);
|
||||
const [searchBarHeight, setSearchBarHeight] = useState(0);
|
||||
const [searchBarTitleHeight, setSearchBarTitleHeight] = useState(0);
|
||||
|
||||
const onLayoutHeader = useCallback((e: LayoutChangeEvent) => {
|
||||
setHeaderFieldHeight(e.nativeEvent.layout.height);
|
||||
const onLayoutSelectionTeamBar = useCallback((e: LayoutChangeEvent) => {
|
||||
setTeamBarHeight(e.nativeEvent.layout.height);
|
||||
}, []);
|
||||
|
||||
const onLayoutSearchBar = useCallback((e: LayoutChangeEvent) => {
|
||||
setSearchBarHeight(e.nativeEvent.layout.height);
|
||||
}, []);
|
||||
|
||||
const onLayoutSearchBarTitle = useCallback((e: LayoutChangeEvent) => {
|
||||
setSearchBarTitleHeight(e.nativeEvent.layout.height);
|
||||
}, []);
|
||||
|
||||
const handleSearchChange = (text: string) => {
|
||||
onSearchChange(text);
|
||||
};
|
||||
|
||||
const handleOnRemoveItem = (id: string) => {
|
||||
onRemoveItem(id);
|
||||
};
|
||||
|
||||
const handleOnShareLink = async () => {
|
||||
const url = `${serverUrl}/signup_user_complete/?id=${teamInviteId}`;
|
||||
const title = formatMessage({id: 'invite_people_to_team.title', defaultMessage: 'Join the {team} team'}, {team: teamDisplayName});
|
||||
const message = formatMessage({id: 'invite_people_to_team.message', defaultMessage: 'Here’s a link to collaborate and communicate with us on Mattermost.'});
|
||||
const icon = 'data:<data_type>/<file_extension>;base64,<base64_data>';
|
||||
|
||||
const options: ShareOptions = Platform.select({
|
||||
ios: {
|
||||
activityItemSources: [
|
||||
{
|
||||
placeholderItem: {
|
||||
type: 'url',
|
||||
content: url,
|
||||
},
|
||||
item: {
|
||||
default: {
|
||||
type: 'text',
|
||||
content: `${message} ${url}`,
|
||||
},
|
||||
copyToPasteBoard: {
|
||||
type: 'url',
|
||||
content: url,
|
||||
},
|
||||
},
|
||||
subject: {
|
||||
default: title,
|
||||
},
|
||||
linkMetadata: {
|
||||
originalUrl: url,
|
||||
url,
|
||||
title,
|
||||
icon,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
default: {
|
||||
title,
|
||||
subject: title,
|
||||
url,
|
||||
showAppsToView: true,
|
||||
},
|
||||
});
|
||||
|
||||
await onClose();
|
||||
|
||||
Share.open(
|
||||
options,
|
||||
).catch(() => {
|
||||
// do nothing
|
||||
});
|
||||
};
|
||||
|
||||
const handleShareLink = useCallback(preventDoubleTap(() => handleOnShareLink()), []);
|
||||
|
||||
const bottomSpace = dimensions.height - wrapperHeight - modalPosition;
|
||||
const otherElementsSize = headerFieldHeight + searchBarHeight + searchBarTitleHeight +
|
||||
SEARCH_BAR_MARGIN_TOP + SEARCH_BAR_TITLE_MARGIN_TOP + SEARCH_BAR_BORDER;
|
||||
const insetsAdjust = keyboardHeight || insets.bottom;
|
||||
const otherElementsSize = teamBarHeight + searchBarHeight;
|
||||
const insetsAdjust = (keyboardHeight + KEYBOARD_HEIGHT_ADJUST) || insets.bottom;
|
||||
|
||||
const keyboardOverlap = Platform.select({
|
||||
ios: isTablet ? (
|
||||
@@ -301,11 +226,11 @@ export default function Selection({
|
||||
|
||||
const workingSpace = wrapperHeight - keyboardOverlap;
|
||||
const spaceOnTop = otherElementsSize - AUTOCOMPLETE_ADJUST;
|
||||
const spaceOnBottom = workingSpace - (otherElementsSize + BOTTOM_AUTOCOMPLETE_SEPARATION);
|
||||
const spaceOnBottom = workingSpace - (otherElementsSize + keyboardAdjust);
|
||||
const autocompletePosition = spaceOnBottom > spaceOnTop ? (
|
||||
otherElementsSize
|
||||
) : (
|
||||
(workingSpace + AUTOCOMPLETE_ADJUST + keyboardAdjust) - otherElementsSize
|
||||
workingSpace - otherElementsSize
|
||||
);
|
||||
const autocompleteAvailableSpace = spaceOnBottom > spaceOnTop ? spaceOnBottom : spaceOnTop;
|
||||
const isLandscape = dimensions.width > dimensions.height;
|
||||
@@ -316,7 +241,7 @@ export default function Selection({
|
||||
|
||||
const maxHeight = useDerivedValue(() => {
|
||||
return Math.min(animatedAutocompleteAvailableSpace.value, defaultMaxHeight);
|
||||
}, [defaultMaxHeight]);
|
||||
}, [animatedAutocompleteAvailableSpace, defaultMaxHeight]);
|
||||
|
||||
const searchListContainerStyle = useMemo(() => {
|
||||
const style = [];
|
||||
@@ -329,14 +254,22 @@ export default function Selection({
|
||||
},
|
||||
);
|
||||
|
||||
if (searchResults.length) {
|
||||
style.push(styles.searchListBorder);
|
||||
}
|
||||
|
||||
if (Platform.OS === 'ios') {
|
||||
style.push(styles.searchListShadow);
|
||||
}
|
||||
|
||||
return style;
|
||||
}, [searchResults, styles, animatedAutocompletePosition, maxHeight]);
|
||||
|
||||
const searchListFlatListStyle = useMemo(() => {
|
||||
const style = [];
|
||||
|
||||
style.push(styles.searchListFlatList);
|
||||
|
||||
if (searchResults.length) {
|
||||
style.push(styles.searchListBorder, styles.searchListPadding);
|
||||
}
|
||||
|
||||
return style;
|
||||
}, [searchResults, styles]);
|
||||
|
||||
@@ -346,11 +279,13 @@ export default function Selection({
|
||||
}
|
||||
|
||||
return (
|
||||
<TextItem
|
||||
text={term}
|
||||
type={TextItemType.SEARCH_NO_RESULTS}
|
||||
testID='invite.search_list_no_results'
|
||||
/>
|
||||
<View style={[styles.searchListBorder, styles.searchListPadding]}>
|
||||
<TextItem
|
||||
text={term}
|
||||
type={TextItemType.SEARCH_NO_RESULTS}
|
||||
testID='invite.search_list_no_results'
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
}, [term, loading]);
|
||||
|
||||
@@ -386,15 +321,24 @@ export default function Selection({
|
||||
const selectedItems = [];
|
||||
|
||||
for (const id of Object.keys(selectedIds)) {
|
||||
selectedItems.push(
|
||||
const selectedItem = selectedIds[id];
|
||||
|
||||
selectedItems.push(typeof selectedItem === 'string' ? (
|
||||
<SelectedEmail
|
||||
key={id}
|
||||
email={selectedItem}
|
||||
onRemove={handleOnRemoveItem}
|
||||
testID='invite.selected_item'
|
||||
/>
|
||||
) : (
|
||||
<SelectedUser
|
||||
key={id}
|
||||
user={selectedIds[id]}
|
||||
user={selectedItem}
|
||||
teammateNameDisplay={teammateNameDisplay}
|
||||
onRemove={handleOnRemoveItem}
|
||||
testID='invite.selected_item'
|
||||
/>,
|
||||
);
|
||||
/>
|
||||
));
|
||||
}
|
||||
|
||||
return selectedItems;
|
||||
@@ -405,84 +349,20 @@ export default function Selection({
|
||||
style={styles.container}
|
||||
testID={testID}
|
||||
>
|
||||
<View
|
||||
style={styles.teamContainer}
|
||||
onLayout={onLayoutHeader}
|
||||
>
|
||||
<View style={styles.iconContainer}>
|
||||
<TeamIcon
|
||||
id={teamId}
|
||||
displayName={teamDisplayName}
|
||||
lastIconUpdate={teamLastIconUpdate}
|
||||
selected={false}
|
||||
textColor={theme.centerChannelColor}
|
||||
backgroundColor={changeOpacity(theme.centerChannelColor, 0.16)}
|
||||
testID='invite.team_icon'
|
||||
/>
|
||||
</View>
|
||||
<View style={styles.textContainer}>
|
||||
<Text
|
||||
style={styles.teamText}
|
||||
numberOfLines={1}
|
||||
testID='invite.team_display_name'
|
||||
>
|
||||
{teamDisplayName}
|
||||
</Text>
|
||||
<Text
|
||||
style={styles.serverText}
|
||||
numberOfLines={1}
|
||||
testID='invite.server_display_name'
|
||||
>
|
||||
{serverDisplayName}
|
||||
</Text>
|
||||
</View>
|
||||
<TouchableOpacity
|
||||
onPress={handleShareLink}
|
||||
style={styles.shareLink}
|
||||
>
|
||||
<View
|
||||
style={styles.shareLinkButton}
|
||||
testID='invite.share_link.button'
|
||||
>
|
||||
<CompassIcon
|
||||
name='export-variant'
|
||||
size={18}
|
||||
style={styles.shareLinkIcon}
|
||||
/>
|
||||
<FormattedText
|
||||
id='invite.shareLink'
|
||||
defaultMessage='Share link'
|
||||
style={styles.shareLinkText}
|
||||
/>
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
<FormattedText
|
||||
id='invite.sendInvitationsTo'
|
||||
defaultMessage='Send invitations to…'
|
||||
style={styles.searchBarTitleText}
|
||||
onLayout={onLayoutSearchBarTitle}
|
||||
testID='invite.search_bar_title'
|
||||
<SelectionTeamBar
|
||||
teamId={teamId}
|
||||
teamDisplayName={teamDisplayName}
|
||||
teamLastIconUpdate={teamLastIconUpdate}
|
||||
teamInviteId={teamInviteId}
|
||||
serverUrl={serverUrl}
|
||||
onLayoutContainer={onLayoutSelectionTeamBar}
|
||||
onClose={onClose}
|
||||
/>
|
||||
<SelectionSearchBar
|
||||
term={term}
|
||||
onSearchChange={onSearchChange}
|
||||
onLayoutContainer={onLayoutSearchBar}
|
||||
/>
|
||||
<View
|
||||
style={styles.searchBar}
|
||||
onLayout={onLayoutSearchBar}
|
||||
>
|
||||
<FloatingTextInput
|
||||
autoCorrect={false}
|
||||
autoCapitalize={'none'}
|
||||
blurOnSubmit={false}
|
||||
disableFullscreenUI={true}
|
||||
enablesReturnKeyAutomatically={true}
|
||||
placeholder={formatMessage({id: 'invite.searchPlaceholder', defaultMessage: 'Type a name or email address…'})}
|
||||
onChangeText={handleSearchChange}
|
||||
keyboardAppearance={getKeyboardAppearanceFromTheme(theme)}
|
||||
returnKeyType='search'
|
||||
value={term}
|
||||
theme={theme}
|
||||
testID='invite.search_bar_input'
|
||||
/>
|
||||
</View>
|
||||
{Object.keys(selectedIds).length > 0 && (
|
||||
<ScrollView
|
||||
style={styles.selectedItems}
|
||||
@@ -503,8 +383,8 @@ export default function Selection({
|
||||
maxToRenderPerBatch={INITIAL_BATCH_TO_RENDER + 1}
|
||||
renderItem={renderItem}
|
||||
scrollEventThrottle={SCROLL_EVENT_THROTTLE}
|
||||
style={styles.searchListFlatList}
|
||||
testID='invite.search_list'
|
||||
style={searchListFlatListStyle}
|
||||
/>
|
||||
</Animated.View>
|
||||
</View>
|
||||
|
||||
131
app/screens/invite/selection_search_bar.tsx
Normal file
131
app/screens/invite/selection_search_bar.tsx
Normal file
@@ -0,0 +1,131 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React, {useCallback, useState, useMemo} from 'react';
|
||||
import {useIntl} from 'react-intl';
|
||||
import {View, TextInput, LayoutChangeEvent} from 'react-native';
|
||||
|
||||
import FormattedText from '@components/formatted_text';
|
||||
import {useTheme} from '@context/theme';
|
||||
import {makeStyleSheetFromTheme, changeOpacity, getKeyboardAppearanceFromTheme} from '@utils/theme';
|
||||
import {typography} from '@utils/typography';
|
||||
|
||||
const SEARCH_BAR_TITLE_MARGIN_TOP = 24;
|
||||
const SEARCH_BAR_MARGIN_TOP = 16;
|
||||
|
||||
const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => {
|
||||
return {
|
||||
container: {
|
||||
display: 'flex',
|
||||
},
|
||||
searchBarTitleText: {
|
||||
marginHorizontal: 20,
|
||||
marginTop: SEARCH_BAR_TITLE_MARGIN_TOP,
|
||||
color: theme.centerChannelColor,
|
||||
...typography('Heading', 700, 'SemiBold'),
|
||||
},
|
||||
searchBar: {
|
||||
marginHorizontal: 20,
|
||||
marginTop: SEARCH_BAR_MARGIN_TOP,
|
||||
},
|
||||
searchInput: {
|
||||
height: 48,
|
||||
backgroundColor: 'transparent',
|
||||
...typography('Body', 200, 'Regular'),
|
||||
lineHeight: 20,
|
||||
color: theme.centerChannelColor,
|
||||
borderWidth: 1,
|
||||
borderColor: changeOpacity(theme.centerChannelColor, 0.16),
|
||||
borderRadius: 4,
|
||||
paddingHorizontal: 16,
|
||||
},
|
||||
searchInputPlaceholder: {
|
||||
color: changeOpacity(theme.centerChannelColor, 0.64),
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
type SelectionSearchBarProps = {
|
||||
term: string;
|
||||
onSearchChange: (text: string) => void;
|
||||
onLayoutContainer: (e: LayoutChangeEvent) => void;
|
||||
}
|
||||
|
||||
export default function SelectionSearchBar({
|
||||
term,
|
||||
onSearchChange,
|
||||
onLayoutContainer,
|
||||
}: SelectionSearchBarProps) {
|
||||
const {formatMessage} = useIntl();
|
||||
const theme = useTheme();
|
||||
const styles = getStyleSheet(theme);
|
||||
const [isFocused, setIsFocused] = useState(false);
|
||||
|
||||
const onLayoutSearchBar = useCallback((e: LayoutChangeEvent) => {
|
||||
onLayoutContainer(e);
|
||||
}, [onLayoutContainer]);
|
||||
|
||||
const onTextInputFocus = () => {
|
||||
setIsFocused(true);
|
||||
};
|
||||
|
||||
const onTextInputBlur = () => {
|
||||
setIsFocused(false);
|
||||
};
|
||||
|
||||
const handleSearchChange = (text: string) => {
|
||||
onSearchChange(text);
|
||||
};
|
||||
|
||||
const searchInputStyle = useMemo(() => {
|
||||
const style = [];
|
||||
|
||||
style.push(styles.searchInput);
|
||||
|
||||
if (isFocused) {
|
||||
style.push({
|
||||
borderWidth: 2,
|
||||
borderColor: theme.buttonBg,
|
||||
});
|
||||
}
|
||||
|
||||
return style;
|
||||
}, [isFocused, styles]);
|
||||
|
||||
return (
|
||||
<View
|
||||
style={styles.container}
|
||||
onLayout={onLayoutSearchBar}
|
||||
testID='invite.search_bar'
|
||||
>
|
||||
<FormattedText
|
||||
id='invite.sendInvitationsTo'
|
||||
defaultMessage='Send invitations to…'
|
||||
style={styles.searchBarTitleText}
|
||||
testID='invite.search_bar_title'
|
||||
/>
|
||||
<View style={styles.searchBar}>
|
||||
<TextInput
|
||||
autoCorrect={false}
|
||||
autoCapitalize={'none'}
|
||||
autoFocus={true}
|
||||
blurOnSubmit={false}
|
||||
disableFullscreenUI={true}
|
||||
enablesReturnKeyAutomatically={true}
|
||||
returnKeyType='search'
|
||||
style={searchInputStyle}
|
||||
placeholder={formatMessage({id: 'invite.searchPlaceholder', defaultMessage: 'Type a name or email address…'})}
|
||||
placeholderTextColor={styles.searchInputPlaceholder.color}
|
||||
onChangeText={handleSearchChange}
|
||||
onFocus={onTextInputFocus}
|
||||
onBlur={onTextInputBlur}
|
||||
keyboardAppearance={getKeyboardAppearanceFromTheme(theme)}
|
||||
value={term}
|
||||
pointerEvents='auto'
|
||||
underlineColorAndroid='transparent'
|
||||
testID='invite.search_bar_input'
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
215
app/screens/invite/selection_team_bar.tsx
Normal file
215
app/screens/invite/selection_team_bar.tsx
Normal file
@@ -0,0 +1,215 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React, {useCallback} from 'react';
|
||||
import {useIntl} from 'react-intl';
|
||||
import {
|
||||
Platform,
|
||||
View,
|
||||
Text,
|
||||
TouchableOpacity,
|
||||
LayoutChangeEvent,
|
||||
} from 'react-native';
|
||||
import Share from 'react-native-share';
|
||||
import {ShareOptions} from 'react-native-share/lib/typescript/types';
|
||||
|
||||
import CompassIcon from '@components/compass_icon';
|
||||
import FormattedText from '@components/formatted_text';
|
||||
import TeamIcon from '@components/team_sidebar/team_list/team_item/team_icon';
|
||||
import {useServerDisplayName} from '@context/server';
|
||||
import {useTheme} from '@context/theme';
|
||||
import {preventDoubleTap} from '@utils/tap';
|
||||
import {makeStyleSheetFromTheme, changeOpacity} from '@utils/theme';
|
||||
import {typography} from '@utils/typography';
|
||||
|
||||
const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => {
|
||||
return {
|
||||
container: {
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
width: '100%',
|
||||
paddingVertical: 16,
|
||||
paddingHorizontal: 20,
|
||||
backgroundColor: changeOpacity(theme.centerChannelColor, 0.04),
|
||||
},
|
||||
iconContainer: {
|
||||
width: 40,
|
||||
height: 40,
|
||||
},
|
||||
textContainer: {
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
},
|
||||
teamText: {
|
||||
color: theme.centerChannelColor,
|
||||
marginLeft: 12,
|
||||
...typography('Body', 200, 'SemiBold'),
|
||||
},
|
||||
serverText: {
|
||||
color: changeOpacity(theme.centerChannelColor, 0.72),
|
||||
marginLeft: 12,
|
||||
...typography('Body', 75, 'Regular'),
|
||||
},
|
||||
shareLink: {
|
||||
display: 'flex',
|
||||
marginLeft: 'auto',
|
||||
},
|
||||
shareLinkButton: {
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
height: 40,
|
||||
paddingHorizontal: 20,
|
||||
backgroundColor: changeOpacity(theme.buttonBg, 0.08),
|
||||
borderRadius: 4,
|
||||
},
|
||||
shareLinkText: {
|
||||
color: theme.buttonBg,
|
||||
...typography('Body', 100, 'SemiBold'),
|
||||
paddingLeft: 7,
|
||||
},
|
||||
shareLinkIcon: {
|
||||
color: theme.buttonBg,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
type SelectionTeamBarProps = {
|
||||
teamId: string;
|
||||
teamDisplayName: string;
|
||||
teamLastIconUpdate: number;
|
||||
teamInviteId: string;
|
||||
serverUrl: string;
|
||||
onLayoutContainer: (e: LayoutChangeEvent) => void;
|
||||
onClose: () => Promise<void>;
|
||||
}
|
||||
|
||||
export default function SelectionTeamBar({
|
||||
teamId,
|
||||
teamDisplayName,
|
||||
teamLastIconUpdate,
|
||||
teamInviteId,
|
||||
serverUrl,
|
||||
onLayoutContainer,
|
||||
onClose,
|
||||
}: SelectionTeamBarProps) {
|
||||
const {formatMessage} = useIntl();
|
||||
const theme = useTheme();
|
||||
const styles = getStyleSheet(theme);
|
||||
const serverDisplayName = useServerDisplayName();
|
||||
|
||||
const handleOnLayoutContainer = useCallback((e: LayoutChangeEvent) => {
|
||||
onLayoutContainer(e);
|
||||
}, [onLayoutContainer]);
|
||||
|
||||
const handleOnShareLink = async () => {
|
||||
const url = `${serverUrl}/signup_user_complete/?id=${teamInviteId}`;
|
||||
const title = formatMessage({id: 'invite_people_to_team.title', defaultMessage: 'Join the {team} team'}, {team: teamDisplayName});
|
||||
const message = formatMessage({id: 'invite_people_to_team.message', defaultMessage: 'Here’s a link to collaborate and communicate with us on Mattermost.'});
|
||||
const icon = 'data:<data_type>/<file_extension>;base64,<base64_data>';
|
||||
|
||||
const options: ShareOptions = Platform.select({
|
||||
ios: {
|
||||
activityItemSources: [
|
||||
{
|
||||
placeholderItem: {
|
||||
type: 'url',
|
||||
content: url,
|
||||
},
|
||||
item: {
|
||||
default: {
|
||||
type: 'text',
|
||||
content: `${message} ${url}`,
|
||||
},
|
||||
copyToPasteBoard: {
|
||||
type: 'url',
|
||||
content: url,
|
||||
},
|
||||
},
|
||||
subject: {
|
||||
default: title,
|
||||
},
|
||||
linkMetadata: {
|
||||
originalUrl: url,
|
||||
url,
|
||||
title,
|
||||
icon,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
default: {
|
||||
title,
|
||||
subject: title,
|
||||
url,
|
||||
showAppsToView: true,
|
||||
},
|
||||
});
|
||||
|
||||
await onClose();
|
||||
|
||||
Share.open(
|
||||
options,
|
||||
).catch(() => {
|
||||
// do nothing
|
||||
});
|
||||
};
|
||||
|
||||
const handleShareLink = useCallback(preventDoubleTap(() => handleOnShareLink()), []);
|
||||
|
||||
return (
|
||||
<View
|
||||
style={styles.container}
|
||||
onLayout={handleOnLayoutContainer}
|
||||
>
|
||||
<View style={styles.iconContainer}>
|
||||
<TeamIcon
|
||||
id={teamId}
|
||||
displayName={teamDisplayName}
|
||||
lastIconUpdate={teamLastIconUpdate}
|
||||
selected={false}
|
||||
textColor={theme.centerChannelColor}
|
||||
backgroundColor={changeOpacity(theme.centerChannelColor, 0.16)}
|
||||
testID='invite.team_icon'
|
||||
/>
|
||||
</View>
|
||||
<View style={styles.textContainer}>
|
||||
<Text
|
||||
style={styles.teamText}
|
||||
numberOfLines={1}
|
||||
testID='invite.team_display_name'
|
||||
>
|
||||
{teamDisplayName}
|
||||
</Text>
|
||||
<Text
|
||||
style={styles.serverText}
|
||||
numberOfLines={1}
|
||||
testID='invite.server_display_name'
|
||||
>
|
||||
{serverDisplayName}
|
||||
</Text>
|
||||
</View>
|
||||
<TouchableOpacity
|
||||
onPress={handleShareLink}
|
||||
style={styles.shareLink}
|
||||
>
|
||||
<View
|
||||
style={styles.shareLinkButton}
|
||||
testID='invite.share_link.button'
|
||||
>
|
||||
<CompassIcon
|
||||
name='export-variant'
|
||||
size={18}
|
||||
style={styles.shareLinkIcon}
|
||||
/>
|
||||
<FormattedText
|
||||
id='invite.shareLink'
|
||||
defaultMessage='Share link'
|
||||
style={styles.shareLinkText}
|
||||
/>
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
@@ -18,6 +18,8 @@ import {typography} from '@utils/typography';
|
||||
import {SearchResult, Result} from './invite';
|
||||
import SummaryReport, {SummaryReportType} from './summary_report';
|
||||
|
||||
const MAX_WIDTH_CONTENT = 480;
|
||||
|
||||
const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => {
|
||||
return {
|
||||
container: {
|
||||
@@ -26,36 +28,40 @@ const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => {
|
||||
},
|
||||
summary: {
|
||||
display: 'flex',
|
||||
flexGrowth: 1,
|
||||
flex: 1,
|
||||
},
|
||||
summaryContainer: {
|
||||
flexGrow: 1,
|
||||
flexDirection: 'column',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
margin: 20,
|
||||
alignSelf: 'center',
|
||||
marginTop: 20,
|
||||
marginHorizontal: 20,
|
||||
paddingBottom: 20,
|
||||
maxWidth: MAX_WIDTH_CONTENT,
|
||||
},
|
||||
summarySvg: {
|
||||
marginBottom: 20,
|
||||
alignSelf: 'center',
|
||||
},
|
||||
summaryMessageText: {
|
||||
color: theme.centerChannelColor,
|
||||
...typography('Heading', 700, 'SemiBold'),
|
||||
textAlign: 'center',
|
||||
marginHorizontal: 32,
|
||||
marginBottom: 16,
|
||||
marginBottom: 24,
|
||||
},
|
||||
summaryButton: {
|
||||
flex: 1,
|
||||
backgroundColor: changeOpacity(theme.centerChannelColor, 0.08),
|
||||
margin: 20,
|
||||
padding: 15,
|
||||
},
|
||||
summaryButtonContainer: {
|
||||
footer: {
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'center',
|
||||
borderTopWidth: 1,
|
||||
borderTopColor: changeOpacity(theme.centerChannelColor, 0.16),
|
||||
padding: 20,
|
||||
},
|
||||
summaryButtonContainer: {
|
||||
flexGrow: 1,
|
||||
maxWidth: MAX_WIDTH_CONTENT,
|
||||
},
|
||||
};
|
||||
});
|
||||
@@ -63,6 +69,7 @@ const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => {
|
||||
type SummaryProps = {
|
||||
result: Result;
|
||||
selectedIds: {[id: string]: SearchResult};
|
||||
error?: string;
|
||||
testID: string;
|
||||
onClose: () => void;
|
||||
}
|
||||
@@ -70,6 +77,7 @@ type SummaryProps = {
|
||||
export default function Summary({
|
||||
result,
|
||||
selectedIds,
|
||||
error,
|
||||
testID,
|
||||
onClose,
|
||||
}: SummaryProps) {
|
||||
@@ -84,10 +92,21 @@ export default function Summary({
|
||||
const styleButtonText = buttonTextStyle(theme, 'lg', 'primary');
|
||||
const styleButtonBackground = buttonBackgroundStyle(theme, 'lg', 'primary');
|
||||
|
||||
let message = '';
|
||||
let svg = <></>;
|
||||
let message = '';
|
||||
|
||||
if (sentCount && !notSentCount) {
|
||||
if (error) {
|
||||
svg = <ErrorSvg/>;
|
||||
} else if (!sentCount && notSentCount) {
|
||||
svg = <ErrorSvg/>;
|
||||
message = error || formatMessage(
|
||||
{
|
||||
id: 'invite.summary.not_sent',
|
||||
defaultMessage: '{notSentCount, plural, one {Invitation wasn’t} other {Invitations weren’t}} sent',
|
||||
},
|
||||
{notSentCount},
|
||||
);
|
||||
} else if (sentCount && !notSentCount) {
|
||||
svg = <SuccessSvg/>;
|
||||
message = formatMessage(
|
||||
{
|
||||
@@ -105,15 +124,6 @@ export default function Summary({
|
||||
},
|
||||
{notSentCount},
|
||||
);
|
||||
} else if (!sentCount && notSentCount) {
|
||||
svg = <ErrorSvg/>;
|
||||
message = formatMessage(
|
||||
{
|
||||
id: 'invite.summary.not_sent',
|
||||
defaultMessage: '{notSentCount, plural, one {Invitation wasn’t} other {Invitations weren’t}} sent',
|
||||
},
|
||||
{notSentCount},
|
||||
);
|
||||
}
|
||||
|
||||
const handleOnPressButton = () => {
|
||||
@@ -136,31 +146,37 @@ export default function Summary({
|
||||
<Text style={styles.summaryMessageText}>
|
||||
{message}
|
||||
</Text>
|
||||
<SummaryReport
|
||||
type={SummaryReportType.NOT_SENT}
|
||||
invites={notSent}
|
||||
selectedIds={selectedIds}
|
||||
testID='invite.summary_report'
|
||||
/>
|
||||
<SummaryReport
|
||||
type={SummaryReportType.SENT}
|
||||
invites={sent}
|
||||
selectedIds={selectedIds}
|
||||
testID='invite.summary_report'
|
||||
/>
|
||||
{!error && (
|
||||
<>
|
||||
<SummaryReport
|
||||
type={SummaryReportType.NOT_SENT}
|
||||
invites={notSent}
|
||||
selectedIds={selectedIds}
|
||||
testID='invite.summary_report'
|
||||
/>
|
||||
<SummaryReport
|
||||
type={SummaryReportType.SENT}
|
||||
invites={sent}
|
||||
selectedIds={selectedIds}
|
||||
testID='invite.summary_report'
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</ScrollView>
|
||||
<View style={styles.summaryButtonContainer}>
|
||||
<Button
|
||||
containerStyle={[styles.summaryButton, styleButtonBackground]}
|
||||
onPress={handleOnPressButton}
|
||||
testID='invite.summary_button'
|
||||
>
|
||||
<FormattedText
|
||||
id='invite.summary.done'
|
||||
defaultMessage='Done'
|
||||
style={styleButtonText}
|
||||
/>
|
||||
</Button>
|
||||
<View style={styles.footer}>
|
||||
<View style={styles.summaryButtonContainer}>
|
||||
<Button
|
||||
containerStyle={styleButtonBackground}
|
||||
onPress={handleOnPressButton}
|
||||
testID='invite.summary_button'
|
||||
>
|
||||
<FormattedText
|
||||
id='invite.summary.done'
|
||||
defaultMessage='Done'
|
||||
style={styleButtonText}
|
||||
/>
|
||||
</Button>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
|
||||
@@ -22,14 +22,12 @@ const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => {
|
||||
borderWidth: 1,
|
||||
borderColor: changeOpacity(theme.centerChannelColor, 0.16),
|
||||
borderRadius: 4,
|
||||
width: '100%',
|
||||
marginBottom: 16,
|
||||
paddingVertical: 8,
|
||||
},
|
||||
summaryInvitationsTitle: {
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
flexGrow: 1,
|
||||
alignItems: 'center',
|
||||
paddingHorizontal: 20,
|
||||
paddingVertical: 12,
|
||||
@@ -42,7 +40,6 @@ const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => {
|
||||
summaryInvitationsItem: {
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
flexGrow: 1,
|
||||
paddingVertical: 12,
|
||||
},
|
||||
summaryInvitationsUser: {
|
||||
|
||||
@@ -331,6 +331,7 @@
|
||||
"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_invite": "Send",
|
||||
"invite.sendInvitationsTo": "Send invitations to…",
|
||||
"invite.shareLink": "Share link",
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {ChannelListScreen} from '@support/ui/screen';
|
||||
import {timeouts} from '@support/utils';
|
||||
import {timeouts, wait} from '@support/utils';
|
||||
import {expect} from 'detox';
|
||||
|
||||
class InviteScreen {
|
||||
@@ -111,6 +111,7 @@ class InviteScreen {
|
||||
|
||||
open = async () => {
|
||||
await ChannelListScreen.headerPlusButton.tap();
|
||||
await wait(timeouts.ONE_SEC);
|
||||
await ChannelListScreen.invitePeopleToTeamItem.tap();
|
||||
|
||||
return this.toBeVisible();
|
||||
|
||||
@@ -45,6 +45,7 @@ class ServerScreen {
|
||||
connectToServer = async (serverUrl: string, serverDisplayName: string) => {
|
||||
await this.toBeVisible();
|
||||
await this.serverUrlInput.replaceText(serverUrl);
|
||||
await this.serverUrlInput.tapReturnKey();
|
||||
await this.serverDisplayNameInput.replaceText(serverDisplayName);
|
||||
await this.connectButton.tap();
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user