Mention, embedded link and handling of post update on offline device (#9104)

* typescript and view component for permalink with user and message

* Old post edited handling in permalink

* Added test and update flag value to EnablePermalinkPreview

* Added test for permalink_preview component

* Added test for content/index.tsx for permalink

* Addressed review comments

* Unit test for missing file and review comments

* Added test to check handlePostEdited permalink sync only calls one time only

* Change TouchableOpacity to Pressable

* When user not found fetch the user from the server

* Removed the redundant test in the test for permalink_preview/index?

* ts to tsx

* Removed the circular dependency

* Address review comments

* displayname fallback

* remove permalink when permalink post is deleted

* UX review comments

* Linter fixes

* Test fixes

* File attachment in permalink preview component

* Fix the width and height of the image in permalink

* Added gredient when exceeds height of permalink container

* Minor

* Updated tests

* Minor

* Review comments

* Minor review comments

* type fixes

* Review comments

* Minor

* Mention ability in permalink preview

* Support for external link in permalink

* Handle device not connected update permalink post

* test fixes

* Address review comments

* Minor

* Merge fixes

* Addressed review comments

* Fix content.test.tsx after permalink branch merge

- Updated test expectations to use embedData prop instead of post prop
- Fixed assertions to match the new PermalinkPreview API
- All content tests now pass

* review comments

* Review comments

* Some more review comments

* Minor

* Fixed the undefined issue for opengraph component metadata

* More fixes

* type chech for site name

* linter fixes

* UX review and remove show more height not require

* Fix tests

* Review nitpick and fixes

* Minor UX changes

---------

Co-authored-by: yasserfaraazkhan <attitude3cena.yf@gmail.com>
This commit is contained in:
Rajat Dabade 2025-09-12 14:06:58 +05:30 committed by GitHub
parent d732b5a33e
commit da2f38e072
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
11 changed files with 557 additions and 62 deletions

View file

@ -95,6 +95,7 @@ describe('components/post_list/post/body/content/Content - PermalinkPreview', ()
);
const permalinkPreview = getByTestId('permalink-preview');
expect(permalinkPreview.props.embedData).toBeDefined();
expect(permalinkPreview.props.embedData).toEqual(permalinkEmbedData);
});
@ -134,6 +135,7 @@ describe('components/post_list/post/body/content/Content - PermalinkPreview', ()
);
const permalinkPreview = getByTestId('permalink-preview');
expect(permalinkPreview.props.embedData).toBeDefined();
expect(permalinkPreview.props.embedData).toEqual(customEmbedData);
});
@ -202,6 +204,7 @@ describe('components/post_list/post/body/content/Content - PermalinkPreview', ()
);
const permalinkPreview = getByTestId('permalink-preview');
expect(permalinkPreview.props.embedData).toBeDefined();
expect(permalinkPreview.props.embedData).toEqual(firstEmbedData);
});
@ -234,6 +237,7 @@ describe('components/post_list/post/body/content/Content - PermalinkPreview', ()
);
const permalinkPreview = getByTestId('permalink-preview');
expect(permalinkPreview.props.embedData).toBeDefined();
expect(permalinkPreview.props.embedData).toEqual(permalinkEmbedData);
});
@ -271,6 +275,7 @@ describe('components/post_list/post/body/content/Content - PermalinkPreview', ()
);
const permalinkPreview = getByTestId('permalink-preview');
expect(permalinkPreview.props.embedData).toBeDefined();
expect(permalinkPreview.props.embedData).toEqual(dmEmbedData);
expect(permalinkPreview.props.embedData.channel_type).toBe('D');
});
@ -309,6 +314,7 @@ describe('components/post_list/post/body/content/Content - PermalinkPreview', ()
);
const permalinkPreview = getByTestId('permalink-preview');
expect(permalinkPreview.props.embedData).toBeDefined();
expect(permalinkPreview.props.embedData).toEqual(gmEmbedData);
expect(permalinkPreview.props.embedData.channel_type).toBe('G');
});

View file

