Skip to content
This repository has been archived by the owner on Mar 13, 2024. It is now read-only.

Commit

Permalink
[MM-37048] Do not save drafts until typing has stopped for X millisec…
Browse files Browse the repository at this point in the history
…onds (#8396)

* Do not save drafts until typing has stopped for X milliseconds

* Fix comment box relying on draft stored in redux

* Fix tests

* Fix another test
  • Loading branch information
jwilander committed Jul 23, 2021
1 parent 5fe6620 commit 6efce9e
Show file tree
Hide file tree
Showing 7 changed files with 77 additions and 95 deletions.
3 changes: 1 addition & 2 deletions actions/views/create_comment.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -146,8 +146,7 @@ export function submitCommand(channelId, rootId, draft) {
}

export function makeOnSubmit(channelId, rootId, latestPostId) {
return (options = {}) => async (dispatch, getState) => {
const draft = getPostDraft(getState(), StoragePrefixes.COMMENT_DRAFT, rootId);
return (draft, options = {}) => async (dispatch, getState) => {
const {message} = draft;

dispatch(addMessageIntoHistory(message));
Expand Down
97 changes: 27 additions & 70 deletions actions/views/create_comment.test.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -332,9 +332,14 @@ describe('rhs view actions', () => {

describe('makeOnSubmit', () => {
const onSubmit = makeOnSubmit(channelId, rootId, latestPostId);
const draft = {
message: '',
fileInfos: [],
uploadsInProgress: [],
};

test('it adds message into history', () => {
store.dispatch(onSubmit());
store.dispatch(onSubmit(draft));

const testStore = mockStore(initialState);
testStore.dispatch(addMessageIntoHistory(''));
Expand All @@ -345,7 +350,7 @@ describe('rhs view actions', () => {
});

test('it clears comment draft', () => {
store.dispatch(onSubmit());
store.dispatch(onSubmit(draft));

const testStore = mockStore(initialState);
testStore.dispatch(updateCommentDraft(rootId, null));
Expand All @@ -356,23 +361,11 @@ describe('rhs view actions', () => {
});

test('it submits a reaction when message is +:smile:', () => {
store = mockStore({
...initialState,
storage: {
storage: {
[`${StoragePrefixes.COMMENT_DRAFT}${rootId}`]: {
value: {
message: '+:smile:',
fileInfos: [],
uploadsInProgress: [],
},
timestamp: new Date(),
},
},
},
});

store.dispatch(onSubmit());
store.dispatch(onSubmit({
message: '+:smile:',
fileInfos: [],
uploadsInProgress: [],
}));

const testStore = mockStore(initialState);
testStore.dispatch(submitReaction(latestPostId, '+', 'smile'));
Expand All @@ -383,23 +376,11 @@ describe('rhs view actions', () => {
});

test('it submits a command when message is /away', () => {
store = mockStore({
...initialState,
storage: {
storage: {
[`${StoragePrefixes.COMMENT_DRAFT}${latestPostId}`]: {
value: {
message: '/away',
fileInfos: [],
uploadsInProgress: [],
},
timestamp: new Date(),
},
},
},
});

store.dispatch(onSubmit());
store.dispatch(onSubmit({
message: '/away',
fileInfos: [],
uploadsInProgress: [],
}));

const testStore = mockStore(initialState);
testStore.dispatch(submitCommand(channelId, rootId, {message: '/away', fileInfos: [], uploadsInProgress: []}));
Expand All @@ -412,23 +393,11 @@ describe('rhs view actions', () => {
});

test('it submits a regular post when options.ignoreSlash is true', () => {
store = mockStore({
...initialState,
storage: {
storage: {
[`${StoragePrefixes.COMMENT_DRAFT}${latestPostId}`]: {
value: {
message: '/fakecommand',
fileInfos: [],
uploadsInProgress: [],
},
timestamp: new Date(),
},
},
},
});

store.dispatch(onSubmit({ignoreSlash: true}));
store.dispatch(onSubmit({
message: '/fakecommand',
fileInfos: [],
uploadsInProgress: [],
}, {ignoreSlash: true}));

const testStore = mockStore(initialState);
testStore.dispatch(submitPost(channelId, rootId, {message: '/fakecommand', fileInfos: [], uploadsInProgress: []}));
Expand All @@ -440,23 +409,11 @@ describe('rhs view actions', () => {
});

test('it submits a regular post when message is something else', () => {
store = mockStore({
...initialState,
storage: {
storage: {
[`${StoragePrefixes.COMMENT_DRAFT}${latestPostId}`]: {
value: {
message: 'test msg',
fileInfos: [],
uploadsInProgress: [],
},
timestamp: new Date(),
},
},
},
});

store.dispatch(onSubmit());
store.dispatch(onSubmit({
message: 'test msg',
fileInfos: [],
uploadsInProgress: [],
}));

const testStore = mockStore(initialState);
testStore.dispatch(submitPost(channelId, rootId, {message: 'test msg', fileInfos: [], uploadsInProgress: []}));
Expand Down
30 changes: 18 additions & 12 deletions components/create_comment/create_comment.jsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.

/* eslint-disable max-lines */

import PropTypes from 'prop-types';
import React from 'react';
import classNames from 'classnames';
Expand Down Expand Up @@ -38,6 +40,8 @@ import MessageSubmitError from 'components/message_submit_error';

const KeyCodes = Constants.KeyCodes;

const CreateCommentDraftTimeoutMilliseconds = 500;

class CreateComment extends React.PureComponent {
static propTypes = {

Expand Down Expand Up @@ -75,11 +79,6 @@ class CreateComment extends React.PureComponent {
fileInfos: PropTypes.array.isRequired,
}).isRequired,

/**
* Whether the submit button is enabled
*/
enableAddButton: PropTypes.bool.isRequired,

/**
* Force message submission on CTRL/CMD + ENTER
*/
Expand Down Expand Up @@ -306,7 +305,7 @@ class CreateComment extends React.PureComponent {
document.removeEventListener('keydown', this.focusTextboxIfNecessary);

if (this.saveDraftFrame) {
cancelAnimationFrame(this.saveDraftFrame);
clearTimeout(this.saveDraftFrame);

this.props.onUpdateCommentDraft(this.state.draft);
}
Expand Down Expand Up @@ -629,7 +628,7 @@ class CreateComment extends React.PureComponent {
const options = {ignoreSlash};

try {
await this.props.onSubmit(options);
await this.props.onSubmit(draft, options);

this.setState({
postError: null,
Expand All @@ -644,7 +643,9 @@ class CreateComment extends React.PureComponent {
return;
}

clearTimeout(this.saveDraftFrame);
this.setState({draft: {...this.props.draft, uploadsInProgress: []}});
this.draftsForPost[this.props.rootId] = null;
}

commentMsgKeyPress = (e) => {
Expand Down Expand Up @@ -708,10 +709,10 @@ class CreateComment extends React.PureComponent {
const {draft} = this.state;
const updatedDraft = {...draft, message};

cancelAnimationFrame(this.saveDraftFrame);
this.saveDraftFrame = requestAnimationFrame(() => {
clearTimeout(this.saveDraftFrame);
this.saveDraftFrame = setTimeout(() => {
this.props.onUpdateCommentDraft(updatedDraft);
});
}, CreateCommentDraftTimeoutMilliseconds);

this.setState({draft: updatedDraft, serverError}, () => {
if (this.props.scrollToBottom) {
Expand Down Expand Up @@ -964,8 +965,13 @@ class CreateComment extends React.PureComponent {
}

shouldEnableAddButton = () => {
if (this.props.enableAddButton) {
return true;
const {draft} = this.state;
if (draft) {
const message = draft.message ? draft.message.trim() : '';
const fileInfos = draft.fileInfos ? draft.fileInfos : [];
if (message.trim().length !== 0 || fileInfos.length !== 0) {
return true;
}
}

return isErrorInvalidSlashCommand(this.state.serverError);
Expand Down
24 changes: 20 additions & 4 deletions components/create_comment/create_comment.test.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -510,7 +510,11 @@ describe('components/CreateComment', () => {

await wrapper.instance().handleSubmit({preventDefault: jest.fn()});

expect(onSubmit).toHaveBeenCalledWith({ignoreSlash: false});
expect(onSubmit).toHaveBeenCalledWith({
message: '/fakecommand other text',
uploadsInProgress: [],
fileInfos: [{}, {}, {}],
}, {ignoreSlash: false});
expect(wrapper.find('[id="postServerError"]').exists()).toBe(true);

wrapper.instance().handleChange({
Expand All @@ -521,7 +525,11 @@ describe('components/CreateComment', () => {

wrapper.instance().handleSubmit({preventDefault: jest.fn()});

expect(onSubmit).toHaveBeenCalledWith({ignoreSlash: false});
expect(onSubmit).toHaveBeenCalledWith({
message: 'some valid text',
uploadsInProgress: [],
fileInfos: [{}, {}, {}],
}, {ignoreSlash: false});
});

test('should scroll to bottom when uploadsInProgress increase', () => {
Expand Down Expand Up @@ -925,14 +933,22 @@ describe('components/CreateComment', () => {

await wrapper.instance().handleSubmit({preventDefault});

expect(onSubmitWithError).toHaveBeenCalledWith({ignoreSlash: false});
expect(onSubmitWithError).toHaveBeenCalledWith({
message: '/fakecommand other text',
uploadsInProgress: [],
fileInfos: [{}, {}, {}],
}, {ignoreSlash: false});
expect(preventDefault).toHaveBeenCalled();
expect(wrapper.find('[id="postServerError"]').exists()).toBe(true);

wrapper.setProps({onSubmit});
await wrapper.instance().handleSubmit({preventDefault});

expect(onSubmit).toHaveBeenCalledWith({ignoreSlash: true});
expect(onSubmit).toHaveBeenCalledWith({
message: '/fakecommand other text',
uploadsInProgress: [],
fileInfos: [{}, {}, {}],
}, {ignoreSlash: true});
expect(wrapper.find('[id="postServerError"]').exists()).toBe(false);
});

Expand Down
2 changes: 0 additions & 2 deletions components/create_comment/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,6 @@ function makeMapStateToProps() {
const err = state.requests.posts.createPost.error || {};

const draft = getPostDraft(state, StoragePrefixes.COMMENT_DRAFT, ownProps.rootId);
const enableAddButton = draft.message.trim().length !== 0 || draft.fileInfos.length !== 0;

const channelMembersCount = getAllChannelStats(state)[ownProps.channelId] ? getAllChannelStats(state)[ownProps.channelId].member_count : 1;
const messageInHistory = getMessageInHistoryItem(state);
Expand Down Expand Up @@ -80,7 +79,6 @@ function makeMapStateToProps() {
return {
draft,
messageInHistory,
enableAddButton,
channelMembersCount,
codeBlockOnCtrlEnter: getBool(state, Preferences.CATEGORY_ADVANCED_SETTINGS, 'code_block_ctrl_enter', true),
ctrlSend: getBool(state, Preferences.CATEGORY_ADVANCED_SETTINGS, 'send_on_ctrl_enter'),
Expand Down
14 changes: 9 additions & 5 deletions components/create_post/create_post.jsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.

/* eslint-disable max-lines */

import PropTypes from 'prop-types';
import React from 'react';
import classNames from 'classnames';
Expand Down Expand Up @@ -46,6 +48,8 @@ import MessageSubmitError from 'components/message_submit_error';

const KeyCodes = Constants.KeyCodes;

const CreatePostDraftTimeoutMilliseconds = 500;

// Temporary fix for IE-11, see MM-13423
function trimRight(str) {
if (String.prototype.trimRight) {
Expand Down Expand Up @@ -375,7 +379,7 @@ class CreatePost extends React.PureComponent {
if (this.saveDraftFrame) {
const channelId = this.props.currentChannel.id;
this.props.actions.setDraft(StoragePrefixes.DRAFT + channelId, this.draftsForChannel[channelId]);
cancelAnimationFrame(this.saveDraftFrame);
clearTimeout(this.saveDraftFrame);
}
}

Expand Down Expand Up @@ -532,7 +536,7 @@ class CreatePost extends React.PureComponent {
postError: null,
});

cancelAnimationFrame(this.saveDraftFrame);
clearTimeout(this.saveDraftFrame);
this.props.actions.setDraft(StoragePrefixes.DRAFT + channelId, null);
this.draftsForChannel[channelId] = null;
}
Expand Down Expand Up @@ -802,10 +806,10 @@ class CreatePost extends React.PureComponent {
...this.props.draft,
message,
};
cancelAnimationFrame(this.saveDraftFrame);
this.saveDraftFrame = requestAnimationFrame(() => {
clearTimeout(this.saveDraftFrame);
this.saveDraftFrame = setTimeout(() => {
this.props.actions.setDraft(StoragePrefixes.DRAFT + channelId, draft);
});
}, CreatePostDraftTimeoutMilliseconds);
this.draftsForChannel[channelId] = draft;
}

Expand Down
2 changes: 2 additions & 0 deletions components/threading/thread_viewer/thread_viewer.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.

/* eslint-disable max-lines */

import React, {HTMLAttributes} from 'react';
import Scrollbars from 'react-custom-scrollbars';
import classNames from 'classnames';
Expand Down

0 comments on commit 6efce9e

Please sign in to comment.