parent
0b2c2a5b6f
commit
835322768e
27 changed files with 548 additions and 59 deletions
|
|
@ -382,7 +382,7 @@ describe('handleGotoLocation', () => {
|
|||
|
||||
expect(getConfig).toHaveBeenCalledWith(mockOperator.database);
|
||||
expect(matchDeepLink).toHaveBeenCalledWith(location, serverUrl, serverUrl);
|
||||
expect(handleDeepLink).toHaveBeenCalledWith(location, intl, location);
|
||||
expect(handleDeepLink).toHaveBeenCalledWith({url: location}, intl, location);
|
||||
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);
|
||||
|
||||
if (match) {
|
||||
handleDeepLink(match.url, intl, location);
|
||||
handleDeepLink(match, intl, location);
|
||||
} else {
|
||||
const {formatMessage} = intl;
|
||||
const onError = () => Alert.alert(
|
||||
|
|
|
|||
|
|
@ -9,6 +9,8 @@ import FormattedText from '@components/formatted_text';
|
|||
import JumboEmoji from '@components/jumbo_emoji';
|
||||
import {Screens} from '@constants';
|
||||
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 {makeStyleSheetFromTheme} from '@utils/theme';
|
||||
|
||||
|
|
@ -139,6 +141,14 @@ const Body = ({
|
|||
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) {
|
||||
message = (
|
||||
<AddMembers
|
||||
|
|
|
|||
|
|
@ -7,6 +7,8 @@ const DeepLinkType = {
|
|||
GroupMessage: 'gm',
|
||||
Invalid: 'invalid',
|
||||
Permalink: 'permalink',
|
||||
Playbooks: 'playbooks',
|
||||
PlaybookRuns: 'playbook_runs',
|
||||
Redirect: '_redirect',
|
||||
Server: 'server',
|
||||
} as const;
|
||||
|
|
|
|||
|
|
@ -53,6 +53,8 @@ export default schemaMigrations({migrations: [
|
|||
{name: 'retrospective', type: 'string'},
|
||||
{name: 'retrospective_published_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({
|
||||
|
|
@ -62,6 +64,8 @@ export default schemaMigrations({migrations: [
|
|||
{name: 'items_order', type: 'string'},
|
||||
{name: 'title', type: 'string'},
|
||||
{name: 'update_at', type: 'number'},
|
||||
{name: 'sync', type: 'string', isIndexed: true, isOptional: true},
|
||||
{name: 'last_sync_at', type: 'number', isOptional: true},
|
||||
],
|
||||
}),
|
||||
createTable({
|
||||
|
|
@ -80,6 +84,8 @@ export default schemaMigrations({migrations: [
|
|||
{name: 'completed_at', type: 'number'},
|
||||
{name: 'task_actions', type: 'string', isOptional: true}, // JSON string
|
||||
{name: 'update_at', type: 'number'},
|
||||
{name: 'sync', type: 'string', isIndexed: true, isOptional: true},
|
||||
{name: 'last_sync_at', type: 'number', isOptional: true},
|
||||
],
|
||||
}),
|
||||
addColumns({
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ import {queryTeamDefaultChannel} from '@queries/servers/channel';
|
|||
import {getCommonSystemValues} from '@queries/servers/system';
|
||||
import {getTeamChannelHistory} from '@queries/servers/team';
|
||||
import {setScreensOrientation} from '@screens/navigation';
|
||||
import {alertInvalidDeepLink, handleDeepLink} from '@utils/deep_link';
|
||||
import {alertInvalidDeepLink, parseAndHandleDeepLink} from '@utils/deep_link';
|
||||
import {getIntlShape} from '@utils/general';
|
||||
|
||||
type LinkingCallbackArg = {url: string};
|
||||
|
|
@ -43,7 +43,7 @@ class GlobalEventHandlerSingleton {
|
|||
}
|
||||
|
||||
if (event.url) {
|
||||
const {error} = await handleDeepLink(event.url, undefined, undefined, true);
|
||||
const {error} = await parseAndHandleDeepLink(event.url, undefined, undefined, true);
|
||||
if (error) {
|
||||
alertInvalidDeepLink(getIntlShape(DEFAULT_LOCALE));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -48,13 +48,17 @@ export const fetchPlaybookRunsForChannel = async (serverUrl: string, channelId:
|
|||
return {runs: []};
|
||||
}
|
||||
|
||||
updateLastPlaybookRunsFetchAt(serverUrl, channelId, getMaxRunUpdateAt(allRuns));
|
||||
|
||||
if (!fetchOnly) {
|
||||
const result = await handlePlaybookRuns(serverUrl, allRuns, false, true);
|
||||
if (result && result.error) {
|
||||
throw result.error;
|
||||
}
|
||||
|
||||
EphemeralStore.setChannelPlaybooksSynced(serverUrl, channelId);
|
||||
handlePlaybookRuns(serverUrl, allRuns, false, true);
|
||||
}
|
||||
|
||||
updateLastPlaybookRunsFetchAt(serverUrl, channelId, getMaxRunUpdateAt(allRuns));
|
||||
|
||||
return {runs: allRuns};
|
||||
} catch (error) {
|
||||
logDebug('error on fetchPlaybookRunsForChannel', getFullErrorMessage(error));
|
||||
|
|
@ -83,3 +87,24 @@ export const fetchFinishedRunsForChannel = async (serverUrl: string, channelId:
|
|||
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
|
||||
fetchPlaybookRuns: (params: FetchPlaybookRunsParams, groupLabel?: RequestGroupLabel) => Promise<FetchPlaybookRunsReturn>;
|
||||
|
||||
// fetchPlaybookRun: (id: string, groupLabel?: RequestGroupLabel) => Promise<PlaybookRun>;
|
||||
fetchPlaybookRun: (id: string, groupLabel?: RequestGroupLabel) => Promise<PlaybookRun>;
|
||||
|
||||
// Run Management
|
||||
// finishRun: (playbookRunId: string) => Promise<any>;
|
||||
|
|
@ -54,12 +54,12 @@ const ClientPlaybooks = <TBase extends Constructor<ClientBase>>(superclass: TBas
|
|||
}
|
||||
};
|
||||
|
||||
// fetchPlaybookRun = async (id: string, groupLabel?: RequestGroupLabel) => {
|
||||
// return this.doFetch(
|
||||
// `${this.getPlaybookRunRoute(id)}`,
|
||||
// {method: 'get', groupLabel},
|
||||
// );
|
||||
// };
|
||||
fetchPlaybookRun = async (id: string, groupLabel?: RequestGroupLabel) => {
|
||||
return this.doFetch(
|
||||
`${this.getPlaybookRunRoute(id)}`,
|
||||
{method: 'get', groupLabel},
|
||||
);
|
||||
};
|
||||
|
||||
// Run Management
|
||||
// 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.
|
||||
|
||||
export const PLAYBOOKS_PLUGIN_ID = 'playbooks';
|
||||
export const PLAYBOOKS_UPDATE_STATUS_POST_TYPE = 'custom_run_update';
|
||||
|
|
|
|||
|
|
@ -35,6 +35,7 @@ type Props = {
|
|||
componentId: AvailableScreens;
|
||||
isBookmarksEnabled: boolean;
|
||||
isCallsEnabledInChannel: boolean;
|
||||
isPlaybooksEnabled: boolean;
|
||||
groupCallsAllowed: boolean;
|
||||
canManageMembers: boolean;
|
||||
isConvertGMFeatureAvailable: boolean;
|
||||
|
|
@ -70,6 +71,7 @@ const ChannelInfo = ({
|
|||
componentId,
|
||||
isBookmarksEnabled,
|
||||
isCallsEnabledInChannel,
|
||||
isPlaybooksEnabled,
|
||||
groupCallsAllowed,
|
||||
isConvertGMFeatureAvailable,
|
||||
isCRTEnabled,
|
||||
|
|
@ -139,6 +141,7 @@ const ChannelInfo = ({
|
|||
canManageMembers={canManageMembers}
|
||||
isCRTEnabled={isCRTEnabled}
|
||||
canManageSettings={canManageSettings}
|
||||
isPlaybooksEnabled={isPlaybooksEnabled}
|
||||
/>
|
||||
<View style={styles.separator}/>
|
||||
{convertGMOptionAvailable &&
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import {distinctUntilChanged, switchMap, combineLatestWith} from 'rxjs/operators
|
|||
import {observeIsCallsEnabledInChannel} from '@calls/observers';
|
||||
import {observeCallsConfig} from '@calls/state';
|
||||
import {withServerUrl} from '@context/server';
|
||||
import {observeIsPlaybooksEnabled} from '@playbooks/database/queries/version';
|
||||
import {observeCurrentChannel} from '@queries/servers/channel';
|
||||
import {observeCanAddBookmarks} from '@queries/servers/channel_bookmark';
|
||||
import {observeCanManageChannelMembers, observeCanManageChannelSettings} from '@queries/servers/role';
|
||||
|
|
@ -146,6 +147,8 @@ const enhanced = withObservables([], ({serverUrl, database}: Props) => {
|
|||
}),
|
||||
);
|
||||
|
||||
const isPlaybooksEnabled = observeIsPlaybooksEnabled(database);
|
||||
|
||||
return {
|
||||
type,
|
||||
canEnableDisableCalls,
|
||||
|
|
@ -158,6 +161,7 @@ const enhanced = withObservables([], ({serverUrl, database}: Props) => {
|
|||
isCRTEnabled: observeIsCRTEnabled(database),
|
||||
isGuestUser,
|
||||
isConvertGMFeatureAvailable,
|
||||
isPlaybooksEnabled,
|
||||
};
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -28,6 +28,7 @@ describe('ChannelInfoOptions', () => {
|
|||
canManageMembers: false,
|
||||
isCRTEnabled: false,
|
||||
canManageSettings: false,
|
||||
isPlaybooksEnabled: true,
|
||||
};
|
||||
}
|
||||
beforeEach(async () => {
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ type Props = {
|
|||
callsEnabled: boolean;
|
||||
canManageMembers: boolean;
|
||||
isCRTEnabled: boolean;
|
||||
isPlaybooksEnabled: boolean;
|
||||
canManageSettings: boolean;
|
||||
}
|
||||
|
||||
|
|
@ -32,6 +33,7 @@ const Options = ({
|
|||
callsEnabled,
|
||||
canManageMembers,
|
||||
isCRTEnabled,
|
||||
isPlaybooksEnabled,
|
||||
canManageSettings,
|
||||
}: Props) => {
|
||||
const isDMorGM = isTypeDMorGM(type);
|
||||
|
|
@ -49,10 +51,12 @@ const Options = ({
|
|||
<NotificationPreference channelId={channelId}/>
|
||||
<PinnedMessages channelId={channelId}/>
|
||||
<ChannelFiles channelId={channelId}/>
|
||||
{isPlaybooksEnabled &&
|
||||
<PlaybookRunsOption
|
||||
channelId={channelId}
|
||||
location='channel_actions'
|
||||
/>
|
||||
}
|
||||
{type !== General.DM_CHANNEL &&
|
||||
<Members channelId={channelId}/>
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ import SecurityManager from '@managers/security_manager';
|
|||
import {getAllServers} from '@queries/app/servers';
|
||||
import {findChannels, popToRoot} from '@screens/navigation';
|
||||
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 {alertChannelArchived, alertChannelRemove, alertTeamRemove} from '@utils/navigation';
|
||||
import {notificationError} from '@utils/notification';
|
||||
|
|
@ -136,7 +136,7 @@ export function HomeScreen(props: HomeProps) {
|
|||
|
||||
const deepLink = props.extra as DeepLinkWithData;
|
||||
if (deepLink?.url) {
|
||||
handleDeepLink(deepLink.url, intl, props.componentId, true).then((result) => {
|
||||
parseAndHandleDeepLink(deepLink.url, intl, props.componentId, true).then((result) => {
|
||||
if (result.error) {
|
||||
alertInvalidDeepLink(intl);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ import {addNewServer} from '@utils/server';
|
|||
|
||||
import {alertErrorWithFallback, errorBadChannel, errorUnkownUser} from '../draft';
|
||||
|
||||
import {alertInvalidDeepLink, extractServerUrl, getLaunchPropsFromDeepLink, handleDeepLink} from '.';
|
||||
import {alertInvalidDeepLink, extractServerUrl, getLaunchPropsFromDeepLink, parseAndHandleDeepLink} from '.';
|
||||
|
||||
jest.mock('@actions/remote/user', () => ({
|
||||
fetchUsersByUsernames: jest.fn(),
|
||||
|
|
@ -94,7 +94,7 @@ describe('extractServerUrl', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('handleDeepLink', () => {
|
||||
describe('parseAndHandleDeepLink', () => {
|
||||
const intl = createIntl({locale: 'en', messages: {}});
|
||||
|
||||
beforeEach(() => {
|
||||
|
|
@ -102,14 +102,14 @@ describe('handleDeepLink', () => {
|
|||
});
|
||||
|
||||
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});
|
||||
});
|
||||
|
||||
it('should add new server if not existing', async () => {
|
||||
(getActiveServerUrl as jest.Mock).mockResolvedValueOnce('https://currentserver.com');
|
||||
(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,
|
||||
data: {
|
||||
serverUrl: 'newserver.com',
|
||||
|
|
@ -124,7 +124,7 @@ describe('handleDeepLink', () => {
|
|||
it('should handle existing server and switch to home screen', async () => {
|
||||
(getActiveServerUrl as jest.Mock).mockResolvedValueOnce('https://currentserver.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(DatabaseManager.setActiveServerDatabase).toHaveBeenCalledWith('https://existingserver.com');
|
||||
expect(WebsocketManager.initializeClient).toHaveBeenCalledWith('https://existingserver.com', 'DeepLink');
|
||||
|
|
@ -136,7 +136,8 @@ describe('handleDeepLink', () => {
|
|||
(DatabaseManager.searchUrl as jest.Mock).mockReturnValueOnce(null);
|
||||
|
||||
(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');
|
||||
expect(spyOnUpdateProps).toHaveBeenCalledWith(Screens.SERVER, {serverUrl: 'currentserver.com'});
|
||||
expect(result).toEqual({error: false});
|
||||
|
|
@ -148,7 +149,7 @@ describe('handleDeepLink', () => {
|
|||
|
||||
(NavigationStore.getVisibleScreen as jest.Mock).mockReturnValueOnce(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(result).toEqual({error: false});
|
||||
});
|
||||
|
|
@ -156,7 +157,7 @@ describe('handleDeepLink', () => {
|
|||
it('should switch to channel by name for Channel deep link', async () => {
|
||||
(DatabaseManager.searchUrl as jest.Mock).mockReturnValueOnce('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(result).toEqual({error: false});
|
||||
});
|
||||
|
|
@ -165,7 +166,7 @@ describe('handleDeepLink', () => {
|
|||
(DatabaseManager.searchUrl as jest.Mock).mockReturnValueOnce('https://existingserver.com');
|
||||
(getActiveServerUrl as jest.Mock).mockResolvedValueOnce('https://existingserver.com');
|
||||
(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(result).toEqual({error: false});
|
||||
});
|
||||
|
|
@ -175,7 +176,7 @@ describe('handleDeepLink', () => {
|
|||
(getActiveServerUrl as jest.Mock).mockResolvedValueOnce('https://existingserver.com');
|
||||
(queryUsersByUsername as jest.Mock).mockReturnValueOnce({fetchIds: jest.fn(() => [])});
|
||||
(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(result).toEqual({error: false});
|
||||
});
|
||||
|
|
@ -185,7 +186,7 @@ describe('handleDeepLink', () => {
|
|||
(getActiveServerUrl as jest.Mock).mockResolvedValueOnce('https://existingserver.com');
|
||||
(queryUsersByUsername as jest.Mock).mockReturnValueOnce({fetchIds: jest.fn(() => [])});
|
||||
(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(result).toEqual({error: false});
|
||||
});
|
||||
|
|
@ -193,7 +194,7 @@ describe('handleDeepLink', () => {
|
|||
it('should switch to group message channel for GroupMessage deep link', async () => {
|
||||
(DatabaseManager.searchUrl as jest.Mock).mockReturnValueOnce('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(result).toEqual({error: false});
|
||||
});
|
||||
|
|
@ -202,7 +203,7 @@ describe('handleDeepLink', () => {
|
|||
(DatabaseManager.searchUrl as jest.Mock).mockReturnValueOnce('https://existingserver.com');
|
||||
(getActiveServerUrl as jest.Mock).mockResolvedValueOnce('https://existingserver.com');
|
||||
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(result).toEqual({error: false});
|
||||
});
|
||||
|
|
@ -211,7 +212,7 @@ describe('handleDeepLink', () => {
|
|||
(getActiveServerUrl as jest.Mock).mockImplementationOnce(() => {
|
||||
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(result).toEqual({error: true});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@
|
|||
|
||||
import {match} from 'path-to-regexp';
|
||||
import {type IntlShape} from 'react-intl';
|
||||
import {Alert} from 'react-native';
|
||||
import {Navigation} from 'react-native-navigation';
|
||||
import urlParse from 'url-parse';
|
||||
|
||||
|
|
@ -15,6 +16,10 @@ import {getDefaultThemeByAppearance} from '@context/theme';
|
|||
import DatabaseManager from '@database/manager';
|
||||
import {DEFAULT_LOCALE, t} from '@i18n';
|
||||
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 {getCurrentUser, queryUsersByUsername} from '@queries/servers/user';
|
||||
import {dismissAllModalsAndPopToRoot} from '@screens/navigation';
|
||||
|
|
@ -32,28 +37,27 @@ import {
|
|||
ID_PATH_PATTERN,
|
||||
} 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';
|
||||
|
||||
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 {
|
||||
const parsed = parseDeepLink(deepLinkUrl, asServer);
|
||||
if (parsed.type === DeepLink.Invalid || !parsed.data || !parsed.data.serverUrl) {
|
||||
if (deepLink.type === DeepLink.Invalid || !deepLink.data || !deepLink.data.serverUrl) {
|
||||
return {error: true};
|
||||
}
|
||||
|
||||
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
|
||||
if (!existingServerUrl) {
|
||||
const theme = EphemeralStore.theme || getDefaultThemeByAppearance();
|
||||
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)) {
|
||||
addNewServer(theme, parsed.data.serverUrl, undefined, parsed);
|
||||
addNewServer(theme, deepLink.data.serverUrl, undefined, deepLink);
|
||||
}
|
||||
return {error: false};
|
||||
}
|
||||
|
|
@ -70,14 +74,14 @@ export async function handleDeepLink(deepLinkUrl: string, intlShape?: IntlShape,
|
|||
const locale = currentUser?.locale || DEFAULT_LOCALE;
|
||||
const intl = intlShape || getIntlShape(locale);
|
||||
|
||||
switch (parsed.type) {
|
||||
switch (deepLink.type) {
|
||||
case DeepLink.Channel: {
|
||||
const deepLinkData = parsed.data as DeepLinkChannel;
|
||||
const deepLinkData = deepLink.data as DeepLinkChannel;
|
||||
switchToChannelByName(existingServerUrl, deepLinkData.channelName, deepLinkData.teamName, errorBadChannel, intl);
|
||||
break;
|
||||
}
|
||||
case DeepLink.DirectMessage: {
|
||||
const deepLinkData = parsed.data as DeepLinkDM;
|
||||
const deepLinkData = deepLink.data as DeepLinkDM;
|
||||
const userIds = await queryUsersByUsername(database, [deepLinkData.userName]).fetchIds();
|
||||
let userId = userIds.length ? userIds[0] : undefined;
|
||||
if (!userId) {
|
||||
|
|
@ -95,12 +99,12 @@ export async function handleDeepLink(deepLinkUrl: string, intlShape?: IntlShape,
|
|||
break;
|
||||
}
|
||||
case DeepLink.GroupMessage: {
|
||||
const deepLinkData = parsed.data as DeepLinkGM;
|
||||
const deepLinkData = deepLink.data as DeepLinkGM;
|
||||
switchToChannelByName(existingServerUrl, deepLinkData.channelName, deepLinkData.teamName, errorBadChannel, intl);
|
||||
break;
|
||||
}
|
||||
case DeepLink.Permalink: {
|
||||
const deepLinkData = parsed.data as DeepLinkPermalink;
|
||||
const deepLinkData = deepLink.data as DeepLinkPermalink;
|
||||
if (
|
||||
NavigationStore.hasModalsOpened() ||
|
||||
!deepLinkScreens.includes(NavigationStore.getVisibleScreen())
|
||||
|
|
@ -110,6 +114,49 @@ export async function handleDeepLink(deepLinkUrl: string, intlShape?: IntlShape,
|
|||
showPermalink(existingServerUrl, deepLinkData.teamName, deepLinkData.postId);
|
||||
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};
|
||||
} 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 = {
|
||||
hostname: string;
|
||||
serverUrl: string[];
|
||||
|
|
@ -129,6 +181,22 @@ type ChannelPathParams = {
|
|||
const CHANNEL_PATH = '*serverUrl/:teamName/:path/:identifier';
|
||||
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 = {
|
||||
serverUrl: string[];
|
||||
teamName: string;
|
||||
|
|
@ -204,14 +272,16 @@ function isValidIdentifierPathPattern(id: string): boolean {
|
|||
return regex.test(id);
|
||||
}
|
||||
|
||||
function isValidPostId(id: string): boolean {
|
||||
function isValidId(id: string): boolean {
|
||||
const regex = new RegExp(`^${ID_PATH_PATTERN}$`);
|
||||
return regex.test(id);
|
||||
}
|
||||
|
||||
export function parseDeepLink(deepLinkUrl: string, asServer = false): DeepLinkWithData {
|
||||
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);
|
||||
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);
|
||||
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;
|
||||
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) {
|
||||
const serverMatch = extractServerUrl(url);
|
||||
if (serverMatch) {
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@
|
|||
import {createIntl} from 'react-intl';
|
||||
import {Alert} from 'react-native';
|
||||
|
||||
import {onOpenLinkError, openLink} from './links';
|
||||
import * as Links from './links';
|
||||
|
||||
describe('onOpenLinkError', () => {
|
||||
const intl = createIntl({
|
||||
|
|
@ -26,7 +26,7 @@ describe('onOpenLinkError', () => {
|
|||
mockReturnValueOnce('Error').
|
||||
mockReturnValueOnce('Unable to open the link.'); // Mock message
|
||||
|
||||
onOpenLinkError(intl);
|
||||
Links.onOpenLinkError(intl);
|
||||
|
||||
expect(intl.formatMessage).toHaveBeenCalledTimes(2);
|
||||
expect(intl.formatMessage).toHaveBeenCalledWith({
|
||||
|
|
@ -55,7 +55,7 @@ describe('openLink', () => {
|
|||
it('should call normalizeProtocol and return early if the URL is invalid', async () => {
|
||||
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('');
|
||||
normalizeProtocolMock.mockRestore();
|
||||
|
|
@ -66,11 +66,11 @@ describe('openLink', () => {
|
|||
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});
|
||||
|
||||
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(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();
|
||||
matchDeepLinkMock.mockRestore();
|
||||
|
|
@ -83,9 +83,9 @@ describe('openLink', () => {
|
|||
const handleDeepLinkMock = jest.spyOn(require('@utils/deep_link'), 'handleDeepLink').mockResolvedValue({error: true});
|
||||
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();
|
||||
matchDeepLinkMock.mockRestore();
|
||||
|
|
@ -98,9 +98,9 @@ describe('openLink', () => {
|
|||
const matchDeepLinkMock = jest.spyOn(require('@utils/deep_link'), 'matchDeepLink').mockReturnValue(null);
|
||||
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();
|
||||
matchDeepLinkMock.mockRestore();
|
||||
|
|
|
|||
|
|
@ -31,11 +31,15 @@ export const openLink = async (link: string, serverUrl: string, siteURL: string,
|
|||
const match = matchDeepLink(url, serverUrl, siteURL);
|
||||
|
||||
if (match) {
|
||||
const {error} = await handleDeepLink(match.url, intl);
|
||||
const {error} = await handleDeepLink(match, intl);
|
||||
if (error) {
|
||||
tryOpenURL(match.url, onOpenLinkError);
|
||||
tryOpenURL(match.url, () => {
|
||||
onOpenLinkError(intl);
|
||||
});
|
||||
}
|
||||
} else {
|
||||
tryOpenURL(url, onOpenLinkError);
|
||||
tryOpenURL(url, () => {
|
||||
onOpenLinkError(intl);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -996,6 +996,15 @@
|
|||
"playbook.participants": "Run Participants",
|
||||
"playbook.runs.finished": "Finished",
|
||||
"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.title": "Unable to fetch run details",
|
||||
"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.title": "No in progress runs",
|
||||
"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.create_new_channel.title": "Create New Channel",
|
||||
"plus_menu.invite_people_to_team.title": "Invite people to the team",
|
||||
|
|
|
|||
|
|
@ -8,6 +8,8 @@ BETA_BUILD=true
|
|||
BUILD_FOR_RELEASE=true
|
||||
COLLECT_NETWORK_METRICS=true
|
||||
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_BUILD_EXPORT_METHOD=app-store
|
||||
IOS_ICLOUD_CONTAINER=iCloud.com.mattermost.rnbeta
|
||||
|
|
|
|||
|
|
@ -9,6 +9,8 @@ BUILD_FOR_RELEASE=true
|
|||
BUILD_PR=true
|
||||
COLLECT_NETWORK_METRICS=true
|
||||
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_BUILD_EXPORT_METHOD=ad-hoc
|
||||
IOS_ICLOUD_CONTAINER=iCloud.com.mattermost.rnbeta
|
||||
|
|
|
|||
|
|
@ -2,3 +2,5 @@
|
|||
AWS_BUCKET_NAME=releases.mattermost.com
|
||||
AWS_FOLDER_NAME=mattermost-mobile-beta
|
||||
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'
|
||||
| 'system_auto_responder'
|
||||
| 'custom_calls'
|
||||
| 'custom_calls_recording';
|
||||
| 'custom_calls_recording'
|
||||
| 'custom_run_update';
|
||||
|
||||
type PostEmbedType = 'image' | 'message_attachment' | 'opengraph';
|
||||
|
||||
|
|
|
|||
|
|
@ -33,12 +33,20 @@ export interface DeepLinkPlugin extends DeepLink {
|
|||
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 interface DeepLinkWithData {
|
||||
type: DeepLinkType;
|
||||
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];
|
||||
|
|
|
|||
Loading…
Reference in a new issue