@ -2,13 +2,11 @@
// See LICENSE.txt for license information.
import {Image} from 'expo-image';
import React, {useCallback} from 'react';
import {useIntl} from 'react-intl';
import React from 'react';
import {Text, View} from 'react-native';
import {useExternalLinkHandler} from '@hooks/use_external_link_handler';
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
import {tryOpenURL} from '@utils/url';
import {onOpenLinkError} from '@utils/url/links';
type Props = {
icon?: string;
@ -37,18 +35,8 @@ const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => {
});
const AttachmentAuthor = ({icon, link, name, theme}: Props) => {
const intl = useIntl();
const style = getStyleSheet(theme);
const openLink = useCallback(() => {
if (link) {
const onError = () => {
onOpenLinkError(intl);
};
tryOpenURL(link, onError);
}
}, [intl, link]);
const openLink = useExternalLinkHandler(link);
return (
<View style={style.container}>

View file

@ -1,14 +1,12 @@
// 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 React from 'react';
import {Text, View} from 'react-native';
import Markdown from '@components/markdown';
import {useExternalLinkHandler} from '@hooks/use_external_link_handler';
import {makeStyleSheetFromTheme} from '@utils/theme';
import {tryOpenURL} from '@utils/url';
import {onOpenLinkError} from '@utils/url/links';
import type {AvailableScreens} from '@typings/screens/navigation';
@ -39,18 +37,8 @@ const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => {
});
const AttachmentTitle = ({channelId, link, location, theme, value}: Props) => {
const intl = useIntl();
const style = getStyleSheet(theme);
const openLink = useCallback(() => {
if (link) {
const onError = () => {
onOpenLinkError(intl);
};
tryOpenURL(link, onError);
}
}, [intl, link]);
const openLink = useExternalLinkHandler(link);
let title;
if (link) {

View file

@ -1,13 +1,13 @@
// 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 React from 'react';
import {Text, TouchableOpacity, View} from 'react-native';
import {useExternalLinkHandler} from '@hooks/use_external_link_handler';
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
import {tryOpenURL} from '@utils/url';
import {onOpenLinkError} from '@utils/url/links';
import ExternalLinkPreview from '../permalink_preview/external_link_preview';
import OpengraphImage from './opengraph_image';
@ -19,6 +19,7 @@ type OpengraphProps = {
postId: string;
showLinkPreviews: boolean;
theme: Theme;
isEmbedded?: boolean;
}
const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => {
@ -60,9 +61,8 @@ const selectOpenGraphData = (url: string, metadata: PostMetadata | undefined | n
})?.data;
};
const Opengraph = ({isReplyPost, layoutWidth, location, metadata, postId, showLinkPreviews, theme}: OpengraphProps) => {
const intl = useIntl();
const link = metadata?.embeds![0]!.url || '';
const Opengraph = ({isReplyPost, layoutWidth, location, metadata, postId, showLinkPreviews, theme, isEmbedded}: OpengraphProps) => {
const link = metadata?.embeds?.[0]?.url || '';
const openGraphData = selectOpenGraphData(link, metadata);
if (!showLinkPreviews || !openGraphData) {
@ -76,13 +76,7 @@ const Opengraph = ({isReplyPost, layoutWidth, location, metadata, postId, showLi
openGraphData.images.length &&
metadata?.images);
const goToLink = useCallback(() => {
const onError = () => {
onOpenLinkError(intl);
};
tryOpenURL(link, onError);
}, [intl, link]);
const goToLink = useExternalLinkHandler(link);
let siteName;
if (openGraphData.site_name) {
@ -136,22 +130,28 @@ const Opengraph = ({isReplyPost, layoutWidth, location, metadata, postId, showLi
}
return (
<View style={style.container}>
{siteName}
{siteTitle}
{siteDescription}
{hasImage &&
<OpengraphImage
isReplyPost={isReplyPost}
layoutWidth={layoutWidth}
location={location}
openGraphImages={openGraphData.images as never[]}
metadata={metadata}
postId={postId}
theme={theme}
/>
}
</View>
<>
{isEmbedded ? (
<ExternalLinkPreview embeds={metadata?.embeds}/>
) : (
<View style={style.container}>
{siteName}
{siteTitle}
{siteDescription}
{hasImage &&
<OpengraphImage
isReplyPost={isReplyPost}
layoutWidth={layoutWidth}
location={location}
openGraphImages={openGraphData.images as never[]}
metadata={metadata}
postId={postId}
theme={theme}
/>
}
</View>
)}
</>
);
};

View file

@ -0,0 +1,212 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {fireEvent} from '@testing-library/react-native';
import React from 'react';
import DatabaseManager from '@database/manager';
import {renderWithEverything} from '@test/intl-test-helper';
import TestHelper from '@test/test_helper';
import * as UrlLinks from '@utils/url/links';
import ExternalLinkPreview from './external_link_preview';
import type ServerDataOperator from '@database/operator/server_data_operator';
import type {Database} from '@nozbe/watermelondb';
describe('components/post_list/post/body/content/permalink_preview/ExternalLinkPreview', () => {
const serverUrl = 'http://localhost:8065';
let database: Database;
let operator: ServerDataOperator;
const mockOpenLink = jest.spyOn(UrlLinks, 'openLink').mockImplementation(() => Promise.resolve());
beforeEach(async () => {
await DatabaseManager.init([serverUrl]);
const serverDatabaseAndOperator = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
database = serverDatabaseAndOperator.database;
operator = serverDatabaseAndOperator.operator;
// Add a current user
const currentUser = TestHelper.fakeUser({id: 'current-user', locale: 'en'});
await operator.handleUsers({users: [currentUser], prepareRecordsOnly: false});
await operator.handleSystem({
systems: [{id: 'currentUserId', value: currentUser.id}],
prepareRecordsOnly: false,
});
mockOpenLink.mockClear();
});
afterEach(async () => {
await DatabaseManager.destroyServerDatabase(serverUrl);
});
it('should not render when no embeds are provided', () => {
const {queryByTestId} = renderWithEverything(
<ExternalLinkPreview embeds={undefined}/>,
{database, serverUrl},
);
expect(queryByTestId('external-link-preview')).toBeNull();
});
it('should not render when embeds array is empty', () => {
const {queryByTestId} = renderWithEverything(
<ExternalLinkPreview embeds={[]}/>,
{database, serverUrl},
);
expect(queryByTestId('external-link-preview')).toBeNull();
});
it('should not render when no opengraph embed is present', () => {
const embeds = [{
type: 'image' as PostEmbedType,
url: 'https://example.com/image.jpg',
data: {},
}];
const {queryByTestId} = renderWithEverything(
<ExternalLinkPreview embeds={embeds}/>,
{database, serverUrl},
);
expect(queryByTestId('external-link-preview')).toBeNull();
});
it('should render external link when opengraph embed is present', () => {
const embeds = [{
type: 'opengraph' as PostEmbedType,
url: 'https://example.com',
data: {
title: 'Example Website',
description: 'This is an example website',
site_name: 'Example',
},
}];
const {getByTestId, getByText} = renderWithEverything(
<ExternalLinkPreview embeds={embeds}/>,
{database, serverUrl},
);
expect(getByTestId('external-link-preview')).toBeTruthy();
expect(getByText('Example Website')).toBeTruthy();
expect(getByText('This is an example website')).toBeTruthy();
});
it('should show fallback text when link data is incomplete', () => {
const embeds = [{
type: 'opengraph' as PostEmbedType,
url: 'https://example.com',
data: {},
}];
const {getByText} = renderWithEverything(
<ExternalLinkPreview embeds={embeds}/>,
{database, serverUrl},
);
expect(getByText('External Link')).toBeTruthy();
expect(getByText('https://example.com')).toBeTruthy();
});
it('should prioritize title over site_name', () => {
const embeds = [{
type: 'opengraph' as PostEmbedType,
url: 'https://example.com',
data: {
title: 'Page Title',
site_name: 'Site Name',
description: 'Description text',
},
}];
const {getByText, queryByText} = renderWithEverything(
<ExternalLinkPreview embeds={embeds}/>,
{database, serverUrl},
);
expect(getByText('Page Title')).toBeTruthy();
expect(queryByText('Site Name')).toBeNull();
expect(getByText('Description text')).toBeTruthy();
});
it('should use site_name when title is not available', () => {
const embeds = [{
type: 'opengraph' as PostEmbedType,
url: 'https://example.com',
data: {
site_name: 'Site Name',
description: 'Description text',
},
}];
const {getByText} = renderWithEverything(
<ExternalLinkPreview embeds={embeds}/>,
{database, serverUrl},
);
expect(getByText('Site Name')).toBeTruthy();
expect(getByText('Description text')).toBeTruthy();
});
it('should show URL when description is not available', () => {
const embeds = [{
type: 'opengraph' as PostEmbedType,
url: 'https://example.com',
data: {
title: 'Page Title',
},
}];
const {getByText} = renderWithEverything(
<ExternalLinkPreview embeds={embeds}/>,
{database, serverUrl},
);
expect(getByText('Page Title')).toBeTruthy();
expect(getByText('https://example.com')).toBeTruthy();
});
it('should handle press event and open URL', () => {
const embeds = [{
type: 'opengraph' as PostEmbedType,
url: 'https://example.com',
data: {
title: 'Example Website',
description: 'Example description',
},
}];
const {getByTestId} = renderWithEverything(
<ExternalLinkPreview embeds={embeds}/>,
{database, serverUrl},
);
const linkContainer = getByTestId('external-link-preview');
fireEvent.press(linkContainer);
expect(mockOpenLink).toHaveBeenCalledWith('https://example.com', 'http://localhost:8065', '', expect.any(Object));
});
it('should use custom testID when provided', () => {
const embeds = [{
type: 'opengraph' as PostEmbedType,
url: 'https://example.com',
data: {
title: 'Example Website',
},
}];
const {getByTestId, queryByTestId} = renderWithEverything(
<ExternalLinkPreview
embeds={embeds}
testID='custom-test-id'
/>,
{database, serverUrl},
);
expect(getByTestId('custom-test-id')).toBeTruthy();
expect(queryByTestId('external-link-preview')).toBeNull();
});
});

View file

@ -0,0 +1,125 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React, {useMemo} from 'react';
import {Text, TouchableOpacity} from 'react-native';
import FormattedText from '@components/formatted_text';
import {useTheme} from '@context/theme';
import {useExternalLinkHandler} from '@hooks/use_external_link_handler';
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
import {typography} from '@utils/typography';
type ExternalLinkData = {
url: string;
data: Record<string, any>;
};
type ExternalLinkPreviewProps = {
embeds?: PostEmbed[];
testID?: string;
};
const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => {
return {
externalLinkContainer: {
backgroundColor: theme.centerChannelBg,
borderColor: changeOpacity(theme.centerChannelColor, 0.16),
borderWidth: 1,
borderRadius: 4,
padding: 12,
marginBottom: 12,
},
externalLinkTitle: {
color: theme.linkColor,
...typography('Body', 100, 'SemiBold'),
marginBottom: 4,
},
externalLinkUrl: {
color: changeOpacity(theme.centerChannelColor, 0.64),
...typography('Body', 75),
},
};
});
const ExternalLinkPreview = ({embeds, testID = 'external-link-preview'}: ExternalLinkPreviewProps) => {
const theme = useTheme();
const styles = getStyleSheet(theme);
const externalLinkData = useMemo((): ExternalLinkData | null => {
if (!embeds || embeds.length === 0) {
return null;
}
const linkEmbed = embeds.find((embed) => embed.type === 'opengraph');
if (!linkEmbed) {
return null;
}
return {
url: linkEmbed.url,
data: linkEmbed.data,
};
}, [embeds]);
const displayTitle = useMemo(() => {
if (!externalLinkData?.data) {
return null;
}
const title = (typeof externalLinkData.data.title === 'string' && externalLinkData.data.title.trim()) ? externalLinkData.data.title : null;
const siteName = (typeof externalLinkData.data.site_name === 'string' && externalLinkData.data.site_name.trim()) ? externalLinkData.data.site_name : null;
return title || siteName;
}, [externalLinkData?.data]);
const displayDescription = useMemo(() => {
if (!externalLinkData?.data) {
return externalLinkData?.url || '';
}
const description = (typeof externalLinkData.data.description === 'string' && externalLinkData.data.description.trim()) ? externalLinkData.data.description : null;
return description || externalLinkData.url;
}, [externalLinkData?.data, externalLinkData?.url]);
const handleExternalLinkPress = useExternalLinkHandler(externalLinkData?.url);
if (!externalLinkData) {
return null;
}
return (
<TouchableOpacity
style={styles.externalLinkContainer}
onPress={handleExternalLinkPress}
testID={testID}
>
{displayTitle ? (
<Text
style={styles.externalLinkTitle}
numberOfLines={2}
ellipsizeMode='tail'
>
{displayTitle}
</Text>
) : (
<FormattedText
id='mobile.permalink_preview.external_link'
defaultMessage='External Link'
style={styles.externalLinkTitle}
numberOfLines={2}
ellipsizeMode='tail'
/>
)}
<Text
style={styles.externalLinkUrl}
numberOfLines={1}
ellipsizeMode='tail'
>
{displayDescription}
</Text>
</TouchableOpacity>
);
};
export default ExternalLinkPreview;

View file

@ -14,6 +14,34 @@ import EnhancedPermalinkPreview from './index';
import type ServerDataOperator from '@database/operator/server_data_operator';
import type {Database} from '@nozbe/watermelondb';
import type PostModel from '@typings/database/models/servers/post';
const createPostWithPermalinkEmbed = async (
operator: ServerDataOperator,
database: Database,
embedData: PermalinkEmbedData,
): Promise<PostModel> => {
const postWithPermalink = TestHelper.fakePost({
id: `referencing-post-${Date.now()}`,
metadata: {
embeds: [{
type: 'permalink' as PostEmbedType,
url: '',
data: embedData,
}],
},
});
const models = await operator.handlePosts({
actionType: 'POSTS.RECEIVED_NEW' as 'POSTS.RECEIVED_NEW',
order: [postWithPermalink.id],
posts: [postWithPermalink],
prepareRecordsOnly: true,
});
await operator.batchRecords(models, 'test');
return await database.get('Post').find(postWithPermalink.id) as PostModel;
};
jest.mock('./permalink_preview', () => ({
__esModule: true,
@ -75,6 +103,25 @@ describe('PermalinkPreview Enhanced Component', () => {
channel_id: 'channel-123',
};
const postWithPermalink = TestHelper.fakePost({
id: 'referencing-post',
metadata: {
embeds: [{
type: 'permalink' as PostEmbedType,
url: '',
data: embedData,
}],
},
});
const models = await operator.handlePosts({
actionType: 'POSTS.RECEIVED_NEW' as 'POSTS.RECEIVED_NEW',
order: [postWithPermalink.id],
posts: [postWithPermalink],
prepareRecordsOnly: true,
});
await operator.batchRecords(models, 'test');
const {getByTestId} = renderWithEverything(
<EnhancedPermalinkPreview
embedData={embedData}
@ -106,6 +153,8 @@ describe('PermalinkPreview Enhanced Component', () => {
channel_id: 'channel-123',
};
await createPostWithPermalinkEmbed(operator, database, embedData);
const {getByTestId} = renderWithEverything(
<EnhancedPermalinkPreview
embedData={embedData}
@ -145,6 +194,8 @@ describe('PermalinkPreview Enhanced Component', () => {
channel_id: 'channel-123',
};
await createPostWithPermalinkEmbed(operator, database, embedData);
const {getByTestId} = renderWithEverything(
<EnhancedPermalinkPreview
embedData={embedData}
@ -174,6 +225,8 @@ describe('PermalinkPreview Enhanced Component', () => {
channel_id: 'channel-123',
};
await createPostWithPermalinkEmbed(operator, database, embedData);
const {getByTestId} = renderWithEverything(
<EnhancedPermalinkPreview
embedData={embedData}
@ -213,6 +266,8 @@ describe('PermalinkPreview Enhanced Component', () => {
channel_id: 'channel-123',
};
await createPostWithPermalinkEmbed(operator, database, embedData);
const {getByTestId} = renderWithEverything(
<EnhancedPermalinkPreview
embedData={embedData}

View file

@ -72,6 +72,16 @@ describe('components/post_list/post/body/content/permalink_preview/PermalinkPrev
message: 'This is a test message',
create_at: 1234567890000,
edit_at: 0,
metadata: {
embeds: [{
type: 'opengraph' as PostEmbedType,
url: 'https://example.com',
data: {
title: 'Example Title',
description: 'Example Description',
},
}],
},
}),
team_name: 'test-team',
channel_display_name: 'Test Channel',
@ -158,6 +168,16 @@ describe('components/post_list/post/body/content/permalink_preview/PermalinkPrev
id: 'post-123',
user_id: '',
message: 'Test message',
metadata: {
embeds: [{
type: 'opengraph' as PostEmbedType,
url: 'https://example.com',
data: {
title: 'Example Title',
description: 'Example Description',
},
}],
},
}),
},
};
@ -196,6 +216,16 @@ describe('components/post_list/post/body/content/permalink_preview/PermalinkPrev
id: 'post-123',
user_id: 'user-123',
message: longMessage,
metadata: {
embeds: [{
type: 'opengraph' as PostEmbedType,
url: 'https://example.com',
data: {
title: 'Example Title',
description: 'Example Description',
},
}],
},
}),
},
};
@ -214,6 +244,16 @@ describe('components/post_list/post/body/content/permalink_preview/PermalinkPrev
id: 'post-123',
user_id: 'user-123',
message: '',
metadata: {
embeds: [{
type: 'opengraph' as PostEmbedType,
url: 'https://example.com',
data: {
title: 'Example Title',
description: 'Example Description',
},
}],
},
}),
},
};
@ -233,6 +273,16 @@ describe('components/post_list/post/body/content/permalink_preview/PermalinkPrev
message: 'Edited message',
edit_at: 1234567891000,
create_at: 1234567890000,
metadata: {
embeds: [{
type: 'opengraph' as PostEmbedType,
url: 'https://example.com',
data: {
title: 'Example Title',
description: 'Example Description',
},
}],
},
}),
},
};
@ -271,6 +321,14 @@ describe('components/post_list/post/body/content/permalink_preview/PermalinkPrev
message: 'Post with file attachment',
metadata: {
files: [fileInfo],
embeds: [{
type: 'opengraph' as PostEmbedType,
url: 'https://example.com',
data: {
title: 'Example Title',
description: 'Example Description',
},
}],
},
}),
},
@ -309,6 +367,14 @@ describe('components/post_list/post/body/content/permalink_preview/PermalinkPrev
message: 'Post with multiple attachments',
metadata: {
files: fileInfos,
embeds: [{
type: 'opengraph' as PostEmbedType,
url: 'https://example.com',
data: {
title: 'Example Title',
description: 'Example Description',
},
}],
},
}),
},
@ -330,7 +396,16 @@ describe('components/post_list/post/body/content/permalink_preview/PermalinkPrev
id: 'post-123',
user_id: 'user-123',
message: 'Post without files',
metadata: {},
metadata: {
embeds: [{
type: 'opengraph' as PostEmbedType,
url: 'https://example.com',
data: {
title: 'Example Title',
description: 'Example Description',
},
}],
},
}),
},
};
@ -353,6 +428,14 @@ describe('components/post_list/post/body/content/permalink_preview/PermalinkPrev
message: 'Post with empty files',
metadata: {
files: [],
embeds: [{
type: 'opengraph' as PostEmbedType,
url: 'https://example.com',
data: {
title: 'Example Title',
description: 'Example Description',
},
}],
},
}),
},

