Ability to show attachment if the permalink post contains attachments. (#9101)
* 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 * Address review comments * Minor * Merge fixes * Addressed review comments * Some more review comments * linter fixes * UX review and remove show more height not require * Fix tests * Review fixes, dev and ux --------- Co-authored-by: yasserfaraazkhan <attitude3cena.yf@gmail.com>
This commit is contained in:
parent
71fed8aa50
commit
d732b5a33e
9 changed files with 511 additions and 120 deletions
|
|
@ -45,7 +45,6 @@ type FileProps = {
|
||||||
const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => {
|
const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => {
|
||||||
return {
|
return {
|
||||||
fileWrapper: {
|
fileWrapper: {
|
||||||
flex: 1,
|
|
||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
borderWidth: 1,
|
borderWidth: 1,
|
||||||
|
|
@ -64,7 +63,6 @@ const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => {
|
||||||
margin: 4,
|
margin: 4,
|
||||||
},
|
},
|
||||||
audioFile: {
|
audioFile: {
|
||||||
flex: 1,
|
|
||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -26,16 +26,17 @@ type FilesProps = {
|
||||||
isReplyPost: boolean;
|
isReplyPost: boolean;
|
||||||
postId?: string;
|
postId?: string;
|
||||||
postProps?: Record<string, unknown>;
|
postProps?: Record<string, unknown>;
|
||||||
|
isPermalinkPreview?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const MAX_VISIBLE_ROW_IMAGES = 4;
|
const MAX_VISIBLE_ROW_IMAGES = 4;
|
||||||
const styles = StyleSheet.create({
|
const styles = StyleSheet.create({
|
||||||
row: {
|
row: {
|
||||||
flex: 1,
|
|
||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
marginTop: 5,
|
marginTop: 5,
|
||||||
|
flexShrink: 0,
|
||||||
},
|
},
|
||||||
container: {
|
rowItemContainer: {
|
||||||
flex: 1,
|
flex: 1,
|
||||||
},
|
},
|
||||||
gutter: {
|
gutter: {
|
||||||
|
|
@ -59,6 +60,7 @@ const Files = ({
|
||||||
location,
|
location,
|
||||||
postId,
|
postId,
|
||||||
postProps,
|
postProps,
|
||||||
|
isPermalinkPreview = false,
|
||||||
}: FilesProps) => {
|
}: FilesProps) => {
|
||||||
const galleryIdentifier = `${postId}-fileAttachments-${location}`;
|
const galleryIdentifier = `${postId}-fileAttachments-${location}`;
|
||||||
const [inViewPort, setInViewPort] = useState(false);
|
const [inViewPort, setInViewPort] = useState(false);
|
||||||
|
|
@ -84,9 +86,14 @@ const Files = ({
|
||||||
|
|
||||||
const isSingleImage = useMemo(() => filesInfo.filter((f) => isImage(f) || isVideo(f)).length === 1, [filesInfo]);
|
const isSingleImage = useMemo(() => filesInfo.filter((f) => isImage(f) || isVideo(f)).length === 1, [filesInfo]);
|
||||||
|
|
||||||
const renderItems = (items: FileInfo[], moreImagesCount = 0, includeGutter = false) => {
|
const renderItems = (items: FileInfo[], moreImagesCount = 0, includeGutter = false, isImageRow = false) => {
|
||||||
let nonVisibleImagesCount: number;
|
let nonVisibleImagesCount: number;
|
||||||
let container: StyleProp<ViewStyle> = items.length > 1 ? styles.container : undefined;
|
|
||||||
|
// Apply flex: 1 for multiple items, except in permalink previews where vertical
|
||||||
|
// document lists should not use flex to prevent shrinking under maxHeight constraints.
|
||||||
|
// Horizontal image rows (includeGutter=true) still need flex for space distribution.
|
||||||
|
const shouldApplyFlex = items.length > 1 && !(isPermalinkPreview && !includeGutter);
|
||||||
|
let container: StyleProp<ViewStyle> = shouldApplyFlex ? styles.rowItemContainer : undefined;
|
||||||
const containerWithGutter = [container, styles.gutter];
|
const containerWithGutter = [container, styles.gutter];
|
||||||
const wrapperWidth = getViewPortWidth(isReplyPost, isTablet) - 6;
|
const wrapperWidth = getViewPortWidth(isReplyPost, isTablet) - 6;
|
||||||
|
|
||||||
|
|
@ -98,9 +105,13 @@ const Files = ({
|
||||||
if (idx !== 0 && includeGutter) {
|
if (idx !== 0 && includeGutter) {
|
||||||
container = containerWithGutter;
|
container = containerWithGutter;
|
||||||
}
|
}
|
||||||
|
const shouldRemoveMarginTop = isPermalinkPreview && (
|
||||||
|
(isImageRow) || // Remove marginTop for all images in image row
|
||||||
|
(!isImageRow && idx === 0 && imageAttachments.length === 0) // Remove marginTop for first non-image only if no images present
|
||||||
|
);
|
||||||
return (
|
return (
|
||||||
<View
|
<View
|
||||||
style={[container, styles.marginTop]}
|
style={[container, styles.marginTop, shouldRemoveMarginTop && {marginTop: 0}]}
|
||||||
testID={`${file.id}-file-container`}
|
testID={`${file.id}-file-container`}
|
||||||
key={file.id}
|
key={file.id}
|
||||||
>
|
>
|
||||||
|
|
@ -138,10 +149,14 @@ const Files = ({
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View
|
<View
|
||||||
style={[styles.row, {width: portraitPostWidth}]}
|
style={[
|
||||||
|
styles.row,
|
||||||
|
{width: portraitPostWidth},
|
||||||
|
isPermalinkPreview && {marginTop: 0},
|
||||||
|
]}
|
||||||
testID='image-row'
|
testID='image-row'
|
||||||
>
|
>
|
||||||
{ renderItems(visibleImages, nonVisibleImagesCount, true) }
|
{ renderItems(visibleImages, nonVisibleImagesCount, true, true) }
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
@ -164,7 +179,10 @@ const Files = ({
|
||||||
<GalleryInit galleryIdentifier={galleryIdentifier}>
|
<GalleryInit galleryIdentifier={galleryIdentifier}>
|
||||||
<Animated.View
|
<Animated.View
|
||||||
testID='files-container'
|
testID='files-container'
|
||||||
style={failed ? styles.failed : undefined}
|
style={[
|
||||||
|
failed ? styles.failed : undefined,
|
||||||
|
isPermalinkPreview && {marginTop: 0},
|
||||||
|
]}
|
||||||
>
|
>
|
||||||
{renderImageRow()}
|
{renderImageRow()}
|
||||||
{renderItems(nonImageAttachments)}
|
{renderItems(nonImageAttachments)}
|
||||||
|
|
|
||||||
|
|
@ -116,6 +116,8 @@ const Content = ({isReplyPost, layoutWidth, location, post, theme, showPermalink
|
||||||
<PermalinkPreview
|
<PermalinkPreview
|
||||||
embedData={post.metadata!.embeds![0].data as PermalinkEmbedData}
|
embedData={post.metadata!.embeds![0].data as PermalinkEmbedData}
|
||||||
location={location}
|
location={location}
|
||||||
|
parentLocation={location}
|
||||||
|
parentPostId={post.id}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -24,19 +24,21 @@ const enhance = withObservables(['embedData'], ({database, embedData}: WithDatab
|
||||||
const userId = embedData?.post?.user_id;
|
const userId = embedData?.post?.user_id;
|
||||||
const author = userId ? observeUser(database, userId) : of$(undefined);
|
const author = userId ? observeUser(database, userId) : of$(undefined);
|
||||||
|
|
||||||
const isOriginPostDeleted = embedData?.post_id ? observePost(database, embedData.post_id).pipe(
|
const post = embedData?.post_id ? observePost(database, embedData.post_id) : of$(undefined);
|
||||||
|
const isOriginPostDeleted = post.pipe(
|
||||||
switchMap((p) => {
|
switchMap((p) => {
|
||||||
const initialDeleted = Boolean(embedData?.post?.delete_at > 0 || embedData?.post?.state === 'DELETED');
|
const initialDeleted = Boolean(embedData?.post?.delete_at > 0 || embedData?.post?.state === 'DELETED');
|
||||||
return of$(p ? p.deleteAt > 0 : initialDeleted);
|
return of$(p ? p.deleteAt > 0 : initialDeleted);
|
||||||
}),
|
}),
|
||||||
distinctUntilChanged(),
|
distinctUntilChanged(),
|
||||||
) : of$(false);
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
teammateNameDisplay,
|
teammateNameDisplay,
|
||||||
currentUser,
|
currentUser,
|
||||||
isMilitaryTime,
|
isMilitaryTime,
|
||||||
author,
|
author,
|
||||||
|
post,
|
||||||
isOriginPostDeleted,
|
isOriginPostDeleted,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,74 @@
|
||||||
|
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||||
|
// See LICENSE.txt for license information.
|
||||||
|
|
||||||
|
import React, {useEffect, useState, useCallback} from 'react';
|
||||||
|
import {DeviceEventEmitter, View, type LayoutChangeEvent, StyleSheet} from 'react-native';
|
||||||
|
|
||||||
|
import Files from '@components/files';
|
||||||
|
import {Events} from '@constants';
|
||||||
|
|
||||||
|
import type PostModel from '@typings/database/models/servers/post';
|
||||||
|
|
||||||
|
type PermalinkFilesProps = {
|
||||||
|
post: PostModel;
|
||||||
|
location: string;
|
||||||
|
isReplyPost: boolean;
|
||||||
|
failed?: boolean;
|
||||||
|
parentLocation?: string;
|
||||||
|
parentPostId?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const styles = StyleSheet.create({
|
||||||
|
container: {
|
||||||
|
marginBottom: 8,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const PermalinkFiles = (props: PermalinkFilesProps) => {
|
||||||
|
const {parentLocation, parentPostId, post, location, isReplyPost, failed} = props;
|
||||||
|
const [layoutWidth, setLayoutWidth] = useState(0);
|
||||||
|
|
||||||
|
const listener = useCallback((viewableItemsMap: Record<string, boolean>) => {
|
||||||
|
if (!parentLocation || !parentPostId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const parentKey = `${parentLocation}-${parentPostId}`;
|
||||||
|
if (viewableItemsMap[parentKey]) {
|
||||||
|
const viewableItems = {[`${location}-${post.id}`]: true};
|
||||||
|
DeviceEventEmitter.emit(Events.ITEM_IN_VIEWPORT, viewableItems);
|
||||||
|
}
|
||||||
|
}, [parentLocation, parentPostId, location, post.id]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!parentLocation || !parentPostId) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const subscription = DeviceEventEmitter.addListener(Events.ITEM_IN_VIEWPORT, listener);
|
||||||
|
return () => subscription.remove();
|
||||||
|
}, [listener, parentLocation, parentPostId]);
|
||||||
|
|
||||||
|
const onLayout = useCallback((event: LayoutChangeEvent) => {
|
||||||
|
setLayoutWidth(event.nativeEvent.layout.width);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View
|
||||||
|
onLayout={onLayout}
|
||||||
|
testID='permalink-files-container'
|
||||||
|
style={styles.container}
|
||||||
|
>
|
||||||
|
<Files
|
||||||
|
post={post}
|
||||||
|
location={location}
|
||||||
|
isReplyPost={isReplyPost}
|
||||||
|
failed={failed}
|
||||||
|
layoutWidth={layoutWidth}
|
||||||
|
isPermalinkPreview={true}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default PermalinkFiles;
|
||||||
|
|
@ -6,11 +6,15 @@ import React from 'react';
|
||||||
|
|
||||||
import Markdown from '@components/markdown';
|
import Markdown from '@components/markdown';
|
||||||
import {Screens} from '@constants';
|
import {Screens} from '@constants';
|
||||||
import {renderWithIntlAndTheme} from '@test/intl-test-helper';
|
import DatabaseManager from '@database/manager';
|
||||||
|
import {renderWithEverything} from '@test/intl-test-helper';
|
||||||
import TestHelper from '@test/test_helper';
|
import TestHelper from '@test/test_helper';
|
||||||
|
|
||||||
import PermalinkPreview from './permalink_preview';
|
import PermalinkPreview from './permalink_preview';
|
||||||
|
|
||||||
|
import type ServerDataOperator from '@database/operator/server_data_operator';
|
||||||
|
import type {Database} from '@nozbe/watermelondb';
|
||||||
|
|
||||||
jest.mock('@components/markdown', () => ({
|
jest.mock('@components/markdown', () => ({
|
||||||
__esModule: true,
|
__esModule: true,
|
||||||
default: jest.fn(),
|
default: jest.fn(),
|
||||||
|
|
@ -20,6 +24,45 @@ jest.mocked(Markdown).mockImplementation((props) =>
|
||||||
);
|
);
|
||||||
|
|
||||||
describe('components/post_list/post/body/content/permalink_preview/PermalinkPreview', () => {
|
describe('components/post_list/post/body/content/permalink_preview/PermalinkPreview', () => {
|
||||||
|
let database: Database;
|
||||||
|
let operator: ServerDataOperator;
|
||||||
|
const serverUrl = 'http://localhost:8065';
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
await DatabaseManager.init([serverUrl]);
|
||||||
|
const serverDatabaseAndOperator = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
|
||||||
|
database = serverDatabaseAndOperator.database;
|
||||||
|
operator = serverDatabaseAndOperator.operator;
|
||||||
|
|
||||||
|
// Setup basic configs
|
||||||
|
await operator.handleConfigs({
|
||||||
|
configs: [
|
||||||
|
{id: 'EnablePermalinkPreviews', value: 'true'},
|
||||||
|
{id: 'TeammateNameDisplay', value: 'username'},
|
||||||
|
{id: 'EnablePublicLink', value: 'true'},
|
||||||
|
{id: 'EnableSecureFilePreview', value: 'false'},
|
||||||
|
],
|
||||||
|
configsToDelete: [],
|
||||||
|
prepareRecordsOnly: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
// 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,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(async () => {
|
||||||
|
await DatabaseManager.destroyServerDatabase(serverUrl);
|
||||||
|
});
|
||||||
|
|
||||||
|
const renderPermalinkPreview = (props: Parameters<typeof PermalinkPreview>[0]) => {
|
||||||
|
return renderWithEverything(<PermalinkPreview {...props}/>, {database, serverUrl});
|
||||||
|
};
|
||||||
|
|
||||||
const baseProps: Parameters<typeof PermalinkPreview>[0] = {
|
const baseProps: Parameters<typeof PermalinkPreview>[0] = {
|
||||||
embedData: {
|
embedData: {
|
||||||
post_id: 'post-123',
|
post_id: 'post-123',
|
||||||
|
|
@ -45,16 +88,23 @@ describe('components/post_list/post/body/content/permalink_preview/PermalinkPrev
|
||||||
id: 'current-user',
|
id: 'current-user',
|
||||||
locale: 'en',
|
locale: 'en',
|
||||||
}),
|
}),
|
||||||
|
post: TestHelper.fakePostModel({
|
||||||
|
id: 'post-123',
|
||||||
|
userId: 'user-123',
|
||||||
|
message: 'This is a test message',
|
||||||
|
createAt: 1234567890000,
|
||||||
|
editAt: 0,
|
||||||
|
}),
|
||||||
isMilitaryTime: false,
|
isMilitaryTime: false,
|
||||||
teammateNameDisplay: 'username',
|
teammateNameDisplay: 'username',
|
||||||
location: Screens.CHANNEL,
|
location: Screens.CHANNEL,
|
||||||
isOriginPostDeleted: false,
|
isOriginPostDeleted: false,
|
||||||
|
parentLocation: Screens.CHANNEL,
|
||||||
|
parentPostId: 'parent-post-123',
|
||||||
};
|
};
|
||||||
|
|
||||||
it('should render permalink preview correctly', () => {
|
it('should render permalink preview correctly', () => {
|
||||||
const {getByText} = renderWithIntlAndTheme(
|
const {getByText} = renderPermalinkPreview(baseProps);
|
||||||
<PermalinkPreview {...baseProps}/>,
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(getByText('testuser')).toBeTruthy();
|
expect(getByText('testuser')).toBeTruthy();
|
||||||
expect(getByText('This is a test message')).toBeTruthy();
|
expect(getByText('This is a test message')).toBeTruthy();
|
||||||
|
|
@ -63,9 +113,7 @@ describe('components/post_list/post/body/content/permalink_preview/PermalinkPrev
|
||||||
|
|
||||||
it('should not render when origin post is deleted', () => {
|
it('should not render when origin post is deleted', () => {
|
||||||
const props = {...baseProps, isOriginPostDeleted: true};
|
const props = {...baseProps, isOriginPostDeleted: true};
|
||||||
const {queryByText} = renderWithIntlAndTheme(
|
const {queryByText} = renderPermalinkPreview(props);
|
||||||
<PermalinkPreview {...props}/>,
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(queryByText('testuser')).toBeNull();
|
expect(queryByText('testuser')).toBeNull();
|
||||||
expect(queryByText('This is a test message')).toBeNull();
|
expect(queryByText('This is a test message')).toBeNull();
|
||||||
|
|
@ -78,18 +126,15 @@ describe('components/post_list/post/body/content/permalink_preview/PermalinkPrev
|
||||||
...baseProps.embedData,
|
...baseProps.embedData,
|
||||||
post: undefined as unknown as Post,
|
post: undefined as unknown as Post,
|
||||||
},
|
},
|
||||||
|
post: undefined,
|
||||||
};
|
};
|
||||||
const {queryByText} = renderWithIntlAndTheme(
|
const {queryByText} = renderPermalinkPreview(props);
|
||||||
<PermalinkPreview {...props}/>,
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(queryByText('testuser')).toBeNull();
|
expect(queryByText('testuser')).toBeNull();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should handle press event without crashing', () => {
|
it('should handle press event without crashing', () => {
|
||||||
const {getByTestId} = renderWithIntlAndTheme(
|
const {getByTestId} = renderPermalinkPreview(baseProps);
|
||||||
<PermalinkPreview {...baseProps}/>,
|
|
||||||
);
|
|
||||||
const permalinkContainer = getByTestId('permalink-preview-container');
|
const permalinkContainer = getByTestId('permalink-preview-container');
|
||||||
expect(() => {
|
expect(() => {
|
||||||
fireEvent.press(permalinkContainer);
|
fireEvent.press(permalinkContainer);
|
||||||
|
|
@ -98,9 +143,7 @@ describe('components/post_list/post/body/content/permalink_preview/PermalinkPrev
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should display author name from user model', () => {
|
it('should display author name from user model', () => {
|
||||||
const {getByText} = renderWithIntlAndTheme(
|
const {getByText} = renderPermalinkPreview(baseProps);
|
||||||
<PermalinkPreview {...baseProps}/>,
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(getByText('testuser')).toBeTruthy();
|
expect(getByText('testuser')).toBeTruthy();
|
||||||
});
|
});
|
||||||
|
|
@ -118,17 +161,13 @@ describe('components/post_list/post/body/content/permalink_preview/PermalinkPrev
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
const {getByText} = renderWithIntlAndTheme(
|
const {getByText} = renderPermalinkPreview(props);
|
||||||
<PermalinkPreview {...props}/>,
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(getByText('Someone')).toBeTruthy();
|
expect(getByText('Someone')).toBeTruthy();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should display channel name with ~ prefix for public channels', () => {
|
it('should display channel name with ~ prefix for public channels', () => {
|
||||||
const {getByText} = renderWithIntlAndTheme(
|
const {getByText} = renderPermalinkPreview(baseProps);
|
||||||
<PermalinkPreview {...baseProps}/>,
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(getByText('Originally posted in ~Test Channel')).toBeTruthy();
|
expect(getByText('Originally posted in ~Test Channel')).toBeTruthy();
|
||||||
});
|
});
|
||||||
|
|
@ -142,15 +181,13 @@ describe('components/post_list/post/body/content/permalink_preview/PermalinkPrev
|
||||||
channel_display_name: 'testuser',
|
channel_display_name: 'testuser',
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
const {getByText} = renderWithIntlAndTheme(
|
const {getByText} = renderPermalinkPreview(props);
|
||||||
<PermalinkPreview {...props}/>,
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(getByText('Originally posted in ~testuser')).toBeTruthy();
|
expect(getByText('Originally posted in ~testuser')).toBeTruthy();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should truncate long messages', () => {
|
it('should truncate long messages', () => {
|
||||||
const longMessage = 'Line 1\nLine 2\nLine 3\nLine 4\nLine 5\nLine 6';
|
const longMessage = 'This is a very long message that should be truncated when it exceeds the maximum character limit of 150 characters for permalink previews in the mobile app.';
|
||||||
const props = {
|
const props = {
|
||||||
...baseProps,
|
...baseProps,
|
||||||
embedData: {
|
embedData: {
|
||||||
|
|
@ -162,11 +199,10 @@ describe('components/post_list/post/body/content/permalink_preview/PermalinkPrev
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
const {getByText} = renderWithIntlAndTheme(
|
const {getByText} = renderPermalinkPreview(props);
|
||||||
<PermalinkPreview {...props}/>,
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(getByText('Line 1\n...Line 2\n...Line 3\n...Line 4')).toBeTruthy();
|
const expectedTruncated = longMessage.substring(0, 150) + '...';
|
||||||
|
expect(getByText(expectedTruncated)).toBeTruthy();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should handle empty message', () => {
|
it('should handle empty message', () => {
|
||||||
|
|
@ -181,11 +217,8 @@ describe('components/post_list/post/body/content/permalink_preview/PermalinkPrev
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
const {queryByText} = renderWithIntlAndTheme(
|
const {queryByText} = renderPermalinkPreview(props);
|
||||||
<PermalinkPreview {...props}/>,
|
|
||||||
);
|
|
||||||
|
|
||||||
// Should render but with empty message
|
|
||||||
expect(queryByText('This is a test message')).toBeNull();
|
expect(queryByText('This is a test message')).toBeNull();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -203,37 +236,133 @@ describe('components/post_list/post/body/content/permalink_preview/PermalinkPrev
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
const {getByTestId} = renderWithIntlAndTheme(
|
const {getByTestId} = renderPermalinkPreview(props);
|
||||||
<PermalinkPreview {...props}/>,
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(getByTestId('permalink_preview.edited_indicator_separate')).toBeTruthy();
|
expect(getByTestId('permalink_preview.edited_indicator_separate')).toBeTruthy();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should not show edited indicator when post is not edited', () => {
|
it('should not show edited indicator when post is not edited', () => {
|
||||||
const {queryByTestId} = renderWithIntlAndTheme(
|
const {queryByTestId} = renderPermalinkPreview(baseProps);
|
||||||
<PermalinkPreview {...baseProps}/>,
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(queryByTestId('permalink_preview.edited_indicator_separate')).toBeNull();
|
expect(queryByTestId('permalink_preview.edited_indicator_separate')).toBeNull();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should display formatted time correctly', () => {
|
it('should display formatted time correctly', () => {
|
||||||
const {getByText} = renderWithIntlAndTheme(
|
const {getByText} = renderPermalinkPreview(baseProps);
|
||||||
<PermalinkPreview {...baseProps}/>,
|
|
||||||
);
|
|
||||||
|
|
||||||
// FormattedTime component should render the time text
|
|
||||||
expect(getByText('11:31 PM')).toBeTruthy();
|
expect(getByText('11:31 PM')).toBeTruthy();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should display profile picture', () => {
|
describe('file attachments', () => {
|
||||||
const {root} = renderWithIntlAndTheme(
|
it('should render PermalinkFiles component when post has file attachments', () => {
|
||||||
<PermalinkPreview {...baseProps}/>,
|
const fileInfo = TestHelper.fakeFileInfo({
|
||||||
);
|
id: 'file-123',
|
||||||
|
name: 'document.pdf',
|
||||||
|
mime_type: 'application/pdf',
|
||||||
|
size: 1048576,
|
||||||
|
});
|
||||||
|
|
||||||
// Profile picture should be rendered (check for the image component)
|
const propsWithFiles = {
|
||||||
const profilePicture = root.findByType('ViewManagerAdapter_ExpoImage');
|
...baseProps,
|
||||||
expect(profilePicture).toBeTruthy();
|
embedData: {
|
||||||
|
...baseProps.embedData,
|
||||||
|
post: TestHelper.fakePost({
|
||||||
|
id: 'post-123',
|
||||||
|
user_id: 'user-123',
|
||||||
|
message: 'Post with file attachment',
|
||||||
|
metadata: {
|
||||||
|
files: [fileInfo],
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const {getByText, getByTestId} = renderPermalinkPreview(propsWithFiles);
|
||||||
|
|
||||||
|
expect(getByText('Post with file attachment')).toBeTruthy();
|
||||||
|
|
||||||
|
expect(getByTestId('permalink-files-container')).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should render PermalinkFiles with multiple file attachments', () => {
|
||||||
|
const fileInfos = [
|
||||||
|
TestHelper.fakeFileInfo({
|
||||||
|
id: 'file-1',
|
||||||
|
name: 'image.jpg',
|
||||||
|
mime_type: 'image/jpeg',
|
||||||
|
size: 2048576,
|
||||||
|
}),
|
||||||
|
TestHelper.fakeFileInfo({
|
||||||
|
id: 'file-2',
|
||||||
|
name: 'document.pdf',
|
||||||
|
mime_type: 'application/pdf',
|
||||||
|
size: 1048576,
|
||||||
|
}),
|
||||||
|
];
|
||||||
|
|
||||||
|
const propsWithMultipleFiles = {
|
||||||
|
...baseProps,
|
||||||
|
embedData: {
|
||||||
|
...baseProps.embedData,
|
||||||
|
post: TestHelper.fakePost({
|
||||||
|
id: 'post-123',
|
||||||
|
user_id: 'user-123',
|
||||||
|
message: 'Post with multiple attachments',
|
||||||
|
metadata: {
|
||||||
|
files: fileInfos,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const {getByText, getByTestId} = renderPermalinkPreview(propsWithMultipleFiles);
|
||||||
|
|
||||||
|
expect(getByText('Post with multiple attachments')).toBeTruthy();
|
||||||
|
|
||||||
|
expect(getByTestId('permalink-files-container')).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not render PermalinkFiles when post has no file attachments', () => {
|
||||||
|
const propsWithoutFiles = {
|
||||||
|
...baseProps,
|
||||||
|
embedData: {
|
||||||
|
...baseProps.embedData,
|
||||||
|
post: TestHelper.fakePost({
|
||||||
|
id: 'post-123',
|
||||||
|
user_id: 'user-123',
|
||||||
|
message: 'Post without files',
|
||||||
|
metadata: {},
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const {getByText, queryByTestId} = renderPermalinkPreview(propsWithoutFiles);
|
||||||
|
|
||||||
|
expect(getByText('Post without files')).toBeTruthy();
|
||||||
|
|
||||||
|
expect(queryByTestId('permalink-files-container')).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not render PermalinkFiles when files array is empty', () => {
|
||||||
|
const propsWithEmptyFiles = {
|
||||||
|
...baseProps,
|
||||||
|
embedData: {
|
||||||
|
...baseProps.embedData,
|
||||||
|
post: TestHelper.fakePost({
|
||||||
|
id: 'post-123',
|
||||||
|
user_id: 'user-123',
|
||||||
|
message: 'Post with empty files',
|
||||||
|
metadata: {
|
||||||
|
files: [],
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const {getByText, queryByTestId} = renderPermalinkPreview(propsWithEmptyFiles);
|
||||||
|
|
||||||
|
expect(getByText('Post with empty files')).toBeTruthy();
|
||||||
|
|
||||||
|
expect(queryByTestId('permalink-files-container')).toBeNull();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,9 @@
|
||||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||||
// See LICENSE.txt for license information.
|
// See LICENSE.txt for license information.
|
||||||
|
|
||||||
import React, {useMemo, useCallback, useEffect} from 'react';
|
import {LinearGradient} from 'expo-linear-gradient';
|
||||||
import {Text, View, Pressable} from 'react-native';
|
import React, {useMemo, useCallback, useEffect, useState} from 'react';
|
||||||
|
import {Text, View, Pressable, type LayoutChangeEvent} from 'react-native';
|
||||||
|
|
||||||
import {fetchUsersByIds} from '@actions/remote/user';
|
import {fetchUsersByIds} from '@actions/remote/user';
|
||||||
import EditedIndicator from '@components/edited_indicator';
|
import EditedIndicator from '@components/edited_indicator';
|
||||||
|
|
@ -13,16 +14,20 @@ import ProfilePicture from '@components/profile_picture';
|
||||||
import {useServerUrl} from '@context/server';
|
import {useServerUrl} from '@context/server';
|
||||||
import {useTheme} from '@context/theme';
|
import {useTheme} from '@context/theme';
|
||||||
import {useUserLocale} from '@context/user_locale';
|
import {useUserLocale} from '@context/user_locale';
|
||||||
|
import {useWindowDimensions} from '@hooks/device';
|
||||||
import {usePreventDoubleTap} from '@hooks/utils';
|
import {usePreventDoubleTap} from '@hooks/utils';
|
||||||
import {getMarkdownTextStyles, getMarkdownBlockStyles} from '@utils/markdown';
|
import {getMarkdownTextStyles, getMarkdownBlockStyles} from '@utils/markdown';
|
||||||
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
|
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
|
||||||
import {typography} from '@utils/typography';
|
import {typography} from '@utils/typography';
|
||||||
import {displayUsername, getUserTimezone} from '@utils/user';
|
import {displayUsername, getUserTimezone} from '@utils/user';
|
||||||
|
|
||||||
|
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 UserModel from '@typings/database/models/servers/user';
|
||||||
import type {AvailableScreens} from '@typings/screens/navigation';
|
import type {AvailableScreens} from '@typings/screens/navigation';
|
||||||
|
|
||||||
const MAX_PERMALINK_PREVIEW_LINES = 4;
|
const MAX_PERMALINK_PREVIEW_CHARACTERS = 150;
|
||||||
const EDITED_INDICATOR_CONTEXT = ['paragraph'];
|
const EDITED_INDICATOR_CONTEXT = ['paragraph'];
|
||||||
|
|
||||||
type PermalinkPreviewProps = {
|
type PermalinkPreviewProps = {
|
||||||
|
|
@ -31,8 +36,11 @@ type PermalinkPreviewProps = {
|
||||||
currentUser?: UserModel;
|
currentUser?: UserModel;
|
||||||
isMilitaryTime: boolean;
|
isMilitaryTime: boolean;
|
||||||
teammateNameDisplay: string;
|
teammateNameDisplay: string;
|
||||||
|
post?: PostModel;
|
||||||
isOriginPostDeleted?: boolean;
|
isOriginPostDeleted?: boolean;
|
||||||
location: AvailableScreens;
|
location: AvailableScreens;
|
||||||
|
parentLocation?: string;
|
||||||
|
parentPostId?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => {
|
const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => {
|
||||||
|
|
@ -51,7 +59,8 @@ const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => {
|
||||||
},
|
},
|
||||||
shadowOpacity: 0.08,
|
shadowOpacity: 0.08,
|
||||||
shadowRadius: 3,
|
shadowRadius: 3,
|
||||||
elevation: 2,
|
elevation: 1,
|
||||||
|
overflow: 'hidden',
|
||||||
},
|
},
|
||||||
header: {
|
header: {
|
||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
|
|
@ -75,8 +84,6 @@ const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => {
|
||||||
},
|
},
|
||||||
messageContainer: {
|
messageContainer: {
|
||||||
marginBottom: 8,
|
marginBottom: 8,
|
||||||
maxHeight: 20 * MAX_PERMALINK_PREVIEW_LINES,
|
|
||||||
overflow: 'hidden',
|
|
||||||
},
|
},
|
||||||
messageText: {
|
messageText: {
|
||||||
color: theme.centerChannelColor,
|
color: theme.centerChannelColor,
|
||||||
|
|
@ -85,6 +92,7 @@ const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => {
|
||||||
},
|
},
|
||||||
|
|
||||||
channelContext: {
|
channelContext: {
|
||||||
|
marginTop: 8,
|
||||||
color: changeOpacity(theme.centerChannelColor, 0.64),
|
color: changeOpacity(theme.centerChannelColor, 0.64),
|
||||||
...typography('Body', 75),
|
...typography('Body', 75),
|
||||||
},
|
},
|
||||||
|
|
@ -92,6 +100,17 @@ const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => {
|
||||||
color: theme.linkColor,
|
color: theme.linkColor,
|
||||||
...typography('Body', 75, 'SemiBold'),
|
...typography('Body', 75, 'SemiBold'),
|
||||||
},
|
},
|
||||||
|
contentContainer: {
|
||||||
|
overflow: 'hidden',
|
||||||
|
position: 'relative',
|
||||||
|
},
|
||||||
|
gradientOverlay: {
|
||||||
|
position: 'absolute',
|
||||||
|
bottom: 0,
|
||||||
|
left: 0,
|
||||||
|
right: 0,
|
||||||
|
height: 60,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -101,14 +120,20 @@ const PermalinkPreview = ({
|
||||||
currentUser,
|
currentUser,
|
||||||
isMilitaryTime,
|
isMilitaryTime,
|
||||||
teammateNameDisplay,
|
teammateNameDisplay,
|
||||||
|
post,
|
||||||
isOriginPostDeleted,
|
isOriginPostDeleted,
|
||||||
location,
|
location,
|
||||||
|
parentLocation,
|
||||||
|
parentPostId,
|
||||||
}: PermalinkPreviewProps) => {
|
}: PermalinkPreviewProps) => {
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
const serverUrl = useServerUrl();
|
const serverUrl = useServerUrl();
|
||||||
const locale = useUserLocale();
|
const locale = useUserLocale();
|
||||||
|
const dimensions = useWindowDimensions();
|
||||||
const styles = getStyleSheet(theme);
|
const styles = getStyleSheet(theme);
|
||||||
|
const [showGradient, setShowGradient] = useState(false);
|
||||||
|
|
||||||
|
const maxPermalinkHeight = Math.round(dimensions.height * 0.5);
|
||||||
const textStyles = getMarkdownTextStyles(theme);
|
const textStyles = getMarkdownTextStyles(theme);
|
||||||
const blockStyles = getMarkdownBlockStyles(theme);
|
const blockStyles = getMarkdownBlockStyles(theme);
|
||||||
|
|
||||||
|
|
@ -125,28 +150,27 @@ const PermalinkPreview = ({
|
||||||
}
|
}
|
||||||
|
|
||||||
const {
|
const {
|
||||||
post,
|
post: embedPost,
|
||||||
channel_display_name,
|
channel_display_name,
|
||||||
channel_type,
|
channel_type,
|
||||||
} = embedData;
|
} = embedData;
|
||||||
|
|
||||||
const truncatedMessage = useMemo(() => {
|
const truncatedMessage = useMemo(() => {
|
||||||
const message = post?.message;
|
const msg = embedPost?.message;
|
||||||
|
if (!msg || typeof msg !== 'string') {
|
||||||
if (!message || typeof message !== 'string') {
|
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
|
|
||||||
const cleanMessage = message.trim();
|
const cleanMessage = msg.trim();
|
||||||
const lines = cleanMessage.split('\n');
|
|
||||||
if (lines.length > MAX_PERMALINK_PREVIEW_LINES) {
|
if (cleanMessage.length > MAX_PERMALINK_PREVIEW_CHARACTERS) {
|
||||||
return lines.slice(0, MAX_PERMALINK_PREVIEW_LINES).join('\n...');
|
return cleanMessage.substring(0, MAX_PERMALINK_PREVIEW_CHARACTERS) + '...';
|
||||||
}
|
}
|
||||||
|
|
||||||
return cleanMessage;
|
return cleanMessage;
|
||||||
}, [post?.message]);
|
}, [embedPost?.message]);
|
||||||
|
|
||||||
const isEdited = useMemo(() => post && post.edit_at > 0, [post]);
|
const isEdited = useMemo(() => embedData && embedData.post && embedData.post.edit_at > 0, [embedData]);
|
||||||
|
|
||||||
const authorDisplayName = useMemo(() => {
|
const authorDisplayName = useMemo(() => {
|
||||||
return displayUsername(author, locale, teammateNameDisplay);
|
return displayUsername(author, locale, teammateNameDisplay);
|
||||||
|
|
@ -157,10 +181,21 @@ const PermalinkPreview = ({
|
||||||
return `~${displayName}`;
|
return `~${displayName}`;
|
||||||
}, [channel_display_name, channel_type, authorDisplayName]);
|
}, [channel_display_name, channel_type, authorDisplayName]);
|
||||||
|
|
||||||
|
const filesInfo = useMemo(() => {
|
||||||
|
return embedData?.post?.metadata?.files || [];
|
||||||
|
}, [embedData?.post?.metadata?.files]);
|
||||||
|
|
||||||
|
const hasFiles = filesInfo.length > 0;
|
||||||
|
|
||||||
const handlePress = usePreventDoubleTap(useCallback(() => {
|
const handlePress = usePreventDoubleTap(useCallback(() => {
|
||||||
// Navigation will be implemented in Task 5
|
// Navigation will be implemented in Task 5
|
||||||
}, []));
|
}, []));
|
||||||
|
|
||||||
|
const handleContentLayout = useCallback((event: LayoutChangeEvent) => {
|
||||||
|
const {height} = event.nativeEvent.layout;
|
||||||
|
setShowGradient(height >= maxPermalinkHeight);
|
||||||
|
}, [maxPermalinkHeight]);
|
||||||
|
|
||||||
if (!post) {
|
if (!post) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
@ -174,48 +209,70 @@ const PermalinkPreview = ({
|
||||||
onPress={handlePress}
|
onPress={handlePress}
|
||||||
testID='permalink-preview-container'
|
testID='permalink-preview-container'
|
||||||
>
|
>
|
||||||
<View style={styles.header}>
|
<View style={[styles.contentContainer, {maxHeight: maxPermalinkHeight}]}>
|
||||||
<ProfilePicture
|
<View onLayout={handleContentLayout}>
|
||||||
author={author || {id: post.user_id} as UserModel}
|
<View style={styles.header}>
|
||||||
size={32}
|
<ProfilePicture
|
||||||
showStatus={false}
|
author={author}
|
||||||
/>
|
size={32}
|
||||||
<View style={styles.authorInfo}>
|
showStatus={false}
|
||||||
<Text
|
/>
|
||||||
style={styles.authorName}
|
<View style={styles.authorInfo}>
|
||||||
numberOfLines={1}
|
<Text
|
||||||
>
|
style={styles.authorName}
|
||||||
{authorDisplayName}
|
numberOfLines={1}
|
||||||
</Text>
|
>
|
||||||
<FormattedTime
|
{authorDisplayName}
|
||||||
timezone={getUserTimezone(currentUser)}
|
</Text>
|
||||||
isMilitaryTime={isMilitaryTime}
|
<FormattedTime
|
||||||
value={post.create_at}
|
timezone={getUserTimezone(currentUser)}
|
||||||
style={styles.timestamp}
|
isMilitaryTime={isMilitaryTime}
|
||||||
/>
|
value={embedPost.create_at}
|
||||||
</View>
|
style={styles.timestamp}
|
||||||
</View>
|
/>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
|
||||||
<View style={styles.messageContainer}>
|
<View style={styles.messageContainer}>
|
||||||
<Markdown
|
<Markdown
|
||||||
baseTextStyle={styles.messageText}
|
baseTextStyle={styles.messageText}
|
||||||
blockStyles={blockStyles}
|
blockStyles={blockStyles}
|
||||||
channelId={embedData.channel_id}
|
channelId={embedData.channel_id}
|
||||||
disableAtMentions={true}
|
disableAtMentions={true}
|
||||||
location={location}
|
location={location}
|
||||||
theme={theme}
|
theme={theme}
|
||||||
textStyles={textStyles}
|
textStyles={textStyles}
|
||||||
value={truncatedMessage}
|
value={truncatedMessage}
|
||||||
/>
|
/>
|
||||||
{isEdited ? (
|
{isEdited ? (
|
||||||
<EditedIndicator
|
<EditedIndicator
|
||||||
baseTextStyle={styles.messageText}
|
baseTextStyle={styles.messageText}
|
||||||
theme={theme}
|
theme={theme}
|
||||||
context={EDITED_INDICATOR_CONTEXT}
|
context={EDITED_INDICATOR_CONTEXT}
|
||||||
iconSize={12}
|
iconSize={12}
|
||||||
testID='permalink_preview.edited_indicator_separate'
|
testID='permalink_preview.edited_indicator_separate'
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{hasFiles && post && (
|
||||||
|
<PermalinkFiles
|
||||||
|
post={post}
|
||||||
|
location='permalink_preview'
|
||||||
|
isReplyPost={false}
|
||||||
|
parentLocation={parentLocation}
|
||||||
|
parentPostId={parentPostId}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{showGradient && (
|
||||||
|
<LinearGradient
|
||||||
|
colors={[changeOpacity(theme.centerChannelBg, 0), theme.centerChannelBg]}
|
||||||
|
style={styles.gradientOverlay}
|
||||||
|
pointerEvents='none'
|
||||||
/>
|
/>
|
||||||
) : null}
|
)}
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
<Text style={styles.channelContext}>
|
<Text style={styles.channelContext}>
|
||||||
|
|
|
||||||
|
|
@ -187,6 +187,115 @@ describe('Permalink Sync Utils', () => {
|
||||||
expect(result).toBeNull();
|
expect(result).toBeNull();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should update permalink metadata including file metadata when fresh post is newer', async () => {
|
||||||
|
const referencedPostId = 'referenced_post_with_files';
|
||||||
|
|
||||||
|
const oldFileMetadata: FileInfo[] = [
|
||||||
|
{
|
||||||
|
id: 'file1',
|
||||||
|
name: 'old_file.txt',
|
||||||
|
extension: 'txt',
|
||||||
|
size: 100,
|
||||||
|
mime_type: 'text/plain',
|
||||||
|
width: 0,
|
||||||
|
height: 0,
|
||||||
|
has_preview_image: false,
|
||||||
|
user_id: 'user_123',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const newFileMetadata: FileInfo[] = [
|
||||||
|
{
|
||||||
|
id: 'file1',
|
||||||
|
name: 'old_file.txt',
|
||||||
|
extension: 'txt',
|
||||||
|
size: 100,
|
||||||
|
mime_type: 'text/plain',
|
||||||
|
width: 0,
|
||||||
|
height: 0,
|
||||||
|
has_preview_image: false,
|
||||||
|
user_id: 'user_123',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'file2',
|
||||||
|
name: 'new_file.jpg',
|
||||||
|
extension: 'jpg',
|
||||||
|
size: 2000,
|
||||||
|
mime_type: 'image/jpeg',
|
||||||
|
width: 1920,
|
||||||
|
height: 1080,
|
||||||
|
has_preview_image: true,
|
||||||
|
user_id: 'user_123',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const referencingPost = TestHelper.fakePost({
|
||||||
|
id: 'referencing_post_files',
|
||||||
|
channel_id: 'channel1',
|
||||||
|
metadata: {
|
||||||
|
embeds: [
|
||||||
|
{
|
||||||
|
type: 'permalink',
|
||||||
|
url: '',
|
||||||
|
data: {
|
||||||
|
post_id: referencedPostId,
|
||||||
|
post: {
|
||||||
|
id: referencedPostId,
|
||||||
|
message: 'Post with old files',
|
||||||
|
edit_at: 1000,
|
||||||
|
update_at: 1000,
|
||||||
|
user_id: 'user_123',
|
||||||
|
create_at: 500,
|
||||||
|
metadata: {
|
||||||
|
files: oldFileMetadata,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const models = await operator.handlePosts({
|
||||||
|
actionType: ActionType.POSTS.RECEIVED_NEW,
|
||||||
|
order: [referencingPost.id],
|
||||||
|
posts: [referencingPost],
|
||||||
|
prepareRecordsOnly: true,
|
||||||
|
});
|
||||||
|
await operator.batchRecords(models, 'test');
|
||||||
|
|
||||||
|
const postModel = await database.get('Post').find(referencingPost.id) as PostModel;
|
||||||
|
|
||||||
|
const freshPostData = TestHelper.fakePost({
|
||||||
|
id: referencedPostId,
|
||||||
|
message: 'Post with updated files',
|
||||||
|
edit_at: 2000,
|
||||||
|
update_at: 2000,
|
||||||
|
user_id: 'user_123',
|
||||||
|
create_at: 500,
|
||||||
|
metadata: {
|
||||||
|
files: newFileMetadata,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = updatePermalinkMetadata(postModel, referencedPostId, freshPostData);
|
||||||
|
|
||||||
|
expect(result).not.toBeNull();
|
||||||
|
expect(result!.id).toBe(referencingPost.id);
|
||||||
|
expect(result!._preparedState).toBe('update');
|
||||||
|
|
||||||
|
// Verify the metadata changes
|
||||||
|
expect(result!.metadata!.embeds![0].data.post.message).toBe('Post with updated files');
|
||||||
|
expect(result!.metadata!.embeds![0].data.post.edit_at).toBe(2000);
|
||||||
|
expect(result!.metadata!.embeds![0].data.post.metadata).toEqual({
|
||||||
|
files: newFileMetadata,
|
||||||
|
});
|
||||||
|
expect(result!.metadata!.embeds![0].data.post.metadata!.files).toHaveLength(2);
|
||||||
|
expect(result!.metadata!.embeds![0].data.post.metadata!.files![1].name).toBe('new_file.jpg');
|
||||||
|
|
||||||
|
await operator.batchRecords([result!], 'test file metadata update');
|
||||||
|
});
|
||||||
|
|
||||||
it('should handle errors during post update gracefully', async () => {
|
it('should handle errors during post update gracefully', async () => {
|
||||||
const referencedPostId = 'referenced_post_456';
|
const referencedPostId = 'referenced_post_456';
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -39,6 +39,8 @@ export function updatePermalinkMetadata(
|
||||||
update_at: freshPostData.update_at,
|
update_at: freshPostData.update_at,
|
||||||
user_id: freshPostData.user_id,
|
user_id: freshPostData.user_id,
|
||||||
create_at: freshPostData.create_at,
|
create_at: freshPostData.create_at,
|
||||||
|
file_ids: freshPostData.file_ids,
|
||||||
|
metadata: freshPostData.metadata,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue