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:
parent
d732b5a33e
commit
da2f38e072
11 changed files with 557 additions and 62 deletions
|
|
@ -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');
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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}>
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
|
|
@ -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;
|
||||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
},
|
||||
}],
|
||||
},
|
||||
}),
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
23
app/hooks/use_external_link_handler.ts
Normal file
23
app/hooks/use_external_link_handler.ts
Normal 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;
|
||||
|
|
@ -772,6 +772,7 @@
|
|||
"mobile.pdf_viewer.password_limit_reached": "You’ve 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",
|
||||
|
|
|
|||
Loading…
Reference in a new issue