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 {StoragePrefixes} from 'utils/constants';
|
||||
import {TestHelper} from 'utils/test_helper';
|
||||
|
||||
/* eslint-disable global-require */
|
||||
|
||||
@ -121,13 +122,21 @@ describe('rhs view actions', () => {
|
||||
messages: ['test message'],
|
||||
},
|
||||
},
|
||||
channels: {
|
||||
channels: {
|
||||
[channelId]: TestHelper.getChannelMock({id: channelId}),
|
||||
},
|
||||
roles: {
|
||||
[channelId]: new Set(['channel_roles']),
|
||||
},
|
||||
},
|
||||
preferences: {
|
||||
myPreferences: {},
|
||||
},
|
||||
users: {
|
||||
currentUserId,
|
||||
profiles: {
|
||||
[currentUserId]: {id: currentUserId},
|
||||
[currentUserId]: TestHelper.getUserMock({id: currentUserId}),
|
||||
},
|
||||
},
|
||||
teams: {
|
||||
@ -136,6 +145,13 @@ describe('rhs view actions', () => {
|
||||
emojis: {
|
||||
customEmoji: {},
|
||||
},
|
||||
roles: {
|
||||
roles: {
|
||||
channel_roles: {
|
||||
permissions: '',
|
||||
},
|
||||
},
|
||||
},
|
||||
general: {
|
||||
config: {
|
||||
EnableCustomEmoji: 'true',
|
||||
|
@ -6,12 +6,20 @@ import type {Post} from '@mattermost/types/posts';
|
||||
import {
|
||||
addMessageIntoHistory,
|
||||
} from 'mattermost-redux/actions/posts';
|
||||
import {Permissions} from 'mattermost-redux/constants';
|
||||
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 {getLicense} from 'mattermost-redux/selectors/entities/general';
|
||||
import {getAssociatedGroupsForReferenceByMention} from 'mattermost-redux/selectors/entities/groups';
|
||||
import {
|
||||
getLatestInteractablePostId,
|
||||
getLatestPostToEdit,
|
||||
getPost,
|
||||
makeGetPostIdsForThread,
|
||||
} 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 {getCurrentUserId} from 'mattermost-redux/selectors/entities/users';
|
||||
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 EmojiMap from 'utils/emoji_map';
|
||||
import {containsAtChannel, groupsMentionedInText} from 'utils/post_utils';
|
||||
import * as Utils from 'utils/utils';
|
||||
|
||||
import type {GlobalState} from 'types/store';
|
||||
@ -63,10 +72,30 @@ export function submitPost(channelId: string, rootId: string, draft: PostDraft):
|
||||
pending_post_id: `${userId}:${time}`,
|
||||
user_id: userId,
|
||||
create_at: time,
|
||||
metadata: {},
|
||||
metadata: {...draft.metadata},
|
||||
props: {...draft.props},
|
||||
} 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));
|
||||
if (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() {
|
||||
const getPostIdsInThread = makeGetPostIdsForThread();
|
||||
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;
|
||||
if (value) {
|
||||
const timestamp = new Date().getTime();
|
||||
const data = getGlobalItem(state, key, {});
|
||||
const data = getGlobalItem<Partial<PostDraft>>(state, key, {});
|
||||
updatedValue = {
|
||||
...value,
|
||||
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.
|
||||
// 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';
|
||||
|
||||
type OwnProps = {
|
||||
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);
|
||||
export default 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.
|
||||
// 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';
|
||||
|
||||
function makeMapStateToProps() {
|
||||
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);
|
||||
export default AdvancedCreatePost;
|
||||
|
@ -3,19 +3,24 @@
|
||||
|
||||
import React, {useMemo, memo} from 'react';
|
||||
import {defineMessage, useIntl} from 'react-intl';
|
||||
import {useSelector} from 'react-redux';
|
||||
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 Chip from 'components/common/chip/chip';
|
||||
|
||||
import Constants from 'utils/constants';
|
||||
|
||||
import type {GlobalState} from 'types/store';
|
||||
|
||||
type Props = {
|
||||
prefillMessage: (msg: string, shouldFocus: boolean) => void;
|
||||
currentChannel: Channel;
|
||||
channelId: string;
|
||||
currentUserId: string;
|
||||
currentChannelTeammateUsername?: string;
|
||||
}
|
||||
|
||||
const UsernameMention = styled.span`
|
||||
@ -28,8 +33,11 @@ const ChipContainer = styled.div`
|
||||
flex-wrap: wrap;
|
||||
`;
|
||||
|
||||
const PrewrittenChips = ({currentChannel, currentUserId, currentChannelTeammateUsername, prefillMessage}: Props) => {
|
||||
const PrewrittenChips = ({channelId, currentUserId, prefillMessage}: Props) => {
|
||||
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 customChip = {
|
||||
@ -45,7 +53,11 @@ const PrewrittenChips = ({currentChannel, currentUserId, currentChannelTeammateU
|
||||
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 [
|
||||
{
|
||||
event: 'prefilled_message_selected_team_hi',
|
||||
@ -87,7 +99,7 @@ const PrewrittenChips = ({currentChannel, currentUserId, currentChannelTeammateU
|
||||
];
|
||||
}
|
||||
|
||||
if (currentChannel.teammate_id === currentUserId) {
|
||||
if (channelTeammateId === currentUserId) {
|
||||
return [
|
||||
{
|
||||
event: 'prefilled_message_selected_self_note',
|
||||
@ -144,12 +156,12 @@ const PrewrittenChips = ({currentChannel, currentUserId, currentChannelTeammateU
|
||||
},
|
||||
customChip,
|
||||
];
|
||||
}, [currentChannel, currentUserId]);
|
||||
}, [channelType, channelTeammateId, currentUserId]);
|
||||
|
||||
return (
|
||||
<ChipContainer>
|
||||
{chips.map(({event, message, display, leadingIcon}) => {
|
||||
const values = {username: currentChannelTeammateUsername};
|
||||
const values = {username: channelTeammateUsername};
|
||||
const messageToPrefill = message.id ? formatMessage(
|
||||
message,
|
||||
values,
|
||||
@ -157,15 +169,14 @@ const PrewrittenChips = ({currentChannel, currentUserId, currentChannelTeammateU
|
||||
|
||||
const additionalMarkup = message.id === 'create_post.prewritten.tip.dm_hey' ? (
|
||||
<UsernameMention>
|
||||
{'@'}{currentChannelTeammateUsername}
|
||||
{'@'}{channelTeammateUsername}
|
||||
</UsernameMention>
|
||||
) : null;
|
||||
|
||||
return (
|
||||
<Chip
|
||||
key={display.id}
|
||||
id={display.id}
|
||||
defaultMessage={display.defaultMessage}
|
||||
display={display}
|
||||
additionalMarkup={additionalMarkup}
|
||||
values={values}
|
||||
onClick={() => {
|
||||
|
@ -1,7 +1,6 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {screen} from '@testing-library/react';
|
||||
import React from 'react';
|
||||
|
||||
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 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 type {PostDraft} from 'types/store/draft';
|
||||
@ -146,71 +145,6 @@ const baseProps = {
|
||||
|
||||
describe('components/avanced_text_editor/advanced_text_editor', () => {
|
||||
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', () => {
|
||||
renderWithContext(
|
||||
<AdavancedTextEditor
|
||||
@ -230,59 +164,5 @@ describe('components/avanced_text_editor/advanced_text_editor', () => {
|
||||
userEvent.type(textbox, 'something{esc}');
|
||||
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 React, {useState, useEffect} from 'react';
|
||||
import {FormattedMessage} from 'react-intl';
|
||||
import {useSelector} from 'react-redux';
|
||||
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 {getUser} from 'mattermost-redux/selectors/entities/users';
|
||||
|
||||
import Moon from 'components/common/svg_images_components/moon_svg';
|
||||
import Timestamp from 'components/timestamp';
|
||||
@ -43,15 +45,26 @@ const Icon = styled(Moon)`
|
||||
`;
|
||||
|
||||
type Props = {
|
||||
teammate: UserProfile;
|
||||
teammateId: 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 [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(() => {
|
||||
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) {
|
||||
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);
|
||||
|
||||
return {
|
||||
|
@ -158,9 +158,7 @@ exports[`components/channel_view Should match snapshot with base props 1`] = `
|
||||
data-testid="post-create"
|
||||
id="post-create"
|
||||
>
|
||||
<Connect(AdvancedCreatePost)
|
||||
getChannelView={[Function]}
|
||||
/>
|
||||
<Memo(AdvancedCreatePost) />
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
@ -80,10 +80,6 @@ export default class ChannelView extends React.PureComponent<Props, State> {
|
||||
this.channelViewRef = React.createRef();
|
||||
}
|
||||
|
||||
getChannelView = () => {
|
||||
return this.channelViewRef.current;
|
||||
};
|
||||
|
||||
onClickCloseChannel = () => {
|
||||
this.props.goToLastViewedChannel();
|
||||
};
|
||||
@ -160,7 +156,7 @@ export default class ChannelView extends React.PureComponent<Props, State> {
|
||||
data-testid='post-create'
|
||||
className='post-create__container AdvancedTextEditor__ctr'
|
||||
>
|
||||
<AdvancedCreatePost getChannelView={this.getChannelView}/>
|
||||
<AdvancedCreatePost/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -2,6 +2,7 @@
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React, {useCallback} from 'react';
|
||||
import type {MessageDescriptor} from 'react-intl';
|
||||
import {FormattedMessage} from 'react-intl';
|
||||
import styled from 'styled-components';
|
||||
|
||||
@ -11,8 +12,7 @@ import RenderEmoji from 'components/emoji/render_emoji';
|
||||
|
||||
type Props = {
|
||||
onClick?: () => void;
|
||||
id?: string;
|
||||
defaultMessage?: string;
|
||||
display?: MessageDescriptor;
|
||||
values?: Record<string, any>;
|
||||
className?: string;
|
||||
|
||||
@ -59,8 +59,7 @@ const Chip = ({
|
||||
otherOption,
|
||||
className,
|
||||
leadingIcon,
|
||||
id,
|
||||
defaultMessage,
|
||||
display,
|
||||
values,
|
||||
additionalMarkup,
|
||||
}: Props) => {
|
||||
@ -81,10 +80,9 @@ const Chip = ({
|
||||
emojiStyle={emojiStyles}
|
||||
/>
|
||||
)}
|
||||
{(id && defaultMessage && values) && (
|
||||
{(display && values) && (
|
||||
<FormattedMessage
|
||||
id={id}
|
||||
defaultMessage={defaultMessage}
|
||||
{...display}
|
||||
values={values}
|
||||
/>
|
||||
)}
|
||||
|
@ -9,7 +9,7 @@ import type {UserProfile, UserStatus} from '@mattermost/types/users';
|
||||
|
||||
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 Markdown from 'components/markdown';
|
||||
import ProfilePicture from 'components/profile_picture';
|
||||
|
@ -5,10 +5,8 @@ import React, {memo, forwardRef, useMemo} from 'react';
|
||||
import {useSelector} from 'react-redux';
|
||||
|
||||
import {ArchiveOutlineIcon} from '@mattermost/compass-icons/components';
|
||||
import type {Post} from '@mattermost/types/posts';
|
||||
import type {UserProfile} from '@mattermost/types/users';
|
||||
|
||||
import {Posts} from 'mattermost-redux/constants';
|
||||
import {makeGetChannel} from 'mattermost-redux/selectors/entities/channels';
|
||||
import {getPost, getLimitedViews} from 'mattermost-redux/selectors/entities/posts';
|
||||
|
||||
@ -23,7 +21,6 @@ import type {GlobalState} from 'types/store';
|
||||
type Props = {
|
||||
teammate?: UserProfile;
|
||||
threadId: string;
|
||||
latestPostId: Post['id'];
|
||||
isThreadView?: boolean;
|
||||
placeholder?: string;
|
||||
};
|
||||
@ -31,7 +28,6 @@ type Props = {
|
||||
const CreateComment = forwardRef<HTMLDivElement, Props>(({
|
||||
teammate,
|
||||
threadId,
|
||||
latestPostId,
|
||||
isThreadView,
|
||||
placeholder,
|
||||
}: Props, ref) => {
|
||||
@ -47,7 +43,6 @@ const CreateComment = forwardRef<HTMLDivElement, Props>(({
|
||||
if (!channel || threadIsLimited) {
|
||||
return null;
|
||||
}
|
||||
const rootDeleted = (rootPost as Post).state === Posts.POST_DELETED;
|
||||
const isFakeDeletedPost = rootPost.type === Constants.PostTypes.FAKE_PARENT_DELETED;
|
||||
|
||||
const channelType = channel.type;
|
||||
@ -97,8 +92,6 @@ const CreateComment = forwardRef<HTMLDivElement, Props>(({
|
||||
<AdvancedCreateComment
|
||||
placeholder={placeholder}
|
||||
channelId={channel.id}
|
||||
latestPostId={latestPostId}
|
||||
rootDeleted={rootDeleted}
|
||||
rootId={threadId}
|
||||
isThreadView={isThreadView}
|
||||
/>
|
||||
|
@ -355,7 +355,6 @@ class ThreadViewerVirtualized extends PureComponent<Props, State> {
|
||||
<CreateComment
|
||||
placeholder={this.props.inputPlaceholder}
|
||||
isThreadView={this.props.isThreadView}
|
||||
latestPostId={this.props.lastPost.id}
|
||||
ref={this.postCreateContainerRef}
|
||||
teammate={this.props.directTeammate}
|
||||
threadId={this.props.selected.id}
|
||||
|
@ -5,7 +5,6 @@ import React from 'react';
|
||||
import {FormattedMessage} from 'react-intl';
|
||||
|
||||
import {useMeasurePunchouts} from '@mattermost/components';
|
||||
import type {Channel} from '@mattermost/types/channels';
|
||||
|
||||
import PrewrittenChips from 'components/advanced_create_post/prewritten_chips';
|
||||
|
||||
@ -13,25 +12,22 @@ import OnboardingTourTip from './onboarding_tour_tip';
|
||||
|
||||
type Props = {
|
||||
prefillMessage: (msg: string, shouldFocus: boolean) => void;
|
||||
currentChannel: Channel;
|
||||
channelId: string;
|
||||
currentUserId: string;
|
||||
currentChannelTeammateUsername?: string;
|
||||
}
|
||||
|
||||
const translate = {x: -6, y: -6};
|
||||
|
||||
export const SendMessageTour = ({
|
||||
prefillMessage,
|
||||
currentChannel,
|
||||
channelId,
|
||||
currentUserId,
|
||||
currentChannelTeammateUsername,
|
||||
}: Props) => {
|
||||
const chips = (
|
||||
<PrewrittenChips
|
||||
prefillMessage={prefillMessage}
|
||||
currentChannel={currentChannel}
|
||||
channelId={channelId}
|
||||
currentUserId={currentUserId}
|
||||
currentChannelTeammateUsername={currentChannelTeammateUsername}
|
||||
/>
|
||||
);
|
||||
|
||||
|
@ -3341,7 +3341,6 @@
|
||||
"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.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.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>",
|
||||
|
@ -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', () => {
|
||||
it('should return profiles for threads in the right order and exclude current user', () => {
|
||||
const getProfilesForThread = Selectors.makeGetProfilesForThread();
|
||||
|
@ -22,13 +22,13 @@ import type {
|
||||
import {General, Posts, Preferences} from 'mattermost-redux/constants';
|
||||
import {createSelector} from 'mattermost-redux/selectors/create_selector';
|
||||
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 {getMyPreferences} from 'mattermost-redux/selectors/entities/preferences';
|
||||
import {getCurrentTeamId} from 'mattermost-redux/selectors/entities/teams';
|
||||
import {getUsers, getCurrentUserId, getUserStatuses} from 'mattermost-redux/selectors/entities/users';
|
||||
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 {
|
||||
isPostEphemeral,
|
||||
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
|
||||
// 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 {
|
||||
return createSelector(
|
||||
'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 {
|
||||
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 {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';
|
||||
|
||||
@ -138,6 +138,37 @@ export function getIsSearchGettingMore(state: GlobalState): boolean {
|
||||
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() {
|
||||
const defaultDraft = Object.freeze({message: '', fileInfos: [], uploadsInProgress: [], createAt: 0, updateAt: 0, channelId: '', rootId: ''});
|
||||
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) => {
|
||||
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) => {
|
||||
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;
|
||||
};
|
||||
|
||||
export const makeGetGlobalItemWithDefault = <T = any>(defaultValue: T) => {
|
||||
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,
|
||||
lastChannelSwitchAt = 0,
|
||||
caretPosition = 0,
|
||||
): {allowSending: boolean; ignoreKeyPress?: boolean} {
|
||||
): {allowSending: boolean; ignoreKeyPress?: boolean; withClosedCodeBlock?: boolean; message?: string} {
|
||||
if (!event) {
|
||||
return {allowSending: false};
|
||||
}
|
||||
@ -341,6 +341,10 @@ export function postMessageOnKeyPress(
|
||||
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 {
|
||||
if (error && error.server_error_id) {
|
||||
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);
|
||||
}
|
||||
|
||||
export function setCaretPosition(input: HTMLInputElement, pos: number) {
|
||||
export function setCaretPosition(input: HTMLInputElement | HTMLTextAreaElement, pos: number) {
|
||||
if (!input) {
|
||||
return;
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user