PLT-6215 Major post list refactor (#6501)

* Major post list refactor

* Fix post and thread deletion

* Fix preferences not selecting correctly

* Fix military time displaying

* Fix UP key for editing posts

* Fix ESLint error

* Various fixes and updates per feedback

* Fix for permalink view

* Revert to old scrolling method and various fixes

* Add floating timestamp, new message indicator, scroll arrows

* Update post loading for focus mode and add visibility limit

* Fix pinning posts and a react warning

* Add loading UI updates from Asaad

* Fix refreshing loop

* Temporarily bump post visibility limit

* Update infinite scrolling

* Remove infinite scrolling
Этот коммит содержится в:
Joram Wilander
2017-06-18 14:42:32 -04:00
коммит произвёл GitHub
родитель 0231e95f1c
Коммит ab67f6e257
90 изменённых файлов: 2464 добавлений и 3986 удалений

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

@@ -1,25 +1,24 @@
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import * as AsyncClient from 'utils/async_client.jsx';
import Client from 'client/web_client.jsx';
import store from 'stores/redux_store.jsx';
const dispatch = store.dispatch;
const getState = store.getState;
import {uploadFile as uploadFileRedux} from 'mattermost-redux/actions/files';
export function uploadFile(file, name, channelId, clientId, success, error) {
Client.uploadFile(
file,
name,
channelId,
clientId,
(data) => {
if (success) {
success(data);
}
},
(err) => {
AsyncClient.dispatchError(err, 'uploadFile');
const fileFormData = new FormData();
fileFormData.append('files', file, name);
fileFormData.append('channel_id', channelId);
fileFormData.append('client_ids', clientId);
if (error) {
error(err);
uploadFileRedux(channelId, null, [clientId], fileFormData)(dispatch, getState).then(
(data) => {
if (data && success) {
success(data);
} else if (data == null && error) {
const serverError = getState().requests.files.uploadFiles.error;
error({id: serverError.server_error_id, ...serverError});
}
}
);

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

@@ -4,14 +4,13 @@
import AppDispatcher from 'dispatcher/app_dispatcher.jsx';
import ChannelStore from 'stores/channel_store.jsx';
import PostStore from 'stores/post_store.jsx';
import UserStore from 'stores/user_store.jsx';
import BrowserStore from 'stores/browser_store.jsx';
import ErrorStore from 'stores/error_store.jsx';
import TeamStore from 'stores/team_store.jsx';
import SearchStore from 'stores/search_store.jsx';
import {handleNewPost, loadPosts, loadPostsBefore, loadPostsAfter} from 'actions/post_actions.jsx';
import {handleNewPost} from 'actions/post_actions.jsx';
import {loadProfilesForSidebar} from 'actions/user_actions.jsx';
import {loadChannelsForCurrentUser} from 'actions/channel_actions.jsx';
import {stopPeriodicStatusUpdates} from 'actions/status_actions.jsx';
@@ -59,7 +58,6 @@ export function emitChannelClickEvent(channel) {
getMyChannelMemberPromise.then(() => {
getChannelStats(chan.id)(dispatch, getState);
viewChannel(chan.id, oldChannelId)(dispatch, getState);
loadPosts(chan.id);
// Mark previous and next channel as read
ChannelStore.resetCounts([chan.id, oldChannelId]);
@@ -106,10 +104,15 @@ export function doFocusPost(channelId, postId, data) {
channelId,
post_list: data
});
dispatch({
type: ActionTypes.RECEIVED_FOCUSED_POST,
data: postId,
channelId
});
loadChannelsForCurrentUser();
getChannelStats(channelId)(dispatch, getState);
loadPostsBefore(postId, 0, Constants.POST_FOCUS_CONTEXT_RADIUS, true);
loadPostsAfter(postId, 0, Constants.POST_FOCUS_CONTEXT_RADIUS, true);
}
export function emitPostFocusEvent(postId, onSuccess) {
@@ -148,8 +151,10 @@ export function emitCloseRightHandSide() {
SearchStore.storeSearchResults(null, false, false);
SearchStore.emitSearchChange();
PostStore.storeSelectedPostId(null);
PostStore.emitSelectedPostChange(false, false);
dispatch({
type: ActionTypes.SELECT_POST,
postId: ''
});
}
export function emitPostFocusRightHandSideFromSearch(post, isMentionSearch) {
@@ -188,29 +193,6 @@ export function emitLeaveTeam() {
removeUserFromTeam(TeamStore.getCurrentId(), UserStore.getCurrentId())(dispatch, getState);
}
export function emitLoadMorePostsEvent() {
const id = ChannelStore.getCurrentId();
loadMorePostsTop(id, false);
}
export function emitLoadMorePostsFocusedTopEvent() {
const id = PostStore.getFocusedPostId();
loadMorePostsTop(id, true);
}
export function loadMorePostsTop(id, isFocusPost) {
const earliestPostId = PostStore.getEarliestPostFromPage(id).id;
if (PostStore.requestVisibilityIncrease(id, Constants.POST_CHUNK_SIZE)) {
loadPostsBefore(earliestPostId, 0, Constants.POST_CHUNK_SIZE, isFocusPost);
}
}
export function emitLoadMorePostsFocusedBottomEvent() {
const id = PostStore.getFocusedPostId();
const latestPostId = PostStore.getLatestPost(id).id;
loadPostsAfter(latestPostId, 0, Constants.POST_CHUNK_SIZE, Boolean(id));
}
export function emitUserPostedEvent(post) {
AppDispatcher.handleServerAction({
type: ActionTypes.CREATE_POST,
@@ -225,13 +207,6 @@ export function emitUserCommentedEvent(post) {
});
}
export function emitPostDeletedEvent(post) {
AppDispatcher.handleServerAction({
type: ActionTypes.POST_DELETED,
post
});
}
export function showDeletePostModal(post, commentCount = 0) {
AppDispatcher.handleViewAction({
type: ActionTypes.TOGGLE_DELETE_POST_MODAL,
@@ -421,11 +396,6 @@ export function loadDefaultLocale() {
return newLocalizationSelected(locale);
}
export function viewLoggedIn() {
// Clear pending posts (shouldn't have pending posts if we are loading)
PostStore.clearPendingPosts();
}
let lastTimeTypingSent = 0;
export function emitLocalUserTypingEvent(channelId, parentId) {
const t = Date.now();

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

@@ -4,14 +4,12 @@
import AppDispatcher from 'dispatcher/app_dispatcher.jsx';
import ChannelStore from 'stores/channel_store.jsx';
import PostStore from 'stores/post_store.jsx';
import UserStore from 'stores/user_store.jsx';
import PostStore from 'stores/post_store.jsx';
import {loadStatusesForChannel} from 'actions/status_actions.jsx';
import {loadNewDMIfNeeded, loadNewGMIfNeeded} from 'actions/user_actions.jsx';
import {trackEvent} from 'actions/diagnostics_actions.jsx';
import {sendDesktopNotification} from 'actions/notification_actions.jsx';
import * as GlobalActions from 'actions/global_actions.jsx';
import Client from 'client/web_client.jsx';
import * as AsyncClient from 'utils/async_client.jsx';
@@ -25,7 +23,20 @@ import store from 'stores/redux_store.jsx';
const dispatch = store.dispatch;
const getState = store.getState;
import {getProfilesByIds} from 'mattermost-redux/actions/users';
import {
createPost as createPostRedux,
getPostThread,
editPost,
deletePost as deletePostRedux,
getPosts,
getPostsBefore,
addReaction as addReactionRedux,
removeReaction as removeReactionRedux
} from 'mattermost-redux/actions/posts';
import {getMyChannelMember} from 'mattermost-redux/actions/channels';
import {PostTypes} from 'mattermost-redux/action_types';
import * as Selectors from 'mattermost-redux/selectors/entities/posts';
import {batchActions} from 'redux-batched-actions';
export function handleNewPost(post, msg) {
let websocketMessageProps = {};
@@ -54,19 +65,22 @@ export function handleNewPost(post, msg) {
}
function completePostReceive(post, websocketMessageProps) {
if (post.root_id && PostStore.getPost(post.channel_id, post.root_id) == null) {
Client.getPost(
post.channel_id,
post.root_id,
if (post.root_id && Selectors.getPost(getState(), post.root_id) != null) {
getPostThread(post.root_id)(dispatch, getState).then(
(data) => {
AppDispatcher.handleServerAction({
type: ActionTypes.RECEIVED_POSTS,
id: post.channel_id,
numRequested: 0,
post_list: data
// Need manual dispatch to remove pending post
dispatch({
type: PostTypes.RECEIVED_POSTS,
data: {
order: [],
posts: {
[post.id]: post
}
},
channelId: post.channel_id
});
// Required to update order
// Still needed to update unreads
AppDispatcher.handleServerAction({
type: ActionTypes.RECEIVED_POST,
post,
@@ -74,17 +88,25 @@ function completePostReceive(post, websocketMessageProps) {
});
sendDesktopNotification(post, websocketMessageProps);
loadProfilesForPosts(data.posts);
},
(err) => {
AsyncClient.dispatchError(err, 'getPost');
}
);
return;
}
dispatch({
type: PostTypes.RECEIVED_POSTS,
data: {
order: [],
posts: {
[post.id]: post
}
},
channelId: post.channel_id
});
// Still needed to update unreads
AppDispatcher.handleServerAction({
type: ActionTypes.RECEIVED_POST,
post,
@@ -167,138 +189,6 @@ export function getPinnedPosts(channelId = ChannelStore.getCurrentId()) {
);
}
export function loadPosts(channelId = ChannelStore.getCurrentId(), isPost = false) {
const postList = PostStore.getAllPosts(channelId);
const latestPostTime = PostStore.getLatestPostFromPageTime(channelId);
if (
!postList || Object.keys(postList).length === 0 ||
(!isPost && postList.order.length < Constants.POST_CHUNK_SIZE) ||
latestPostTime === 0
) {
loadPostsPage(channelId, Constants.POST_CHUNK_SIZE, isPost);
return;
}
Client.getPosts(
channelId,
latestPostTime,
(data) => {
AppDispatcher.handleServerAction({
type: ActionTypes.RECEIVED_POSTS,
id: channelId,
before: true,
numRequested: 0,
post_list: data,
isPost
});
loadProfilesForPosts(data.posts);
loadStatusesForChannel(channelId);
},
(err) => {
AsyncClient.dispatchError(err, 'loadPosts');
}
);
}
export function loadPostsPage(channelId = ChannelStore.getCurrentId(), max = Constants.POST_CHUNK_SIZE, isPost = false) {
const postList = PostStore.getAllPosts(channelId);
// if we already have more than POST_CHUNK_SIZE posts,
// let's get the amount we have but rounded up to next multiple of POST_CHUNK_SIZE,
// with a max
let numPosts = Math.min(max, Constants.POST_CHUNK_SIZE);
if (postList && postList.order.length > 0) {
numPosts = Math.min(max, Constants.POST_CHUNK_SIZE * Math.ceil(postList.order.length / Constants.POST_CHUNK_SIZE));
}
Client.getPostsPage(
channelId,
0,
numPosts,
(data) => {
AppDispatcher.handleServerAction({
type: ActionTypes.RECEIVED_POSTS,
id: channelId,
before: true,
numRequested: numPosts,
checkLatest: true,
checkEarliest: true,
post_list: data,
isPost
});
loadProfilesForPosts(data.posts);
loadStatusesForChannel(channelId);
},
(err) => {
AsyncClient.dispatchError(err, 'loadPostsPage');
}
);
}
export function loadPostsBefore(postId, offset, numPost, isPost) {
const channelId = ChannelStore.getCurrentId();
if (channelId == null) {
return;
}
Client.getPostsBefore(
channelId,
postId,
offset,
numPost,
(data) => {
AppDispatcher.handleServerAction({
type: ActionTypes.RECEIVED_POSTS,
id: channelId,
before: true,
checkEarliest: true,
numRequested: numPost,
post_list: data,
isPost
});
loadProfilesForPosts(data.posts);
loadStatusesForChannel(channelId);
},
(err) => {
AsyncClient.dispatchError(err, 'loadPostsBefore');
}
);
}
export function loadPostsAfter(postId, offset, numPost, isPost) {
const channelId = ChannelStore.getCurrentId();
if (channelId == null) {
return;
}
Client.getPostsAfter(
channelId,
postId,
offset,
numPost,
(data) => {
AppDispatcher.handleServerAction({
type: ActionTypes.RECEIVED_POSTS,
id: channelId,
before: false,
numRequested: numPost,
post_list: data,
isPost
});
loadProfilesForPosts(data.posts);
loadStatusesForChannel(channelId);
},
(err) => {
AsyncClient.dispatchError(err, 'loadPostsAfter');
}
);
}
export function loadProfilesForPosts(posts) {
const profilesToLoad = {};
for (const pid in posts) {
@@ -321,124 +211,37 @@ export function loadProfilesForPosts(posts) {
}
export function addReaction(channelId, postId, emojiName) {
const reaction = {
post_id: postId,
user_id: UserStore.getCurrentId(),
emoji_name: emojiName
};
emitEmojiPosted(emojiName);
AsyncClient.saveReaction(channelId, reaction);
addReactionRedux(postId, emojiName)(dispatch, getState);
}
export function removeReaction(channelId, postId, emojiName) {
const reaction = {
post_id: postId,
user_id: UserStore.getCurrentId(),
emoji_name: emojiName
};
AsyncClient.deleteReaction(channelId, reaction);
removeReactionRedux(postId, emojiName)(dispatch, getState);
}
const postQueue = [];
export function createPost(post, files, success) {
createPostRedux(post, files)(dispatch, getState).then(() => {
if (post.root_id) {
PostStore.storeCommentDraft(post.root_id, null);
} else {
PostStore.storeDraft(post.channel_id, null);
}
export function queuePost(post, doLoadPost, success, error) {
postQueue.push(
createPost.bind(
this,
post,
doLoadPost,
(data) => {
if (success) {
success(data);
}
postSendComplete();
},
(err) => {
if (error) {
error(err);
}
postSendComplete();
}
)
);
sendFirstPostInQueue();
if (success) {
success();
}
});
}
// Remove the completed post from the queue and send the next one
function postSendComplete() {
postQueue.shift();
sendNextPostInQueue();
}
// Start sending posts if a new queue has started
function sendFirstPostInQueue() {
if (postQueue.length === 1) {
sendNextPostInQueue();
}
}
// Send the next post in the queue if there is one
function sendNextPostInQueue() {
const nextPostAction = postQueue[0];
if (nextPostAction) {
nextPostAction();
}
}
export function createPost(post, doLoadPost, success, error) {
Client.createPost(post,
export function updatePost(post, success) {
editPost(post)(dispatch, getState).then(
(data) => {
if (doLoadPost) {
loadPosts(post.channel_id);
} else {
PostStore.removePendingPost(post.pending_post_id);
}
AppDispatcher.handleServerAction({
type: ActionTypes.RECEIVED_POST,
post: data
});
if (success) {
success(data);
}
},
(err) => {
if (err.id === 'api.post.create_post.root_id.app_error') {
PostStore.removePendingPost(post.pending_post_id);
} else {
post.state = Constants.POST_FAILED;
PostStore.updatePendingPost(post);
}
if (error) {
error(err);
if (data && success) {
success();
}
}
);
}
export function updatePost(post, success, isPost) {
Client.updatePost(
post,
() => {
loadPosts(post.channel_id, isPost);
if (success) {
success();
}
},
(err) => {
AsyncClient.dispatchError(err, 'updatePost');
});
}
export function emitEmojiPosted(emoji) {
AppDispatcher.handleServerAction({
type: ActionTypes.EMOJI_POSTED,
@@ -446,29 +249,31 @@ export function emitEmojiPosted(emoji) {
});
}
export function deletePost(channelId, post, success, error) {
Client.deletePost(
channelId,
post.id,
export function deletePost(channelId, post, success) {
const {currentUserId} = getState().entities.users;
let hardDelete = false;
if (post.user_id === currentUserId) {
hardDelete = true;
}
deletePostRedux(post, hardDelete)(dispatch, getState).then(
() => {
GlobalActions.emitRemovePost(post);
if (post.id === PostStore.getSelectedPostId()) {
AppDispatcher.handleServerAction({
type: ActionTypes.RECEIVED_POST_SELECTED,
postId: null
if (post.id === getState().views.rhs.selectedPostId) {
dispatch({
type: ActionTypes.SELECT_POST,
postId: ''
});
}
dispatch({
type: PostTypes.REMOVE_POST,
data: post
});
if (success) {
success();
}
},
(err) => {
AsyncClient.dispatchError(err, 'deletePost');
if (error) {
error(err);
}
}
);
}
@@ -500,10 +305,49 @@ export function performSearch(terms, isMentionSearch, success, error) {
);
}
export function storePostDraft(channelId, draft) {
AppDispatcher.handleViewAction({
type: ActionTypes.POST_DRAFT_CHANGED,
channelId,
draft
});
const POST_INCREASE_AMOUNT = Constants.POST_CHUNK_SIZE / 2;
// Returns true if there are more posts to load
export function increasePostVisibility(channelId, focusedPostId) {
return async (doDispatch, doGetState) => {
if (doGetState().views.channel.loadingPosts[channelId]) {
return true;
}
const currentPostVisibility = doGetState().views.channel.postVisibility[channelId];
if (currentPostVisibility >= Constants.MAX_POST_VISIBILITY) {
return true;
}
doDispatch(batchActions([
{
type: ActionTypes.LOADING_POSTS,
data: true,
channelId
},
{
type: ActionTypes.INCREASE_POST_VISIBILITY,
data: channelId,
amount: POST_INCREASE_AMOUNT
}
]));
const page = Math.floor(currentPostVisibility / POST_INCREASE_AMOUNT);
let posts;
if (focusedPostId) {
posts = await getPostsBefore(channelId, focusedPostId, page, POST_INCREASE_AMOUNT)(dispatch, getState);
} else {
posts = await getPosts(channelId, page, POST_INCREASE_AMOUNT)(doDispatch, doGetState);
}
doDispatch({
type: ActionTypes.LOADING_POSTS,
data: false,
channelId
});
return posts.order.length >= POST_INCREASE_AMOUNT;
};
}

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

@@ -5,7 +5,6 @@ import $ from 'jquery';
import UserStore from 'stores/user_store.jsx';
import TeamStore from 'stores/team_store.jsx';
import PostStore from 'stores/post_store.jsx';
import PreferenceStore from 'stores/preference_store.jsx';
import ChannelStore from 'stores/channel_store.jsx';
import BrowserStore from 'stores/browser_store.jsx';
@@ -21,7 +20,7 @@ import * as AsyncClient from 'utils/async_client.jsx';
import {getSiteURL} from 'utils/url.jsx';
import * as GlobalActions from 'actions/global_actions.jsx';
import {handleNewPost, loadPosts, loadProfilesForPosts} from 'actions/post_actions.jsx';
import {handleNewPost, loadProfilesForPosts} from 'actions/post_actions.jsx';
import {loadProfilesForSidebar} from 'actions/user_actions.jsx';
import {loadChannelsForCurrentUser} from 'actions/channel_actions.jsx';
import * as StatusActions from 'actions/status_actions.jsx';
@@ -36,8 +35,9 @@ const dispatch = store.dispatch;
const getState = store.getState;
import {batchActions} from 'redux-batched-actions';
import {viewChannel, getChannelAndMyMember, getChannelStats} from 'mattermost-redux/actions/channels';
import {getPosts} from 'mattermost-redux/actions/posts';
import {setServerVersion} from 'mattermost-redux/actions/general';
import {ChannelTypes, TeamTypes, UserTypes} from 'mattermost-redux/action_types';
import {ChannelTypes, TeamTypes, UserTypes, PostTypes} from 'mattermost-redux/action_types';
const MAX_WEBSOCKET_FAILS = 7;
@@ -97,7 +97,7 @@ export function reconnect(includeWebSocket = true) {
if (Client.teamId) {
loadChannelsForCurrentUser();
loadPosts(ChannelStore.getCurrentId());
getPosts(ChannelStore.getCurrentId())(dispatch, getState);
StatusActions.loadStatusesForChannelAndSidebar();
}
@@ -246,8 +246,7 @@ function handleNewPostEvent(msg) {
function handlePostEditEvent(msg) {
// Store post
const post = JSON.parse(msg.data.post);
PostStore.storePost(post, false);
PostStore.emitChange();
dispatch({type: PostTypes.RECEIVED_POST, data: post});
// Update channel state
if (ChannelStore.getCurrentId() === msg.broadcast.channel_id) {
@@ -259,7 +258,7 @@ function handlePostEditEvent(msg) {
function handlePostDeleteEvent(msg) {
const post = JSON.parse(msg.data.post);
GlobalActions.emitPostDeletedEvent(post);
dispatch({type: PostTypes.POST_DELETED, data: post});
}
function handleTeamAddedEvent(msg) {
@@ -424,19 +423,17 @@ function handleWebrtc(msg) {
function handleReactionAddedEvent(msg) {
const reaction = JSON.parse(msg.data.reaction);
AppDispatcher.handleServerAction({
type: ActionTypes.ADDED_REACTION,
postId: reaction.post_id,
reaction
dispatch({
type: PostTypes.RECEIVED_REACTION,
data: reaction
});
}
function handleReactionRemovedEvent(msg) {
const reaction = JSON.parse(msg.data.reaction);
AppDispatcher.handleServerAction({
type: ActionTypes.REMOVED_REACTION,
postId: reaction.post_id,
reaction
dispatch({
type: PostTypes.REACTION_DELETED,
data: reaction
});
}