View file

@ -21,14 +21,18 @@ import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
import {typography} from '@utils/typography';
import {displayUsername, getUserTimezone} from '@utils/user';
import Opengraph from '../opengraph';
import PermalinkFiles from './permalink_files';
import type PostModel from '@typings/database/models/servers/post';
import type UserModel from '@typings/database/models/servers/user';
import type {UserMentionKey} from '@typings/global/markdown';
import type {AvailableScreens} from '@typings/screens/navigation';
const MAX_PERMALINK_PREVIEW_CHARACTERS = 150;
const EDITED_INDICATOR_CONTEXT = ['paragraph'];
const EMPTY_MENTION_KEYS: UserMentionKey[] = [];
type PermalinkPreviewProps = {
embedData: PermalinkEmbedData;
@ -238,11 +242,11 @@ const PermalinkPreview = ({
baseTextStyle={styles.messageText}
blockStyles={blockStyles}
channelId={embedData.channel_id}
disableAtMentions={true}
location={location}
theme={theme}
textStyles={textStyles}
value={truncatedMessage}
mentionKeys={currentUser?.mentionKeys ?? EMPTY_MENTION_KEYS}
/>
{isEdited ? (
<EditedIndicator
@ -255,6 +259,16 @@ const PermalinkPreview = ({
) : null}
</View>
<Opengraph
isReplyPost={false}
removeLinkPreview={false}
location={location}
metadata={embedData?.post?.metadata || null}
postId={embedData?.post?.id || ''}
theme={theme}
isEmbedded={true}
/>
{hasFiles && post && (
<PermalinkFiles
post={post}

View file

@ -0,0 +1,23 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {useCallback} from 'react';
import {useIntl} from 'react-intl';
import {useServerUrl} from '@context/server';
import {openLink} from '@utils/url/links';
export const useExternalLinkHandler = (url?: string, siteURL?: string) => {
const intl = useIntl();
const serverUrl = useServerUrl();
return useCallback(() => {
if (!url || !serverUrl) {
return;
}
openLink(url, serverUrl, siteURL || '', intl);
}, [intl, url, serverUrl, siteURL]);
};
export default useExternalLinkHandler;

View file

@ -772,6 +772,7 @@
"mobile.pdf_viewer.password_limit_reached": "Youve entered the wrong password too many times.",
"mobile.pdf_viewer.password_required": "This document is password protected.",
"mobile.pdf_viewer.unlock": "Unlock",
"mobile.permalink_preview.external_link": "External Link",
"mobile.permalink_preview.originally_posted": "Originally posted in ",
"mobile.permission_denied_dismiss": "Don't Allow",
"mobile.permission_denied_retry": "Settings",