Viewing Files in Edit mode in mobile with ability to delete and save (#8918)
* Viewing Files in Edit mode in mobile with ability to delete and save * Added upload attachment to keyboard tracker view * using state instread of use ref * Minor * Added tests * intl extract * new function getFiles ById, batch file deletion and tests * Files fetching in edit options and tests * Removed DeviceEventEmitter and used React context * Added support to check minimum required version to show edit file attachments * resolve forward ref issue * Minor * memotized props for context and observe config with value * Import fixes
This commit is contained in:
parent
a207be38ce
commit
8e1af4298d
20 changed files with 700 additions and 144 deletions
|
|
@ -411,7 +411,7 @@ describe('create, update & delete posts', () => {
|
|||
|
||||
it('editPost - handle error', async () => {
|
||||
mockClient.deletePost.mockImplementationOnce(jest.fn(throwFunc));
|
||||
const result = await editPost('foo', '', '');
|
||||
const result = await editPost('foo', '', '', [], []);
|
||||
expect(result).toBeDefined();
|
||||
expect(result.error).toBeTruthy();
|
||||
});
|
||||
|
|
@ -424,12 +424,84 @@ describe('create, update & delete posts', () => {
|
|||
prepareRecordsOnly: false,
|
||||
});
|
||||
|
||||
const result = await editPost(serverUrl, post1.id, 'new message');
|
||||
const result = await editPost(serverUrl, post1.id, 'new message', [], []);
|
||||
expect(result).toBeDefined();
|
||||
expect(result.error).toBeUndefined();
|
||||
expect(result.post).toBeDefined();
|
||||
});
|
||||
|
||||
it('editPost - delete files', async () => {
|
||||
await operator.handlePosts({
|
||||
actionType: ActionType.POSTS.RECEIVED_IN_CHANNEL,
|
||||
order: [post1.id],
|
||||
posts: [post1],
|
||||
prepareRecordsOnly: false,
|
||||
});
|
||||
|
||||
const testFiles: FileInfo[] = [
|
||||
TestHelper.fakeFileInfo({
|
||||
id: 'file-1',
|
||||
post_id: post1.id,
|
||||
name: 'test-file-1.jpg',
|
||||
extension: 'jpg',
|
||||
size: 1024,
|
||||
mime_type: 'image/jpeg',
|
||||
user_id: user1.id,
|
||||
}),
|
||||
TestHelper.fakeFileInfo({
|
||||
id: 'file-2',
|
||||
post_id: post1.id,
|
||||
name: 'test-file-2.png',
|
||||
extension: 'png',
|
||||
size: 2048,
|
||||
mime_type: 'image/png',
|
||||
user_id: user1.id,
|
||||
}),
|
||||
TestHelper.fakeFileInfo({
|
||||
id: 'file-3',
|
||||
post_id: post1.id,
|
||||
name: 'test-file-3.pdf',
|
||||
extension: 'pdf',
|
||||
size: 4096,
|
||||
mime_type: 'application/pdf',
|
||||
user_id: user1.id,
|
||||
}),
|
||||
];
|
||||
|
||||
await operator.handleFiles({
|
||||
files: testFiles,
|
||||
prepareRecordsOnly: false,
|
||||
});
|
||||
|
||||
const {database} = operator;
|
||||
const filesBefore = await database.get('File').query().fetch();
|
||||
expect(filesBefore).toHaveLength(3);
|
||||
|
||||
const file1Before = filesBefore.find((f) => f.id === 'file-1');
|
||||
const file2Before = filesBefore.find((f) => f.id === 'file-2');
|
||||
const file3Before = filesBefore.find((f) => f.id === 'file-3');
|
||||
expect(file1Before).toBeDefined();
|
||||
expect(file2Before).toBeDefined();
|
||||
expect(file3Before).toBeDefined();
|
||||
|
||||
const result = await editPost(serverUrl, post1.id, 'new message', [], ['file-1', 'file-2']);
|
||||
expect(result).toBeDefined();
|
||||
expect(result.error).toBeUndefined();
|
||||
expect(result.post).toBeDefined();
|
||||
|
||||
const filesAfter = await database.get('File').query().fetch();
|
||||
expect(filesAfter).toHaveLength(1);
|
||||
|
||||
const remainingFile = filesAfter[0];
|
||||
expect(remainingFile.id).toBe('file-3');
|
||||
|
||||
const deletedFiles = await database.get('File').query().fetch();
|
||||
const file1After = deletedFiles.find((f) => f.id === 'file-1');
|
||||
const file2After = deletedFiles.find((f) => f.id === 'file-2');
|
||||
expect(file1After).toBeUndefined();
|
||||
expect(file2After).toBeUndefined();
|
||||
});
|
||||
|
||||
it('acknowledgePost - handle error', async () => {
|
||||
mockClient.deletePost.mockImplementationOnce(jest.fn(throwFunc));
|
||||
const result = await acknowledgePost('foo', '');
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ import {getNeededAtMentionedUsernames} from '@helpers/api/user';
|
|||
import NetworkManager from '@managers/network_manager';
|
||||
import {getMyChannel, prepareMissingChannelsForAllTeams, queryAllMyChannel} from '@queries/servers/channel';
|
||||
import {queryAllCustomEmojis} from '@queries/servers/custom_emoji';
|
||||
import {getFilesByIds} from '@queries/servers/file';
|
||||
import {getPostById, getRecentPostsInChannel} from '@queries/servers/post';
|
||||
import {getCurrentUserId} from '@queries/servers/system';
|
||||
import {getIsCRTEnabled, prepareThreadsFromReceivedPosts} from '@queries/servers/thread';
|
||||
|
|
@ -907,14 +908,14 @@ export const markPostAsUnread = async (serverUrl: string, postId: string) => {
|
|||
}
|
||||
};
|
||||
|
||||
export const editPost = async (serverUrl: string, postId: string, postMessage: string) => {
|
||||
export const editPost = async (serverUrl: string, postId: string, postMessage: string, file_ids: string[], removed_file_ids: string[]) => {
|
||||
try {
|
||||
const client = NetworkManager.getClient(serverUrl);
|
||||
const {database} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
|
||||
const {database, operator} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
|
||||
|
||||
const post = await getPostById(database, postId);
|
||||
if (post) {
|
||||
const {update_at, edit_at, message: updatedMessage, message_source} = await client.patchPost({message: postMessage, id: postId});
|
||||
const {update_at, edit_at, message: updatedMessage, message_source} = await client.patchPost({message: postMessage, id: postId, file_ids});
|
||||
await database.write(async () => {
|
||||
await post.update((p) => {
|
||||
p.updateAt = update_at;
|
||||
|
|
@ -923,6 +924,17 @@ export const editPost = async (serverUrl: string, postId: string, postMessage: s
|
|||
p.messageSource = message_source || '';
|
||||
});
|
||||
});
|
||||
|
||||
if (removed_file_ids.length > 0) {
|
||||
const filesToDelete = await getFilesByIds(database, removed_file_ids);
|
||||
if (filesToDelete.length > 0) {
|
||||
const fileDeleteModels = filesToDelete.map((file) => {
|
||||
file.prepareDestroyPermanently();
|
||||
return file;
|
||||
});
|
||||
await operator.batchRecords(fileDeleteModels, 'delete files');
|
||||
}
|
||||
}
|
||||
}
|
||||
return {post};
|
||||
} catch (error) {
|
||||
|
|
|
|||
|
|
@ -8,34 +8,17 @@ import {switchMap} from 'rxjs/operators';
|
|||
import {queryFilesForPost} from '@queries/servers/file';
|
||||
import {observeCanDownloadFiles, observeEnableSecureFilePreview} from '@queries/servers/security';
|
||||
import {observeConfigBooleanValue} from '@queries/servers/system';
|
||||
import {fileExists} from '@utils/file';
|
||||
import {filesLocalPathValidation} from '@utils/file';
|
||||
|
||||
import Files from './files';
|
||||
|
||||
import type {WithDatabaseArgs} from '@typings/database/database';
|
||||
import type FileModel from '@typings/database/models/servers/file';
|
||||
import type PostModel from '@typings/database/models/servers/post';
|
||||
|
||||
type EnhanceProps = WithDatabaseArgs & {
|
||||
post: PostModel;
|
||||
}
|
||||
|
||||
const filesLocalPathValidation = async (files: FileModel[], authorId: string) => {
|
||||
const filesInfo: FileInfo[] = [];
|
||||
for await (const f of files) {
|
||||
const info = f.toFileInfo(authorId);
|
||||
if (info.localPath) {
|
||||
const exists = await fileExists(info.localPath);
|
||||
if (!exists) {
|
||||
info.localPath = '';
|
||||
}
|
||||
}
|
||||
filesInfo.push(info);
|
||||
}
|
||||
|
||||
return filesInfo;
|
||||
};
|
||||
|
||||
const enhance = withObservables(['post'], ({database, post}: EnhanceProps) => {
|
||||
const publicLinkEnabled = observeConfigBooleanValue(database, 'EnablePublicLink');
|
||||
|
||||
|
|
|
|||
|
|
@ -242,6 +242,7 @@ function DraftInput({
|
|||
uploadFileError={uploadFileError}
|
||||
channelId={channelId}
|
||||
rootId={rootId}
|
||||
isEditMode={false}
|
||||
/>
|
||||
<View style={style.actionsContainer}>
|
||||
<QuickActions
|
||||
|
|
|
|||
|
|
@ -29,6 +29,7 @@ type Props = {
|
|||
uploadFileError: React.ReactNode;
|
||||
channelId: string;
|
||||
rootId: string;
|
||||
isEditMode: boolean;
|
||||
}
|
||||
|
||||
const getStyleSheet = makeStyleSheetFromTheme((theme) => {
|
||||
|
|
@ -74,6 +75,7 @@ function Uploads({
|
|||
uploadFileError,
|
||||
channelId,
|
||||
rootId,
|
||||
isEditMode,
|
||||
}: Props) {
|
||||
const galleryIdentifier = `${channelId}-uploadedItems-${rootId}`;
|
||||
const theme = useTheme();
|
||||
|
|
@ -134,9 +136,10 @@ function Uploads({
|
|||
galleryIdentifier={galleryIdentifier}
|
||||
index={index}
|
||||
file={file}
|
||||
key={file.clientId}
|
||||
key={file.clientId || file.id}
|
||||
openGallery={openGallery}
|
||||
rootId={rootId}
|
||||
isEditMode={isEditMode}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -27,6 +27,7 @@ type Props = {
|
|||
file: FileInfo;
|
||||
openGallery: (file: FileInfo) => void;
|
||||
rootId: string;
|
||||
isEditMode: boolean;
|
||||
}
|
||||
|
||||
const style = StyleSheet.create({
|
||||
|
|
@ -61,7 +62,7 @@ const style = StyleSheet.create({
|
|||
|
||||
export default function UploadItem({
|
||||
channelId, galleryIdentifier, index, file,
|
||||
rootId, openGallery,
|
||||
rootId, openGallery, isEditMode,
|
||||
}: Props) {
|
||||
const theme = useTheme();
|
||||
const serverUrl = useServerUrl();
|
||||
|
|
@ -124,6 +125,7 @@ export default function UploadItem({
|
|||
backgroundColor={changeOpacity(theme.centerChannelColor, 0.08)}
|
||||
iconSize={60}
|
||||
file={file}
|
||||
testID={file.id}
|
||||
/>
|
||||
);
|
||||
}, [file, ref, theme.centerChannelColor]);
|
||||
|
|
@ -158,6 +160,8 @@ export default function UploadItem({
|
|||
clientId={file.clientId!}
|
||||
channelId={channelId}
|
||||
rootId={rootId}
|
||||
isEditMode={isEditMode}
|
||||
fileId={file.id!}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import {View, Platform} from 'react-native';
|
|||
import {removeDraftFile} from '@actions/local/draft';
|
||||
import CompassIcon from '@components/compass_icon';
|
||||
import TouchableWithFeedback from '@components/touchable_with_feedback';
|
||||
import {useEditPost} from '@context/edit_post';
|
||||
import {useServerUrl} from '@context/server';
|
||||
import {useTheme} from '@context/theme';
|
||||
import DraftUploadManager from '@managers/draft_upload_manager';
|
||||
|
|
@ -16,6 +17,8 @@ type Props = {
|
|||
channelId: string;
|
||||
rootId: string;
|
||||
clientId: string;
|
||||
isEditMode: boolean;
|
||||
fileId: string;
|
||||
}
|
||||
|
||||
const getStyleSheet = makeStyleSheetFromTheme((theme) => {
|
||||
|
|
@ -46,12 +49,19 @@ export default function UploadRemove({
|
|||
channelId,
|
||||
rootId,
|
||||
clientId,
|
||||
isEditMode,
|
||||
fileId,
|
||||
}: Props) {
|
||||
const theme = useTheme();
|
||||
const style = getStyleSheet(theme);
|
||||
const serverUrl = useServerUrl();
|
||||
const {onFileRemove} = useEditPost();
|
||||
|
||||
const onPress = () => {
|
||||
if (isEditMode) {
|
||||
onFileRemove?.(fileId);
|
||||
return;
|
||||
}
|
||||
DraftUploadManager.cancel(clientId);
|
||||
removeDraftFile(serverUrl, channelId, rootId, clientId);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -34,4 +34,5 @@ export default keyMirror({
|
|||
JOIN_CALL_BAR_VISIBLE: null,
|
||||
DRAFT_SWIPEABLE: null,
|
||||
ACTIVE_SCREEN: null,
|
||||
FILE_ADD_REMOVED: null,
|
||||
});
|
||||
|
|
|
|||
31
app/context/edit_post/index.tsx
Normal file
31
app/context/edit_post/index.tsx
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React, {createContext, useContext, useMemo, type ReactNode} from 'react';
|
||||
|
||||
type EditPostContextType = {
|
||||
onFileRemove?: (fileId: string) => void;
|
||||
};
|
||||
|
||||
const EditPostContext = createContext<EditPostContextType>({});
|
||||
|
||||
type EditPostProviderProps = {
|
||||
children: ReactNode;
|
||||
onFileRemove?: (fileId: string) => void;
|
||||
};
|
||||
|
||||
export const EditPostProvider = ({children, onFileRemove}: EditPostProviderProps) => {
|
||||
const contextValue = useMemo(() => ({
|
||||
onFileRemove,
|
||||
}), [onFileRemove]);
|
||||
|
||||
return (
|
||||
<EditPostContext.Provider value={contextValue}>
|
||||
{children}
|
||||
</EditPostContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
export const useEditPost = () => {
|
||||
return useContext(EditPostContext);
|
||||
};
|
||||
|
|
@ -35,3 +35,18 @@ export const queryFilesForPost = (database: Database, postId: string) => {
|
|||
export const observeFilesForPost = (database: Database, postId: string) => {
|
||||
return queryFilesForPost(database, postId).observe();
|
||||
};
|
||||
|
||||
export const getFilesByIds = async (database: Database, fileIds: string[]) => {
|
||||
if (!fileIds.length) {
|
||||
return [];
|
||||
}
|
||||
|
||||
try {
|
||||
const records = await database.get<FileModel>(FILE).query(
|
||||
Q.where('id', Q.oneOf(fileIds)),
|
||||
).fetch();
|
||||
return records;
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
};
|
||||
|
|
|
|||
84
app/screens/edit_post/edit_post.test.tsx
Normal file
84
app/screens/edit_post/edit_post.test.tsx
Normal file
|
|
@ -0,0 +1,84 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import {renderWithEverything} from '@test/intl-test-helper';
|
||||
import TestHelper from '@test/test_helper';
|
||||
|
||||
import EditPost from './edit_post';
|
||||
|
||||
import type {Database} from '@nozbe/watermelondb';
|
||||
import type PostModel from '@typings/database/models/servers/post';
|
||||
|
||||
const serverUrl = 'baseHandler.test.com';
|
||||
|
||||
describe('Edit Post', () => {
|
||||
|
||||
let database: Database;
|
||||
|
||||
const baseProps: Parameters<typeof EditPost>[0] = {
|
||||
componentId: 'EditPost',
|
||||
closeButtonId: 'edit-post',
|
||||
post: {
|
||||
id: '1',
|
||||
channelId: '1',
|
||||
message: 'test',
|
||||
messageSource: 'test',
|
||||
userId: '1',
|
||||
rootId: '1',
|
||||
metadata: {},
|
||||
} as PostModel,
|
||||
maxPostSize: 1000,
|
||||
hasFilesAttached: true,
|
||||
canDelete: true,
|
||||
files: [
|
||||
{
|
||||
id: 'file-1',
|
||||
name: 'test-1',
|
||||
extension: 'png',
|
||||
has_preview_image: true,
|
||||
height: 100,
|
||||
mime_type: 'test',
|
||||
size: 100,
|
||||
user_id: '1',
|
||||
width: 100,
|
||||
},
|
||||
{
|
||||
id: 'file-2',
|
||||
name: 'test-2',
|
||||
extension: 'pdf',
|
||||
has_preview_image: false,
|
||||
height: 100,
|
||||
mime_type: 'test',
|
||||
size: 100,
|
||||
user_id: '1',
|
||||
width: 100,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
beforeAll(async () => {
|
||||
const server = await TestHelper.setupServerDatabase(serverUrl);
|
||||
database = server.database;
|
||||
const operator = server.operator;
|
||||
operator.handleConfigs({
|
||||
configs: [
|
||||
{id: 'Version', value: '10.5.0'},
|
||||
],
|
||||
configsToDelete: [],
|
||||
prepareRecordsOnly: false,
|
||||
});
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('should render the attachments in edit mode', () => {
|
||||
const {getByTestId} = renderWithEverything(<EditPost {...baseProps}/>, {database, serverUrl});
|
||||
expect(getByTestId('uploads')).toBeVisible();
|
||||
expect(getByTestId('file-1')).toBeVisible();
|
||||
expect(getByTestId('file-2')).toBeVisible();
|
||||
});
|
||||
});
|
||||
|
|
@ -8,6 +8,7 @@ import {Alert, Keyboard, type LayoutChangeEvent, Platform, SafeAreaView, View, S
|
|||
import {deletePost, editPost} from '@actions/remote/post';
|
||||
import Autocomplete from '@components/autocomplete';
|
||||
import Loading from '@components/loading';
|
||||
import {EditPostProvider} from '@context/edit_post';
|
||||
import {ExtraKeyboardProvider} from '@context/extra_keyboard';
|
||||
import {useServerUrl} from '@context/server';
|
||||
import {useTheme} from '@context/theme';
|
||||
|
|
@ -22,8 +23,9 @@ import PostError from '@screens/edit_post/post_error';
|
|||
import {buildNavigationButton, dismissModal, setButtons} from '@screens/navigation';
|
||||
import {changeOpacity} from '@utils/theme';
|
||||
|
||||
import EditPostInput, {type EditPostInputRef} from './edit_post_input';
|
||||
import EditPostInput from './edit_post_input';
|
||||
|
||||
import type {PasteInputRef} from '@mattermost/react-native-paste-input';
|
||||
import type PostModel from '@typings/database/models/servers/post';
|
||||
import type {AvailableScreens} from '@typings/screens/navigation';
|
||||
|
||||
|
|
@ -55,8 +57,9 @@ type EditPostProps = {
|
|||
maxPostSize: number;
|
||||
hasFilesAttached: boolean;
|
||||
canDelete: boolean;
|
||||
files?: FileInfo[];
|
||||
}
|
||||
const EditPost = ({componentId, maxPostSize, post, closeButtonId, hasFilesAttached, canDelete}: EditPostProps) => {
|
||||
const EditPost = ({componentId, maxPostSize, post, closeButtonId, hasFilesAttached, canDelete, files}: EditPostProps) => {
|
||||
const editingMessage = post.messageSource || post.message;
|
||||
const [postMessage, setPostMessage] = useState(editingMessage);
|
||||
const [cursorPosition, setCursorPosition] = useState(editingMessage.length);
|
||||
|
|
@ -65,10 +68,11 @@ const EditPost = ({componentId, maxPostSize, post, closeButtonId, hasFilesAttach
|
|||
const [isUpdating, setIsUpdating] = useState(false);
|
||||
const [containerHeight, setContainerHeight] = useState(0);
|
||||
const [propagateValue, shouldProcessEvent] = useInputPropagation();
|
||||
const [postFiles, setPostFiles] = useState<FileInfo[]>(files || []);
|
||||
|
||||
const mainView = useRef<View>(null);
|
||||
|
||||
const postInputRef = useRef<EditPostInputRef>(null);
|
||||
const postInputRef = useRef<PasteInputRef | undefined>(undefined);
|
||||
const theme = useTheme();
|
||||
const intl = useIntl();
|
||||
const serverUrl = useServerUrl();
|
||||
|
|
@ -117,6 +121,44 @@ const EditPost = ({componentId, maxPostSize, post, closeButtonId, hasFilesAttach
|
|||
});
|
||||
}, [componentId, intl, theme]);
|
||||
|
||||
const handleFileRemoval = useCallback((id: string) => {
|
||||
const filterFileById = (file: FileInfo) => {
|
||||
return file.id !== id;
|
||||
};
|
||||
Alert.alert(
|
||||
intl.formatMessage({
|
||||
id: 'edit_post.delete_file.title',
|
||||
defaultMessage: 'Delete attachment',
|
||||
}),
|
||||
intl.formatMessage({
|
||||
id: 'edit_post.delete_file.confirmation',
|
||||
defaultMessage: 'Are you sure you want to remove {filename}?',
|
||||
}, {
|
||||
filename: postFiles?.find((file) => file.id === id)?.name || '',
|
||||
}),
|
||||
[
|
||||
{
|
||||
text: intl.formatMessage({
|
||||
id: 'edit_post.delete_file.cancel',
|
||||
defaultMessage: 'Cancel',
|
||||
}),
|
||||
style: 'cancel',
|
||||
},
|
||||
{
|
||||
text: intl.formatMessage({
|
||||
id: 'edit_post.delete_file.confirm',
|
||||
defaultMessage: 'Delete',
|
||||
}),
|
||||
style: 'destructive',
|
||||
onPress: () => {
|
||||
setPostFiles((prevFiles) => prevFiles?.filter(filterFileById) || []);
|
||||
toggleSaveButton(true);
|
||||
},
|
||||
},
|
||||
],
|
||||
);
|
||||
}, [intl, toggleSaveButton, postFiles]);
|
||||
|
||||
const onChangeTextCommon = useCallback((message: string) => {
|
||||
const tooLong = message.trim().length > maxPostSize;
|
||||
setErrorLine(undefined);
|
||||
|
|
@ -192,15 +234,21 @@ const EditPost = ({componentId, maxPostSize, post, closeButtonId, hasFilesAttach
|
|||
return;
|
||||
}
|
||||
|
||||
const res = await editPost(serverUrl, post.id, postMessage);
|
||||
const currentFileIds = postFiles.map((file) => file.id).filter((id): id is string => Boolean(id));
|
||||
const originalFiles = post.metadata?.files || [];
|
||||
const originalFileIds = originalFiles.map((file) => file.id).filter((id): id is string => Boolean(id));
|
||||
const currentFileIdSet = new Set(currentFileIds);
|
||||
const removedFileIds = originalFileIds.filter((id) => !currentFileIdSet.has(id));
|
||||
|
||||
const res = await editPost(serverUrl, post.id, postMessage, currentFileIds, removedFileIds);
|
||||
handleUIUpdates(res);
|
||||
}, [toggleSaveButton, shouldDeleteOnSave, serverUrl, post.id, postMessage, handleUIUpdates, handleDeletePost]);
|
||||
}, [toggleSaveButton, shouldDeleteOnSave, post.metadata?.files, post.id, serverUrl, postMessage, handleUIUpdates, handleDeletePost, postFiles]);
|
||||
|
||||
const onLayout = useCallback((e: LayoutChangeEvent) => {
|
||||
setContainerHeight(e.nativeEvent.layout.height);
|
||||
}, []);
|
||||
|
||||
useNavButtonPressed(RIGHT_BUTTON.id, componentId, onSavePostMessage, [postMessage]);
|
||||
useNavButtonPressed(RIGHT_BUTTON.id, componentId, onSavePostMessage, [postMessage, postFiles]);
|
||||
useNavButtonPressed(closeButtonId, componentId, onClose, []);
|
||||
useAndroidHardwareBackHandler(componentId, onClose);
|
||||
|
||||
|
|
@ -222,7 +270,7 @@ const EditPost = ({componentId, maxPostSize, post, closeButtonId, hasFilesAttach
|
|||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<EditPostProvider onFileRemove={handleFileRemoval}>
|
||||
<SafeAreaView
|
||||
testID='edit_post.screen'
|
||||
style={styles.container}
|
||||
|
|
@ -246,7 +294,9 @@ const EditPost = ({componentId, maxPostSize, post, closeButtonId, hasFilesAttach
|
|||
message={postMessage}
|
||||
onChangeText={onInputChangeText}
|
||||
onTextSelectionChange={onTextSelectionChange}
|
||||
ref={postInputRef}
|
||||
inputRef={postInputRef}
|
||||
post={post}
|
||||
postFiles={postFiles}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
|
|
@ -265,7 +315,7 @@ const EditPost = ({componentId, maxPostSize, post, closeButtonId, hasFilesAttach
|
|||
inPost={false}
|
||||
serverUrl={serverUrl}
|
||||
/>
|
||||
</>
|
||||
</EditPostProvider>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,87 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {Database} from '@nozbe/watermelondb';
|
||||
|
||||
import {renderWithEverything} from '@test/intl-test-helper';
|
||||
import TestHelper from '@test/test_helper';
|
||||
|
||||
import EditPostInput from './edit_post_input';
|
||||
|
||||
import type PostModel from '@typings/database/models/servers/post';
|
||||
|
||||
const serverUrl = 'baseHandler.test.com';
|
||||
|
||||
describe('EditPostInput', () => {
|
||||
let database: Database;
|
||||
|
||||
const baseProps: Parameters<typeof EditPostInput>[0] = {
|
||||
message: 'test',
|
||||
hasError: false,
|
||||
post: {
|
||||
id: '1',
|
||||
channelId: '1',
|
||||
message: 'test',
|
||||
messageSource: 'test',
|
||||
userId: '1',
|
||||
rootId: '1',
|
||||
metadata: {},
|
||||
} as PostModel,
|
||||
postFiles: [
|
||||
{
|
||||
id: 'file-1',
|
||||
name: 'test-1',
|
||||
extension: 'png',
|
||||
has_preview_image: true,
|
||||
height: 100,
|
||||
mime_type: 'test',
|
||||
size: 100,
|
||||
user_id: '1',
|
||||
width: 100,
|
||||
},
|
||||
{
|
||||
id: 'file-2',
|
||||
name: 'test-2',
|
||||
extension: 'pdf',
|
||||
has_preview_image: false,
|
||||
height: 100,
|
||||
mime_type: 'test',
|
||||
size: 100,
|
||||
user_id: '1',
|
||||
width: 100,
|
||||
},
|
||||
],
|
||||
version: '10.5.0',
|
||||
inputRef: {current: undefined},
|
||||
onTextSelectionChange: jest.fn(),
|
||||
onChangeText: jest.fn(),
|
||||
};
|
||||
|
||||
beforeAll(async () => {
|
||||
const server = await TestHelper.setupServerDatabase(serverUrl);
|
||||
database = server.database;
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('should render attachments in edit mode', () => {
|
||||
const {getByTestId} = renderWithEverything(<EditPostInput {...baseProps}/>, {database, serverUrl});
|
||||
expect(getByTestId('uploads')).toBeVisible();
|
||||
expect(getByTestId('file-1')).toBeVisible();
|
||||
expect(getByTestId('file-2')).toBeVisible();
|
||||
});
|
||||
|
||||
it('should match the count of the attachments in edit mode', () => {
|
||||
const {getByTestId} = renderWithEverything(<EditPostInput {...baseProps}/>, {database, serverUrl});
|
||||
expect(getByTestId('uploads')).toBeVisible();
|
||||
expect(getByTestId('uploads').children).toHaveLength(2);
|
||||
});
|
||||
|
||||
it('should not render attachments if the server version is less than 10.5.0', () => {
|
||||
const props = {...baseProps, version: '10.4.0'};
|
||||
const {queryByTestId} = renderWithEverything(<EditPostInput {...props}/>, {database, serverUrl});
|
||||
expect(queryByTestId('uploads')).toBeNull();
|
||||
});
|
||||
});
|
||||
129
app/screens/edit_post/edit_post_input/edit_post_input.tsx
Normal file
129
app/screens/edit_post/edit_post_input/edit_post_input.tsx
Normal file
|
|
@ -0,0 +1,129 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {useManagedConfig} from '@mattermost/react-native-emm';
|
||||
import PasteInput, {type PasteInputRef} from '@mattermost/react-native-paste-input';
|
||||
import React, {useCallback, useMemo} from 'react';
|
||||
import {useIntl} from 'react-intl';
|
||||
import {type NativeSyntheticEvent, Platform, type TextInputSelectionChangeEventData, View} from 'react-native';
|
||||
|
||||
import Uploads from '@components/post_draft/uploads';
|
||||
import {ExtraKeyboard, useExtraKeyboardContext} from '@context/extra_keyboard';
|
||||
import {useTheme} from '@context/theme';
|
||||
import {emptyFunction} from '@utils/general';
|
||||
import {isMinimumServerVersion} from '@utils/helpers';
|
||||
import {changeOpacity, getKeyboardAppearanceFromTheme, makeStyleSheetFromTheme} from '@utils/theme';
|
||||
import {typography} from '@utils/typography';
|
||||
|
||||
import type PostModel from '@typings/database/models/servers/post';
|
||||
|
||||
const MAJOR_VERSION_TO_SHOW_ATTACHMENTS = 10;
|
||||
const MINOR_VERSION_TO_SHOW_ATTACHMENTS = 5;
|
||||
|
||||
const getStyleSheet = makeStyleSheetFromTheme((theme) => ({
|
||||
input: {
|
||||
color: theme.centerChannelColor,
|
||||
padding: 15,
|
||||
textAlignVertical: 'top',
|
||||
flex: 1,
|
||||
...typography('Body', 200),
|
||||
},
|
||||
inputContainer: {
|
||||
backgroundColor: theme.centerChannelBg,
|
||||
marginTop: 2,
|
||||
flex: 1,
|
||||
},
|
||||
}));
|
||||
|
||||
type PostInputProps = {
|
||||
message: string;
|
||||
hasError: boolean;
|
||||
post: PostModel;
|
||||
postFiles: FileInfo[];
|
||||
version?: string;
|
||||
onTextSelectionChange: (curPos: number) => void;
|
||||
onChangeText: (text: string) => void;
|
||||
inputRef: React.MutableRefObject<PasteInputRef | undefined>;
|
||||
}
|
||||
|
||||
const EditPostInput = ({
|
||||
message,
|
||||
onChangeText,
|
||||
onTextSelectionChange,
|
||||
hasError,
|
||||
post,
|
||||
postFiles,
|
||||
version,
|
||||
inputRef,
|
||||
}: PostInputProps) => {
|
||||
const intl = useIntl();
|
||||
const theme = useTheme();
|
||||
const styles = getStyleSheet(theme);
|
||||
const managedConfig = useManagedConfig<ManagedConfig>();
|
||||
const disableCopyAndPaste = managedConfig.copyAndPasteProtection === 'true';
|
||||
|
||||
const keyboardContext = useExtraKeyboardContext();
|
||||
|
||||
const onFocus = useCallback(() => {
|
||||
keyboardContext?.registerTextInputFocus();
|
||||
}, [keyboardContext]);
|
||||
|
||||
const onBlur = useCallback(() => {
|
||||
keyboardContext?.registerTextInputBlur();
|
||||
}, [keyboardContext]);
|
||||
|
||||
const inputStyle = useMemo(() => {
|
||||
return [styles.input];
|
||||
}, [styles]);
|
||||
|
||||
const onSelectionChange = useCallback((event: NativeSyntheticEvent<TextInputSelectionChangeEventData>) => {
|
||||
const curPos = event.nativeEvent.selection.end;
|
||||
onTextSelectionChange(curPos);
|
||||
}, [onTextSelectionChange]);
|
||||
|
||||
const containerStyle = useMemo(() => [
|
||||
styles.inputContainer,
|
||||
hasError && {marginTop: 0},
|
||||
], [styles, hasError]);
|
||||
|
||||
return (
|
||||
<View style={containerStyle}>
|
||||
<PasteInput
|
||||
allowFontScaling={true}
|
||||
disableCopyPaste={disableCopyAndPaste}
|
||||
disableFullscreenUI={true}
|
||||
keyboardAppearance={getKeyboardAppearanceFromTheme(theme)}
|
||||
multiline={true}
|
||||
onChangeText={onChangeText}
|
||||
onPaste={emptyFunction}
|
||||
onSelectionChange={onSelectionChange}
|
||||
placeholder={intl.formatMessage({id: 'edit_post.editPost', defaultMessage: 'Edit the post...'})}
|
||||
placeholderTextColor={changeOpacity(theme.centerChannelColor, 0.5)}
|
||||
ref={inputRef}
|
||||
smartPunctuation='disable'
|
||||
submitBehavior='newline'
|
||||
style={inputStyle}
|
||||
testID='edit_post.message.input'
|
||||
underlineColorAndroid='transparent'
|
||||
value={message}
|
||||
onFocus={onFocus}
|
||||
onBlur={onBlur}
|
||||
/>
|
||||
{isMinimumServerVersion(version, MAJOR_VERSION_TO_SHOW_ATTACHMENTS, MINOR_VERSION_TO_SHOW_ATTACHMENTS) &&
|
||||
<Uploads
|
||||
channelId={post.channelId}
|
||||
currentUserId={post.userId}
|
||||
files={postFiles}
|
||||
uploadFileError={undefined} // TODO: Add upload file error
|
||||
rootId={post.rootId}
|
||||
isEditMode={true}
|
||||
/>
|
||||
}
|
||||
{Platform.select({ios: <ExtraKeyboard/>})}
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
EditPostInput.displayName = 'EditPostInput';
|
||||
|
||||
export default EditPostInput;
|
||||
|
|
@ -1,114 +1,16 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {useManagedConfig} from '@mattermost/react-native-emm';
|
||||
import PasteInput, {type PasteInputRef} from '@mattermost/react-native-paste-input';
|
||||
import React, {forwardRef, useCallback, useImperativeHandle, useMemo, useRef} from 'react';
|
||||
import {useIntl} from 'react-intl';
|
||||
import {type NativeSyntheticEvent, Platform, type TextInputSelectionChangeEventData, View} from 'react-native';
|
||||
import {withDatabase, withObservables} from '@nozbe/watermelondb/react';
|
||||
|
||||
import {ExtraKeyboard, useExtraKeyboardContext} from '@context/extra_keyboard';
|
||||
import {useTheme} from '@context/theme';
|
||||
import {emptyFunction} from '@utils/general';
|
||||
import {changeOpacity, getKeyboardAppearanceFromTheme, makeStyleSheetFromTheme} from '@utils/theme';
|
||||
import {typography} from '@utils/typography';
|
||||
import {observeConfigValue} from '@queries/servers/system';
|
||||
|
||||
const getStyleSheet = makeStyleSheetFromTheme((theme) => ({
|
||||
input: {
|
||||
color: theme.centerChannelColor,
|
||||
padding: 15,
|
||||
textAlignVertical: 'top',
|
||||
flex: 1,
|
||||
...typography('Body', 200),
|
||||
},
|
||||
inputContainer: {
|
||||
backgroundColor: theme.centerChannelBg,
|
||||
marginTop: 2,
|
||||
flex: 1,
|
||||
},
|
||||
import EditPostInput from './edit_post_input';
|
||||
|
||||
import type {WithDatabaseArgs} from '@typings/database/database';
|
||||
|
||||
const enhanced = withObservables([], ({database}: WithDatabaseArgs) => ({
|
||||
version: observeConfigValue(database, 'Version'),
|
||||
}));
|
||||
|
||||
export type EditPostInputRef = {
|
||||
focus: () => void;
|
||||
}
|
||||
|
||||
type PostInputProps = {
|
||||
message: string;
|
||||
hasError: boolean;
|
||||
onTextSelectionChange: (curPos: number) => void;
|
||||
onChangeText: (text: string) => void;
|
||||
}
|
||||
|
||||
const EditPostInput = forwardRef<EditPostInputRef, PostInputProps>(({
|
||||
message,
|
||||
onChangeText,
|
||||
onTextSelectionChange,
|
||||
hasError,
|
||||
}: PostInputProps, ref) => {
|
||||
const intl = useIntl();
|
||||
const theme = useTheme();
|
||||
const styles = getStyleSheet(theme);
|
||||
const managedConfig = useManagedConfig<ManagedConfig>();
|
||||
const disableCopyAndPaste = managedConfig.copyAndPasteProtection === 'true';
|
||||
|
||||
const inputRef = useRef<PasteInputRef>();
|
||||
|
||||
const keyboardContext = useExtraKeyboardContext();
|
||||
|
||||
const onFocus = useCallback(() => {
|
||||
keyboardContext?.registerTextInputFocus();
|
||||
}, [keyboardContext]);
|
||||
|
||||
const onBlur = useCallback(() => {
|
||||
keyboardContext?.registerTextInputBlur();
|
||||
}, [keyboardContext]);
|
||||
|
||||
const inputStyle = useMemo(() => {
|
||||
return [styles.input];
|
||||
}, [styles]);
|
||||
|
||||
const onSelectionChange = useCallback((event: NativeSyntheticEvent<TextInputSelectionChangeEventData>) => {
|
||||
const curPos = event.nativeEvent.selection.end;
|
||||
onTextSelectionChange(curPos);
|
||||
}, [onTextSelectionChange]);
|
||||
|
||||
const containerStyle = useMemo(() => [
|
||||
styles.inputContainer,
|
||||
hasError && {marginTop: 0},
|
||||
], [styles, hasError]);
|
||||
|
||||
useImperativeHandle(ref, () => ({
|
||||
focus: () => inputRef.current?.focus(),
|
||||
}), []);
|
||||
|
||||
return (
|
||||
<View style={containerStyle}>
|
||||
<PasteInput
|
||||
allowFontScaling={true}
|
||||
disableCopyPaste={disableCopyAndPaste}
|
||||
disableFullscreenUI={true}
|
||||
keyboardAppearance={getKeyboardAppearanceFromTheme(theme)}
|
||||
multiline={true}
|
||||
onChangeText={onChangeText}
|
||||
onPaste={emptyFunction}
|
||||
onSelectionChange={onSelectionChange}
|
||||
placeholder={intl.formatMessage({id: 'edit_post.editPost', defaultMessage: 'Edit the post...'})}
|
||||
placeholderTextColor={changeOpacity(theme.centerChannelColor, 0.5)}
|
||||
ref={inputRef}
|
||||
smartPunctuation='disable'
|
||||
submitBehavior='newline'
|
||||
style={inputStyle}
|
||||
testID='edit_post.message.input'
|
||||
underlineColorAndroid='transparent'
|
||||
value={message}
|
||||
onFocus={onFocus}
|
||||
onBlur={onBlur}
|
||||
/>
|
||||
{Platform.select({ios: <ExtraKeyboard/>})}
|
||||
</View>
|
||||
);
|
||||
});
|
||||
|
||||
EditPostInput.displayName = 'EditPostInput';
|
||||
|
||||
export default EditPostInput;
|
||||
export default withDatabase(enhanced(EditPostInput));
|
||||
|
|
|
|||
|
|
@ -18,8 +18,9 @@ type Props = {
|
|||
bottomSheetId: AvailableScreens;
|
||||
post: PostModel;
|
||||
canDelete: boolean;
|
||||
files?: FileInfo[];
|
||||
}
|
||||
const EditOption = ({bottomSheetId, post, canDelete}: Props) => {
|
||||
const EditOption = ({bottomSheetId, post, canDelete, files}: Props) => {
|
||||
const intl = useIntl();
|
||||
const theme = useTheme();
|
||||
|
||||
|
|
@ -29,7 +30,7 @@ const EditOption = ({bottomSheetId, post, canDelete}: Props) => {
|
|||
const title = intl.formatMessage({id: 'mobile.edit_post.title', defaultMessage: 'Editing Message'});
|
||||
const closeButton = CompassIcon.getImageSourceSync('close', 24, theme.sidebarHeaderTextColor);
|
||||
const closeButtonId = 'close-edit-post';
|
||||
const passProps = {post, closeButtonId, canDelete};
|
||||
const passProps = {post, closeButtonId, canDelete, files};
|
||||
const options = {
|
||||
topBar: {
|
||||
leftButtons: [{
|
||||
|
|
@ -40,7 +41,7 @@ const EditOption = ({bottomSheetId, post, canDelete}: Props) => {
|
|||
},
|
||||
};
|
||||
showModal(Screens.EDIT_POST, title, passProps, options);
|
||||
}, [bottomSheetId, post]);
|
||||
}, [bottomSheetId, canDelete, files, intl, post, theme.sidebarHeaderTextColor]);
|
||||
|
||||
return (
|
||||
<BaseOption
|
||||
121
app/screens/post_options/options/edit_option/index.test.tsx
Normal file
121
app/screens/post_options/options/edit_option/index.test.tsx
Normal file
|
|
@ -0,0 +1,121 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import {ActionType, Screens} from '@constants';
|
||||
import {SYSTEM_IDENTIFIERS} from '@constants/database';
|
||||
import DatabaseManager from '@database/manager';
|
||||
import {renderWithEverything, waitFor} from '@test/intl-test-helper';
|
||||
import TestHelper from '@test/test_helper';
|
||||
|
||||
import EditOption from './edit_option';
|
||||
|
||||
import EnhancedEditOption 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';
|
||||
|
||||
jest.mock('./edit_option', () => ({
|
||||
__esModule: true,
|
||||
default: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mocked(EditOption).mockImplementation((props) => React.createElement('EditOption', {...props, testID: 'edit-option'}));
|
||||
|
||||
describe('EditOption', () => {
|
||||
const serverUrl = 'server-1';
|
||||
const teamId = 'team1';
|
||||
let database: Database;
|
||||
let operator: ServerDataOperator;
|
||||
let post: PostModel;
|
||||
|
||||
beforeEach(async () => {
|
||||
await DatabaseManager.init([serverUrl]);
|
||||
const serverDatabaseAndOperator = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
|
||||
database = serverDatabaseAndOperator.database;
|
||||
operator = serverDatabaseAndOperator.operator;
|
||||
|
||||
await operator.handleSystem({systems: [{id: SYSTEM_IDENTIFIERS.CURRENT_TEAM_ID, value: teamId}], prepareRecordsOnly: false});
|
||||
await operator.handleTeam({teams: [TestHelper.fakeTeam({id: teamId})], prepareRecordsOnly: false});
|
||||
await operator.handleConfigs({
|
||||
configs: [
|
||||
{id: 'Version', value: '7.6.0'},
|
||||
],
|
||||
configsToDelete: [],
|
||||
prepareRecordsOnly: false,
|
||||
});
|
||||
post = TestHelper.fakePostModel();
|
||||
const postRaw = TestHelper.fakePost({
|
||||
id: post.id,
|
||||
message: post.message,
|
||||
});
|
||||
await operator.handlePosts({
|
||||
actionType: ActionType.POSTS.RECEIVED_NEW,
|
||||
order: [post.id],
|
||||
posts: [postRaw],
|
||||
prepareRecordsOnly: false,
|
||||
});
|
||||
|
||||
const files = [
|
||||
TestHelper.fakeFileInfo(
|
||||
{
|
||||
post_id: post.id,
|
||||
id: 'file-1',
|
||||
name: 'file1',
|
||||
create_at: 1000,
|
||||
}),
|
||||
TestHelper.fakeFileInfo({
|
||||
post_id: post.id,
|
||||
id: 'file-2',
|
||||
name: 'file2',
|
||||
create_at: 1001,
|
||||
}),
|
||||
];
|
||||
await operator.handleFiles({
|
||||
files,
|
||||
prepareRecordsOnly: false,
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await DatabaseManager.destroyServerDatabase(serverUrl);
|
||||
});
|
||||
|
||||
it('should fetch the files for the post', async () => {
|
||||
const {getByTestId} = renderWithEverything(
|
||||
<EnhancedEditOption
|
||||
post={post}
|
||||
canDelete={true}
|
||||
bottomSheetId={Screens.EDIT_POST}
|
||||
/>,
|
||||
{database},
|
||||
);
|
||||
await waitFor(() => {
|
||||
const editPost = getByTestId('edit-option');
|
||||
expect(editPost).toBeDefined();
|
||||
const files = editPost.props.files;
|
||||
expect(files).toHaveLength(2);
|
||||
expect(files[0].name).toBe('file2');
|
||||
expect(files[1].name).toBe('file1');
|
||||
});
|
||||
});
|
||||
|
||||
it('should not fetch the files for the post if the post has no files', async () => {
|
||||
const {getByTestId} = renderWithEverything(
|
||||
<EnhancedEditOption
|
||||
post={{id: 'post-2', message: 'test'} as PostModel}
|
||||
canDelete={true}
|
||||
bottomSheetId={Screens.EDIT_POST}
|
||||
/>,
|
||||
{database},
|
||||
);
|
||||
await waitFor(() => {
|
||||
const editPost = getByTestId('edit-option');
|
||||
expect(editPost).toBeDefined();
|
||||
const files = editPost.props.files;
|
||||
expect(files).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
});
|
||||
30
app/screens/post_options/options/edit_option/index.ts
Normal file
30
app/screens/post_options/options/edit_option/index.ts
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {withDatabase, withObservables} from '@nozbe/watermelondb/react';
|
||||
import {from as from$} from 'rxjs';
|
||||
import {switchMap} from 'rxjs/operators';
|
||||
|
||||
import {observeFilesForPost} from '@queries/servers/file';
|
||||
import {filesLocalPathValidation} from '@utils/file';
|
||||
|
||||
import EditOption from './edit_option';
|
||||
|
||||
import type {WithDatabaseArgs} from '@typings/database/database';
|
||||
import type PostModel from '@typings/database/models/servers/post';
|
||||
|
||||
type Props = WithDatabaseArgs & {
|
||||
post: PostModel;
|
||||
}
|
||||
|
||||
const enhance = withObservables(['post'], ({post, database}: Props) => {
|
||||
const files = observeFilesForPost(database, post.id).pipe(
|
||||
switchMap((fs) => from$(filesLocalPathValidation(fs, post.userId))),
|
||||
);
|
||||
|
||||
return {
|
||||
files,
|
||||
};
|
||||
});
|
||||
|
||||
export default withDatabase(enhance(EditOption));
|
||||
|
|
@ -597,3 +597,19 @@ export const pathWithPrefix = (prefix: string, path: string) => {
|
|||
export const deleteFile = async (path: string) => {
|
||||
await deleteAsync(pathWithPrefix('file://', path));
|
||||
};
|
||||
|
||||
export const filesLocalPathValidation = async (files: FileModel[], authorId: string) => {
|
||||
const filesInfo: FileInfo[] = [];
|
||||
for await (const f of files) {
|
||||
const info = f.toFileInfo(authorId);
|
||||
if (info.localPath) {
|
||||
const exists = await fileExists(info.localPath);
|
||||
if (!exists) {
|
||||
info.localPath = '';
|
||||
}
|
||||
}
|
||||
filesInfo.push(info);
|
||||
}
|
||||
|
||||
return filesInfo;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -328,6 +328,10 @@
|
|||
"drafts_tab.title.scheduled": "Scheduled",
|
||||
"drafts.empty.subtitle": "Any message you have started will show here.",
|
||||
"drafts.empty.title": "No drafts at the moment",
|
||||
"edit_post.delete_file.cancel": "Cancel",
|
||||
"edit_post.delete_file.confirm": "Delete",
|
||||
"edit_post.delete_file.confirmation": "Are you sure you want to remove {filename}?",
|
||||
"edit_post.delete_file.title": "Delete attachment",
|
||||
"edit_post.editPost": "Edit the post...",
|
||||
"edit_post.save": "Save",
|
||||
"edit_server.description": "Specify a display name for this server",
|
||||
|
|
|
|||
Loading…
Reference in a new issue