Fix draft sent to show and rise errors (#28097)
* Fix draft sent to show and rise errors * Remove event and set skipCommands * Fix tests * Improve error styles * Fix lint * Fix lint * Fix tests * Various fixes * Fix lint * fix test * Extract onSubmit options type * Address feedback * Fix text
Этот коммит содержится в:
коммит произвёл
GitHub
родитель
d58b048965
Коммит
0676d300de
@@ -75,7 +75,7 @@ export function sendDesktopNotification(post, msgProps) {
|
||||
|
||||
const teamId = msgProps.team_id;
|
||||
|
||||
let channel = makeGetChannel()(state, {id: post.channel_id});
|
||||
let channel = makeGetChannel()(state, post.channel_id);
|
||||
const user = getCurrentUser(state);
|
||||
const userStatus = getStatusForUserId(state, user.id);
|
||||
const member = getMyChannelMember(state, post.channel_id);
|
||||
|
||||
@@ -106,7 +106,12 @@ export function unflagPost(postId: string): ActionFuncAsync {
|
||||
};
|
||||
}
|
||||
|
||||
export function createPost(post: Post, files: FileInfo[], afterSubmit?: (response: SubmitPostReturnType) => void): ActionFuncAsync<PostActions.CreatePostReturnType, GlobalState> {
|
||||
export function createPost(
|
||||
post: Post,
|
||||
files: FileInfo[],
|
||||
afterSubmit?: (response: SubmitPostReturnType) => void,
|
||||
afterOptimisticSubmit?: () => void,
|
||||
): ActionFuncAsync<PostActions.CreatePostReturnType, GlobalState> {
|
||||
return async (dispatch) => {
|
||||
// parse message and emit emoji event
|
||||
const emojis = matchEmoticons(post.message);
|
||||
@@ -123,6 +128,7 @@ export function createPost(post: Post, files: FileInfo[], afterSubmit?: (respons
|
||||
dispatch(storeDraft(post.channel_id, null));
|
||||
}
|
||||
|
||||
afterOptimisticSubmit?.();
|
||||
return result;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -13,12 +13,12 @@ import {setGlobalItem, actionOnGlobalItemsWithPrefix} from 'actions/storage';
|
||||
import {
|
||||
clearCommentDraftUploads,
|
||||
updateCommentDraft,
|
||||
onSubmit,
|
||||
submitPost,
|
||||
submitCommand,
|
||||
makeOnSubmit,
|
||||
makeOnEditLatestPost,
|
||||
} from 'actions/views/create_comment';
|
||||
import {removeDraft, setGlobalDraftSource} from 'actions/views/drafts';
|
||||
import {setGlobalDraftSource} from 'actions/views/drafts';
|
||||
|
||||
import mockStore from 'tests/test_store';
|
||||
import {StoragePrefixes} from 'utils/constants';
|
||||
@@ -312,8 +312,7 @@ describe('rhs view actions', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('makeOnSubmit', () => {
|
||||
const onSubmit = makeOnSubmit(channelId, rootId, latestPostId);
|
||||
describe('onSubmit', () => {
|
||||
const draft = {
|
||||
message: 'test',
|
||||
fileInfos: [],
|
||||
@@ -323,7 +322,7 @@ describe('rhs view actions', () => {
|
||||
};
|
||||
|
||||
test('it adds message into history', () => {
|
||||
store.dispatch(onSubmit(draft));
|
||||
store.dispatch(onSubmit(draft, {}));
|
||||
|
||||
const testStore = mockStore(initialState);
|
||||
testStore.dispatch(addMessageIntoHistory('test'));
|
||||
@@ -333,39 +332,12 @@ describe('rhs view actions', () => {
|
||||
);
|
||||
});
|
||||
|
||||
test('it clears comment draft', () => {
|
||||
store.dispatch(onSubmit(draft));
|
||||
|
||||
const testStore = mockStore(initialState);
|
||||
const key = `${StoragePrefixes.COMMENT_DRAFT}${rootId}`;
|
||||
testStore.dispatch(removeDraft(key, channelId, rootId));
|
||||
|
||||
expect(store.getActions()).toEqual(
|
||||
expect.arrayContaining(testStore.getActions()),
|
||||
);
|
||||
});
|
||||
|
||||
test('it submits a reaction when message is +:smile:', () => {
|
||||
store.dispatch(onSubmit({
|
||||
message: '+:smile:',
|
||||
fileInfos: [],
|
||||
uploadsInProgress: [],
|
||||
}));
|
||||
|
||||
const testStore = mockStore(initialState);
|
||||
testStore.dispatch(PostActions.submitReaction(latestPostId, '+', 'smile'));
|
||||
|
||||
expect(store.getActions()).toEqual(
|
||||
expect.arrayContaining(testStore.getActions()),
|
||||
);
|
||||
});
|
||||
|
||||
test('it submits a command when message is /away', () => {
|
||||
store.dispatch(onSubmit({
|
||||
message: '/away',
|
||||
fileInfos: [],
|
||||
uploadsInProgress: [],
|
||||
}));
|
||||
}, {}));
|
||||
|
||||
const testStore = mockStore(initialState);
|
||||
testStore.dispatch(submitCommand(channelId, rootId, {message: '/away', fileInfos: [], uploadsInProgress: []}));
|
||||
@@ -398,7 +370,7 @@ describe('rhs view actions', () => {
|
||||
message: 'test msg',
|
||||
fileInfos: [],
|
||||
uploadsInProgress: [],
|
||||
}));
|
||||
}, {}));
|
||||
|
||||
const testStore = mockStore(initialState);
|
||||
testStore.dispatch(submitPost(channelId, rootId, {message: 'test msg', fileInfos: [], uploadsInProgress: []}));
|
||||
|
||||
@@ -29,7 +29,7 @@ import {executeCommand} from 'actions/command';
|
||||
import {runMessageWillBePostedHooks, runSlashCommandWillBePostedHooks} from 'actions/hooks';
|
||||
import * as PostActions from 'actions/post_actions';
|
||||
import {actionOnGlobalItemsWithPrefix} from 'actions/storage';
|
||||
import {updateDraft, removeDraft} from 'actions/views/drafts';
|
||||
import {updateDraft} from 'actions/views/drafts';
|
||||
|
||||
import {Constants, StoragePrefixes} from 'utils/constants';
|
||||
import EmojiMap from 'utils/emoji_map';
|
||||
@@ -56,7 +56,13 @@ export function updateCommentDraft(rootId: string, draft?: PostDraft, save = fal
|
||||
return updateDraft(key, draft ?? null, rootId, save);
|
||||
}
|
||||
|
||||
export function submitPost(channelId: string, rootId: string, draft: PostDraft, afterSubmit?: (response: SubmitPostReturnType) => void): ActionFuncAsync<CreatePostReturnType, GlobalState> {
|
||||
export function submitPost(
|
||||
channelId: string,
|
||||
rootId: string,
|
||||
draft: PostDraft,
|
||||
afterSubmit?: (response: SubmitPostReturnType) => void,
|
||||
afterOptimisticSubmit?: () => void,
|
||||
): ActionFuncAsync<CreatePostReturnType, GlobalState> {
|
||||
return async (dispatch, getState) => {
|
||||
const state = getState();
|
||||
|
||||
@@ -103,7 +109,7 @@ export function submitPost(channelId: string, rootId: string, draft: PostDraft,
|
||||
|
||||
post = hookResult.data;
|
||||
|
||||
return dispatch(PostActions.createPost(post, draft.fileInfos, afterSubmit));
|
||||
return dispatch(PostActions.createPost(post, draft.fileInfos, afterSubmit, afterOptimisticSubmit));
|
||||
};
|
||||
}
|
||||
|
||||
@@ -147,39 +153,18 @@ export function submitCommand(channelId: string, rootId: string, draft: PostDraf
|
||||
};
|
||||
}
|
||||
|
||||
export function makeOnSubmit(channelId: string, rootId: string, latestPostId: string): (draft: PostDraft, options?: {ignoreSlash?: boolean}) => ActionFuncAsync<boolean, GlobalState> {
|
||||
return (draft, options = {}) => async (dispatch, getState) => {
|
||||
const {message} = draft;
|
||||
|
||||
dispatch(addMessageIntoHistory(message));
|
||||
|
||||
const key = `${StoragePrefixes.COMMENT_DRAFT}${rootId}`;
|
||||
dispatch(removeDraft(key, channelId, rootId));
|
||||
|
||||
const isReaction = Utils.REACTION_PATTERN.exec(message);
|
||||
|
||||
const emojis = getCustomEmojisByName(getState());
|
||||
const emojiMap = new EmojiMap(emojis);
|
||||
|
||||
if (isReaction && emojiMap.has(isReaction[2])) {
|
||||
dispatch(PostActions.submitReaction(latestPostId, isReaction[1], isReaction[2]));
|
||||
} else if (message.indexOf('/') === 0 && !options.ignoreSlash) {
|
||||
try {
|
||||
await dispatch(submitCommand(channelId, rootId, draft));
|
||||
} catch (err) {
|
||||
dispatch(updateCommentDraft(rootId, draft, true));
|
||||
throw err;
|
||||
}
|
||||
} else {
|
||||
dispatch(submitPost(channelId, rootId, draft));
|
||||
}
|
||||
return {data: true};
|
||||
};
|
||||
}
|
||||
|
||||
export type SubmitPostReturnType = CreatePostReturnType & SubmitCommandRerturnType & SubmitReactionReturnType;
|
||||
|
||||
export function onSubmit(draft: PostDraft, options: {ignoreSlash?: boolean; afterSubmit?: (response: SubmitPostReturnType) => void}): ActionFuncAsync<SubmitPostReturnType, GlobalState> {
|
||||
export type OnSubmitOptions = {
|
||||
ignoreSlash?: boolean;
|
||||
afterSubmit?: (response: SubmitPostReturnType) => void;
|
||||
afterOptimisticSubmit?: () => void;
|
||||
}
|
||||
|
||||
export function onSubmit(
|
||||
draft: PostDraft,
|
||||
options: OnSubmitOptions,
|
||||
): ActionFuncAsync<SubmitPostReturnType, GlobalState> {
|
||||
return async (dispatch, getState) => {
|
||||
const {message, channelId, rootId} = draft;
|
||||
const state = getState();
|
||||
@@ -191,19 +176,19 @@ export function onSubmit(draft: PostDraft, options: {ignoreSlash?: boolean; afte
|
||||
const emojis = getCustomEmojisByName(state);
|
||||
const emojiMap = new EmojiMap(emojis);
|
||||
|
||||
if (isReaction && emojiMap.has(isReaction[2])) {
|
||||
if (isReaction && emojiMap.has(isReaction[2]) && !options.ignoreSlash) {
|
||||
const latestPostId = getLatestInteractablePostId(state, channelId, rootId);
|
||||
if (latestPostId) {
|
||||
return dispatch(PostActions.submitReaction(latestPostId, isReaction[1], isReaction[2]));
|
||||
}
|
||||
return {error: new Error('no post to react to')};
|
||||
return {error: new Error('No post to react to')};
|
||||
}
|
||||
|
||||
if (message.indexOf('/') === 0 && !options.ignoreSlash) {
|
||||
return dispatch(submitCommand(channelId, rootId, draft));
|
||||
}
|
||||
|
||||
return dispatch(submitPost(channelId, rootId, draft, options.afterSubmit));
|
||||
return dispatch(submitPost(channelId, rootId, draft, options.afterSubmit, options.afterOptimisticSubmit));
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -539,8 +539,8 @@ export function selectPost(post: Post, previousRhsState?: RhsState) {
|
||||
export function selectPostById(postId: string): ActionFuncAsync {
|
||||
return async (dispatch, getState) => {
|
||||
const state = getState();
|
||||
const post = getPost(state, postId) ?? (await dispatch(fetchPost(postId))).data;
|
||||
if (post) {
|
||||
const post: Post | undefined = getPost(state, postId) ?? (await dispatch(fetchPost(postId))).data;
|
||||
if (post && post.state !== 'DELETED' && post.delete_at === 0) {
|
||||
const channel = getChannelSelector(state, post.channel_id);
|
||||
if (!channel) {
|
||||
await dispatch(getChannel(post.channel_id));
|
||||
|
||||
@@ -108,7 +108,7 @@ const AdvancedTextEditor = ({
|
||||
const isRHS = Boolean(postId && !isThreadView);
|
||||
|
||||
const currentUserId = useSelector(getCurrentUserId);
|
||||
const channelDisplayName = useSelector((state: GlobalState) => getChannelSelector(state, {id: channelId})?.display_name || '');
|
||||
const channelDisplayName = useSelector((state: GlobalState) => getChannelSelector(state, channelId)?.display_name || '');
|
||||
const draftFromStore = useSelector((state: GlobalState) => getDraftSelector(state, channelId, postId));
|
||||
const badConnection = useSelector((state: GlobalState) => connectionErrorCount(state) > 1);
|
||||
const maxPostSize = useSelector((state: GlobalState) => parseInt(getConfig(state).MaxPostSize || '', 10) || Constants.DEFAULT_CHARACTER_LIMIT);
|
||||
@@ -253,7 +253,21 @@ const AdvancedTextEditor = ({
|
||||
isValidPersistentNotifications,
|
||||
onSubmitCheck: prioritySubmitCheck,
|
||||
} = usePriority(draft, handleDraftChange, focusTextbox, showPreview);
|
||||
const [handleSubmit, errorClass] = useSubmit(draft, postError, channelId, postId, serverError, lastBlurAt, focusTextbox, setServerError, setPostError, setShowPreview, handleDraftChange, prioritySubmitCheck, afterSubmit);
|
||||
const [handleSubmit, errorClass] = useSubmit(
|
||||
draft,
|
||||
postError,
|
||||
channelId,
|
||||
postId,
|
||||
serverError,
|
||||
lastBlurAt,
|
||||
focusTextbox,
|
||||
setServerError,
|
||||
setShowPreview,
|
||||
handleDraftChange,
|
||||
prioritySubmitCheck,
|
||||
undefined,
|
||||
afterSubmit,
|
||||
);
|
||||
const [handleKeyDown, postMsgKeyPress] = useKeyHandler(
|
||||
draft,
|
||||
channelId,
|
||||
@@ -272,6 +286,8 @@ const AdvancedTextEditor = ({
|
||||
toggleEmojiPicker,
|
||||
);
|
||||
|
||||
const noArgumentHandleSubmit = useCallback(() => handleSubmit(), [handleSubmit]);
|
||||
|
||||
const handlePostError = useCallback((err: React.ReactNode) => {
|
||||
setPostError(err);
|
||||
}, []);
|
||||
@@ -388,6 +404,7 @@ const AdvancedTextEditor = ({
|
||||
// Remove show preview when we switch channels or posts
|
||||
useEffect(() => {
|
||||
setShowPreview(false);
|
||||
setServerError(null);
|
||||
}, [channelId, postId]);
|
||||
|
||||
// Remove uploads in progress on mount
|
||||
@@ -532,7 +549,7 @@ const AdvancedTextEditor = ({
|
||||
id={postId ? undefined : 'create_post'}
|
||||
data-testid={postId ? undefined : 'create-post'}
|
||||
className={(!postId && !fullWidthTextBox) ? 'center' : undefined}
|
||||
onSubmit={handleSubmit}
|
||||
onSubmit={noArgumentHandleSubmit}
|
||||
>
|
||||
{canPost && (draft.fileInfos.length > 0 || draft.uploadsInProgress.length > 0) && (
|
||||
<FileLimitStickyBanner/>
|
||||
@@ -657,7 +674,7 @@ const AdvancedTextEditor = ({
|
||||
<MessageSubmitError
|
||||
error={serverError}
|
||||
submittedMessage={serverError.submittedMessage}
|
||||
handleSubmit={handleSubmit}
|
||||
handleSubmit={noArgumentHandleSubmit}
|
||||
/>
|
||||
)}
|
||||
<MsgTyping
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React, {FormEvent, memo} from 'react';
|
||||
import React, {memo} from 'react';
|
||||
import {useIntl} from 'react-intl';
|
||||
import styled from 'styled-components';
|
||||
|
||||
import {SendIcon} from '@mattermost/compass-icons/components';
|
||||
|
||||
type SendButtonProps = {
|
||||
handleSubmit: (e: React.FormEvent) => void;
|
||||
handleSubmit: () => void;
|
||||
disabled: boolean;
|
||||
}
|
||||
|
||||
@@ -46,7 +46,7 @@ const SendButton = ({disabled, handleSubmit}: SendButtonProps) => {
|
||||
const sendMessage = (e: React.FormEvent) => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
handleSubmit(e);
|
||||
handleSubmit();
|
||||
};
|
||||
|
||||
return (
|
||||
|
||||
@@ -39,7 +39,7 @@ const useKeyHandler = (
|
||||
focusTextbox: (forceFocus?: boolean) => void,
|
||||
applyMarkdown: (params: ApplyMarkdownOptions) => void,
|
||||
handleDraftChange: (draft: PostDraft, options?: {instant?: boolean; show?: boolean}) => void,
|
||||
handleSubmit: (e: React.FormEvent, submittingDraft?: PostDraft) => void,
|
||||
handleSubmit: (submittingDraft?: PostDraft) => void,
|
||||
emitTypingEvent: () => void,
|
||||
toggleShowPreview: () => void,
|
||||
toggleAdvanceTextEditor: () => void,
|
||||
@@ -124,19 +124,9 @@ const useKeyHandler = (
|
||||
}
|
||||
|
||||
if (allowSending && isValidPersistentNotifications) {
|
||||
e.persist?.();
|
||||
|
||||
// textboxRef.current?.blur();
|
||||
|
||||
if (withClosedCodeBlock && message) {
|
||||
handleSubmit(e, {...draft, message});
|
||||
} else {
|
||||
handleSubmit(e);
|
||||
}
|
||||
|
||||
// setTimeout(() => {
|
||||
// focusTextbox();
|
||||
// });
|
||||
e.preventDefault();
|
||||
const updatedDraft = (withClosedCodeBlock && message) ? {...draft, message} : undefined;
|
||||
handleSubmit(updatedDraft);
|
||||
}
|
||||
|
||||
emitTypingEvent();
|
||||
|
||||
@@ -16,7 +16,7 @@ import {haveIChannelPermission} from 'mattermost-redux/selectors/entities/roles'
|
||||
import {getCurrentUserId, getStatusForUserId} from 'mattermost-redux/selectors/entities/users';
|
||||
|
||||
import {scrollPostListToBottom} from 'actions/views/channel';
|
||||
import type {SubmitPostReturnType} from 'actions/views/create_comment';
|
||||
import type {OnSubmitOptions, SubmitPostReturnType} from 'actions/views/create_comment';
|
||||
import {onSubmit} from 'actions/views/create_comment';
|
||||
import {openModal} from 'actions/views/modals';
|
||||
|
||||
@@ -57,13 +57,14 @@ const useSubmit = (
|
||||
lastBlurAt: React.MutableRefObject<number>,
|
||||
focusTextbox: (forceFocust?: boolean) => void,
|
||||
setServerError: (err: (ServerError & { submittedMessage?: string }) | null) => void,
|
||||
setPostError: (err: React.ReactNode) => void,
|
||||
setShowPreview: (showPreview: boolean) => void,
|
||||
handleDraftChange: (draft: PostDraft, options?: {instant?: boolean; show?: boolean}) => void,
|
||||
prioritySubmitCheck: (onConfirm: () => void) => boolean,
|
||||
afterOptimisticSubmit?: () => void,
|
||||
afterSubmit?: (response: SubmitPostReturnType) => void,
|
||||
skipCommands?: boolean,
|
||||
): [
|
||||
(e: React.FormEvent, submittingDraft?: PostDraft) => void,
|
||||
(submittingDraft?: PostDraft) => Promise<void>,
|
||||
string | null,
|
||||
] => {
|
||||
const getGroupMentions = useGroups(channelId, draft.message);
|
||||
@@ -117,9 +118,7 @@ const useSubmit = (
|
||||
}));
|
||||
}, [dispatch]);
|
||||
|
||||
const doSubmit = useCallback(async (e?: React.FormEvent, submittingDraft = draft) => {
|
||||
e?.preventDefault();
|
||||
|
||||
const doSubmit = useCallback(async (submittingDraft = draft) => {
|
||||
if (submittingDraft.uploadsInProgress.length > 0) {
|
||||
isDraftSubmitting.current = false;
|
||||
return;
|
||||
@@ -135,6 +134,7 @@ const useSubmit = (
|
||||
}
|
||||
|
||||
if (submittingDraft.message.trim().length === 0 && submittingDraft.fileInfos.length === 0) {
|
||||
isDraftSubmitting.current = false;
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -145,6 +145,7 @@ const useSubmit = (
|
||||
}
|
||||
|
||||
if (serverError && !isErrorInvalidSlashCommand(serverError)) {
|
||||
isDraftSubmitting.current = false;
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -154,13 +155,15 @@ const useSubmit = (
|
||||
|
||||
setServerError(null);
|
||||
|
||||
const ignoreSlash = isErrorInvalidSlashCommand(serverError) && serverError?.submittedMessage === submittingDraft.message;
|
||||
const options = {ignoreSlash, afterSubmit};
|
||||
const ignoreSlash = skipCommands || (isErrorInvalidSlashCommand(serverError) && serverError?.submittedMessage === submittingDraft.message);
|
||||
const options: OnSubmitOptions = {ignoreSlash, afterSubmit, afterOptimisticSubmit};
|
||||
|
||||
try {
|
||||
await dispatch(onSubmit(submittingDraft, options));
|
||||
const res = await dispatch(onSubmit(submittingDraft, options));
|
||||
if (res.error) {
|
||||
throw res.error;
|
||||
}
|
||||
|
||||
setPostError(null);
|
||||
setServerError(null);
|
||||
handleDraftChange({
|
||||
message: '',
|
||||
@@ -180,6 +183,8 @@ const useSubmit = (
|
||||
...err,
|
||||
submittedMessage: submittingDraft.message,
|
||||
});
|
||||
} else {
|
||||
setServerError(err as any);
|
||||
}
|
||||
isDraftSubmitting.current = false;
|
||||
return;
|
||||
@@ -190,7 +195,22 @@ const useSubmit = (
|
||||
}
|
||||
|
||||
isDraftSubmitting.current = false;
|
||||
}, [handleDraftChange, dispatch, draft, focusTextbox, isRootDeleted, postError, serverError, showPostDeletedModal, channelId, postId, lastBlurAt, setPostError, setServerError, afterSubmit]);
|
||||
}, [draft,
|
||||
postError,
|
||||
isRootDeleted,
|
||||
serverError,
|
||||
lastBlurAt,
|
||||
focusTextbox,
|
||||
setServerError,
|
||||
skipCommands,
|
||||
afterSubmit,
|
||||
afterOptimisticSubmit,
|
||||
postId,
|
||||
showPostDeletedModal,
|
||||
dispatch,
|
||||
handleDraftChange,
|
||||
channelId,
|
||||
]);
|
||||
|
||||
const showNotifyAllModal = useCallback((mentions: string[], channelTimezoneCount: number, memberNotifyCount: number) => {
|
||||
dispatch(openModal({
|
||||
@@ -205,11 +225,15 @@ const useSubmit = (
|
||||
}));
|
||||
}, [doSubmit, dispatch]);
|
||||
|
||||
const handleSubmit = useCallback(async (e: React.FormEvent, submittingDraft = draft) => {
|
||||
const handleSubmit = useCallback(async (submittingDraft = draft) => {
|
||||
if (!channel) {
|
||||
return;
|
||||
}
|
||||
e.preventDefault();
|
||||
|
||||
if (isDraftSubmitting.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
setShowPreview(false);
|
||||
isDraftSubmitting.current = true;
|
||||
|
||||
@@ -249,59 +273,61 @@ const useSubmit = (
|
||||
return;
|
||||
}
|
||||
|
||||
const status = getStatusFromSlashCommand(submittingDraft.message);
|
||||
if (userIsOutOfOffice && status) {
|
||||
const resetStatusModalData = {
|
||||
modalId: ModalIdentifiers.RESET_STATUS,
|
||||
dialogType: ResetStatusModal,
|
||||
dialogProps: {newStatus: status},
|
||||
};
|
||||
if (!skipCommands) {
|
||||
const status = getStatusFromSlashCommand(submittingDraft.message);
|
||||
if (userIsOutOfOffice && status) {
|
||||
const resetStatusModalData = {
|
||||
modalId: ModalIdentifiers.RESET_STATUS,
|
||||
dialogType: ResetStatusModal,
|
||||
dialogProps: {newStatus: status},
|
||||
};
|
||||
|
||||
dispatch(openModal(resetStatusModalData));
|
||||
dispatch(openModal(resetStatusModalData));
|
||||
|
||||
handleDraftChange({
|
||||
...submittingDraft,
|
||||
message: '',
|
||||
});
|
||||
isDraftSubmitting.current = false;
|
||||
return;
|
||||
handleDraftChange({
|
||||
...submittingDraft,
|
||||
message: '',
|
||||
});
|
||||
isDraftSubmitting.current = false;
|
||||
return;
|
||||
}
|
||||
|
||||
if (submittingDraft.message.trimEnd() === '/header') {
|
||||
const editChannelHeaderModalData = {
|
||||
modalId: ModalIdentifiers.EDIT_CHANNEL_HEADER,
|
||||
dialogType: EditChannelHeaderModal,
|
||||
dialogProps: {channel},
|
||||
};
|
||||
|
||||
dispatch(openModal(editChannelHeaderModalData));
|
||||
|
||||
handleDraftChange({
|
||||
...submittingDraft,
|
||||
message: '',
|
||||
});
|
||||
isDraftSubmitting.current = false;
|
||||
return;
|
||||
}
|
||||
|
||||
if (!isDirectOrGroup && submittingDraft.message.trimEnd() === '/purpose') {
|
||||
const editChannelPurposeModalData = {
|
||||
modalId: ModalIdentifiers.EDIT_CHANNEL_PURPOSE,
|
||||
dialogType: EditChannelPurposeModal,
|
||||
dialogProps: {channel},
|
||||
};
|
||||
|
||||
dispatch(openModal(editChannelPurposeModalData));
|
||||
|
||||
handleDraftChange({
|
||||
...submittingDraft,
|
||||
message: '',
|
||||
});
|
||||
isDraftSubmitting.current = false;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (submittingDraft.message.trimEnd() === '/header') {
|
||||
const editChannelHeaderModalData = {
|
||||
modalId: ModalIdentifiers.EDIT_CHANNEL_HEADER,
|
||||
dialogType: EditChannelHeaderModal,
|
||||
dialogProps: {channel},
|
||||
};
|
||||
|
||||
dispatch(openModal(editChannelHeaderModalData));
|
||||
|
||||
handleDraftChange({
|
||||
...submittingDraft,
|
||||
message: '',
|
||||
});
|
||||
isDraftSubmitting.current = false;
|
||||
return;
|
||||
}
|
||||
|
||||
if (!isDirectOrGroup && submittingDraft.message.trimEnd() === '/purpose') {
|
||||
const editChannelPurposeModalData = {
|
||||
modalId: ModalIdentifiers.EDIT_CHANNEL_PURPOSE,
|
||||
dialogType: EditChannelPurposeModal,
|
||||
dialogProps: {channel},
|
||||
};
|
||||
|
||||
dispatch(openModal(editChannelPurposeModalData));
|
||||
|
||||
handleDraftChange({
|
||||
...submittingDraft,
|
||||
message: '',
|
||||
});
|
||||
isDraftSubmitting.current = false;
|
||||
return;
|
||||
}
|
||||
|
||||
await doSubmit(e, submittingDraft);
|
||||
await doSubmit(submittingDraft);
|
||||
}, [
|
||||
doSubmit,
|
||||
draft,
|
||||
@@ -311,6 +337,7 @@ const useSubmit = (
|
||||
channelMembersCount,
|
||||
dispatch,
|
||||
enableConfirmNotificationsToChannel,
|
||||
skipCommands,
|
||||
handleDraftChange,
|
||||
showNotifyAllModal,
|
||||
useChannelMentions,
|
||||
|
||||
@@ -1,101 +0,0 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`components/drafts/drafts_row should match snapshot for channel draft 1`] = `
|
||||
<ContextProvider
|
||||
value={
|
||||
Object {
|
||||
"store": Object {
|
||||
"clearActions": [Function],
|
||||
"dispatch": [Function],
|
||||
"getActions": [Function],
|
||||
"getState": [Function],
|
||||
"replaceReducer": [Function],
|
||||
"subscribe": [Function],
|
||||
},
|
||||
"subscription": Subscription {
|
||||
"handleChangeWrapper": [Function],
|
||||
"listeners": Object {
|
||||
"notify": [Function],
|
||||
},
|
||||
"onStateChange": [Function],
|
||||
"parentSub": undefined,
|
||||
"store": Object {
|
||||
"clearActions": [Function],
|
||||
"dispatch": [Function],
|
||||
"getActions": [Function],
|
||||
"getState": [Function],
|
||||
"replaceReducer": [Function],
|
||||
"subscribe": [Function],
|
||||
},
|
||||
"unsubscribe": null,
|
||||
},
|
||||
}
|
||||
}
|
||||
>
|
||||
<Memo(ChannelDraft)
|
||||
channel={
|
||||
Object {
|
||||
"id": "",
|
||||
}
|
||||
}
|
||||
channelUrl=""
|
||||
displayName=""
|
||||
draftId=""
|
||||
id={Object {}}
|
||||
isRemote={false}
|
||||
postPriorityEnabled={false}
|
||||
status={Object {}}
|
||||
type="channel"
|
||||
user={Object {}}
|
||||
value={Object {}}
|
||||
/>
|
||||
</ContextProvider>
|
||||
`;
|
||||
|
||||
exports[`components/drafts/drafts_row should match snapshot for undefined channel 1`] = `
|
||||
<ContextProvider
|
||||
value={
|
||||
Object {
|
||||
"store": Object {
|
||||
"clearActions": [Function],
|
||||
"dispatch": [Function],
|
||||
"getActions": [Function],
|
||||
"getState": [Function],
|
||||
"replaceReducer": [Function],
|
||||
"subscribe": [Function],
|
||||
},
|
||||
"subscription": Subscription {
|
||||
"handleChangeWrapper": [Function],
|
||||
"listeners": Object {
|
||||
"notify": [Function],
|
||||
},
|
||||
"onStateChange": [Function],
|
||||
"parentSub": undefined,
|
||||
"store": Object {
|
||||
"clearActions": [Function],
|
||||
"dispatch": [Function],
|
||||
"getActions": [Function],
|
||||
"getState": [Function],
|
||||
"replaceReducer": [Function],
|
||||
"subscribe": [Function],
|
||||
},
|
||||
"unsubscribe": null,
|
||||
},
|
||||
}
|
||||
}
|
||||
>
|
||||
<Memo(ChannelDraft)
|
||||
channel={null}
|
||||
channelUrl=""
|
||||
displayName=""
|
||||
draftId=""
|
||||
id={Object {}}
|
||||
isRemote={false}
|
||||
postPriorityEnabled={false}
|
||||
status={Object {}}
|
||||
type="channel"
|
||||
user={Object {}}
|
||||
value={Object {}}
|
||||
/>
|
||||
</ContextProvider>
|
||||
`;
|
||||
@@ -1,64 +0,0 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {shallow} from 'enzyme';
|
||||
import React from 'react';
|
||||
import {Provider} from 'react-redux';
|
||||
|
||||
import type {Channel} from '@mattermost/types/channels';
|
||||
import type {UserProfile, UserStatus} from '@mattermost/types/users';
|
||||
|
||||
import mockStore from 'tests/test_store';
|
||||
|
||||
import type {PostDraft} from 'types/store/draft';
|
||||
|
||||
import ChannelDraft from './channel_draft';
|
||||
|
||||
describe('components/drafts/drafts_row', () => {
|
||||
const baseProps = {
|
||||
channel: {
|
||||
id: '',
|
||||
} as Channel,
|
||||
channelUrl: '',
|
||||
displayName: '',
|
||||
draftId: '',
|
||||
id: {} as Channel['id'],
|
||||
status: {} as UserStatus['status'],
|
||||
type: 'channel' as 'channel' | 'thread',
|
||||
user: {} as UserProfile,
|
||||
value: {} as PostDraft,
|
||||
postPriorityEnabled: false,
|
||||
isRemote: false,
|
||||
};
|
||||
|
||||
it('should match snapshot for channel draft', () => {
|
||||
const store = mockStore();
|
||||
|
||||
const wrapper = shallow(
|
||||
<Provider store={store}>
|
||||
<ChannelDraft
|
||||
{...baseProps}
|
||||
/>
|
||||
</Provider>,
|
||||
);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('should match snapshot for undefined channel', () => {
|
||||
const store = mockStore();
|
||||
|
||||
const props = {
|
||||
...baseProps,
|
||||
channel: null as unknown as Channel,
|
||||
};
|
||||
|
||||
const wrapper = shallow(
|
||||
<Provider store={store}>
|
||||
<ChannelDraft
|
||||
{...props}
|
||||
/>
|
||||
</Provider>,
|
||||
);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
@@ -1,159 +0,0 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React, {memo, useCallback} from 'react';
|
||||
import {useDispatch} from 'react-redux';
|
||||
import {useHistory} from 'react-router-dom';
|
||||
|
||||
import type {Channel} from '@mattermost/types/channels';
|
||||
import type {Post, PostMetadata} from '@mattermost/types/posts';
|
||||
import type {UserProfile, UserStatus} from '@mattermost/types/users';
|
||||
|
||||
import {createPost} from 'actions/post_actions';
|
||||
import {removeDraft} from 'actions/views/drafts';
|
||||
import {openModal} from 'actions/views/modals';
|
||||
|
||||
import PersistNotificationConfirmModal from 'components/persist_notification_confirm_modal';
|
||||
|
||||
import {ModalIdentifiers} from 'utils/constants';
|
||||
import {hasRequestedPersistentNotifications, specialMentionsInText} from 'utils/post_utils';
|
||||
|
||||
import type {PostDraft} from 'types/store/draft';
|
||||
|
||||
import DraftActions from '../draft_actions';
|
||||
import DraftTitle from '../draft_title';
|
||||
import Panel from '../panel/panel';
|
||||
import PanelBody from '../panel/panel_body';
|
||||
import Header from '../panel/panel_header';
|
||||
|
||||
type Props = {
|
||||
channel?: Channel;
|
||||
channelUrl: string;
|
||||
displayName: string;
|
||||
draftId: string;
|
||||
id: Channel['id'];
|
||||
postPriorityEnabled: boolean;
|
||||
status: UserStatus['status'];
|
||||
type: 'channel' | 'thread';
|
||||
user: UserProfile;
|
||||
value: PostDraft;
|
||||
isRemote?: boolean;
|
||||
}
|
||||
|
||||
function ChannelDraft({
|
||||
channel,
|
||||
channelUrl,
|
||||
displayName,
|
||||
draftId,
|
||||
postPriorityEnabled,
|
||||
status,
|
||||
type,
|
||||
user,
|
||||
value,
|
||||
isRemote,
|
||||
id: channelId,
|
||||
}: Props) {
|
||||
const dispatch = useDispatch();
|
||||
const history = useHistory();
|
||||
|
||||
const handleOnEdit = useCallback(() => {
|
||||
history.push(channelUrl);
|
||||
}, [history, channelUrl]);
|
||||
|
||||
const handleOnDelete = useCallback((id: string) => {
|
||||
dispatch(removeDraft(id, channelId));
|
||||
}, [dispatch, channelId]);
|
||||
|
||||
const doSubmit = useCallback((id: string, post: Post) => {
|
||||
dispatch(createPost(post, value.fileInfos));
|
||||
dispatch(removeDraft(id, channelId));
|
||||
history.push(channelUrl);
|
||||
}, [dispatch, history, value.fileInfos, channelId, channelUrl]);
|
||||
|
||||
const showPersistNotificationModal = useCallback((id: string, post: Post) => {
|
||||
if (!channel) {
|
||||
return;
|
||||
}
|
||||
|
||||
dispatch(openModal({
|
||||
modalId: ModalIdentifiers.PERSIST_NOTIFICATION_CONFIRM_MODAL,
|
||||
dialogType: PersistNotificationConfirmModal,
|
||||
dialogProps: {
|
||||
message: post.message,
|
||||
channelType: channel.type,
|
||||
specialMentions: specialMentionsInText(post.message),
|
||||
onConfirm: () => doSubmit(id, post),
|
||||
},
|
||||
}));
|
||||
}, [channel, dispatch, doSubmit]);
|
||||
|
||||
const handleOnSend = useCallback(async (id: string) => {
|
||||
const post = {} as Post;
|
||||
post.file_ids = [];
|
||||
post.message = value.message;
|
||||
post.props = value.props || {};
|
||||
post.user_id = user.id;
|
||||
post.channel_id = value.channelId;
|
||||
post.metadata = (value.metadata || {}) as PostMetadata;
|
||||
|
||||
if (post.message.trim().length === 0 && value.fileInfos.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (postPriorityEnabled && hasRequestedPersistentNotifications(value?.metadata?.priority)) {
|
||||
showPersistNotificationModal(id, post);
|
||||
return;
|
||||
}
|
||||
doSubmit(id, post);
|
||||
}, [doSubmit, postPriorityEnabled, value, user.id, showPersistNotificationModal]);
|
||||
|
||||
if (!channel) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Panel onClick={handleOnEdit}>
|
||||
{({hover}) => (
|
||||
<>
|
||||
<Header
|
||||
hover={hover}
|
||||
actions={(
|
||||
<DraftActions
|
||||
channelDisplayName={channel.display_name}
|
||||
channelType={channel.type}
|
||||
channelName={channel.name}
|
||||
userId={user.id}
|
||||
draftId={draftId}
|
||||
onDelete={handleOnDelete}
|
||||
onEdit={handleOnEdit}
|
||||
onSend={handleOnSend}
|
||||
/>
|
||||
)}
|
||||
title={(
|
||||
<DraftTitle
|
||||
channel={channel}
|
||||
type={type}
|
||||
userId={user.id}
|
||||
/>
|
||||
)}
|
||||
timestamp={value.updateAt}
|
||||
remote={isRemote || false}
|
||||
/>
|
||||
<PanelBody
|
||||
channelId={channelId}
|
||||
displayName={displayName}
|
||||
fileInfos={value.fileInfos}
|
||||
message={value.message}
|
||||
status={status}
|
||||
priority={value.metadata?.priority}
|
||||
uploadsInProgress={value.uploadsInProgress}
|
||||
userId={user.id}
|
||||
username={user.username}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</Panel>
|
||||
);
|
||||
}
|
||||
|
||||
export default memo(ChannelDraft);
|
||||
@@ -1,36 +0,0 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {connect} from 'react-redux';
|
||||
|
||||
import {makeGetChannel} from 'mattermost-redux/selectors/entities/channels';
|
||||
import {isPostPriorityEnabled} from 'mattermost-redux/selectors/entities/posts';
|
||||
import {getCurrentTeamId} from 'mattermost-redux/selectors/entities/teams';
|
||||
|
||||
import {getChannelURL} from 'selectors/urls';
|
||||
|
||||
import type {GlobalState} from 'types/store';
|
||||
|
||||
import ChannelDraft from './channel_draft';
|
||||
|
||||
type OwnProps = {
|
||||
id: string;
|
||||
}
|
||||
|
||||
function makeMapStateToProps() {
|
||||
const getChannel = makeGetChannel();
|
||||
|
||||
return (state: GlobalState, ownProps: OwnProps) => {
|
||||
const channel = getChannel(state, ownProps);
|
||||
|
||||
const teamId = getCurrentTeamId(state);
|
||||
const channelUrl = channel ? getChannelURL(state, channel, teamId) : '';
|
||||
|
||||
return {
|
||||
channel,
|
||||
channelUrl,
|
||||
postPriorityEnabled: isPostPriorityEnabled(state),
|
||||
};
|
||||
};
|
||||
}
|
||||
export default connect(makeMapStateToProps)(ChannelDraft);
|
||||
@@ -11,11 +11,11 @@ exports[`components/drafts/draft_actions/action should match snapshot 1`] = `
|
||||
>
|
||||
<button
|
||||
className="DraftAction__button"
|
||||
id="draft_{icon}_"
|
||||
id="draft__"
|
||||
onClick={[MockFunction]}
|
||||
>
|
||||
<i
|
||||
className="icon "
|
||||
className="icon"
|
||||
/>
|
||||
</button>
|
||||
</WithTooltip>
|
||||
|
||||
@@ -33,6 +33,8 @@ exports[`components/drafts/draft_actions should match snapshot 1`] = `
|
||||
}
|
||||
>
|
||||
<Memo(DraftActions)
|
||||
canEdit={true}
|
||||
canSend={true}
|
||||
displayName=""
|
||||
draftId=""
|
||||
onDelete={[MockFunction]}
|
||||
|
||||
@@ -4,9 +4,10 @@
|
||||
import classNames from 'classnames';
|
||||
import React from 'react';
|
||||
|
||||
import './action.scss';
|
||||
import WithTooltip from 'components/with_tooltip';
|
||||
|
||||
import './action.scss';
|
||||
|
||||
type Props = {
|
||||
icon: string;
|
||||
id: string;
|
||||
@@ -15,7 +16,13 @@ type Props = {
|
||||
tooltipText: React.ReactElement | string;
|
||||
};
|
||||
|
||||
function Action({name, icon, onClick, id, tooltipText}: Props) {
|
||||
function Action({
|
||||
name,
|
||||
icon,
|
||||
onClick,
|
||||
id,
|
||||
tooltipText,
|
||||
}: Props) {
|
||||
return (
|
||||
<div className='DraftAction'>
|
||||
<WithTooltip
|
||||
@@ -28,10 +35,15 @@ function Action({name, icon, onClick, id, tooltipText}: Props) {
|
||||
'DraftAction__button',
|
||||
{'DraftAction__button--delete': name === 'delete'},
|
||||
)}
|
||||
id={`draft_{icon}_${id}`}
|
||||
id={`draft_${icon}_${id}`}
|
||||
onClick={onClick}
|
||||
>
|
||||
<i className={`icon ${icon}`}/>
|
||||
<i
|
||||
className={classNames(
|
||||
'icon',
|
||||
icon,
|
||||
)}
|
||||
/>
|
||||
</button>
|
||||
</WithTooltip>
|
||||
</div>
|
||||
|
||||
@@ -16,6 +16,8 @@ describe('components/drafts/draft_actions', () => {
|
||||
onDelete: jest.fn(),
|
||||
onEdit: jest.fn(),
|
||||
onSend: jest.fn(),
|
||||
canSend: true,
|
||||
canEdit: true,
|
||||
};
|
||||
|
||||
it('should match snapshot', () => {
|
||||
|
||||
@@ -15,18 +15,20 @@ import SendDraftModal from './send_draft_modal';
|
||||
|
||||
type Props = {
|
||||
displayName: string;
|
||||
draftId: string;
|
||||
onDelete: (draftId: string) => void;
|
||||
onDelete: () => void;
|
||||
onEdit: () => void;
|
||||
onSend: (draftId: string) => void;
|
||||
onSend: () => void;
|
||||
canEdit: boolean;
|
||||
canSend: boolean;
|
||||
}
|
||||
|
||||
function DraftActions({
|
||||
displayName,
|
||||
draftId,
|
||||
onDelete,
|
||||
onEdit,
|
||||
onSend,
|
||||
canEdit,
|
||||
canSend,
|
||||
}: Props) {
|
||||
const dispatch = useDispatch();
|
||||
|
||||
@@ -36,10 +38,10 @@ function DraftActions({
|
||||
dialogType: DeleteDraftModal,
|
||||
dialogProps: {
|
||||
displayName,
|
||||
onConfirm: () => onDelete(draftId),
|
||||
onConfirm: onDelete,
|
||||
},
|
||||
}));
|
||||
}, [displayName]);
|
||||
}, [dispatch, displayName, onDelete]);
|
||||
|
||||
const handleSend = useCallback(() => {
|
||||
dispatch(openModal({
|
||||
@@ -47,10 +49,10 @@ function DraftActions({
|
||||
dialogType: SendDraftModal,
|
||||
dialogProps: {
|
||||
displayName,
|
||||
onConfirm: () => onSend(draftId),
|
||||
onConfirm: onSend,
|
||||
},
|
||||
}));
|
||||
}, [displayName]);
|
||||
}, [dispatch, displayName, onSend]);
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -66,30 +68,34 @@ function DraftActions({
|
||||
)}
|
||||
onClick={handleDelete}
|
||||
/>
|
||||
<Action
|
||||
icon='icon-pencil-outline'
|
||||
id='edit'
|
||||
name='edit'
|
||||
tooltipText={(
|
||||
<FormattedMessage
|
||||
id='drafts.actions.edit'
|
||||
defaultMessage='Edit draft'
|
||||
/>
|
||||
)}
|
||||
onClick={onEdit}
|
||||
/>
|
||||
<Action
|
||||
icon='icon-send-outline'
|
||||
id='send'
|
||||
name='send'
|
||||
tooltipText={(
|
||||
<FormattedMessage
|
||||
id='drafts.actions.send'
|
||||
defaultMessage='Send draft'
|
||||
/>
|
||||
)}
|
||||
onClick={handleSend}
|
||||
/>
|
||||
{canEdit && (
|
||||
<Action
|
||||
icon='icon-pencil-outline'
|
||||
id='edit'
|
||||
name='edit'
|
||||
tooltipText={(
|
||||
<FormattedMessage
|
||||
id='drafts.actions.edit'
|
||||
defaultMessage='Edit draft'
|
||||
/>
|
||||
)}
|
||||
onClick={onEdit}
|
||||
/>
|
||||
)}
|
||||
{canSend && (
|
||||
<Action
|
||||
icon='icon-send-outline'
|
||||
id='send'
|
||||
name='send'
|
||||
tooltipText={(
|
||||
<FormattedMessage
|
||||
id='drafts.actions.send'
|
||||
defaultMessage='Send draft'
|
||||
/>
|
||||
)}
|
||||
onClick={handleSend}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,14 +1,41 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React, {memo} from 'react';
|
||||
import noop from 'lodash/noop';
|
||||
import React, {memo, useCallback, useMemo, useEffect, useState} from 'react';
|
||||
import {useIntl} from 'react-intl';
|
||||
import {useDispatch, useSelector} from 'react-redux';
|
||||
import {useHistory} from 'react-router-dom';
|
||||
|
||||
import type {ServerError} from '@mattermost/types/errors';
|
||||
import type {UserProfile, UserStatus} from '@mattermost/types/users';
|
||||
|
||||
import type {Draft} from 'selectors/drafts';
|
||||
import {getPost as getPostAction} from 'mattermost-redux/actions/posts';
|
||||
import {Permissions} from 'mattermost-redux/constants';
|
||||
import {makeGetChannel} from 'mattermost-redux/selectors/entities/channels';
|
||||
import {getConfig} from 'mattermost-redux/selectors/entities/general';
|
||||
import {getPost} from 'mattermost-redux/selectors/entities/posts';
|
||||
import {haveIChannelPermission} from 'mattermost-redux/selectors/entities/roles';
|
||||
import {getCurrentTeamId} from 'mattermost-redux/selectors/entities/teams';
|
||||
import {makeGetThreadOrSynthetic} from 'mattermost-redux/selectors/entities/threads';
|
||||
|
||||
import ChannelDraft from './channel_draft';
|
||||
import ThreadDraft from './thread_draft';
|
||||
import {removeDraft} from 'actions/views/drafts';
|
||||
import {selectPostById} from 'actions/views/rhs';
|
||||
import type {Draft} from 'selectors/drafts';
|
||||
import {getChannelURL} from 'selectors/urls';
|
||||
|
||||
import usePriority from 'components/advanced_text_editor/use_priority';
|
||||
import useSubmit from 'components/advanced_text_editor/use_submit';
|
||||
|
||||
import Constants, {StoragePrefixes} from 'utils/constants';
|
||||
|
||||
import type {GlobalState} from 'types/store';
|
||||
|
||||
import DraftActions from './draft_actions';
|
||||
import DraftTitle from './draft_title';
|
||||
import Panel from './panel/panel';
|
||||
import PanelBody from './panel/panel_body';
|
||||
import Header from './panel/panel_header';
|
||||
|
||||
type Props = {
|
||||
user: UserProfile;
|
||||
@@ -18,34 +45,174 @@ type Props = {
|
||||
isRemote?: boolean;
|
||||
}
|
||||
|
||||
function DraftRow({draft, user, status, displayName, isRemote}: Props) {
|
||||
switch (draft.type) {
|
||||
case 'channel':
|
||||
return (
|
||||
<ChannelDraft
|
||||
{...draft}
|
||||
draftId={String(draft.key)}
|
||||
user={user}
|
||||
status={status}
|
||||
displayName={displayName}
|
||||
isRemote={isRemote}
|
||||
/>
|
||||
);
|
||||
case 'thread':
|
||||
return (
|
||||
<ThreadDraft
|
||||
{...draft}
|
||||
rootId={draft.id}
|
||||
draftId={String(draft.key)}
|
||||
user={user}
|
||||
status={status}
|
||||
displayName={displayName}
|
||||
isRemote={isRemote}
|
||||
/>
|
||||
);
|
||||
default:
|
||||
const mockLastBlurAt = {current: 0};
|
||||
|
||||
function DraftRow({
|
||||
draft,
|
||||
user,
|
||||
status,
|
||||
displayName,
|
||||
isRemote,
|
||||
}: Props) {
|
||||
const intl = useIntl();
|
||||
|
||||
const rootId = draft.value.rootId;
|
||||
const channelId = draft.value.channelId;
|
||||
|
||||
const [serverError, setServerError] = useState<(ServerError & { submittedMessage?: string }) | null>(null);
|
||||
|
||||
const history = useHistory();
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const getChannel = useMemo(() => makeGetChannel(), []);
|
||||
const getThreadOrSynthetic = useMemo(() => makeGetThreadOrSynthetic(), []);
|
||||
|
||||
const rootPostDeleted = useSelector((state: GlobalState) => {
|
||||
if (!rootId) {
|
||||
return false;
|
||||
}
|
||||
const rootPost = getPost(state, rootId);
|
||||
return !rootPost || rootPost.delete_at > 0 || rootPost.state === 'DELETED';
|
||||
});
|
||||
|
||||
const tooLong = useSelector((state: GlobalState) => {
|
||||
const maxPostSize = parseInt(getConfig(state).MaxPostSize || '', 10) || Constants.DEFAULT_CHARACTER_LIMIT;
|
||||
return draft.value.message.length > maxPostSize;
|
||||
});
|
||||
|
||||
const readOnly = !useSelector((state: GlobalState) => {
|
||||
const channel = getChannel(state, channelId);
|
||||
return channel ? haveIChannelPermission(state, channel.team_id, channel.id, Permissions.CREATE_POST) : false;
|
||||
});
|
||||
|
||||
let postError = '';
|
||||
if (rootPostDeleted) {
|
||||
postError = intl.formatMessage({id: 'drafts.error.post_not_found', defaultMessage: 'Thread not found'});
|
||||
} else if (tooLong) {
|
||||
postError = intl.formatMessage({id: 'drafts.error.too_long', defaultMessage: 'Message too long'});
|
||||
} else if (readOnly) {
|
||||
postError = intl.formatMessage({id: 'drafts.error.read_only', defaultMessage: 'Channel is read only'});
|
||||
}
|
||||
|
||||
const canSend = !postError;
|
||||
const canEdit = !(rootPostDeleted || readOnly);
|
||||
|
||||
const channel = useSelector((state: GlobalState) => getChannel(state, channelId));
|
||||
const channelUrl = useSelector((state: GlobalState) => {
|
||||
if (!channel) {
|
||||
return '';
|
||||
}
|
||||
|
||||
const teamId = getCurrentTeamId(state);
|
||||
return getChannelURL(state, channel, teamId);
|
||||
});
|
||||
|
||||
const goToMessage = useCallback(async () => {
|
||||
if (rootId) {
|
||||
if (rootPostDeleted) {
|
||||
return;
|
||||
}
|
||||
await dispatch(selectPostById(rootId));
|
||||
return;
|
||||
}
|
||||
history.push(channelUrl);
|
||||
}, [channelUrl, dispatch, history, rootId, rootPostDeleted]);
|
||||
|
||||
const {onSubmitCheck: prioritySubmitCheck} = usePriority(draft.value, noop, noop, false);
|
||||
const [handleOnSend] = useSubmit(
|
||||
draft.value,
|
||||
postError,
|
||||
channelId,
|
||||
rootId,
|
||||
serverError,
|
||||
mockLastBlurAt,
|
||||
noop,
|
||||
setServerError,
|
||||
noop,
|
||||
noop,
|
||||
prioritySubmitCheck,
|
||||
goToMessage,
|
||||
undefined,
|
||||
true,
|
||||
);
|
||||
|
||||
const thread = useSelector((state: GlobalState) => {
|
||||
if (!rootId) {
|
||||
return undefined;
|
||||
}
|
||||
const post = getPost(state, rootId);
|
||||
if (!post) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return getThreadOrSynthetic(state, post);
|
||||
});
|
||||
|
||||
const handleOnDelete = useCallback(() => {
|
||||
let key = `${StoragePrefixes.DRAFT}${channelId}`;
|
||||
if (rootId) {
|
||||
key = `${StoragePrefixes.COMMENT_DRAFT}${rootId}`;
|
||||
}
|
||||
dispatch(removeDraft(key, channelId, rootId));
|
||||
}, [dispatch, channelId, rootId]);
|
||||
|
||||
useEffect(() => {
|
||||
if (rootId && !thread?.id) {
|
||||
dispatch(getPostAction(rootId));
|
||||
}
|
||||
}, [thread?.id]);
|
||||
|
||||
if (!channel) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Panel
|
||||
onClick={goToMessage}
|
||||
hasError={Boolean(postError)}
|
||||
>
|
||||
{({hover}) => (
|
||||
<>
|
||||
<Header
|
||||
hover={hover}
|
||||
actions={(
|
||||
<DraftActions
|
||||
channelDisplayName={channel.display_name}
|
||||
channelName={channel.name}
|
||||
channelType={channel.type}
|
||||
userId={user.id}
|
||||
onDelete={handleOnDelete}
|
||||
onEdit={goToMessage}
|
||||
onSend={handleOnSend}
|
||||
canEdit={canEdit}
|
||||
canSend={canSend}
|
||||
/>
|
||||
)}
|
||||
title={(
|
||||
<DraftTitle
|
||||
type={draft.type}
|
||||
channel={channel}
|
||||
userId={user.id}
|
||||
/>
|
||||
)}
|
||||
timestamp={draft.value.updateAt}
|
||||
remote={isRemote || false}
|
||||
error={postError || serverError?.message}
|
||||
/>
|
||||
<PanelBody
|
||||
channelId={channel.id}
|
||||
displayName={displayName}
|
||||
fileInfos={draft.value.fileInfos}
|
||||
message={draft.value.message}
|
||||
status={status}
|
||||
uploadsInProgress={draft.value.uploadsInProgress}
|
||||
userId={user.id}
|
||||
username={user.username}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</Panel>
|
||||
);
|
||||
}
|
||||
|
||||
export default memo(DraftRow);
|
||||
|
||||
@@ -18,4 +18,8 @@
|
||||
box-shadow: var(--elevation-2);
|
||||
transition-duration: 0.15s;
|
||||
}
|
||||
|
||||
&.draftError {
|
||||
border-color: var(--error-text);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ describe('components/drafts/panel/', () => {
|
||||
const baseProps = {
|
||||
children: jest.fn(),
|
||||
onClick: jest.fn(),
|
||||
hasError: false,
|
||||
};
|
||||
|
||||
it('should match snapshot', () => {
|
||||
|
||||
@@ -1,19 +1,26 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import classNames from 'classnames';
|
||||
import React, {memo, useState} from 'react';
|
||||
|
||||
import {makeIsEligibleForClick} from 'utils/utils';
|
||||
|
||||
import './panel.scss';
|
||||
|
||||
type Props = {
|
||||
children: ({hover}: {hover: boolean}) => React.ReactNode;
|
||||
onClick: () => void;
|
||||
hasError: boolean;
|
||||
};
|
||||
|
||||
const isEligibleForClick = makeIsEligibleForClick('.hljs, code');
|
||||
|
||||
function Panel({children, onClick}: Props) {
|
||||
function Panel({
|
||||
children,
|
||||
onClick,
|
||||
hasError,
|
||||
}: Props) {
|
||||
const [hover, setHover] = useState(false);
|
||||
|
||||
const handleMouseOver = () => {
|
||||
@@ -32,7 +39,12 @@ function Panel({children, onClick}: Props) {
|
||||
|
||||
return (
|
||||
<article
|
||||
className='Panel'
|
||||
className={classNames(
|
||||
'Panel',
|
||||
{
|
||||
draftError: hasError,
|
||||
},
|
||||
)}
|
||||
onMouseOver={handleMouseOver}
|
||||
onClick={handleOnClick}
|
||||
onMouseLeave={handleMouseLeave}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import type {ComponentProps} from 'react';
|
||||
import React from 'react';
|
||||
import {Provider} from 'react-redux';
|
||||
|
||||
@@ -16,7 +17,7 @@ import type {PostDraft} from 'types/store/draft';
|
||||
import PanelBody from './panel_body';
|
||||
|
||||
describe('components/drafts/panel/panel_body', () => {
|
||||
const baseProps = {
|
||||
const baseProps: ComponentProps<typeof PanelBody> = {
|
||||
channelId: 'channel_id',
|
||||
displayName: 'display_name',
|
||||
fileInfos: [] as PostDraft['fileInfos'],
|
||||
|
||||
@@ -55,7 +55,6 @@ function PanelBody({
|
||||
}, [currentRelativeTeamUrl]);
|
||||
|
||||
return (
|
||||
|
||||
<div className='DraftPanelBody post'>
|
||||
<div className='DraftPanelBody__left post__img'>
|
||||
<ProfilePicture
|
||||
|
||||
@@ -27,9 +27,17 @@ type Props = {
|
||||
timestamp: number;
|
||||
remote: boolean;
|
||||
title: React.ReactNode;
|
||||
error?: string;
|
||||
};
|
||||
|
||||
function PanelHeader({actions, hover, timestamp, remote, title}: Props) {
|
||||
function PanelHeader({
|
||||
actions,
|
||||
hover,
|
||||
timestamp,
|
||||
remote,
|
||||
title,
|
||||
error,
|
||||
}: Props) {
|
||||
return (
|
||||
<header className='PanelHeader'>
|
||||
<div className='PanelHeader__left'>{title}</div>
|
||||
@@ -62,11 +70,21 @@ function PanelHeader({actions, hover, timestamp, remote, title}: Props) {
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<Tag
|
||||
variant={'danger'}
|
||||
uppercase={true}
|
||||
text={'draft'}
|
||||
/>
|
||||
{!error && (
|
||||
<Tag
|
||||
variant={'danger'}
|
||||
uppercase={true}
|
||||
text={'draft'}
|
||||
/>
|
||||
)}
|
||||
{error && (
|
||||
<Tag
|
||||
text={error}
|
||||
variant={'danger'}
|
||||
uppercase={true}
|
||||
icon={'alert-outline'}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
@@ -1,111 +0,0 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`components/drafts/drafts_row should match snapshot for channel draft 1`] = `
|
||||
<ContextProvider
|
||||
value={
|
||||
Object {
|
||||
"store": Object {
|
||||
"clearActions": [Function],
|
||||
"dispatch": [Function],
|
||||
"getActions": [Function],
|
||||
"getState": [Function],
|
||||
"replaceReducer": [Function],
|
||||
"subscribe": [Function],
|
||||
},
|
||||
"subscription": Subscription {
|
||||
"handleChangeWrapper": [Function],
|
||||
"listeners": Object {
|
||||
"notify": [Function],
|
||||
},
|
||||
"onStateChange": [Function],
|
||||
"parentSub": undefined,
|
||||
"store": Object {
|
||||
"clearActions": [Function],
|
||||
"dispatch": [Function],
|
||||
"getActions": [Function],
|
||||
"getState": [Function],
|
||||
"replaceReducer": [Function],
|
||||
"subscribe": [Function],
|
||||
},
|
||||
"unsubscribe": null,
|
||||
},
|
||||
}
|
||||
}
|
||||
>
|
||||
<Memo(ThreadDraft)
|
||||
channel={
|
||||
Object {
|
||||
"id": "",
|
||||
}
|
||||
}
|
||||
channelUrl=""
|
||||
displayName=""
|
||||
draftId=""
|
||||
id={Object {}}
|
||||
isRemote={false}
|
||||
rootId=""
|
||||
status={Object {}}
|
||||
thread={
|
||||
Object {
|
||||
"id": "",
|
||||
}
|
||||
}
|
||||
type="thread"
|
||||
user={Object {}}
|
||||
value={Object {}}
|
||||
/>
|
||||
</ContextProvider>
|
||||
`;
|
||||
|
||||
exports[`components/drafts/drafts_row should match snapshot for undefined thread 1`] = `
|
||||
<ContextProvider
|
||||
value={
|
||||
Object {
|
||||
"store": Object {
|
||||
"clearActions": [Function],
|
||||
"dispatch": [Function],
|
||||
"getActions": [Function],
|
||||
"getState": [Function],
|
||||
"replaceReducer": [Function],
|
||||
"subscribe": [Function],
|
||||
},
|
||||
"subscription": Subscription {
|
||||
"handleChangeWrapper": [Function],
|
||||
"listeners": Object {
|
||||
"notify": [Function],
|
||||
},
|
||||
"onStateChange": [Function],
|
||||
"parentSub": undefined,
|
||||
"store": Object {
|
||||
"clearActions": [Function],
|
||||
"dispatch": [Function],
|
||||
"getActions": [Function],
|
||||
"getState": [Function],
|
||||
"replaceReducer": [Function],
|
||||
"subscribe": [Function],
|
||||
},
|
||||
"unsubscribe": null,
|
||||
},
|
||||
}
|
||||
}
|
||||
>
|
||||
<Memo(ThreadDraft)
|
||||
channel={
|
||||
Object {
|
||||
"id": "",
|
||||
}
|
||||
}
|
||||
channelUrl=""
|
||||
displayName=""
|
||||
draftId=""
|
||||
id={Object {}}
|
||||
isRemote={false}
|
||||
rootId=""
|
||||
status={Object {}}
|
||||
thread={null}
|
||||
type="thread"
|
||||
user={Object {}}
|
||||
value={Object {}}
|
||||
/>
|
||||
</ContextProvider>
|
||||
`;
|
||||
@@ -1,39 +0,0 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {connect} from 'react-redux';
|
||||
|
||||
import {makeGetChannel} from 'mattermost-redux/selectors/entities/channels';
|
||||
import {getPost} from 'mattermost-redux/selectors/entities/posts';
|
||||
import {makeGetThreadOrSynthetic} from 'mattermost-redux/selectors/entities/threads';
|
||||
|
||||
import type {GlobalState} from 'types/store';
|
||||
import type {PostDraft} from 'types/store/draft';
|
||||
|
||||
import ThreadDraft from './thread_draft';
|
||||
|
||||
type OwnProps = {
|
||||
id: string;
|
||||
value: PostDraft;
|
||||
}
|
||||
|
||||
function makeMapStatetoProps() {
|
||||
const getThreadOrSynthetic = makeGetThreadOrSynthetic();
|
||||
const getChannel = makeGetChannel();
|
||||
return (state: GlobalState, ownProps: OwnProps) => {
|
||||
const channel = getChannel(state, {id: ownProps.value.channelId});
|
||||
const post = getPost(state, ownProps.id);
|
||||
|
||||
let thread;
|
||||
if (post) {
|
||||
thread = getThreadOrSynthetic(state, post);
|
||||
}
|
||||
|
||||
return {
|
||||
channel,
|
||||
thread,
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
export default connect(makeMapStatetoProps)(ThreadDraft);
|
||||
@@ -1,68 +0,0 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {shallow} from 'enzyme';
|
||||
import React from 'react';
|
||||
import {Provider} from 'react-redux';
|
||||
|
||||
import type {Channel} from '@mattermost/types/channels';
|
||||
import type {UserThread, UserThreadSynthetic} from '@mattermost/types/threads';
|
||||
import type {UserProfile, UserStatus} from '@mattermost/types/users';
|
||||
|
||||
import mockStore from 'tests/test_store';
|
||||
|
||||
import type {PostDraft} from 'types/store/draft';
|
||||
|
||||
import ThreadDraft from './thread_draft';
|
||||
|
||||
describe('components/drafts/drafts_row', () => {
|
||||
const baseProps = {
|
||||
channel: {
|
||||
id: '',
|
||||
} as Channel,
|
||||
channelUrl: '',
|
||||
displayName: '',
|
||||
draftId: '',
|
||||
rootId: '' as UserThread['id'] | UserThreadSynthetic['id'],
|
||||
id: {} as Channel['id'],
|
||||
status: {} as UserStatus['status'],
|
||||
thread: {
|
||||
id: '',
|
||||
} as UserThread | UserThreadSynthetic,
|
||||
type: 'thread' as 'channel' | 'thread',
|
||||
user: {} as UserProfile,
|
||||
value: {} as PostDraft,
|
||||
isRemote: false,
|
||||
};
|
||||
|
||||
it('should match snapshot for channel draft', () => {
|
||||
const store = mockStore();
|
||||
|
||||
const wrapper = shallow(
|
||||
<Provider store={store}>
|
||||
<ThreadDraft
|
||||
{...baseProps}
|
||||
/>
|
||||
</Provider>,
|
||||
);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('should match snapshot for undefined thread', () => {
|
||||
const store = mockStore();
|
||||
|
||||
const props = {
|
||||
...baseProps,
|
||||
thread: null as unknown as UserThread | UserThreadSynthetic,
|
||||
};
|
||||
|
||||
const wrapper = shallow(
|
||||
<Provider store={store}>
|
||||
<ThreadDraft
|
||||
{...props}
|
||||
/>
|
||||
</Provider>,
|
||||
);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
@@ -1,130 +0,0 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React, {memo, useCallback, useMemo, useEffect} from 'react';
|
||||
import {useDispatch} from 'react-redux';
|
||||
|
||||
import type {Channel} from '@mattermost/types/channels';
|
||||
import type {Post} from '@mattermost/types/posts';
|
||||
import type {UserThread, UserThreadSynthetic} from '@mattermost/types/threads';
|
||||
import type {UserProfile, UserStatus} from '@mattermost/types/users';
|
||||
|
||||
import {getPost} from 'mattermost-redux/actions/posts';
|
||||
|
||||
import {makeOnSubmit} from 'actions/views/create_comment';
|
||||
import {removeDraft} from 'actions/views/drafts';
|
||||
import {selectPost} from 'actions/views/rhs';
|
||||
|
||||
import type {PostDraft} from 'types/store/draft';
|
||||
|
||||
import DraftActions from '../draft_actions';
|
||||
import DraftTitle from '../draft_title';
|
||||
import Panel from '../panel/panel';
|
||||
import PanelBody from '../panel/panel_body';
|
||||
import Header from '../panel/panel_header';
|
||||
|
||||
type Props = {
|
||||
channel?: Channel;
|
||||
displayName: string;
|
||||
draftId: string;
|
||||
rootId: UserThread['id'] | UserThreadSynthetic['id'];
|
||||
status: UserStatus['status'];
|
||||
thread?: UserThread | UserThreadSynthetic;
|
||||
type: 'channel' | 'thread';
|
||||
user: UserProfile;
|
||||
value: PostDraft;
|
||||
isRemote?: boolean;
|
||||
}
|
||||
|
||||
function ThreadDraft({
|
||||
channel,
|
||||
displayName,
|
||||
draftId,
|
||||
rootId,
|
||||
status,
|
||||
thread,
|
||||
type,
|
||||
user,
|
||||
value,
|
||||
isRemote,
|
||||
}: Props) {
|
||||
const dispatch = useDispatch();
|
||||
|
||||
useEffect(() => {
|
||||
if (!thread?.id) {
|
||||
dispatch(getPost(rootId));
|
||||
}
|
||||
}, [thread?.id]);
|
||||
|
||||
const onSubmit = useMemo(() => {
|
||||
if (thread?.id) {
|
||||
return makeOnSubmit(value.channelId, thread.id, '');
|
||||
}
|
||||
|
||||
return () => Promise.resolve({data: true});
|
||||
}, [value.channelId, thread?.id]);
|
||||
|
||||
const handleOnDelete = useCallback((id: string) => {
|
||||
dispatch(removeDraft(id, value.channelId, rootId));
|
||||
}, [value.channelId, rootId, dispatch]);
|
||||
|
||||
const handleOnEdit = useCallback(() => {
|
||||
dispatch(selectPost({id: rootId, channel_id: value.channelId} as Post));
|
||||
}, [value.channelId, dispatch, rootId]);
|
||||
|
||||
const handleOnSend = useCallback(async (id: string) => {
|
||||
await dispatch(onSubmit(value));
|
||||
|
||||
handleOnDelete(id);
|
||||
handleOnEdit();
|
||||
}, [value, onSubmit, dispatch, handleOnDelete, handleOnEdit]);
|
||||
|
||||
if (!thread || !channel) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Panel onClick={handleOnEdit}>
|
||||
{({hover}) => (
|
||||
<>
|
||||
<Header
|
||||
hover={hover}
|
||||
actions={(
|
||||
<DraftActions
|
||||
channelDisplayName={channel.display_name}
|
||||
channelName={channel.name}
|
||||
channelType={channel.type}
|
||||
userId={user.id}
|
||||
draftId={draftId}
|
||||
onDelete={handleOnDelete}
|
||||
onEdit={handleOnEdit}
|
||||
onSend={handleOnSend}
|
||||
/>
|
||||
)}
|
||||
title={(
|
||||
<DraftTitle
|
||||
type={type}
|
||||
channel={channel}
|
||||
userId={user.id}
|
||||
/>
|
||||
)}
|
||||
timestamp={value.updateAt}
|
||||
remote={isRemote || false}
|
||||
/>
|
||||
<PanelBody
|
||||
channelId={channel.id}
|
||||
displayName={displayName}
|
||||
fileInfos={value.fileInfos}
|
||||
message={value.message}
|
||||
status={status}
|
||||
uploadsInProgress={value.uploadsInProgress}
|
||||
userId={user.id}
|
||||
username={user.username}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</Panel>
|
||||
);
|
||||
}
|
||||
|
||||
export default memo(ThreadDraft);
|
||||
@@ -34,7 +34,7 @@ const FilePreviewModalInfo: React.FC<Props> = (props: Props) => {
|
||||
const user = useSelector((state: GlobalState) => selectUser(state, props.post?.user_id ?? '')) as UserProfile | undefined;
|
||||
const channel = useSelector((state: GlobalState) => {
|
||||
const getChannel = makeGetChannel();
|
||||
return getChannel(state, {id: props.post?.channel_id ?? ''});
|
||||
return getChannel(state, props.post?.channel_id ?? '');
|
||||
});
|
||||
const name = useSelector((state: GlobalState) => displayNameGetter(state, props.post?.user_id ?? '', true));
|
||||
|
||||
|
||||
@@ -52,7 +52,7 @@ const ForwardPostModal = ({onExited, post}: Props) => {
|
||||
|
||||
const getChannel = useMemo(makeGetChannel, []);
|
||||
|
||||
const channel = useSelector((state: GlobalState) => getChannel(state, {id: post.channel_id}));
|
||||
const channel = useSelector((state: GlobalState) => getChannel(state, post.channel_id));
|
||||
const currentTeam = useSelector(getCurrentTeam);
|
||||
|
||||
const relativePermaLink = useSelector((state: GlobalState) => (currentTeam ? getPermalinkURL(state, currentTeam.id, post.id) : ''));
|
||||
|
||||
@@ -63,7 +63,7 @@ const preventActionOnPreview = (e: React.MouseEvent) => {
|
||||
const MoveThreadModal = ({onExited, post, actions}: Props) => {
|
||||
const {formatMessage} = useIntl();
|
||||
|
||||
const originalChannel = useSelector((state: GlobalState) => getChannel(state, {id: post.channel_id}));
|
||||
const originalChannel = useSelector((state: GlobalState) => getChannel(state, post.channel_id));
|
||||
const currentTeam = useSelector(getCurrentTeam);
|
||||
|
||||
const timeoutRef = useRef<NodeJS.Timeout | null>(null);
|
||||
|
||||
@@ -49,7 +49,7 @@ function makeMapStateToProps() {
|
||||
}
|
||||
|
||||
if (ownProps.metadata.channel_type === General.DM_CHANNEL) {
|
||||
channelDisplayName = getChannel(state, {id: ownProps.metadata.channel_id})?.display_name || '';
|
||||
channelDisplayName = getChannel(state, ownProps.metadata.channel_id)?.display_name || '';
|
||||
}
|
||||
|
||||
return {
|
||||
|
||||
@@ -32,7 +32,7 @@ function makeMapStateToProps() {
|
||||
const getUnreadCount = makeGetChannelUnreadCount();
|
||||
|
||||
return (state: GlobalState, ownProps: OwnProps) => {
|
||||
const channel = getChannel(state, {id: ownProps.channelId});
|
||||
const channel = getChannel(state, ownProps.channelId);
|
||||
const currentTeam = getCurrentTeam(state);
|
||||
|
||||
const currentChannelId = getCurrentChannelId(state);
|
||||
|
||||
@@ -32,7 +32,7 @@ function makeMapStateToProps() {
|
||||
|
||||
return {
|
||||
post,
|
||||
channel: getChannel(state, {id: post.channel_id}),
|
||||
channel: getChannel(state, post.channel_id),
|
||||
currentRelativeTeamUrl: getCurrentRelativeTeamUrl(state),
|
||||
displayName: getDisplayName(state, post.user_id, true),
|
||||
postsInThread: getPostsForThread(state, post.id),
|
||||
|
||||
@@ -54,7 +54,7 @@ const ThreadPane = ({
|
||||
},
|
||||
} = thread;
|
||||
|
||||
const channel = useSelector((state: GlobalState) => getChannel(state, {id: channelId}));
|
||||
const channel = useSelector((state: GlobalState) => getChannel(state, channelId));
|
||||
const post = useSelector((state: GlobalState) => getPost(state, thread.id));
|
||||
const postsInThread = useSelector((state: GlobalState) => getPostsForThread(state, post.id));
|
||||
const selectHandler = useCallback(() => select(), []);
|
||||
@@ -72,7 +72,7 @@ const ThreadPane = ({
|
||||
|
||||
const followHandler = useCallback(() => {
|
||||
dispatch(setThreadFollow(currentUserId, currentTeamId, threadId, !isFollowing));
|
||||
}, [currentUserId, currentTeamId, threadId, isFollowing, setThreadFollow]);
|
||||
}, [dispatch, currentUserId, currentTeamId, threadId, isFollowing]);
|
||||
|
||||
return (
|
||||
<div
|
||||
|
||||
@@ -55,7 +55,7 @@ function makeMapStateToProps() {
|
||||
if (selected) {
|
||||
postIds = getPostIdsForThread(state, selected.id);
|
||||
userThread = getThread(state, selected.id);
|
||||
channel = getChannel(state, {id: selected.channel_id});
|
||||
channel = getChannel(state, selected.channel_id);
|
||||
}
|
||||
|
||||
return {
|
||||
|
||||
@@ -38,7 +38,7 @@ const CreateComment = forwardRef<HTMLDivElement, Props>(({
|
||||
if (threadIsLimited) {
|
||||
return null;
|
||||
}
|
||||
return getChannel(state, {id: rootPost.channel_id});
|
||||
return getChannel(state, rootPost.channel_id);
|
||||
});
|
||||
if (!channel || threadIsLimited) {
|
||||
return null;
|
||||
|
||||
@@ -3493,6 +3493,9 @@
|
||||
"drafts.draft_title.you": "(you)",
|
||||
"drafts.empty.subtitle": "Any messages you’ve started will show here.",
|
||||
"drafts.empty.title": "No drafts at the moment",
|
||||
"drafts.error.post_not_found": "Thread not found",
|
||||
"drafts.error.read_only": "Channel is read only",
|
||||
"drafts.error.too_long": "Message too long",
|
||||
"drafts.heading": "Drafts",
|
||||
"drafts.info.sync": "Updated from another device",
|
||||
"drafts.sidebarLink": "Drafts",
|
||||
|
||||
@@ -174,7 +174,11 @@ export type CreatePostReturnType = {
|
||||
pending?: string;
|
||||
}
|
||||
|
||||
export function createPost(post: Post, files: any[] = [], afterSubmit?: (response: any) => void): ActionFuncAsync<CreatePostReturnType, GlobalState> {
|
||||
export function createPost(
|
||||
post: Post,
|
||||
files: any[] = [],
|
||||
afterSubmit?: (response: any) => void,
|
||||
): ActionFuncAsync<CreatePostReturnType, GlobalState> {
|
||||
return async (dispatch, getState) => {
|
||||
const state = getState();
|
||||
const currentUserId = state.entities.users.currentUserId;
|
||||
|
||||
@@ -481,13 +481,13 @@ describe('makeGetChannel', () => {
|
||||
test('should return non-DM/non-GM channels directly from the store', () => {
|
||||
const getChannel = Selectors.makeGetChannel();
|
||||
|
||||
expect(getChannel(testState, {id: channel1.id})).toBe(channel1);
|
||||
expect(getChannel(testState, channel1.id)).toBe(channel1);
|
||||
});
|
||||
|
||||
test('should return DMs with computed data added', () => {
|
||||
const getChannel = Selectors.makeGetChannel();
|
||||
|
||||
expect(getChannel(testState, {id: channel2.id})).toEqual({
|
||||
expect(getChannel(testState, channel2.id)).toEqual({
|
||||
...channel2,
|
||||
display_name: user2.username,
|
||||
status: 'offline',
|
||||
@@ -498,7 +498,7 @@ describe('makeGetChannel', () => {
|
||||
test('should return GMs with computed data added', () => {
|
||||
const getChannel = Selectors.makeGetChannel();
|
||||
|
||||
expect(getChannel(testState, {id: channel3.id})).toEqual({
|
||||
expect(getChannel(testState, channel3.id)).toEqual({
|
||||
...channel3,
|
||||
display_name: [user2.username, user3.username].sort(sortUsernames).join(', '),
|
||||
});
|
||||
|
||||
@@ -147,18 +147,21 @@ export function getChannelMember(state: GlobalState, channelId: string, userId:
|
||||
return getChannelMembersInChannels(state)[channelId]?.[userId];
|
||||
}
|
||||
|
||||
type OldMakeChannelArgument = {id: string};
|
||||
|
||||
// makeGetChannel returns a selector that returns a channel from the store with the following filled in for DM/GM channels:
|
||||
// - The display_name set to the other user(s) names, following the Teammate Name Display setting
|
||||
// - The teammate_id for DM channels
|
||||
// - The status of the other user in a DM channel
|
||||
export function makeGetChannel(): (state: GlobalState, props: {id: string}) => Channel | undefined {
|
||||
export function makeGetChannel(): (state: GlobalState, id: string) => Channel | undefined {
|
||||
return createSelector(
|
||||
'makeGetChannel',
|
||||
getCurrentUserId,
|
||||
(state: GlobalState) => state.entities.users.profiles,
|
||||
(state: GlobalState) => state.entities.users.profilesInChannel,
|
||||
(state: GlobalState, props: {id: string}) => {
|
||||
const channel = getChannel(state, props.id);
|
||||
(state: GlobalState, channelId: string | OldMakeChannelArgument) => {
|
||||
const id = typeof channelId === 'string' ? channelId : channelId.id;
|
||||
const channel = getChannel(state, id);
|
||||
if (!channel || !isDirectChannel(channel)) {
|
||||
return '';
|
||||
}
|
||||
@@ -169,7 +172,10 @@ export function makeGetChannel(): (state: GlobalState, props: {id: string}) => C
|
||||
|
||||
return teammateStatus || 'offline';
|
||||
},
|
||||
(state: GlobalState, props: {id: string}) => getChannel(state, props.id),
|
||||
(state: GlobalState, channelId: string | OldMakeChannelArgument) => {
|
||||
const id = typeof channelId === 'string' ? channelId : channelId.id;
|
||||
return getChannel(state, id);
|
||||
},
|
||||
getTeammateNameDisplaySetting,
|
||||
(currentUserId, profiles, profilesInChannel, teammateStatus, channel, teammateNameDisplay) => {
|
||||
if (channel) {
|
||||
|
||||
@@ -57,7 +57,7 @@ export const getSelectedChannel = (() => {
|
||||
return (state: GlobalState) => {
|
||||
const channelId = getSelectedChannelId(state);
|
||||
|
||||
return getChannel(state, {id: channelId});
|
||||
return getChannel(state, channelId);
|
||||
};
|
||||
})();
|
||||
|
||||
|
||||
Ссылка в новой задаче
Block a user