Merge pull request #6719 from julmondragon/MM-39720

This commit is contained in:
Julian Mondragón 2022-11-08 12:49:53 -05:00 committed by GitHub
commit b9ae96d45b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
17 changed files with 251 additions and 10 deletions

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

@ -24,11 +24,14 @@ import {typography} from '@utils/typography';
import LoadingUnreads from './loading_unreads';
import PlusMenu from './plus_menu';
import {SEPARATOR_HEIGHT} from './plus_menu/separator';
type Props = {
canCreateChannels: boolean;
canJoinChannels: boolean;
displayName: string;
canInvitePeople: boolean;
displayName?: string;
inviteId?: string;
iconPad?: boolean;
onHeaderPress?: () => void;
pushProxyStatus: string;
@ -92,7 +95,9 @@ const hitSlop: Insets = {top: 10, bottom: 30, left: 20, right: 20};
const ChannelListHeader = ({
canCreateChannels,
canJoinChannels,
canInvitePeople,
displayName,
inviteId,
iconPad,
onHeaderPress,
pushProxyStatus,
@ -118,12 +123,17 @@ const ChannelListHeader = ({
<PlusMenu
canCreateChannels={canCreateChannels}
canJoinChannels={canJoinChannels}
canInvitePeople={canInvitePeople}
displayName={displayName}
inviteId={inviteId}
/>
);
};
const closeButtonId = 'close-plus-menu';
let items = 1;
let separators = 0;
if (canCreateChannels) {
items += 1;
}
@ -132,10 +142,15 @@ const ChannelListHeader = ({
items += 1;
}
if (canInvitePeople) {
items += 1;
separators += 1;
}
bottomSheet({
closeButtonId,
renderContent,
snapPoints: [bottomSheetSnapPoint(items, ITEM_HEIGHT, insets.bottom), 10],
snapPoints: [bottomSheetSnapPoint(items, ITEM_HEIGHT, insets.bottom) + (separators * SEPARATOR_HEIGHT), 10],
theme,
title: intl.formatMessage({id: 'home.header.plus_menu', defaultMessage: 'Options'}),
});

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 {observeConfigBooleanValue, observePushVerificationStatus} from '@queries/servers/system';
import {observeCurrentTeam} from '@queries/servers/team';
import {observeCurrentUser} from '@queries/servers/user';
@ -21,6 +21,8 @@ const enhanced = withObservables([], ({database}: WithDatabaseArgs) => {
const currentUser = observeCurrentUser(database);
const enableOpenServer = observeConfigBooleanValue(database, 'EnableOpenServer');
const canJoinChannels = combineLatest([currentUser, team]).pipe(
switchMap(([u, t]) => observePermissionForTeam(database, t, u, Permissions.JOIN_PUBLIC_CHANNELS, true)),
);
@ -37,12 +39,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,22 +3,31 @@
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';
import PlusMenuItem from './item';
import PlusMenuSeparator from './separator';
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 +57,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: 'Heres 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 +128,15 @@ const PlusMenuList = ({canCreateChannels, canJoinChannels}: Props) => {
pickerAction='openDirectMessage'
onPress={openDirectMessage}
/>
{canInvitePeople &&
<>
<PlusMenuSeparator/>
<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,19 @@ 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',
},
};
const itemType = menuItems[pickerAction];
return (
<SlideUpPanelItem
text={itemType.text}
icon={itemType.icon}
{...itemType}
onPress={onPress}
testID={itemType.testID}
/>
);
};

View file

@ -0,0 +1,33 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React from 'react';
import {View} from 'react-native';
import {useTheme} from '@context/theme';
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
const MARGIN_VERTICAL = 8;
const BORDER = 1;
export const SEPARATOR_HEIGHT = (MARGIN_VERTICAL * 2) + BORDER;
const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => ({
separator: {
marginTop: MARGIN_VERTICAL,
marginBottom: MARGIN_VERTICAL,
borderTopWidth: BORDER,
borderStyle: 'solid',
borderColor: changeOpacity(theme.centerChannelColor, 0.08),
},
}));
const PlusMenuSeparator = () => {
const theme = useTheme();
const style = getStyleSheet(theme);
return (
<View style={style.separator}/>
);
};
export default PlusMenuSeparator;

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": "Heres 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 ",
@ -679,6 +681,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-T5221 - 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>;