PLT-6282 Make post list stay visible when post textbox height changes (#6323)

* PLT-6282 Changed post drafts to use an action when being stored

* PLT-6282 Triggered post list to update scroll position when post draft changes

* PLT-6282 Changed SuggestionBox to complete suggestions without an event
Этот коммит содержится в:
Harrison Healey
2017-05-23 10:17:06 -04:00
коммит произвёл GitHub
родитель 52f73c30ca
Коммит 69f3f2fdce
8 изменённых файлов: 87 добавлений и 56 удалений

Просмотреть файл

@@ -499,3 +499,11 @@ export function performSearch(terms, isMentionSearch, success, error) {
} }
); );
} }
export function storePostDraft(channelId, draft) {
AppDispatcher.handleViewAction({
type: ActionTypes.POST_DRAFT_CHANGED,
channelId,
draft
});
}

Просмотреть файл

@@ -71,10 +71,11 @@ export default class CreatePost extends React.Component {
PostStore.clearDraftUploads(); PostStore.clearDraftUploads();
const draft = PostStore.getCurrentDraft(); const channelId = ChannelStore.getCurrentId();
const draft = PostStore.getPostDraft(channelId);
this.state = { this.state = {
channelId: ChannelStore.getCurrentId(), channelId,
message: draft.message, message: draft.message,
uploadsInProgress: draft.uploadsInProgress, uploadsInProgress: draft.uploadsInProgress,
fileInfos: draft.fileInfos, fileInfos: draft.fileInfos,
@@ -136,7 +137,7 @@ export default class CreatePost extends React.Component {
const isReaction = REACTION_PATTERN.exec(post.message); const isReaction = REACTION_PATTERN.exec(post.message);
if (post.message.indexOf('/') === 0) { if (post.message.indexOf('/') === 0) {
PostStore.storeDraft(this.state.channelId, null); PostActions.storePostDraft(this.state.channelId, null);
this.setState({message: '', postError: null, fileInfos: [], enableSendButton: false}); this.setState({message: '', postError: null, fileInfos: [], enableSendButton: false});
const args = {}; const args = {};
@@ -241,7 +242,7 @@ export default class CreatePost extends React.Component {
PostActions.removeReaction(this.state.channelId, postId, emojiName); PostActions.removeReaction(this.state.channelId, postId, emojiName);
} }
PostStore.storeCurrentDraft(null); PostActions.storePostDraft(this.state.channelId, null);
} }
focusTextbox(keepFocus = false) { focusTextbox(keepFocus = false) {
@@ -271,9 +272,9 @@ export default class CreatePost extends React.Component {
enableSendButton enableSendButton
}); });
const draft = PostStore.getCurrentDraft(); const draft = PostStore.getPostDraft(this.state.channelId);
draft.message = message; draft.message = message;
PostStore.storeCurrentDraft(draft); PostActions.storePostDraft(this.state.channelId, draft);
} }
handleFileUploadChange() { handleFileUploadChange() {
@@ -281,10 +282,10 @@ export default class CreatePost extends React.Component {
} }
handleUploadStart(clientIds, channelId) { handleUploadStart(clientIds, channelId) {
const draft = PostStore.getDraft(channelId); const draft = PostStore.getPostDraft(channelId);
draft.uploadsInProgress = draft.uploadsInProgress.concat(clientIds); draft.uploadsInProgress = draft.uploadsInProgress.concat(clientIds);
PostStore.storeDraft(channelId, draft); PostActions.storePostDraft(channelId, draft);
this.setState({uploadsInProgress: draft.uploadsInProgress}); this.setState({uploadsInProgress: draft.uploadsInProgress});
@@ -294,7 +295,7 @@ export default class CreatePost extends React.Component {
} }
handleFileUploadComplete(fileInfos, clientIds, channelId) { handleFileUploadComplete(fileInfos, clientIds, channelId) {
const draft = PostStore.getDraft(channelId); const draft = PostStore.getPostDraft(channelId);
// remove each finished file from uploads // remove each finished file from uploads
for (let i = 0; i < clientIds.length; i++) { for (let i = 0; i < clientIds.length; i++) {
@@ -306,7 +307,7 @@ export default class CreatePost extends React.Component {
} }
draft.fileInfos = draft.fileInfos.concat(fileInfos); draft.fileInfos = draft.fileInfos.concat(fileInfos);
PostStore.storeDraft(channelId, draft); PostActions.storePostDraft(channelId, draft);
if (channelId === this.state.channelId) { if (channelId === this.state.channelId) {
this.setState({ this.setState({
@@ -325,14 +326,14 @@ export default class CreatePost extends React.Component {
} }
if (clientId !== -1) { if (clientId !== -1) {
const draft = PostStore.getDraft(channelId); const draft = PostStore.getPostDraft(channelId);
const index = draft.uploadsInProgress.indexOf(clientId); const index = draft.uploadsInProgress.indexOf(clientId);
if (index !== -1) { if (index !== -1) {
draft.uploadsInProgress.splice(index, 1); draft.uploadsInProgress.splice(index, 1);
} }
PostStore.storeDraft(channelId, draft); PostActions.storePostDraft(channelId, draft);
if (channelId === this.state.channelId) { if (channelId === this.state.channelId) {
this.setState({uploadsInProgress: draft.uploadsInProgress}); this.setState({uploadsInProgress: draft.uploadsInProgress});
@@ -362,10 +363,10 @@ export default class CreatePost extends React.Component {
fileInfos.splice(index, 1); fileInfos.splice(index, 1);
} }
const draft = PostStore.getCurrentDraft(); const draft = PostStore.getPostDraft(this.state.channelId);
draft.fileInfos = fileInfos; draft.fileInfos = fileInfos;
draft.uploadsInProgress = uploadsInProgress; draft.uploadsInProgress = uploadsInProgress;
PostStore.storeCurrentDraft(draft); PostActions.storePostDraft(this.state.channelId, draft);
const enableSendButton = this.handleEnableSendButton(this.state.message, fileInfos); const enableSendButton = this.handleEnableSendButton(this.state.message, fileInfos);
this.setState({fileInfos, uploadsInProgress, enableSendButton}); this.setState({fileInfos, uploadsInProgress, enableSendButton});
@@ -432,7 +433,7 @@ export default class CreatePost extends React.Component {
onChange() { onChange() {
const channelId = ChannelStore.getCurrentId(); const channelId = ChannelStore.getCurrentId();
if (this.state.channelId !== channelId) { if (this.state.channelId !== channelId) {
const draft = PostStore.getCurrentDraft(); const draft = PostStore.getPostDraft(channelId);
this.setState({channelId, message: draft.message, submitting: false, serverError: null, postError: null, fileInfos: draft.fileInfos, uploadsInProgress: draft.uploadsInProgress}); this.setState({channelId, message: draft.message, submitting: false, serverError: null, postError: null, fileInfos: draft.fileInfos, uploadsInProgress: draft.uploadsInProgress});
} }
@@ -453,7 +454,7 @@ export default class CreatePost extends React.Component {
return this.state.fileInfos.length + this.state.uploadsInProgress.length; return this.state.fileInfos.length + this.state.uploadsInProgress.length;
} }
const draft = PostStore.getDraft(channelId); const draft = PostStore.getPostDraft(channelId);
return draft.fileInfos.length + draft.uploadsInProgress.length; return draft.fileInfos.length + draft.uploadsInProgress.length;
} }

Просмотреть файл

@@ -21,6 +21,7 @@ import * as ChannelActions from 'actions/channel_actions.jsx';
import Constants from 'utils/constants.jsx'; import Constants from 'utils/constants.jsx';
const ScrollTypes = Constants.ScrollTypes; const ScrollTypes = Constants.ScrollTypes;
import PostStore from 'stores/post_store.jsx';
import PreferenceStore from 'stores/preference_store.jsx'; import PreferenceStore from 'stores/preference_store.jsx';
import {FormattedDate, FormattedMessage} from 'react-intl'; import {FormattedDate, FormattedMessage} from 'react-intl';
@@ -96,6 +97,11 @@ export default class PostList extends React.Component {
}, 0); }, 0);
} }
this.setState({unViewedCount}); this.setState({unViewedCount});
if (this.props.channelId !== nextProps.channelId) {
PostStore.removePostDraftChangeListener(this.props.channelId, this.handlePostDraftChange);
PostStore.addPostDraftChangeListener(nextProps.channelId, this.handlePostDraftChange);
}
} }
handleKeyDown(e) { handleKeyDown(e) {
@@ -527,6 +533,16 @@ export default class PostList extends React.Component {
window.addEventListener('resize', this.handleResize); window.addEventListener('resize', this.handleResize);
window.addEventListener('keydown', this.handleKeyDown); window.addEventListener('keydown', this.handleKeyDown);
PostStore.addPostDraftChangeListener(this.props.channelId, this.handlePostDraftChange);
}
handlePostDraftChange = (draft) => {
// this.state.draft isn't used anywhere, but this will cause an update to the scroll position
// without causing two updates to trigger when something else changes
this.setState({
draft
});
} }
componentWillUnmount() { componentWillUnmount() {
@@ -534,6 +550,8 @@ export default class PostList extends React.Component {
window.removeEventListener('resize', this.handleResize); window.removeEventListener('resize', this.handleResize);
window.removeEventListener('keydown', this.handleKeyDown); window.removeEventListener('keydown', this.handleKeyDown);
this.scrollStopAction.cancel(); this.scrollStopAction.cancel();
PostStore.removePostDraftChangeListener(this.handlePostDraftChange);
} }
componentDidUpdate() { componentDidUpdate() {
@@ -545,13 +563,6 @@ export default class PostList extends React.Component {
} }
render() { render() {
if (this.props.postList == null) {
return <div/>;
}
const posts = this.props.postList.posts;
const order = this.props.postList.order;
// Create intro message or top loadmore link // Create intro message or top loadmore link
let moreMessagesTop; let moreMessagesTop;
if (this.props.showMoreMessagesTop) { if (this.props.showMoreMessagesTop) {
@@ -588,11 +599,17 @@ export default class PostList extends React.Component {
} }
// Create post elements // Create post elements
const postElements = this.createPosts(posts, order); let postElements = null;
let topPostCreateAt = 0; let topPostCreateAt = 0;
if (this.state.topPostId && this.props.postList.posts[this.state.topPostId]) { if (this.props.postList) {
topPostCreateAt = this.props.postList.posts[this.state.topPostId].create_at; const posts = this.props.postList.posts;
const order = this.props.postList.order;
postElements = this.createPosts(posts, order);
if (this.state.topPostId && this.props.postList.posts[this.state.topPostId]) {
topPostCreateAt = this.props.postList.posts[this.state.topPostId].create_at;
}
} }
return ( return (
@@ -642,6 +659,7 @@ PostList.propTypes = {
postList: PropTypes.object, postList: PropTypes.object,
profiles: PropTypes.object, profiles: PropTypes.object,
channel: PropTypes.object, channel: PropTypes.object,
channelId: PropTypes.string.isRequired,
currentUser: PropTypes.object, currentUser: PropTypes.object,
scrollPostId: PropTypes.string, scrollPostId: PropTypes.string,
scrollType: PropTypes.number, scrollType: PropTypes.number,

Просмотреть файл

@@ -363,6 +363,7 @@ export default class PostViewController extends React.Component {
<PostList <PostList
postList={this.state.postList} postList={this.state.postList}
profiles={this.state.profiles} profiles={this.state.profiles}
channelId={this.state.channel.id}
channel={this.state.channel} channel={this.state.channel}
currentUser={this.state.currentUser} currentUser={this.state.currentUser}
showMoreMessagesTop={!this.state.atTop} showMoreMessagesTop={!this.state.atTop}

Просмотреть файл

@@ -37,12 +37,10 @@ export default class SuggestionBox extends React.Component {
} }
componentDidMount() { componentDidMount() {
SuggestionStore.addCompleteWordListener(this.suggestionId, this.handleCompleteWord);
SuggestionStore.addPretextChangedListener(this.suggestionId, this.handlePretextChanged); SuggestionStore.addPretextChangedListener(this.suggestionId, this.handlePretextChanged);
} }
componentWillUnmount() { componentWillUnmount() {
SuggestionStore.removeCompleteWordListener(this.suggestionId, this.handleCompleteWord);
SuggestionStore.removePretextChangedListener(this.suggestionId, this.handlePretextChanged); SuggestionStore.removePretextChangedListener(this.suggestionId, this.handlePretextChanged);
SuggestionStore.unregisterSuggestionBox(this.suggestionId); SuggestionStore.unregisterSuggestionBox(this.suggestionId);
@@ -161,6 +159,8 @@ export default class SuggestionBox extends React.Component {
provider.handleCompleteWord(term, matchedPretext); provider.handleCompleteWord(term, matchedPretext);
} }
} }
GlobalActions.emitCompleteWordSuggestion(this.suggestionId);
} }
handleKeyDown(e) { handleKeyDown(e) {
@@ -172,7 +172,7 @@ export default class SuggestionBox extends React.Component {
GlobalActions.emitSelectNextSuggestion(this.suggestionId); GlobalActions.emitSelectNextSuggestion(this.suggestionId);
e.preventDefault(); e.preventDefault();
} else if (e.which === KeyCodes.ENTER || e.which === KeyCodes.TAB) { } else if (e.which === KeyCodes.ENTER || e.which === KeyCodes.TAB) {
GlobalActions.emitCompleteWordSuggestion(this.suggestionId); this.handleCompleteWord(SuggestionStore.getSelection(this.suggestionId), SuggestionStore.getSelectedMatchedPretext(this.suggestionId));
this.props.onKeyDown(e); this.props.onKeyDown(e);
e.preventDefault(); e.preventDefault();
} else if (e.which === KeyCodes.ESCAPE) { } else if (e.which === KeyCodes.ESCAPE) {
@@ -212,6 +212,7 @@ export default class SuggestionBox extends React.Component {
// Don't pass props used by SuggestionBox // Don't pass props used by SuggestionBox
Reflect.deleteProperty(props, 'providers'); Reflect.deleteProperty(props, 'providers');
Reflect.deleteProperty(props, 'onChange'); // We use onInput instead of onChange on the actual input
Reflect.deleteProperty(props, 'onItemSelected'); Reflect.deleteProperty(props, 'onItemSelected');
const childProps = { const childProps = {
@@ -260,6 +261,7 @@ export default class SuggestionBox extends React.Component {
suggestionId={this.suggestionId} suggestionId={this.suggestionId}
location={listStyle} location={listStyle}
renderDividers={renderDividers} renderDividers={renderDividers}
onCompleteWord={this.handleCompleteWord}
/> />
</div> </div>
); );

Просмотреть файл

@@ -2,20 +2,19 @@
// See License.txt for license information. // See License.txt for license information.
import $ from 'jquery'; import $ from 'jquery';
import PropTypes from 'prop-types';
import React from 'react';
import ReactDOM from 'react-dom'; import ReactDOM from 'react-dom';
import * as GlobalActions from 'actions/global_actions.jsx';
import SuggestionStore from 'stores/suggestion_store.jsx';
import {FormattedMessage} from 'react-intl'; import {FormattedMessage} from 'react-intl';
import PropTypes from 'prop-types'; import SuggestionStore from 'stores/suggestion_store.jsx';
import React from 'react';
export default class SuggestionList extends React.Component { export default class SuggestionList extends React.Component {
static propTypes = { static propTypes = {
suggestionId: PropTypes.string.isRequired, suggestionId: PropTypes.string.isRequired,
location: PropTypes.string, location: PropTypes.string,
renderDividers: PropTypes.bool renderDividers: PropTypes.bool,
onCompleteWord: PropTypes.func.isRequired
}; };
static defaultProps = { static defaultProps = {
@@ -29,7 +28,6 @@ export default class SuggestionList extends React.Component {
this.getContent = this.getContent.bind(this); this.getContent = this.getContent.bind(this);
this.handleItemClick = this.handleItemClick.bind(this);
this.handleSuggestionsChanged = this.handleSuggestionsChanged.bind(this); this.handleSuggestionsChanged = this.handleSuggestionsChanged.bind(this);
this.scrollToItem = this.scrollToItem.bind(this); this.scrollToItem = this.scrollToItem.bind(this);
@@ -67,10 +65,6 @@ export default class SuggestionList extends React.Component {
return $(ReactDOM.findDOMNode(this.refs.content)); return $(ReactDOM.findDOMNode(this.refs.content));
} }
handleItemClick(term, matchedPretext) {
GlobalActions.emitCompleteWordSuggestion(this.props.suggestionId, term, matchedPretext);
}
handleSuggestionsChanged() { handleSuggestionsChanged() {
this.setState(this.getStateFromStores()); this.setState(this.getStateFromStores());
} }
@@ -145,7 +139,7 @@ export default class SuggestionList extends React.Component {
term={term} term={term}
matchedPretext={this.state.matchedPretext[i]} matchedPretext={this.state.matchedPretext[i]}
isSelection={isSelection} isSelection={isSelection}
onClick={this.handleItemClick} onClick={this.props.onCompleteWord}
/> />
); );
} }

Просмотреть файл

@@ -4,7 +4,6 @@
import AppDispatcher from '../dispatcher/app_dispatcher.jsx'; import AppDispatcher from '../dispatcher/app_dispatcher.jsx';
import EventEmitter from 'events'; import EventEmitter from 'events';
import ChannelStore from 'stores/channel_store.jsx';
import BrowserStore from 'stores/browser_store.jsx'; import BrowserStore from 'stores/browser_store.jsx';
import UserStore from 'stores/user_store.jsx'; import UserStore from 'stores/user_store.jsx';
@@ -17,6 +16,7 @@ const EDIT_POST_EVENT = 'edit_post';
const POSTS_VIEW_JUMP_EVENT = 'post_list_jump'; const POSTS_VIEW_JUMP_EVENT = 'post_list_jump';
const SELECTED_POST_CHANGE_EVENT = 'selected_post_change'; const SELECTED_POST_CHANGE_EVENT = 'selected_post_change';
const POST_PINNED_CHANGE_EVENT = 'post_pinned_change'; const POST_PINNED_CHANGE_EVENT = 'post_pinned_change';
const POST_DRAFT_CHANGE_EVENT = 'post_draft_change';
class PostStoreClass extends EventEmitter { class PostStoreClass extends EventEmitter {
constructor() { constructor() {
@@ -75,6 +75,18 @@ class PostStoreClass extends EventEmitter {
this.removeListener(POSTS_VIEW_JUMP_EVENT, callback); this.removeListener(POSTS_VIEW_JUMP_EVENT, callback);
} }
emitPostDraftChange(channelId) {
this.emit(POST_DRAFT_CHANGE_EVENT + channelId, this.getPostDraft(channelId));
}
addPostDraftChangeListener(channelId, callback) {
this.on(POST_DRAFT_CHANGE_EVENT + channelId, callback);
}
removePostDraftChangeListener(channelId, callback) {
this.removeListener(POST_DRAFT_CHANGE_EVENT + channelId, callback);
}
jumpPostsViewToBottom() { jumpPostsViewToBottom() {
this.emitPostsViewJump(Constants.PostsViewJumpTypes.BOTTOM, null); this.emitPostsViewJump(Constants.PostsViewJumpTypes.BOTTOM, null);
} }
@@ -585,21 +597,11 @@ class PostStoreClass extends EventEmitter {
return draft; return draft;
} }
storeCurrentDraft(draft) { storePostDraft(channelId, draft) {
var channelId = ChannelStore.getCurrentId();
BrowserStore.setGlobalItem('draft_' + channelId, draft); BrowserStore.setGlobalItem('draft_' + channelId, draft);
} }
getCurrentDraft() { getPostDraft(channelId) {
var channelId = ChannelStore.getCurrentId();
return this.getDraft(channelId);
}
storeDraft(channelId, draft) {
BrowserStore.setGlobalItem('draft_' + channelId, draft);
}
getDraft(channelId) {
return this.normalizeDraft(BrowserStore.getGlobalItem('draft_' + channelId)); return this.normalizeDraft(BrowserStore.getGlobalItem('draft_' + channelId));
} }
@@ -700,7 +702,7 @@ PostStore.dispatchToken = AppDispatcher.register((payload) => {
break; break;
case ActionTypes.CREATE_POST: case ActionTypes.CREATE_POST:
PostStore.storePendingPost(action.post); PostStore.storePendingPost(action.post);
PostStore.storeDraft(action.post.channel_id, null); PostStore.storePostDraft(action.post.channel_id, null);
PostStore.jumpPostsViewToBottom(); PostStore.jumpPostsViewToBottom();
break; break;
case ActionTypes.CREATE_COMMENT: case ActionTypes.CREATE_COMMENT:
@@ -723,6 +725,10 @@ PostStore.dispatchToken = AppDispatcher.register((payload) => {
case ActionTypes.RECEIVED_POST_UNPINNED: case ActionTypes.RECEIVED_POST_UNPINNED:
PostStore.emitPostPinnedChange(); PostStore.emitPostPinnedChange();
break; break;
case ActionTypes.POST_DRAFT_CHANGED:
PostStore.storePostDraft(action.channelId, action.draft);
PostStore.emitPostDraftChange(action.channelId);
break;
default: default:
} }
}); });

Просмотреть файл

@@ -92,6 +92,7 @@ export const ActionTypes = keyMirror({
RECEIVED_ADD_MENTION: null, RECEIVED_ADD_MENTION: null,
RECEIVED_POST_PINNED: null, RECEIVED_POST_PINNED: null,
RECEIVED_POST_UNPINNED: null, RECEIVED_POST_UNPINNED: null,
POST_DRAFT_CHANGED: null,
RECEIVED_PROFILES: null, RECEIVED_PROFILES: null,
RECEIVED_PROFILES_IN_TEAM: null, RECEIVED_PROFILES_IN_TEAM: null,