MM-39720_Invite People - phase 1

This commit is contained in:
Julian Mondragon
2022-10-31 17:25:52 -05:00
parent f033a28eb2
commit 647cd4c9c2
17 changed files with 224 additions and 11 deletions

View File

@@ -21,6 +21,7 @@ type SlideUpPanelProps = {
textStyles?: TextStyle;
testID?: string;
text: string;
topDivider?: boolean;
}
export const ITEM_HEIGHT = 48;
@@ -61,10 +62,15 @@ const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => {
color: theme.centerChannelColor,
...typography('Body', 200, 'Regular'),
},
divider: {
borderTopWidth: 1,
borderStyle: 'solid',
borderColor: changeOpacity(theme.centerChannelColor, 0.08),
},
};
});
const SlideUpPanelItem = ({destructive, icon, imageStyles, onPress, testID, text, textStyles, rightIcon = false}: SlideUpPanelProps) => {
const SlideUpPanelItem = ({destructive, icon, imageStyles, onPress, testID, text, textStyles, rightIcon = false, topDivider}: SlideUpPanelProps) => {
const theme = useTheme();
const handleOnPress = useCallback(preventDoubleTap(onPress, 500), []);
const style = getStyleSheet(theme);
@@ -99,10 +105,12 @@ const SlideUpPanelItem = ({destructive, icon, imageStyles, onPress, testID, text
}
}
const divider = topDivider ? style.divider : {};
return (
<TouchableHighlight
onPress={handleOnPress}
style={style.container}
style={{...style.container, ...divider}}
testID={testID}
underlayColor={changeOpacity(theme.buttonBg, 0.08)}
>

View File

@@ -11,10 +11,22 @@ import {MM_TABLES} from '@constants/database';
const {SERVER: {
GROUP,
MY_CHANNEL,
TEAM,
THREAD,
}} = MM_TABLES;
export default schemaMigrations({migrations: [
{
toVersion: 4,
steps: [
addColumns({
table: TEAM,
columns: [
{name: 'invite_id', type: 'string'},
],
}),
],
},
{
toVersion: 3,
steps: [

View File

@@ -87,6 +87,9 @@ export default class TeamModel extends Model implements TeamModelInterface {
/** allowed_domains : List of domains that can join this team */
@field('allowed_domains') allowedDomains!: string;
/** invite_id : The token id to use in invites to the team */
@field('invite_id') inviteId!: string;
/** categories : All the categories associated with this team */
@children(CATEGORY) categories!: Query<CategoryModel>;

View File

@@ -72,6 +72,7 @@ export const transformTeamRecord = ({action, database, value}: TransformerArgs):
team.allowedDomains = raw.allowed_domains;
team.isGroupConstrained = Boolean(raw.group_constrained);
team.lastTeamIconUpdatedAt = raw.last_team_icon_update;
team.inviteId = raw.invite_id;
};
return prepareBaseRecord({

View File

@@ -37,7 +37,7 @@ import {
} from './table_schemas';
export const serverSchema: AppSchema = appSchema({
version: 3,
version: 4,
tables: [
CategorySchema,
CategoryChannelSchema,

View File

@@ -19,5 +19,6 @@ export default tableSchema({
{name: 'name', type: 'string'},
{name: 'type', type: 'string'},
{name: 'update_at', type: 'number'},
{name: 'invite_id', type: 'string'},
],
});

View File

@@ -43,7 +43,7 @@ const {
describe('*** Test schema for SERVER database ***', () => {
it('=> The SERVER SCHEMA should strictly match', () => {
expect(serverSchema).toEqual({
version: 3,
version: 4,
tables: {
[CATEGORY]: {
name: CATEGORY,
@@ -463,6 +463,7 @@ describe('*** Test schema for SERVER database ***', () => {
name: {name: 'name', type: 'string'},
type: {name: 'type', type: 'string'},
update_at: {name: 'update_at', type: 'number'},
invite_id: {name: 'invite_id', type: 'string'},
},
columnArray: [
{name: 'allowed_domains', type: 'string'},
@@ -474,6 +475,7 @@ describe('*** Test schema for SERVER database ***', () => {
{name: 'name', type: 'string'},
{name: 'type', type: 'string'},
{name: 'update_at', type: 'number'},
{name: 'invite_id', type: 'string'},
],
},
[TEAM_CHANNEL_HISTORY]: {

View File

@@ -15,6 +15,7 @@ describe('components/channel_list/header', () => {
pushProxyStatus={PUSH_PROXY_STATUS_VERIFIED}
canCreateChannels={true}
canJoinChannels={true}
canInvitePeople={true}
displayName={'Test!'}
/>,
);

View File

@@ -28,7 +28,9 @@ import PlusMenu from './plus_menu';
type Props = {
canCreateChannels: boolean;
canJoinChannels: boolean;
displayName: string;
canInvitePeople: boolean;
displayName?: string;
inviteId?: string;
iconPad?: boolean;
onHeaderPress?: () => void;
pushProxyStatus: string;
@@ -92,7 +94,9 @@ const hitSlop: Insets = {top: 10, bottom: 30, left: 20, right: 20};
const ChannelListHeader = ({
canCreateChannels,
canJoinChannels,
canInvitePeople,
displayName,
inviteId,
iconPad,
onHeaderPress,
pushProxyStatus,
@@ -118,6 +122,9 @@ const ChannelListHeader = ({
<PlusMenu
canCreateChannels={canCreateChannels}
canJoinChannels={canJoinChannels}
canInvitePeople={canInvitePeople}
displayName={displayName}
inviteId={inviteId}
/>
);
};
@@ -132,6 +139,10 @@ const ChannelListHeader = ({
items += 1;
}
if (canInvitePeople) {
items += 1;
}
bottomSheet({
closeButtonId,
renderContent,

View File

@@ -8,7 +8,7 @@ import {switchMap} from 'rxjs/operators';
import {Permissions} from '@constants';
import {observePermissionForTeam} from '@queries/servers/role';
import {observePushVerificationStatus} from '@queries/servers/system';
import {observeConfig, observePushVerificationStatus} from '@queries/servers/system';
import {observeCurrentTeam} from '@queries/servers/team';
import {observeCurrentUser} from '@queries/servers/user';
@@ -21,6 +21,12 @@ const enhanced = withObservables([], ({database}: WithDatabaseArgs) => {
const currentUser = observeCurrentUser(database);
const config = observeConfig(database);
const enableOpenServer = config.pipe(
switchMap((t) => of$(t?.EnableOpenServer === 'true')),
);
const canJoinChannels = combineLatest([currentUser, team]).pipe(
switchMap(([u, t]) => observePermissionForTeam(database, t, u, Permissions.JOIN_PUBLIC_CHANNELS, true)),
);
@@ -37,12 +43,22 @@ const enhanced = withObservables([], ({database}: WithDatabaseArgs) => {
switchMap(([open, priv]) => of$(open || priv)),
);
const canAddUserToTeam = combineLatest([currentUser, team]).pipe(
switchMap(([u, t]) => observePermissionForTeam(database, t, u, Permissions.ADD_USER_TO_TEAM, false)),
);
return {
canCreateChannels,
canJoinChannels,
canInvitePeople: combineLatest([enableOpenServer, canAddUserToTeam]).pipe(
switchMap(([openServer, addUser]) => of$(openServer && addUser)),
),
displayName: team.pipe(
switchMap((t) => of$(t?.displayName)),
),
inviteId: team.pipe(
switchMap((t) => of$(t?.inviteId)),
),
pushProxyStatus: observePushVerificationStatus(database),
};
});

View File

@@ -3,9 +3,13 @@
import React, {useCallback} from 'react';
import {useIntl} from 'react-intl';
import {Platform} 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 {Screens} from '@constants';
import {useServerUrl} from '@context/server';
import {useTheme} from '@context/theme';
import {dismissBottomSheet, showModal} from '@screens/navigation';
@@ -14,11 +18,15 @@ import PlusMenuItem from './item';
type Props = {
canCreateChannels: boolean;
canJoinChannels: boolean;
canInvitePeople: boolean;
displayName?: string;
inviteId?: string;
}
const PlusMenuList = ({canCreateChannels, canJoinChannels}: Props) => {
const PlusMenuList = ({canCreateChannels, canJoinChannels, canInvitePeople, displayName, inviteId}: Props) => {
const intl = useIntl();
const theme = useTheme();
const serverUrl = useServerUrl();
const browseChannels = useCallback(async () => {
await dismissBottomSheet();
@@ -48,6 +56,59 @@ const PlusMenuList = ({canCreateChannels, canJoinChannels}: Props) => {
});
}, [intl, theme]);
const invitePeopleToTeam = async () => {
await dismissBottomSheet();
const url = `${serverUrl}/signup_user_complete/?id=${inviteId}`;
const title = intl.formatMessage({id: 'invite_people_to_team.title', defaultMessage: 'Join the {team} team'}, {team: displayName});
const message = intl.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,
},
});
Share.open(
options,
).catch(() => {
// do nothing
});
};
return (
<>
{canJoinChannels &&
@@ -66,6 +127,12 @@ const PlusMenuList = ({canCreateChannels, canJoinChannels}: Props) => {
pickerAction='openDirectMessage'
onPress={openDirectMessage}
/>
{canInvitePeople &&
<PlusMenuItem
pickerAction='invitePeopleToTeam'
onPress={invitePeopleToTeam}
/>
}
</>
);
};

View File

@@ -7,7 +7,7 @@ import {useIntl} from 'react-intl';
import SlideUpPanelItem from '@components/slide_up_panel_item';
type PlusMenuItemProps = {
pickerAction: 'browseChannels' | 'createNewChannel' | 'openDirectMessage';
pickerAction: 'browseChannels' | 'createNewChannel' | 'openDirectMessage' | 'invitePeopleToTeam';
onPress: () => void;
};
@@ -32,14 +32,20 @@ const PlusMenuItem = ({pickerAction, onPress}: PlusMenuItemProps) => {
text: intl.formatMessage({id: 'plus_menu.open_direct_message.title', defaultMessage: 'Open a Direct Message'}),
testID: 'plus_menu_item.open_direct_message',
},
invitePeopleToTeam: {
icon: 'account-plus-outline',
text: intl.formatMessage({id: 'plus_menu.invite_people_to_team.title', defaultMessage: 'Invite people to the team'}),
testID: 'plus_menu_item.invite_people_to_team',
topDivider: true,
},
};
const itemType = menuItems[pickerAction];
return (
<SlideUpPanelItem
text={itemType.text}
icon={itemType.icon}
{...itemType}
onPress={onPress}
testID={itemType.testID}
/>
);
};

View File

@@ -315,6 +315,8 @@
"intro.welcome": "Welcome to {displayName} channel.",
"intro.welcome.private": "Only invited members can see messages posted in this private channel.",
"intro.welcome.public": "Add some more team members to the channel or start a conversation below.",
"invite_people_to_team.message": "Here´s a link to collaborate and communicate with us on Mattermost.",
"invite_people_to_team.title": "Join the {team} team",
"last_users_message.added_to_channel.type": "were **added to the channel** by {actor}.",
"last_users_message.added_to_team.type": "were **added to the team** by {actor}.",
"last_users_message.first": "{firstUser} and ",
@@ -684,6 +686,7 @@
"pinned_messages.empty.title": "No pinned messages yet",
"plus_menu.browse_channels.title": "Browse Channels",
"plus_menu.create_new_channel.title": "Create New Channel",
"plus_menu.invite_people_to_team.title": "Invite people to the team",
"plus_menu.open_direct_message.title": "Open a Direct Message",
"post_body.check_for_out_of_channel_groups_mentions.message": "did not get notified by this mention because they are not in the channel. They are also not a member of the groups linked to this channel.",
"post_body.check_for_out_of_channel_mentions.link.and": " and ",

View File

@@ -6,11 +6,13 @@ class PlusMenu {
browseChannelsItem: 'plus_menu_item.browse_channels',
createNewChannelItem: 'plus_menu_item.create_new_channel',
openDirectMessageItem: 'plus_menu_item.open_direct_message',
invitePeopleToTeamItem: 'plus_menu_item.invite_people_to_team',
};
browseChannelsItem = element(by.id(this.testID.browseChannelsItem));
createNewChannelItem = element(by.id(this.testID.createNewChannelItem));
openDirectMessageItem = element(by.id(this.testID.openDirectMessageItem));
invitePeopleToTeamItem = element(by.id(this.testID.invitePeopleToTeamItem));
}
const plusMenu = new PlusMenu();

View File

@@ -39,6 +39,7 @@ class ChannelListScreen {
browseChannelsItem = PlusMenu.browseChannelsItem;
createNewChannelItem = PlusMenu.createNewChannelItem;
openDirectMessageItem = PlusMenu.openDirectMessageItem;
invitePeopleToTeamItem = PlusMenu.invitePeopleToTeamItem;
getCategoryCollapsed = (categoryKey: string) => {
return element(by.id(`${this.testID.categoryHeaderPrefix}${categoryKey}.collapsed.true`));

View File

@@ -0,0 +1,76 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
// *******************************************************************
// - [#] indicates a test step (e.g. # Go to a screen)
// - [*] indicates an assertion (e.g. * Check the title)
// - Use element testID when selecting an element. Create one if none.
// *******************************************************************
import {Setup} from '@support/server_api';
import {
serverOneUrl,
siteOneUrl,
} from '@support/test_config';
import {
ChannelListScreen,
HomeScreen,
LoginScreen,
ServerScreen,
} from '@support/ui/screen';
import {isIos} from '@support/utils';
import {expect} from 'detox';
function systemDialog(label: string) {
if (device.getPlatform() === 'ios') {
return element(by.label(label)).atIndex(0);
}
return element(by.text(label));
}
describe('Teams - Invite people', () => {
const serverOneDisplayName = 'Server 1';
let testTeam: any;
let testUser: any;
beforeAll(async () => {
const {team, user} = await Setup.apiInit(siteOneUrl);
testTeam = team;
testUser = user;
// # Log in to server
await ServerScreen.connectToServer(serverOneUrl, serverOneDisplayName);
await LoginScreen.login(testUser);
});
beforeEach(async () => {
// * Verify on channel list screen
await ChannelListScreen.toBeVisible();
});
afterAll(async () => {
// # Close share dialog
await ChannelListScreen.headerTeamDisplayName.tap();
// # Log out
await HomeScreen.logout();
});
it('MM-T4729_2 - should be able to share a URL invite to the team', async () => {
// # Open plus menu
await ChannelListScreen.headerPlusButton.tap();
// * Verify invite people to team item is available
await expect(ChannelListScreen.invitePeopleToTeamItem).toExist();
// # Tap on invite people to team item
await ChannelListScreen.invitePeopleToTeamItem.tap();
if (isIos()) {
// * Verify share dialog is open
await expect(systemDialog(`Join the ${testTeam.display_name} team`)).toExist();
}
});
});

View File

@@ -47,6 +47,9 @@ declare class TeamModel extends Model {
/** allowed_domains : List of domains that can join this team */
allowedDomains: string;
/** invite_id : The token id to use in invites to the team */
inviteId: string;
/** categories : All the categories associated with this team */
categories: Query<CategoryModel>;