mirror of
https://github.com/mattermost/mattermost.git
synced 2025-02-25 18:55:24 -06:00
Unify advanced create post and advanced create comment (#26419)
* Unify advanced create post and advanced create comment * Re-add focus on mount prop and fix minor selector issue with get draft * Address feedback * Some merge fixes and some comments addressed * Remove tests * Fix tests * Address feedback * Fix formatting bar spacer and minor refactoring * Fix remove upload from clean draft issue * Fix types --------- Co-authored-by: Mattermost Build <build@mattermost.com>
This commit is contained in:
parent
ff3ed78124
commit
a272fb29a5
@ -22,6 +22,7 @@ import {removeDraft, setGlobalDraftSource} from 'actions/views/drafts';
|
|||||||
|
|
||||||
import mockStore from 'tests/test_store';
|
import mockStore from 'tests/test_store';
|
||||||
import {StoragePrefixes} from 'utils/constants';
|
import {StoragePrefixes} from 'utils/constants';
|
||||||
|
import {TestHelper} from 'utils/test_helper';
|
||||||
|
|
||||||
/* eslint-disable global-require */
|
/* eslint-disable global-require */
|
||||||
|
|
||||||
@ -121,13 +122,21 @@ describe('rhs view actions', () => {
|
|||||||
messages: ['test message'],
|
messages: ['test message'],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
channels: {
|
||||||
|
channels: {
|
||||||
|
[channelId]: TestHelper.getChannelMock({id: channelId}),
|
||||||
|
},
|
||||||
|
roles: {
|
||||||
|
[channelId]: new Set(['channel_roles']),
|
||||||
|
},
|
||||||
|
},
|
||||||
preferences: {
|
preferences: {
|
||||||
myPreferences: {},
|
myPreferences: {},
|
||||||
},
|
},
|
||||||
users: {
|
users: {
|
||||||
currentUserId,
|
currentUserId,
|
||||||
profiles: {
|
profiles: {
|
||||||
[currentUserId]: {id: currentUserId},
|
[currentUserId]: TestHelper.getUserMock({id: currentUserId}),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
teams: {
|
teams: {
|
||||||
@ -136,6 +145,13 @@ describe('rhs view actions', () => {
|
|||||||
emojis: {
|
emojis: {
|
||||||
customEmoji: {},
|
customEmoji: {},
|
||||||
},
|
},
|
||||||
|
roles: {
|
||||||
|
roles: {
|
||||||
|
channel_roles: {
|
||||||
|
permissions: '',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
general: {
|
general: {
|
||||||
config: {
|
config: {
|
||||||
EnableCustomEmoji: 'true',
|
EnableCustomEmoji: 'true',
|
||||||
|
@ -6,12 +6,20 @@ import type {Post} from '@mattermost/types/posts';
|
|||||||
import {
|
import {
|
||||||
addMessageIntoHistory,
|
addMessageIntoHistory,
|
||||||
} from 'mattermost-redux/actions/posts';
|
} from 'mattermost-redux/actions/posts';
|
||||||
|
import {Permissions} from 'mattermost-redux/constants';
|
||||||
import {createSelector} from 'mattermost-redux/selectors/create_selector';
|
import {createSelector} from 'mattermost-redux/selectors/create_selector';
|
||||||
|
import {getChannel} from 'mattermost-redux/selectors/entities/channels';
|
||||||
import {getCustomEmojisByName} from 'mattermost-redux/selectors/entities/emojis';
|
import {getCustomEmojisByName} from 'mattermost-redux/selectors/entities/emojis';
|
||||||
|
import {getLicense} from 'mattermost-redux/selectors/entities/general';
|
||||||
|
import {getAssociatedGroupsForReferenceByMention} from 'mattermost-redux/selectors/entities/groups';
|
||||||
import {
|
import {
|
||||||
|
getLatestInteractablePostId,
|
||||||
|
getLatestPostToEdit,
|
||||||
getPost,
|
getPost,
|
||||||
makeGetPostIdsForThread,
|
makeGetPostIdsForThread,
|
||||||
} from 'mattermost-redux/selectors/entities/posts';
|
} from 'mattermost-redux/selectors/entities/posts';
|
||||||
|
import {isCustomGroupsEnabled} from 'mattermost-redux/selectors/entities/preferences';
|
||||||
|
import {haveIChannelPermission} from 'mattermost-redux/selectors/entities/roles';
|
||||||
import {getCurrentTeamId} from 'mattermost-redux/selectors/entities/teams';
|
import {getCurrentTeamId} from 'mattermost-redux/selectors/entities/teams';
|
||||||
import {getCurrentUserId} from 'mattermost-redux/selectors/entities/users';
|
import {getCurrentUserId} from 'mattermost-redux/selectors/entities/users';
|
||||||
import type {ActionFunc, ActionFuncAsync} from 'mattermost-redux/types/actions';
|
import type {ActionFunc, ActionFuncAsync} from 'mattermost-redux/types/actions';
|
||||||
@ -25,6 +33,7 @@ import {updateDraft, removeDraft} from 'actions/views/drafts';
|
|||||||
|
|
||||||
import {Constants, StoragePrefixes} from 'utils/constants';
|
import {Constants, StoragePrefixes} from 'utils/constants';
|
||||||
import EmojiMap from 'utils/emoji_map';
|
import EmojiMap from 'utils/emoji_map';
|
||||||
|
import {containsAtChannel, groupsMentionedInText} from 'utils/post_utils';
|
||||||
import * as Utils from 'utils/utils';
|
import * as Utils from 'utils/utils';
|
||||||
|
|
||||||
import type {GlobalState} from 'types/store';
|
import type {GlobalState} from 'types/store';
|
||||||
@ -63,10 +72,30 @@ export function submitPost(channelId: string, rootId: string, draft: PostDraft):
|
|||||||
pending_post_id: `${userId}:${time}`,
|
pending_post_id: `${userId}:${time}`,
|
||||||
user_id: userId,
|
user_id: userId,
|
||||||
create_at: time,
|
create_at: time,
|
||||||
metadata: {},
|
metadata: {...draft.metadata},
|
||||||
props: {...draft.props},
|
props: {...draft.props},
|
||||||
} as unknown as Post;
|
} as unknown as Post;
|
||||||
|
|
||||||
|
const channel = getChannel(state, channelId);
|
||||||
|
if (!channel) {
|
||||||
|
return {error: new Error('cannot find channel')};
|
||||||
|
}
|
||||||
|
const useChannelMentions = haveIChannelPermission(state, channel.team_id, channel.id, Permissions.USE_CHANNEL_MENTIONS);
|
||||||
|
if (!useChannelMentions && containsAtChannel(post.message, {checkAllMentions: true})) {
|
||||||
|
post.props.mentionHighlightDisabled = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const license = getLicense(state);
|
||||||
|
const isLDAPEnabled = license?.IsLicensed === 'true' && license?.LDAPGroups === 'true';
|
||||||
|
const useLDAPGroupMentions = isLDAPEnabled && haveIChannelPermission(state, channel.team_id, channel.id, Permissions.USE_GROUP_MENTIONS);
|
||||||
|
|
||||||
|
const useCustomGroupMentions = isCustomGroupsEnabled(state) && haveIChannelPermission(state, channel.team_id, channel.id, Permissions.USE_GROUP_MENTIONS);
|
||||||
|
|
||||||
|
const groupsWithAllowReference = useLDAPGroupMentions || useCustomGroupMentions ? getAssociatedGroupsForReferenceByMention(state, channel.team_id, channel.id) : null;
|
||||||
|
if (!useLDAPGroupMentions && !useCustomGroupMentions && groupsMentionedInText(post.message, groupsWithAllowReference)) {
|
||||||
|
post.props.disable_group_highlight = true;
|
||||||
|
}
|
||||||
|
|
||||||
const hookResult = await dispatch(runMessageWillBePostedHooks(post));
|
const hookResult = await dispatch(runMessageWillBePostedHooks(post));
|
||||||
if (hookResult.error) {
|
if (hookResult.error) {
|
||||||
return {error: hookResult.error};
|
return {error: hookResult.error};
|
||||||
@ -146,6 +175,32 @@ export function makeOnSubmit(channelId: string, rootId: string, latestPostId: st
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function onSubmit(draft: PostDraft, options: {ignoreSlash?: boolean}): ActionFuncAsync<boolean, GlobalState> {
|
||||||
|
return async (dispatch, getState) => {
|
||||||
|
const {message, channelId, rootId} = draft;
|
||||||
|
const state = getState();
|
||||||
|
|
||||||
|
dispatch(addMessageIntoHistory(message));
|
||||||
|
|
||||||
|
const isReaction = Utils.REACTION_PATTERN.exec(message);
|
||||||
|
|
||||||
|
const emojis = getCustomEmojisByName(state);
|
||||||
|
const emojiMap = new EmojiMap(emojis);
|
||||||
|
|
||||||
|
if (isReaction && emojiMap.has(isReaction[2])) {
|
||||||
|
const latestPostId = getLatestInteractablePostId(state, channelId, rootId);
|
||||||
|
if (latestPostId) {
|
||||||
|
dispatch(PostActions.submitReaction(latestPostId, isReaction[1], isReaction[2]));
|
||||||
|
}
|
||||||
|
} else if (message.indexOf('/') === 0 && !options.ignoreSlash) {
|
||||||
|
await dispatch(submitCommand(channelId, rootId, draft));
|
||||||
|
} else {
|
||||||
|
await dispatch(submitPost(channelId, rootId, draft));
|
||||||
|
}
|
||||||
|
return {data: true};
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
function makeGetCurrentUsersLatestReply() {
|
function makeGetCurrentUsersLatestReply() {
|
||||||
const getPostIdsInThread = makeGetPostIdsForThread();
|
const getPostIdsInThread = makeGetPostIdsForThread();
|
||||||
return createSelector(
|
return createSelector(
|
||||||
@ -211,3 +266,22 @@ export function makeOnEditLatestPost(rootId: string): () => ActionFunc<boolean>
|
|||||||
));
|
));
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function editLatestPost(channelId: string, rootId = ''): ActionFunc<boolean> {
|
||||||
|
return (dispatch, getState) => {
|
||||||
|
const state = getState();
|
||||||
|
|
||||||
|
const lastPostId = getLatestPostToEdit(state, channelId, rootId);
|
||||||
|
|
||||||
|
if (!lastPostId) {
|
||||||
|
return {data: false};
|
||||||
|
}
|
||||||
|
|
||||||
|
return dispatch(PostActions.setEditingPost(
|
||||||
|
lastPostId,
|
||||||
|
rootId ? 'reply_textbox' : 'post_textbox',
|
||||||
|
'', // title is no longer used
|
||||||
|
Boolean(rootId),
|
||||||
|
));
|
||||||
|
};
|
||||||
|
}
|
||||||
|
@ -97,7 +97,7 @@ export function updateDraft(key: string, value: PostDraft|null, rootId = '', sav
|
|||||||
let updatedValue: PostDraft|null = null;
|
let updatedValue: PostDraft|null = null;
|
||||||
if (value) {
|
if (value) {
|
||||||
const timestamp = new Date().getTime();
|
const timestamp = new Date().getTime();
|
||||||
const data = getGlobalItem(state, key, {});
|
const data = getGlobalItem<Partial<PostDraft>>(state, key, {});
|
||||||
updatedValue = {
|
updatedValue = {
|
||||||
...value,
|
...value,
|
||||||
createAt: data.createAt || timestamp,
|
createAt: data.createAt || timestamp,
|
||||||
|
@ -1,530 +0,0 @@
|
|||||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
|
||||||
|
|
||||||
exports[`components/AdvancedCreateComment should match snapshot when cannot post 1`] = `
|
|
||||||
<form
|
|
||||||
onSubmit={[Function]}
|
|
||||||
>
|
|
||||||
<AdvanceTextEditor
|
|
||||||
additionalControls={Array []}
|
|
||||||
applyMarkdown={[Function]}
|
|
||||||
badConnection={false}
|
|
||||||
canPost={false}
|
|
||||||
canUploadFiles={true}
|
|
||||||
caretPosition={12}
|
|
||||||
channelId="g6139tbospd18cmxroesdk3kkc"
|
|
||||||
ctrlSend={false}
|
|
||||||
currentUserId="zaktnt8bpbgu8mb6ez9k64r7sa"
|
|
||||||
draft={
|
|
||||||
Object {
|
|
||||||
"channelId": "",
|
|
||||||
"createAt": 0,
|
|
||||||
"fileInfos": Array [
|
|
||||||
Object {
|
|
||||||
"archived": false,
|
|
||||||
"clientId": "client_id",
|
|
||||||
"create_at": 1,
|
|
||||||
"delete_at": 1,
|
|
||||||
"extension": "jpg",
|
|
||||||
"has_preview_image": true,
|
|
||||||
"height": 200,
|
|
||||||
"id": "file_info_id",
|
|
||||||
"mime_type": "mime_type",
|
|
||||||
"name": "name",
|
|
||||||
"size": 1,
|
|
||||||
"update_at": 1,
|
|
||||||
"user_id": "user_id",
|
|
||||||
"width": 350,
|
|
||||||
},
|
|
||||||
Object {
|
|
||||||
"archived": false,
|
|
||||||
"clientId": "client_id",
|
|
||||||
"create_at": 1,
|
|
||||||
"delete_at": 1,
|
|
||||||
"extension": "jpg",
|
|
||||||
"has_preview_image": true,
|
|
||||||
"height": 200,
|
|
||||||
"id": "file_info_id",
|
|
||||||
"mime_type": "mime_type",
|
|
||||||
"name": "name",
|
|
||||||
"size": 1,
|
|
||||||
"update_at": 1,
|
|
||||||
"user_id": "user_id",
|
|
||||||
"width": 350,
|
|
||||||
},
|
|
||||||
Object {
|
|
||||||
"archived": false,
|
|
||||||
"clientId": "client_id",
|
|
||||||
"create_at": 1,
|
|
||||||
"delete_at": 1,
|
|
||||||
"extension": "jpg",
|
|
||||||
"has_preview_image": true,
|
|
||||||
"height": 200,
|
|
||||||
"id": "file_info_id",
|
|
||||||
"mime_type": "mime_type",
|
|
||||||
"name": "name",
|
|
||||||
"size": 1,
|
|
||||||
"update_at": 1,
|
|
||||||
"user_id": "user_id",
|
|
||||||
"width": 350,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
"message": "Test message",
|
|
||||||
"rootId": "",
|
|
||||||
"updateAt": 0,
|
|
||||||
"uploadsInProgress": Array [],
|
|
||||||
}
|
|
||||||
}
|
|
||||||
emitTypingEvent={[Function]}
|
|
||||||
enableEmojiPicker={true}
|
|
||||||
enableGifPicker={true}
|
|
||||||
errorClass={null}
|
|
||||||
fileUploadRef={
|
|
||||||
Object {
|
|
||||||
"current": null,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
getFileUploadTarget={[Function]}
|
|
||||||
handleBlur={[Function]}
|
|
||||||
handleChange={[Function]}
|
|
||||||
handleEmojiClick={[Function]}
|
|
||||||
handleFileUploadChange={[Function]}
|
|
||||||
handleFileUploadComplete={[Function]}
|
|
||||||
handleGifClick={[Function]}
|
|
||||||
handleMouseUpKeyUp={[Function]}
|
|
||||||
handlePostError={[Function]}
|
|
||||||
handleSubmit={[Function]}
|
|
||||||
handleUploadError={[Function]}
|
|
||||||
handleUploadProgress={[Function]}
|
|
||||||
handleUploadStart={[Function]}
|
|
||||||
hideEmojiPicker={[Function]}
|
|
||||||
isFormattingBarHidden={false}
|
|
||||||
loadNextMessage={[Function]}
|
|
||||||
loadPrevMessage={[Function]}
|
|
||||||
location="RHS_COMMENT"
|
|
||||||
maxPostSize={4000}
|
|
||||||
message="Test message"
|
|
||||||
onEditLatestPost={[Function]}
|
|
||||||
onMessageChange={[Function]}
|
|
||||||
postId=""
|
|
||||||
postMsgKeyPress={[Function]}
|
|
||||||
removePreview={[Function]}
|
|
||||||
serverError={null}
|
|
||||||
setShowPreview={[Function]}
|
|
||||||
shouldShowPreview={false}
|
|
||||||
showEmojiPicker={false}
|
|
||||||
textboxRef={
|
|
||||||
Object {
|
|
||||||
"current": null,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
toggleAdvanceTextEditor={[Function]}
|
|
||||||
toggleEmojiPicker={[Function]}
|
|
||||||
uploadsProgressPercent={Object {}}
|
|
||||||
useChannelMentions={true}
|
|
||||||
/>
|
|
||||||
</form>
|
|
||||||
`;
|
|
||||||
|
|
||||||
exports[`components/AdvancedCreateComment should match snapshot, comment with message 1`] = `
|
|
||||||
<form
|
|
||||||
onSubmit={[Function]}
|
|
||||||
>
|
|
||||||
<AdvanceTextEditor
|
|
||||||
additionalControls={Array []}
|
|
||||||
applyMarkdown={[Function]}
|
|
||||||
badConnection={false}
|
|
||||||
canPost={true}
|
|
||||||
canUploadFiles={true}
|
|
||||||
caretPosition={12}
|
|
||||||
channelId="g6139tbospd18cmxroesdk3kkc"
|
|
||||||
ctrlSend={true}
|
|
||||||
currentUserId="zaktnt8bpbgu8mb6ez9k64r7sa"
|
|
||||||
draft={
|
|
||||||
Object {
|
|
||||||
"channelId": "",
|
|
||||||
"createAt": 0,
|
|
||||||
"fileInfos": Array [],
|
|
||||||
"message": "Test message",
|
|
||||||
"rootId": "",
|
|
||||||
"updateAt": 0,
|
|
||||||
"uploadsInProgress": Array [],
|
|
||||||
}
|
|
||||||
}
|
|
||||||
emitTypingEvent={[Function]}
|
|
||||||
enableEmojiPicker={true}
|
|
||||||
enableGifPicker={true}
|
|
||||||
errorClass={null}
|
|
||||||
fileUploadRef={
|
|
||||||
Object {
|
|
||||||
"current": null,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
getFileUploadTarget={[Function]}
|
|
||||||
handleBlur={[Function]}
|
|
||||||
handleChange={[Function]}
|
|
||||||
handleEmojiClick={[Function]}
|
|
||||||
handleFileUploadChange={[Function]}
|
|
||||||
handleFileUploadComplete={[Function]}
|
|
||||||
handleGifClick={[Function]}
|
|
||||||
handleMouseUpKeyUp={[Function]}
|
|
||||||
handlePostError={[Function]}
|
|
||||||
handleSubmit={[Function]}
|
|
||||||
handleUploadError={[Function]}
|
|
||||||
handleUploadProgress={[Function]}
|
|
||||||
handleUploadStart={[Function]}
|
|
||||||
hideEmojiPicker={[Function]}
|
|
||||||
isFormattingBarHidden={false}
|
|
||||||
loadNextMessage={[Function]}
|
|
||||||
loadPrevMessage={[Function]}
|
|
||||||
location="RHS_COMMENT"
|
|
||||||
maxPostSize={4000}
|
|
||||||
message="Test message"
|
|
||||||
onEditLatestPost={[Function]}
|
|
||||||
onMessageChange={[Function]}
|
|
||||||
postId=""
|
|
||||||
postMsgKeyPress={[Function]}
|
|
||||||
removePreview={[Function]}
|
|
||||||
serverError={null}
|
|
||||||
setShowPreview={[Function]}
|
|
||||||
shouldShowPreview={false}
|
|
||||||
showEmojiPicker={false}
|
|
||||||
textboxRef={
|
|
||||||
Object {
|
|
||||||
"current": null,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
toggleAdvanceTextEditor={[Function]}
|
|
||||||
toggleEmojiPicker={[Function]}
|
|
||||||
uploadsProgressPercent={Object {}}
|
|
||||||
useChannelMentions={true}
|
|
||||||
/>
|
|
||||||
</form>
|
|
||||||
`;
|
|
||||||
|
|
||||||
exports[`components/AdvancedCreateComment should match snapshot, emoji picker disabled 1`] = `
|
|
||||||
<form
|
|
||||||
onSubmit={[Function]}
|
|
||||||
>
|
|
||||||
<FileLimitStickyBanner />
|
|
||||||
<AdvanceTextEditor
|
|
||||||
additionalControls={Array []}
|
|
||||||
applyMarkdown={[Function]}
|
|
||||||
badConnection={false}
|
|
||||||
canPost={true}
|
|
||||||
canUploadFiles={true}
|
|
||||||
caretPosition={12}
|
|
||||||
channelId="g6139tbospd18cmxroesdk3kkc"
|
|
||||||
ctrlSend={false}
|
|
||||||
currentUserId="zaktnt8bpbgu8mb6ez9k64r7sa"
|
|
||||||
draft={
|
|
||||||
Object {
|
|
||||||
"channelId": "",
|
|
||||||
"createAt": 0,
|
|
||||||
"fileInfos": Array [
|
|
||||||
Object {
|
|
||||||
"archived": false,
|
|
||||||
"clientId": "client_id",
|
|
||||||
"create_at": 1,
|
|
||||||
"delete_at": 1,
|
|
||||||
"extension": "jpg",
|
|
||||||
"has_preview_image": true,
|
|
||||||
"height": 200,
|
|
||||||
"id": "file_info_id",
|
|
||||||
"mime_type": "mime_type",
|
|
||||||
"name": "name",
|
|
||||||
"size": 1,
|
|
||||||
"update_at": 1,
|
|
||||||
"user_id": "user_id",
|
|
||||||
"width": 350,
|
|
||||||
},
|
|
||||||
Object {
|
|
||||||
"archived": false,
|
|
||||||
"clientId": "client_id",
|
|
||||||
"create_at": 1,
|
|
||||||
"delete_at": 1,
|
|
||||||
"extension": "jpg",
|
|
||||||
"has_preview_image": true,
|
|
||||||
"height": 200,
|
|
||||||
"id": "file_info_id",
|
|
||||||
"mime_type": "mime_type",
|
|
||||||
"name": "name",
|
|
||||||
"size": 1,
|
|
||||||
"update_at": 1,
|
|
||||||
"user_id": "user_id",
|
|
||||||
"width": 350,
|
|
||||||
},
|
|
||||||
Object {
|
|
||||||
"archived": false,
|
|
||||||
"clientId": "client_id",
|
|
||||||
"create_at": 1,
|
|
||||||
"delete_at": 1,
|
|
||||||
"extension": "jpg",
|
|
||||||
"has_preview_image": true,
|
|
||||||
"height": 200,
|
|
||||||
"id": "file_info_id",
|
|
||||||
"mime_type": "mime_type",
|
|
||||||
"name": "name",
|
|
||||||
"size": 1,
|
|
||||||
"update_at": 1,
|
|
||||||
"user_id": "user_id",
|
|
||||||
"width": 350,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
"message": "Test message",
|
|
||||||
"rootId": "",
|
|
||||||
"updateAt": 0,
|
|
||||||
"uploadsInProgress": Array [],
|
|
||||||
}
|
|
||||||
}
|
|
||||||
emitTypingEvent={[Function]}
|
|
||||||
enableEmojiPicker={false}
|
|
||||||
enableGifPicker={true}
|
|
||||||
errorClass={null}
|
|
||||||
fileUploadRef={
|
|
||||||
Object {
|
|
||||||
"current": null,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
getFileUploadTarget={[Function]}
|
|
||||||
handleBlur={[Function]}
|
|
||||||
handleChange={[Function]}
|
|
||||||
handleEmojiClick={[Function]}
|
|
||||||
handleFileUploadChange={[Function]}
|
|
||||||
handleFileUploadComplete={[Function]}
|
|
||||||
handleGifClick={[Function]}
|
|
||||||
handleMouseUpKeyUp={[Function]}
|
|
||||||
handlePostError={[Function]}
|
|
||||||
handleSubmit={[Function]}
|
|
||||||
handleUploadError={[Function]}
|
|
||||||
handleUploadProgress={[Function]}
|
|
||||||
handleUploadStart={[Function]}
|
|
||||||
hideEmojiPicker={[Function]}
|
|
||||||
isFormattingBarHidden={false}
|
|
||||||
loadNextMessage={[Function]}
|
|
||||||
loadPrevMessage={[Function]}
|
|
||||||
location="RHS_COMMENT"
|
|
||||||
maxPostSize={4000}
|
|
||||||
message="Test message"
|
|
||||||
onEditLatestPost={[Function]}
|
|
||||||
onMessageChange={[Function]}
|
|
||||||
postId=""
|
|
||||||
postMsgKeyPress={[Function]}
|
|
||||||
removePreview={[Function]}
|
|
||||||
serverError={null}
|
|
||||||
setShowPreview={[Function]}
|
|
||||||
shouldShowPreview={false}
|
|
||||||
showEmojiPicker={false}
|
|
||||||
textboxRef={
|
|
||||||
Object {
|
|
||||||
"current": null,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
toggleAdvanceTextEditor={[Function]}
|
|
||||||
toggleEmojiPicker={[Function]}
|
|
||||||
uploadsProgressPercent={Object {}}
|
|
||||||
useChannelMentions={true}
|
|
||||||
/>
|
|
||||||
</form>
|
|
||||||
`;
|
|
||||||
|
|
||||||
exports[`components/AdvancedCreateComment should match snapshot, empty comment 1`] = `
|
|
||||||
<form
|
|
||||||
onSubmit={[Function]}
|
|
||||||
>
|
|
||||||
<AdvanceTextEditor
|
|
||||||
additionalControls={Array []}
|
|
||||||
applyMarkdown={[Function]}
|
|
||||||
badConnection={false}
|
|
||||||
canPost={true}
|
|
||||||
canUploadFiles={true}
|
|
||||||
caretPosition={0}
|
|
||||||
channelId="g6139tbospd18cmxroesdk3kkc"
|
|
||||||
ctrlSend={true}
|
|
||||||
currentUserId="zaktnt8bpbgu8mb6ez9k64r7sa"
|
|
||||||
draft={
|
|
||||||
Object {
|
|
||||||
"channelId": "",
|
|
||||||
"createAt": 0,
|
|
||||||
"fileInfos": Array [],
|
|
||||||
"message": "",
|
|
||||||
"rootId": "",
|
|
||||||
"updateAt": 0,
|
|
||||||
"uploadsInProgress": Array [],
|
|
||||||
}
|
|
||||||
}
|
|
||||||
emitTypingEvent={[Function]}
|
|
||||||
enableEmojiPicker={true}
|
|
||||||
enableGifPicker={true}
|
|
||||||
errorClass={null}
|
|
||||||
fileUploadRef={
|
|
||||||
Object {
|
|
||||||
"current": null,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
getFileUploadTarget={[Function]}
|
|
||||||
handleBlur={[Function]}
|
|
||||||
handleChange={[Function]}
|
|
||||||
handleEmojiClick={[Function]}
|
|
||||||
handleFileUploadChange={[Function]}
|
|
||||||
handleFileUploadComplete={[Function]}
|
|
||||||
handleGifClick={[Function]}
|
|
||||||
handleMouseUpKeyUp={[Function]}
|
|
||||||
handlePostError={[Function]}
|
|
||||||
handleSubmit={[Function]}
|
|
||||||
handleUploadError={[Function]}
|
|
||||||
handleUploadProgress={[Function]}
|
|
||||||
handleUploadStart={[Function]}
|
|
||||||
hideEmojiPicker={[Function]}
|
|
||||||
isFormattingBarHidden={false}
|
|
||||||
loadNextMessage={[Function]}
|
|
||||||
loadPrevMessage={[Function]}
|
|
||||||
location="RHS_COMMENT"
|
|
||||||
maxPostSize={4000}
|
|
||||||
message=""
|
|
||||||
onEditLatestPost={[Function]}
|
|
||||||
onMessageChange={[Function]}
|
|
||||||
postId=""
|
|
||||||
postMsgKeyPress={[Function]}
|
|
||||||
removePreview={[Function]}
|
|
||||||
serverError={null}
|
|
||||||
setShowPreview={[Function]}
|
|
||||||
shouldShowPreview={false}
|
|
||||||
showEmojiPicker={false}
|
|
||||||
textboxRef={
|
|
||||||
Object {
|
|
||||||
"current": null,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
toggleAdvanceTextEditor={[Function]}
|
|
||||||
toggleEmojiPicker={[Function]}
|
|
||||||
uploadsProgressPercent={Object {}}
|
|
||||||
useChannelMentions={true}
|
|
||||||
/>
|
|
||||||
</form>
|
|
||||||
`;
|
|
||||||
|
|
||||||
exports[`components/AdvancedCreateComment should match snapshot, non-empty message and uploadsInProgress + fileInfos 1`] = `
|
|
||||||
<form
|
|
||||||
onSubmit={[Function]}
|
|
||||||
>
|
|
||||||
<FileLimitStickyBanner />
|
|
||||||
<AdvanceTextEditor
|
|
||||||
additionalControls={Array []}
|
|
||||||
applyMarkdown={[Function]}
|
|
||||||
badConnection={false}
|
|
||||||
canPost={true}
|
|
||||||
canUploadFiles={true}
|
|
||||||
caretPosition={12}
|
|
||||||
channelId="g6139tbospd18cmxroesdk3kkc"
|
|
||||||
ctrlSend={false}
|
|
||||||
currentUserId="zaktnt8bpbgu8mb6ez9k64r7sa"
|
|
||||||
draft={
|
|
||||||
Object {
|
|
||||||
"channelId": "",
|
|
||||||
"createAt": 0,
|
|
||||||
"fileInfos": Array [
|
|
||||||
Object {
|
|
||||||
"archived": false,
|
|
||||||
"clientId": "client_id",
|
|
||||||
"create_at": 1,
|
|
||||||
"delete_at": 1,
|
|
||||||
"extension": "jpg",
|
|
||||||
"has_preview_image": true,
|
|
||||||
"height": 200,
|
|
||||||
"id": "file_info_id",
|
|
||||||
"mime_type": "mime_type",
|
|
||||||
"name": "name",
|
|
||||||
"size": 1,
|
|
||||||
"update_at": 1,
|
|
||||||
"user_id": "user_id",
|
|
||||||
"width": 350,
|
|
||||||
},
|
|
||||||
Object {
|
|
||||||
"archived": false,
|
|
||||||
"clientId": "client_id",
|
|
||||||
"create_at": 1,
|
|
||||||
"delete_at": 1,
|
|
||||||
"extension": "jpg",
|
|
||||||
"has_preview_image": true,
|
|
||||||
"height": 200,
|
|
||||||
"id": "file_info_id",
|
|
||||||
"mime_type": "mime_type",
|
|
||||||
"name": "name",
|
|
||||||
"size": 1,
|
|
||||||
"update_at": 1,
|
|
||||||
"user_id": "user_id",
|
|
||||||
"width": 350,
|
|
||||||
},
|
|
||||||
Object {
|
|
||||||
"archived": false,
|
|
||||||
"clientId": "client_id",
|
|
||||||
"create_at": 1,
|
|
||||||
"delete_at": 1,
|
|
||||||
"extension": "jpg",
|
|
||||||
"has_preview_image": true,
|
|
||||||
"height": 200,
|
|
||||||
"id": "file_info_id",
|
|
||||||
"mime_type": "mime_type",
|
|
||||||
"name": "name",
|
|
||||||
"size": 1,
|
|
||||||
"update_at": 1,
|
|
||||||
"user_id": "user_id",
|
|
||||||
"width": 350,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
"message": "Test message",
|
|
||||||
"rootId": "",
|
|
||||||
"updateAt": 0,
|
|
||||||
"uploadsInProgress": Array [],
|
|
||||||
}
|
|
||||||
}
|
|
||||||
emitTypingEvent={[Function]}
|
|
||||||
enableEmojiPicker={true}
|
|
||||||
enableGifPicker={true}
|
|
||||||
errorClass={null}
|
|
||||||
fileUploadRef={
|
|
||||||
Object {
|
|
||||||
"current": null,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
getFileUploadTarget={[Function]}
|
|
||||||
handleBlur={[Function]}
|
|
||||||
handleChange={[Function]}
|
|
||||||
handleEmojiClick={[Function]}
|
|
||||||
handleFileUploadChange={[Function]}
|
|
||||||
handleFileUploadComplete={[Function]}
|
|
||||||
handleGifClick={[Function]}
|
|
||||||
handleMouseUpKeyUp={[Function]}
|
|
||||||
handlePostError={[Function]}
|
|
||||||
handleSubmit={[Function]}
|
|
||||||
handleUploadError={[Function]}
|
|
||||||
handleUploadProgress={[Function]}
|
|
||||||
handleUploadStart={[Function]}
|
|
||||||
hideEmojiPicker={[Function]}
|
|
||||||
isFormattingBarHidden={false}
|
|
||||||
loadNextMessage={[Function]}
|
|
||||||
loadPrevMessage={[Function]}
|
|
||||||
location="RHS_COMMENT"
|
|
||||||
maxPostSize={4000}
|
|
||||||
message="Test message"
|
|
||||||
onEditLatestPost={[Function]}
|
|
||||||
onMessageChange={[Function]}
|
|
||||||
postId=""
|
|
||||||
postMsgKeyPress={[Function]}
|
|
||||||
removePreview={[Function]}
|
|
||||||
serverError={null}
|
|
||||||
setShowPreview={[Function]}
|
|
||||||
shouldShowPreview={false}
|
|
||||||
showEmojiPicker={false}
|
|
||||||
textboxRef={
|
|
||||||
Object {
|
|
||||||
"current": null,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
toggleAdvanceTextEditor={[Function]}
|
|
||||||
toggleEmojiPicker={[Function]}
|
|
||||||
uploadsProgressPercent={Object {}}
|
|
||||||
useChannelMentions={true}
|
|
||||||
/>
|
|
||||||
</form>
|
|
||||||
`;
|
|
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@ -1,189 +1,6 @@
|
|||||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||||
// See LICENSE.txt for license information.
|
// See LICENSE.txt for license information.
|
||||||
|
|
||||||
import {connect} from 'react-redux';
|
|
||||||
import {bindActionCreators} from 'redux';
|
|
||||||
import type {Dispatch} from 'redux';
|
|
||||||
|
|
||||||
import {getChannelTimezones, getChannelMemberCountsByGroup} from 'mattermost-redux/actions/channels';
|
|
||||||
import {moveHistoryIndexBack, moveHistoryIndexForward, resetCreatePostRequest, resetHistoryIndex} from 'mattermost-redux/actions/posts';
|
|
||||||
import {savePreferences} from 'mattermost-redux/actions/preferences';
|
|
||||||
import {Permissions, Preferences, Posts} from 'mattermost-redux/constants';
|
|
||||||
import {getAllChannelStats, getChannelMemberCountsByGroup as selectChannelMemberCountsByGroup} from 'mattermost-redux/selectors/entities/channels';
|
|
||||||
import {getCurrentUserId} from 'mattermost-redux/selectors/entities/common';
|
|
||||||
import {getConfig, getLicense} from 'mattermost-redux/selectors/entities/general';
|
|
||||||
import {getAssociatedGroupsForReferenceByMention} from 'mattermost-redux/selectors/entities/groups';
|
|
||||||
import {makeGetMessageInHistoryItem} from 'mattermost-redux/selectors/entities/posts';
|
|
||||||
import {getBool, isCustomGroupsEnabled} from 'mattermost-redux/selectors/entities/preferences';
|
|
||||||
import {haveIChannelPermission} from 'mattermost-redux/selectors/entities/roles';
|
|
||||||
import {getCurrentTeamId} from 'mattermost-redux/selectors/entities/teams';
|
|
||||||
|
|
||||||
import {emitShortcutReactToLastPostFrom} from 'actions/post_actions';
|
|
||||||
import {
|
|
||||||
clearCommentDraftUploads,
|
|
||||||
updateCommentDraft,
|
|
||||||
makeOnSubmit,
|
|
||||||
makeOnEditLatestPost,
|
|
||||||
} from 'actions/views/create_comment';
|
|
||||||
import {searchAssociatedGroupsForReference} from 'actions/views/group';
|
|
||||||
import {openModal} from 'actions/views/modals';
|
|
||||||
import {focusedRHS} from 'actions/views/rhs';
|
|
||||||
import {setShowPreviewOnCreateComment} from 'actions/views/textbox';
|
|
||||||
import {getCurrentLocale} from 'selectors/i18n';
|
|
||||||
import {getPostDraft, getIsRhsExpanded, getSelectedPostFocussedAt} from 'selectors/rhs';
|
|
||||||
import {getShouldFocusRHS} from 'selectors/views/rhs';
|
|
||||||
import {connectionErrorCount} from 'selectors/views/system';
|
|
||||||
import {showPreviewOnCreateComment} from 'selectors/views/textbox';
|
|
||||||
|
|
||||||
import {AdvancedTextEditor, Constants, StoragePrefixes} from 'utils/constants';
|
|
||||||
import {canUploadFiles} from 'utils/file_utils';
|
|
||||||
|
|
||||||
import type {PostDraft} from 'types/store/draft';
|
|
||||||
import type {GlobalState} from 'types/store/index.js';
|
|
||||||
|
|
||||||
import AdvancedCreateComment from './advanced_create_comment';
|
import AdvancedCreateComment from './advanced_create_comment';
|
||||||
|
|
||||||
type OwnProps = {
|
export default AdvancedCreateComment;
|
||||||
rootId: string;
|
|
||||||
channelId: string;
|
|
||||||
latestPostId: string;
|
|
||||||
isPlugin?: boolean;
|
|
||||||
};
|
|
||||||
|
|
||||||
function makeMapStateToProps() {
|
|
||||||
const getMessageInHistoryItem = makeGetMessageInHistoryItem(Posts.MESSAGE_TYPES.COMMENT as 'comment');
|
|
||||||
|
|
||||||
return (state: GlobalState, ownProps: OwnProps) => {
|
|
||||||
const err = state.requests.posts.createPost.error || {};
|
|
||||||
|
|
||||||
const draft = getPostDraft(state, StoragePrefixes.COMMENT_DRAFT, ownProps.rootId);
|
|
||||||
const isRemoteDraft = state.views.drafts.remotes[`${StoragePrefixes.COMMENT_DRAFT}${ownProps.rootId}`] || false;
|
|
||||||
|
|
||||||
const channelMembersCount = getAllChannelStats(state)[ownProps.channelId] ? getAllChannelStats(state)[ownProps.channelId].member_count : 1;
|
|
||||||
const messageInHistory = getMessageInHistoryItem(state);
|
|
||||||
|
|
||||||
const channel = state.entities.channels.channels[ownProps.channelId] || {};
|
|
||||||
|
|
||||||
const config = getConfig(state);
|
|
||||||
const license = getLicense(state);
|
|
||||||
const currentUserId = getCurrentUserId(state);
|
|
||||||
const enableConfirmNotificationsToChannel = config.EnableConfirmNotificationsToChannel === 'true';
|
|
||||||
const enableEmojiPicker = config.EnableEmojiPicker === 'true';
|
|
||||||
const enableGifPicker = config.EnableGifPicker === 'true';
|
|
||||||
const badConnection = connectionErrorCount(state) > 1;
|
|
||||||
const canPost = haveIChannelPermission(state, channel.team_id, channel.id, Permissions.CREATE_POST);
|
|
||||||
const useChannelMentions = haveIChannelPermission(state, channel.team_id, channel.id, Permissions.USE_CHANNEL_MENTIONS);
|
|
||||||
const isLDAPEnabled = license?.IsLicensed === 'true' && license?.LDAPGroups === 'true';
|
|
||||||
const useCustomGroupMentions = isCustomGroupsEnabled(state) && haveIChannelPermission(state, channel.team_id, channel.id, Permissions.USE_GROUP_MENTIONS);
|
|
||||||
const useLDAPGroupMentions = isLDAPEnabled && haveIChannelPermission(state, channel.team_id, channel.id, Permissions.USE_GROUP_MENTIONS);
|
|
||||||
const channelMemberCountsByGroup = selectChannelMemberCountsByGroup(state, ownProps.channelId);
|
|
||||||
const groupsWithAllowReference = useLDAPGroupMentions || useCustomGroupMentions ? getAssociatedGroupsForReferenceByMention(state, channel.team_id, channel.id) : null;
|
|
||||||
const isFormattingBarHidden = getBool(state, Constants.Preferences.ADVANCED_TEXT_EDITOR, AdvancedTextEditor.COMMENT);
|
|
||||||
const currentTeamId = getCurrentTeamId(state);
|
|
||||||
const postEditorActions = state.plugins.components.PostEditorAction;
|
|
||||||
const shouldFocusRHS = getShouldFocusRHS(state);
|
|
||||||
|
|
||||||
return {
|
|
||||||
currentTeamId,
|
|
||||||
draft,
|
|
||||||
isRemoteDraft,
|
|
||||||
messageInHistory,
|
|
||||||
channelMembersCount,
|
|
||||||
currentUserId,
|
|
||||||
isFormattingBarHidden,
|
|
||||||
codeBlockOnCtrlEnter: getBool(state, Preferences.CATEGORY_ADVANCED_SETTINGS, 'code_block_ctrl_enter', true),
|
|
||||||
ctrlSend: getBool(state, Preferences.CATEGORY_ADVANCED_SETTINGS, 'send_on_ctrl_enter'),
|
|
||||||
createPostErrorId: err.server_error_id,
|
|
||||||
enableConfirmNotificationsToChannel,
|
|
||||||
enableEmojiPicker,
|
|
||||||
enableGifPicker,
|
|
||||||
locale: getCurrentLocale(state),
|
|
||||||
maxPostSize: parseInt(config.MaxPostSize || '', 10) || Constants.DEFAULT_CHARACTER_LIMIT,
|
|
||||||
rhsExpanded: getIsRhsExpanded(state),
|
|
||||||
badConnection,
|
|
||||||
selectedPostFocussedAt: getSelectedPostFocussedAt(state),
|
|
||||||
canPost,
|
|
||||||
useChannelMentions,
|
|
||||||
shouldShowPreview: showPreviewOnCreateComment(state),
|
|
||||||
groupsWithAllowReference,
|
|
||||||
useLDAPGroupMentions,
|
|
||||||
channelMemberCountsByGroup,
|
|
||||||
useCustomGroupMentions,
|
|
||||||
canUploadFiles: canUploadFiles(config),
|
|
||||||
postEditorActions,
|
|
||||||
shouldFocusRHS,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function makeOnUpdateCommentDraft(rootId: string, channelId: string) {
|
|
||||||
return (draft?: PostDraft, save = false) => updateCommentDraft(rootId, draft ? {...draft, channelId} : draft, save);
|
|
||||||
}
|
|
||||||
|
|
||||||
function makeUpdateCommentDraftWithRootId(channelId: string) {
|
|
||||||
return (rootId: string, draft?: PostDraft, save = false) => updateCommentDraft(rootId, draft ? {...draft, channelId} : draft, save);
|
|
||||||
}
|
|
||||||
|
|
||||||
function makeMapDispatchToProps() {
|
|
||||||
let onUpdateCommentDraft: ReturnType<typeof makeOnUpdateCommentDraft>;
|
|
||||||
let updateCommentDraftWithRootId: ReturnType<typeof makeUpdateCommentDraftWithRootId>;
|
|
||||||
let onSubmit: ReturnType<typeof makeOnSubmit>;
|
|
||||||
let onEditLatestPost: ReturnType<typeof makeOnEditLatestPost>;
|
|
||||||
|
|
||||||
function onResetHistoryIndex() {
|
|
||||||
return resetHistoryIndex(Posts.MESSAGE_TYPES.COMMENT);
|
|
||||||
}
|
|
||||||
|
|
||||||
let rootId: string;
|
|
||||||
let channelId: string;
|
|
||||||
let latestPostId: string;
|
|
||||||
|
|
||||||
return (dispatch: Dispatch, ownProps: OwnProps) => {
|
|
||||||
if (!ownProps.isPlugin) {
|
|
||||||
if (rootId !== ownProps.rootId) {
|
|
||||||
onUpdateCommentDraft = makeOnUpdateCommentDraft(ownProps.rootId, ownProps.channelId);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (channelId !== ownProps.channelId) {
|
|
||||||
updateCommentDraftWithRootId = makeUpdateCommentDraftWithRootId(ownProps.channelId);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (rootId !== ownProps.rootId) {
|
|
||||||
onEditLatestPost = makeOnEditLatestPost(ownProps.rootId);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (rootId !== ownProps.rootId || channelId !== ownProps.channelId || latestPostId !== ownProps.latestPostId) {
|
|
||||||
onSubmit = makeOnSubmit(ownProps.channelId, ownProps.rootId, ownProps.latestPostId);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
rootId = ownProps.rootId;
|
|
||||||
channelId = ownProps.channelId;
|
|
||||||
latestPostId = ownProps.latestPostId;
|
|
||||||
|
|
||||||
return bindActionCreators(
|
|
||||||
{
|
|
||||||
clearCommentDraftUploads,
|
|
||||||
onUpdateCommentDraft,
|
|
||||||
updateCommentDraftWithRootId,
|
|
||||||
onSubmit,
|
|
||||||
onResetHistoryIndex,
|
|
||||||
moveHistoryIndexBack,
|
|
||||||
moveHistoryIndexForward,
|
|
||||||
onEditLatestPost,
|
|
||||||
resetCreatePostRequest,
|
|
||||||
getChannelTimezones,
|
|
||||||
emitShortcutReactToLastPostFrom,
|
|
||||||
setShowPreview: setShowPreviewOnCreateComment,
|
|
||||||
getChannelMemberCountsByGroup,
|
|
||||||
openModal,
|
|
||||||
savePreferences,
|
|
||||||
searchAssociatedGroupsForReference,
|
|
||||||
focusedRHS,
|
|
||||||
},
|
|
||||||
dispatch,
|
|
||||||
);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export default connect(makeMapStateToProps, makeMapDispatchToProps, null, {forwardRef: true})(AdvancedCreateComment);
|
|
||||||
|
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@ -1,201 +1,6 @@
|
|||||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||||
// See LICENSE.txt for license information.
|
// See LICENSE.txt for license information.
|
||||||
|
|
||||||
import {connect} from 'react-redux';
|
|
||||||
import {bindActionCreators} from 'redux';
|
|
||||||
import type {Dispatch} from 'redux';
|
|
||||||
|
|
||||||
import type {FileInfo} from '@mattermost/types/files';
|
|
||||||
import type {Post} from '@mattermost/types/posts';
|
|
||||||
|
|
||||||
import {getChannelTimezones, getChannelMemberCountsByGroup} from 'mattermost-redux/actions/channels';
|
|
||||||
import {
|
|
||||||
addMessageIntoHistory,
|
|
||||||
moveHistoryIndexBack,
|
|
||||||
moveHistoryIndexForward,
|
|
||||||
removeReaction,
|
|
||||||
} from 'mattermost-redux/actions/posts';
|
|
||||||
import {savePreferences} from 'mattermost-redux/actions/preferences';
|
|
||||||
import {Permissions, Posts, Preferences as PreferencesRedux} from 'mattermost-redux/constants';
|
|
||||||
import {getCurrentChannelId, getCurrentChannel, getCurrentChannelStats, getChannelMemberCountsByGroup as selectChannelMemberCountsByGroup} from 'mattermost-redux/selectors/entities/channels';
|
|
||||||
import {getConfig, getLicense} from 'mattermost-redux/selectors/entities/general';
|
|
||||||
import {getAssociatedGroupsForReferenceByMention} from 'mattermost-redux/selectors/entities/groups';
|
|
||||||
import {
|
|
||||||
getCurrentUsersLatestPost,
|
|
||||||
getLatestReplyablePostId,
|
|
||||||
makeGetMessageInHistoryItem,
|
|
||||||
isPostPriorityEnabled,
|
|
||||||
} from 'mattermost-redux/selectors/entities/posts';
|
|
||||||
import {get, getInt, getBool, isCustomGroupsEnabled} from 'mattermost-redux/selectors/entities/preferences';
|
|
||||||
import {haveICurrentChannelPermission} from 'mattermost-redux/selectors/entities/roles';
|
|
||||||
import {getCurrentTeamId} from 'mattermost-redux/selectors/entities/teams';
|
|
||||||
import {getCurrentUserId, getStatusForUserId, getUser, isCurrentUserGuestUser} from 'mattermost-redux/selectors/entities/users';
|
|
||||||
import type {ActionFuncAsync} from 'mattermost-redux/types/actions.js';
|
|
||||||
|
|
||||||
import {executeCommand} from 'actions/command';
|
|
||||||
import {runMessageWillBePostedHooks, runSlashCommandWillBePostedHooks} from 'actions/hooks';
|
|
||||||
import {addReaction, createPost, setEditingPost, emitShortcutReactToLastPostFrom, submitReaction} from 'actions/post_actions';
|
|
||||||
import {actionOnGlobalItemsWithPrefix} from 'actions/storage';
|
|
||||||
import {scrollPostListToBottom} from 'actions/views/channel';
|
|
||||||
import {removeDraft, updateDraft} from 'actions/views/drafts';
|
|
||||||
import {searchAssociatedGroupsForReference} from 'actions/views/group';
|
|
||||||
import {openModal} from 'actions/views/modals';
|
|
||||||
import {selectPostFromRightHandSideSearchByPostId} from 'actions/views/rhs';
|
|
||||||
import {setShowPreviewOnCreatePost} from 'actions/views/textbox';
|
|
||||||
import {getEmojiMap} from 'selectors/emojis';
|
|
||||||
import {getCurrentLocale} from 'selectors/i18n';
|
|
||||||
import {makeGetChannelDraft, getIsRhsExpanded, getIsRhsOpen} from 'selectors/rhs';
|
|
||||||
import {connectionErrorCount} from 'selectors/views/system';
|
|
||||||
import {showPreviewOnCreatePost} from 'selectors/views/textbox';
|
|
||||||
|
|
||||||
import {OnboardingTourSteps, TutorialTourName, OnboardingTourStepsForGuestUsers} from 'components/tours';
|
|
||||||
|
|
||||||
import {AdvancedTextEditor, Constants, Preferences, StoragePrefixes, UserStatuses} from 'utils/constants';
|
|
||||||
import {canUploadFiles} from 'utils/file_utils';
|
|
||||||
|
|
||||||
import type {PostDraft} from 'types/store/draft';
|
|
||||||
import type {GlobalState} from 'types/store/index.js';
|
|
||||||
|
|
||||||
import AdvancedCreatePost from './advanced_create_post';
|
import AdvancedCreatePost from './advanced_create_post';
|
||||||
|
|
||||||
function makeMapStateToProps() {
|
export default AdvancedCreatePost;
|
||||||
const getMessageInHistoryItem = makeGetMessageInHistoryItem(Posts.MESSAGE_TYPES.POST as any);
|
|
||||||
const getChannelDraft = makeGetChannelDraft();
|
|
||||||
|
|
||||||
return (state: GlobalState) => {
|
|
||||||
const config = getConfig(state);
|
|
||||||
const license = getLicense(state);
|
|
||||||
const currentChannel = getCurrentChannel(state);
|
|
||||||
const currentChannelTeammateUsername = currentChannel ? getUser(state, currentChannel.teammate_id || '')?.username : undefined;
|
|
||||||
const draft = getChannelDraft(state, currentChannel?.id || '');
|
|
||||||
const isRemoteDraft = (currentChannel && state.views.drafts.remotes[`${StoragePrefixes.DRAFT}${currentChannel.id}`]) || false;
|
|
||||||
const latestReplyablePostId = getLatestReplyablePostId(state);
|
|
||||||
const currentChannelMembersCount = getCurrentChannelStats(state)?.member_count ?? 1;
|
|
||||||
const enableEmojiPicker = config.EnableEmojiPicker === 'true';
|
|
||||||
const enableGifPicker = config.EnableGifPicker === 'true';
|
|
||||||
const enableConfirmNotificationsToChannel = config.EnableConfirmNotificationsToChannel === 'true';
|
|
||||||
const currentUserId = getCurrentUserId(state);
|
|
||||||
const userIsOutOfOffice = getStatusForUserId(state, currentUserId) === UserStatuses.OUT_OF_OFFICE;
|
|
||||||
const badConnection = connectionErrorCount(state) > 1;
|
|
||||||
const canPost = haveICurrentChannelPermission(state, Permissions.CREATE_POST);
|
|
||||||
const useChannelMentions = haveICurrentChannelPermission(state, Permissions.USE_CHANNEL_MENTIONS);
|
|
||||||
const isLDAPEnabled = license?.IsLicensed === 'true' && license?.LDAPGroups === 'true';
|
|
||||||
const useCustomGroupMentions = isCustomGroupsEnabled(state) && haveICurrentChannelPermission(state, Permissions.USE_GROUP_MENTIONS);
|
|
||||||
const useLDAPGroupMentions = isLDAPEnabled && haveICurrentChannelPermission(state, Permissions.USE_GROUP_MENTIONS);
|
|
||||||
const channelMemberCountsByGroup = currentChannel ? selectChannelMemberCountsByGroup(state, currentChannel.id) : {};
|
|
||||||
const currentTeamId = getCurrentTeamId(state);
|
|
||||||
const groupsWithAllowReference = (currentChannel && (useLDAPGroupMentions || useCustomGroupMentions)) ?
|
|
||||||
getAssociatedGroupsForReferenceByMention(state, currentTeamId, currentChannel.id) :
|
|
||||||
null;
|
|
||||||
const enableTutorial = config.EnableTutorial === 'true';
|
|
||||||
const tutorialStep = getInt(state, TutorialTourName.ONBOARDING_TUTORIAL_STEP, currentUserId, 0);
|
|
||||||
|
|
||||||
// guest validation to see which point the messaging tour tip starts
|
|
||||||
const isGuestUser = isCurrentUserGuestUser(state);
|
|
||||||
const tourStep = isGuestUser ? OnboardingTourStepsForGuestUsers.SEND_MESSAGE : OnboardingTourSteps.SEND_MESSAGE;
|
|
||||||
const showSendTutorialTip = enableTutorial && tutorialStep === tourStep;
|
|
||||||
const isFormattingBarHidden = getBool(state, Preferences.ADVANCED_TEXT_EDITOR, AdvancedTextEditor.POST);
|
|
||||||
const postEditorActions = state.plugins.components.PostEditorAction;
|
|
||||||
|
|
||||||
return {
|
|
||||||
currentTeamId,
|
|
||||||
currentChannel,
|
|
||||||
currentChannelTeammateUsername,
|
|
||||||
currentChannelMembersCount,
|
|
||||||
currentUserId,
|
|
||||||
isFormattingBarHidden,
|
|
||||||
codeBlockOnCtrlEnter: getBool(state, PreferencesRedux.CATEGORY_ADVANCED_SETTINGS, 'code_block_ctrl_enter', true),
|
|
||||||
ctrlSend: getBool(state, Preferences.CATEGORY_ADVANCED_SETTINGS, 'send_on_ctrl_enter'),
|
|
||||||
fullWidthTextBox: get(state, Preferences.CATEGORY_DISPLAY_SETTINGS, Preferences.CHANNEL_DISPLAY_MODE, Preferences.CHANNEL_DISPLAY_MODE_DEFAULT) === Preferences.CHANNEL_DISPLAY_MODE_FULL_SCREEN,
|
|
||||||
showSendTutorialTip,
|
|
||||||
messageInHistoryItem: getMessageInHistoryItem(state),
|
|
||||||
draft,
|
|
||||||
isRemoteDraft,
|
|
||||||
latestReplyablePostId,
|
|
||||||
locale: getCurrentLocale(state),
|
|
||||||
currentUsersLatestPost: getCurrentUsersLatestPost(state, ''),
|
|
||||||
canUploadFiles: canUploadFiles(config),
|
|
||||||
enableEmojiPicker,
|
|
||||||
enableGifPicker,
|
|
||||||
enableConfirmNotificationsToChannel,
|
|
||||||
maxPostSize: parseInt(config.MaxPostSize || '', 10) || Constants.DEFAULT_CHARACTER_LIMIT,
|
|
||||||
userIsOutOfOffice,
|
|
||||||
rhsExpanded: getIsRhsExpanded(state),
|
|
||||||
rhsOpen: getIsRhsOpen(state),
|
|
||||||
emojiMap: getEmojiMap(state),
|
|
||||||
badConnection,
|
|
||||||
canPost,
|
|
||||||
useChannelMentions,
|
|
||||||
shouldShowPreview: showPreviewOnCreatePost(state),
|
|
||||||
groupsWithAllowReference,
|
|
||||||
useLDAPGroupMentions,
|
|
||||||
channelMemberCountsByGroup,
|
|
||||||
isLDAPEnabled,
|
|
||||||
useCustomGroupMentions,
|
|
||||||
isPostPriorityEnabled: isPostPriorityEnabled(state),
|
|
||||||
postEditorActions,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function onSubmitPost(post: Post, fileInfos: FileInfo[]) {
|
|
||||||
return (dispatch: Dispatch) => {
|
|
||||||
dispatch(createPost(post, fileInfos) as any);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function setDraft(key: string, value: PostDraft | null, draftChannelId: string, save = false): ActionFuncAsync<boolean, GlobalState> {
|
|
||||||
return (dispatch, getState) => {
|
|
||||||
const channelId = draftChannelId || getCurrentChannelId(getState());
|
|
||||||
let updatedValue = null;
|
|
||||||
if (value) {
|
|
||||||
updatedValue = {...value, channelId};
|
|
||||||
}
|
|
||||||
if (updatedValue) {
|
|
||||||
return dispatch(updateDraft(key, updatedValue, '', save));
|
|
||||||
}
|
|
||||||
|
|
||||||
return dispatch(removeDraft(key, channelId));
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function clearDraftUploads() {
|
|
||||||
return actionOnGlobalItemsWithPrefix(StoragePrefixes.DRAFT, (_key: string, draft: PostDraft) => {
|
|
||||||
if (!draft || !draft.uploadsInProgress || draft.uploadsInProgress.length === 0) {
|
|
||||||
return draft;
|
|
||||||
}
|
|
||||||
|
|
||||||
return {...draft, uploadsInProgress: []};
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function mapDispatchToProps(dispatch: Dispatch) {
|
|
||||||
return {
|
|
||||||
actions: bindActionCreators({
|
|
||||||
addMessageIntoHistory,
|
|
||||||
onSubmitPost,
|
|
||||||
moveHistoryIndexBack,
|
|
||||||
moveHistoryIndexForward,
|
|
||||||
submitReaction,
|
|
||||||
addReaction,
|
|
||||||
removeReaction,
|
|
||||||
setDraft,
|
|
||||||
clearDraftUploads,
|
|
||||||
selectPostFromRightHandSideSearchByPostId,
|
|
||||||
setEditingPost,
|
|
||||||
emitShortcutReactToLastPostFrom,
|
|
||||||
openModal,
|
|
||||||
executeCommand,
|
|
||||||
getChannelTimezones,
|
|
||||||
runMessageWillBePostedHooks,
|
|
||||||
runSlashCommandWillBePostedHooks,
|
|
||||||
scrollPostListToBottom,
|
|
||||||
setShowPreview: setShowPreviewOnCreatePost,
|
|
||||||
getChannelMemberCountsByGroup,
|
|
||||||
savePreferences,
|
|
||||||
searchAssociatedGroupsForReference,
|
|
||||||
}, dispatch),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export default connect(makeMapStateToProps, mapDispatchToProps)(AdvancedCreatePost);
|
|
||||||
|
@ -3,19 +3,24 @@
|
|||||||
|
|
||||||
import React, {useMemo, memo} from 'react';
|
import React, {useMemo, memo} from 'react';
|
||||||
import {defineMessage, useIntl} from 'react-intl';
|
import {defineMessage, useIntl} from 'react-intl';
|
||||||
|
import {useSelector} from 'react-redux';
|
||||||
import styled from 'styled-components';
|
import styled from 'styled-components';
|
||||||
|
|
||||||
import type {Channel} from '@mattermost/types/channels';
|
import {getChannel, getDirectTeammate} from 'mattermost-redux/selectors/entities/channels';
|
||||||
|
import {getUser} from 'mattermost-redux/selectors/entities/users';
|
||||||
|
|
||||||
import {trackEvent} from 'actions/telemetry_actions';
|
import {trackEvent} from 'actions/telemetry_actions';
|
||||||
|
|
||||||
import Chip from 'components/common/chip/chip';
|
import Chip from 'components/common/chip/chip';
|
||||||
|
|
||||||
|
import Constants from 'utils/constants';
|
||||||
|
|
||||||
|
import type {GlobalState} from 'types/store';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
prefillMessage: (msg: string, shouldFocus: boolean) => void;
|
prefillMessage: (msg: string, shouldFocus: boolean) => void;
|
||||||
currentChannel: Channel;
|
channelId: string;
|
||||||
currentUserId: string;
|
currentUserId: string;
|
||||||
currentChannelTeammateUsername?: string;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const UsernameMention = styled.span`
|
const UsernameMention = styled.span`
|
||||||
@ -28,8 +33,11 @@ const ChipContainer = styled.div`
|
|||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const PrewrittenChips = ({currentChannel, currentUserId, currentChannelTeammateUsername, prefillMessage}: Props) => {
|
const PrewrittenChips = ({channelId, currentUserId, prefillMessage}: Props) => {
|
||||||
const {formatMessage} = useIntl();
|
const {formatMessage} = useIntl();
|
||||||
|
const channelType = useSelector((state: GlobalState) => getChannel(state, channelId)?.type || Constants.OPEN_CHANNEL);
|
||||||
|
const channelTeammateId = useSelector((state: GlobalState) => getDirectTeammate(state, channelId)?.id || '');
|
||||||
|
const channelTeammateUsername = useSelector((state: GlobalState) => getUser(state, channelTeammateId)?.username || '');
|
||||||
|
|
||||||
const chips = useMemo(() => {
|
const chips = useMemo(() => {
|
||||||
const customChip = {
|
const customChip = {
|
||||||
@ -45,7 +53,11 @@ const PrewrittenChips = ({currentChannel, currentUserId, currentChannelTeammateU
|
|||||||
leadingIcon: '',
|
leadingIcon: '',
|
||||||
};
|
};
|
||||||
|
|
||||||
if (currentChannel.type === 'O' || currentChannel.type === 'P' || currentChannel.type === 'G') {
|
if (
|
||||||
|
channelType === Constants.OPEN_CHANNEL ||
|
||||||
|
channelType === Constants.PRIVATE_CHANNEL ||
|
||||||
|
channelType === Constants.GM_CHANNEL
|
||||||
|
) {
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
event: 'prefilled_message_selected_team_hi',
|
event: 'prefilled_message_selected_team_hi',
|
||||||
@ -87,7 +99,7 @@ const PrewrittenChips = ({currentChannel, currentUserId, currentChannelTeammateU
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
if (currentChannel.teammate_id === currentUserId) {
|
if (channelTeammateId === currentUserId) {
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
event: 'prefilled_message_selected_self_note',
|
event: 'prefilled_message_selected_self_note',
|
||||||
@ -144,12 +156,12 @@ const PrewrittenChips = ({currentChannel, currentUserId, currentChannelTeammateU
|
|||||||
},
|
},
|
||||||
customChip,
|
customChip,
|
||||||
];
|
];
|
||||||
}, [currentChannel, currentUserId]);
|
}, [channelType, channelTeammateId, currentUserId]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ChipContainer>
|
<ChipContainer>
|
||||||
{chips.map(({event, message, display, leadingIcon}) => {
|
{chips.map(({event, message, display, leadingIcon}) => {
|
||||||
const values = {username: currentChannelTeammateUsername};
|
const values = {username: channelTeammateUsername};
|
||||||
const messageToPrefill = message.id ? formatMessage(
|
const messageToPrefill = message.id ? formatMessage(
|
||||||
message,
|
message,
|
||||||
values,
|
values,
|
||||||
@ -157,15 +169,14 @@ const PrewrittenChips = ({currentChannel, currentUserId, currentChannelTeammateU
|
|||||||
|
|
||||||
const additionalMarkup = message.id === 'create_post.prewritten.tip.dm_hey' ? (
|
const additionalMarkup = message.id === 'create_post.prewritten.tip.dm_hey' ? (
|
||||||
<UsernameMention>
|
<UsernameMention>
|
||||||
{'@'}{currentChannelTeammateUsername}
|
{'@'}{channelTeammateUsername}
|
||||||
</UsernameMention>
|
</UsernameMention>
|
||||||
) : null;
|
) : null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Chip
|
<Chip
|
||||||
key={display.id}
|
key={display.id}
|
||||||
id={display.id}
|
display={display}
|
||||||
defaultMessage={display.defaultMessage}
|
|
||||||
additionalMarkup={additionalMarkup}
|
additionalMarkup={additionalMarkup}
|
||||||
values={values}
|
values={values}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||||
// See LICENSE.txt for license information.
|
// See LICENSE.txt for license information.
|
||||||
|
|
||||||
import {screen} from '@testing-library/react';
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
import type {Channel} from '@mattermost/types/channels';
|
import type {Channel} from '@mattermost/types/channels';
|
||||||
@ -12,7 +11,7 @@ import type {FileUpload} from 'components/file_upload/file_upload';
|
|||||||
import type Textbox from 'components/textbox/textbox';
|
import type Textbox from 'components/textbox/textbox';
|
||||||
|
|
||||||
import mergeObjects from 'packages/mattermost-redux/test/merge_objects';
|
import mergeObjects from 'packages/mattermost-redux/test/merge_objects';
|
||||||
import {renderWithContext, userEvent} from 'tests/react_testing_utils';
|
import {renderWithContext, userEvent, screen} from 'tests/react_testing_utils';
|
||||||
import {TestHelper} from 'utils/test_helper';
|
import {TestHelper} from 'utils/test_helper';
|
||||||
|
|
||||||
import type {PostDraft} from 'types/store/draft';
|
import type {PostDraft} from 'types/store/draft';
|
||||||
@ -146,71 +145,6 @@ const baseProps = {
|
|||||||
|
|
||||||
describe('components/avanced_text_editor/advanced_text_editor', () => {
|
describe('components/avanced_text_editor/advanced_text_editor', () => {
|
||||||
describe('keyDown behavior', () => {
|
describe('keyDown behavior', () => {
|
||||||
it('Enter should call postMsgKeyPress', () => {
|
|
||||||
const postMsgKeyPress = jest.fn();
|
|
||||||
renderWithContext(
|
|
||||||
<AdavancedTextEditor
|
|
||||||
{...baseProps}
|
|
||||||
postMsgKeyPress={postMsgKeyPress}
|
|
||||||
message={'test'}
|
|
||||||
/>,
|
|
||||||
mergeObjects(initialState, {
|
|
||||||
entities: {
|
|
||||||
roles: {
|
|
||||||
roles: {
|
|
||||||
user_roles: {permissions: [Permissions.CREATE_POST]},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
userEvent.type(screen.getByTestId('post_textbox'), '{enter}');
|
|
||||||
expect(postMsgKeyPress).toHaveBeenCalledTimes(1);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('Ctrl+up should call loadPrevMessage', () => {
|
|
||||||
const loadPrevMessage = jest.fn();
|
|
||||||
renderWithContext(
|
|
||||||
<AdavancedTextEditor
|
|
||||||
{...baseProps}
|
|
||||||
loadPrevMessage={loadPrevMessage}
|
|
||||||
/>,
|
|
||||||
mergeObjects(initialState, {
|
|
||||||
entities: {
|
|
||||||
roles: {
|
|
||||||
roles: {
|
|
||||||
user_roles: {permissions: [Permissions.CREATE_POST]},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
userEvent.type(screen.getByTestId('post_textbox'), '{ctrl}{arrowup}');
|
|
||||||
expect(loadPrevMessage).toHaveBeenCalledTimes(1);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('up should call onEditLatestPost', () => {
|
|
||||||
const onEditLatestPost = jest.fn();
|
|
||||||
renderWithContext(
|
|
||||||
<AdavancedTextEditor
|
|
||||||
{...baseProps}
|
|
||||||
onEditLatestPost={onEditLatestPost}
|
|
||||||
/>,
|
|
||||||
mergeObjects(initialState, {
|
|
||||||
entities: {
|
|
||||||
roles: {
|
|
||||||
roles: {
|
|
||||||
user_roles: {permissions: [Permissions.CREATE_POST]},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
userEvent.type(screen.getByTestId('post_textbox'), '{arrowup}');
|
|
||||||
expect(onEditLatestPost).toHaveBeenCalledTimes(1);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('ESC should blur the input', () => {
|
it('ESC should blur the input', () => {
|
||||||
renderWithContext(
|
renderWithContext(
|
||||||
<AdavancedTextEditor
|
<AdavancedTextEditor
|
||||||
@ -230,59 +164,5 @@ describe('components/avanced_text_editor/advanced_text_editor', () => {
|
|||||||
userEvent.type(textbox, 'something{esc}');
|
userEvent.type(textbox, 'something{esc}');
|
||||||
expect(textbox).not.toHaveFocus();
|
expect(textbox).not.toHaveFocus();
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('markdown', () => {
|
|
||||||
const ttcc = [
|
|
||||||
{
|
|
||||||
input: '{ctrl}b',
|
|
||||||
markdownMode: 'bold',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
input: '{ctrl}i',
|
|
||||||
markdownMode: 'italic',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
input: '{ctrl}k',
|
|
||||||
markdownMode: 'link',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
input: '{ctrl}{alt}k',
|
|
||||||
markdownMode: 'link',
|
|
||||||
},
|
|
||||||
];
|
|
||||||
for (const tc of ttcc) {
|
|
||||||
it(`component adds ${tc.markdownMode} markdown`, () => {
|
|
||||||
const applyMarkdown = jest.fn();
|
|
||||||
const message = 'Some markdown text';
|
|
||||||
const selectionStart = 5;
|
|
||||||
const selectionEnd = 10;
|
|
||||||
|
|
||||||
renderWithContext(
|
|
||||||
<AdavancedTextEditor
|
|
||||||
{...baseProps}
|
|
||||||
applyMarkdown={applyMarkdown}
|
|
||||||
message={'Some markdown text'}
|
|
||||||
/>,
|
|
||||||
mergeObjects(initialState, {
|
|
||||||
entities: {
|
|
||||||
roles: {
|
|
||||||
roles: {
|
|
||||||
user_roles: {permissions: [Permissions.CREATE_POST]},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
const textbox = screen.getByTestId('post_textbox');
|
|
||||||
userEvent.type(textbox, tc.input, {initialSelectionStart: selectionStart, initialSelectionEnd: selectionEnd});
|
|
||||||
expect(applyMarkdown).toHaveBeenCalledWith({
|
|
||||||
markdownMode: tc.markdownMode,
|
|
||||||
selectionStart,
|
|
||||||
selectionEnd,
|
|
||||||
message,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
File diff suppressed because it is too large
Load Diff
@ -4,11 +4,13 @@
|
|||||||
import {DateTime} from 'luxon';
|
import {DateTime} from 'luxon';
|
||||||
import React, {useState, useEffect} from 'react';
|
import React, {useState, useEffect} from 'react';
|
||||||
import {FormattedMessage} from 'react-intl';
|
import {FormattedMessage} from 'react-intl';
|
||||||
|
import {useSelector} from 'react-redux';
|
||||||
import styled from 'styled-components';
|
import styled from 'styled-components';
|
||||||
|
|
||||||
import type {UserProfile} from '@mattermost/types/users';
|
import type {GlobalState} from '@mattermost/types/store';
|
||||||
|
|
||||||
import {getTimezoneForUserProfile} from 'mattermost-redux/selectors/entities/timezone';
|
import {getTimezoneForUserProfile} from 'mattermost-redux/selectors/entities/timezone';
|
||||||
|
import {getUser} from 'mattermost-redux/selectors/entities/users';
|
||||||
|
|
||||||
import Moon from 'components/common/svg_images_components/moon_svg';
|
import Moon from 'components/common/svg_images_components/moon_svg';
|
||||||
import Timestamp from 'components/timestamp';
|
import Timestamp from 'components/timestamp';
|
||||||
@ -43,15 +45,26 @@ const Icon = styled(Moon)`
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
teammate: UserProfile;
|
teammateId: string;
|
||||||
displayName: string;
|
displayName: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const RemoteUserHour = ({teammate, displayName}: Props) => {
|
const DEFAULT_TIMEZONE = {
|
||||||
|
useAutomaticTimezone: true,
|
||||||
|
automaticTimezone: '',
|
||||||
|
manualTimezone: '',
|
||||||
|
};
|
||||||
|
|
||||||
|
const RemoteUserHour = ({teammateId, displayName}: Props) => {
|
||||||
const [timestamp, setTimestamp] = useState(0);
|
const [timestamp, setTimestamp] = useState(0);
|
||||||
const [showIt, setShowIt] = useState(false);
|
const [showIt, setShowIt] = useState(false);
|
||||||
|
|
||||||
const teammateTimezone = getTimezoneForUserProfile(teammate);
|
const teammateTimezone = useSelector((state: GlobalState) => {
|
||||||
|
const teammate = teammateId ? getUser(state, teammateId) : undefined;
|
||||||
|
return teammate ? getTimezoneForUserProfile(teammate) : DEFAULT_TIMEZONE;
|
||||||
|
}, (a, b) => a.automaticTimezone === b.automaticTimezone &&
|
||||||
|
a.manualTimezone === b.manualTimezone &&
|
||||||
|
a.useAutomaticTimezone === b.useAutomaticTimezone);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const teammateUserDate = DateTime.local().setZone(teammateTimezone.useAutomaticTimezone ? teammateTimezone.automaticTimezone : teammateTimezone.manualTimezone);
|
const teammateUserDate = DateTime.local().setZone(teammateTimezone.useAutomaticTimezone ? teammateTimezone.automaticTimezone : teammateTimezone.manualTimezone);
|
||||||
|
@ -0,0 +1,170 @@
|
|||||||
|
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||||
|
// See LICENSE.txt for license information.
|
||||||
|
|
||||||
|
import classNames from 'classnames';
|
||||||
|
import React, {useCallback, useRef, useState} from 'react';
|
||||||
|
import {useIntl} from 'react-intl';
|
||||||
|
import {useSelector} from 'react-redux';
|
||||||
|
|
||||||
|
import {EmoticonHappyOutlineIcon} from '@mattermost/compass-icons/components';
|
||||||
|
import type {Emoji} from '@mattermost/types/emojis';
|
||||||
|
|
||||||
|
import {getConfig} from 'mattermost-redux/selectors/entities/general';
|
||||||
|
import {getEmojiName} from 'mattermost-redux/utils/emoji_utils';
|
||||||
|
|
||||||
|
import useDidUpdate from 'components/common/hooks/useDidUpdate';
|
||||||
|
import EmojiPickerOverlay from 'components/emoji_picker/emoji_picker_overlay';
|
||||||
|
import KeyboardShortcutSequence, {KEYBOARD_SHORTCUTS} from 'components/keyboard_shortcuts/keyboard_shortcuts_sequence';
|
||||||
|
import OverlayTrigger from 'components/overlay_trigger';
|
||||||
|
import Tooltip from 'components/tooltip';
|
||||||
|
|
||||||
|
import Constants from 'utils/constants';
|
||||||
|
import {splitMessageBasedOnCaretPosition} from 'utils/post_utils';
|
||||||
|
|
||||||
|
import type {GlobalState} from 'types/store';
|
||||||
|
import type {PostDraft} from 'types/store/draft';
|
||||||
|
|
||||||
|
import {IconContainer} from './formatting_bar/formatting_icon';
|
||||||
|
|
||||||
|
const useEmojiPicker = (
|
||||||
|
readOnlyChannel: boolean,
|
||||||
|
draft: PostDraft,
|
||||||
|
caretPosition: number,
|
||||||
|
setCaretPosition: (pos: number) => void,
|
||||||
|
handleDraftChange: (draft: PostDraft) => void,
|
||||||
|
shouldShowPreview: boolean,
|
||||||
|
focusTextbox: () => void,
|
||||||
|
) => {
|
||||||
|
const intl = useIntl();
|
||||||
|
|
||||||
|
const enableEmojiPicker = useSelector((state: GlobalState) => getConfig(state).EnableEmojiPicker === 'true');
|
||||||
|
const enableGifPicker = useSelector((state: GlobalState) => getConfig(state).EnableGifPicker === 'true');
|
||||||
|
|
||||||
|
const emojiPickerRef = useRef<HTMLButtonElement>(null);
|
||||||
|
|
||||||
|
const [showEmojiPicker, setShowEmojiPicker] = useState(false);
|
||||||
|
|
||||||
|
const toggleEmojiPicker = useCallback((e?: React.MouseEvent<HTMLButtonElement, MouseEvent>): void => {
|
||||||
|
e?.stopPropagation();
|
||||||
|
setShowEmojiPicker((prev) => !prev);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const hideEmojiPicker = useCallback(() => {
|
||||||
|
setShowEmojiPicker(false);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const getEmojiPickerRef = useCallback(() => {
|
||||||
|
return emojiPickerRef.current;
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleEmojiClick = useCallback((emoji: Emoji) => {
|
||||||
|
const emojiAlias = getEmojiName(emoji);
|
||||||
|
|
||||||
|
if (!emojiAlias) {
|
||||||
|
//Oops.. There went something wrong
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let newMessage;
|
||||||
|
if (draft.message === '') {
|
||||||
|
newMessage = `:${emojiAlias}: `;
|
||||||
|
setCaretPosition(newMessage.length);
|
||||||
|
} else {
|
||||||
|
const {message} = draft;
|
||||||
|
const {firstPiece, lastPiece} = splitMessageBasedOnCaretPosition(caretPosition, message);
|
||||||
|
|
||||||
|
// check whether the first piece of the message is empty when cursor is placed at beginning of message and avoid adding an empty string at the beginning of the message
|
||||||
|
newMessage =
|
||||||
|
firstPiece === '' ? `:${emojiAlias}: ${lastPiece}` : `${firstPiece} :${emojiAlias}: ${lastPiece}`;
|
||||||
|
|
||||||
|
const newCaretPosition =
|
||||||
|
firstPiece === '' ? `:${emojiAlias}: `.length : `${firstPiece} :${emojiAlias}: `.length;
|
||||||
|
setCaretPosition(newCaretPosition);
|
||||||
|
}
|
||||||
|
|
||||||
|
handleDraftChange({
|
||||||
|
...draft,
|
||||||
|
message: newMessage,
|
||||||
|
});
|
||||||
|
|
||||||
|
setShowEmojiPicker(false);
|
||||||
|
}, [draft, caretPosition, handleDraftChange, setCaretPosition]);
|
||||||
|
|
||||||
|
const handleGifClick = useCallback((gif: string) => {
|
||||||
|
let newMessage: string;
|
||||||
|
if (draft.message === '') {
|
||||||
|
newMessage = gif;
|
||||||
|
} else if ((/\s+$/).test(draft.message)) {
|
||||||
|
// Check whether there is already a blank at the end of the current message
|
||||||
|
newMessage = `${draft.message}${gif} `;
|
||||||
|
} else {
|
||||||
|
newMessage = `${draft.message} ${gif} `;
|
||||||
|
}
|
||||||
|
|
||||||
|
handleDraftChange({
|
||||||
|
...draft,
|
||||||
|
message: newMessage,
|
||||||
|
});
|
||||||
|
|
||||||
|
setShowEmojiPicker(false);
|
||||||
|
}, [draft, handleDraftChange]);
|
||||||
|
|
||||||
|
// Focus textbox when the emoji picker closes
|
||||||
|
useDidUpdate(() => {
|
||||||
|
if (!showEmojiPicker) {
|
||||||
|
focusTextbox();
|
||||||
|
}
|
||||||
|
}, [showEmojiPicker]);
|
||||||
|
|
||||||
|
let emojiPicker = null;
|
||||||
|
|
||||||
|
if (enableEmojiPicker && !readOnlyChannel) {
|
||||||
|
const emojiPickerTooltip = (
|
||||||
|
<Tooltip id='upload-tooltip'>
|
||||||
|
<KeyboardShortcutSequence
|
||||||
|
shortcut={KEYBOARD_SHORTCUTS.msgShowEmojiPicker}
|
||||||
|
hoistDescription={true}
|
||||||
|
isInsideTooltip={true}
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
);
|
||||||
|
emojiPicker = (
|
||||||
|
<>
|
||||||
|
<EmojiPickerOverlay
|
||||||
|
show={showEmojiPicker}
|
||||||
|
target={getEmojiPickerRef}
|
||||||
|
onHide={hideEmojiPicker}
|
||||||
|
onEmojiClick={handleEmojiClick}
|
||||||
|
onGifClick={handleGifClick}
|
||||||
|
enableGifPicker={enableGifPicker}
|
||||||
|
topOffset={-7}
|
||||||
|
/>
|
||||||
|
<OverlayTrigger
|
||||||
|
placement='top'
|
||||||
|
delayShow={Constants.OVERLAY_TIME_DELAY}
|
||||||
|
trigger={Constants.OVERLAY_DEFAULT_TRIGGER}
|
||||||
|
overlay={emojiPickerTooltip}
|
||||||
|
>
|
||||||
|
<IconContainer
|
||||||
|
id={'emojiPickerButton'}
|
||||||
|
ref={emojiPickerRef}
|
||||||
|
onClick={toggleEmojiPicker}
|
||||||
|
type='button'
|
||||||
|
aria-label={intl.formatMessage({id: 'emoji_picker.emojiPicker.button.ariaLabel', defaultMessage: 'select an emoji'})}
|
||||||
|
disabled={shouldShowPreview}
|
||||||
|
className={classNames({active: showEmojiPicker})}
|
||||||
|
>
|
||||||
|
<EmoticonHappyOutlineIcon
|
||||||
|
color={'currentColor'}
|
||||||
|
size={18}
|
||||||
|
/>
|
||||||
|
</IconContainer>
|
||||||
|
</OverlayTrigger>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {emojiPicker, enableEmojiPicker, toggleEmojiPicker};
|
||||||
|
};
|
||||||
|
|
||||||
|
export default useEmojiPicker;
|
@ -0,0 +1,108 @@
|
|||||||
|
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||||
|
// See LICENSE.txt for license information.
|
||||||
|
|
||||||
|
import {useCallback, useEffect} from 'react';
|
||||||
|
import {useDispatch, useSelector} from 'react-redux';
|
||||||
|
|
||||||
|
import {GroupSource} from '@mattermost/types/groups';
|
||||||
|
|
||||||
|
import {getChannelMemberCountsByGroup} from 'mattermost-redux/actions/channels';
|
||||||
|
import {Permissions} from 'mattermost-redux/constants';
|
||||||
|
import {getChannel, getChannelMemberCountsByGroup as selectChannelMemberCountsByGroup} from 'mattermost-redux/selectors/entities/channels';
|
||||||
|
import {getLicense} from 'mattermost-redux/selectors/entities/general';
|
||||||
|
import {getAssociatedGroupsForReferenceByMention} from 'mattermost-redux/selectors/entities/groups';
|
||||||
|
import {isCustomGroupsEnabled} from 'mattermost-redux/selectors/entities/preferences';
|
||||||
|
import {haveIChannelPermission} from 'mattermost-redux/selectors/entities/roles';
|
||||||
|
import {getCurrentTeamId} from 'mattermost-redux/selectors/entities/teams';
|
||||||
|
|
||||||
|
import {searchAssociatedGroupsForReference} from 'actions/views/group';
|
||||||
|
|
||||||
|
import Constants from 'utils/constants';
|
||||||
|
import {groupsMentionedInText, mentionsMinusSpecialMentionsInText} from 'utils/post_utils';
|
||||||
|
|
||||||
|
import type {GlobalState} from 'types/store';
|
||||||
|
|
||||||
|
const useGroups = (
|
||||||
|
channelId: string,
|
||||||
|
message: string,
|
||||||
|
) => {
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
|
||||||
|
const teamId = useSelector((state: GlobalState) => {
|
||||||
|
const channel = getChannel(state, channelId);
|
||||||
|
return channel?.team_id || getCurrentTeamId(state);
|
||||||
|
});
|
||||||
|
|
||||||
|
const canUseLDAPGroupMentions = useSelector((state: GlobalState) => {
|
||||||
|
const channel = getChannel(state, channelId);
|
||||||
|
if (!channel) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const license = getLicense(state);
|
||||||
|
const isLDAPEnabled = license?.IsLicensed === 'true' && license?.LDAPGroups === 'true';
|
||||||
|
return isLDAPEnabled && haveIChannelPermission(state, channel.team_id, channel.id, Permissions.USE_GROUP_MENTIONS);
|
||||||
|
});
|
||||||
|
|
||||||
|
const canUseCustomGroupMentions = useSelector((state: GlobalState) => {
|
||||||
|
const channel = getChannel(state, channelId);
|
||||||
|
if (!channel) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return isCustomGroupsEnabled(state) && haveIChannelPermission(state, channel.team_id, channel.id, Permissions.USE_GROUP_MENTIONS);
|
||||||
|
});
|
||||||
|
|
||||||
|
const groupsWithAllowReference = useSelector((state: GlobalState) => {
|
||||||
|
const channel = getChannel(state, channelId);
|
||||||
|
if (!channel) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return canUseLDAPGroupMentions || canUseCustomGroupMentions ? getAssociatedGroupsForReferenceByMention(state, channel.team_id, channel.id) : null;
|
||||||
|
});
|
||||||
|
|
||||||
|
const channelMemberCountsByGroup = useSelector((state: GlobalState) => selectChannelMemberCountsByGroup(state, channelId));
|
||||||
|
|
||||||
|
const getGroupMentions = useCallback((message: string) => {
|
||||||
|
let memberNotifyCount = 0;
|
||||||
|
let channelTimezoneCount = 0;
|
||||||
|
let mentions: string[] = [];
|
||||||
|
if (canUseLDAPGroupMentions || canUseCustomGroupMentions) {
|
||||||
|
const mentionGroups = groupsMentionedInText(message, groupsWithAllowReference);
|
||||||
|
if (mentionGroups.length > 0) {
|
||||||
|
mentionGroups.
|
||||||
|
forEach((group) => {
|
||||||
|
if (group.source === GroupSource.Ldap && !canUseLDAPGroupMentions) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (group.source === GroupSource.Custom && !canUseCustomGroupMentions) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const mappedValue = channelMemberCountsByGroup[group.id];
|
||||||
|
if (mappedValue && mappedValue.channel_member_count > Constants.NOTIFY_ALL_MEMBERS && mappedValue.channel_member_count > memberNotifyCount) {
|
||||||
|
memberNotifyCount = mappedValue.channel_member_count;
|
||||||
|
channelTimezoneCount = mappedValue.channel_member_timezones_count;
|
||||||
|
}
|
||||||
|
mentions.push(`@${group.name}`);
|
||||||
|
});
|
||||||
|
mentions = [...new Set(mentions)];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return {mentions, memberNotifyCount, channelTimezoneCount};
|
||||||
|
}, [channelMemberCountsByGroup, groupsWithAllowReference, canUseCustomGroupMentions, canUseLDAPGroupMentions]);
|
||||||
|
|
||||||
|
// Get channel member counts by group on channel switch
|
||||||
|
useEffect(() => {
|
||||||
|
if (canUseLDAPGroupMentions || canUseCustomGroupMentions) {
|
||||||
|
const mentions = mentionsMinusSpecialMentionsInText(message);
|
||||||
|
|
||||||
|
if (mentions.length === 1) {
|
||||||
|
dispatch(searchAssociatedGroupsForReference(mentions[0], teamId, channelId));
|
||||||
|
} else if (mentions.length > 1) {
|
||||||
|
dispatch(getChannelMemberCountsByGroup(channelId));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [channelId]);
|
||||||
|
|
||||||
|
return getGroupMentions;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default useGroups;
|
@ -0,0 +1,385 @@
|
|||||||
|
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||||
|
// See LICENSE.txt for license information.
|
||||||
|
|
||||||
|
import type React from 'react';
|
||||||
|
import {useCallback, useEffect, useRef} from 'react';
|
||||||
|
import {useDispatch, useSelector} from 'react-redux';
|
||||||
|
|
||||||
|
import {getLatestReplyablePostId} from 'mattermost-redux/selectors/entities/posts';
|
||||||
|
import {getBool} from 'mattermost-redux/selectors/entities/preferences';
|
||||||
|
|
||||||
|
import {emitShortcutReactToLastPostFrom} from 'actions/post_actions';
|
||||||
|
import {editLatestPost} from 'actions/views/create_comment';
|
||||||
|
import {selectPostFromRightHandSideSearchByPostId} from 'actions/views/rhs';
|
||||||
|
|
||||||
|
import type {TextboxElement} from 'components/textbox';
|
||||||
|
import type TextboxClass from 'components/textbox/textbox';
|
||||||
|
|
||||||
|
import Constants, {Locations, Preferences} from 'utils/constants';
|
||||||
|
import * as Keyboard from 'utils/keyboard';
|
||||||
|
import {type ApplyMarkdownOptions} from 'utils/markdown/apply_markdown';
|
||||||
|
import {pasteHandler} from 'utils/paste';
|
||||||
|
import {isWithinCodeBlock, postMessageOnKeyPress} from 'utils/post_utils';
|
||||||
|
import * as UserAgent from 'utils/user_agent';
|
||||||
|
import * as Utils from 'utils/utils';
|
||||||
|
|
||||||
|
import type {GlobalState} from 'types/store';
|
||||||
|
import type {PostDraft} from 'types/store/draft';
|
||||||
|
|
||||||
|
const KeyCodes = Constants.KeyCodes;
|
||||||
|
|
||||||
|
const useKeyHandler = (
|
||||||
|
draft: PostDraft,
|
||||||
|
channelId: string,
|
||||||
|
postId: string,
|
||||||
|
caretPosition: number,
|
||||||
|
isValidPersistentNotifications: boolean,
|
||||||
|
location: string,
|
||||||
|
textboxRef: React.RefObject<TextboxClass>,
|
||||||
|
focusTextbox: (forceFocus?: boolean) => void,
|
||||||
|
applyMarkdown: (params: ApplyMarkdownOptions) => void,
|
||||||
|
handleDraftChange: (draft: PostDraft, options?: {instant?: boolean; show?: boolean}) => void,
|
||||||
|
handleSubmit: (e: React.FormEvent, submittingDraft?: PostDraft) => void,
|
||||||
|
emitTypingEvent: () => void,
|
||||||
|
toggleShowPreview: () => void,
|
||||||
|
toggleAdvanceTextEditor: () => void,
|
||||||
|
toggleEmojiPicker: () => void,
|
||||||
|
): [
|
||||||
|
(e: React.KeyboardEvent<TextboxElement>) => void,
|
||||||
|
(e: React.KeyboardEvent<TextboxElement>) => void,
|
||||||
|
] => {
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
|
||||||
|
const ctrlSend = useSelector((state: GlobalState) => getBool(state, Preferences.CATEGORY_ADVANCED_SETTINGS, 'send_on_ctrl_enter'));
|
||||||
|
const codeBlockOnCtrlEnter = useSelector((state: GlobalState) => getBool(state, Preferences.CATEGORY_ADVANCED_SETTINGS, 'code_block_ctrl_enter', true));
|
||||||
|
const messageHistory = useSelector((state: GlobalState) => state.entities.posts.messagesHistory.messages);
|
||||||
|
|
||||||
|
const timeoutId = useRef<number>();
|
||||||
|
const messageHistoryIndex = useRef(messageHistory.length);
|
||||||
|
const lastChannelSwitchAt = useRef(0);
|
||||||
|
const isNonFormattedPaste = useRef(false);
|
||||||
|
|
||||||
|
const latestReplyablePostId = useSelector((state: GlobalState) => (postId ? '' : getLatestReplyablePostId(state)));
|
||||||
|
const replyToLastPost = useCallback((e: React.KeyboardEvent) => {
|
||||||
|
if (postId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
e.preventDefault();
|
||||||
|
const replyBox = document.getElementById('reply_textbox');
|
||||||
|
if (replyBox) {
|
||||||
|
replyBox.focus();
|
||||||
|
}
|
||||||
|
if (latestReplyablePostId) {
|
||||||
|
dispatch(selectPostFromRightHandSideSearchByPostId(latestReplyablePostId));
|
||||||
|
}
|
||||||
|
}, [latestReplyablePostId, dispatch, postId]);
|
||||||
|
|
||||||
|
const onEditLatestPost = useCallback((e: React.KeyboardEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
const {data: canEditNow} = dispatch(editLatestPost(channelId, postId));
|
||||||
|
if (!canEditNow) {
|
||||||
|
focusTextbox(true);
|
||||||
|
}
|
||||||
|
}, [focusTextbox, channelId, postId, dispatch]);
|
||||||
|
|
||||||
|
const loadPrevMessage = useCallback((e: React.KeyboardEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
if (messageHistoryIndex.current === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
messageHistoryIndex.current -= 1;
|
||||||
|
handleDraftChange({
|
||||||
|
...draft,
|
||||||
|
message: messageHistory[messageHistoryIndex.current] || '',
|
||||||
|
});
|
||||||
|
}, [draft, handleDraftChange, messageHistory]);
|
||||||
|
|
||||||
|
const loadNextMessage = useCallback((e: React.KeyboardEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
if (messageHistoryIndex.current >= messageHistory.length) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
messageHistoryIndex.current += 1;
|
||||||
|
handleDraftChange({
|
||||||
|
...draft,
|
||||||
|
message: messageHistory[messageHistoryIndex.current] || '',
|
||||||
|
});
|
||||||
|
}, [draft, handleDraftChange, messageHistory]);
|
||||||
|
|
||||||
|
const postMsgKeyPress = useCallback((e: React.KeyboardEvent<TextboxElement>) => {
|
||||||
|
const {allowSending, withClosedCodeBlock, ignoreKeyPress, message} = postMessageOnKeyPress(
|
||||||
|
e,
|
||||||
|
draft.message,
|
||||||
|
ctrlSend,
|
||||||
|
codeBlockOnCtrlEnter,
|
||||||
|
postId ? 0 : Date.now(),
|
||||||
|
postId ? 0 : lastChannelSwitchAt.current,
|
||||||
|
caretPosition,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (ignoreKeyPress) {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (allowSending && isValidPersistentNotifications) {
|
||||||
|
e.persist?.();
|
||||||
|
|
||||||
|
// textboxRef.current?.blur();
|
||||||
|
|
||||||
|
if (withClosedCodeBlock && message) {
|
||||||
|
handleSubmit(e, {...draft, message});
|
||||||
|
} else {
|
||||||
|
handleSubmit(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
// setTimeout(() => {
|
||||||
|
// focusTextbox();
|
||||||
|
// });
|
||||||
|
}
|
||||||
|
|
||||||
|
emitTypingEvent();
|
||||||
|
}, [draft, ctrlSend, codeBlockOnCtrlEnter, caretPosition, postId, emitTypingEvent, handleSubmit, isValidPersistentNotifications]);
|
||||||
|
|
||||||
|
const handleKeyDown = useCallback((e: React.KeyboardEvent<TextboxElement>) => {
|
||||||
|
const ctrlOrMetaKeyPressed = e.ctrlKey || e.metaKey;
|
||||||
|
const ctrlEnterKeyCombo = (ctrlSend || codeBlockOnCtrlEnter) &&
|
||||||
|
Keyboard.isKeyPressed(e, KeyCodes.ENTER) &&
|
||||||
|
ctrlOrMetaKeyPressed;
|
||||||
|
|
||||||
|
const ctrlKeyCombo = Keyboard.cmdOrCtrlPressed(e) && !e.altKey && !e.shiftKey;
|
||||||
|
const ctrlAltCombo = Keyboard.cmdOrCtrlPressed(e, true) && e.altKey;
|
||||||
|
const shiftAltCombo = !Keyboard.cmdOrCtrlPressed(e) && e.shiftKey && e.altKey;
|
||||||
|
const ctrlShiftCombo = Keyboard.cmdOrCtrlPressed(e, true) && e.shiftKey;
|
||||||
|
|
||||||
|
// fix for FF not capturing the paste without formatting event when using ctrl|cmd + shift + v
|
||||||
|
if (e.key === KeyCodes.V[0] && ctrlOrMetaKeyPressed) {
|
||||||
|
if (e.shiftKey) {
|
||||||
|
isNonFormattedPaste.current = true;
|
||||||
|
timeoutId.current = window.setTimeout(() => {
|
||||||
|
isNonFormattedPaste.current = false;
|
||||||
|
}, 250);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// listen for line break key combo and insert new line character
|
||||||
|
if (Utils.isUnhandledLineBreakKeyCombo(e)) {
|
||||||
|
handleDraftChange({
|
||||||
|
...draft,
|
||||||
|
message: Utils.insertLineBreakFromKeyEvent(e.nativeEvent),
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ctrlEnterKeyCombo) {
|
||||||
|
postMsgKeyPress(e);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Keyboard.isKeyPressed(e, KeyCodes.ESCAPE)) {
|
||||||
|
textboxRef.current?.blur();
|
||||||
|
}
|
||||||
|
|
||||||
|
const upKeyOnly = !ctrlOrMetaKeyPressed && !e.altKey && !e.shiftKey && Keyboard.isKeyPressed(e, KeyCodes.UP);
|
||||||
|
const messageIsEmpty = draft.message.length === 0;
|
||||||
|
const allowHistoryNavigation = draft.message.length === 0 || draft.message === messageHistory[messageHistoryIndex.current];
|
||||||
|
const caretIsWithinCodeBlock = caretPosition && isWithinCodeBlock(draft.message, caretPosition); // REVIEW
|
||||||
|
|
||||||
|
if (upKeyOnly && messageIsEmpty) {
|
||||||
|
e.preventDefault();
|
||||||
|
if (textboxRef.current) {
|
||||||
|
textboxRef.current.blur();
|
||||||
|
}
|
||||||
|
|
||||||
|
onEditLatestPost(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
const {
|
||||||
|
selectionStart,
|
||||||
|
selectionEnd,
|
||||||
|
value,
|
||||||
|
} = e.target as TextboxElement;
|
||||||
|
|
||||||
|
if (ctrlKeyCombo && !caretIsWithinCodeBlock) {
|
||||||
|
if (allowHistoryNavigation && Keyboard.isKeyPressed(e, KeyCodes.UP)) {
|
||||||
|
e.stopPropagation();
|
||||||
|
e.preventDefault();
|
||||||
|
loadPrevMessage(e);
|
||||||
|
} else if (allowHistoryNavigation && Keyboard.isKeyPressed(e, KeyCodes.DOWN)) {
|
||||||
|
e.stopPropagation();
|
||||||
|
e.preventDefault();
|
||||||
|
loadNextMessage(e);
|
||||||
|
} else if (Keyboard.isKeyPressed(e, KeyCodes.B)) {
|
||||||
|
e.stopPropagation();
|
||||||
|
e.preventDefault();
|
||||||
|
applyMarkdown({
|
||||||
|
markdownMode: 'bold',
|
||||||
|
selectionStart,
|
||||||
|
selectionEnd,
|
||||||
|
message: value,
|
||||||
|
});
|
||||||
|
} else if (Keyboard.isKeyPressed(e, KeyCodes.I)) {
|
||||||
|
e.stopPropagation();
|
||||||
|
e.preventDefault();
|
||||||
|
applyMarkdown({
|
||||||
|
markdownMode: 'italic',
|
||||||
|
selectionStart,
|
||||||
|
selectionEnd,
|
||||||
|
message: value,
|
||||||
|
});
|
||||||
|
} else if (Utils.isTextSelectedInPostOrReply(e) && Keyboard.isKeyPressed(e, KeyCodes.K)) {
|
||||||
|
e.stopPropagation();
|
||||||
|
e.preventDefault();
|
||||||
|
applyMarkdown({
|
||||||
|
markdownMode: 'link',
|
||||||
|
selectionStart,
|
||||||
|
selectionEnd,
|
||||||
|
message: value,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else if (ctrlAltCombo && !caretIsWithinCodeBlock) {
|
||||||
|
if (Keyboard.isKeyPressed(e, KeyCodes.K)) {
|
||||||
|
e.stopPropagation();
|
||||||
|
e.preventDefault();
|
||||||
|
applyMarkdown({
|
||||||
|
markdownMode: 'link',
|
||||||
|
selectionStart,
|
||||||
|
selectionEnd,
|
||||||
|
message: value,
|
||||||
|
});
|
||||||
|
} else if (Keyboard.isKeyPressed(e, KeyCodes.C)) {
|
||||||
|
e.stopPropagation();
|
||||||
|
e.preventDefault();
|
||||||
|
applyMarkdown({
|
||||||
|
markdownMode: 'code',
|
||||||
|
selectionStart,
|
||||||
|
selectionEnd,
|
||||||
|
message: value,
|
||||||
|
});
|
||||||
|
} else if (Keyboard.isKeyPressed(e, KeyCodes.E)) {
|
||||||
|
e.stopPropagation();
|
||||||
|
e.preventDefault();
|
||||||
|
toggleEmojiPicker();
|
||||||
|
} else if (Keyboard.isKeyPressed(e, KeyCodes.T)) {
|
||||||
|
e.stopPropagation();
|
||||||
|
e.preventDefault();
|
||||||
|
toggleAdvanceTextEditor();
|
||||||
|
} else if (Keyboard.isKeyPressed(e, KeyCodes.P) && draft.message.length && !UserAgent.isMac()) {
|
||||||
|
e.stopPropagation();
|
||||||
|
e.preventDefault();
|
||||||
|
toggleShowPreview();
|
||||||
|
}
|
||||||
|
} else if (shiftAltCombo && !caretIsWithinCodeBlock) {
|
||||||
|
if (Keyboard.isKeyPressed(e, KeyCodes.X)) {
|
||||||
|
e.stopPropagation();
|
||||||
|
e.preventDefault();
|
||||||
|
applyMarkdown({
|
||||||
|
markdownMode: 'strike',
|
||||||
|
selectionStart,
|
||||||
|
selectionEnd,
|
||||||
|
message: value,
|
||||||
|
});
|
||||||
|
} else if (Keyboard.isKeyPressed(e, KeyCodes.SEVEN)) {
|
||||||
|
e.preventDefault();
|
||||||
|
applyMarkdown({
|
||||||
|
markdownMode: 'ol',
|
||||||
|
selectionStart,
|
||||||
|
selectionEnd,
|
||||||
|
message: value,
|
||||||
|
});
|
||||||
|
} else if (Keyboard.isKeyPressed(e, KeyCodes.EIGHT)) {
|
||||||
|
e.preventDefault();
|
||||||
|
applyMarkdown({
|
||||||
|
markdownMode: 'ul',
|
||||||
|
selectionStart,
|
||||||
|
selectionEnd,
|
||||||
|
message: value,
|
||||||
|
});
|
||||||
|
} else if (Keyboard.isKeyPressed(e, KeyCodes.NINE)) {
|
||||||
|
e.preventDefault();
|
||||||
|
applyMarkdown({
|
||||||
|
markdownMode: 'quote',
|
||||||
|
selectionStart,
|
||||||
|
selectionEnd,
|
||||||
|
message: value,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else if (ctrlShiftCombo && !caretIsWithinCodeBlock) {
|
||||||
|
if (Keyboard.isKeyPressed(e, KeyCodes.P) && draft.message.length && UserAgent.isMac()) { // REVIEW
|
||||||
|
e.stopPropagation();
|
||||||
|
e.preventDefault();
|
||||||
|
toggleShowPreview();
|
||||||
|
} else if (Keyboard.isKeyPressed(e, KeyCodes.E)) {
|
||||||
|
e.stopPropagation();
|
||||||
|
e.preventDefault();
|
||||||
|
toggleEmojiPicker();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const lastMessageReactionKeyCombo = ctrlShiftCombo && Keyboard.isKeyPressed(e, KeyCodes.BACK_SLASH);
|
||||||
|
if (lastMessageReactionKeyCombo) {
|
||||||
|
e.stopPropagation();
|
||||||
|
e.preventDefault();
|
||||||
|
dispatch(emitShortcutReactToLastPostFrom(postId ? Locations.RHS_ROOT : Locations.CENTER));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!postId) {
|
||||||
|
const shiftUpKeyCombo = !ctrlOrMetaKeyPressed && !e.altKey && e.shiftKey && Keyboard.isKeyPressed(e, KeyCodes.UP);
|
||||||
|
if (shiftUpKeyCombo && messageIsEmpty) {
|
||||||
|
replyToLastPost?.(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [
|
||||||
|
applyMarkdown,
|
||||||
|
caretPosition,
|
||||||
|
codeBlockOnCtrlEnter,
|
||||||
|
ctrlSend,
|
||||||
|
dispatch,
|
||||||
|
draft,
|
||||||
|
handleDraftChange,
|
||||||
|
loadNextMessage,
|
||||||
|
loadPrevMessage,
|
||||||
|
messageHistory,
|
||||||
|
onEditLatestPost,
|
||||||
|
postId,
|
||||||
|
postMsgKeyPress,
|
||||||
|
replyToLastPost,
|
||||||
|
textboxRef,
|
||||||
|
toggleAdvanceTextEditor,
|
||||||
|
toggleEmojiPicker,
|
||||||
|
toggleShowPreview,
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Register paste events
|
||||||
|
useEffect(() => {
|
||||||
|
function onPaste(event: ClipboardEvent) {
|
||||||
|
pasteHandler(event, location, draft.message, isNonFormattedPaste.current, caretPosition);
|
||||||
|
}
|
||||||
|
|
||||||
|
document.addEventListener('paste', onPaste);
|
||||||
|
return () => {
|
||||||
|
document.removeEventListener('paste', onPaste);
|
||||||
|
};
|
||||||
|
}, [location, draft.message, caretPosition]);
|
||||||
|
|
||||||
|
// Reset history index
|
||||||
|
useEffect(() => {
|
||||||
|
if (messageHistoryIndex.current === messageHistory.length) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (draft.message !== messageHistory[messageHistoryIndex.current]) {
|
||||||
|
messageHistoryIndex.current = messageHistory.length;
|
||||||
|
}
|
||||||
|
}, [draft.message]);
|
||||||
|
|
||||||
|
// Update last channel switch at
|
||||||
|
useEffect(() => {
|
||||||
|
lastChannelSwitchAt.current = Date.now();
|
||||||
|
}, [channelId]);
|
||||||
|
|
||||||
|
return [handleKeyDown, postMsgKeyPress];
|
||||||
|
};
|
||||||
|
|
||||||
|
export default useKeyHandler;
|
@ -0,0 +1,63 @@
|
|||||||
|
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||||
|
// See LICENSE.txt for license information.
|
||||||
|
|
||||||
|
import {useCallback, useEffect, useRef} from 'react';
|
||||||
|
|
||||||
|
import type TextboxClass from 'components/textbox/textbox';
|
||||||
|
|
||||||
|
import * as UserAgent from 'utils/user_agent';
|
||||||
|
|
||||||
|
const useOrientationHandler = (
|
||||||
|
textboxRef: React.RefObject<TextboxClass>,
|
||||||
|
postId: string,
|
||||||
|
) => {
|
||||||
|
const lastOrientation = useRef('');
|
||||||
|
|
||||||
|
const onOrientationChange = useCallback(() => {
|
||||||
|
if (!UserAgent.isIosWeb()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const LANDSCAPE_ANGLE = 90;
|
||||||
|
let orientation = 'portrait';
|
||||||
|
if (window.orientation) {
|
||||||
|
orientation = Math.abs(window.orientation as number) === LANDSCAPE_ANGLE ? 'landscape' : 'portrait';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (window.screen.orientation) {
|
||||||
|
orientation = window.screen.orientation.type.split('-')[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
lastOrientation.current &&
|
||||||
|
orientation !== lastOrientation.current &&
|
||||||
|
(document.activeElement || {}).id === 'post_textbox'
|
||||||
|
) {
|
||||||
|
textboxRef.current?.blur();
|
||||||
|
}
|
||||||
|
|
||||||
|
lastOrientation.current = orientation;
|
||||||
|
}, [textboxRef]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!postId && UserAgent.isIosWeb()) {
|
||||||
|
onOrientationChange();
|
||||||
|
if (window.screen.orientation && 'onchange' in window.screen.orientation) {
|
||||||
|
window.screen.orientation.addEventListener('change', onOrientationChange);
|
||||||
|
} else if ('onorientationchange' in window) {
|
||||||
|
window.addEventListener('orientationchange', onOrientationChange);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return () => {
|
||||||
|
if (!postId) {
|
||||||
|
if (window.screen.orientation && 'onchange' in window.screen.orientation) {
|
||||||
|
window.screen.orientation.removeEventListener('change', onOrientationChange);
|
||||||
|
} else if ('onorientationchange' in window) {
|
||||||
|
window.removeEventListener('orientationchange', onOrientationChange);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default useOrientationHandler;
|
@ -0,0 +1,56 @@
|
|||||||
|
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||||
|
// See LICENSE.txt for license information.
|
||||||
|
|
||||||
|
import React, {useCallback, useMemo} from 'react';
|
||||||
|
import {useSelector} from 'react-redux';
|
||||||
|
|
||||||
|
import type TextboxClass from 'components/textbox/textbox';
|
||||||
|
|
||||||
|
import type {GlobalState} from 'types/store';
|
||||||
|
import type {PostDraft} from 'types/store/draft';
|
||||||
|
|
||||||
|
const usePluginItems = (
|
||||||
|
draft: PostDraft,
|
||||||
|
textboxRef: React.RefObject<TextboxClass>,
|
||||||
|
handleDraftChange: (draft: PostDraft) => void,
|
||||||
|
) => {
|
||||||
|
const postEditorActions = useSelector((state: GlobalState) => state.plugins.components.PostEditorAction);
|
||||||
|
|
||||||
|
const getSelectedText = useCallback(() => {
|
||||||
|
const input = textboxRef.current?.getInputBox();
|
||||||
|
|
||||||
|
return {
|
||||||
|
start: input?.selectionStart,
|
||||||
|
end: input?.selectionEnd,
|
||||||
|
};
|
||||||
|
}, [textboxRef]);
|
||||||
|
|
||||||
|
const updateText = useCallback((message: string) => {
|
||||||
|
handleDraftChange({
|
||||||
|
...draft,
|
||||||
|
message,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Missing setting the state eventually?
|
||||||
|
}, [handleDraftChange, draft]);
|
||||||
|
|
||||||
|
const items = useMemo(() => postEditorActions?.map((item) => {
|
||||||
|
if (!item.component) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const Component = item.component as any;
|
||||||
|
return (
|
||||||
|
<Component
|
||||||
|
key={item.id}
|
||||||
|
draft={draft}
|
||||||
|
getSelectedText={getSelectedText}
|
||||||
|
updateText={updateText}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}), [postEditorActions, draft, getSelectedText, updateText]);
|
||||||
|
|
||||||
|
return items;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default usePluginItems;
|
@ -0,0 +1,168 @@
|
|||||||
|
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||||
|
// See LICENSE.txt for license information.
|
||||||
|
|
||||||
|
import React, {useCallback, useMemo} from 'react';
|
||||||
|
import {useDispatch, useSelector} from 'react-redux';
|
||||||
|
|
||||||
|
import type {Channel} from '@mattermost/types/channels';
|
||||||
|
import type {PostPriorityMetadata} from '@mattermost/types/posts';
|
||||||
|
import {PostPriority} from '@mattermost/types/posts';
|
||||||
|
|
||||||
|
import {getChannel} from 'mattermost-redux/selectors/entities/channels';
|
||||||
|
import {isPostPriorityEnabled as isPostPriorityEnabledSelector} from 'mattermost-redux/selectors/entities/posts';
|
||||||
|
import {getUser} from 'mattermost-redux/selectors/entities/users';
|
||||||
|
|
||||||
|
import {openModal} from 'actions/views/modals';
|
||||||
|
|
||||||
|
import PersistNotificationConfirmModal from 'components/persist_notification_confirm_modal';
|
||||||
|
import PostPriorityPickerOverlay from 'components/post_priority/post_priority_picker_overlay';
|
||||||
|
|
||||||
|
import Constants, {ModalIdentifiers} from 'utils/constants';
|
||||||
|
import {hasRequestedPersistentNotifications, mentionsMinusSpecialMentionsInText, specialMentionsInText} from 'utils/post_utils';
|
||||||
|
|
||||||
|
import type {GlobalState} from 'types/store';
|
||||||
|
import type {PostDraft} from 'types/store/draft';
|
||||||
|
|
||||||
|
import PriorityLabels from './priority_labels';
|
||||||
|
|
||||||
|
const usePriority = (
|
||||||
|
draft: PostDraft,
|
||||||
|
handleDraftChange: (draft: PostDraft, options: {instant?: boolean; show?: boolean}) => void,
|
||||||
|
focusTextbox: (keepFocus?: boolean) => void,
|
||||||
|
shouldShowPreview: boolean,
|
||||||
|
) => {
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
|
||||||
|
const isPostPriorityEnabled = useSelector(isPostPriorityEnabledSelector);
|
||||||
|
const channelType = useSelector((state: GlobalState) => getChannel(state, draft.channelId)?.type || 'O');
|
||||||
|
const channelTeammateUsername = useSelector((state: GlobalState) => {
|
||||||
|
const channel = getChannel(state, draft.channelId);
|
||||||
|
return getUser(state, channel?.teammate_id || '')?.username || '';
|
||||||
|
});
|
||||||
|
|
||||||
|
const hasPrioritySet = isPostPriorityEnabled &&
|
||||||
|
draft.metadata?.priority &&
|
||||||
|
(
|
||||||
|
draft.metadata.priority.priority ||
|
||||||
|
draft.metadata.priority.requested_ack
|
||||||
|
);
|
||||||
|
|
||||||
|
const specialMentions = useMemo(() => {
|
||||||
|
return specialMentionsInText(draft.message);
|
||||||
|
}, [draft.message]);
|
||||||
|
|
||||||
|
const hasSpecialMentions = useMemo(() => {
|
||||||
|
return Object.values(specialMentions).includes(true);
|
||||||
|
}, [specialMentions]);
|
||||||
|
|
||||||
|
const isValidPersistentNotifications = useMemo(() => {
|
||||||
|
if (!hasPrioritySet) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const {priority, persistent_notifications: persistentNotifications} = draft.metadata!.priority!;
|
||||||
|
if (priority !== PostPriority.URGENT || !persistentNotifications) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (channelType === Constants.DM_CHANNEL) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hasSpecialMentions) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const mentions = mentionsMinusSpecialMentionsInText(draft.message);
|
||||||
|
|
||||||
|
return mentions.length > 0;
|
||||||
|
}, [hasPrioritySet, draft, channelType, hasSpecialMentions]);
|
||||||
|
|
||||||
|
const handlePostPriorityApply = useCallback((settings?: PostPriorityMetadata) => {
|
||||||
|
const updatedDraft = {
|
||||||
|
...draft,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (settings?.priority || settings?.requested_ack) {
|
||||||
|
updatedDraft.metadata = {
|
||||||
|
priority: {
|
||||||
|
...settings,
|
||||||
|
priority: settings!.priority || '',
|
||||||
|
requested_ack: settings!.requested_ack,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
updatedDraft.metadata = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
handleDraftChange(updatedDraft, {instant: true});
|
||||||
|
focusTextbox();
|
||||||
|
}, [focusTextbox, draft, handleDraftChange]);
|
||||||
|
|
||||||
|
const handlePostPriorityHide = useCallback(() => {
|
||||||
|
focusTextbox(true);
|
||||||
|
}, [focusTextbox]);
|
||||||
|
|
||||||
|
const handleRemovePriority = useCallback(() => {
|
||||||
|
handlePostPriorityApply();
|
||||||
|
}, [handlePostPriorityApply]);
|
||||||
|
|
||||||
|
const showPersistNotificationModal = useCallback((message: string, specialMentions: {[key: string]: boolean}, channelType: Channel['type'], onConfirm: () => void) => {
|
||||||
|
dispatch(openModal({
|
||||||
|
modalId: ModalIdentifiers.PERSIST_NOTIFICATION_CONFIRM_MODAL,
|
||||||
|
dialogType: PersistNotificationConfirmModal,
|
||||||
|
dialogProps: {
|
||||||
|
currentChannelTeammateUsername: channelTeammateUsername,
|
||||||
|
specialMentions,
|
||||||
|
channelType,
|
||||||
|
message,
|
||||||
|
onConfirm,
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
}, [channelTeammateUsername, dispatch]);
|
||||||
|
|
||||||
|
const onSubmitCheck = useCallback((onConfirm: () => void) => {
|
||||||
|
if (
|
||||||
|
isPostPriorityEnabled &&
|
||||||
|
hasRequestedPersistentNotifications(draft?.metadata?.priority)
|
||||||
|
) {
|
||||||
|
showPersistNotificationModal(draft.message, specialMentions, channelType, onConfirm);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}, [isPostPriorityEnabled, showPersistNotificationModal, draft, channelType, specialMentions]);
|
||||||
|
|
||||||
|
const labels = useMemo(() => (
|
||||||
|
(hasPrioritySet && !draft.rootId) ? (
|
||||||
|
<PriorityLabels
|
||||||
|
canRemove={!shouldShowPreview}
|
||||||
|
hasError={!isValidPersistentNotifications}
|
||||||
|
specialMentions={specialMentions}
|
||||||
|
onRemove={handleRemovePriority}
|
||||||
|
persistentNotifications={draft!.metadata!.priority?.persistent_notifications}
|
||||||
|
priority={draft!.metadata!.priority?.priority}
|
||||||
|
requestedAck={draft!.metadata!.priority?.requested_ack}
|
||||||
|
/>
|
||||||
|
) : undefined
|
||||||
|
), [shouldShowPreview, draft, hasPrioritySet, isValidPersistentNotifications, specialMentions, handleRemovePriority]);
|
||||||
|
|
||||||
|
const additionalControl = useMemo(() =>
|
||||||
|
!draft.rootId && isPostPriorityEnabled && (
|
||||||
|
<PostPriorityPickerOverlay
|
||||||
|
key='post-priority-picker-key'
|
||||||
|
settings={draft.metadata?.priority}
|
||||||
|
onApply={handlePostPriorityApply}
|
||||||
|
onClose={handlePostPriorityHide}
|
||||||
|
disabled={shouldShowPreview}
|
||||||
|
/>
|
||||||
|
), [draft.rootId, isPostPriorityEnabled, draft.metadata?.priority, handlePostPriorityApply, handlePostPriorityHide, shouldShowPreview]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
labels,
|
||||||
|
additionalControl,
|
||||||
|
isValidPersistentNotifications,
|
||||||
|
onSubmitCheck,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export default usePriority;
|
@ -0,0 +1,324 @@
|
|||||||
|
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||||
|
// See LICENSE.txt for license information.
|
||||||
|
|
||||||
|
import type React from 'react';
|
||||||
|
import {useCallback, useRef, useState} from 'react';
|
||||||
|
import {useDispatch, useSelector} from 'react-redux';
|
||||||
|
|
||||||
|
import type {ServerError} from '@mattermost/types/errors';
|
||||||
|
|
||||||
|
import {getChannelTimezones} from 'mattermost-redux/actions/channels';
|
||||||
|
import {Permissions} from 'mattermost-redux/constants';
|
||||||
|
import {getChannel, getAllChannelStats} 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 {getCurrentUserId, getStatusForUserId} from 'mattermost-redux/selectors/entities/users';
|
||||||
|
|
||||||
|
import {scrollPostListToBottom} from 'actions/views/channel';
|
||||||
|
import {onSubmit} from 'actions/views/create_comment';
|
||||||
|
import {openModal} from 'actions/views/modals';
|
||||||
|
|
||||||
|
import EditChannelHeaderModal from 'components/edit_channel_header_modal';
|
||||||
|
import EditChannelPurposeModal from 'components/edit_channel_purpose_modal';
|
||||||
|
import NotifyConfirmModal from 'components/notify_confirm_modal';
|
||||||
|
import PostDeletedModal from 'components/post_deleted_modal';
|
||||||
|
import ResetStatusModal from 'components/reset_status_modal';
|
||||||
|
|
||||||
|
import Constants, {ModalIdentifiers, UserStatuses} from 'utils/constants';
|
||||||
|
import {isErrorInvalidSlashCommand, isServerError, specialMentionsInText} from 'utils/post_utils';
|
||||||
|
|
||||||
|
import type {GlobalState} from 'types/store';
|
||||||
|
import type {PostDraft} from 'types/store/draft';
|
||||||
|
|
||||||
|
import useGroups from './use_groups';
|
||||||
|
|
||||||
|
function getStatusFromSlashCommand(message: string) {
|
||||||
|
const tokens = message.split(' ');
|
||||||
|
const command = tokens[0] || '';
|
||||||
|
if (command[0] !== '/') {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
const status = command.substring(1);
|
||||||
|
if (status === 'online' || status === 'away' || status === 'dnd' || status === 'offline') {
|
||||||
|
return status;
|
||||||
|
}
|
||||||
|
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
const useSubmit = (
|
||||||
|
draft: PostDraft,
|
||||||
|
postError: React.ReactNode,
|
||||||
|
channelId: string,
|
||||||
|
postId: string,
|
||||||
|
serverError: (ServerError & { submittedMessage?: string }) | null,
|
||||||
|
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,
|
||||||
|
): [
|
||||||
|
(e: React.FormEvent, submittingDraft?: PostDraft) => void,
|
||||||
|
string | null,
|
||||||
|
] => {
|
||||||
|
const getGroupMentions = useGroups(channelId, draft.message);
|
||||||
|
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
|
||||||
|
const isDraftSubmitting = useRef(false);
|
||||||
|
const [errorClass, setErrorClass] = useState<string | null>(null);
|
||||||
|
const isDirectOrGroup = useSelector((state: GlobalState) => {
|
||||||
|
const channel = getChannel(state, channelId);
|
||||||
|
if (!channel) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return channel.type === Constants.DM_CHANNEL || channel.type === Constants.GM_CHANNEL;
|
||||||
|
});
|
||||||
|
|
||||||
|
const channel = useSelector((state: GlobalState) => {
|
||||||
|
return getChannel(state, channelId);
|
||||||
|
});
|
||||||
|
|
||||||
|
const isRootDeleted = useSelector((state: GlobalState) => {
|
||||||
|
if (!postId) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const post = getPost(state, postId);
|
||||||
|
if (!post || post.delete_at) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
|
||||||
|
const enableConfirmNotificationsToChannel = useSelector((state: GlobalState) => getConfig(state).EnableConfirmNotificationsToChannel === 'true');
|
||||||
|
const channelMembersCount = useSelector((state: GlobalState) => getAllChannelStats(state)[channelId]?.member_count ?? 1);
|
||||||
|
const userIsOutOfOffice = useSelector((state: GlobalState) => {
|
||||||
|
const currentUserId = getCurrentUserId(state);
|
||||||
|
return getStatusForUserId(state, currentUserId) === UserStatuses.OUT_OF_OFFICE;
|
||||||
|
});
|
||||||
|
const useChannelMentions = useSelector((state: GlobalState) => {
|
||||||
|
const channel = getChannel(state, channelId);
|
||||||
|
if (!channel) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return haveIChannelPermission(state, channel.team_id, channel.id, Permissions.USE_CHANNEL_MENTIONS);
|
||||||
|
});
|
||||||
|
|
||||||
|
const showPostDeletedModal = useCallback(() => {
|
||||||
|
dispatch(openModal({
|
||||||
|
modalId: ModalIdentifiers.POST_DELETED_MODAL,
|
||||||
|
dialogType: PostDeletedModal,
|
||||||
|
}));
|
||||||
|
}, [dispatch]);
|
||||||
|
|
||||||
|
const doSubmit = useCallback(async (e?: React.FormEvent, submittingDraft = draft) => {
|
||||||
|
e?.preventDefault();
|
||||||
|
|
||||||
|
if (submittingDraft.uploadsInProgress.length > 0) {
|
||||||
|
isDraftSubmitting.current = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (postError) {
|
||||||
|
setErrorClass('animation--highlight');
|
||||||
|
setTimeout(() => {
|
||||||
|
setErrorClass(null);
|
||||||
|
}, Constants.ANIMATION_TIMEOUT);
|
||||||
|
isDraftSubmitting.current = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (submittingDraft.message.trim().length === 0 && submittingDraft.fileInfos.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isRootDeleted) {
|
||||||
|
showPostDeletedModal();
|
||||||
|
isDraftSubmitting.current = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (serverError && !isErrorInvalidSlashCommand(serverError)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const fasterThanHumanWillClick = 150;
|
||||||
|
const forceFocus = Date.now() - lastBlurAt.current < fasterThanHumanWillClick;
|
||||||
|
focusTextbox(forceFocus);
|
||||||
|
|
||||||
|
setServerError(null);
|
||||||
|
|
||||||
|
const ignoreSlash = isErrorInvalidSlashCommand(serverError) && serverError?.submittedMessage === submittingDraft.message;
|
||||||
|
const options = {ignoreSlash};
|
||||||
|
|
||||||
|
try {
|
||||||
|
await dispatch(onSubmit(submittingDraft, options));
|
||||||
|
|
||||||
|
setPostError(null);
|
||||||
|
setServerError(null);
|
||||||
|
handleDraftChange({
|
||||||
|
message: '',
|
||||||
|
fileInfos: [],
|
||||||
|
uploadsInProgress: [],
|
||||||
|
createAt: 0,
|
||||||
|
updateAt: 0,
|
||||||
|
channelId,
|
||||||
|
rootId: postId,
|
||||||
|
}, {instant: true});
|
||||||
|
} catch (err: unknown) {
|
||||||
|
if (isServerError(err)) {
|
||||||
|
if (isErrorInvalidSlashCommand(err)) {
|
||||||
|
handleDraftChange(submittingDraft, {instant: true});
|
||||||
|
}
|
||||||
|
setServerError({
|
||||||
|
...err,
|
||||||
|
submittedMessage: submittingDraft.message,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
isDraftSubmitting.current = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!postId) {
|
||||||
|
dispatch(scrollPostListToBottom());
|
||||||
|
}
|
||||||
|
|
||||||
|
isDraftSubmitting.current = false;
|
||||||
|
}, [handleDraftChange, dispatch, draft, focusTextbox, isRootDeleted, postError, serverError, showPostDeletedModal, channelId, postId, lastBlurAt, setPostError, setServerError]);
|
||||||
|
|
||||||
|
const showNotifyAllModal = useCallback((mentions: string[], channelTimezoneCount: number, memberNotifyCount: number) => {
|
||||||
|
dispatch(openModal({
|
||||||
|
modalId: ModalIdentifiers.NOTIFY_CONFIRM_MODAL,
|
||||||
|
dialogType: NotifyConfirmModal,
|
||||||
|
dialogProps: {
|
||||||
|
mentions,
|
||||||
|
channelTimezoneCount,
|
||||||
|
memberNotifyCount,
|
||||||
|
onConfirm: () => doSubmit(),
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
}, [doSubmit, dispatch]);
|
||||||
|
|
||||||
|
const handleSubmit = useCallback(async (e: React.FormEvent, submittingDraft = draft) => {
|
||||||
|
if (!channel) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
e.preventDefault();
|
||||||
|
setShowPreview(false);
|
||||||
|
isDraftSubmitting.current = true;
|
||||||
|
|
||||||
|
const notificationsToChannel = enableConfirmNotificationsToChannel && useChannelMentions;
|
||||||
|
let memberNotifyCount = 0;
|
||||||
|
let channelTimezoneCount = 0;
|
||||||
|
let mentions: string[] = [];
|
||||||
|
|
||||||
|
const specialMentions = specialMentionsInText(submittingDraft.message);
|
||||||
|
const hasSpecialMentions = Object.values(specialMentions).includes(true);
|
||||||
|
|
||||||
|
if (enableConfirmNotificationsToChannel && !hasSpecialMentions) {
|
||||||
|
({memberNotifyCount, channelTimezoneCount, mentions} = getGroupMentions(submittingDraft.message));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (notificationsToChannel && channelMembersCount > Constants.NOTIFY_ALL_MEMBERS && hasSpecialMentions) {
|
||||||
|
memberNotifyCount = channelMembersCount - 1;
|
||||||
|
|
||||||
|
for (const k in specialMentions) {
|
||||||
|
if (specialMentions[k]) {
|
||||||
|
mentions.push('@' + k);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const {data} = await dispatch(getChannelTimezones(channelId));
|
||||||
|
channelTimezoneCount = data ? data.length : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (prioritySubmitCheck(doSubmit)) {
|
||||||
|
isDraftSubmitting.current = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (memberNotifyCount > 0) {
|
||||||
|
showNotifyAllModal(mentions, channelTimezoneCount, memberNotifyCount);
|
||||||
|
isDraftSubmitting.current = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const status = getStatusFromSlashCommand(submittingDraft.message);
|
||||||
|
if (userIsOutOfOffice && status) {
|
||||||
|
const resetStatusModalData = {
|
||||||
|
modalId: ModalIdentifiers.RESET_STATUS,
|
||||||
|
dialogType: ResetStatusModal,
|
||||||
|
dialogProps: {newStatus: status},
|
||||||
|
};
|
||||||
|
|
||||||
|
dispatch(openModal(resetStatusModalData));
|
||||||
|
|
||||||
|
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);
|
||||||
|
}, [
|
||||||
|
doSubmit,
|
||||||
|
draft,
|
||||||
|
isDirectOrGroup,
|
||||||
|
channel,
|
||||||
|
channelId,
|
||||||
|
channelMembersCount,
|
||||||
|
dispatch,
|
||||||
|
enableConfirmNotificationsToChannel,
|
||||||
|
handleDraftChange,
|
||||||
|
showNotifyAllModal,
|
||||||
|
useChannelMentions,
|
||||||
|
userIsOutOfOffice,
|
||||||
|
getGroupMentions,
|
||||||
|
setShowPreview,
|
||||||
|
prioritySubmitCheck,
|
||||||
|
]);
|
||||||
|
|
||||||
|
return [handleSubmit, errorClass];
|
||||||
|
};
|
||||||
|
|
||||||
|
export default useSubmit;
|
@ -0,0 +1,96 @@
|
|||||||
|
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||||
|
// See LICENSE.txt for license information.
|
||||||
|
|
||||||
|
import type React from 'react';
|
||||||
|
import {useCallback, useEffect} from 'react';
|
||||||
|
import {useDispatch, useSelector} from 'react-redux';
|
||||||
|
|
||||||
|
import {focusedRHS} from 'actions/views/rhs';
|
||||||
|
import {getIsRhsExpanded, getIsRhsOpen} from 'selectors/rhs';
|
||||||
|
import {getShouldFocusRHS} from 'selectors/views/rhs';
|
||||||
|
|
||||||
|
import useDidUpdate from 'components/common/hooks/useDidUpdate';
|
||||||
|
import type TextboxClass from 'components/textbox/textbox';
|
||||||
|
|
||||||
|
import {shouldFocusMainTextbox} from 'utils/post_utils';
|
||||||
|
import * as UserAgent from 'utils/user_agent';
|
||||||
|
|
||||||
|
const useTextboxFocus = (
|
||||||
|
textboxRef: React.RefObject<TextboxClass>,
|
||||||
|
channelId: string,
|
||||||
|
isRHS: boolean,
|
||||||
|
canPost: boolean,
|
||||||
|
) => {
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
|
||||||
|
const rhsExpanded = useSelector(getIsRhsExpanded);
|
||||||
|
const rhsOpen = useSelector(getIsRhsOpen);
|
||||||
|
|
||||||
|
// We force the selector to always think it is the same value to avoid re-renders
|
||||||
|
// because we only use this value during mount.
|
||||||
|
const shouldFocusRHS = useSelector(getShouldFocusRHS, () => true);
|
||||||
|
|
||||||
|
const focusTextbox = useCallback((keepFocus = false) => {
|
||||||
|
const postTextboxDisabled = !canPost;
|
||||||
|
if (textboxRef.current && postTextboxDisabled) {
|
||||||
|
textboxRef.current.blur(); // Fixes Firefox bug which causes keyboard shortcuts to be ignored (MM-22482)
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (textboxRef.current && (keepFocus || !UserAgent.isMobile())) {
|
||||||
|
textboxRef.current.focus();
|
||||||
|
}
|
||||||
|
}, [canPost, textboxRef]);
|
||||||
|
|
||||||
|
const focusTextboxIfNecessary = useCallback((e: KeyboardEvent) => {
|
||||||
|
// Do not focus if the rhs is expanded and this is not the RHS
|
||||||
|
if (!isRHS && rhsExpanded) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Do not focus if the rhs is not expanded and this is the RHS
|
||||||
|
if (isRHS && !rhsExpanded) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Do not focus the main textbox when the RHS is open as a hacky fix to avoid cursor jumping textbox sometimes
|
||||||
|
if (isRHS && rhsOpen && document.activeElement?.tagName === 'BODY') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bit of a hack to not steal focus from the channel switch modal if it's open
|
||||||
|
// This is a special case as the channel switch modal does not enforce focus like
|
||||||
|
// most modals do
|
||||||
|
if (document.getElementsByClassName('channel-switch-modal').length) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (shouldFocusMainTextbox(e, document.activeElement)) {
|
||||||
|
focusTextbox();
|
||||||
|
}
|
||||||
|
}, [focusTextbox, rhsExpanded, rhsOpen, isRHS]);
|
||||||
|
|
||||||
|
// Register events for onkeydown
|
||||||
|
useEffect(() => {
|
||||||
|
document.addEventListener('keydown', focusTextboxIfNecessary);
|
||||||
|
return () => {
|
||||||
|
document.removeEventListener('keydown', focusTextboxIfNecessary);
|
||||||
|
};
|
||||||
|
}, [focusTextboxIfNecessary]);
|
||||||
|
|
||||||
|
// Focus on textbox on channel switch
|
||||||
|
useDidUpdate(() => {
|
||||||
|
focusTextbox();
|
||||||
|
}, [channelId]);
|
||||||
|
|
||||||
|
// Focus on mount
|
||||||
|
useEffect(() => {
|
||||||
|
if (isRHS && shouldFocusRHS) {
|
||||||
|
focusTextbox();
|
||||||
|
dispatch(focusedRHS());
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return focusTextbox;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default useTextboxFocus;
|
@ -0,0 +1,179 @@
|
|||||||
|
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||||
|
// See LICENSE.txt for license information.
|
||||||
|
|
||||||
|
import React, {useCallback, useRef, useState} from 'react';
|
||||||
|
import {useSelector} from 'react-redux';
|
||||||
|
|
||||||
|
import type {ServerError} from '@mattermost/types/errors';
|
||||||
|
import type {FileInfo} from '@mattermost/types/files';
|
||||||
|
|
||||||
|
import {sortFileInfos} from 'mattermost-redux/utils/file_utils';
|
||||||
|
|
||||||
|
import {getCurrentLocale} from 'selectors/i18n';
|
||||||
|
|
||||||
|
import FilePreview from 'components/file_preview';
|
||||||
|
import type {FilePreviewInfo} from 'components/file_preview/file_preview';
|
||||||
|
import FileUpload from 'components/file_upload';
|
||||||
|
import type {FileUpload as FileUploadClass} from 'components/file_upload/file_upload';
|
||||||
|
import type TextboxClass from 'components/textbox/textbox';
|
||||||
|
|
||||||
|
import type {PostDraft} from 'types/store/draft';
|
||||||
|
|
||||||
|
const getFileCount = (draft: PostDraft) => {
|
||||||
|
return draft.fileInfos.length + draft.uploadsInProgress.length;
|
||||||
|
};
|
||||||
|
|
||||||
|
const useUploadFiles = (
|
||||||
|
draft: PostDraft,
|
||||||
|
postId: string,
|
||||||
|
channelId: string,
|
||||||
|
isThreadView: boolean,
|
||||||
|
storedDrafts: React.MutableRefObject<Record<string, PostDraft | undefined>>,
|
||||||
|
readOnlyChannel: boolean,
|
||||||
|
textboxRef: React.RefObject<TextboxClass>,
|
||||||
|
handleDraftChange: (draft: PostDraft, options?: {instant?: boolean; show?: boolean}) => void,
|
||||||
|
focusTextbox: (forceFocust?: boolean) => void,
|
||||||
|
setServerError: (err: (ServerError & { submittedMessage?: string }) | null) => void,
|
||||||
|
): [React.ReactNode, React.ReactNode] => {
|
||||||
|
const locale = useSelector(getCurrentLocale);
|
||||||
|
|
||||||
|
const [uploadsProgressPercent, setUploadsProgressPercent] = useState<{ [clientID: string]: FilePreviewInfo }>({});
|
||||||
|
|
||||||
|
const fileUploadRef = useRef<FileUploadClass>(null);
|
||||||
|
|
||||||
|
const handleFileUploadChange = useCallback(() => {
|
||||||
|
focusTextbox();
|
||||||
|
}, [focusTextbox]);
|
||||||
|
|
||||||
|
const getFileUploadTarget = useCallback(() => {
|
||||||
|
return textboxRef.current?.getInputBox();
|
||||||
|
}, [textboxRef]);
|
||||||
|
|
||||||
|
const handleUploadProgress = useCallback((filePreviewInfo: FilePreviewInfo) => {
|
||||||
|
setUploadsProgressPercent((prev) => ({
|
||||||
|
...prev,
|
||||||
|
[filePreviewInfo.clientId]: filePreviewInfo,
|
||||||
|
}));
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleFileUploadComplete = useCallback((fileInfos: FileInfo[], clientIds: string[], channelId: string, rootId?: string) => {
|
||||||
|
const key = rootId || channelId;
|
||||||
|
const draftToUpdate = storedDrafts.current[key];
|
||||||
|
if (!draftToUpdate) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const newFileInfos = sortFileInfos([...draftToUpdate.fileInfos || [], ...fileInfos], locale);
|
||||||
|
|
||||||
|
const clientIdsSet = new Set(clientIds);
|
||||||
|
const uploadsInProgress = (draftToUpdate.uploadsInProgress || []).filter((v) => !clientIdsSet.has(v));
|
||||||
|
|
||||||
|
const modifiedDraft = {
|
||||||
|
...draftToUpdate,
|
||||||
|
fileInfos: newFileInfos,
|
||||||
|
uploadsInProgress,
|
||||||
|
};
|
||||||
|
|
||||||
|
handleDraftChange(modifiedDraft, {instant: true});
|
||||||
|
}, [locale, handleDraftChange, storedDrafts]);
|
||||||
|
|
||||||
|
const handleUploadStart = useCallback((clientIds: string[]) => {
|
||||||
|
const uploadsInProgress = [...draft.uploadsInProgress, ...clientIds];
|
||||||
|
|
||||||
|
const updatedDraft = {
|
||||||
|
...draft,
|
||||||
|
uploadsInProgress,
|
||||||
|
};
|
||||||
|
|
||||||
|
handleDraftChange(updatedDraft, {instant: true});
|
||||||
|
|
||||||
|
focusTextbox();
|
||||||
|
}, [draft, handleDraftChange, focusTextbox]);
|
||||||
|
|
||||||
|
const handleUploadError = useCallback((uploadError: string | ServerError | null, clientId?: string, channelId = '', rootId = '') => {
|
||||||
|
if (clientId) {
|
||||||
|
const id = rootId || channelId;
|
||||||
|
const storedDraft = storedDrafts.current[id];
|
||||||
|
if (storedDraft) {
|
||||||
|
const modifiedDraft = {...storedDraft};
|
||||||
|
const index = modifiedDraft.uploadsInProgress.indexOf(clientId) ?? -1;
|
||||||
|
if (index !== -1) {
|
||||||
|
modifiedDraft.uploadsInProgress = [...modifiedDraft.uploadsInProgress];
|
||||||
|
modifiedDraft.uploadsInProgress.splice(index, 1);
|
||||||
|
handleDraftChange(modifiedDraft, {instant: true});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof uploadError === 'string') {
|
||||||
|
if (uploadError) {
|
||||||
|
setServerError(new Error(uploadError));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
setServerError(uploadError);
|
||||||
|
}
|
||||||
|
}, [handleDraftChange, setServerError, storedDrafts]);
|
||||||
|
|
||||||
|
const removePreview = useCallback((clientId: string) => {
|
||||||
|
handleUploadError(null, clientId, draft.channelId, draft.rootId);
|
||||||
|
|
||||||
|
const modifiedDraft = {...draft};
|
||||||
|
let index = draft.fileInfos.findIndex((info) => info.id === clientId);
|
||||||
|
if (index === -1) {
|
||||||
|
index = draft.uploadsInProgress.indexOf(clientId);
|
||||||
|
|
||||||
|
if (index >= 0) {
|
||||||
|
modifiedDraft.uploadsInProgress = [...draft.uploadsInProgress];
|
||||||
|
modifiedDraft.uploadsInProgress.splice(index, 1);
|
||||||
|
|
||||||
|
fileUploadRef.current?.cancelUpload(clientId);
|
||||||
|
} else {
|
||||||
|
// No modification
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
modifiedDraft.fileInfos = [...draft.fileInfos];
|
||||||
|
modifiedDraft.fileInfos.splice(index, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
handleDraftChange(modifiedDraft, {instant: true});
|
||||||
|
handleFileUploadChange();
|
||||||
|
}, [draft, fileUploadRef, handleDraftChange, handleUploadError, handleFileUploadChange]);
|
||||||
|
|
||||||
|
let attachmentPreview = null;
|
||||||
|
if (!readOnlyChannel && (draft.fileInfos.length > 0 || draft.uploadsInProgress.length > 0)) {
|
||||||
|
attachmentPreview = (
|
||||||
|
<FilePreview
|
||||||
|
fileInfos={draft.fileInfos}
|
||||||
|
onRemove={removePreview}
|
||||||
|
uploadsInProgress={draft.uploadsInProgress}
|
||||||
|
uploadsProgressPercent={uploadsProgressPercent}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let postType = 'post';
|
||||||
|
if (postId) {
|
||||||
|
postType = isThreadView ? 'thread' : 'comment';
|
||||||
|
}
|
||||||
|
|
||||||
|
const fileUploadJSX = readOnlyChannel ? null : (
|
||||||
|
<FileUpload
|
||||||
|
ref={fileUploadRef}
|
||||||
|
fileCount={getFileCount(draft)}
|
||||||
|
getTarget={getFileUploadTarget}
|
||||||
|
onFileUploadChange={handleFileUploadChange}
|
||||||
|
onUploadStart={handleUploadStart}
|
||||||
|
onFileUpload={handleFileUploadComplete}
|
||||||
|
onUploadError={handleUploadError}
|
||||||
|
onUploadProgress={handleUploadProgress}
|
||||||
|
rootId={postId}
|
||||||
|
channelId={channelId}
|
||||||
|
postType={postType}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
return [attachmentPreview, fileUploadJSX];
|
||||||
|
};
|
||||||
|
|
||||||
|
export default useUploadFiles;
|
@ -21,7 +21,7 @@ const LAST_ANALYTICS_TEAM = 'last_analytics_team';
|
|||||||
|
|
||||||
function mapStateToProps(state: GlobalState) {
|
function mapStateToProps(state: GlobalState) {
|
||||||
const teams = getTeamsList(state);
|
const teams = getTeamsList(state);
|
||||||
const teamId = makeGetGlobalItem(LAST_ANALYTICS_TEAM, null)(state);
|
const teamId = makeGetGlobalItem(LAST_ANALYTICS_TEAM, '')(state);
|
||||||
const initialTeam = state.entities.teams.teams[teamId] || (teams.length > 0 ? teams[0] : null);
|
const initialTeam = state.entities.teams.teams[teamId] || (teams.length > 0 ? teams[0] : null);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
@ -158,9 +158,7 @@ exports[`components/channel_view Should match snapshot with base props 1`] = `
|
|||||||
data-testid="post-create"
|
data-testid="post-create"
|
||||||
id="post-create"
|
id="post-create"
|
||||||
>
|
>
|
||||||
<Connect(AdvancedCreatePost)
|
<Memo(AdvancedCreatePost) />
|
||||||
getChannelView={[Function]}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
@ -80,10 +80,6 @@ export default class ChannelView extends React.PureComponent<Props, State> {
|
|||||||
this.channelViewRef = React.createRef();
|
this.channelViewRef = React.createRef();
|
||||||
}
|
}
|
||||||
|
|
||||||
getChannelView = () => {
|
|
||||||
return this.channelViewRef.current;
|
|
||||||
};
|
|
||||||
|
|
||||||
onClickCloseChannel = () => {
|
onClickCloseChannel = () => {
|
||||||
this.props.goToLastViewedChannel();
|
this.props.goToLastViewedChannel();
|
||||||
};
|
};
|
||||||
@ -160,7 +156,7 @@ export default class ChannelView extends React.PureComponent<Props, State> {
|
|||||||
data-testid='post-create'
|
data-testid='post-create'
|
||||||
className='post-create__container AdvancedTextEditor__ctr'
|
className='post-create__container AdvancedTextEditor__ctr'
|
||||||
>
|
>
|
||||||
<AdvancedCreatePost getChannelView={this.getChannelView}/>
|
<AdvancedCreatePost/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -2,6 +2,7 @@
|
|||||||
// See LICENSE.txt for license information.
|
// See LICENSE.txt for license information.
|
||||||
|
|
||||||
import React, {useCallback} from 'react';
|
import React, {useCallback} from 'react';
|
||||||
|
import type {MessageDescriptor} from 'react-intl';
|
||||||
import {FormattedMessage} from 'react-intl';
|
import {FormattedMessage} from 'react-intl';
|
||||||
import styled from 'styled-components';
|
import styled from 'styled-components';
|
||||||
|
|
||||||
@ -11,8 +12,7 @@ import RenderEmoji from 'components/emoji/render_emoji';
|
|||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
onClick?: () => void;
|
onClick?: () => void;
|
||||||
id?: string;
|
display?: MessageDescriptor;
|
||||||
defaultMessage?: string;
|
|
||||||
values?: Record<string, any>;
|
values?: Record<string, any>;
|
||||||
className?: string;
|
className?: string;
|
||||||
|
|
||||||
@ -59,8 +59,7 @@ const Chip = ({
|
|||||||
otherOption,
|
otherOption,
|
||||||
className,
|
className,
|
||||||
leadingIcon,
|
leadingIcon,
|
||||||
id,
|
display,
|
||||||
defaultMessage,
|
|
||||||
values,
|
values,
|
||||||
additionalMarkup,
|
additionalMarkup,
|
||||||
}: Props) => {
|
}: Props) => {
|
||||||
@ -81,10 +80,9 @@ const Chip = ({
|
|||||||
emojiStyle={emojiStyles}
|
emojiStyle={emojiStyles}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{(id && defaultMessage && values) && (
|
{(display && values) && (
|
||||||
<FormattedMessage
|
<FormattedMessage
|
||||||
id={id}
|
{...display}
|
||||||
defaultMessage={defaultMessage}
|
|
||||||
values={values}
|
values={values}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
@ -9,7 +9,7 @@ import type {UserProfile, UserStatus} from '@mattermost/types/users';
|
|||||||
|
|
||||||
import {getCurrentRelativeTeamUrl} from 'mattermost-redux/selectors/entities/teams';
|
import {getCurrentRelativeTeamUrl} from 'mattermost-redux/selectors/entities/teams';
|
||||||
|
|
||||||
import PriorityLabels from 'components/advanced_create_post/priority_labels';
|
import PriorityLabels from 'components/advanced_text_editor/priority_labels';
|
||||||
import FilePreview from 'components/file_preview';
|
import FilePreview from 'components/file_preview';
|
||||||
import Markdown from 'components/markdown';
|
import Markdown from 'components/markdown';
|
||||||
import ProfilePicture from 'components/profile_picture';
|
import ProfilePicture from 'components/profile_picture';
|
||||||
|
@ -5,10 +5,8 @@ import React, {memo, forwardRef, useMemo} from 'react';
|
|||||||
import {useSelector} from 'react-redux';
|
import {useSelector} from 'react-redux';
|
||||||
|
|
||||||
import {ArchiveOutlineIcon} from '@mattermost/compass-icons/components';
|
import {ArchiveOutlineIcon} from '@mattermost/compass-icons/components';
|
||||||
import type {Post} from '@mattermost/types/posts';
|
|
||||||
import type {UserProfile} from '@mattermost/types/users';
|
import type {UserProfile} from '@mattermost/types/users';
|
||||||
|
|
||||||
import {Posts} from 'mattermost-redux/constants';
|
|
||||||
import {makeGetChannel} from 'mattermost-redux/selectors/entities/channels';
|
import {makeGetChannel} from 'mattermost-redux/selectors/entities/channels';
|
||||||
import {getPost, getLimitedViews} from 'mattermost-redux/selectors/entities/posts';
|
import {getPost, getLimitedViews} from 'mattermost-redux/selectors/entities/posts';
|
||||||
|
|
||||||
@ -23,7 +21,6 @@ import type {GlobalState} from 'types/store';
|
|||||||
type Props = {
|
type Props = {
|
||||||
teammate?: UserProfile;
|
teammate?: UserProfile;
|
||||||
threadId: string;
|
threadId: string;
|
||||||
latestPostId: Post['id'];
|
|
||||||
isThreadView?: boolean;
|
isThreadView?: boolean;
|
||||||
placeholder?: string;
|
placeholder?: string;
|
||||||
};
|
};
|
||||||
@ -31,7 +28,6 @@ type Props = {
|
|||||||
const CreateComment = forwardRef<HTMLDivElement, Props>(({
|
const CreateComment = forwardRef<HTMLDivElement, Props>(({
|
||||||
teammate,
|
teammate,
|
||||||
threadId,
|
threadId,
|
||||||
latestPostId,
|
|
||||||
isThreadView,
|
isThreadView,
|
||||||
placeholder,
|
placeholder,
|
||||||
}: Props, ref) => {
|
}: Props, ref) => {
|
||||||
@ -47,7 +43,6 @@ const CreateComment = forwardRef<HTMLDivElement, Props>(({
|
|||||||
if (!channel || threadIsLimited) {
|
if (!channel || threadIsLimited) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
const rootDeleted = (rootPost as Post).state === Posts.POST_DELETED;
|
|
||||||
const isFakeDeletedPost = rootPost.type === Constants.PostTypes.FAKE_PARENT_DELETED;
|
const isFakeDeletedPost = rootPost.type === Constants.PostTypes.FAKE_PARENT_DELETED;
|
||||||
|
|
||||||
const channelType = channel.type;
|
const channelType = channel.type;
|
||||||
@ -97,8 +92,6 @@ const CreateComment = forwardRef<HTMLDivElement, Props>(({
|
|||||||
<AdvancedCreateComment
|
<AdvancedCreateComment
|
||||||
placeholder={placeholder}
|
placeholder={placeholder}
|
||||||
channelId={channel.id}
|
channelId={channel.id}
|
||||||
latestPostId={latestPostId}
|
|
||||||
rootDeleted={rootDeleted}
|
|
||||||
rootId={threadId}
|
rootId={threadId}
|
||||||
isThreadView={isThreadView}
|
isThreadView={isThreadView}
|
||||||
/>
|
/>
|
||||||
|
@ -355,7 +355,6 @@ class ThreadViewerVirtualized extends PureComponent<Props, State> {
|
|||||||
<CreateComment
|
<CreateComment
|
||||||
placeholder={this.props.inputPlaceholder}
|
placeholder={this.props.inputPlaceholder}
|
||||||
isThreadView={this.props.isThreadView}
|
isThreadView={this.props.isThreadView}
|
||||||
latestPostId={this.props.lastPost.id}
|
|
||||||
ref={this.postCreateContainerRef}
|
ref={this.postCreateContainerRef}
|
||||||
teammate={this.props.directTeammate}
|
teammate={this.props.directTeammate}
|
||||||
threadId={this.props.selected.id}
|
threadId={this.props.selected.id}
|
||||||
|
@ -5,7 +5,6 @@ import React from 'react';
|
|||||||
import {FormattedMessage} from 'react-intl';
|
import {FormattedMessage} from 'react-intl';
|
||||||
|
|
||||||
import {useMeasurePunchouts} from '@mattermost/components';
|
import {useMeasurePunchouts} from '@mattermost/components';
|
||||||
import type {Channel} from '@mattermost/types/channels';
|
|
||||||
|
|
||||||
import PrewrittenChips from 'components/advanced_create_post/prewritten_chips';
|
import PrewrittenChips from 'components/advanced_create_post/prewritten_chips';
|
||||||
|
|
||||||
@ -13,25 +12,22 @@ import OnboardingTourTip from './onboarding_tour_tip';
|
|||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
prefillMessage: (msg: string, shouldFocus: boolean) => void;
|
prefillMessage: (msg: string, shouldFocus: boolean) => void;
|
||||||
currentChannel: Channel;
|
channelId: string;
|
||||||
currentUserId: string;
|
currentUserId: string;
|
||||||
currentChannelTeammateUsername?: string;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const translate = {x: -6, y: -6};
|
const translate = {x: -6, y: -6};
|
||||||
|
|
||||||
export const SendMessageTour = ({
|
export const SendMessageTour = ({
|
||||||
prefillMessage,
|
prefillMessage,
|
||||||
currentChannel,
|
channelId,
|
||||||
currentUserId,
|
currentUserId,
|
||||||
currentChannelTeammateUsername,
|
|
||||||
}: Props) => {
|
}: Props) => {
|
||||||
const chips = (
|
const chips = (
|
||||||
<PrewrittenChips
|
<PrewrittenChips
|
||||||
prefillMessage={prefillMessage}
|
prefillMessage={prefillMessage}
|
||||||
currentChannel={currentChannel}
|
channelId={channelId}
|
||||||
currentUserId={currentUserId}
|
currentUserId={currentUserId}
|
||||||
currentChannelTeammateUsername={currentChannelTeammateUsername}
|
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -3341,7 +3341,6 @@
|
|||||||
"create_group_memberships_modal.create": "Yes",
|
"create_group_memberships_modal.create": "Yes",
|
||||||
"create_group_memberships_modal.desc": "You're about to add or re-add {username} to teams and channels based on their LDAP group membership. You can revert this change at any time.",
|
"create_group_memberships_modal.desc": "You're about to add or re-add {username} to teams and channels based on their LDAP group membership. You can revert this change at any time.",
|
||||||
"create_group_memberships_modal.title": "Re-add {username} to teams and channels",
|
"create_group_memberships_modal.title": "Re-add {username} to teams and channels",
|
||||||
"create_post.comment": "Comment",
|
|
||||||
"create_post.deactivated": "You are viewing an archived channel with a **deactivated user**. New messages cannot be posted.",
|
"create_post.deactivated": "You are viewing an archived channel with a **deactivated user**. New messages cannot be posted.",
|
||||||
"create_post.error_message": "Your message is too long. Character count: {length}/{limit}",
|
"create_post.error_message": "Your message is too long. Character count: {length}/{limit}",
|
||||||
"create_post.file_limit_sticky_banner.admin_message": "New uploads will automatically archive older files. To view them again, you can delete older files or <a>upgrade to a paid plan.</a>",
|
"create_post.file_limit_sticky_banner.admin_message": "New uploads will automatically archive older files. To view them again, you can delete older files or <a>upgrade to a paid plan.</a>",
|
||||||
|
@ -2243,206 +2243,6 @@ describe('getPostsInCurrentChannel', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('getCurrentUsersLatestPost', () => {
|
|
||||||
const user1 = TestHelper.fakeUserWithId();
|
|
||||||
const profiles: Record<string, UserProfile> = {};
|
|
||||||
profiles[user1.id] = user1;
|
|
||||||
it('no posts', () => {
|
|
||||||
const noPosts = {};
|
|
||||||
const state = {
|
|
||||||
entities: {
|
|
||||||
users: {
|
|
||||||
currentUserId: user1.id,
|
|
||||||
profiles,
|
|
||||||
},
|
|
||||||
posts: {
|
|
||||||
posts: noPosts,
|
|
||||||
postsInChannel: [],
|
|
||||||
},
|
|
||||||
preferences: {
|
|
||||||
myPreferences: {},
|
|
||||||
},
|
|
||||||
channels: {
|
|
||||||
currentChannelId: 'abcd',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
} as unknown as GlobalState;
|
|
||||||
const actual = Selectors.getCurrentUsersLatestPost(state, '');
|
|
||||||
|
|
||||||
expect(actual).toEqual(null);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('return first post which user can edit', () => {
|
|
||||||
const postsAny = {
|
|
||||||
a: {id: 'a', channel_id: 'a', create_at: 1, highlight: false, user_id: 'a'},
|
|
||||||
b: {id: 'b', root_id: 'a', channel_id: 'abcd', create_at: 3, highlight: false, user_id: 'b', state: Posts.POST_DELETED},
|
|
||||||
c: {id: 'c', root_id: 'a', channel_id: 'abcd', create_at: 3, highlight: false, user_id: 'b', type: 'system_join_channel'},
|
|
||||||
d: {id: 'd', root_id: 'a', channel_id: 'abcd', create_at: 3, highlight: false, user_id: 'b', type: Posts.POST_TYPES.EPHEMERAL},
|
|
||||||
e: {id: 'e', channel_id: 'abcd', create_at: 4, highlight: false, user_id: 'c'},
|
|
||||||
f: {id: 'f', channel_id: 'abcd', create_at: 4, highlight: false, user_id: user1.id},
|
|
||||||
};
|
|
||||||
const state = {
|
|
||||||
entities: {
|
|
||||||
users: {
|
|
||||||
currentUserId: user1.id,
|
|
||||||
profiles,
|
|
||||||
},
|
|
||||||
posts: {
|
|
||||||
posts: postsAny,
|
|
||||||
postsInChannel: {
|
|
||||||
abcd: [
|
|
||||||
{order: ['b', 'c', 'd', 'e', 'f'], recent: true},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
postsInThread: {},
|
|
||||||
},
|
|
||||||
preferences: {
|
|
||||||
myPreferences: {},
|
|
||||||
},
|
|
||||||
channels: {
|
|
||||||
currentChannelId: 'abcd',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
} as unknown as GlobalState;
|
|
||||||
const actual = Selectors.getCurrentUsersLatestPost(state, '');
|
|
||||||
|
|
||||||
expect(actual).toMatchObject(postsAny.f);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('return first post which user can edit ignore pending and failed', () => {
|
|
||||||
const postsAny = {
|
|
||||||
a: {id: 'a', channel_id: 'a', create_at: 1, highlight: false, user_id: 'a'},
|
|
||||||
b: {id: 'b', channel_id: 'abcd', create_at: 4, highlight: false, user_id: user1.id, pending_post_id: 'b'},
|
|
||||||
c: {id: 'c', channel_id: 'abcd', create_at: 4, highlight: false, user_id: user1.id, failed: true},
|
|
||||||
d: {id: 'd', root_id: 'a', channel_id: 'abcd', create_at: 3, highlight: false, user_id: 'b', type: Posts.POST_TYPES.EPHEMERAL},
|
|
||||||
e: {id: 'e', channel_id: 'abcd', create_at: 4, highlight: false, user_id: 'c'},
|
|
||||||
f: {id: 'f', channel_id: 'abcd', create_at: 4, highlight: false, user_id: user1.id},
|
|
||||||
};
|
|
||||||
const state = {
|
|
||||||
entities: {
|
|
||||||
users: {
|
|
||||||
currentUserId: user1.id,
|
|
||||||
profiles,
|
|
||||||
},
|
|
||||||
posts: {
|
|
||||||
posts: postsAny,
|
|
||||||
postsInChannel: {
|
|
||||||
abcd: [
|
|
||||||
{order: ['b', 'c', 'd', 'e', 'f'], recent: true},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
postsInThread: {},
|
|
||||||
},
|
|
||||||
preferences: {
|
|
||||||
myPreferences: {},
|
|
||||||
},
|
|
||||||
channels: {
|
|
||||||
currentChannelId: 'abcd',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
} as unknown as GlobalState;
|
|
||||||
const actual = Selectors.getCurrentUsersLatestPost(state, '');
|
|
||||||
|
|
||||||
expect(actual).toMatchObject(postsAny.f);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('return first post which has rootId match', () => {
|
|
||||||
const postsAny = {
|
|
||||||
a: {id: 'a', channel_id: 'a', create_at: 1, highlight: false, user_id: 'a'},
|
|
||||||
b: {id: 'b', root_id: 'a', channel_id: 'abcd', create_at: 3, highlight: false, user_id: 'b', state: Posts.POST_DELETED},
|
|
||||||
c: {id: 'c', root_id: 'a', channel_id: 'abcd', create_at: 3, highlight: false, user_id: 'b', type: 'system_join_channel'},
|
|
||||||
d: {id: 'd', root_id: 'a', channel_id: 'abcd', create_at: 3, highlight: false, user_id: 'b', type: Posts.POST_TYPES.EPHEMERAL},
|
|
||||||
e: {id: 'e', channel_id: 'abcd', create_at: 4, highlight: false, user_id: 'c'},
|
|
||||||
f: {id: 'f', root_id: 'e', channel_id: 'abcd', create_at: 4, highlight: false, user_id: user1.id},
|
|
||||||
};
|
|
||||||
const state = {
|
|
||||||
entities: {
|
|
||||||
users: {
|
|
||||||
currentUserId: user1.id,
|
|
||||||
profiles,
|
|
||||||
},
|
|
||||||
posts: {
|
|
||||||
posts: postsAny,
|
|
||||||
postsInChannel: {
|
|
||||||
abcd: [
|
|
||||||
{order: ['b', 'c', 'd', 'e', 'f'], recent: true},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
postsInThread: {},
|
|
||||||
},
|
|
||||||
preferences: {
|
|
||||||
myPreferences: {},
|
|
||||||
},
|
|
||||||
channels: {
|
|
||||||
currentChannelId: 'abcd',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
} as unknown as GlobalState;
|
|
||||||
const actual = Selectors.getCurrentUsersLatestPost(state, 'e');
|
|
||||||
|
|
||||||
expect(actual).toMatchObject(postsAny.f);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should not return posts outside of the recent block', () => {
|
|
||||||
const postsAny = {
|
|
||||||
a: {id: 'a', channel_id: 'a', create_at: 1, user_id: 'a'},
|
|
||||||
};
|
|
||||||
const state = {
|
|
||||||
entities: {
|
|
||||||
users: {
|
|
||||||
currentUserId: user1.id,
|
|
||||||
profiles,
|
|
||||||
},
|
|
||||||
posts: {
|
|
||||||
posts: postsAny,
|
|
||||||
postsInChannel: {
|
|
||||||
abcd: [
|
|
||||||
{order: ['a'], recent: false},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
preferences: {
|
|
||||||
myPreferences: {},
|
|
||||||
},
|
|
||||||
channels: {
|
|
||||||
currentChannelId: 'abcd',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
} as unknown as GlobalState;
|
|
||||||
const actual = Selectors.getCurrentUsersLatestPost(state, 'e');
|
|
||||||
|
|
||||||
expect(actual).toEqual(null);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('determine the sending posts', () => {
|
|
||||||
const state = {
|
|
||||||
entities: {
|
|
||||||
users: {
|
|
||||||
currentUserId: user1.id,
|
|
||||||
profiles,
|
|
||||||
},
|
|
||||||
posts: {
|
|
||||||
posts: {},
|
|
||||||
postsInChannel: {},
|
|
||||||
pendingPostIds: ['1', '2', '3'],
|
|
||||||
},
|
|
||||||
preferences: {
|
|
||||||
myPreferences: {},
|
|
||||||
},
|
|
||||||
channels: {
|
|
||||||
currentChannelId: 'abcd',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
} as unknown as GlobalState;
|
|
||||||
|
|
||||||
expect(Selectors.isPostIdSending(state, '1')).toEqual(true);
|
|
||||||
expect(Selectors.isPostIdSending(state, '2')).toEqual(true);
|
|
||||||
expect(Selectors.isPostIdSending(state, '3')).toEqual(true);
|
|
||||||
expect(Selectors.isPostIdSending(state, '4')).toEqual(false);
|
|
||||||
expect(Selectors.isPostIdSending(state, '')).toEqual(false);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('makeGetProfilesForThread', () => {
|
describe('makeGetProfilesForThread', () => {
|
||||||
it('should return profiles for threads in the right order and exclude current user', () => {
|
it('should return profiles for threads in the right order and exclude current user', () => {
|
||||||
const getProfilesForThread = Selectors.makeGetProfilesForThread();
|
const getProfilesForThread = Selectors.makeGetProfilesForThread();
|
||||||
|
@ -22,13 +22,13 @@ import type {
|
|||||||
import {General, Posts, Preferences} from 'mattermost-redux/constants';
|
import {General, Posts, Preferences} from 'mattermost-redux/constants';
|
||||||
import {createSelector} from 'mattermost-redux/selectors/create_selector';
|
import {createSelector} from 'mattermost-redux/selectors/create_selector';
|
||||||
import {getChannel} from 'mattermost-redux/selectors/entities/channels';
|
import {getChannel} from 'mattermost-redux/selectors/entities/channels';
|
||||||
import {getCurrentUser} from 'mattermost-redux/selectors/entities/common';
|
import {getCurrentChannelId, getCurrentUser} from 'mattermost-redux/selectors/entities/common';
|
||||||
import {getConfig} from 'mattermost-redux/selectors/entities/general';
|
import {getConfig} from 'mattermost-redux/selectors/entities/general';
|
||||||
import {getMyPreferences} from 'mattermost-redux/selectors/entities/preferences';
|
import {getMyPreferences} from 'mattermost-redux/selectors/entities/preferences';
|
||||||
import {getCurrentTeamId} from 'mattermost-redux/selectors/entities/teams';
|
import {getCurrentTeamId} from 'mattermost-redux/selectors/entities/teams';
|
||||||
import {getUsers, getCurrentUserId, getUserStatuses} from 'mattermost-redux/selectors/entities/users';
|
import {getUsers, getCurrentUserId, getUserStatuses} from 'mattermost-redux/selectors/entities/users';
|
||||||
import {createIdsSelector} from 'mattermost-redux/utils/helpers';
|
import {createIdsSelector} from 'mattermost-redux/utils/helpers';
|
||||||
import {shouldShowJoinLeaveMessages} from 'mattermost-redux/utils/post_list';
|
import {isCombinedUserActivityPost, shouldShowJoinLeaveMessages} from 'mattermost-redux/utils/post_list';
|
||||||
import {
|
import {
|
||||||
isPostEphemeral,
|
isPostEphemeral,
|
||||||
isSystemMessage,
|
isSystemMessage,
|
||||||
@ -269,9 +269,71 @@ function formatPostInChannel(post: Post, previousPost: Post | undefined | null,
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getLatestInteractablePostId(state: GlobalState, channelId: string, rootId = '') {
|
||||||
|
const postsIds = rootId ? getPostsInThread(state)[rootId] : getPostIdsInChannel(state, channelId);
|
||||||
|
if (!postsIds) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
const allPosts = getAllPosts(state);
|
||||||
|
|
||||||
|
for (const postId of postsIds) {
|
||||||
|
if (isCombinedUserActivityPost(postId)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const post = allPosts[postId];
|
||||||
|
if (!post) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (post.delete_at) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isPostEphemeral(post)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isSystemMessage(post)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
return postId;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (rootId && allPosts[rootId] && !allPosts[rootId].delete_at) {
|
||||||
|
return rootId;
|
||||||
|
}
|
||||||
|
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getLatestPostToEdit(state: GlobalState, channelId: string, rootId = '') {
|
||||||
|
const postsIds = rootId ? getPostsInThread(state)[rootId] : getPostIdsInChannel(state, channelId);
|
||||||
|
if (!postsIds) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
const allPosts = getAllPosts(state);
|
||||||
|
const currentUserId = getCurrentUserId(state);
|
||||||
|
|
||||||
|
for (const postId of postsIds) {
|
||||||
|
const post = allPosts[postId];
|
||||||
|
if (!post || post.user_id !== currentUserId || (post.props?.from_webhook) || post.state === Posts.POST_DELETED || isSystemMessage(post) || isPostEphemeral(post) || isPostPendingOrFailed(post)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
return post.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getLatestReplyablePostId: (state: GlobalState) => Post['id'] = (state) => getLatestInteractablePostId(state, getCurrentChannelId(state));
|
||||||
|
|
||||||
// makeGetPostsInChannel creates a selector that returns up to the given number of posts loaded at the bottom of the
|
// makeGetPostsInChannel creates a selector that returns up to the given number of posts loaded at the bottom of the
|
||||||
// given channel. It does not include older posts such as those loaded by viewing a thread or a permalink.
|
// given channel. It does not include older posts such as those loaded by viewing a thread or a permalink.
|
||||||
|
|
||||||
export function makeGetPostsInChannel(): (state: GlobalState, channelId: Channel['id'], numPosts: number) => PostWithFormatData[] | undefined | null {
|
export function makeGetPostsInChannel(): (state: GlobalState, channelId: Channel['id'], numPosts: number) => PostWithFormatData[] | undefined | null {
|
||||||
return createSelector(
|
return createSelector(
|
||||||
'makeGetPostsInChannel',
|
'makeGetPostsInChannel',
|
||||||
@ -526,50 +588,6 @@ export const getMostRecentPostIdInChannel: (state: GlobalState, channelId: Chann
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
export const getLatestReplyablePostId: (state: GlobalState) => Post['id'] = createSelector(
|
|
||||||
'getLatestReplyablePostId',
|
|
||||||
getPostsInCurrentChannel,
|
|
||||||
(posts) => {
|
|
||||||
if (!posts) {
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
|
|
||||||
const latestReplyablePost = posts.find((post) => post.state !== Posts.POST_DELETED && !isSystemMessage(post) && !isPostEphemeral(post));
|
|
||||||
if (!latestReplyablePost) {
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
|
|
||||||
return latestReplyablePost.id;
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
export const getCurrentUsersLatestPost: (state: GlobalState, postId: Post['id']) => PostWithFormatData | undefined | null = createSelector(
|
|
||||||
'getCurrentUsersLatestPost',
|
|
||||||
getPostsInCurrentChannel,
|
|
||||||
getCurrentUser,
|
|
||||||
(state: GlobalState, rootId: string) => rootId,
|
|
||||||
(posts, currentUser, rootId) => {
|
|
||||||
if (!posts) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const lastPost = posts.find((post) => {
|
|
||||||
// don't edit webhook posts, deleted posts, or system messages
|
|
||||||
if (post.user_id !== currentUser.id || (post.props && post.props.from_webhook) || post.state === Posts.POST_DELETED || isSystemMessage(post) || isPostEphemeral(post) || isPostPendingOrFailed(post)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (rootId) {
|
|
||||||
return post.root_id === rootId || post.id === rootId;
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
});
|
|
||||||
|
|
||||||
return lastPost;
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
export function getRecentPostsChunkInChannel(state: GlobalState, channelId: Channel['id']): PostOrderBlock | null | undefined {
|
export function getRecentPostsChunkInChannel(state: GlobalState, channelId: Channel['id']): PostOrderBlock | null | undefined {
|
||||||
const postsForChannel = state.entities.posts.postsInChannel[channelId];
|
const postsForChannel = state.entities.posts.postsInChannel[channelId];
|
||||||
|
|
||||||
|
@ -8,7 +8,7 @@ import {createSelector} from 'mattermost-redux/selectors/create_selector';
|
|||||||
import {makeGetChannel} from 'mattermost-redux/selectors/entities/channels';
|
import {makeGetChannel} from 'mattermost-redux/selectors/entities/channels';
|
||||||
import {getCurrentUserId} from 'mattermost-redux/selectors/entities/users';
|
import {getCurrentUserId} from 'mattermost-redux/selectors/entities/users';
|
||||||
|
|
||||||
import {makeGetGlobalItem, makeGetGlobalItemWithDefault} from 'selectors/storage';
|
import {getGlobalItem, makeGetGlobalItem, makeGetGlobalItemWithDefault} from 'selectors/storage';
|
||||||
|
|
||||||
import type {SidebarSize} from 'components/resizable_sidebar/constants';
|
import type {SidebarSize} from 'components/resizable_sidebar/constants';
|
||||||
|
|
||||||
@ -138,6 +138,37 @@ export function getIsSearchGettingMore(state: GlobalState): boolean {
|
|||||||
return state.entities.search.isSearchGettingMore;
|
return state.entities.search.isSearchGettingMore;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function makeGetDraft() {
|
||||||
|
let defaultDraft = {message: '', fileInfos: [], uploadsInProgress: [], createAt: 0, updateAt: 0, channelId: '', rootId: ''};
|
||||||
|
return (state: GlobalState, channelId: string, rootId = ''): PostDraft => {
|
||||||
|
if (defaultDraft.channelId !== channelId || defaultDraft.rootId !== rootId) {
|
||||||
|
defaultDraft = {message: '', fileInfos: [], uploadsInProgress: [], createAt: 0, updateAt: 0, channelId, rootId};
|
||||||
|
}
|
||||||
|
const prefix = rootId ? StoragePrefixes.COMMENT_DRAFT : StoragePrefixes.DRAFT;
|
||||||
|
const suffix = rootId || channelId;
|
||||||
|
const draft = getGlobalItem(state, `${prefix}${suffix}`, defaultDraft);
|
||||||
|
|
||||||
|
let toReturn = defaultDraft;
|
||||||
|
if (
|
||||||
|
typeof draft.message !== 'undefined' &&
|
||||||
|
typeof draft.uploadsInProgress !== 'undefined' &&
|
||||||
|
typeof draft.fileInfos !== 'undefined'
|
||||||
|
) {
|
||||||
|
toReturn = draft;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (draft.rootId !== rootId || draft.channelId !== channelId) {
|
||||||
|
toReturn = {
|
||||||
|
...draft,
|
||||||
|
rootId,
|
||||||
|
channelId,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return toReturn;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export function makeGetChannelDraft() {
|
export function makeGetChannelDraft() {
|
||||||
const defaultDraft = Object.freeze({message: '', fileInfos: [], uploadsInProgress: [], createAt: 0, updateAt: 0, channelId: '', rootId: ''});
|
const defaultDraft = Object.freeze({message: '', fileInfos: [], uploadsInProgress: [], createAt: 0, updateAt: 0, channelId: '', rootId: ''});
|
||||||
const getDraft = makeGetGlobalItemWithDefault(defaultDraft);
|
const getDraft = makeGetGlobalItemWithDefault(defaultDraft);
|
||||||
|
@ -6,21 +6,21 @@ import type {GlobalState} from 'types/store';
|
|||||||
export const getGlobalItem = <T = any>(state: GlobalState, name: string, defaultValue: T) => {
|
export const getGlobalItem = <T = any>(state: GlobalState, name: string, defaultValue: T) => {
|
||||||
const storage = state && state.storage && state.storage.storage;
|
const storage = state && state.storage && state.storage.storage;
|
||||||
|
|
||||||
return getItemFromStorage(storage, name, defaultValue);
|
return getItemFromStorage<T>(storage, name, defaultValue);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const makeGetGlobalItem = <T = any>(name: string, defaultValue: T) => {
|
export const makeGetGlobalItem = <T = any>(name: string, defaultValue: T) => {
|
||||||
return (state: GlobalState) => {
|
return (state: GlobalState) => {
|
||||||
return getGlobalItem(state, name, defaultValue);
|
return getGlobalItem<T>(state, name, defaultValue);
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getItemFromStorage = <T = any>(storage: Record<string, any>, name: string, defaultValue: T) => {
|
export const getItemFromStorage = <T = any>(storage: Record<string, any>, name: string, defaultValue: T): T => {
|
||||||
return storage[name]?.value ?? defaultValue;
|
return storage[name]?.value ?? defaultValue;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const makeGetGlobalItemWithDefault = <T = any>(defaultValue: T) => {
|
export const makeGetGlobalItemWithDefault = <T = any>(defaultValue: T) => {
|
||||||
return (state: GlobalState, name: string) => {
|
return (state: GlobalState, name: string) => {
|
||||||
return getGlobalItem(state, name, defaultValue);
|
return getGlobalItem<T>(state, name, defaultValue);
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
@ -303,7 +303,7 @@ export function postMessageOnKeyPress(
|
|||||||
now = 0,
|
now = 0,
|
||||||
lastChannelSwitchAt = 0,
|
lastChannelSwitchAt = 0,
|
||||||
caretPosition = 0,
|
caretPosition = 0,
|
||||||
): {allowSending: boolean; ignoreKeyPress?: boolean} {
|
): {allowSending: boolean; ignoreKeyPress?: boolean; withClosedCodeBlock?: boolean; message?: string} {
|
||||||
if (!event) {
|
if (!event) {
|
||||||
return {allowSending: false};
|
return {allowSending: false};
|
||||||
}
|
}
|
||||||
@ -341,6 +341,10 @@ export function postMessageOnKeyPress(
|
|||||||
return {allowSending: false};
|
return {allowSending: false};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function isServerError(err: unknown): err is ServerError {
|
||||||
|
return Boolean(err && typeof err === 'object' && 'server_error_id' in err);
|
||||||
|
}
|
||||||
|
|
||||||
export function isErrorInvalidSlashCommand(error: ServerError | null): boolean {
|
export function isErrorInvalidSlashCommand(error: ServerError | null): boolean {
|
||||||
if (error && error.server_error_id) {
|
if (error && error.server_error_id) {
|
||||||
return error.server_error_id === 'api.command.execute_command.not_found.app_error';
|
return error.server_error_id === 'api.command.execute_command.not_found.app_error';
|
||||||
|
@ -803,7 +803,7 @@ export function setSelectionRange(input: HTMLInputElement | HTMLTextAreaElement,
|
|||||||
input.setSelectionRange(selectionStart, selectionEnd);
|
input.setSelectionRange(selectionStart, selectionEnd);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function setCaretPosition(input: HTMLInputElement, pos: number) {
|
export function setCaretPosition(input: HTMLInputElement | HTMLTextAreaElement, pos: number) {
|
||||||
if (!input) {
|
if (!input) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user