diff --git a/app/actions/remote/permalink.ts b/app/actions/remote/permalink.ts
index 98b7d7231..19aa9b3b4 100644
--- a/app/actions/remote/permalink.ts
+++ b/app/actions/remote/permalink.ts
@@ -1,11 +1,8 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
-import {fetchMyChannelsForTeam} from '@actions/remote/channel';
import DatabaseManager from '@database/manager';
-import {getCommonSystemValues} from '@queries/servers/system';
-import {getTeamById, getTeamByName} from '@queries/servers/team';
-import {permalinkBadTeam} from '@utils/draft';
+import {getCurrentTeam} from '@queries/servers/team';
import {displayPermalink} from '@utils/permalink';
import {PERMALINK_GENERIC_TEAM_NAME_REDIRECT} from '@utils/url';
@@ -21,30 +18,14 @@ export const showPermalink = async (serverUrl: string, teamName: string, postId:
try {
let name = teamName;
let team: TeamModel | undefined;
- const system = await getCommonSystemValues(database);
if (!name || name === PERMALINK_GENERIC_TEAM_NAME_REDIRECT) {
- team = await getTeamById(database, system.currentTeamId);
+ team = await getCurrentTeam(database);
if (team) {
name = team.name;
}
}
- if (!team) {
- team = await getTeamByName(database, name);
- if (!team) {
- permalinkBadTeam(intl);
- return {error: 'Bad Permalink team'};
- }
- }
-
- if (team.id !== system.currentTeamId) {
- const result = await fetchMyChannelsForTeam(serverUrl, team.id, true, 0, false, true);
- if (result.error) {
- return {error: result.error};
- }
- }
-
- await displayPermalink(team.name, postId, openAsPermalink);
+ await displayPermalink(name, postId, openAsPermalink);
return {error: undefined};
} catch (error) {
diff --git a/app/components/illustrations/join_private_channel.tsx b/app/components/illustrations/join_private_channel.tsx
new file mode 100644
index 000000000..26ecb36b7
--- /dev/null
+++ b/app/components/illustrations/join_private_channel.tsx
@@ -0,0 +1,149 @@
+// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
+// See LICENSE.txt for license information.
+import * as React from 'react';
+import Svg, {
+ Path,
+ Mask,
+ G,
+ EMaskUnits,
+} from 'react-native-svg';
+
+type Props = {
+ theme: Theme;
+}
+
+function SvgComponent({theme}: Props) {
+ return (
+
+ );
+}
+
+export default SvgComponent;
diff --git a/app/components/illustrations/join_public_channel.tsx b/app/components/illustrations/join_public_channel.tsx
new file mode 100644
index 000000000..e3f859cf9
--- /dev/null
+++ b/app/components/illustrations/join_public_channel.tsx
@@ -0,0 +1,125 @@
+// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
+// See LICENSE.txt for license information.
+import * as React from 'react';
+import Svg, {
+ Path,
+ Mask,
+ G,
+ EMaskUnits,
+} from 'react-native-svg';
+
+type Props = {
+ theme: Theme;
+}
+
+function SvgComponent({theme}: Props) {
+ return (
+
+ );
+}
+
+export default SvgComponent;
diff --git a/app/components/illustrations/message_not_viewable.tsx b/app/components/illustrations/message_not_viewable.tsx
new file mode 100644
index 000000000..82807dbde
--- /dev/null
+++ b/app/components/illustrations/message_not_viewable.tsx
@@ -0,0 +1,56 @@
+// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
+// See LICENSE.txt for license information.
+import * as React from 'react';
+import Svg, {Path, Ellipse} from 'react-native-svg';
+
+type Props = {
+ theme: Theme;
+}
+
+function SvgComponent({theme}: Props) {
+ return (
+
+ );
+}
+
+export default SvgComponent;
diff --git a/app/components/markdown/markdown.tsx b/app/components/markdown/markdown.tsx
index 8e6a6d87a..c95348aac 100644
--- a/app/components/markdown/markdown.tsx
+++ b/app/components/markdown/markdown.tsx
@@ -37,6 +37,7 @@ import type {
type MarkdownProps = {
autolinkedUrlSchemes?: string[];
baseTextStyle: StyleProp;
+ baseParagraphStyle?: StyleProp;
blockStyles?: MarkdownBlockStyles;
channelId?: string;
channelMentions?: ChannelMentions;
@@ -131,7 +132,7 @@ const Markdown = ({
enableInlineLatex, enableLatex,
imagesMetadata, isEdited, isReplyPost, isSearchResult, layoutHeight, layoutWidth,
location, mentionKeys, minimumHashtagLength = 3, onPostPress, postId, searchPatterns,
- textStyles = {}, theme, value = '',
+ textStyles = {}, theme, value = '', baseParagraphStyle,
}: MarkdownProps) => {
const style = getStyleSheet(theme);
@@ -419,7 +420,7 @@ const Markdown = ({
return (
-
+
{children}
diff --git a/app/queries/servers/team.ts b/app/queries/servers/team.ts
index cbb67db12..a6db7a42f 100644
--- a/app/queries/servers/team.ts
+++ b/app/queries/servers/team.ts
@@ -332,7 +332,7 @@ export const queryTeamsById = (database: Database, teamIds: string[]) => {
return database.get(TEAM).query(Q.where('id', Q.oneOf(teamIds)));
};
-export const queryTeamByName = async (database: Database, teamName: string) => {
+export const queryTeamByName = (database: Database, teamName: string) => {
return database.get(TEAM).query(Q.where('name', teamName));
};
diff --git a/app/screens/permalink/index.ts b/app/screens/permalink/index.ts
index f49c6d8b3..f12601914 100644
--- a/app/screens/permalink/index.ts
+++ b/app/screens/permalink/index.ts
@@ -7,21 +7,38 @@ import {of as of$} from 'rxjs';
import {switchMap} from 'rxjs/operators';
import {observePost} from '@queries/servers/post';
+import {observeCurrentTeamId, observeCurrentUserId} from '@queries/servers/system';
+import {queryMyTeamsByIds, queryTeamByName} from '@queries/servers/team';
import {observeIsCRTEnabled} from '@queries/servers/thread';
import {WithDatabaseArgs} from '@typings/database/database';
import PostModel from '@typings/database/models/servers/post';
import Permalink from './permalink';
-type OwnProps = {postId: PostModel['id']} & WithDatabaseArgs;
+type OwnProps = {
+ postId: PostModel['id'];
+ teamName?: string;
+} & WithDatabaseArgs;
-const enhance = withObservables([], ({database, postId}: OwnProps) => {
+const enhance = withObservables([], ({database, postId, teamName}: OwnProps) => {
const post = observePost(database, postId);
+ const team = teamName ? queryTeamByName(database, teamName).observe().pipe(
+ switchMap((ts) => {
+ const t = ts[0];
+ return t ? t.observe() : of$(undefined);
+ }),
+ ) : of$(undefined);
return {
channel: post.pipe(
switchMap((p) => (p ? p.channel.observe() : of$(undefined))),
),
+ isTeamMember: team.pipe(
+ switchMap((t) => (t ? queryMyTeamsByIds(database, [t.id]).observe() : of$(undefined))),
+ switchMap((ms) => of$(Boolean(ms?.[0]))),
+ ),
+ currentTeamId: observeCurrentTeamId(database),
+ currentUserId: observeCurrentUserId(database),
isCRTEnabled: observeIsCRTEnabled(database),
};
});
diff --git a/app/screens/permalink/permalink.tsx b/app/screens/permalink/permalink.tsx
index bb063ece7..9275bbbdf 100644
--- a/app/screens/permalink/permalink.tsx
+++ b/app/screens/permalink/permalink.tsx
@@ -2,12 +2,13 @@
// See LICENSE.txt for license information.
import React, {useCallback, useEffect, useMemo, useState} from 'react';
-import {BackHandler, Text, TouchableOpacity, View} from 'react-native';
+import {Alert, BackHandler, Text, TouchableOpacity, View} from 'react-native';
import Animated from 'react-native-reanimated';
import {Edge, SafeAreaView, useSafeAreaInsets} from 'react-native-safe-area-context';
-import {switchToChannelById} from '@actions/remote/channel';
-import {fetchPostsAround} from '@actions/remote/post';
+import {fetchChannelById, joinChannel, switchToChannelById} from '@actions/remote/channel';
+import {fetchPostById, fetchPostsAround} from '@actions/remote/post';
+import {addUserToTeam, fetchTeamByName, removeUserFromTeam} from '@actions/remote/team';
import CompassIcon from '@components/compass_icon';
import FormattedText from '@components/formatted_text';
import Loading from '@components/loading';
@@ -15,7 +16,9 @@ import PostList from '@components/post_list';
import {Screens} from '@constants';
import {useServerUrl} from '@context/server';
import {useTheme} from '@context/theme';
+import DatabaseManager from '@database/manager';
import {useIsTablet} from '@hooks/device';
+import {getChannelById, getMyChannel} from '@queries/servers/channel';
import {dismissModal} from '@screens/navigation';
import NavigationStore from '@store/navigation_store';
import {buttonBackgroundStyle, buttonTextStyle} from '@utils/buttonStyles';
@@ -24,11 +27,17 @@ import {preventDoubleTap} from '@utils/tap';
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
import {typography} from '@utils/typography';
+import PermalinkError from './permalink_error';
+
import type ChannelModel from '@typings/database/models/servers/channel';
import type PostModel from '@typings/database/models/servers/post';
type Props = {
channel?: ChannelModel;
+ teamName?: string;
+ isTeamMember?: boolean;
+ currentUserId: string;
+ currentTeamId: string;
isCRTEnabled: boolean;
postId: PostModel['id'];
}
@@ -48,6 +57,8 @@ const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => ({
flex: 1,
margin: 10,
opacity: 1,
+ borderWidth: 1,
+ borderColor: changeOpacity(theme.centerChannelColor, 0.16),
},
header: {
alignItems: 'center',
@@ -102,23 +113,18 @@ const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => ({
fontWeight: '600',
textAlignVertical: 'center',
},
- errorContainer: {
- alignItems: 'center',
- justifyContent: 'center',
- padding: 15,
- },
- errorText: {
- color: changeOpacity(theme.centerChannelColor, 0.4),
- fontSize: 15,
- },
- archiveIcon: {
- color: theme.centerChannelColor,
- fontSize: 16,
- paddingRight: 20,
- },
-}));
+}),
+);
-function Permalink({channel, isCRTEnabled, postId}: Props) {
+function Permalink({
+ channel,
+ isCRTEnabled,
+ postId,
+ teamName,
+ isTeamMember,
+ currentUserId,
+ currentTeamId,
+}: Props) {
const [posts, setPosts] = useState([]);
const [loading, setLoading] = useState(true);
const theme = useTheme();
@@ -126,6 +132,8 @@ function Permalink({channel, isCRTEnabled, postId}: Props) {
const insets = useSafeAreaInsets();
const isTablet = useIsTablet();
const style = getStyleSheet(theme);
+ const [error, setError] = useState();
+ const [channelId, setChannelId] = useState(channel?.id);
const containerStyle = useMemo(() => {
const marginTop = isTablet ? 60 : 20;
@@ -135,20 +143,113 @@ function Permalink({channel, isCRTEnabled, postId}: Props) {
useEffect(() => {
(async () => {
- if (channel?.id) {
- const data = await fetchPostsAround(serverUrl, channel.id, postId, 5, isCRTEnabled);
+ if (channelId) {
+ const data = await fetchPostsAround(serverUrl, channelId, postId, 5, isCRTEnabled);
+ if (data.error) {
+ setError({unreachable: true});
+ }
if (data?.posts) {
- setLoading(false);
setPosts(data.posts);
}
+ setLoading(false);
+ return;
}
+
+ const database = DatabaseManager.serverDatabases[serverUrl]?.database;
+ if (!database) {
+ setError({unreachable: true});
+ setLoading(false);
+ return;
+ }
+
+ // If a team is provided, try to join the team, but do not fail here, to take into account:
+ // - Wrong team name
+ // - DMs/GMs
+ let joinedTeam: Team | undefined;
+ if (teamName && !isTeamMember) {
+ const fetchData = await fetchTeamByName(serverUrl, teamName, true);
+ joinedTeam = fetchData.team;
+
+ if (joinedTeam) {
+ const addData = await addUserToTeam(serverUrl, joinedTeam.id, currentUserId);
+ if (addData.error) {
+ joinedTeam = undefined;
+ }
+ }
+ }
+
+ const {post} = await fetchPostById(serverUrl, postId, true);
+ if (!post) {
+ if (joinedTeam) {
+ removeUserFromTeam(serverUrl, joinedTeam.id, currentUserId);
+ }
+ setError({notExist: true});
+ setLoading(false);
+ return;
+ }
+
+ const myChannel = await getMyChannel(database, post.channel_id);
+ if (myChannel) {
+ const localChannel = await getChannelById(database, myChannel.id);
+
+ // Wrong team passed or DM/GM
+ if (joinedTeam && localChannel?.teamId !== '' && localChannel?.teamId !== joinedTeam.id) {
+ removeUserFromTeam(serverUrl, joinedTeam.id, currentUserId);
+ joinedTeam = undefined;
+ }
+
+ if (joinedTeam) {
+ setError({
+ joinedTeam: true,
+ channelId: myChannel.id,
+ channelName: localChannel?.displayName,
+ privateTeam: !joinedTeam.allow_open_invite,
+ teamName: joinedTeam.display_name,
+ teamId: joinedTeam.id,
+ });
+ setLoading(false);
+ return;
+ }
+ setChannelId(post.channel_id);
+ return;
+ }
+
+ const {channel: fetchedChannel} = await fetchChannelById(serverUrl, post.channel_id);
+ if (!fetchedChannel) {
+ if (joinedTeam) {
+ removeUserFromTeam(serverUrl, joinedTeam.id, currentUserId);
+ }
+ setError({notExist: true});
+ setLoading(false);
+ return;
+ }
+
+ // Wrong team passed or DM/GM
+ if (joinedTeam && fetchedChannel.team_id !== '' && fetchedChannel.team_id !== joinedTeam.id) {
+ removeUserFromTeam(serverUrl, joinedTeam.id, currentUserId);
+ joinedTeam = undefined;
+ }
+
+ setError({
+ privateChannel: fetchedChannel.type === 'P',
+ joinedTeam: Boolean(joinedTeam),
+ channelId: fetchedChannel.id,
+ channelName: fetchedChannel.display_name,
+ teamId: fetchedChannel.team_id || currentTeamId,
+ teamName: joinedTeam?.display_name,
+ privateTeam: joinedTeam && !joinedTeam.allow_open_invite,
+ });
+ setLoading(false);
})();
- }, [channel?.id]);
+ }, [channelId, teamName]);
const handleClose = useCallback(() => {
+ if (error?.joinedTeam && error.teamId) {
+ removeUserFromTeam(serverUrl, error.teamId, currentUserId);
+ }
dismissModal({componentId: Screens.PERMALINK});
closePermalink();
- }, []);
+ }, [error]);
useEffect(() => {
const listener = BackHandler.addEventListener('hardwareBackPress', () => {
@@ -171,6 +272,71 @@ function Permalink({channel, isCRTEnabled, postId}: Props) {
}
}), []);
+ const handleJoin = useCallback(preventDoubleTap(async () => {
+ if (error?.teamId && error.channelId) {
+ const {error: joinError} = await joinChannel(serverUrl, currentUserId, error.teamId, error.channelId);
+ if (joinError) {
+ Alert.alert('Error joining the channel', 'There was an error trying to join the channel');
+ return;
+ }
+ setLoading(true);
+ setError(undefined);
+ setChannelId(error.channelId);
+ }
+ }), [error, serverUrl, currentUserId]);
+
+ let content;
+ if (loading) {
+ content = (
+
+
+
+ );
+ } else if (error) {
+ content = (
+
+ );
+ } else {
+ content = (
+ <>
+
+
+
+
+
+
+
+
+ >
+ );
+ }
+
+ const showHeaderDivider = Boolean(channel?.displayName) && !error && !loading;
return (
- {Boolean(channel?.displayName) &&
-
- }
- {loading ? (
-
-
-
- ) : (
-
-
+ {showHeaderDivider && (
+
+
)}
-
-
-
-
-
+ {content}
);
diff --git a/app/screens/permalink/permalink_error.tsx b/app/screens/permalink/permalink_error.tsx
new file mode 100644
index 000000000..f0eea6939
--- /dev/null
+++ b/app/screens/permalink/permalink_error.tsx
@@ -0,0 +1,168 @@
+// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
+// See LICENSE.txt for license information.
+import React from 'react';
+import {useIntl} from 'react-intl';
+import {Text, TouchableOpacity, View} from 'react-native';
+
+import FormattedText from '@components/formatted_text';
+import JoinPrivateChannel from '@components/illustrations/join_private_channel';
+import JoinPublicChannel from '@components/illustrations/join_public_channel';
+import MessageNotViewable from '@components/illustrations/message_not_viewable';
+import Markdown from '@components/markdown';
+import {Screens} from '@constants';
+import {useTheme} from '@context/theme';
+import {buttonBackgroundStyle, buttonTextStyle} from '@utils/buttonStyles';
+import {getMarkdownBlockStyles, getMarkdownTextStyles} from '@utils/markdown';
+import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
+import {typography} from '@utils/typography';
+
+type Props = {
+ error: PermalinkErrorType;
+ handleClose: () => void;
+ handleJoin: () => void;
+}
+
+const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => {
+ return {
+ errorContainer: {
+ alignItems: 'center',
+ justifyContent: 'center',
+ marginHorizontal: 36,
+ flex: 1,
+ },
+ errorTitle: {
+ color: theme.centerChannelColor,
+ textAlign: 'center',
+ ...typography('Heading', 600, 'SemiBold'),
+ marginTop: 8,
+ marginBottom: 16,
+ },
+ errorText: {
+ textAlign: 'center',
+ color: theme.centerChannelColor,
+ ...typography('Body', 200, 'Regular'),
+ },
+ errorTextParagraph: {
+ textAlign: 'center',
+ },
+ errorButtonContainer: {
+ borderTopWidth: 1,
+ borderTopColor: changeOpacity(theme.centerChannelColor, 0.16),
+ padding: 20,
+ },
+
+ };
+});
+
+function PermalinkError({
+ error,
+ handleClose,
+ handleJoin,
+}: Props) {
+ const theme = useTheme();
+ const style = getStyleSheet(theme);
+ const intl = useIntl();
+
+ const buttonStylePrimary = buttonBackgroundStyle(theme, 'lg', 'primary');
+ const buttonTextStylePrimary = buttonTextStyle(theme, 'lg', 'primary');
+
+ if (error.notExist || error.unreachable) {
+ const title = intl.formatMessage({id: 'permalink.error.access.title', defaultMessage: 'Message not viewable'});
+ const text = intl.formatMessage({id: 'permalink.error.access.text', defaultMessage: 'The message you are trying to view is in a channel you don’t have access to or has been deleted.'});
+ return (
+ <>
+
+
+ {title}
+ {text}
+
+
+
+
+
+
+ >
+ );
+ }
+
+ const buttonStyleTertiary = buttonBackgroundStyle(theme, 'lg', 'tertiary');
+ const buttonTextStyleTertiary = buttonTextStyle(theme, 'lg', 'tertiary');
+
+ const isPrivate = error.privateChannel || error.privateTeam;
+ let image;
+ let title;
+ let text;
+ let button;
+ if (isPrivate && error.joinedTeam) {
+ image = ();
+ title = intl.formatMessage({id: 'permalink.error.private_channel_and_team.title', defaultMessage: 'Join private channel and team'});
+ text = intl.formatMessage({id: 'permalink.error.private_channel_and_team.text', defaultMessage: 'The message you are trying to view is in a private channel in a team you are not a member of. You have access as an admin. Do you want to join **{channelName}** and the **{teamName}** team to view it?'}, {channelName: error.channelName, teamName: error.teamName});
+ button = intl.formatMessage({id: 'permalink.error.private_channel_and_team.button', defaultMessage: 'Join channel and team'});
+ } else if (isPrivate) {
+ image = ();
+ title = intl.formatMessage({id: 'permalink.error.private_channel.title', defaultMessage: 'Join private channel'});
+ text = intl.formatMessage({id: 'permalink.error.private_channel.text', defaultMessage: 'The message you are trying to view is in a private channel you have not been invited to, but you have access as an admin. Do you still want to join **{channelName}**?'}, {channelName: error.channelName});
+ button = intl.formatMessage({id: 'permalink.error.private_channel.button', defaultMessage: 'Join channel'});
+ } else if (error.joinedTeam) {
+ image = ();
+ title = intl.formatMessage({id: 'permalink.error.public_channel_and_team.title', defaultMessage: 'Join channel and team'});
+ text = intl.formatMessage({id: 'permalink.error.public_channel_and_team.text', defaultMessage: 'The message you are trying to view is in a channel you don’t belong and a team you are not a member of. Do you want to join **{channelName}** and the **{teamName}** team to view it?'}, {channelName: error.channelName, teamName: error.teamName});
+ button = intl.formatMessage({id: 'permalink.error.public_channel_and_team.button', defaultMessage: 'Join channel and team'});
+ } else {
+ image = ();
+ title = intl.formatMessage({id: 'permalink.error.public_channel.title', defaultMessage: 'Join channel'});
+ text = intl.formatMessage({id: 'permalink.error.public_channel.text', defaultMessage: 'The message you are trying to view is in a channel you don’t belong to. Do you want to join **{channelName}** to view it?'}, {channelName: error.channelName});
+ button = intl.formatMessage({id: 'permalink.error.public_channel.button', defaultMessage: 'Join channel'});
+ }
+ return (
+ <>
+
+ {image}
+ {title}
+
+
+
+
+ {button}
+
+
+
+
+
+ >
+ );
+}
+
+export default PermalinkError;
diff --git a/app/utils/permalink/index.ts b/app/utils/permalink/index.ts
index 4d04d096a..b52f1dba2 100644
--- a/app/utils/permalink/index.ts
+++ b/app/utils/permalink/index.ts
@@ -4,6 +4,7 @@
import {Keyboard, Platform} from 'react-native';
import {OptionsModalPresentationStyle} from 'react-native-navigation';
+import {Screens} from '@constants';
import {dismissAllModals, showModalOverCurrentContext} from '@screens/navigation';
import {changeOpacity} from '@utils/theme';
@@ -15,7 +16,7 @@ export const displayPermalink = async (teamName: string, postId: string, openAsP
await dismissAllModals();
}
- const screen = 'Permalink';
+ const screen = Screens.PERMALINK;
const passProps = {
isPermalink: openAsPermalink,
teamName,
diff --git a/assets/base/i18n/en.json b/assets/base/i18n/en.json
index 4909276a5..b74e49830 100644
--- a/assets/base/i18n/en.json
+++ b/assets/base/i18n/en.json
@@ -598,6 +598,22 @@
"password_send.link.title": "Reset Link Sent",
"password_send.reset": "Reset Your Password",
"password_send.return": "Return to Log In",
+ "permalink.error.access.text": "The message you are trying to view is in a channel you don’t have access to or has been deleted.",
+ "permalink.error.access.title": "Message not viewable",
+ "permalink.error.cancel": "Cancel",
+ "permalink.error.okay": "Okay",
+ "permalink.error.private_channel_and_team.button": "Join channel and team",
+ "permalink.error.private_channel_and_team.text": "The message you are trying to view is in a private channel in a team you are not a member of. You have access as an admin. Do you want to join **{channelName}** and the **{teamName}** team to view it?",
+ "permalink.error.private_channel_and_team.title": "Join private channel and team",
+ "permalink.error.private_channel.button": "Join channel",
+ "permalink.error.private_channel.text": "The message you are trying to view is in a private channel you have not been invited to, but you have access as an admin. Do you still want to join **{channelName}**?",
+ "permalink.error.private_channel.title": "Join private channel",
+ "permalink.error.public_channel_and_team.button": "Join channel and team",
+ "permalink.error.public_channel_and_team.text": "The message you are trying to view is in a channel you don’t belong and a team you are not a member of. Do you want to join **{channelName}** and the **{teamName}** team to view it?",
+ "permalink.error.public_channel_and_team.title": "Join channel and team",
+ "permalink.error.public_channel.button": "Join channel",
+ "permalink.error.public_channel.text": "The message you are trying to view is in a channel you don’t belong to. Do you want to join **{channelName}** to view it?",
+ "permalink.error.public_channel.title": "Join channel",
"permalink.show_dialog_warn.cancel": "Cancel",
"permalink.show_dialog_warn.description": "You are about to join {channel} without explicitly being added by the channel admin. Are you sure you wish to join this private channel?",
"permalink.show_dialog_warn.join": "Join",
diff --git a/types/components/permalink.d.ts b/types/components/permalink.d.ts
new file mode 100644
index 000000000..276bb5ec7
--- /dev/null
+++ b/types/components/permalink.d.ts
@@ -0,0 +1,14 @@
+// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
+// See LICENSE.txt for license information.
+
+type PermalinkErrorType = {
+ unreachable?: boolean;
+ notExist?: boolean;
+ joinedTeam?: boolean;
+ privateChannel?: boolean;
+ privateTeam?: boolean;
+ teamName?: string;
+ channelName?: string;
+ teamId?: string;
+ channelId?: string;
+}