Playbooks update (#9039)
* Playbook run status update post * Show Playbooks button in Channel Info screen only if playbooks is enabled * Handle Playbook links * Fetch playbook if needed * fix playbooks migration * fix deeplinks using parsedUrl * update last time playbooks where fetched if no errors * show participants for run status update post * fix tests * remove console.log in test * feedback review * wrap participants footer * add fastlane FASTLANE_XCODEBUILD_SETTINGS_RETRIES env vars
This commit is contained in:
parent
21708aa2ee
commit
40d8a0cbb4
27 changed files with 548 additions and 59 deletions
|
|
@ -382,7 +382,7 @@ describe('handleGotoLocation', () => {
|
||||||
|
|
||||||
expect(getConfig).toHaveBeenCalledWith(mockOperator.database);
|
expect(getConfig).toHaveBeenCalledWith(mockOperator.database);
|
||||||
expect(matchDeepLink).toHaveBeenCalledWith(location, serverUrl, serverUrl);
|
expect(matchDeepLink).toHaveBeenCalledWith(location, serverUrl, serverUrl);
|
||||||
expect(handleDeepLink).toHaveBeenCalledWith(location, intl, location);
|
expect(handleDeepLink).toHaveBeenCalledWith({url: location}, intl, location);
|
||||||
expect(result).toEqual({data: true});
|
expect(result).toEqual({data: true});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -143,7 +143,7 @@ export const handleGotoLocation = async (serverUrl: string, intl: IntlShape, loc
|
||||||
const match = matchDeepLink(location, serverUrl, config?.SiteURL);
|
const match = matchDeepLink(location, serverUrl, config?.SiteURL);
|
||||||
|
|
||||||
if (match) {
|
if (match) {
|
||||||
handleDeepLink(match.url, intl, location);
|
handleDeepLink(match, intl, location);
|
||||||
} else {
|
} else {
|
||||||
const {formatMessage} = intl;
|
const {formatMessage} = intl;
|
||||||
const onError = () => Alert.alert(
|
const onError = () => Alert.alert(
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,8 @@ import FormattedText from '@components/formatted_text';
|
||||||
import JumboEmoji from '@components/jumbo_emoji';
|
import JumboEmoji from '@components/jumbo_emoji';
|
||||||
import {Screens} from '@constants';
|
import {Screens} from '@constants';
|
||||||
import {THREAD} from '@constants/screens';
|
import {THREAD} from '@constants/screens';
|
||||||
|
import StatusUpdatePost from '@playbooks/components/status_update_post';
|
||||||
|
import {PLAYBOOKS_UPDATE_STATUS_POST_TYPE} from '@playbooks/constants/plugin';
|
||||||
import {isEdited as postEdited, isPostFailed} from '@utils/post';
|
import {isEdited as postEdited, isPostFailed} from '@utils/post';
|
||||||
import {makeStyleSheetFromTheme} from '@utils/theme';
|
import {makeStyleSheetFromTheme} from '@utils/theme';
|
||||||
|
|
||||||
|
|
@ -139,6 +141,14 @@ const Body = ({
|
||||||
defaultMessage='(message deleted)'
|
defaultMessage='(message deleted)'
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
} else if (post.type === PLAYBOOKS_UPDATE_STATUS_POST_TYPE && post.props != null) {
|
||||||
|
message = (
|
||||||
|
<StatusUpdatePost
|
||||||
|
location={location}
|
||||||
|
post={post}
|
||||||
|
theme={theme}
|
||||||
|
/>
|
||||||
|
);
|
||||||
} else if (isPostAddChannelMember) {
|
} else if (isPostAddChannelMember) {
|
||||||
message = (
|
message = (
|
||||||
<AddMembers
|
<AddMembers
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,8 @@ const DeepLinkType = {
|
||||||
GroupMessage: 'gm',
|
GroupMessage: 'gm',
|
||||||
Invalid: 'invalid',
|
Invalid: 'invalid',
|
||||||
Permalink: 'permalink',
|
Permalink: 'permalink',
|
||||||
|
Playbooks: 'playbooks',
|
||||||
|
PlaybookRuns: 'playbook_runs',
|
||||||
Redirect: '_redirect',
|
Redirect: '_redirect',
|
||||||
Server: 'server',
|
Server: 'server',
|
||||||
} as const;
|
} as const;
|
||||||
|
|
|
||||||
|
|
@ -53,6 +53,8 @@ export default schemaMigrations({migrations: [
|
||||||
{name: 'retrospective', type: 'string'},
|
{name: 'retrospective', type: 'string'},
|
||||||
{name: 'retrospective_published_at', type: 'number'},
|
{name: 'retrospective_published_at', type: 'number'},
|
||||||
{name: 'update_at', type: 'number'},
|
{name: 'update_at', type: 'number'},
|
||||||
|
{name: 'sync', type: 'string', isIndexed: true, isOptional: true},
|
||||||
|
{name: 'last_sync_at', type: 'number', isOptional: true},
|
||||||
],
|
],
|
||||||
}),
|
}),
|
||||||
createTable({
|
createTable({
|
||||||
|
|
@ -62,6 +64,8 @@ export default schemaMigrations({migrations: [
|
||||||
{name: 'items_order', type: 'string'},
|
{name: 'items_order', type: 'string'},
|
||||||
{name: 'title', type: 'string'},
|
{name: 'title', type: 'string'},
|
||||||
{name: 'update_at', type: 'number'},
|
{name: 'update_at', type: 'number'},
|
||||||
|
{name: 'sync', type: 'string', isIndexed: true, isOptional: true},
|
||||||
|
{name: 'last_sync_at', type: 'number', isOptional: true},
|
||||||
],
|
],
|
||||||
}),
|
}),
|
||||||
createTable({
|
createTable({
|
||||||
|
|
@ -80,6 +84,8 @@ export default schemaMigrations({migrations: [
|
||||||
{name: 'completed_at', type: 'number'},
|
{name: 'completed_at', type: 'number'},
|
||||||
{name: 'task_actions', type: 'string', isOptional: true}, // JSON string
|
{name: 'task_actions', type: 'string', isOptional: true}, // JSON string
|
||||||
{name: 'update_at', type: 'number'},
|
{name: 'update_at', type: 'number'},
|
||||||
|
{name: 'sync', type: 'string', isIndexed: true, isOptional: true},
|
||||||
|
{name: 'last_sync_at', type: 'number', isOptional: true},
|
||||||
],
|
],
|
||||||
}),
|
}),
|
||||||
addColumns({
|
addColumns({
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,7 @@ import {queryTeamDefaultChannel} from '@queries/servers/channel';
|
||||||
import {getCommonSystemValues} from '@queries/servers/system';
|
import {getCommonSystemValues} from '@queries/servers/system';
|
||||||
import {getTeamChannelHistory} from '@queries/servers/team';
|
import {getTeamChannelHistory} from '@queries/servers/team';
|
||||||
import {setScreensOrientation} from '@screens/navigation';
|
import {setScreensOrientation} from '@screens/navigation';
|
||||||
import {alertInvalidDeepLink, handleDeepLink} from '@utils/deep_link';
|
import {alertInvalidDeepLink, parseAndHandleDeepLink} from '@utils/deep_link';
|
||||||
import {getIntlShape} from '@utils/general';
|
import {getIntlShape} from '@utils/general';
|
||||||
|
|
||||||
type LinkingCallbackArg = {url: string};
|
type LinkingCallbackArg = {url: string};
|
||||||
|
|
@ -43,7 +43,7 @@ class GlobalEventHandlerSingleton {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (event.url) {
|
if (event.url) {
|
||||||
const {error} = await handleDeepLink(event.url, undefined, undefined, true);
|
const {error} = await parseAndHandleDeepLink(event.url, undefined, undefined, true);
|
||||||
if (error) {
|
if (error) {
|
||||||
alertInvalidDeepLink(getIntlShape(DEFAULT_LOCALE));
|
alertInvalidDeepLink(getIntlShape(DEFAULT_LOCALE));
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -48,13 +48,17 @@ export const fetchPlaybookRunsForChannel = async (serverUrl: string, channelId:
|
||||||
return {runs: []};
|
return {runs: []};
|
||||||
}
|
}
|
||||||
|
|
||||||
updateLastPlaybookRunsFetchAt(serverUrl, channelId, getMaxRunUpdateAt(allRuns));
|
|
||||||
|
|
||||||
if (!fetchOnly) {
|
if (!fetchOnly) {
|
||||||
|
const result = await handlePlaybookRuns(serverUrl, allRuns, false, true);
|
||||||
|
if (result && result.error) {
|
||||||
|
throw result.error;
|
||||||
|
}
|
||||||
|
|
||||||
EphemeralStore.setChannelPlaybooksSynced(serverUrl, channelId);
|
EphemeralStore.setChannelPlaybooksSynced(serverUrl, channelId);
|
||||||
handlePlaybookRuns(serverUrl, allRuns, false, true);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
updateLastPlaybookRunsFetchAt(serverUrl, channelId, getMaxRunUpdateAt(allRuns));
|
||||||
|
|
||||||
return {runs: allRuns};
|
return {runs: allRuns};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logDebug('error on fetchPlaybookRunsForChannel', getFullErrorMessage(error));
|
logDebug('error on fetchPlaybookRunsForChannel', getFullErrorMessage(error));
|
||||||
|
|
@ -83,3 +87,24 @@ export const fetchFinishedRunsForChannel = async (serverUrl: string, channelId:
|
||||||
return {error};
|
return {error};
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const fetchPlaybookRun = async (serverUrl: string, runId: string, fetchOnly = false): Promise<PlaybookRunsRequest> => {
|
||||||
|
try {
|
||||||
|
const client = NetworkManager.getClient(serverUrl);
|
||||||
|
const run = await client.fetchPlaybookRun(runId);
|
||||||
|
|
||||||
|
if (!fetchOnly) {
|
||||||
|
const result = await handlePlaybookRuns(serverUrl, [run], false, true);
|
||||||
|
if (result.error) {
|
||||||
|
logDebug('error on handlePlaybookRuns', getFullErrorMessage(result.error));
|
||||||
|
return {error: result.error};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {runs: [run]};
|
||||||
|
} catch (error) {
|
||||||
|
logDebug('error on fetchPlaybookRun', getFullErrorMessage(error));
|
||||||
|
forceLogoutIfNecessary(serverUrl, error);
|
||||||
|
return {error};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,7 @@ export interface ClientPlaybooksMix {
|
||||||
// Playbook Runs
|
// Playbook Runs
|
||||||
fetchPlaybookRuns: (params: FetchPlaybookRunsParams, groupLabel?: RequestGroupLabel) => Promise<FetchPlaybookRunsReturn>;
|
fetchPlaybookRuns: (params: FetchPlaybookRunsParams, groupLabel?: RequestGroupLabel) => Promise<FetchPlaybookRunsReturn>;
|
||||||
|
|
||||||
// fetchPlaybookRun: (id: string, groupLabel?: RequestGroupLabel) => Promise<PlaybookRun>;
|
fetchPlaybookRun: (id: string, groupLabel?: RequestGroupLabel) => Promise<PlaybookRun>;
|
||||||
|
|
||||||
// Run Management
|
// Run Management
|
||||||
// finishRun: (playbookRunId: string) => Promise<any>;
|
// finishRun: (playbookRunId: string) => Promise<any>;
|
||||||
|
|
@ -54,12 +54,12 @@ const ClientPlaybooks = <TBase extends Constructor<ClientBase>>(superclass: TBas
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// fetchPlaybookRun = async (id: string, groupLabel?: RequestGroupLabel) => {
|
fetchPlaybookRun = async (id: string, groupLabel?: RequestGroupLabel) => {
|
||||||
// return this.doFetch(
|
return this.doFetch(
|
||||||
// `${this.getPlaybookRunRoute(id)}`,
|
`${this.getPlaybookRunRoute(id)}`,
|
||||||
// {method: 'get', groupLabel},
|
{method: 'get', groupLabel},
|
||||||
// );
|
);
|
||||||
// };
|
};
|
||||||
|
|
||||||
// Run Management
|
// Run Management
|
||||||
// finishRun = async (playbookRunId: string) => {
|
// finishRun = async (playbookRunId: string) => {
|
||||||
|
|
|
||||||
169
app/products/playbooks/components/status_update_post/index.tsx
Normal file
169
app/products/playbooks/components/status_update_post/index.tsx
Normal file
|
|
@ -0,0 +1,169 @@
|
||||||
|
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||||
|
// See LICENSE.txt for license information.
|
||||||
|
|
||||||
|
import React, {useEffect} from 'react';
|
||||||
|
import {useIntl} from 'react-intl';
|
||||||
|
import {Text, View} from 'react-native';
|
||||||
|
|
||||||
|
import {fetchUsersByIds} from '@actions/remote/user';
|
||||||
|
import CompassIcon from '@components/compass_icon';
|
||||||
|
import Markdown from '@components/markdown';
|
||||||
|
import {useServerUrl} from '@context/server';
|
||||||
|
import {getMarkdownBlockStyles, getMarkdownTextStyles} from '@utils/markdown';
|
||||||
|
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
|
||||||
|
import {typography} from '@utils/typography';
|
||||||
|
|
||||||
|
import Participants from './participants';
|
||||||
|
|
||||||
|
import type PostModel from '@typings/database/models/servers/post';
|
||||||
|
import type {AvailableScreens} from '@typings/screens/navigation';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
location: AvailableScreens;
|
||||||
|
post: PostModel;
|
||||||
|
theme: Theme;
|
||||||
|
};
|
||||||
|
|
||||||
|
type StatusUpdatePostProps = {
|
||||||
|
authorUsername: string;
|
||||||
|
numTasks: number;
|
||||||
|
numTasksChecked: number;
|
||||||
|
participantIds: string[];
|
||||||
|
playbookRunId: string;
|
||||||
|
runName: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => {
|
||||||
|
return {
|
||||||
|
messageContainer: {
|
||||||
|
width: '100%',
|
||||||
|
},
|
||||||
|
icon: {
|
||||||
|
marginRight: 5,
|
||||||
|
},
|
||||||
|
reply: {
|
||||||
|
paddingRight: 10,
|
||||||
|
},
|
||||||
|
message: {
|
||||||
|
color: theme.centerChannelColor,
|
||||||
|
...typography('Body', 200),
|
||||||
|
lineHeight: undefined, // remove line height, not needed and causes problems with md images
|
||||||
|
},
|
||||||
|
pendingPost: {
|
||||||
|
opacity: 0.5,
|
||||||
|
},
|
||||||
|
updateContainer: {
|
||||||
|
marginTop: 5,
|
||||||
|
borderWidth: 1,
|
||||||
|
borderColor: changeOpacity(theme.centerChannelColor, 0.16),
|
||||||
|
padding: 12,
|
||||||
|
borderRadius: 4,
|
||||||
|
},
|
||||||
|
updateSeparator: {
|
||||||
|
width: '100%',
|
||||||
|
height: 1,
|
||||||
|
backgroundColor: changeOpacity(theme.centerChannelColor, 0.16),
|
||||||
|
marginVertical: 10,
|
||||||
|
},
|
||||||
|
detailsContainer: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
flexWrap: 'wrap',
|
||||||
|
gap: 8,
|
||||||
|
},
|
||||||
|
row: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
},
|
||||||
|
detailsText: {
|
||||||
|
color: changeOpacity(theme.centerChannelColor, 0.64),
|
||||||
|
...typography('Body', 75),
|
||||||
|
},
|
||||||
|
detailsSeparator: {
|
||||||
|
color: changeOpacity(theme.centerChannelColor, 0.24),
|
||||||
|
...typography('Body', 75),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
const StatusUpdatePost = ({location, post, theme}: Props) => {
|
||||||
|
const {authorUsername, numTasks, numTasksChecked, participantIds, playbookRunId, runName} = post.props as StatusUpdatePostProps;
|
||||||
|
const serverUrl = useServerUrl();
|
||||||
|
const style = getStyleSheet(theme);
|
||||||
|
const blockStyles = getMarkdownBlockStyles(theme);
|
||||||
|
const textStyles = getMarkdownTextStyles(theme);
|
||||||
|
const intl = useIntl();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetchUsersByIds(serverUrl, participantIds);
|
||||||
|
|
||||||
|
// Only do this on mount
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const updatePosted = intl.formatMessage({
|
||||||
|
id: 'playbooks.status_update_post.update',
|
||||||
|
defaultMessage: '@{authorUsername} posted an update for [{runName}]({link})',
|
||||||
|
}, {authorUsername, runName, link: `/playbooks/runs/${playbookRunId}`});
|
||||||
|
const tasks = intl.formatMessage({
|
||||||
|
id: 'playbooks.status_update_post.num_tasks',
|
||||||
|
defaultMessage: '**{numTasksChecked, number}** of **{numTasks, number}** {numTasks, plural, =1 {task} other {tasks}} checked',
|
||||||
|
}, {numTasksChecked, numTasks});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View style={style.messageContainer}>
|
||||||
|
<Markdown
|
||||||
|
baseTextStyle={style.message}
|
||||||
|
blockStyles={blockStyles}
|
||||||
|
channelId={post.channelId}
|
||||||
|
postId={post.id}
|
||||||
|
textStyles={textStyles}
|
||||||
|
value={updatePosted}
|
||||||
|
mentionKeys={authorUsername ? [{key: authorUsername, caseSensitive: false}] : []}
|
||||||
|
theme={theme}
|
||||||
|
location={location}
|
||||||
|
/>
|
||||||
|
<View style={style.updateContainer}>
|
||||||
|
<Markdown
|
||||||
|
baseTextStyle={style.message}
|
||||||
|
blockStyles={blockStyles}
|
||||||
|
channelId={post.channelId}
|
||||||
|
postId={post.id}
|
||||||
|
textStyles={textStyles}
|
||||||
|
value={post.message}
|
||||||
|
theme={theme}
|
||||||
|
location={location}
|
||||||
|
/>
|
||||||
|
<View style={style.updateSeparator}/>
|
||||||
|
<View style={style.detailsContainer}>
|
||||||
|
<View style={style.row}>
|
||||||
|
<CompassIcon
|
||||||
|
name='check-all'
|
||||||
|
size={14}
|
||||||
|
color={changeOpacity(theme.centerChannelColor, 0.64)}
|
||||||
|
style={style.icon}
|
||||||
|
/>
|
||||||
|
<Markdown
|
||||||
|
baseTextStyle={style.detailsText}
|
||||||
|
value={tasks}
|
||||||
|
textStyles={textStyles}
|
||||||
|
theme={theme}
|
||||||
|
location={location}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<Text style={style.detailsSeparator}>
|
||||||
|
{'•'}
|
||||||
|
</Text>
|
||||||
|
|
||||||
|
<Participants
|
||||||
|
participantIds={participantIds}
|
||||||
|
location={location}
|
||||||
|
baseTextStyle={style.detailsText}
|
||||||
|
textStyles={textStyles}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default StatusUpdatePost;
|
||||||
|
|
@ -0,0 +1,22 @@
|
||||||
|
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||||
|
// See LICENSE.txt for license information.
|
||||||
|
|
||||||
|
import {withDatabase, withObservables} from '@nozbe/watermelondb/react';
|
||||||
|
|
||||||
|
import {queryUsersById} from '@queries/servers/user';
|
||||||
|
|
||||||
|
import Participants from './participants';
|
||||||
|
|
||||||
|
import type {WithDatabaseArgs} from '@typings/database/database';
|
||||||
|
|
||||||
|
type Props = WithDatabaseArgs & {
|
||||||
|
participantIds: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
const enhanced = withObservables(['participantIds'], ({database, participantIds}: Props) => {
|
||||||
|
return {
|
||||||
|
users: queryUsersById(database, participantIds).observe(),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
export default withDatabase(enhanced(Participants));
|
||||||
|
|
@ -0,0 +1,128 @@
|
||||||
|
// 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, Text, TouchableOpacity, View, type StyleProp, type TextStyle} from 'react-native';
|
||||||
|
|
||||||
|
import CompassIcon from '@components/compass_icon';
|
||||||
|
import Markdown from '@components/markdown';
|
||||||
|
import UsersList from '@components/user_avatars_stack/users_list';
|
||||||
|
import {useTheme} from '@context/theme';
|
||||||
|
import {useIsTablet} from '@hooks/device';
|
||||||
|
import {usePreventDoubleTap} from '@hooks/utils';
|
||||||
|
import {BOTTOM_SHEET_ANDROID_OFFSET} from '@screens/bottom_sheet';
|
||||||
|
import {TITLE_HEIGHT} from '@screens/bottom_sheet/content';
|
||||||
|
import {bottomSheet} from '@screens/navigation';
|
||||||
|
import {bottomSheetSnapPoint} from '@utils/helpers';
|
||||||
|
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
|
||||||
|
import {typography} from '@utils/typography';
|
||||||
|
|
||||||
|
import type {UserModel} from '@database/models/server';
|
||||||
|
import type {MarkdownTextStyles} from '@typings/global/markdown';
|
||||||
|
import type {AvailableScreens} from '@typings/screens/navigation';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
participantIds: string[];
|
||||||
|
users: UserModel[];
|
||||||
|
location: AvailableScreens;
|
||||||
|
baseTextStyle: StyleProp<TextStyle>;
|
||||||
|
textStyles: MarkdownTextStyles;
|
||||||
|
}
|
||||||
|
|
||||||
|
const USER_ROW_HEIGHT = 40;
|
||||||
|
const MAX_USERS_DISPLAYED = 5;
|
||||||
|
const BOTTOM_SHEET_MAX_HEIGHT = '80%';
|
||||||
|
|
||||||
|
const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => ({
|
||||||
|
participantsContainer: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
},
|
||||||
|
icon: {
|
||||||
|
marginRight: 5,
|
||||||
|
},
|
||||||
|
listHeader: {
|
||||||
|
marginBottom: 12,
|
||||||
|
},
|
||||||
|
listHeaderText: {
|
||||||
|
color: theme.centerChannelColor,
|
||||||
|
...typography('Heading', 600, 'SemiBold'),
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
const Participants = ({baseTextStyle, participantIds, users, location, textStyles}: Props) => {
|
||||||
|
const intl = useIntl();
|
||||||
|
const theme = useTheme();
|
||||||
|
const isTablet = useIsTablet();
|
||||||
|
const style = getStyleSheet(theme);
|
||||||
|
|
||||||
|
const participants = intl.formatMessage({
|
||||||
|
id: 'playbooks.status_update_post.participants',
|
||||||
|
defaultMessage: '{numParticipants, number} {numParticipants, plural, =1 {participant} other {participants}}',
|
||||||
|
}, {numParticipants: participantIds.length});
|
||||||
|
|
||||||
|
const showParticipantsList = usePreventDoubleTap(useCallback(() => {
|
||||||
|
const bottomSheetTitle = intl.formatMessage({
|
||||||
|
id: 'playbooks.playbook_run.participants_title',
|
||||||
|
defaultMessage: 'Run Participants',
|
||||||
|
});
|
||||||
|
|
||||||
|
const renderContent = () => (
|
||||||
|
<>
|
||||||
|
{!isTablet && (
|
||||||
|
<View style={style.listHeader}>
|
||||||
|
<Text style={style.listHeaderText}>
|
||||||
|
{bottomSheetTitle}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
<UsersList
|
||||||
|
location={location}
|
||||||
|
users={users}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
|
||||||
|
let height = bottomSheetSnapPoint(Math.min(users.length, MAX_USERS_DISPLAYED), USER_ROW_HEIGHT) + TITLE_HEIGHT;
|
||||||
|
if (Platform.OS === 'android') {
|
||||||
|
height += BOTTOM_SHEET_ANDROID_OFFSET;
|
||||||
|
}
|
||||||
|
|
||||||
|
const snapPoints: Array<string | number> = [1, height];
|
||||||
|
if (users.length > MAX_USERS_DISPLAYED) {
|
||||||
|
snapPoints.push(BOTTOM_SHEET_MAX_HEIGHT);
|
||||||
|
}
|
||||||
|
|
||||||
|
bottomSheet({
|
||||||
|
closeButtonId: 'close-set-user-status',
|
||||||
|
renderContent,
|
||||||
|
initialSnapIndex: 1,
|
||||||
|
snapPoints,
|
||||||
|
title: bottomSheetTitle,
|
||||||
|
theme,
|
||||||
|
});
|
||||||
|
}, [users, intl, theme, isTablet, style.listHeader, style.listHeaderText, location]));
|
||||||
|
|
||||||
|
return (
|
||||||
|
<TouchableOpacity
|
||||||
|
onPress={showParticipantsList}
|
||||||
|
style={style.participantsContainer}
|
||||||
|
>
|
||||||
|
<CompassIcon
|
||||||
|
name='account-multiple-outline'
|
||||||
|
size={14}
|
||||||
|
color={changeOpacity(theme.centerChannelColor, 0.64)}
|
||||||
|
style={style.icon}
|
||||||
|
/>
|
||||||
|
<Markdown
|
||||||
|
baseTextStyle={baseTextStyle}
|
||||||
|
value={participants}
|
||||||
|
textStyles={textStyles}
|
||||||
|
theme={theme}
|
||||||
|
location={location}
|
||||||
|
/>
|
||||||
|
</TouchableOpacity>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Participants;
|
||||||
|
|
@ -2,3 +2,4 @@
|
||||||
// See LICENSE.txt for license information.
|
// See LICENSE.txt for license information.
|
||||||
|
|
||||||
export const PLAYBOOKS_PLUGIN_ID = 'playbooks';
|
export const PLAYBOOKS_PLUGIN_ID = 'playbooks';
|
||||||
|
export const PLAYBOOKS_UPDATE_STATUS_POST_TYPE = 'custom_run_update';
|
||||||
|
|
|
||||||
|
|
@ -35,6 +35,7 @@ type Props = {
|
||||||
componentId: AvailableScreens;
|
componentId: AvailableScreens;
|
||||||
isBookmarksEnabled: boolean;
|
isBookmarksEnabled: boolean;
|
||||||
isCallsEnabledInChannel: boolean;
|
isCallsEnabledInChannel: boolean;
|
||||||
|
isPlaybooksEnabled: boolean;
|
||||||
groupCallsAllowed: boolean;
|
groupCallsAllowed: boolean;
|
||||||
canManageMembers: boolean;
|
canManageMembers: boolean;
|
||||||
isConvertGMFeatureAvailable: boolean;
|
isConvertGMFeatureAvailable: boolean;
|
||||||
|
|
@ -70,6 +71,7 @@ const ChannelInfo = ({
|
||||||
componentId,
|
componentId,
|
||||||
isBookmarksEnabled,
|
isBookmarksEnabled,
|
||||||
isCallsEnabledInChannel,
|
isCallsEnabledInChannel,
|
||||||
|
isPlaybooksEnabled,
|
||||||
groupCallsAllowed,
|
groupCallsAllowed,
|
||||||
isConvertGMFeatureAvailable,
|
isConvertGMFeatureAvailable,
|
||||||
isCRTEnabled,
|
isCRTEnabled,
|
||||||
|
|
@ -139,6 +141,7 @@ const ChannelInfo = ({
|
||||||
canManageMembers={canManageMembers}
|
canManageMembers={canManageMembers}
|
||||||
isCRTEnabled={isCRTEnabled}
|
isCRTEnabled={isCRTEnabled}
|
||||||
canManageSettings={canManageSettings}
|
canManageSettings={canManageSettings}
|
||||||
|
isPlaybooksEnabled={isPlaybooksEnabled}
|
||||||
/>
|
/>
|
||||||
<View style={styles.separator}/>
|
<View style={styles.separator}/>
|
||||||
{convertGMOptionAvailable &&
|
{convertGMOptionAvailable &&
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@ import {distinctUntilChanged, switchMap, combineLatestWith} from 'rxjs/operators
|
||||||
import {observeIsCallsEnabledInChannel} from '@calls/observers';
|
import {observeIsCallsEnabledInChannel} from '@calls/observers';
|
||||||
import {observeCallsConfig} from '@calls/state';
|
import {observeCallsConfig} from '@calls/state';
|
||||||
import {withServerUrl} from '@context/server';
|
import {withServerUrl} from '@context/server';
|
||||||
|
import {observeIsPlaybooksEnabled} from '@playbooks/database/queries/version';
|
||||||
import {observeCurrentChannel} from '@queries/servers/channel';
|
import {observeCurrentChannel} from '@queries/servers/channel';
|
||||||
import {observeCanAddBookmarks} from '@queries/servers/channel_bookmark';
|
import {observeCanAddBookmarks} from '@queries/servers/channel_bookmark';
|
||||||
import {observeCanManageChannelMembers, observeCanManageChannelSettings} from '@queries/servers/role';
|
import {observeCanManageChannelMembers, observeCanManageChannelSettings} from '@queries/servers/role';
|
||||||
|
|
@ -146,6 +147,8 @@ const enhanced = withObservables([], ({serverUrl, database}: Props) => {
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const isPlaybooksEnabled = observeIsPlaybooksEnabled(database);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
type,
|
type,
|
||||||
canEnableDisableCalls,
|
canEnableDisableCalls,
|
||||||
|
|
@ -158,6 +161,7 @@ const enhanced = withObservables([], ({serverUrl, database}: Props) => {
|
||||||
isCRTEnabled: observeIsCRTEnabled(database),
|
isCRTEnabled: observeIsCRTEnabled(database),
|
||||||
isGuestUser,
|
isGuestUser,
|
||||||
isConvertGMFeatureAvailable,
|
isConvertGMFeatureAvailable,
|
||||||
|
isPlaybooksEnabled,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -28,6 +28,7 @@ describe('ChannelInfoOptions', () => {
|
||||||
canManageMembers: false,
|
canManageMembers: false,
|
||||||
isCRTEnabled: false,
|
isCRTEnabled: false,
|
||||||
canManageSettings: false,
|
canManageSettings: false,
|
||||||
|
isPlaybooksEnabled: true,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,7 @@ type Props = {
|
||||||
callsEnabled: boolean;
|
callsEnabled: boolean;
|
||||||
canManageMembers: boolean;
|
canManageMembers: boolean;
|
||||||
isCRTEnabled: boolean;
|
isCRTEnabled: boolean;
|
||||||
|
isPlaybooksEnabled: boolean;
|
||||||
canManageSettings: boolean;
|
canManageSettings: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -32,6 +33,7 @@ const Options = ({
|
||||||
callsEnabled,
|
callsEnabled,
|
||||||
canManageMembers,
|
canManageMembers,
|
||||||
isCRTEnabled,
|
isCRTEnabled,
|
||||||
|
isPlaybooksEnabled,
|
||||||
canManageSettings,
|
canManageSettings,
|
||||||
}: Props) => {
|
}: Props) => {
|
||||||
const isDMorGM = isTypeDMorGM(type);
|
const isDMorGM = isTypeDMorGM(type);
|
||||||
|
|
@ -49,10 +51,12 @@ const Options = ({
|
||||||
<NotificationPreference channelId={channelId}/>
|
<NotificationPreference channelId={channelId}/>
|
||||||
<PinnedMessages channelId={channelId}/>
|
<PinnedMessages channelId={channelId}/>
|
||||||
<ChannelFiles channelId={channelId}/>
|
<ChannelFiles channelId={channelId}/>
|
||||||
|
{isPlaybooksEnabled &&
|
||||||
<PlaybookRunsOption
|
<PlaybookRunsOption
|
||||||
channelId={channelId}
|
channelId={channelId}
|
||||||
location='channel_actions'
|
location='channel_actions'
|
||||||
/>
|
/>
|
||||||
|
}
|
||||||
{type !== General.DM_CHANNEL &&
|
{type !== General.DM_CHANNEL &&
|
||||||
<Members channelId={channelId}/>
|
<Members channelId={channelId}/>
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,7 @@ import SecurityManager from '@managers/security_manager';
|
||||||
import {getAllServers} from '@queries/app/servers';
|
import {getAllServers} from '@queries/app/servers';
|
||||||
import {findChannels, popToRoot} from '@screens/navigation';
|
import {findChannels, popToRoot} from '@screens/navigation';
|
||||||
import NavigationStore from '@store/navigation_store';
|
import NavigationStore from '@store/navigation_store';
|
||||||
import {alertInvalidDeepLink, handleDeepLink} from '@utils/deep_link';
|
import {alertInvalidDeepLink, parseAndHandleDeepLink} from '@utils/deep_link';
|
||||||
import {logError} from '@utils/log';
|
import {logError} from '@utils/log';
|
||||||
import {alertChannelArchived, alertChannelRemove, alertTeamRemove} from '@utils/navigation';
|
import {alertChannelArchived, alertChannelRemove, alertTeamRemove} from '@utils/navigation';
|
||||||
import {notificationError} from '@utils/notification';
|
import {notificationError} from '@utils/notification';
|
||||||
|
|
@ -136,7 +136,7 @@ export function HomeScreen(props: HomeProps) {
|
||||||
|
|
||||||
const deepLink = props.extra as DeepLinkWithData;
|
const deepLink = props.extra as DeepLinkWithData;
|
||||||
if (deepLink?.url) {
|
if (deepLink?.url) {
|
||||||
handleDeepLink(deepLink.url, intl, props.componentId, true).then((result) => {
|
parseAndHandleDeepLink(deepLink.url, intl, props.componentId, true).then((result) => {
|
||||||
if (result.error) {
|
if (result.error) {
|
||||||
alertInvalidDeepLink(intl);
|
alertInvalidDeepLink(intl);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -20,7 +20,7 @@ import {addNewServer} from '@utils/server';
|
||||||
|
|
||||||
import {alertErrorWithFallback, errorBadChannel, errorUnkownUser} from '../draft';
|
import {alertErrorWithFallback, errorBadChannel, errorUnkownUser} from '../draft';
|
||||||
|
|
||||||
import {alertInvalidDeepLink, extractServerUrl, getLaunchPropsFromDeepLink, handleDeepLink} from '.';
|
import {alertInvalidDeepLink, extractServerUrl, getLaunchPropsFromDeepLink, parseAndHandleDeepLink} from '.';
|
||||||
|
|
||||||
jest.mock('@actions/remote/user', () => ({
|
jest.mock('@actions/remote/user', () => ({
|
||||||
fetchUsersByUsernames: jest.fn(),
|
fetchUsersByUsernames: jest.fn(),
|
||||||
|
|
@ -94,7 +94,7 @@ describe('extractServerUrl', () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('handleDeepLink', () => {
|
describe('parseAndHandleDeepLink', () => {
|
||||||
const intl = createIntl({locale: 'en', messages: {}});
|
const intl = createIntl({locale: 'en', messages: {}});
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
|
|
@ -102,14 +102,14 @@ describe('handleDeepLink', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return error for invalid deep link', async () => {
|
it('should return error for invalid deep link', async () => {
|
||||||
const result = await handleDeepLink('invalid-url');
|
const result = await parseAndHandleDeepLink('invalid-url');
|
||||||
expect(result).toEqual({error: true});
|
expect(result).toEqual({error: true});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should add new server if not existing', async () => {
|
it('should add new server if not existing', async () => {
|
||||||
(getActiveServerUrl as jest.Mock).mockResolvedValueOnce('https://currentserver.com');
|
(getActiveServerUrl as jest.Mock).mockResolvedValueOnce('https://currentserver.com');
|
||||||
(DatabaseManager.searchUrl as jest.Mock).mockReturnValueOnce('');
|
(DatabaseManager.searchUrl as jest.Mock).mockReturnValueOnce('');
|
||||||
const result = await handleDeepLink('https://newserver.com/team/channels/town-square');
|
const result = await parseAndHandleDeepLink('https://newserver.com/team/channels/town-square');
|
||||||
expect(addNewServer).toHaveBeenCalledWith(Preferences.THEMES.denim, 'newserver.com', undefined, {type: DeepLink.Channel,
|
expect(addNewServer).toHaveBeenCalledWith(Preferences.THEMES.denim, 'newserver.com', undefined, {type: DeepLink.Channel,
|
||||||
data: {
|
data: {
|
||||||
serverUrl: 'newserver.com',
|
serverUrl: 'newserver.com',
|
||||||
|
|
@ -124,7 +124,7 @@ describe('handleDeepLink', () => {
|
||||||
it('should handle existing server and switch to home screen', async () => {
|
it('should handle existing server and switch to home screen', async () => {
|
||||||
(getActiveServerUrl as jest.Mock).mockResolvedValueOnce('https://currentserver.com');
|
(getActiveServerUrl as jest.Mock).mockResolvedValueOnce('https://currentserver.com');
|
||||||
(DatabaseManager.searchUrl as jest.Mock).mockReturnValueOnce('https://existingserver.com');
|
(DatabaseManager.searchUrl as jest.Mock).mockReturnValueOnce('https://existingserver.com');
|
||||||
const result = await handleDeepLink('https://existingserver.com/team/channels/town-square');
|
const result = await parseAndHandleDeepLink('https://existingserver.com/team/channels/town-square');
|
||||||
expect(dismissAllModalsAndPopToRoot).toHaveBeenCalled();
|
expect(dismissAllModalsAndPopToRoot).toHaveBeenCalled();
|
||||||
expect(DatabaseManager.setActiveServerDatabase).toHaveBeenCalledWith('https://existingserver.com');
|
expect(DatabaseManager.setActiveServerDatabase).toHaveBeenCalledWith('https://existingserver.com');
|
||||||
expect(WebsocketManager.initializeClient).toHaveBeenCalledWith('https://existingserver.com', 'DeepLink');
|
expect(WebsocketManager.initializeClient).toHaveBeenCalledWith('https://existingserver.com', 'DeepLink');
|
||||||
|
|
@ -136,7 +136,8 @@ describe('handleDeepLink', () => {
|
||||||
(DatabaseManager.searchUrl as jest.Mock).mockReturnValueOnce(null);
|
(DatabaseManager.searchUrl as jest.Mock).mockReturnValueOnce(null);
|
||||||
|
|
||||||
(NavigationStore.getVisibleScreen as jest.Mock).mockReturnValueOnce(Screens.SERVER);
|
(NavigationStore.getVisibleScreen as jest.Mock).mockReturnValueOnce(Screens.SERVER);
|
||||||
const result = await handleDeepLink('https://currentserver.com/team/channels/town-square', undefined, undefined, true);
|
const result = await parseAndHandleDeepLink('https://currentserver.com/team/channels/town-square', undefined, undefined, true);
|
||||||
|
console.log('result', JSON.stringify(result, null, 2));
|
||||||
const spyOnUpdateProps = jest.spyOn(Navigation, 'updateProps');
|
const spyOnUpdateProps = jest.spyOn(Navigation, 'updateProps');
|
||||||
expect(spyOnUpdateProps).toHaveBeenCalledWith(Screens.SERVER, {serverUrl: 'currentserver.com'});
|
expect(spyOnUpdateProps).toHaveBeenCalledWith(Screens.SERVER, {serverUrl: 'currentserver.com'});
|
||||||
expect(result).toEqual({error: false});
|
expect(result).toEqual({error: false});
|
||||||
|
|
@ -148,7 +149,7 @@ describe('handleDeepLink', () => {
|
||||||
|
|
||||||
(NavigationStore.getVisibleScreen as jest.Mock).mockReturnValueOnce(Screens.LOGIN);
|
(NavigationStore.getVisibleScreen as jest.Mock).mockReturnValueOnce(Screens.LOGIN);
|
||||||
(NavigationStore.getScreensInStack as jest.Mock).mockReturnValueOnce([Screens.SERVER, Screens.LOGIN]);
|
(NavigationStore.getScreensInStack as jest.Mock).mockReturnValueOnce([Screens.SERVER, Screens.LOGIN]);
|
||||||
const result = await handleDeepLink('https://currentserver.com/team/channels/town-square', undefined, undefined, true);
|
const result = await parseAndHandleDeepLink('https://currentserver.com/team/channels/town-square', undefined, undefined, true);
|
||||||
expect(addNewServer).not.toHaveBeenCalled();
|
expect(addNewServer).not.toHaveBeenCalled();
|
||||||
expect(result).toEqual({error: false});
|
expect(result).toEqual({error: false});
|
||||||
});
|
});
|
||||||
|
|
@ -156,7 +157,7 @@ describe('handleDeepLink', () => {
|
||||||
it('should switch to channel by name for Channel deep link', async () => {
|
it('should switch to channel by name for Channel deep link', async () => {
|
||||||
(DatabaseManager.searchUrl as jest.Mock).mockReturnValueOnce('https://existingserver.com');
|
(DatabaseManager.searchUrl as jest.Mock).mockReturnValueOnce('https://existingserver.com');
|
||||||
(getActiveServerUrl as jest.Mock).mockResolvedValueOnce('https://existingserver.com');
|
(getActiveServerUrl as jest.Mock).mockResolvedValueOnce('https://existingserver.com');
|
||||||
const result = await handleDeepLink('https://existingserver.com/team/channels/town-square', intl);
|
const result = await parseAndHandleDeepLink('https://existingserver.com/team/channels/town-square', intl);
|
||||||
expect(switchToChannelByName).toHaveBeenCalledWith('https://existingserver.com', 'town-square', 'team', errorBadChannel, intl);
|
expect(switchToChannelByName).toHaveBeenCalledWith('https://existingserver.com', 'town-square', 'team', errorBadChannel, intl);
|
||||||
expect(result).toEqual({error: false});
|
expect(result).toEqual({error: false});
|
||||||
});
|
});
|
||||||
|
|
@ -165,7 +166,7 @@ describe('handleDeepLink', () => {
|
||||||
(DatabaseManager.searchUrl as jest.Mock).mockReturnValueOnce('https://existingserver.com');
|
(DatabaseManager.searchUrl as jest.Mock).mockReturnValueOnce('https://existingserver.com');
|
||||||
(getActiveServerUrl as jest.Mock).mockResolvedValueOnce('https://existingserver.com');
|
(getActiveServerUrl as jest.Mock).mockResolvedValueOnce('https://existingserver.com');
|
||||||
(queryUsersByUsername as jest.Mock).mockReturnValueOnce({fetchIds: jest.fn(() => ['user-id'])});
|
(queryUsersByUsername as jest.Mock).mockReturnValueOnce({fetchIds: jest.fn(() => ['user-id'])});
|
||||||
const result = await handleDeepLink('https://existingserver.com/team/messages/@user-id', intl);
|
const result = await parseAndHandleDeepLink('https://existingserver.com/team/messages/@user-id', intl);
|
||||||
expect(makeDirectChannel).toHaveBeenCalledWith('https://existingserver.com', 'user-id', '', true);
|
expect(makeDirectChannel).toHaveBeenCalledWith('https://existingserver.com', 'user-id', '', true);
|
||||||
expect(result).toEqual({error: false});
|
expect(result).toEqual({error: false});
|
||||||
});
|
});
|
||||||
|
|
@ -175,7 +176,7 @@ describe('handleDeepLink', () => {
|
||||||
(getActiveServerUrl as jest.Mock).mockResolvedValueOnce('https://existingserver.com');
|
(getActiveServerUrl as jest.Mock).mockResolvedValueOnce('https://existingserver.com');
|
||||||
(queryUsersByUsername as jest.Mock).mockReturnValueOnce({fetchIds: jest.fn(() => [])});
|
(queryUsersByUsername as jest.Mock).mockReturnValueOnce({fetchIds: jest.fn(() => [])});
|
||||||
(fetchUsersByUsernames as jest.Mock).mockResolvedValueOnce({users: [{id: 'user-id'}]});
|
(fetchUsersByUsernames as jest.Mock).mockResolvedValueOnce({users: [{id: 'user-id'}]});
|
||||||
const result = await handleDeepLink('https://existingserver.com/team/messages/@user-id', intl);
|
const result = await parseAndHandleDeepLink('https://existingserver.com/team/messages/@user-id', intl);
|
||||||
expect(makeDirectChannel).toHaveBeenCalledWith('https://existingserver.com', 'user-id', '', true);
|
expect(makeDirectChannel).toHaveBeenCalledWith('https://existingserver.com', 'user-id', '', true);
|
||||||
expect(result).toEqual({error: false});
|
expect(result).toEqual({error: false});
|
||||||
});
|
});
|
||||||
|
|
@ -185,7 +186,7 @@ describe('handleDeepLink', () => {
|
||||||
(getActiveServerUrl as jest.Mock).mockResolvedValueOnce('https://existingserver.com');
|
(getActiveServerUrl as jest.Mock).mockResolvedValueOnce('https://existingserver.com');
|
||||||
(queryUsersByUsername as jest.Mock).mockReturnValueOnce({fetchIds: jest.fn(() => [])});
|
(queryUsersByUsername as jest.Mock).mockReturnValueOnce({fetchIds: jest.fn(() => [])});
|
||||||
(fetchUsersByUsernames as jest.Mock).mockResolvedValueOnce({users: []});
|
(fetchUsersByUsernames as jest.Mock).mockResolvedValueOnce({users: []});
|
||||||
const result = await handleDeepLink('https://existingserver.com/team/messages/@user-id', intl);
|
const result = await parseAndHandleDeepLink('https://existingserver.com/team/messages/@user-id', intl);
|
||||||
expect(errorUnkownUser).toHaveBeenCalledWith(intl);
|
expect(errorUnkownUser).toHaveBeenCalledWith(intl);
|
||||||
expect(result).toEqual({error: false});
|
expect(result).toEqual({error: false});
|
||||||
});
|
});
|
||||||
|
|
@ -193,7 +194,7 @@ describe('handleDeepLink', () => {
|
||||||
it('should switch to group message channel for GroupMessage deep link', async () => {
|
it('should switch to group message channel for GroupMessage deep link', async () => {
|
||||||
(DatabaseManager.searchUrl as jest.Mock).mockReturnValueOnce('https://existingserver.com');
|
(DatabaseManager.searchUrl as jest.Mock).mockReturnValueOnce('https://existingserver.com');
|
||||||
(getActiveServerUrl as jest.Mock).mockResolvedValueOnce('https://existingserver.com');
|
(getActiveServerUrl as jest.Mock).mockResolvedValueOnce('https://existingserver.com');
|
||||||
const result = await handleDeepLink('https://existingserver.com/team/messages/7b35c77a645e1906e03a2c330f89203385db102f', intl);
|
const result = await parseAndHandleDeepLink('https://existingserver.com/team/messages/7b35c77a645e1906e03a2c330f89203385db102f', intl);
|
||||||
expect(switchToChannelByName).toHaveBeenCalledWith('https://existingserver.com', '7b35c77a645e1906e03a2c330f89203385db102f', 'team', errorBadChannel, intl);
|
expect(switchToChannelByName).toHaveBeenCalledWith('https://existingserver.com', '7b35c77a645e1906e03a2c330f89203385db102f', 'team', errorBadChannel, intl);
|
||||||
expect(result).toEqual({error: false});
|
expect(result).toEqual({error: false});
|
||||||
});
|
});
|
||||||
|
|
@ -202,7 +203,7 @@ describe('handleDeepLink', () => {
|
||||||
(DatabaseManager.searchUrl as jest.Mock).mockReturnValueOnce('https://existingserver.com');
|
(DatabaseManager.searchUrl as jest.Mock).mockReturnValueOnce('https://existingserver.com');
|
||||||
(getActiveServerUrl as jest.Mock).mockResolvedValueOnce('https://existingserver.com');
|
(getActiveServerUrl as jest.Mock).mockResolvedValueOnce('https://existingserver.com');
|
||||||
const postid = '7b35c77a645e1906e03a2c330f';
|
const postid = '7b35c77a645e1906e03a2c330f';
|
||||||
const result = await handleDeepLink(`https://existingserver.com/team/pl/${postid}`, intl);
|
const result = await parseAndHandleDeepLink(`https://existingserver.com/team/pl/${postid}`, intl);
|
||||||
expect(showPermalink).toHaveBeenCalledWith('https://existingserver.com', 'team', postid);
|
expect(showPermalink).toHaveBeenCalledWith('https://existingserver.com', 'team', postid);
|
||||||
expect(result).toEqual({error: false});
|
expect(result).toEqual({error: false});
|
||||||
});
|
});
|
||||||
|
|
@ -211,7 +212,7 @@ describe('handleDeepLink', () => {
|
||||||
(getActiveServerUrl as jest.Mock).mockImplementationOnce(() => {
|
(getActiveServerUrl as jest.Mock).mockImplementationOnce(() => {
|
||||||
throw new Error('DB does not exist error');
|
throw new Error('DB does not exist error');
|
||||||
});
|
});
|
||||||
const result = await handleDeepLink('https://existingserver.com/team/messages/7b35c77a645e1906e03a2c330f89203385db102f');
|
const result = await parseAndHandleDeepLink('https://existingserver.com/team/messages/7b35c77a645e1906e03a2c330f89203385db102f');
|
||||||
expect(logError).toHaveBeenCalledWith('Failed to open channel from deeplink', expect.any(Error), undefined);
|
expect(logError).toHaveBeenCalledWith('Failed to open channel from deeplink', expect.any(Error), undefined);
|
||||||
expect(result).toEqual({error: true});
|
expect(result).toEqual({error: true});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@
|
||||||
|
|
||||||
import {match} from 'path-to-regexp';
|
import {match} from 'path-to-regexp';
|
||||||
import {type IntlShape} from 'react-intl';
|
import {type IntlShape} from 'react-intl';
|
||||||
|
import {Alert} from 'react-native';
|
||||||
import {Navigation} from 'react-native-navigation';
|
import {Navigation} from 'react-native-navigation';
|
||||||
import urlParse from 'url-parse';
|
import urlParse from 'url-parse';
|
||||||
|
|
||||||
|
|
@ -15,6 +16,10 @@ import {getDefaultThemeByAppearance} from '@context/theme';
|
||||||
import DatabaseManager from '@database/manager';
|
import DatabaseManager from '@database/manager';
|
||||||
import {DEFAULT_LOCALE, t} from '@i18n';
|
import {DEFAULT_LOCALE, t} from '@i18n';
|
||||||
import WebsocketManager from '@managers/websocket_manager';
|
import WebsocketManager from '@managers/websocket_manager';
|
||||||
|
import {fetchPlaybookRun} from '@playbooks/actions/remote/runs';
|
||||||
|
import {getPlaybookRunById} from '@playbooks/database/queries/run';
|
||||||
|
import {fetchIsPlaybooksEnabled} from '@playbooks/database/queries/version';
|
||||||
|
import {goToPlaybookRun} from '@playbooks/screens/navigation';
|
||||||
import {getActiveServerUrl} from '@queries/app/servers';
|
import {getActiveServerUrl} from '@queries/app/servers';
|
||||||
import {getCurrentUser, queryUsersByUsername} from '@queries/servers/user';
|
import {getCurrentUser, queryUsersByUsername} from '@queries/servers/user';
|
||||||
import {dismissAllModalsAndPopToRoot} from '@screens/navigation';
|
import {dismissAllModalsAndPopToRoot} from '@screens/navigation';
|
||||||
|
|
@ -32,28 +37,27 @@ import {
|
||||||
ID_PATH_PATTERN,
|
ID_PATH_PATTERN,
|
||||||
} from '@utils/url/path';
|
} from '@utils/url/path';
|
||||||
|
|
||||||
import type {DeepLinkChannel, DeepLinkDM, DeepLinkGM, DeepLinkPermalink, DeepLinkWithData, LaunchProps} from '@typings/launch';
|
import type {DeepLinkChannel, DeepLinkDM, DeepLinkGM, DeepLinkPermalink, DeepLinkPlaybookRuns, DeepLinkWithData, LaunchProps} from '@typings/launch';
|
||||||
import type {AvailableScreens} from '@typings/screens/navigation';
|
import type {AvailableScreens} from '@typings/screens/navigation';
|
||||||
|
|
||||||
const deepLinkScreens: AvailableScreens[] = [Screens.HOME, Screens.CHANNEL, Screens.GLOBAL_THREADS, Screens.THREAD];
|
const deepLinkScreens: AvailableScreens[] = [Screens.HOME, Screens.CHANNEL, Screens.GLOBAL_THREADS, Screens.THREAD];
|
||||||
|
|
||||||
export async function handleDeepLink(deepLinkUrl: string, intlShape?: IntlShape, location?: string, asServer = false) {
|
export async function handleDeepLink(deepLink: DeepLinkWithData, intlShape?: IntlShape, location?: string) {
|
||||||
try {
|
try {
|
||||||
const parsed = parseDeepLink(deepLinkUrl, asServer);
|
if (deepLink.type === DeepLink.Invalid || !deepLink.data || !deepLink.data.serverUrl) {
|
||||||
if (parsed.type === DeepLink.Invalid || !parsed.data || !parsed.data.serverUrl) {
|
|
||||||
return {error: true};
|
return {error: true};
|
||||||
}
|
}
|
||||||
|
|
||||||
const currentServerUrl = await getActiveServerUrl();
|
const currentServerUrl = await getActiveServerUrl();
|
||||||
const existingServerUrl = DatabaseManager.searchUrl(parsed.data.serverUrl);
|
const existingServerUrl = DatabaseManager.searchUrl(deepLink.data.serverUrl);
|
||||||
|
|
||||||
// After checking the server for http & https then we add it
|
// After checking the server for http & https then we add it
|
||||||
if (!existingServerUrl) {
|
if (!existingServerUrl) {
|
||||||
const theme = EphemeralStore.theme || getDefaultThemeByAppearance();
|
const theme = EphemeralStore.theme || getDefaultThemeByAppearance();
|
||||||
if (NavigationStore.getVisibleScreen() === Screens.SERVER) {
|
if (NavigationStore.getVisibleScreen() === Screens.SERVER) {
|
||||||
Navigation.updateProps(Screens.SERVER, {serverUrl: parsed.data.serverUrl});
|
Navigation.updateProps(Screens.SERVER, {serverUrl: deepLink.data.serverUrl});
|
||||||
} else if (!NavigationStore.getScreensInStack().includes(Screens.SERVER)) {
|
} else if (!NavigationStore.getScreensInStack().includes(Screens.SERVER)) {
|
||||||
addNewServer(theme, parsed.data.serverUrl, undefined, parsed);
|
addNewServer(theme, deepLink.data.serverUrl, undefined, deepLink);
|
||||||
}
|
}
|
||||||
return {error: false};
|
return {error: false};
|
||||||
}
|
}
|
||||||
|
|
@ -70,14 +74,14 @@ export async function handleDeepLink(deepLinkUrl: string, intlShape?: IntlShape,
|
||||||
const locale = currentUser?.locale || DEFAULT_LOCALE;
|
const locale = currentUser?.locale || DEFAULT_LOCALE;
|
||||||
const intl = intlShape || getIntlShape(locale);
|
const intl = intlShape || getIntlShape(locale);
|
||||||
|
|
||||||
switch (parsed.type) {
|
switch (deepLink.type) {
|
||||||
case DeepLink.Channel: {
|
case DeepLink.Channel: {
|
||||||
const deepLinkData = parsed.data as DeepLinkChannel;
|
const deepLinkData = deepLink.data as DeepLinkChannel;
|
||||||
switchToChannelByName(existingServerUrl, deepLinkData.channelName, deepLinkData.teamName, errorBadChannel, intl);
|
switchToChannelByName(existingServerUrl, deepLinkData.channelName, deepLinkData.teamName, errorBadChannel, intl);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case DeepLink.DirectMessage: {
|
case DeepLink.DirectMessage: {
|
||||||
const deepLinkData = parsed.data as DeepLinkDM;
|
const deepLinkData = deepLink.data as DeepLinkDM;
|
||||||
const userIds = await queryUsersByUsername(database, [deepLinkData.userName]).fetchIds();
|
const userIds = await queryUsersByUsername(database, [deepLinkData.userName]).fetchIds();
|
||||||
let userId = userIds.length ? userIds[0] : undefined;
|
let userId = userIds.length ? userIds[0] : undefined;
|
||||||
if (!userId) {
|
if (!userId) {
|
||||||
|
|
@ -95,12 +99,12 @@ export async function handleDeepLink(deepLinkUrl: string, intlShape?: IntlShape,
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case DeepLink.GroupMessage: {
|
case DeepLink.GroupMessage: {
|
||||||
const deepLinkData = parsed.data as DeepLinkGM;
|
const deepLinkData = deepLink.data as DeepLinkGM;
|
||||||
switchToChannelByName(existingServerUrl, deepLinkData.channelName, deepLinkData.teamName, errorBadChannel, intl);
|
switchToChannelByName(existingServerUrl, deepLinkData.channelName, deepLinkData.teamName, errorBadChannel, intl);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case DeepLink.Permalink: {
|
case DeepLink.Permalink: {
|
||||||
const deepLinkData = parsed.data as DeepLinkPermalink;
|
const deepLinkData = deepLink.data as DeepLinkPermalink;
|
||||||
if (
|
if (
|
||||||
NavigationStore.hasModalsOpened() ||
|
NavigationStore.hasModalsOpened() ||
|
||||||
!deepLinkScreens.includes(NavigationStore.getVisibleScreen())
|
!deepLinkScreens.includes(NavigationStore.getVisibleScreen())
|
||||||
|
|
@ -110,6 +114,49 @@ export async function handleDeepLink(deepLinkUrl: string, intlShape?: IntlShape,
|
||||||
showPermalink(existingServerUrl, deepLinkData.teamName, deepLinkData.postId);
|
showPermalink(existingServerUrl, deepLinkData.teamName, deepLinkData.postId);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
case DeepLink.Playbooks: {
|
||||||
|
// Alert that playbooks should be access from the webapp or desktop app
|
||||||
|
Alert.alert(
|
||||||
|
intl.formatMessage({id: 'playbooks.only_runs_available.title', defaultMessage: 'Playbooks not available'}),
|
||||||
|
intl.formatMessage({id: 'playbooks.only_runs_available.description', defaultMessage: 'Only Playbook Runs are available on mobile. To access the Playbook, please use the desktop or web app.'}),
|
||||||
|
[{
|
||||||
|
text: intl.formatMessage({id: 'playbooks.only_runs_available.ok', defaultMessage: 'OK'}),
|
||||||
|
}],
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case DeepLink.PlaybookRuns: {
|
||||||
|
const deepLinkData = deepLink.data as DeepLinkPlaybookRuns;
|
||||||
|
const playbookEnabled = await fetchIsPlaybooksEnabled(database);
|
||||||
|
if (playbookEnabled) {
|
||||||
|
// Go to playbook Run
|
||||||
|
const playbook = await getPlaybookRunById(database, deepLinkData.playbookRunId);
|
||||||
|
if (!playbook) {
|
||||||
|
const {error} = await fetchPlaybookRun(existingServerUrl, deepLinkData.playbookRunId);
|
||||||
|
if (error) {
|
||||||
|
Alert.alert(
|
||||||
|
intl.formatMessage({id: 'playbooks.fetch_error.title', defaultMessage: 'Unable to open Run'}),
|
||||||
|
intl.formatMessage({id: 'playbooks.fetch_error.description', defaultMessage: "You don't have permission to view this run, or it may no longer exist."}),
|
||||||
|
[{
|
||||||
|
text: intl.formatMessage({id: 'playbooks.fetch_error.OK', defaultMessage: 'Okay'}),
|
||||||
|
}],
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
goToPlaybookRun(intl, deepLinkData.playbookRunId);
|
||||||
|
} else {
|
||||||
|
// Alert playbooks not enabled or version not supported
|
||||||
|
Alert.alert(
|
||||||
|
intl.formatMessage({id: 'playbooks.not_enabled_or_unsupported.title', defaultMessage: 'Playbooks not available'}),
|
||||||
|
intl.formatMessage({id: 'playbooks.not_enabled_or_unsupported.description', defaultMessage: 'Playbooks are either not enabled on this server or the Playbooks version is not supported. Please contact your system administrator.'}),
|
||||||
|
[{
|
||||||
|
text: intl.formatMessage({id: 'playbooks.not_enabled_or_unsupported.OK', defaultMessage: 'OK'}),
|
||||||
|
}],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return {error: false};
|
return {error: false};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|
@ -118,6 +165,11 @@ export async function handleDeepLink(deepLinkUrl: string, intlShape?: IntlShape,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function parseAndHandleDeepLink(deepLinkUrl: string, intlShape?: IntlShape, location?: string, asServer = false) {
|
||||||
|
const parsed = parseDeepLink(deepLinkUrl, asServer);
|
||||||
|
return handleDeepLink(parsed, intlShape, location);
|
||||||
|
}
|
||||||
|
|
||||||
type ChannelPathParams = {
|
type ChannelPathParams = {
|
||||||
hostname: string;
|
hostname: string;
|
||||||
serverUrl: string[];
|
serverUrl: string[];
|
||||||
|
|
@ -129,6 +181,22 @@ type ChannelPathParams = {
|
||||||
const CHANNEL_PATH = '*serverUrl/:teamName/:path/:identifier';
|
const CHANNEL_PATH = '*serverUrl/:teamName/:path/:identifier';
|
||||||
export const matchChannelDeeplink = match<ChannelPathParams>(CHANNEL_PATH);
|
export const matchChannelDeeplink = match<ChannelPathParams>(CHANNEL_PATH);
|
||||||
|
|
||||||
|
type PlaybooksPathParams = {
|
||||||
|
serverUrl: string[];
|
||||||
|
playbookId: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const PLAYBOOKS_PATH = '*serverUrl/playbooks/playbooks/:playbookId';
|
||||||
|
export const matchPlaybooksDeeplink = match<PlaybooksPathParams>(PLAYBOOKS_PATH);
|
||||||
|
|
||||||
|
type PlaybookRunsPathParams = {
|
||||||
|
serverUrl: string[];
|
||||||
|
playbookRunId: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const PLAYBOOK_RUNS_PATH = '*serverUrl/playbooks/runs/:playbookRunId';
|
||||||
|
export const matchPlaybookRunsDeeplink = match<PlaybookRunsPathParams>(PLAYBOOK_RUNS_PATH);
|
||||||
|
|
||||||
type PermalinkPathParams = {
|
type PermalinkPathParams = {
|
||||||
serverUrl: string[];
|
serverUrl: string[];
|
||||||
teamName: string;
|
teamName: string;
|
||||||
|
|
@ -204,14 +272,16 @@ function isValidIdentifierPathPattern(id: string): boolean {
|
||||||
return regex.test(id);
|
return regex.test(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
function isValidPostId(id: string): boolean {
|
function isValidId(id: string): boolean {
|
||||||
const regex = new RegExp(`^${ID_PATH_PATTERN}$`);
|
const regex = new RegExp(`^${ID_PATH_PATTERN}$`);
|
||||||
return regex.test(id);
|
return regex.test(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function parseDeepLink(deepLinkUrl: string, asServer = false): DeepLinkWithData {
|
export function parseDeepLink(deepLinkUrl: string, asServer = false): DeepLinkWithData {
|
||||||
try {
|
try {
|
||||||
const url = removeProtocol(deepLinkUrl);
|
const parsedUrl = urlParse(deepLinkUrl);
|
||||||
|
const urlWithoutQuery = stripTrailingSlashes(parsedUrl.protocol + '//' + parsedUrl.host + parsedUrl.pathname);
|
||||||
|
const url = removeProtocol(urlWithoutQuery);
|
||||||
|
|
||||||
const channelMatch = matchChannelDeeplink(url);
|
const channelMatch = matchChannelDeeplink(url);
|
||||||
if (channelMatch && isValidTeamName(channelMatch.params.teamName) && isValidIdentifierPathPattern(channelMatch.params.identifier)) {
|
if (channelMatch && isValidTeamName(channelMatch.params.teamName) && isValidIdentifierPathPattern(channelMatch.params.identifier)) {
|
||||||
|
|
@ -231,11 +301,23 @@ export function parseDeepLink(deepLinkUrl: string, asServer = false): DeepLinkWi
|
||||||
}
|
}
|
||||||
|
|
||||||
const permalinkMatch = matchPermalinkDeeplink(url);
|
const permalinkMatch = matchPermalinkDeeplink(url);
|
||||||
if (permalinkMatch && isValidTeamName(permalinkMatch.params.teamName) && isValidPostId(permalinkMatch.params.postId)) {
|
if (permalinkMatch && isValidTeamName(permalinkMatch.params.teamName) && isValidId(permalinkMatch.params.postId)) {
|
||||||
const {params: {serverUrl, teamName, postId}} = permalinkMatch;
|
const {params: {serverUrl, teamName, postId}} = permalinkMatch;
|
||||||
return {type: DeepLink.Permalink, url: deepLinkUrl, data: {serverUrl: serverUrl.join('/'), teamName, postId}};
|
return {type: DeepLink.Permalink, url: deepLinkUrl, data: {serverUrl: serverUrl.join('/'), teamName, postId}};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const playbooksMatch = matchPlaybooksDeeplink(url);
|
||||||
|
if (playbooksMatch && isValidId(playbooksMatch.params.playbookId)) {
|
||||||
|
const {params: {serverUrl, playbookId}} = playbooksMatch;
|
||||||
|
return {type: DeepLink.Playbooks, url: deepLinkUrl, data: {serverUrl: serverUrl.join('/'), playbookId}};
|
||||||
|
}
|
||||||
|
|
||||||
|
const playbooksRunsMatch = matchPlaybookRunsDeeplink(url);
|
||||||
|
if (playbooksRunsMatch && isValidId(playbooksRunsMatch.params.playbookRunId)) {
|
||||||
|
const {params: {serverUrl, playbookRunId}} = playbooksRunsMatch;
|
||||||
|
return {type: DeepLink.PlaybookRuns, url: deepLinkUrl, data: {serverUrl: serverUrl.join('/'), playbookRunId}};
|
||||||
|
}
|
||||||
|
|
||||||
if (asServer) {
|
if (asServer) {
|
||||||
const serverMatch = extractServerUrl(url);
|
const serverMatch = extractServerUrl(url);
|
||||||
if (serverMatch) {
|
if (serverMatch) {
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@
|
||||||
import {createIntl} from 'react-intl';
|
import {createIntl} from 'react-intl';
|
||||||
import {Alert} from 'react-native';
|
import {Alert} from 'react-native';
|
||||||
|
|
||||||
import {onOpenLinkError, openLink} from './links';
|
import * as Links from './links';
|
||||||
|
|
||||||
describe('onOpenLinkError', () => {
|
describe('onOpenLinkError', () => {
|
||||||
const intl = createIntl({
|
const intl = createIntl({
|
||||||
|
|
@ -26,7 +26,7 @@ describe('onOpenLinkError', () => {
|
||||||
mockReturnValueOnce('Error').
|
mockReturnValueOnce('Error').
|
||||||
mockReturnValueOnce('Unable to open the link.'); // Mock message
|
mockReturnValueOnce('Unable to open the link.'); // Mock message
|
||||||
|
|
||||||
onOpenLinkError(intl);
|
Links.onOpenLinkError(intl);
|
||||||
|
|
||||||
expect(intl.formatMessage).toHaveBeenCalledTimes(2);
|
expect(intl.formatMessage).toHaveBeenCalledTimes(2);
|
||||||
expect(intl.formatMessage).toHaveBeenCalledWith({
|
expect(intl.formatMessage).toHaveBeenCalledWith({
|
||||||
|
|
@ -55,7 +55,7 @@ describe('openLink', () => {
|
||||||
it('should call normalizeProtocol and return early if the URL is invalid', async () => {
|
it('should call normalizeProtocol and return early if the URL is invalid', async () => {
|
||||||
const normalizeProtocolMock = jest.spyOn(require('.'), 'normalizeProtocol').mockReturnValue(null);
|
const normalizeProtocolMock = jest.spyOn(require('.'), 'normalizeProtocol').mockReturnValue(null);
|
||||||
|
|
||||||
await openLink('', 'https://server-url.com', 'https://site-url.com', intl);
|
await Links.openLink('', 'https://server-url.com', 'https://site-url.com', intl);
|
||||||
|
|
||||||
expect(normalizeProtocolMock).toHaveBeenCalledWith('');
|
expect(normalizeProtocolMock).toHaveBeenCalledWith('');
|
||||||
normalizeProtocolMock.mockRestore();
|
normalizeProtocolMock.mockRestore();
|
||||||
|
|
@ -66,11 +66,11 @@ describe('openLink', () => {
|
||||||
const matchDeepLinkMock = jest.spyOn(require('@utils/deep_link'), 'matchDeepLink').mockReturnValue({url: 'https://example.com'});
|
const matchDeepLinkMock = jest.spyOn(require('@utils/deep_link'), 'matchDeepLink').mockReturnValue({url: 'https://example.com'});
|
||||||
const handleDeepLinkMock = jest.spyOn(require('@utils/deep_link'), 'handleDeepLink').mockResolvedValue({error: null});
|
const handleDeepLinkMock = jest.spyOn(require('@utils/deep_link'), 'handleDeepLink').mockResolvedValue({error: null});
|
||||||
|
|
||||||
await openLink('https://example.com', 'https://server-url.com', 'https://site-url.com', intl);
|
await Links.openLink('https://example.com', 'https://server-url.com', 'https://site-url.com', intl);
|
||||||
|
|
||||||
expect(normalizeProtocolMock).toHaveBeenCalledWith('https://example.com');
|
expect(normalizeProtocolMock).toHaveBeenCalledWith('https://example.com');
|
||||||
expect(matchDeepLinkMock).toHaveBeenCalledWith('https://example.com', 'https://server-url.com', 'https://site-url.com');
|
expect(matchDeepLinkMock).toHaveBeenCalledWith('https://example.com', 'https://server-url.com', 'https://site-url.com');
|
||||||
expect(handleDeepLinkMock).toHaveBeenCalledWith('https://example.com', intl);
|
expect(handleDeepLinkMock).toHaveBeenCalledWith({url: 'https://example.com'}, intl);
|
||||||
|
|
||||||
normalizeProtocolMock.mockRestore();
|
normalizeProtocolMock.mockRestore();
|
||||||
matchDeepLinkMock.mockRestore();
|
matchDeepLinkMock.mockRestore();
|
||||||
|
|
@ -83,9 +83,9 @@ describe('openLink', () => {
|
||||||
const handleDeepLinkMock = jest.spyOn(require('@utils/deep_link'), 'handleDeepLink').mockResolvedValue({error: true});
|
const handleDeepLinkMock = jest.spyOn(require('@utils/deep_link'), 'handleDeepLink').mockResolvedValue({error: true});
|
||||||
const tryOpenURLMock = jest.spyOn(require('.'), 'tryOpenURL').mockImplementation(() => {});
|
const tryOpenURLMock = jest.spyOn(require('.'), 'tryOpenURL').mockImplementation(() => {});
|
||||||
|
|
||||||
await openLink('https://example.com', 'https://server-url.com', 'https://site-url.com', intl);
|
await Links.openLink('https://example.com', 'https://server-url.com', 'https://site-url.com', intl);
|
||||||
|
|
||||||
expect(tryOpenURLMock).toHaveBeenCalledWith('https://example.com', onOpenLinkError);
|
expect(tryOpenURLMock).toHaveBeenCalledWith('https://example.com', expect.any(Function));
|
||||||
|
|
||||||
normalizeProtocolMock.mockRestore();
|
normalizeProtocolMock.mockRestore();
|
||||||
matchDeepLinkMock.mockRestore();
|
matchDeepLinkMock.mockRestore();
|
||||||
|
|
@ -98,9 +98,9 @@ describe('openLink', () => {
|
||||||
const matchDeepLinkMock = jest.spyOn(require('@utils/deep_link'), 'matchDeepLink').mockReturnValue(null);
|
const matchDeepLinkMock = jest.spyOn(require('@utils/deep_link'), 'matchDeepLink').mockReturnValue(null);
|
||||||
const tryOpenURLMock = jest.spyOn(require('.'), 'tryOpenURL').mockImplementation(() => {});
|
const tryOpenURLMock = jest.spyOn(require('.'), 'tryOpenURL').mockImplementation(() => {});
|
||||||
|
|
||||||
await openLink('https://example.com', 'https://server-url.com', 'https://site-url.com', intl);
|
await Links.openLink('https://example.com', 'https://server-url.com', 'https://site-url.com', intl);
|
||||||
|
|
||||||
expect(tryOpenURLMock).toHaveBeenCalledWith('https://example.com', onOpenLinkError);
|
expect(tryOpenURLMock).toHaveBeenCalledWith('https://example.com', expect.any(Function));
|
||||||
|
|
||||||
normalizeProtocolMock.mockRestore();
|
normalizeProtocolMock.mockRestore();
|
||||||
matchDeepLinkMock.mockRestore();
|
matchDeepLinkMock.mockRestore();
|
||||||
|
|
|
||||||
|
|
@ -31,11 +31,15 @@ export const openLink = async (link: string, serverUrl: string, siteURL: string,
|
||||||
const match = matchDeepLink(url, serverUrl, siteURL);
|
const match = matchDeepLink(url, serverUrl, siteURL);
|
||||||
|
|
||||||
if (match) {
|
if (match) {
|
||||||
const {error} = await handleDeepLink(match.url, intl);
|
const {error} = await handleDeepLink(match, intl);
|
||||||
if (error) {
|
if (error) {
|
||||||
tryOpenURL(match.url, onOpenLinkError);
|
tryOpenURL(match.url, () => {
|
||||||
|
onOpenLinkError(intl);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
tryOpenURL(url, onOpenLinkError);
|
tryOpenURL(url, () => {
|
||||||
|
onOpenLinkError(intl);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -996,6 +996,15 @@
|
||||||
"playbook.participants": "Run Participants",
|
"playbook.participants": "Run Participants",
|
||||||
"playbook.runs.finished": "Finished",
|
"playbook.runs.finished": "Finished",
|
||||||
"playbook.runs.in-progress": "In Progress",
|
"playbook.runs.in-progress": "In Progress",
|
||||||
|
"playbooks.fetch_error.description": "You don't have permission to view this run, or it may no longer exist.",
|
||||||
|
"playbooks.fetch_error.OK": "Okay",
|
||||||
|
"playbooks.fetch_error.title": "Unable to open Run",
|
||||||
|
"playbooks.not_enabled_or_unsupported.description": "Playbooks are either not enabled on this server or the Playbooks version is not supported. Please contact your system administrator.",
|
||||||
|
"playbooks.not_enabled_or_unsupported.OK": "OK",
|
||||||
|
"playbooks.not_enabled_or_unsupported.title": "Playbooks not available",
|
||||||
|
"playbooks.only_runs_available.description": "Only Playbook Runs are available on mobile. To access the Playbook, please use the desktop or web app.",
|
||||||
|
"playbooks.only_runs_available.ok": "OK",
|
||||||
|
"playbooks.only_runs_available.title": "Playbooks not available",
|
||||||
"playbooks.playbook_run.error.description": "Please check your network connection or try again later.",
|
"playbooks.playbook_run.error.description": "Please check your network connection or try again later.",
|
||||||
"playbooks.playbook_run.error.title": "Unable to fetch run details",
|
"playbooks.playbook_run.error.title": "Unable to fetch run details",
|
||||||
"playbooks.playbook_run.finished": "Finished",
|
"playbooks.playbook_run.finished": "Finished",
|
||||||
|
|
@ -1015,6 +1024,9 @@
|
||||||
"playbooks.runs.in_progress.description": "When a run starts in this channel, you’ll see it here.",
|
"playbooks.runs.in_progress.description": "When a run starts in this channel, you’ll see it here.",
|
||||||
"playbooks.runs.in_progress.title": "No in progress runs",
|
"playbooks.runs.in_progress.title": "No in progress runs",
|
||||||
"playbooks.runs.show_more": "Show More",
|
"playbooks.runs.show_more": "Show More",
|
||||||
|
"playbooks.status_update_post.num_tasks": "**{numTasksChecked, number}** of **{numTasks, number}** {numTasks, plural, =1 {task} other {tasks}} checked",
|
||||||
|
"playbooks.status_update_post.participants": "{numParticipants, number} {numParticipants, plural, =1 {participant} other {participants}}",
|
||||||
|
"playbooks.status_update_post.update": "@{authorUsername} posted an update for [{runName}]({link})",
|
||||||
"plus_menu.browse_channels.title": "Browse Channels",
|
"plus_menu.browse_channels.title": "Browse Channels",
|
||||||
"plus_menu.create_new_channel.title": "Create New Channel",
|
"plus_menu.create_new_channel.title": "Create New Channel",
|
||||||
"plus_menu.invite_people_to_team.title": "Invite people to the team",
|
"plus_menu.invite_people_to_team.title": "Invite people to the team",
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,8 @@ BETA_BUILD=true
|
||||||
BUILD_FOR_RELEASE=true
|
BUILD_FOR_RELEASE=true
|
||||||
COLLECT_NETWORK_METRICS=true
|
COLLECT_NETWORK_METRICS=true
|
||||||
EXTENSION_APP_IDENTIFIER=com.mattermost.rnbeta.MattermostShare
|
EXTENSION_APP_IDENTIFIER=com.mattermost.rnbeta.MattermostShare
|
||||||
|
FASTLANE_XCODEBUILD_SETTINGS_TIMEOUT=120
|
||||||
|
FASTLANE_XCODEBUILD_SETTINGS_RETRIES=6
|
||||||
IOS_APP_GROUP=group.com.mattermost.rnbeta
|
IOS_APP_GROUP=group.com.mattermost.rnbeta
|
||||||
IOS_BUILD_EXPORT_METHOD=app-store
|
IOS_BUILD_EXPORT_METHOD=app-store
|
||||||
IOS_ICLOUD_CONTAINER=iCloud.com.mattermost.rnbeta
|
IOS_ICLOUD_CONTAINER=iCloud.com.mattermost.rnbeta
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,8 @@ BUILD_FOR_RELEASE=true
|
||||||
BUILD_PR=true
|
BUILD_PR=true
|
||||||
COLLECT_NETWORK_METRICS=true
|
COLLECT_NETWORK_METRICS=true
|
||||||
EXTENSION_APP_IDENTIFIER=com.mattermost.rnbeta.MattermostShare
|
EXTENSION_APP_IDENTIFIER=com.mattermost.rnbeta.MattermostShare
|
||||||
|
FASTLANE_XCODEBUILD_SETTINGS_TIMEOUT=120
|
||||||
|
FASTLANE_XCODEBUILD_SETTINGS_RETRIES=6
|
||||||
IOS_APP_GROUP=group.com.mattermost.rnbeta
|
IOS_APP_GROUP=group.com.mattermost.rnbeta
|
||||||
IOS_BUILD_EXPORT_METHOD=ad-hoc
|
IOS_BUILD_EXPORT_METHOD=ad-hoc
|
||||||
IOS_ICLOUD_CONTAINER=iCloud.com.mattermost.rnbeta
|
IOS_ICLOUD_CONTAINER=iCloud.com.mattermost.rnbeta
|
||||||
|
|
|
||||||
|
|
@ -2,3 +2,5 @@
|
||||||
AWS_BUCKET_NAME=releases.mattermost.com
|
AWS_BUCKET_NAME=releases.mattermost.com
|
||||||
AWS_FOLDER_NAME=mattermost-mobile-beta
|
AWS_FOLDER_NAME=mattermost-mobile-beta
|
||||||
AWS_REGION=us-east-1
|
AWS_REGION=us-east-1
|
||||||
|
FASTLANE_XCODEBUILD_SETTINGS_TIMEOUT=120
|
||||||
|
FASTLANE_XCODEBUILD_SETTINGS_RETRIES=6
|
||||||
|
|
|
||||||
3
types/api/posts.d.ts
vendored
3
types/api/posts.d.ts
vendored
|
|
@ -27,7 +27,8 @@ type PostType =
|
||||||
| 'add_bot_teams_channels'
|
| 'add_bot_teams_channels'
|
||||||
| 'system_auto_responder'
|
| 'system_auto_responder'
|
||||||
| 'custom_calls'
|
| 'custom_calls'
|
||||||
| 'custom_calls_recording';
|
| 'custom_calls_recording'
|
||||||
|
| 'custom_run_update';
|
||||||
|
|
||||||
type PostEmbedType = 'image' | 'message_attachment' | 'opengraph';
|
type PostEmbedType = 'image' | 'message_attachment' | 'opengraph';
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -33,12 +33,20 @@ export interface DeepLinkPlugin extends DeepLink {
|
||||||
route?: string;
|
route?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface DeepLinkPlaybooks extends DeepLink {
|
||||||
|
playbookId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DeepLinkPlaybookRuns extends DeepLink {
|
||||||
|
playbookRunId: string;
|
||||||
|
}
|
||||||
|
|
||||||
export type DeepLinkType = typeof DeepLinkConstant[keyof typeof DeepLinkConstant];
|
export type DeepLinkType = typeof DeepLinkConstant[keyof typeof DeepLinkConstant];
|
||||||
|
|
||||||
export interface DeepLinkWithData {
|
export interface DeepLinkWithData {
|
||||||
type: DeepLinkType;
|
type: DeepLinkType;
|
||||||
url: string;
|
url: string;
|
||||||
data?: DeepLinkChannel | DeepLinkDM | DeepLinkGM | DeepLinkPermalink | DeepLinkPlugin | DeepLinkServer;
|
data?: DeepLinkChannel | DeepLinkDM | DeepLinkGM | DeepLinkPermalink | DeepLinkPlugin | DeepLinkServer | DeepLinkPlaybooks | DeepLinkPlaybookRuns;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type LaunchType = typeof Launch[keyof typeof Launch];
|
export type LaunchType = typeof Launch[keyof typeof Launch];
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue