Compare commits

...

9 commits

Author SHA1 Message Date
Mario Vitale
94d4a23147
Bump to version 2.10.1 build 496 (#7691)
Some checks failed
github-release / test (push) Has been cancelled
github-release / build-ios-unsigned (push) Has been cancelled
github-release / build-android-unsigned (push) Has been cancelled
github-release / release (push) Has been cancelled
2023-11-29 13:09:10 +01:00
Caleb Roseland
10b2e0f21a
Merge pull request #7690 from mattermost/release-2.10-7613 2023-11-28 12:57:39 -06:00
Caleb Roseland
01e3d62d11 Merge pull request #7613 from mattermost/MM-53902-cont 2023-11-28 12:39:33 -06:00
Daniel Espino García
1f90540d95
Bump app build number to 492 (#7644)
Some checks failed
github-release / test (push) Has been cancelled
github-release / build-ios-unsigned (push) Has been cancelled
github-release / build-android-unsigned (push) Has been cancelled
github-release / release (push) Has been cancelled
2023-11-01 11:56:39 +01:00
Mattermost Build
9ee97a659d
fix: render latex in messages (#7635) (#7641)
Signed-off-by: Jannik Bertram <jannik.bertram94@gmail.com>
Co-authored-by: Harrison Healey <harrisonmhealey@gmail.com>
(cherry picked from commit 3497318083)

Co-authored-by: jannikb <jb@allgravy.com>
2023-10-31 18:11:30 +01:00
Mattermost Build
424e0e0cd8
Bump to version 2.10.0 build 490 (#7621) (#7622)
* Bump app version number to 2.10.0

* Bump app build number to 490

(cherry picked from commit f30384eb22)

Co-authored-by: Elias Nahum <nahumhbl@gmail.com>
2023-10-20 11:03:12 +04:00
Mattermost Build
10be71b536
Update NOTICE.txt (#7607) (#7619)
(cherry picked from commit f1e17d2ef1)

Co-authored-by: Amy Blais <29708087+amyblais@users.noreply.github.com>
2023-10-19 16:38:47 +04:00
Mattermost Build
ee33cb414e
Fix race condition when showing the turorial (#7599) (#7616)
(cherry picked from commit 12a29fe33e)

Co-authored-by: Elias Nahum <nahumhbl@gmail.com>
2023-10-19 11:46:12 +04:00
Mattermost Build
bbe2977fd7
Fix reply from permalink (#7598) (#7608)
(cherry picked from commit 55149a593c)

Co-authored-by: Elias Nahum <nahumhbl@gmail.com>
2023-10-19 11:44:26 +04:00
20 changed files with 391 additions and 186 deletions

View file

@ -667,6 +667,38 @@ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
---
## @voximplant/react-native-foreground-service
This product contains a modified version of '@voximplant/react-native-foreground-service' by Voximplant.
A foreground service performs some operation that is noticeable to the user.
* HOMEPAGE:
* https://github.com/voximplant/react-native-foreground-service
* LICENSE: MIT License
Copyright (c) 2019 Zingaya, Inc
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
---

View file

@ -110,8 +110,8 @@ android {
applicationId "com.mattermost.rnbeta"
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
versionCode 489
versionName "2.9.0"
versionCode 496
versionName "2.10.1"
testBuildType System.getProperty('testBuildType', 'debug')
testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner'
}

View file

@ -94,14 +94,7 @@ export const switchToThread = async (serverUrl: string, rootId: string, isFromNo
}
}
if (currentTeamId === teamId) {
const models = await prepareCommonSystemValues(operator, {
currentChannelId: channel.id,
});
if (models.length) {
await operator.batchRecords(models, 'switchToThread');
}
} else {
if (currentTeamId !== teamId) {
const modelPromises: Array<Promise<Model[]>> = [];
modelPromises.push(addTeamToTeamHistory(operator, teamId, true));
const commonValues: PrepareCommonSystemValuesArgs = {

View file

@ -143,7 +143,7 @@ export const handleGotoLocation = async (serverUrl: string, intl: IntlShape, loc
const match = matchDeepLink(location, serverUrl, config?.SiteURL);
if (match) {
handleDeepLink(match, intl, location);
handleDeepLink(match.url, intl, location);
} else {
const {formatMessage} = intl;
const onError = () => Alert.alert(

View file

@ -76,9 +76,9 @@ const MarkdownLink = ({children, experimentalNormalizeMarkdownLinks, href, siteU
const match = matchDeepLink(url, serverUrl, siteURL);
if (match) {
const {error} = await handleDeepLink(match, intl);
const {error} = await handleDeepLink(match.url, intl);
if (error) {
tryOpenURL(match, onError);
tryOpenURL(match.url, onError);
}
} else {
tryOpenURL(url, onError);

View file

@ -2,7 +2,7 @@
// See LICENSE.txt for license information.
import React, {useCallback} from 'react';
import {Modal, StyleSheet, useWindowDimensions, View} from 'react-native';
import {Modal, useWindowDimensions} from 'react-native';
import HighlightItem from './item';
@ -11,11 +11,10 @@ type Props = {
itemBounds: TutorialItemBounds;
itemBorderRadius?: number;
onDismiss: () => void;
onLayout: () => void;
onShow?: () => void;
}
const TutorialHighlight = ({children, itemBounds, itemBorderRadius, onDismiss, onLayout, onShow}: Props) => {
const TutorialHighlight = ({children, itemBounds, itemBorderRadius, onDismiss, onShow}: Props) => {
const {width, height} = useWindowDimensions();
const handleShowTutorial = useCallback(() => {
@ -26,19 +25,13 @@ const TutorialHighlight = ({children, itemBounds, itemBorderRadius, onDismiss, o
return (
<Modal
visible={true}
visible={itemBounds.endX > 0}
transparent={true}
animationType='fade'
onDismiss={onDismiss}
onRequestClose={onDismiss}
testID='tutorial_highlight'
>
<View
style={StyleSheet.absoluteFill}
pointerEvents='box-none'
onLayout={onLayout}
/>
{itemBounds.endX > 0 &&
<HighlightItem
borderRadius={itemBorderRadius}
itemBounds={itemBounds}
@ -47,7 +40,6 @@ const TutorialHighlight = ({children, itemBounds, itemBorderRadius, onDismiss, o
width={width}
onLayout={handleShowTutorial}
/>
}
{children}
</Modal>
);

View file

@ -32,6 +32,7 @@ type Props = {
rightDecorator?: React.ReactNode;
onUserPress?: (user: UserProfile | UserModel) => void;
onUserLongPress?: (user: UserProfile | UserModel) => void;
onLayout?: () => void;
disabled?: boolean;
viewRef?: React.LegacyRef<View>;
padding?: number;
@ -102,6 +103,7 @@ const UserItem = ({
locale,
teammateNameDisplay,
rightDecorator,
onLayout,
onUserPress,
onUserLongPress,
disabled = false,
@ -159,6 +161,7 @@ const UserItem = ({
onPress={onPress}
onLongPress={onLongPress}
disabled={!(onUserPress || onUserLongPress)}
onLayout={onLayout}
>
<View
ref={viewRef}

View file

@ -138,6 +138,7 @@ exports[`components/channel_list_row should show no results 1`] = `
collapsable={false}
focusable={true}
onClick={[Function]}
onLayout={[Function]}
onResponderGrant={[Function]}
onResponderMove={[Function]}
onResponderRelease={[Function]}
@ -424,6 +425,7 @@ exports[`components/channel_list_row should show results and tutorial 1`] = `
collapsable={false}
focusable={true}
onClick={[Function]}
onLayout={[Function]}
onResponderGrant={[Function]}
onResponderMove={[Function]}
onResponderRelease={[Function]}
@ -551,79 +553,8 @@ exports[`components/channel_list_row should show results and tutorial 1`] = `
onRequestClose={[Function]}
testID="tutorial_highlight"
transparent={true}
visible={true}
>
<View
onLayout={[Function]}
pointerEvents="box-none"
style={
{
"bottom": 0,
"left": 0,
"position": "absolute",
"right": 0,
"top": 0,
}
}
/>
<View
pointerEvents="none"
style={
[
{
"alignItems": "center",
"bottom": 0,
"justifyContent": "center",
"left": 0,
"position": "absolute",
"right": 0,
"top": 0,
},
undefined,
]
}
testID="tutorial_swipe_left"
>
<View
style={
[
{
"alignItems": "center",
"backgroundColor": "#ffffff",
"borderRadius": 8,
"height": 161,
"padding": 16,
"width": 247,
},
{
"top": -74,
},
]
}
>
<long_press_illustration.svg />
<Text
style={
[
{
"color": "#3f4350",
"fontFamily": "Metropolis-SemiBold",
"fontSize": 16,
"fontWeight": "600",
"lineHeight": 24,
"marginTop": 8,
"paddingHorizontal": 12,
"textAlign": "center",
},
undefined,
]
}
>
Long-press on an item to view a user's profile
</Text>
</View>
</View>
</Modal>
visible={false}
/>
</View>
<View
onFocusCapture={[Function]}
@ -790,6 +721,7 @@ exports[`components/channel_list_row should show results no tutorial 1`] = `
collapsable={false}
focusable={true}
onClick={[Function]}
onLayout={[Function]}
onResponderGrant={[Function]}
onResponderMove={[Function]}
onResponderRelease={[Function]}
@ -1108,6 +1040,7 @@ exports[`components/channel_list_row should show results no tutorial 2 users 1`]
collapsable={false}
focusable={true}
onClick={[Function]}
onLayout={[Function]}
onResponderGrant={[Function]}
onResponderMove={[Function]}
onResponderRelease={[Function]}
@ -1299,6 +1232,7 @@ exports[`components/channel_list_row should show results no tutorial 2 users 1`]
collapsable={false}
focusable={true}
onClick={[Function]}
onLayout={[Function]}
onResponderGrant={[Function]}
onResponderMove={[Function]}
onResponderRelease={[Function]}

View file

@ -155,8 +155,10 @@ function UserListRow({
}, [isChannelAdmin, showManageMode, theme]);
const onLayout = useCallback(() => {
startTutorial();
}, []);
if (showTutorial) {
startTutorial();
}
}, [showTutorial]);
const icon = useMemo(() => {
if (!selectable && !selected) {
@ -190,17 +192,19 @@ function UserListRow({
viewRef={viewRef}
padding={20}
includeMargin={includeMargin}
onLayout={onLayout}
/>
{showTutorial &&
<TutorialHighlight
itemBounds={itemBounds}
onDismiss={handleDismissTutorial}
onLayout={onLayout}
>
{Boolean(itemBounds.endX) &&
<TutorialLongPress
message={formatMessage({id: 'user.tutorial.long_press', defaultMessage: "Long-press on an item to view a user's profile"})}
style={isTablet ? style.tutorialTablet : style.tutorial}
/>
}
</TutorialHighlight>
}
</>

View file

@ -213,9 +213,11 @@ const ServerItem = ({
};
const onLayout = useCallback(() => {
swipeable.current?.close();
startTutorial();
}, []);
if (showTutorial) {
swipeable.current?.close();
startTutorial();
}
}, [showTutorial]);
const containerStyle = useMemo(() => {
const style: StyleProp<ViewStyle> = [styles.container];
@ -390,6 +392,7 @@ const ServerItem = ({
style={containerStyle}
ref={viewRef}
testID={serverItemTestId}
onLayout={onLayout}
>
<RectButton
onPress={onServerPressed}
@ -474,13 +477,14 @@ const ServerItem = ({
itemBounds={itemBounds}
onDismiss={handleDismissTutorial}
onShow={handleShowTutorial}
onLayout={onLayout}
itemBorderRadius={8}
>
{Boolean(itemBounds.endX) &&
<TutorialSwipeLeft
message={intl.formatMessage({id: 'server.tutorial.swipe', defaultMessage: 'Swipe left on a server to see more actions'})}
style={isTablet ? styles.tutorialTablet : styles.tutorial}
/>
}
</TutorialHighlight>
}
</>

View file

@ -1,12 +1,14 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {match} from 'path-to-regexp';
import {createIntl, type IntlShape} from 'react-intl';
import urlParse from 'url-parse';
import {makeDirectChannel, switchToChannelByName} from '@actions/remote/channel';
import {showPermalink} from '@actions/remote/permalink';
import {fetchUsersByUsernames} from '@actions/remote/user';
import DeepLinkType from '@app/constants/deep_linking';
import {DeepLink, Launch, Screens} from '@constants';
import {getDefaultThemeByAppearance} from '@context/theme';
import DatabaseManager from '@database/manager';
@ -21,7 +23,14 @@ import {alertErrorWithFallback, errorBadChannel, errorUnkownUser} from '@utils/d
import {logError} from '@utils/log';
import {escapeRegex} from '@utils/markdown';
import {addNewServer} from '@utils/server';
import {removeProtocol} from '@utils/url';
import {
TEAM_NAME_PATH_PATTERN,
IDENTIFIER_PATH_PATTERN,
ID_PATH_PATTERN,
PLUGIN_ID_PATH_PATTERN,
} from '@utils/url/path';
import {removeProtocol} from '../url';
import type {DeepLinkChannel, DeepLinkDM, DeepLinkGM, DeepLinkPermalink, DeepLinkWithData, LaunchProps} from '@typings/launch';
import type {AvailableScreens} from '@typings/screens/navigation';
@ -86,7 +95,7 @@ export async function handleDeepLink(deepLinkUrl: string, intlShape?: IntlShape,
}
case DeepLink.GroupMessage: {
const deepLinkData = parsed.data as DeepLinkGM;
switchToChannelByName(existingServerUrl, deepLinkData.channelId, deepLinkData.teamName, errorBadChannel, intl);
switchToChannelByName(existingServerUrl, deepLinkData.channelName, deepLinkData.teamName, errorBadChannel, intl);
break;
}
case DeepLink.Permalink: {
@ -114,80 +123,89 @@ export async function handleDeepLink(deepLinkUrl: string, intlShape?: IntlShape,
}
}
type ChannelPathParams = {
hostname: string;
serverUrl: string;
teamName: string;
path: 'channels' | 'messages';
identifier: string;
};
const CHANNEL_PATH = `:serverUrl(.*)/:teamName(${TEAM_NAME_PATH_PATTERN})/:path(channels|messages)/:identifier(${IDENTIFIER_PATH_PATTERN})`;
export const matchChannelDeeplink = match<ChannelPathParams>(CHANNEL_PATH);
type PermalinkPathParams = {
serverUrl: string;
teamName: string;
postId: string;
};
const PERMALINK_PATH = `:serverUrl(.*)/:teamName(${TEAM_NAME_PATH_PATTERN})/pl/:postId(${ID_PATH_PATTERN})`;
export const matchPermalinkDeeplink = match<PermalinkPathParams>(PERMALINK_PATH);
export function parseDeepLink(deepLinkUrl: string): DeepLinkWithData {
try {
const url = removeProtocol(decodeURIComponent(deepLinkUrl));
const url = removeProtocol(deepLinkUrl);
if (url.includes('../') || url.includes('/..')) {
return {type: DeepLink.Invalid, url: deepLinkUrl};
const channelMatch = matchChannelDeeplink(url);
if (channelMatch) {
const {params: {serverUrl, teamName, path, identifier}} = channelMatch;
if (path === 'channels') {
return {type: DeepLink.Channel, url: deepLinkUrl, data: {serverUrl, teamName, channelName: identifier}};
}
if (path === 'messages') {
if (identifier.startsWith('@')) {
return {type: DeepLink.DirectMessage, url: deepLinkUrl, data: {serverUrl, teamName, userName: identifier.substring(1)}};
}
return {type: DeepLink.GroupMessage, url: deepLinkUrl, data: {serverUrl, teamName, channelName: identifier}};
}
}
let match = new RegExp('(.*)\\/([^\\/]+)\\/channels\\/(\\S+)').exec(url);
if (match) {
return {type: DeepLink.Channel, url: deepLinkUrl, data: {serverUrl: match[1], teamName: match[2], channelName: match[3]}};
const permalinkMatch = matchPermalinkDeeplink(url);
if (permalinkMatch) {
const {params: {serverUrl, teamName, postId}} = permalinkMatch;
return {type: DeepLink.Permalink, url: deepLinkUrl, data: {serverUrl, teamName, postId}};
}
match = new RegExp('(.*)\\/([^\\/]+)\\/pl\\/(\\w+)').exec(url);
if (match) {
return {type: DeepLink.Permalink, url: deepLinkUrl, data: {serverUrl: match[1], teamName: match[2], postId: match[3]}};
const pluginMatch = match<{serverUrl: string; id: string; route?: string}>(`:serverUrl(.*)/plugins/:id(${PLUGIN_ID_PATH_PATTERN})/:route(.*)?`)(url);
if (pluginMatch) {
const {params: {serverUrl, id, route}} = pluginMatch;
return {type: DeepLink.Plugin, url: deepLinkUrl, data: {serverUrl, teamName: '', id, route}};
}
match = new RegExp('(.*)\\/([^\\/]+)\\/messages\\/@(\\S+)').exec(url);
if (match) {
return {type: DeepLink.DirectMessage, url: deepLinkUrl, data: {serverUrl: match[1], teamName: match[2], userName: match[3]}};
}
match = new RegExp('(.*)\\/([^\\/]+)\\/messages\\/(\\S+)').exec(url);
if (match) {
return {type: DeepLink.GroupMessage, url: deepLinkUrl, data: {serverUrl: match[1], teamName: match[2], channelId: match[3]}};
}
match = new RegExp('(.*)\\/plugins\\/([^\\/]+)\\/(\\S+)').exec(url);
if (match) {
return {type: DeepLink.Plugin, url: deepLinkUrl, data: {serverUrl: match[1], id: match[2], teamName: ''}};
}
} catch {
} catch (err) {
// do nothing just return invalid deeplink
}
return {type: DeepLink.Invalid, url: deepLinkUrl};
}
export function matchDeepLink(url?: string, serverURL?: string, siteURL?: string) {
export function matchDeepLink(url: string, serverURL?: string, siteURL?: string) {
if (!url || (!serverURL && !siteURL)) {
return '';
return null;
}
let urlToMatch = url;
const urlBase = serverURL || siteURL || '';
const parsedUrl = urlParse(url);
const parsedBase = urlParse(urlBase);
if (!parsedUrl.protocol) {
// If url doesn't contain site or server URL, tack it on.
// e.g. <jump to convo> URLs from autolink plugin.
const match = new RegExp(escapeRegex(urlBase)).exec(url);
if (!match) {
const deepLinkMatch = new RegExp(escapeRegex(urlBase)).exec(url);
if (!deepLinkMatch) {
urlToMatch = urlBase + url;
}
}
const finalUrl = urlParse(urlToMatch);
const baseSubpath = parsedBase.pathname.replace('/', '');
const baseHostname = parsedBase.hostname;
const urlSubpath = finalUrl.pathname.split('/')[1];
const urlHostname = finalUrl.hostname;
const parsed = parseDeepLink(urlToMatch);
if (baseSubpath) {
// if the server is in a subpath
if (urlHostname === baseHostname && urlSubpath === baseSubpath) {
return urlToMatch;
}
} else if (urlHostname === baseHostname) {
return urlToMatch;
if (parsed.type === DeepLinkType.Invalid) {
return null;
}
return '';
return parsed;
}
export const getLaunchPropsFromDeepLink = (deepLinkUrl: string, coldStart = false): LaunchProps => {

20
app/utils/url/path.ts Normal file
View file

@ -0,0 +1,20 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
export const ID_PATH_PATTERN = '[a-z0-9]{26}';
export const PLUGIN_ID_PATH_PATTERN = '[a-zA-Z0-9-_.]{3,190}';
// This should cover:
// - Team name (lowercase english characters, numbers or -)
// - Two ids separated by __ (userID__userID)
export const TEAM_NAME_PATH_PATTERN = '[a-z0-9\\-_]+';
// This should cover:
// - Channel name
// - Channel ID
// - Group Channel Name (40 length UID)
// - DM Name (userID__userID)
// - Username prefixed by a @
// - User ID
// - Email
export const IDENTIFIER_PATH_PATTERN = '[@a-zA-Z\\-_0-9][@a-zA-Z\\-_0-9.]*';

View file

@ -5,7 +5,7 @@ import {Linking} from 'react-native';
import DeepLinkType from '@constants/deep_linking';
import TestHelper from '@test/test_helper';
import {matchDeepLink, parseDeepLink} from '@utils/deep_link';
import {matchDeepLink} from '@utils/deep_link';
import * as UrlUtils from '@utils/url';
/* eslint-disable max-nested-callbacks */
@ -129,32 +129,35 @@ describe('UrlUtils', () => {
describe('matchDeepLink', () => {
const URL_NO_PROTOCOL = 'localhost:8065';
const URL_PATH_NO_PROTOCOL = 'localhost:8065/subpath';
const URL_PATH_NO_PROTOCOL = 'localhost:8065/subpath/deepsubpath';
const SITE_URL = `http://${URL_NO_PROTOCOL}`;
const SERVER_URL = `http://${URL_NO_PROTOCOL}`;
const SERVER_WITH_SUBPATH = `http://${URL_PATH_NO_PROTOCOL}`;
const DEEPLINK_URL_ROOT = `mattermost://${URL_NO_PROTOCOL}`;
const DM_USER = TestHelper.fakeUserWithId();
const GM_CHANNEL_NAME = '4862db64e76a321d167fe6677f16e96e9275dabe';
const tests = [
{
name: 'should return null if all inputs are empty',
input: {url: '', serverURL: '', siteURL: ''},
expected: {type: 'invalid'},
expected: null,
},
{
name: 'should return null if any of the input is null',
input: {url: '', serverURL: '', siteURL: null},
expected: {type: 'invalid'},
expected: null,
},
{
name: 'should return null if any of the input is null',
input: {url: '', serverURL: null, siteURL: ''},
expected: {type: 'invalid'},
expected: null,
},
{
name: 'should return null if any of the input is null',
input: {url: null, serverURL: '', siteURL: ''},
expected: {type: 'invalid'},
expected: null,
},
{
name: 'should return null for not supported link',
@ -163,12 +166,12 @@ describe('UrlUtils', () => {
serverURL: SERVER_URL,
siteURL: SITE_URL,
},
expected: {type: 'invalid'},
expected: null,
},
{
name: 'should return null despite url subset match',
input: {url: 'http://myserver.com', serverURL: 'http://myserver.co'},
expected: {type: 'invalid'},
expected: null,
},
{
name: 'should match despite no server URL in input link',
@ -186,6 +189,51 @@ describe('UrlUtils', () => {
type: DeepLinkType.Permalink,
},
},
{
name: 'should return null for invalid deeplink',
input: {
url: DEEPLINK_URL_ROOT + '/ad-1/channels/../town-square',
serverURL: SERVER_URL,
siteURL: SITE_URL,
},
expected: null,
},
{
name: 'should return null for backslash-invalid deeplink',
input: {
url: DEEPLINK_URL_ROOT + '/ad-1/channels/\\..town-square',
serverURL: SERVER_URL,
siteURL: SITE_URL,
},
expected: null,
},
{
name: 'should return null for backslash-invalid-alt deeplink',
input: {
url: DEEPLINK_URL_ROOT + '/ad-1/channels/t..\\town-square',
serverURL: SERVER_URL,
siteURL: SITE_URL,
},
expected: null,
},
{
name: 'should return null for double encoded invalid deeplink',
input: {
url: DEEPLINK_URL_ROOT + '/ad-1/channels/%252f%252e.town-square',
serverURL: SERVER_URL,
siteURL: SITE_URL,
},
expected: null,
},
{
name: 'should return null for double encoded backslash-invalid deeplink',
input: {
url: DEEPLINK_URL_ROOT + '/ad-1/channels/%255C%252e.town-square',
serverURL: SERVER_URL,
siteURL: SITE_URL,
},
expected: null,
},
{
name: 'should match channel link',
input: {
@ -234,10 +282,74 @@ describe('UrlUtils', () => {
type: DeepLinkType.Channel,
},
},
{
name: 'should match channel link (channel name: messages) with deeplink prefix',
input: {
url: DEEPLINK_URL_ROOT + '/ad-1/channels/messages',
serverURL: SERVER_URL,
siteURL: SITE_URL,
},
expected: {
data: {
channelName: 'messages',
serverUrl: URL_NO_PROTOCOL,
teamName: 'ad-1',
},
type: DeepLinkType.Channel,
},
},
{
name: 'should match DM channel link with deeplink prefix',
input: {
url: DEEPLINK_URL_ROOT + `/pl/messages/@${DM_USER.username}`,
serverURL: SERVER_URL,
siteURL: SITE_URL,
},
expected: {
data: {
userName: DM_USER.username,
serverUrl: URL_NO_PROTOCOL,
teamName: 'pl',
},
type: DeepLinkType.DirectMessage,
},
},
{
name: 'should match GM channel link with deeplink prefix',
input: {
url: DEEPLINK_URL_ROOT + `/pl/messages/${GM_CHANNEL_NAME}`,
serverURL: SERVER_URL,
siteURL: SITE_URL,
},
expected: {
data: {
channelName: GM_CHANNEL_NAME,
serverUrl: URL_NO_PROTOCOL,
teamName: 'pl',
},
type: DeepLinkType.GroupMessage,
},
},
{
name: 'should match channel link (team name: pl, channel name: messages) with deeplink prefix',
input: {
url: DEEPLINK_URL_ROOT + '/pl/channels/messages',
serverURL: SERVER_URL,
siteURL: SITE_URL,
},
expected: {
data: {
channelName: 'messages',
serverUrl: URL_NO_PROTOCOL,
teamName: 'pl',
},
type: DeepLinkType.Channel,
},
},
{
name: 'should match permalink with deeplink prefix on a Server hosted in a Subpath',
input: {
url: DEEPLINK_URL_ROOT + '/subpath/ad-1/pl/qe93kkfd7783iqwuwfcwcxbsrr',
url: DEEPLINK_URL_ROOT + '/subpath/deepsubpath/ad-1/pl/qe93kkfd7783iqwuwfcwcxbsrr',
serverURL: SERVER_WITH_SUBPATH,
siteURL: SERVER_WITH_SUBPATH,
},
@ -247,7 +359,23 @@ describe('UrlUtils', () => {
serverUrl: URL_PATH_NO_PROTOCOL,
teamName: 'ad-1',
},
type: 'permalink',
type: DeepLinkType.Permalink,
},
},
{
name: 'should match permalink (team name: pl) with deeplink prefix on a Server hosted in a Subpath',
input: {
url: DEEPLINK_URL_ROOT + '/subpath/deepsubpath/pl/pl/qe93kkfd7783iqwuwfcwcxbsrr',
serverURL: SERVER_WITH_SUBPATH,
siteURL: SERVER_WITH_SUBPATH,
},
expected: {
data: {
postId: 'qe93kkfd7783iqwuwfcwcxbsrr',
serverUrl: URL_PATH_NO_PROTOCOL,
teamName: 'pl',
},
type: DeepLinkType.Permalink,
},
},
{
@ -263,7 +391,7 @@ describe('UrlUtils', () => {
serverUrl: URL_PATH_NO_PROTOCOL,
teamName: 'ad-1',
},
type: 'permalink',
type: DeepLinkType.Permalink,
},
},
{
@ -273,8 +401,75 @@ describe('UrlUtils', () => {
serverURL: SERVER_WITH_SUBPATH,
siteURL: SERVER_WITH_SUBPATH,
},
expected: null,
},
{
name: 'should match plugin path on a Server hosted in a Subpath',
input: {
url: DEEPLINK_URL_ROOT + '/subpath/deepsubpath/plugins/com.acme.abc-test/api/testroute',
serverURL: SERVER_WITH_SUBPATH,
siteURL: SERVER_WITH_SUBPATH,
},
expected: {
type: 'invalid',
data: {
id: 'com.acme.abc-test',
route: 'api/testroute',
serverUrl: URL_PATH_NO_PROTOCOL,
teamName: '',
},
type: DeepLinkType.Plugin,
},
},
{
name: 'should match plugin path with single-level route',
input: {
url: DEEPLINK_URL_ROOT + '/subpath/deepsubpath/plugins/abc_abc_abc_abc_abc_abc_abc_abc_abc_abc_abc_abc_abc_abc_abc_abc_abc_abc_abc_abc_abc_abc_abc_abc_abc_abc_abc_abc_abc_abc_abc_abc_abc_abc_abc_abc_abc_abc_abc_abc_abc_abc_abc_abc_abc_abc_abc_ab/testroute',
serverURL: SERVER_WITH_SUBPATH,
siteURL: SERVER_WITH_SUBPATH,
},
expected: {
data: {
id: 'abc_abc_abc_abc_abc_abc_abc_abc_abc_abc_abc_abc_abc_abc_abc_abc_abc_abc_abc_abc_abc_abc_abc_abc_abc_abc_abc_abc_abc_abc_abc_abc_abc_abc_abc_abc_abc_abc_abc_abc_abc_abc_abc_abc_abc_abc_abc_ab',
route: 'testroute',
serverUrl: URL_PATH_NO_PROTOCOL,
teamName: '',
},
type: DeepLinkType.Plugin,
},
},
{
name: 'should not match plugin path with invalid plugin id len=2',
input: {
url: DEEPLINK_URL_ROOT + '/subpath/deepsubpath/plugins/ab/api/testroute',
serverURL: SERVER_WITH_SUBPATH,
siteURL: SERVER_WITH_SUBPATH,
},
expected: null,
},
{
name: 'should not match plugin path with invalid plugin id len=191',
input: {
url: DEEPLINK_URL_ROOT + '/subpath/deepsubpath/plugins/abc_abc_abc_abc_abc_abc_abc_abc_abc_abc_abc_abc_abc_abc_abc_abc_abc_abc_abc_abc_abc_abc_abc_abc_abc_abc_abc_abc_abc_abc_abc_abc_abc_abc_abc_abc_abc_abc_abc_abc_abc_abc_abc_abc_abc_abc_abc_abc/api/testroute',
serverURL: SERVER_WITH_SUBPATH,
siteURL: SERVER_WITH_SUBPATH,
},
expected: null,
},
{
name: 'should match plugin path without a route',
input: {
url: DEEPLINK_URL_ROOT + '/subpath/deepsubpath/plugins/abc',
serverURL: SERVER_WITH_SUBPATH,
siteURL: SERVER_WITH_SUBPATH,
},
expected: {
data: {
id: 'abc',
route: undefined,
serverUrl: URL_PATH_NO_PROTOCOL,
teamName: '',
},
type: DeepLinkType.Plugin,
},
},
];
@ -283,10 +478,11 @@ describe('UrlUtils', () => {
const {name, input, expected} = test;
it(name, () => {
const match = matchDeepLink(input.url!, input.serverURL!, input.siteURL!);
const parsed = parseDeepLink(match);
Reflect.deleteProperty(parsed, 'url');
expect(parsed).toEqual(expected);
const matched = matchDeepLink(input.url!, input.serverURL!, input.siteURL!);
if (matched) {
Reflect.deleteProperty(matched, 'url');
}
expect(matched).toEqual(expected);
});
}
});

View file

@ -1929,7 +1929,7 @@
CODE_SIGN_ENTITLEMENTS = Mattermost/Mattermost.entitlements;
CODE_SIGN_IDENTITY = "iPhone Developer";
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
CURRENT_PROJECT_VERSION = 489;
CURRENT_PROJECT_VERSION = 496;
DEVELOPMENT_TEAM = UQ8HT4Q2XM;
ENABLE_BITCODE = NO;
HEADER_SEARCH_PATHS = (
@ -1973,7 +1973,7 @@
CODE_SIGN_ENTITLEMENTS = Mattermost/Mattermost.entitlements;
CODE_SIGN_IDENTITY = "iPhone Developer";
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
CURRENT_PROJECT_VERSION = 489;
CURRENT_PROJECT_VERSION = 496;
DEVELOPMENT_TEAM = UQ8HT4Q2XM;
ENABLE_BITCODE = NO;
HEADER_SEARCH_PATHS = (
@ -2116,7 +2116,7 @@
CODE_SIGN_IDENTITY = "iPhone Developer";
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 489;
CURRENT_PROJECT_VERSION = 496;
DEBUG_INFORMATION_FORMAT = dwarf;
DEVELOPMENT_TEAM = UQ8HT4Q2XM;
GCC_C_LANGUAGE_STANDARD = gnu11;
@ -2165,7 +2165,7 @@
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
CODE_SIGN_STYLE = Automatic;
COPY_PHASE_STRIP = NO;
CURRENT_PROJECT_VERSION = 489;
CURRENT_PROJECT_VERSION = 496;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
DEVELOPMENT_TEAM = UQ8HT4Q2XM;
GCC_C_LANGUAGE_STANDARD = gnu11;

View file

@ -21,7 +21,7 @@
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>2.9.0</string>
<string>2.10.1</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleURLTypes</key>
@ -37,7 +37,7 @@
</dict>
</array>
<key>CFBundleVersion</key>
<string>489</string>
<string>496</string>
<key>ITSAppUsesNonExemptEncryption</key>
<false/>
<key>LSRequiresIPhoneOS</key>

View file

@ -19,9 +19,9 @@
<key>CFBundlePackageType</key>
<string>XPC!</string>
<key>CFBundleShortVersionString</key>
<string>2.9.0</string>
<string>2.10.1</string>
<key>CFBundleVersion</key>
<string>489</string>
<string>496</string>
<key>UIAppFonts</key>
<array>
<string>OpenSans-Bold.ttf</string>

View file

@ -19,9 +19,9 @@
<key>CFBundlePackageType</key>
<string>XPC!</string>
<key>CFBundleShortVersionString</key>
<string>2.9.0</string>
<string>2.10.1</string>
<key>CFBundleVersion</key>
<string>489</string>
<string>496</string>
<key>NSExtension</key>
<dict>
<key>NSExtensionPointIdentifier</key>

23
package-lock.json generated
View file

@ -1,6 +1,6 @@
{
"name": "mattermost-mobile",
"version": "2.9.0",
"version": "2.10.1",
"lockfileVersion": 2,
"requires": true,
"packages": {
@ -42,7 +42,7 @@
"@voximplant/react-native-foreground-service": "3.0.2",
"base-64": "1.0.0",
"commonmark": "npm:@mattermost/commonmark@0.30.1-1",
"commonmark-react-renderer": "github:mattermost/commonmark-react-renderer#235bc817bcade503fb81fa51bbbe3c84f958ed12",
"commonmark-react-renderer": "github:mattermost/commonmark-react-renderer#81b5d27509652bae50b4b510ede777dd3bd923cf",
"deep-equal": "2.2.2",
"deepmerge": "4.3.1",
"emoji-regex": "10.2.1",
@ -52,6 +52,7 @@
"mime-db": "1.52.0",
"moment-timezone": "0.5.43",
"pako": "2.1.0",
"path-to-regexp": "6.2.1",
"react": "18.2.0",
"react-freeze": "1.0.3",
"react-intl": "6.4.4",
@ -9679,8 +9680,8 @@
},
"node_modules/commonmark-react-renderer": {
"version": "4.3.5",
"resolved": "git+ssh://git@github.com/mattermost/commonmark-react-renderer.git#235bc817bcade503fb81fa51bbbe3c84f958ed12",
"integrity": "sha512-GP3+17loU8tNal91FAlqDcDyUHdhahMPpV/XBm++VAyoyvsGKx3wGQZY/cVKXNx+SV2UANXVf4VFx8GpAkqCBQ==",
"resolved": "git+ssh://git@github.com/mattermost/commonmark-react-renderer.git#81b5d27509652bae50b4b510ede777dd3bd923cf",
"integrity": "sha512-2UYjN/Fix93L6udBsmZqCUggE7e5NA8rXHO2R3Dxs3lwHac3MsOMdEYZPST3Z/CCFc6gyKqWqDtCQXofbHffPA==",
"license": "MIT",
"dependencies": {
"lodash.assign": "^4.2.0",
@ -18577,6 +18578,11 @@
"resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz",
"integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw=="
},
"node_modules/path-to-regexp": {
"version": "6.2.1",
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-6.2.1.tgz",
"integrity": "sha512-JLyh7xT1kizaEvcaXOQwOc2/Yhw6KZOvPf1S8401UyLk86CU79LN3vl7ztXGm/pZ+YjoyAJ4rxmHwbkBXJX+yw=="
},
"node_modules/path-type": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz",
@ -28521,7 +28527,8 @@
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/@voximplant/react-native-foreground-service/-/react-native-foreground-service-3.0.2.tgz",
"integrity": "sha512-ZIyccOAXPqznA1PAVAYlKZ+GI7kXYCuYgH+gmAkhPouyJbkgrSXaCJJzQ+uBkPr4FBa/PuC/yjzK8vf6tJREQA==",
"requires": {}
"requires": {
}
},
"@webassemblyjs/ast": {
"version": "1.11.1",
@ -30055,9 +30062,9 @@
}
},
"commonmark-react-renderer": {
"version": "git+ssh://git@github.com/mattermost/commonmark-react-renderer.git#235bc817bcade503fb81fa51bbbe3c84f958ed12",
"integrity": "sha512-GP3+17loU8tNal91FAlqDcDyUHdhahMPpV/XBm++VAyoyvsGKx3wGQZY/cVKXNx+SV2UANXVf4VFx8GpAkqCBQ==",
"from": "commonmark-react-renderer@github:mattermost/commonmark-react-renderer#235bc817bcade503fb81fa51bbbe3c84f958ed12",
"version": "git+ssh://git@github.com/mattermost/commonmark-react-renderer.git#81b5d27509652bae50b4b510ede777dd3bd923cf",
"integrity": "sha512-2UYjN/Fix93L6udBsmZqCUggE7e5NA8rXHO2R3Dxs3lwHac3MsOMdEYZPST3Z/CCFc6gyKqWqDtCQXofbHffPA==",
"from": "commonmark-react-renderer@github:mattermost/commonmark-react-renderer#81b5d27509652bae50b4b510ede777dd3bd923cf",
"requires": {
"lodash.assign": "^4.2.0",
"lodash.isplainobject": "^4.0.6",

View file

@ -1,6 +1,6 @@
{
"name": "mattermost-mobile",
"version": "2.9.0",
"version": "2.10.1",
"description": "Mattermost Mobile with React Native",
"repository": "git@github.com:mattermost/mattermost-mobile.git",
"author": "Mattermost, Inc.",
@ -43,7 +43,7 @@
"@voximplant/react-native-foreground-service": "3.0.2",
"base-64": "1.0.0",
"commonmark": "npm:@mattermost/commonmark@0.30.1-1",
"commonmark-react-renderer": "github:mattermost/commonmark-react-renderer#235bc817bcade503fb81fa51bbbe3c84f958ed12",
"commonmark-react-renderer": "github:mattermost/commonmark-react-renderer#81b5d27509652bae50b4b510ede777dd3bd923cf",
"deep-equal": "2.2.2",
"deepmerge": "4.3.1",
"emoji-regex": "10.2.1",
@ -53,6 +53,7 @@
"mime-db": "1.52.0",
"moment-timezone": "0.5.43",
"pako": "2.1.0",
"path-to-regexp": "6.2.1",
"react": "18.2.0",
"react-freeze": "1.0.3",
"react-intl": "6.4.4",

View file

@ -21,11 +21,12 @@ export interface DeepLinkPermalink extends DeepLink {
}
export interface DeepLinkGM extends DeepLink {
channelId: string;
channelName: string;
}
export interface DeepLinkPlugin extends DeepLink {
id: string;
route?: string;
}
export type DeepLinkType = typeof DeepLink[keyof typeof DeepLink];