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
Этот коммит содержится в:
коммит произвёл
GitHub
родитель
0231e95f1c
Коммит
ab67f6e257
1
.gitignore
поставляемый
1
.gitignore
поставляемый
@@ -6,6 +6,7 @@ node_modules
|
|||||||
/webapp/dist
|
/webapp/dist
|
||||||
jobserver
|
jobserver
|
||||||
npm-debug.log
|
npm-debug.log
|
||||||
|
webapp/yarn-error.log
|
||||||
mattermost.mattermost-license
|
mattermost.mattermost-license
|
||||||
config/mattermost.mattermost-license
|
config/mattermost.mattermost-license
|
||||||
|
|
||||||
|
|||||||
@@ -251,7 +251,6 @@
|
|||||||
"react/self-closing-comp": 2,
|
"react/self-closing-comp": 2,
|
||||||
"react/sort-comp": 0,
|
"react/sort-comp": 0,
|
||||||
"react/style-prop-object": 2,
|
"react/style-prop-object": 2,
|
||||||
"require-await": 2,
|
|
||||||
"require-yield": 2,
|
"require-yield": 2,
|
||||||
"rest-spread-spacing": [2, "never"],
|
"rest-spread-spacing": [2, "never"],
|
||||||
"semi": [2, "always"],
|
"semi": [2, "always"],
|
||||||
|
|||||||
@@ -1,25 +1,24 @@
|
|||||||
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
|
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
|
||||||
// See License.txt for license information.
|
// See License.txt for license information.
|
||||||
|
|
||||||
import * as AsyncClient from 'utils/async_client.jsx';
|
import store from 'stores/redux_store.jsx';
|
||||||
import Client from 'client/web_client.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) {
|
export function uploadFile(file, name, channelId, clientId, success, error) {
|
||||||
Client.uploadFile(
|
const fileFormData = new FormData();
|
||||||
file,
|
fileFormData.append('files', file, name);
|
||||||
name,
|
fileFormData.append('channel_id', channelId);
|
||||||
channelId,
|
fileFormData.append('client_ids', clientId);
|
||||||
clientId,
|
|
||||||
(data) => {
|
|
||||||
if (success) {
|
|
||||||
success(data);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
(err) => {
|
|
||||||
AsyncClient.dispatchError(err, 'uploadFile');
|
|
||||||
|
|
||||||
if (error) {
|
uploadFileRedux(channelId, null, [clientId], fileFormData)(dispatch, getState).then(
|
||||||
error(err);
|
(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 AppDispatcher from 'dispatcher/app_dispatcher.jsx';
|
||||||
|
|
||||||
import ChannelStore from 'stores/channel_store.jsx';
|
import ChannelStore from 'stores/channel_store.jsx';
|
||||||
import PostStore from 'stores/post_store.jsx';
|
|
||||||
import UserStore from 'stores/user_store.jsx';
|
import UserStore from 'stores/user_store.jsx';
|
||||||
import BrowserStore from 'stores/browser_store.jsx';
|
import BrowserStore from 'stores/browser_store.jsx';
|
||||||
import ErrorStore from 'stores/error_store.jsx';
|
import ErrorStore from 'stores/error_store.jsx';
|
||||||
import TeamStore from 'stores/team_store.jsx';
|
import TeamStore from 'stores/team_store.jsx';
|
||||||
import SearchStore from 'stores/search_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 {loadProfilesForSidebar} from 'actions/user_actions.jsx';
|
||||||
import {loadChannelsForCurrentUser} from 'actions/channel_actions.jsx';
|
import {loadChannelsForCurrentUser} from 'actions/channel_actions.jsx';
|
||||||
import {stopPeriodicStatusUpdates} from 'actions/status_actions.jsx';
|
import {stopPeriodicStatusUpdates} from 'actions/status_actions.jsx';
|
||||||
@@ -59,7 +58,6 @@ export function emitChannelClickEvent(channel) {
|
|||||||
getMyChannelMemberPromise.then(() => {
|
getMyChannelMemberPromise.then(() => {
|
||||||
getChannelStats(chan.id)(dispatch, getState);
|
getChannelStats(chan.id)(dispatch, getState);
|
||||||
viewChannel(chan.id, oldChannelId)(dispatch, getState);
|
viewChannel(chan.id, oldChannelId)(dispatch, getState);
|
||||||
loadPosts(chan.id);
|
|
||||||
|
|
||||||
// Mark previous and next channel as read
|
// Mark previous and next channel as read
|
||||||
ChannelStore.resetCounts([chan.id, oldChannelId]);
|
ChannelStore.resetCounts([chan.id, oldChannelId]);
|
||||||
@@ -106,10 +104,15 @@ export function doFocusPost(channelId, postId, data) {
|
|||||||
channelId,
|
channelId,
|
||||||
post_list: data
|
post_list: data
|
||||||
});
|
});
|
||||||
|
|
||||||
|
dispatch({
|
||||||
|
type: ActionTypes.RECEIVED_FOCUSED_POST,
|
||||||
|
data: postId,
|
||||||
|
channelId
|
||||||
|
});
|
||||||
|
|
||||||
loadChannelsForCurrentUser();
|
loadChannelsForCurrentUser();
|
||||||
getChannelStats(channelId)(dispatch, getState);
|
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) {
|
export function emitPostFocusEvent(postId, onSuccess) {
|
||||||
@@ -148,8 +151,10 @@ export function emitCloseRightHandSide() {
|
|||||||
SearchStore.storeSearchResults(null, false, false);
|
SearchStore.storeSearchResults(null, false, false);
|
||||||
SearchStore.emitSearchChange();
|
SearchStore.emitSearchChange();
|
||||||
|
|
||||||
PostStore.storeSelectedPostId(null);
|
dispatch({
|
||||||
PostStore.emitSelectedPostChange(false, false);
|
type: ActionTypes.SELECT_POST,
|
||||||
|
postId: ''
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function emitPostFocusRightHandSideFromSearch(post, isMentionSearch) {
|
export function emitPostFocusRightHandSideFromSearch(post, isMentionSearch) {
|
||||||
@@ -188,29 +193,6 @@ export function emitLeaveTeam() {
|
|||||||
removeUserFromTeam(TeamStore.getCurrentId(), UserStore.getCurrentId())(dispatch, getState);
|
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) {
|
export function emitUserPostedEvent(post) {
|
||||||
AppDispatcher.handleServerAction({
|
AppDispatcher.handleServerAction({
|
||||||
type: ActionTypes.CREATE_POST,
|
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) {
|
export function showDeletePostModal(post, commentCount = 0) {
|
||||||
AppDispatcher.handleViewAction({
|
AppDispatcher.handleViewAction({
|
||||||
type: ActionTypes.TOGGLE_DELETE_POST_MODAL,
|
type: ActionTypes.TOGGLE_DELETE_POST_MODAL,
|
||||||
@@ -421,11 +396,6 @@ export function loadDefaultLocale() {
|
|||||||
return newLocalizationSelected(locale);
|
return newLocalizationSelected(locale);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function viewLoggedIn() {
|
|
||||||
// Clear pending posts (shouldn't have pending posts if we are loading)
|
|
||||||
PostStore.clearPendingPosts();
|
|
||||||
}
|
|
||||||
|
|
||||||
let lastTimeTypingSent = 0;
|
let lastTimeTypingSent = 0;
|
||||||
export function emitLocalUserTypingEvent(channelId, parentId) {
|
export function emitLocalUserTypingEvent(channelId, parentId) {
|
||||||
const t = Date.now();
|
const t = Date.now();
|
||||||
|
|||||||
@@ -4,14 +4,12 @@
|
|||||||
import AppDispatcher from 'dispatcher/app_dispatcher.jsx';
|
import AppDispatcher from 'dispatcher/app_dispatcher.jsx';
|
||||||
|
|
||||||
import ChannelStore from 'stores/channel_store.jsx';
|
import ChannelStore from 'stores/channel_store.jsx';
|
||||||
import PostStore from 'stores/post_store.jsx';
|
|
||||||
import UserStore from 'stores/user_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 {loadNewDMIfNeeded, loadNewGMIfNeeded} from 'actions/user_actions.jsx';
|
||||||
import {trackEvent} from 'actions/diagnostics_actions.jsx';
|
import {trackEvent} from 'actions/diagnostics_actions.jsx';
|
||||||
import {sendDesktopNotification} from 'actions/notification_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 Client from 'client/web_client.jsx';
|
||||||
import * as AsyncClient from 'utils/async_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 dispatch = store.dispatch;
|
||||||
const getState = store.getState;
|
const getState = store.getState;
|
||||||
import {getProfilesByIds} from 'mattermost-redux/actions/users';
|
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 {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) {
|
export function handleNewPost(post, msg) {
|
||||||
let websocketMessageProps = {};
|
let websocketMessageProps = {};
|
||||||
@@ -54,19 +65,22 @@ export function handleNewPost(post, msg) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function completePostReceive(post, websocketMessageProps) {
|
function completePostReceive(post, websocketMessageProps) {
|
||||||
if (post.root_id && PostStore.getPost(post.channel_id, post.root_id) == null) {
|
if (post.root_id && Selectors.getPost(getState(), post.root_id) != null) {
|
||||||
Client.getPost(
|
getPostThread(post.root_id)(dispatch, getState).then(
|
||||||
post.channel_id,
|
|
||||||
post.root_id,
|
|
||||||
(data) => {
|
(data) => {
|
||||||
AppDispatcher.handleServerAction({
|
// Need manual dispatch to remove pending post
|
||||||
type: ActionTypes.RECEIVED_POSTS,
|
dispatch({
|
||||||
id: post.channel_id,
|
type: PostTypes.RECEIVED_POSTS,
|
||||||
numRequested: 0,
|
data: {
|
||||||
post_list: data
|
order: [],
|
||||||
|
posts: {
|
||||||
|
[post.id]: post
|
||||||
|
}
|
||||||
|
},
|
||||||
|
channelId: post.channel_id
|
||||||
});
|
});
|
||||||
|
|
||||||
// Required to update order
|
// Still needed to update unreads
|
||||||
AppDispatcher.handleServerAction({
|
AppDispatcher.handleServerAction({
|
||||||
type: ActionTypes.RECEIVED_POST,
|
type: ActionTypes.RECEIVED_POST,
|
||||||
post,
|
post,
|
||||||
@@ -74,17 +88,25 @@ function completePostReceive(post, websocketMessageProps) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
sendDesktopNotification(post, websocketMessageProps);
|
sendDesktopNotification(post, websocketMessageProps);
|
||||||
|
|
||||||
loadProfilesForPosts(data.posts);
|
loadProfilesForPosts(data.posts);
|
||||||
},
|
|
||||||
(err) => {
|
|
||||||
AsyncClient.dispatchError(err, 'getPost');
|
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
dispatch({
|
||||||
|
type: PostTypes.RECEIVED_POSTS,
|
||||||
|
data: {
|
||||||
|
order: [],
|
||||||
|
posts: {
|
||||||
|
[post.id]: post
|
||||||
|
}
|
||||||
|
},
|
||||||
|
channelId: post.channel_id
|
||||||
|
});
|
||||||
|
|
||||||
|
// Still needed to update unreads
|
||||||
AppDispatcher.handleServerAction({
|
AppDispatcher.handleServerAction({
|
||||||
type: ActionTypes.RECEIVED_POST,
|
type: ActionTypes.RECEIVED_POST,
|
||||||
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) {
|
export function loadProfilesForPosts(posts) {
|
||||||
const profilesToLoad = {};
|
const profilesToLoad = {};
|
||||||
for (const pid in posts) {
|
for (const pid in posts) {
|
||||||
@@ -321,124 +211,37 @@ export function loadProfilesForPosts(posts) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function addReaction(channelId, postId, emojiName) {
|
export function addReaction(channelId, postId, emojiName) {
|
||||||
const reaction = {
|
addReactionRedux(postId, emojiName)(dispatch, getState);
|
||||||
post_id: postId,
|
|
||||||
user_id: UserStore.getCurrentId(),
|
|
||||||
emoji_name: emojiName
|
|
||||||
};
|
|
||||||
emitEmojiPosted(emojiName);
|
|
||||||
|
|
||||||
AsyncClient.saveReaction(channelId, reaction);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function removeReaction(channelId, postId, emojiName) {
|
export function removeReaction(channelId, postId, emojiName) {
|
||||||
const reaction = {
|
removeReactionRedux(postId, emojiName)(dispatch, getState);
|
||||||
post_id: postId,
|
|
||||||
user_id: UserStore.getCurrentId(),
|
|
||||||
emoji_name: emojiName
|
|
||||||
};
|
|
||||||
|
|
||||||
AsyncClient.deleteReaction(channelId, reaction);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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) {
|
if (success) {
|
||||||
postQueue.push(
|
success();
|
||||||
createPost.bind(
|
}
|
||||||
this,
|
});
|
||||||
post,
|
|
||||||
doLoadPost,
|
|
||||||
(data) => {
|
|
||||||
if (success) {
|
|
||||||
success(data);
|
|
||||||
}
|
|
||||||
|
|
||||||
postSendComplete();
|
|
||||||
},
|
|
||||||
(err) => {
|
|
||||||
if (error) {
|
|
||||||
error(err);
|
|
||||||
}
|
|
||||||
|
|
||||||
postSendComplete();
|
|
||||||
}
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
sendFirstPostInQueue();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remove the completed post from the queue and send the next one
|
export function updatePost(post, success) {
|
||||||
function postSendComplete() {
|
editPost(post)(dispatch, getState).then(
|
||||||
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,
|
|
||||||
(data) => {
|
(data) => {
|
||||||
if (doLoadPost) {
|
if (data && success) {
|
||||||
loadPosts(post.channel_id);
|
success();
|
||||||
} 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);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
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) {
|
export function emitEmojiPosted(emoji) {
|
||||||
AppDispatcher.handleServerAction({
|
AppDispatcher.handleServerAction({
|
||||||
type: ActionTypes.EMOJI_POSTED,
|
type: ActionTypes.EMOJI_POSTED,
|
||||||
@@ -446,29 +249,31 @@ export function emitEmojiPosted(emoji) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function deletePost(channelId, post, success, error) {
|
export function deletePost(channelId, post, success) {
|
||||||
Client.deletePost(
|
const {currentUserId} = getState().entities.users;
|
||||||
channelId,
|
|
||||||
post.id,
|
let hardDelete = false;
|
||||||
|
if (post.user_id === currentUserId) {
|
||||||
|
hardDelete = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
deletePostRedux(post, hardDelete)(dispatch, getState).then(
|
||||||
() => {
|
() => {
|
||||||
GlobalActions.emitRemovePost(post);
|
if (post.id === getState().views.rhs.selectedPostId) {
|
||||||
if (post.id === PostStore.getSelectedPostId()) {
|
dispatch({
|
||||||
AppDispatcher.handleServerAction({
|
type: ActionTypes.SELECT_POST,
|
||||||
type: ActionTypes.RECEIVED_POST_SELECTED,
|
postId: ''
|
||||||
postId: null
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
dispatch({
|
||||||
|
type: PostTypes.REMOVE_POST,
|
||||||
|
data: post
|
||||||
|
});
|
||||||
|
|
||||||
if (success) {
|
if (success) {
|
||||||
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) {
|
const POST_INCREASE_AMOUNT = Constants.POST_CHUNK_SIZE / 2;
|
||||||
AppDispatcher.handleViewAction({
|
|
||||||
type: ActionTypes.POST_DRAFT_CHANGED,
|
// Returns true if there are more posts to load
|
||||||
channelId,
|
export function increasePostVisibility(channelId, focusedPostId) {
|
||||||
draft
|
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 UserStore from 'stores/user_store.jsx';
|
||||||
import TeamStore from 'stores/team_store.jsx';
|
import TeamStore from 'stores/team_store.jsx';
|
||||||
import PostStore from 'stores/post_store.jsx';
|
|
||||||
import PreferenceStore from 'stores/preference_store.jsx';
|
import PreferenceStore from 'stores/preference_store.jsx';
|
||||||
import ChannelStore from 'stores/channel_store.jsx';
|
import ChannelStore from 'stores/channel_store.jsx';
|
||||||
import BrowserStore from 'stores/browser_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 {getSiteURL} from 'utils/url.jsx';
|
||||||
|
|
||||||
import * as GlobalActions from 'actions/global_actions.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 {loadProfilesForSidebar} from 'actions/user_actions.jsx';
|
||||||
import {loadChannelsForCurrentUser} from 'actions/channel_actions.jsx';
|
import {loadChannelsForCurrentUser} from 'actions/channel_actions.jsx';
|
||||||
import * as StatusActions from 'actions/status_actions.jsx';
|
import * as StatusActions from 'actions/status_actions.jsx';
|
||||||
@@ -36,8 +35,9 @@ const dispatch = store.dispatch;
|
|||||||
const getState = store.getState;
|
const getState = store.getState;
|
||||||
import {batchActions} from 'redux-batched-actions';
|
import {batchActions} from 'redux-batched-actions';
|
||||||
import {viewChannel, getChannelAndMyMember, getChannelStats} from 'mattermost-redux/actions/channels';
|
import {viewChannel, getChannelAndMyMember, getChannelStats} from 'mattermost-redux/actions/channels';
|
||||||
|
import {getPosts} from 'mattermost-redux/actions/posts';
|
||||||
import {setServerVersion} from 'mattermost-redux/actions/general';
|
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;
|
const MAX_WEBSOCKET_FAILS = 7;
|
||||||
|
|
||||||
@@ -97,7 +97,7 @@ export function reconnect(includeWebSocket = true) {
|
|||||||
|
|
||||||
if (Client.teamId) {
|
if (Client.teamId) {
|
||||||
loadChannelsForCurrentUser();
|
loadChannelsForCurrentUser();
|
||||||
loadPosts(ChannelStore.getCurrentId());
|
getPosts(ChannelStore.getCurrentId())(dispatch, getState);
|
||||||
StatusActions.loadStatusesForChannelAndSidebar();
|
StatusActions.loadStatusesForChannelAndSidebar();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -246,8 +246,7 @@ function handleNewPostEvent(msg) {
|
|||||||
function handlePostEditEvent(msg) {
|
function handlePostEditEvent(msg) {
|
||||||
// Store post
|
// Store post
|
||||||
const post = JSON.parse(msg.data.post);
|
const post = JSON.parse(msg.data.post);
|
||||||
PostStore.storePost(post, false);
|
dispatch({type: PostTypes.RECEIVED_POST, data: post});
|
||||||
PostStore.emitChange();
|
|
||||||
|
|
||||||
// Update channel state
|
// Update channel state
|
||||||
if (ChannelStore.getCurrentId() === msg.broadcast.channel_id) {
|
if (ChannelStore.getCurrentId() === msg.broadcast.channel_id) {
|
||||||
@@ -259,7 +258,7 @@ function handlePostEditEvent(msg) {
|
|||||||
|
|
||||||
function handlePostDeleteEvent(msg) {
|
function handlePostDeleteEvent(msg) {
|
||||||
const post = JSON.parse(msg.data.post);
|
const post = JSON.parse(msg.data.post);
|
||||||
GlobalActions.emitPostDeletedEvent(post);
|
dispatch({type: PostTypes.POST_DELETED, data: post});
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleTeamAddedEvent(msg) {
|
function handleTeamAddedEvent(msg) {
|
||||||
@@ -424,19 +423,17 @@ function handleWebrtc(msg) {
|
|||||||
function handleReactionAddedEvent(msg) {
|
function handleReactionAddedEvent(msg) {
|
||||||
const reaction = JSON.parse(msg.data.reaction);
|
const reaction = JSON.parse(msg.data.reaction);
|
||||||
|
|
||||||
AppDispatcher.handleServerAction({
|
dispatch({
|
||||||
type: ActionTypes.ADDED_REACTION,
|
type: PostTypes.RECEIVED_REACTION,
|
||||||
postId: reaction.post_id,
|
data: reaction
|
||||||
reaction
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleReactionRemovedEvent(msg) {
|
function handleReactionRemovedEvent(msg) {
|
||||||
const reaction = JSON.parse(msg.data.reaction);
|
const reaction = JSON.parse(msg.data.reaction);
|
||||||
|
|
||||||
AppDispatcher.handleServerAction({
|
dispatch({
|
||||||
type: ActionTypes.REMOVED_REACTION,
|
type: PostTypes.REACTION_DELETED,
|
||||||
postId: reaction.post_id,
|
data: reaction
|
||||||
reaction
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -137,6 +137,17 @@ class WebClientClass extends Client {
|
|||||||
return success(res.body);
|
return success(res.body);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
uploadFileV4(file, filename, channelId, clientId, success, error) {
|
||||||
|
return request.
|
||||||
|
post(`${this.url}/api/v4/files`).
|
||||||
|
set(this.defaultHeaders).
|
||||||
|
attach('files', file, filename).
|
||||||
|
field('channel_id', channelId).
|
||||||
|
field('client_ids', clientId).
|
||||||
|
accept('application/json').
|
||||||
|
end(this.handleResponse.bind(this, 'uploadFile', success, error));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var WebClient = new WebClientClass();
|
var WebClient = new WebClientClass();
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import * as UserAgent from 'utils/user_agent.jsx';
|
|||||||
import ChannelHeader from 'components/channel_header.jsx';
|
import ChannelHeader from 'components/channel_header.jsx';
|
||||||
import FileUploadOverlay from 'components/file_upload_overlay.jsx';
|
import FileUploadOverlay from 'components/file_upload_overlay.jsx';
|
||||||
import CreatePost from 'components/create_post.jsx';
|
import CreatePost from 'components/create_post.jsx';
|
||||||
import PostViewCache from 'components/post_view';
|
import PostView from 'components/post_view';
|
||||||
|
|
||||||
import ChannelStore from 'stores/channel_store.jsx';
|
import ChannelStore from 'stores/channel_store.jsx';
|
||||||
|
|
||||||
@@ -77,7 +77,9 @@ export default class ChannelView extends React.Component {
|
|||||||
<ChannelHeader
|
<ChannelHeader
|
||||||
channelId={this.state.channelId}
|
channelId={this.state.channelId}
|
||||||
/>
|
/>
|
||||||
<PostViewCache/>
|
<PostView
|
||||||
|
channelId={this.state.channelId}
|
||||||
|
/>
|
||||||
<div
|
<div
|
||||||
className='post-create__container'
|
className='post-create__container'
|
||||||
id='post-create'
|
id='post-create'
|
||||||
|
|||||||
@@ -229,7 +229,6 @@ export default class CreateComment extends React.Component {
|
|||||||
post.channel_id = this.props.channelId;
|
post.channel_id = this.props.channelId;
|
||||||
post.root_id = this.props.rootId;
|
post.root_id = this.props.rootId;
|
||||||
post.parent_id = this.props.rootId;
|
post.parent_id = this.props.rootId;
|
||||||
post.file_ids = this.state.fileInfos.map((info) => info.id);
|
|
||||||
post.pending_post_id = `${userId}:${time}`;
|
post.pending_post_id = `${userId}:${time}`;
|
||||||
post.user_id = userId;
|
post.user_id = userId;
|
||||||
post.create_at = time;
|
post.create_at = time;
|
||||||
@@ -244,7 +243,7 @@ export default class CreateComment extends React.Component {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
PostActions.queuePost(post, false, null,
|
PostActions.createPost(post, this.state.fileInfos, null,
|
||||||
(err) => {
|
(err) => {
|
||||||
if (err.id === 'api.post.create_post.root_id.app_error') {
|
if (err.id === 'api.post.create_post.root_id.app_error') {
|
||||||
this.showPostDeletedModal();
|
this.showPostDeletedModal();
|
||||||
|
|||||||
@@ -77,7 +77,7 @@ export default class CreatePost extends React.Component {
|
|||||||
PostStore.clearDraftUploads();
|
PostStore.clearDraftUploads();
|
||||||
|
|
||||||
const channelId = ChannelStore.getCurrentId();
|
const channelId = ChannelStore.getCurrentId();
|
||||||
const draft = PostStore.getPostDraft(channelId);
|
const draft = PostStore.getDraft(channelId);
|
||||||
|
|
||||||
const stats = ChannelStore.getCurrentStats();
|
const stats = ChannelStore.getCurrentStats();
|
||||||
const members = stats.member_count - 1;
|
const members = stats.member_count - 1;
|
||||||
@@ -141,7 +141,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) {
|
||||||
PostActions.storePostDraft(this.state.channelId, null);
|
PostStore.storeDraft(this.state.channelId, null);
|
||||||
this.setState({message: '', postError: null, fileInfos: [], enableSendButton: false});
|
this.setState({message: '', postError: null, fileInfos: [], enableSendButton: false});
|
||||||
|
|
||||||
const args = {};
|
const args = {};
|
||||||
@@ -228,7 +228,6 @@ export default class CreatePost extends React.Component {
|
|||||||
|
|
||||||
sendMessage(post) {
|
sendMessage(post) {
|
||||||
post.channel_id = this.state.channelId;
|
post.channel_id = this.state.channelId;
|
||||||
post.file_ids = this.state.fileInfos.map((info) => info.id);
|
|
||||||
|
|
||||||
const time = Utils.getTimestamp();
|
const time = Utils.getTimestamp();
|
||||||
const userId = UserStore.getCurrentId();
|
const userId = UserStore.getCurrentId();
|
||||||
@@ -247,7 +246,7 @@ export default class CreatePost extends React.Component {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
PostActions.queuePost(post, false, null,
|
PostActions.createPost(post, this.state.fileInfos, null,
|
||||||
(err) => {
|
(err) => {
|
||||||
if (err.id === 'api.post.create_post.root_id.app_error') {
|
if (err.id === 'api.post.create_post.root_id.app_error') {
|
||||||
// this should never actually happen since you can't reply from this textbox
|
// this should never actually happen since you can't reply from this textbox
|
||||||
@@ -267,7 +266,7 @@ export default class CreatePost extends React.Component {
|
|||||||
const action = isReaction[1];
|
const action = isReaction[1];
|
||||||
|
|
||||||
const emojiName = isReaction[2];
|
const emojiName = isReaction[2];
|
||||||
const postId = PostStore.getLatestNonEphemeralPost(this.state.channelId).id;
|
const postId = PostStore.getLatestPostId(this.state.channelId);
|
||||||
|
|
||||||
if (postId && action === '+') {
|
if (postId && action === '+') {
|
||||||
PostActions.addReaction(this.state.channelId, postId, emojiName);
|
PostActions.addReaction(this.state.channelId, postId, emojiName);
|
||||||
@@ -275,7 +274,7 @@ export default class CreatePost extends React.Component {
|
|||||||
PostActions.removeReaction(this.state.channelId, postId, emojiName);
|
PostActions.removeReaction(this.state.channelId, postId, emojiName);
|
||||||
}
|
}
|
||||||
|
|
||||||
PostActions.storePostDraft(this.state.channelId, null);
|
PostStore.storeDraft(this.state.channelId, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
focusTextbox(keepFocus = false) {
|
focusTextbox(keepFocus = false) {
|
||||||
@@ -305,9 +304,9 @@ export default class CreatePost extends React.Component {
|
|||||||
enableSendButton
|
enableSendButton
|
||||||
});
|
});
|
||||||
|
|
||||||
const draft = PostStore.getPostDraft(this.state.channelId);
|
const draft = PostStore.getDraft(this.state.channelId);
|
||||||
draft.message = message;
|
draft.message = message;
|
||||||
PostActions.storePostDraft(this.state.channelId, draft);
|
PostStore.storeDraft(this.state.channelId, draft);
|
||||||
}
|
}
|
||||||
|
|
||||||
handleFileUploadChange() {
|
handleFileUploadChange() {
|
||||||
@@ -315,10 +314,10 @@ export default class CreatePost extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
handleUploadStart(clientIds, channelId) {
|
handleUploadStart(clientIds, channelId) {
|
||||||
const draft = PostStore.getPostDraft(channelId);
|
const draft = PostStore.getDraft(channelId);
|
||||||
|
|
||||||
draft.uploadsInProgress = draft.uploadsInProgress.concat(clientIds);
|
draft.uploadsInProgress = draft.uploadsInProgress.concat(clientIds);
|
||||||
PostActions.storePostDraft(channelId, draft);
|
PostStore.storeDraft(channelId, draft);
|
||||||
|
|
||||||
this.setState({uploadsInProgress: draft.uploadsInProgress});
|
this.setState({uploadsInProgress: draft.uploadsInProgress});
|
||||||
|
|
||||||
@@ -328,7 +327,7 @@ export default class CreatePost extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
handleFileUploadComplete(fileInfos, clientIds, channelId) {
|
handleFileUploadComplete(fileInfos, clientIds, channelId) {
|
||||||
const draft = PostStore.getPostDraft(channelId);
|
const draft = PostStore.getDraft(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++) {
|
||||||
@@ -340,7 +339,7 @@ export default class CreatePost extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
draft.fileInfos = draft.fileInfos.concat(fileInfos);
|
draft.fileInfos = draft.fileInfos.concat(fileInfos);
|
||||||
PostActions.storePostDraft(channelId, draft);
|
PostStore.storeDraft(channelId, draft);
|
||||||
|
|
||||||
if (channelId === this.state.channelId) {
|
if (channelId === this.state.channelId) {
|
||||||
this.setState({
|
this.setState({
|
||||||
@@ -359,14 +358,14 @@ export default class CreatePost extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (clientId !== -1) {
|
if (clientId !== -1) {
|
||||||
const draft = PostStore.getPostDraft(channelId);
|
const draft = PostStore.getDraft(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);
|
||||||
}
|
}
|
||||||
|
|
||||||
PostActions.storePostDraft(channelId, draft);
|
PostStore.storeDraft(channelId, draft);
|
||||||
|
|
||||||
if (channelId === this.state.channelId) {
|
if (channelId === this.state.channelId) {
|
||||||
this.setState({uploadsInProgress: draft.uploadsInProgress});
|
this.setState({uploadsInProgress: draft.uploadsInProgress});
|
||||||
@@ -396,10 +395,10 @@ export default class CreatePost extends React.Component {
|
|||||||
fileInfos.splice(index, 1);
|
fileInfos.splice(index, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
const draft = PostStore.getPostDraft(this.state.channelId);
|
const draft = PostStore.getDraft(this.state.channelId);
|
||||||
draft.fileInfos = fileInfos;
|
draft.fileInfos = fileInfos;
|
||||||
draft.uploadsInProgress = uploadsInProgress;
|
draft.uploadsInProgress = uploadsInProgress;
|
||||||
PostActions.storePostDraft(this.state.channelId, draft);
|
PostStore.storeDraft(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});
|
||||||
@@ -462,7 +461,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.getPostDraft(channelId);
|
const draft = PostStore.getDraft(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});
|
||||||
}
|
}
|
||||||
@@ -483,7 +482,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.getPostDraft(channelId);
|
const draft = PostStore.getDraft(channelId);
|
||||||
return draft.fileInfos.length + draft.uploadsInProgress.length;
|
return draft.fileInfos.length + draft.uploadsInProgress.length;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -22,7 +22,30 @@ export default class DotMenu extends Component {
|
|||||||
commentCount: PropTypes.number,
|
commentCount: PropTypes.number,
|
||||||
isFlagged: PropTypes.bool,
|
isFlagged: PropTypes.bool,
|
||||||
handleCommentClick: PropTypes.func,
|
handleCommentClick: PropTypes.func,
|
||||||
handleDropdownOpened: PropTypes.func
|
handleDropdownOpened: PropTypes.func,
|
||||||
|
|
||||||
|
actions: PropTypes.shape({
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Function flag the post
|
||||||
|
*/
|
||||||
|
flagPost: PropTypes.func.isRequired,
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Function to unflag the post
|
||||||
|
*/
|
||||||
|
unflagPost: PropTypes.func.isRequired,
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Function to pin the post
|
||||||
|
*/
|
||||||
|
pinPost: PropTypes.func.isRequired,
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Function to unpin the post
|
||||||
|
*/
|
||||||
|
unpinPost: PropTypes.func.isRequired
|
||||||
|
}).isRequired
|
||||||
}
|
}
|
||||||
|
|
||||||
static defaultProps = {
|
static defaultProps = {
|
||||||
@@ -90,6 +113,10 @@ export default class DotMenu extends Component {
|
|||||||
idCount={this.props.idCount}
|
idCount={this.props.idCount}
|
||||||
postId={this.props.post.id}
|
postId={this.props.post.id}
|
||||||
isFlagged={this.props.isFlagged}
|
isFlagged={this.props.isFlagged}
|
||||||
|
actions={{
|
||||||
|
flagPost: this.props.actions.flagPost,
|
||||||
|
unflagPost: this.props.actions.unflagPost
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -121,6 +148,10 @@ export default class DotMenu extends Component {
|
|||||||
idPrefix={idPrefix + 'Pin'}
|
idPrefix={idPrefix + 'Pin'}
|
||||||
idCount={this.props.idCount}
|
idCount={this.props.idCount}
|
||||||
post={this.props.post}
|
post={this.props.post}
|
||||||
|
actions={{
|
||||||
|
pinPost: this.props.actions.pinPost,
|
||||||
|
unpinPost: this.props.actions.unpinPost
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import React from 'react';
|
|||||||
import {FormattedMessage} from 'react-intl';
|
import {FormattedMessage} from 'react-intl';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
import {flagPost, unflagPost} from 'actions/post_actions.jsx';
|
|
||||||
import * as Utils from 'utils/utils.jsx';
|
import * as Utils from 'utils/utils.jsx';
|
||||||
import Constants from 'utils/constants.jsx';
|
import Constants from 'utils/constants.jsx';
|
||||||
|
|
||||||
@@ -21,12 +20,12 @@ function formatMessage(isFlagged) {
|
|||||||
export default function DotMenuFlag(props) {
|
export default function DotMenuFlag(props) {
|
||||||
function onFlagPost(e) {
|
function onFlagPost(e) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
flagPost(props.postId);
|
props.actions.flagPost(props.postId);
|
||||||
}
|
}
|
||||||
|
|
||||||
function onUnflagPost(e) {
|
function onUnflagPost(e) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
unflagPost(props.postId);
|
props.actions.unflagPost(props.postId);
|
||||||
}
|
}
|
||||||
|
|
||||||
const flagFunc = props.isFlagged ? onUnflagPost : onFlagPost;
|
const flagFunc = props.isFlagged ? onUnflagPost : onFlagPost;
|
||||||
@@ -60,7 +59,21 @@ DotMenuFlag.propTypes = {
|
|||||||
idCount: PropTypes.number,
|
idCount: PropTypes.number,
|
||||||
idPrefix: PropTypes.string.isRequired,
|
idPrefix: PropTypes.string.isRequired,
|
||||||
postId: PropTypes.string.isRequired,
|
postId: PropTypes.string.isRequired,
|
||||||
isFlagged: PropTypes.bool.isRequired
|
isFlagged: PropTypes.bool.isRequired,
|
||||||
|
|
||||||
|
actions: PropTypes.shape({
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Function flag the post
|
||||||
|
*/
|
||||||
|
flagPost: PropTypes.func.isRequired,
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Function to unflag the post
|
||||||
|
*/
|
||||||
|
unflagPost: PropTypes.func.isRequired
|
||||||
|
|
||||||
|
}).isRequired
|
||||||
};
|
};
|
||||||
|
|
||||||
DotMenuFlag.defaultProps = {
|
DotMenuFlag.defaultProps = {
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import React from 'react';
|
|||||||
import {FormattedMessage} from 'react-intl';
|
import {FormattedMessage} from 'react-intl';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
import {unpinPost, pinPost} from 'actions/post_actions.jsx';
|
|
||||||
import {showGetPostLinkModal, showDeletePostModal} from 'actions/global_actions.jsx';
|
import {showGetPostLinkModal, showDeletePostModal} from 'actions/global_actions.jsx';
|
||||||
import * as Utils from 'utils/utils.jsx';
|
import * as Utils from 'utils/utils.jsx';
|
||||||
import Constants from 'utils/constants.jsx';
|
import Constants from 'utils/constants.jsx';
|
||||||
@@ -18,12 +17,12 @@ export default function DotMenuItem(props) {
|
|||||||
|
|
||||||
function handleUnpinPost(e) {
|
function handleUnpinPost(e) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
unpinPost(props.post.channel_id, props.post.id);
|
props.actions.unpinPost(props.post.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
function handlePinPost(e) {
|
function handlePinPost(e) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
pinPost(props.post.channel_id, props.post.id);
|
props.actions.pinPost(props.post.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleDeletePost(e) {
|
function handleDeletePost(e) {
|
||||||
@@ -98,7 +97,20 @@ DotMenuItem.propTypes = {
|
|||||||
post: PropTypes.object,
|
post: PropTypes.object,
|
||||||
handleOnClick: PropTypes.func,
|
handleOnClick: PropTypes.func,
|
||||||
type: PropTypes.string,
|
type: PropTypes.string,
|
||||||
commentCount: PropTypes.number
|
commentCount: PropTypes.number,
|
||||||
|
|
||||||
|
actions: PropTypes.shape({
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Function to pin the post
|
||||||
|
*/
|
||||||
|
pinPost: PropTypes.func,
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Function to unpin the post
|
||||||
|
*/
|
||||||
|
unpinPost: PropTypes.func
|
||||||
|
})
|
||||||
};
|
};
|
||||||
|
|
||||||
DotMenuItem.defaultProps = {
|
DotMenuItem.defaultProps = {
|
||||||
|
|||||||
26
webapp/components/dot_menu/index.js
Обычный файл
26
webapp/components/dot_menu/index.js
Обычный файл
@@ -0,0 +1,26 @@
|
|||||||
|
// Copyright (c) 2017 Mattermost, Inc. All Rights Reserved.
|
||||||
|
// See License.txt for license information.
|
||||||
|
|
||||||
|
import {connect} from 'react-redux';
|
||||||
|
import {bindActionCreators} from 'redux';
|
||||||
|
import {flagPost, unflagPost, pinPost, unpinPost} from 'mattermost-redux/actions/posts';
|
||||||
|
|
||||||
|
import DotMenu from './dot_menu.jsx';
|
||||||
|
|
||||||
|
function mapStateToProps(state, ownProps) {
|
||||||
|
return ownProps;
|
||||||
|
}
|
||||||
|
|
||||||
|
function mapDispatchToProps(dispatch) {
|
||||||
|
return {
|
||||||
|
actions: bindActionCreators({
|
||||||
|
flagPost,
|
||||||
|
unflagPost,
|
||||||
|
pinPost,
|
||||||
|
unpinPost
|
||||||
|
}, dispatch)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export default connect(mapStateToProps, mapDispatchToProps)(DotMenu);
|
||||||
|
|
||||||
@@ -21,6 +21,11 @@ import React from 'react';
|
|||||||
import ReactDOM from 'react-dom';
|
import ReactDOM from 'react-dom';
|
||||||
import {FormattedMessage} from 'react-intl';
|
import {FormattedMessage} from 'react-intl';
|
||||||
|
|
||||||
|
import store from 'stores/redux_store.jsx';
|
||||||
|
const getState = store.getState;
|
||||||
|
|
||||||
|
import * as Selectors from 'mattermost-redux/selectors/entities/posts';
|
||||||
|
|
||||||
export default class EditPostModal extends React.Component {
|
export default class EditPostModal extends React.Component {
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
@@ -85,7 +90,7 @@ export default class EditPostModal extends React.Component {
|
|||||||
Reflect.deleteProperty(tempState, 'editText');
|
Reflect.deleteProperty(tempState, 'editText');
|
||||||
BrowserStore.setItem('edit_state_transfer', tempState);
|
BrowserStore.setItem('edit_state_transfer', tempState);
|
||||||
$('#edit_post').modal('hide');
|
$('#edit_post').modal('hide');
|
||||||
GlobalActions.showDeletePostModal(PostStore.getPost(this.state.channel_id, this.state.post_id), this.state.comments);
|
GlobalActions.showDeletePostModal(Selectors.getPost(getState(), this.state.post_id), this.state.comments);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -93,8 +98,7 @@ export default class EditPostModal extends React.Component {
|
|||||||
updatedPost,
|
updatedPost,
|
||||||
() => {
|
() => {
|
||||||
window.scrollTo(0, 0);
|
window.scrollTo(0, 0);
|
||||||
},
|
}
|
||||||
Boolean(PostStore.getFocusedPostId()) // If there is focused post we need to update that post's store too.
|
|
||||||
);
|
);
|
||||||
|
|
||||||
$('#edit_post').modal('hide');
|
$('#edit_post').modal('hide');
|
||||||
@@ -120,7 +124,7 @@ export default class EditPostModal extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
handleEditPostEvent(options) {
|
handleEditPostEvent(options) {
|
||||||
var post = PostStore.getPost(options.channelId, options.postId);
|
const post = Selectors.getPost(getState(), options.postId);
|
||||||
if (global.window.mm_license.IsLicensed === 'true') {
|
if (global.window.mm_license.IsLicensed === 'true') {
|
||||||
if (global.window.mm_config.AllowEditPost === Constants.ALLOW_EDIT_POST_NEVER) {
|
if (global.window.mm_config.AllowEditPost === Constants.ALLOW_EDIT_POST_NEVER) {
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
// See License.txt for license information.
|
// See License.txt for license information.
|
||||||
|
|
||||||
import Constants from 'utils/constants.jsx';
|
import Constants from 'utils/constants.jsx';
|
||||||
import FileStore from 'stores/file_store.jsx';
|
import {getFileUrl, getFileThumbnailUrl} from 'mattermost-redux/utils/file_utils';
|
||||||
import * as Utils from 'utils/utils.jsx';
|
import * as Utils from 'utils/utils.jsx';
|
||||||
|
|
||||||
import {Tooltip, OverlayTrigger} from 'react-bootstrap';
|
import {Tooltip, OverlayTrigger} from 'react-bootstrap';
|
||||||
@@ -46,7 +46,7 @@ export default class FileAttachment extends React.Component {
|
|||||||
const fileType = Utils.getFileType(fileInfo.extension);
|
const fileType = Utils.getFileType(fileInfo.extension);
|
||||||
|
|
||||||
if (fileType === 'image') {
|
if (fileType === 'image') {
|
||||||
const thumbnailUrl = FileStore.getFileThumbnailUrl(fileInfo.id);
|
const thumbnailUrl = getFileThumbnailUrl(fileInfo.id);
|
||||||
|
|
||||||
const img = new Image();
|
const img = new Image();
|
||||||
img.onload = () => {
|
img.onload = () => {
|
||||||
@@ -64,7 +64,7 @@ export default class FileAttachment extends React.Component {
|
|||||||
render() {
|
render() {
|
||||||
const fileInfo = this.props.fileInfo;
|
const fileInfo = this.props.fileInfo;
|
||||||
const fileName = fileInfo.name;
|
const fileName = fileInfo.name;
|
||||||
const fileUrl = FileStore.getFileUrl(fileInfo.id);
|
const fileUrl = getFileUrl(fileInfo.id);
|
||||||
|
|
||||||
let thumbnail;
|
let thumbnail;
|
||||||
if (this.state.loaded) {
|
if (this.state.loaded) {
|
||||||
@@ -83,7 +83,7 @@ export default class FileAttachment extends React.Component {
|
|||||||
<div
|
<div
|
||||||
className={className}
|
className={className}
|
||||||
style={{
|
style={{
|
||||||
backgroundImage: `url(${FileStore.getFileThumbnailUrl(fileInfo.id)})`
|
backgroundImage: `url(${getFileThumbnailUrl(fileInfo.id)})`
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||||
// See License.txt for license information.
|
// See License.txt for license information.
|
||||||
|
|
||||||
import ViewImageModal from './view_image.jsx';
|
import ViewImageModal from 'components/view_image.jsx';
|
||||||
import FileAttachment from './file_attachment.jsx';
|
import FileAttachment from 'components/file_attachment.jsx';
|
||||||
import Constants from 'utils/constants.jsx';
|
import Constants from 'utils/constants.jsx';
|
||||||
|
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
@@ -10,6 +10,37 @@ import PropTypes from 'prop-types';
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
export default class FileAttachmentList extends React.Component {
|
export default class FileAttachmentList extends React.Component {
|
||||||
|
static propTypes = {
|
||||||
|
|
||||||
|
/*
|
||||||
|
* The post the files are attached to
|
||||||
|
*/
|
||||||
|
post: PropTypes.object.isRequired,
|
||||||
|
|
||||||
|
/*
|
||||||
|
* The number of files attached to the post
|
||||||
|
*/
|
||||||
|
fileCount: PropTypes.number.isRequired,
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Array of metadata for each file attached to the post
|
||||||
|
*/
|
||||||
|
fileInfos: PropTypes.arrayOf(PropTypes.object),
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Set to render compactly
|
||||||
|
*/
|
||||||
|
compactDisplay: PropTypes.bool,
|
||||||
|
|
||||||
|
actions: PropTypes.shape({
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Function to get file metadata for a post
|
||||||
|
*/
|
||||||
|
getMissingFilesForPost: PropTypes.func.isRequired
|
||||||
|
}).isRequired
|
||||||
|
}
|
||||||
|
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
|
|
||||||
@@ -18,6 +49,12 @@ export default class FileAttachmentList extends React.Component {
|
|||||||
this.state = {showPreviewModal: false, startImgIndex: 0};
|
this.state = {showPreviewModal: false, startImgIndex: 0};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
if (this.props.post.file_ids || this.props.post.filenames) {
|
||||||
|
this.props.actions.getMissingFilesForPost(this.props.post.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
handleImageClick(indexClicked) {
|
handleImageClick(indexClicked) {
|
||||||
this.setState({showPreviewModal: true, startImgIndex: indexClicked});
|
this.setState({showPreviewModal: true, startImgIndex: indexClicked});
|
||||||
}
|
}
|
||||||
@@ -65,9 +102,3 @@ export default class FileAttachmentList extends React.Component {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
FileAttachmentList.propTypes = {
|
|
||||||
fileCount: PropTypes.number.isRequired,
|
|
||||||
fileInfos: PropTypes.arrayOf(PropTypes.object),
|
|
||||||
compactDisplay: PropTypes.bool
|
|
||||||
};
|
|
||||||
39
webapp/components/file_attachment_list/index.js
Обычный файл
39
webapp/components/file_attachment_list/index.js
Обычный файл
@@ -0,0 +1,39 @@
|
|||||||
|
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
|
||||||
|
// See License.txt for license information.
|
||||||
|
|
||||||
|
import {connect} from 'react-redux';
|
||||||
|
import {bindActionCreators} from 'redux';
|
||||||
|
import {getMissingFilesForPost} from 'mattermost-redux/actions/files';
|
||||||
|
import {makeGetFilesForPost} from 'mattermost-redux/selectors/entities/files';
|
||||||
|
|
||||||
|
import FileAttachmentList from './file_attachment_list.jsx';
|
||||||
|
|
||||||
|
function makeMapStateToProps() {
|
||||||
|
const selectFilesForPost = makeGetFilesForPost();
|
||||||
|
return function mapStateToProps(state, ownProps) {
|
||||||
|
const fileInfos = selectFilesForPost(state, ownProps.post);
|
||||||
|
|
||||||
|
let fileCount = 0;
|
||||||
|
if (ownProps.post.file_ids) {
|
||||||
|
fileCount = ownProps.post.file_ids.length;
|
||||||
|
} else if (this.props.post.filenames) {
|
||||||
|
fileCount = ownProps.post.filenames.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
...ownProps,
|
||||||
|
fileInfos,
|
||||||
|
fileCount
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function mapDispatchToProps(dispatch) {
|
||||||
|
return {
|
||||||
|
actions: bindActionCreators({
|
||||||
|
getMissingFilesForPost
|
||||||
|
}, dispatch)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export default connect(makeMapStateToProps, mapDispatchToProps)(FileAttachmentList);
|
||||||
@@ -1,92 +0,0 @@
|
|||||||
import PropTypes from 'prop-types';
|
|
||||||
|
|
||||||
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
|
|
||||||
// See License.txt for license information.
|
|
||||||
|
|
||||||
import React from 'react';
|
|
||||||
|
|
||||||
import * as AsyncClient from 'utils/async_client.jsx';
|
|
||||||
import FileStore from 'stores/file_store.jsx';
|
|
||||||
|
|
||||||
import FileAttachmentList from './file_attachment_list.jsx';
|
|
||||||
|
|
||||||
export default class FileAttachmentListContainer extends React.Component {
|
|
||||||
static propTypes = {
|
|
||||||
post: PropTypes.object.isRequired,
|
|
||||||
compactDisplay: PropTypes.bool.isRequired
|
|
||||||
}
|
|
||||||
|
|
||||||
constructor(props) {
|
|
||||||
super(props);
|
|
||||||
|
|
||||||
this.handleFileChange = this.handleFileChange.bind(this);
|
|
||||||
|
|
||||||
this.state = {
|
|
||||||
fileInfos: FileStore.getInfosForPost(props.post.id)
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidMount() {
|
|
||||||
FileStore.addChangeListener(this.handleFileChange);
|
|
||||||
|
|
||||||
if (this.props.post.id && !FileStore.hasInfosForPost(this.props.post.id)) {
|
|
||||||
AsyncClient.getFileInfosForPost(this.props.post.channel_id, this.props.post.id);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
componentWillReceiveProps(nextProps) {
|
|
||||||
if (nextProps.post.id !== this.props.post.id) {
|
|
||||||
this.setState({
|
|
||||||
fileInfos: FileStore.getInfosForPost(nextProps.post.id)
|
|
||||||
});
|
|
||||||
|
|
||||||
if (nextProps.post.id && !FileStore.hasInfosForPost(nextProps.post.id)) {
|
|
||||||
AsyncClient.getFileInfosForPost(nextProps.post.channel_id, nextProps.post.id);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
shouldComponentUpdate(nextProps, nextState) {
|
|
||||||
if (this.props.post.id !== nextProps.post.id) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.props.compactDisplay !== nextProps.compactDisplay) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// fileInfos are treated as immutable by the FileStore
|
|
||||||
if (nextState.fileInfos !== this.state.fileInfos) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
handleFileChange() {
|
|
||||||
this.setState({
|
|
||||||
fileInfos: FileStore.getInfosForPost(this.props.post.id)
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
componentWillUnmount() {
|
|
||||||
FileStore.removeChangeListener(this.handleFileChange);
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
let fileCount = 0;
|
|
||||||
if (this.props.post.file_ids) {
|
|
||||||
fileCount = this.props.post.file_ids.length;
|
|
||||||
} else if (this.props.post.filenames) {
|
|
||||||
fileCount = this.props.post.filenames.length;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<FileAttachmentList
|
|
||||||
fileCount={fileCount}
|
|
||||||
fileInfos={this.state.fileInfos}
|
|
||||||
compactDisplay={this.props.compactDisplay}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,9 +1,9 @@
|
|||||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||||
// See License.txt for license information.
|
// See License.txt for license information.
|
||||||
|
|
||||||
import FileStore from 'stores/file_store.jsx';
|
|
||||||
import ReactDOM from 'react-dom';
|
import ReactDOM from 'react-dom';
|
||||||
import * as Utils from 'utils/utils.jsx';
|
import * as Utils from 'utils/utils.jsx';
|
||||||
|
import {getFileUrl} from 'mattermost-redux/utils/file_utils';
|
||||||
|
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
@@ -39,7 +39,7 @@ export default class FilePreview extends React.Component {
|
|||||||
previewImage = (
|
previewImage = (
|
||||||
<img
|
<img
|
||||||
className='file-preview__image'
|
className='file-preview__image'
|
||||||
src={FileStore.getFileUrl(info.id)}
|
src={getFileUrl(info.id)}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -13,7 +13,6 @@ import UserStore from 'stores/user_store.jsx';
|
|||||||
import PreferenceStore from 'stores/preference_store.jsx';
|
import PreferenceStore from 'stores/preference_store.jsx';
|
||||||
import ChannelStore from 'stores/channel_store.jsx';
|
import ChannelStore from 'stores/channel_store.jsx';
|
||||||
import PostStore from 'stores/post_store.jsx';
|
import PostStore from 'stores/post_store.jsx';
|
||||||
import * as GlobalActions from 'actions/global_actions.jsx';
|
|
||||||
import {startPeriodicStatusUpdates, stopPeriodicStatusUpdates} from 'actions/status_actions.jsx';
|
import {startPeriodicStatusUpdates, stopPeriodicStatusUpdates} from 'actions/status_actions.jsx';
|
||||||
import {startPeriodicSync, stopPeriodicSync} from 'actions/websocket_actions.jsx';
|
import {startPeriodicSync, stopPeriodicSync} from 'actions/websocket_actions.jsx';
|
||||||
import {loadProfilesForSidebar} from 'actions/user_actions.jsx';
|
import {loadProfilesForSidebar} from 'actions/user_actions.jsx';
|
||||||
@@ -23,13 +22,16 @@ const TutorialSteps = Constants.TutorialSteps;
|
|||||||
const Preferences = Constants.Preferences;
|
const Preferences = Constants.Preferences;
|
||||||
|
|
||||||
import AnnouncementBar from 'components/announcement_bar';
|
import AnnouncementBar from 'components/announcement_bar';
|
||||||
import SidebarRight from 'components/sidebar_right.jsx';
|
import SidebarRight from 'components/sidebar_right';
|
||||||
import SidebarRightMenu from 'components/sidebar_right_menu.jsx';
|
import SidebarRightMenu from 'components/sidebar_right_menu.jsx';
|
||||||
import Navbar from 'components/navbar.jsx';
|
import Navbar from 'components/navbar.jsx';
|
||||||
import WebrtcSidebar from 'components/webrtc/components/webrtc_sidebar.jsx';
|
import WebrtcSidebar from 'components/webrtc/components/webrtc_sidebar.jsx';
|
||||||
|
|
||||||
import WebrtcNotification from 'components/webrtc/components/webrtc_notification.jsx';
|
import WebrtcNotification from 'components/webrtc/components/webrtc_notification.jsx';
|
||||||
|
|
||||||
|
import store from 'stores/redux_store.jsx';
|
||||||
|
import {getPost} from 'mattermost-redux/selectors/entities/posts';
|
||||||
|
|
||||||
// Modals
|
// Modals
|
||||||
import GetPostLinkModal from 'components/get_post_link_modal.jsx';
|
import GetPostLinkModal from 'components/get_post_link_modal.jsx';
|
||||||
import GetPublicLinkModal from 'components/get_public_link_modal.jsx';
|
import GetPublicLinkModal from 'components/get_public_link_modal.jsx';
|
||||||
@@ -111,9 +113,6 @@ export default class NeedsTeam extends React.Component {
|
|||||||
TeamStore.addChangeListener(this.onTeamChanged);
|
TeamStore.addChangeListener(this.onTeamChanged);
|
||||||
PreferenceStore.addChangeListener(this.onPreferencesChanged);
|
PreferenceStore.addChangeListener(this.onPreferencesChanged);
|
||||||
|
|
||||||
// Emit view action
|
|
||||||
GlobalActions.viewLoggedIn();
|
|
||||||
|
|
||||||
startPeriodicStatusUpdates();
|
startPeriodicStatusUpdates();
|
||||||
startPeriodicSync();
|
startPeriodicSync();
|
||||||
|
|
||||||
@@ -201,7 +200,7 @@ export default class NeedsTeam extends React.Component {
|
|||||||
if (channel == null) {
|
if (channel == null) {
|
||||||
// the permalink view is not really tied to a particular channel but still needs it
|
// the permalink view is not really tied to a particular channel but still needs it
|
||||||
const postId = PostStore.getFocusedPostId();
|
const postId = PostStore.getFocusedPostId();
|
||||||
const post = PostStore.getEarliestPostFromPage(postId);
|
const post = getPost(store.getState(), postId);
|
||||||
|
|
||||||
// the post take some time before being available on page load
|
// the post take some time before being available on page load
|
||||||
if (post != null) {
|
if (post != null) {
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import PropTypes from 'prop-types';
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
import ChannelHeader from 'components/channel_header.jsx';
|
import ChannelHeader from 'components/channel_header.jsx';
|
||||||
import PostFocusViewController from 'components/post_view/post_focus_view_controller.jsx';
|
import PostView from 'components/post_view';
|
||||||
|
|
||||||
import ChannelStore from 'stores/channel_store.jsx';
|
import ChannelStore from 'stores/channel_store.jsx';
|
||||||
import TeamStore from 'stores/team_store.jsx';
|
import TeamStore from 'stores/team_store.jsx';
|
||||||
@@ -14,7 +14,11 @@ import TeamStore from 'stores/team_store.jsx';
|
|||||||
import {Link} from 'react-router/es6';
|
import {Link} from 'react-router/es6';
|
||||||
import {FormattedMessage} from 'react-intl';
|
import {FormattedMessage} from 'react-intl';
|
||||||
|
|
||||||
export default class PermalinkView extends React.Component {
|
export default class PermalinkView extends React.PureComponent {
|
||||||
|
static propTypes = {
|
||||||
|
params: PropTypes.object.isRequired
|
||||||
|
}
|
||||||
|
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
|
|
||||||
@@ -24,6 +28,7 @@ export default class PermalinkView extends React.Component {
|
|||||||
|
|
||||||
this.state = this.getStateFromStores(props);
|
this.state = this.getStateFromStores(props);
|
||||||
}
|
}
|
||||||
|
|
||||||
getStateFromStores(props) {
|
getStateFromStores(props) {
|
||||||
const postId = props.params.postid;
|
const postId = props.params.postid;
|
||||||
const channel = ChannelStore.getCurrent();
|
const channel = ChannelStore.getCurrent();
|
||||||
@@ -38,27 +43,33 @@ export default class PermalinkView extends React.Component {
|
|||||||
postId
|
postId
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
isStateValid() {
|
isStateValid() {
|
||||||
return this.state.channelId !== '' && this.state.teamName;
|
return this.state.channelId !== '' && this.state.teamName;
|
||||||
}
|
}
|
||||||
|
|
||||||
updateState() {
|
updateState() {
|
||||||
this.setState(this.getStateFromStores(this.props));
|
this.setState(this.getStateFromStores(this.props));
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
ChannelStore.addChangeListener(this.updateState);
|
ChannelStore.addChangeListener(this.updateState);
|
||||||
TeamStore.addChangeListener(this.updateState);
|
TeamStore.addChangeListener(this.updateState);
|
||||||
|
|
||||||
$('body').addClass('app__body');
|
$('body').addClass('app__body');
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillUnmount() {
|
componentWillUnmount() {
|
||||||
ChannelStore.removeChangeListener(this.updateState);
|
ChannelStore.removeChangeListener(this.updateState);
|
||||||
TeamStore.removeChangeListener(this.updateState);
|
TeamStore.removeChangeListener(this.updateState);
|
||||||
|
|
||||||
$('body').removeClass('app__body');
|
$('body').removeClass('app__body');
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillReceiveProps(nextProps) {
|
componentWillReceiveProps(nextProps) {
|
||||||
this.setState(this.getStateFromStores(nextProps));
|
this.setState(this.getStateFromStores(nextProps));
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
if (!this.isStateValid()) {
|
if (!this.isStateValid()) {
|
||||||
return null;
|
return null;
|
||||||
@@ -71,7 +82,10 @@ export default class PermalinkView extends React.Component {
|
|||||||
<ChannelHeader
|
<ChannelHeader
|
||||||
channelId={this.state.channelId}
|
channelId={this.state.channelId}
|
||||||
/>
|
/>
|
||||||
<PostFocusViewController/>
|
<PostView
|
||||||
|
channelId={this.state.channelId}
|
||||||
|
focusedPostId={this.state.postId}
|
||||||
|
/>
|
||||||
<div
|
<div
|
||||||
id='archive-link-home'
|
id='archive-link-home'
|
||||||
>
|
>
|
||||||
@@ -89,7 +103,3 @@ export default class PermalinkView extends React.Component {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
PermalinkView.propTypes = {
|
|
||||||
params: PropTypes.object.isRequired
|
|
||||||
};
|
|
||||||
|
|||||||
@@ -0,0 +1,51 @@
|
|||||||
|
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
|
||||||
|
// See License.txt for license information.
|
||||||
|
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import * as Utils from 'utils/utils.jsx';
|
||||||
|
|
||||||
|
export default class CommentedOnFilesMessage extends React.PureComponent {
|
||||||
|
static propTypes = {
|
||||||
|
|
||||||
|
/*
|
||||||
|
* The id of the post that was commented on
|
||||||
|
*/
|
||||||
|
parentPostId: React.PropTypes.string.isRequired,
|
||||||
|
|
||||||
|
/*
|
||||||
|
* An array of file metadata for the parent post
|
||||||
|
*/
|
||||||
|
fileInfos: React.PropTypes.arrayOf(React.PropTypes.object),
|
||||||
|
|
||||||
|
actions: React.PropTypes.shape({
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Function to get file metadata for a post
|
||||||
|
*/
|
||||||
|
getFilesForPost: React.PropTypes.func.isRequired
|
||||||
|
}).isRequired
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
if (!this.props.fileInfos || this.props.fileInfos.length === 0) {
|
||||||
|
this.props.actions.getFilesForPost(this.props.parentPostId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
let message = ' ';
|
||||||
|
|
||||||
|
if (this.props.fileInfos && this.props.fileInfos.length > 0) {
|
||||||
|
message = this.props.fileInfos[0].name;
|
||||||
|
|
||||||
|
if (this.props.fileInfos.length === 2) {
|
||||||
|
message += Utils.localizeMessage('post_body.plusOne', ' plus 1 other file');
|
||||||
|
} else if (this.props.fileInfos.length > 2) {
|
||||||
|
message += Utils.localizeMessage('post_body.plusMore', ' plus {count} other files').replace('{count}', (this.props.fileInfos.length - 1).toString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return <span>{message}</span>;
|
||||||
|
}
|
||||||
|
}
|
||||||
36
webapp/components/post_view/commented_on_files_message/index.js
Обычный файл
36
webapp/components/post_view/commented_on_files_message/index.js
Обычный файл
@@ -0,0 +1,36 @@
|
|||||||
|
// Copyright (c) 2017 Mattermost, Inc. All Rights Reserved.
|
||||||
|
// See License.txt for license information.
|
||||||
|
|
||||||
|
import {connect} from 'react-redux';
|
||||||
|
import {bindActionCreators} from 'redux';
|
||||||
|
import {getFilesForPost} from 'mattermost-redux/actions/files';
|
||||||
|
|
||||||
|
import {makeGetFilesForPost} from 'mattermost-redux/selectors/entities/files';
|
||||||
|
|
||||||
|
import CommentedOnFilesMessage from './commented_on_files_message.jsx';
|
||||||
|
|
||||||
|
function makeMapStateToProps() {
|
||||||
|
const selectFileInfosForPost = makeGetFilesForPost();
|
||||||
|
|
||||||
|
return function mapStateToProps(state, ownProps) {
|
||||||
|
let fileInfos;
|
||||||
|
if (ownProps.parentPostId) {
|
||||||
|
fileInfos = selectFileInfosForPost(state, {id: ownProps.parentPostId});
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
...ownProps,
|
||||||
|
fileInfos
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function mapDispatchToProps(dispatch) {
|
||||||
|
return {
|
||||||
|
actions: bindActionCreators({
|
||||||
|
getFilesForPost
|
||||||
|
}, dispatch)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export default connect(makeMapStateToProps, mapDispatchToProps)(CommentedOnFilesMessage);
|
||||||
@@ -1,90 +0,0 @@
|
|||||||
import PropTypes from 'prop-types';
|
|
||||||
|
|
||||||
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
|
|
||||||
// See License.txt for license information.
|
|
||||||
|
|
||||||
import React from 'react';
|
|
||||||
|
|
||||||
import * as AsyncClient from 'utils/async_client.jsx';
|
|
||||||
import FileStore from 'stores/file_store.jsx';
|
|
||||||
import * as Utils from 'utils/utils.jsx';
|
|
||||||
|
|
||||||
export default class CommentedOnFilesMessageContainer extends React.Component {
|
|
||||||
static propTypes = {
|
|
||||||
parentPostChannelId: PropTypes.string.isRequired,
|
|
||||||
parentPostId: PropTypes.string.isRequired
|
|
||||||
}
|
|
||||||
|
|
||||||
constructor(props) {
|
|
||||||
super(props);
|
|
||||||
|
|
||||||
this.handleFileChange = this.handleFileChange.bind(this);
|
|
||||||
|
|
||||||
this.state = {
|
|
||||||
fileInfos: FileStore.getInfosForPost(this.props.parentPostId)
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidMount() {
|
|
||||||
FileStore.addChangeListener(this.handleFileChange);
|
|
||||||
|
|
||||||
if (!FileStore.hasInfosForPost(this.props.parentPostId)) {
|
|
||||||
AsyncClient.getFileInfosForPost(this.props.parentPostChannelId, this.props.parentPostId);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
componentWillReceiveProps(nextProps) {
|
|
||||||
if (nextProps.parentPostId !== this.props.parentPostId) {
|
|
||||||
this.setState({
|
|
||||||
fileInfos: FileStore.getInfosForPost(this.props.parentPostId)
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!FileStore.hasInfosForPost(this.props.parentPostId)) {
|
|
||||||
AsyncClient.getFileInfosForPost(this.props.parentPostChannelId, this.props.parentPostId);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
shouldComponentUpdate(nextProps, nextState) {
|
|
||||||
if (nextProps.parentPostId !== this.props.parentPostId) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (nextProps.parentPostChannelId !== this.props.parentPostChannelId) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// fileInfos are treated as immutable by the FileStore
|
|
||||||
if (nextState.fileInfos !== this.state.fileInfos) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
handleFileChange() {
|
|
||||||
this.setState({
|
|
||||||
fileInfos: FileStore.getInfosForPost(this.props.parentPostId)
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
componentWillUnmount() {
|
|
||||||
FileStore.removeChangeListener(this.handleFileChange);
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
let message = ' ';
|
|
||||||
|
|
||||||
if (this.state.fileInfos && this.state.fileInfos.length > 0) {
|
|
||||||
message = this.state.fileInfos[0].name;
|
|
||||||
|
|
||||||
if (this.state.fileInfos.length === 2) {
|
|
||||||
message += Utils.localizeMessage('post_body.plusOne', ' plus 1 other file');
|
|
||||||
} else if (this.state.fileInfos.length > 2) {
|
|
||||||
message += Utils.localizeMessage('post_body.plusMore', ' plus {count} other files').replace('{count}', (this.state.fileInfos.length - 1).toString());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return <span>{message}</span>;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
import PropTypes from 'prop-types';
|
|
||||||
import React from 'react';
|
|
||||||
import {FormattedDate} from 'react-intl';
|
|
||||||
|
|
||||||
export default function DateSeparator(props) {
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
className='date-separator'
|
|
||||||
>
|
|
||||||
<hr className='separator__hr'/>
|
|
||||||
<div className='separator__text'>
|
|
||||||
<FormattedDate
|
|
||||||
value={props.date}
|
|
||||||
weekday='short'
|
|
||||||
month='short'
|
|
||||||
day='2-digit'
|
|
||||||
year='numeric'
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
DateSeparator.propTypes = {
|
|
||||||
date: PropTypes.instanceOf(Date)
|
|
||||||
};
|
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
|
||||||
// See License.txt for license information.
|
|
||||||
|
|
||||||
import PostAttachment from './post_attachment.jsx';
|
|
||||||
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
|
|
||||||
import React from 'react';
|
|
||||||
|
|
||||||
export default function PostAttachmentList(props) {
|
|
||||||
const content = [];
|
|
||||||
props.attachments.forEach((attachment, i) => {
|
|
||||||
content.push(
|
|
||||||
<PostAttachment
|
|
||||||
attachment={attachment}
|
|
||||||
key={'att_' + i}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className='attachment_list'>
|
|
||||||
{content}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
PostAttachmentList.propTypes = {
|
|
||||||
attachments: PropTypes.array.isRequired
|
|
||||||
};
|
|
||||||
@@ -1,690 +0,0 @@
|
|||||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
|
||||||
// See License.txt for license information.
|
|
||||||
import $ from 'jquery';
|
|
||||||
|
|
||||||
import Post from './post.jsx';
|
|
||||||
import FloatingTimestamp from './floating_timestamp.jsx';
|
|
||||||
import ScrollToBottomArrows from './scroll_to_bottom_arrows.jsx';
|
|
||||||
import NewMessageIndicator from './new_message_indicator.jsx';
|
|
||||||
|
|
||||||
import * as GlobalActions from 'actions/global_actions.jsx';
|
|
||||||
|
|
||||||
import {createChannelIntroMessage} from 'utils/channel_intro_messages.jsx';
|
|
||||||
|
|
||||||
import * as UserAgent from 'utils/user_agent.jsx';
|
|
||||||
import * as Utils from 'utils/utils.jsx';
|
|
||||||
import * as PostUtils from 'utils/post_utils.jsx';
|
|
||||||
import DelayedAction from 'utils/delayed_action.jsx';
|
|
||||||
|
|
||||||
import * as ChannelActions from 'actions/channel_actions.jsx';
|
|
||||||
|
|
||||||
import Constants from 'utils/constants.jsx';
|
|
||||||
const ScrollTypes = Constants.ScrollTypes;
|
|
||||||
|
|
||||||
import PostStore from 'stores/post_store.jsx';
|
|
||||||
import PreferenceStore from 'stores/preference_store.jsx';
|
|
||||||
import ScrollStore from 'stores/scroll_store.jsx';
|
|
||||||
import {FormattedDate, FormattedMessage} from 'react-intl';
|
|
||||||
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
|
|
||||||
import React from 'react';
|
|
||||||
import ReactDOM from 'react-dom';
|
|
||||||
|
|
||||||
const Preferences = Constants.Preferences;
|
|
||||||
|
|
||||||
export default class PostList extends React.Component {
|
|
||||||
constructor(props) {
|
|
||||||
super(props);
|
|
||||||
|
|
||||||
this.handleScroll = this.handleScroll.bind(this);
|
|
||||||
this.handleScrollStop = this.handleScrollStop.bind(this);
|
|
||||||
this.isAtBottom = this.isAtBottom.bind(this);
|
|
||||||
this.loadMorePostsTop = this.loadMorePostsTop.bind(this);
|
|
||||||
this.loadMorePostsBottom = this.loadMorePostsBottom.bind(this);
|
|
||||||
this.createPosts = this.createPosts.bind(this);
|
|
||||||
this.updateScrolling = this.updateScrolling.bind(this);
|
|
||||||
this.handleResize = this.handleResize.bind(this);
|
|
||||||
this.scrollToBottom = this.scrollToBottom.bind(this);
|
|
||||||
this.scrollToBottomAnimated = this.scrollToBottomAnimated.bind(this);
|
|
||||||
this.handleKeyDown = this.handleKeyDown.bind(this);
|
|
||||||
this.childComponentDidUpdate = this.childComponentDidUpdate.bind(this);
|
|
||||||
this.checkAndUpdateScrolling = this.checkAndUpdateScrolling.bind(this);
|
|
||||||
|
|
||||||
this.jumpToPostNode = null;
|
|
||||||
this.wasAtBottom = true;
|
|
||||||
this.scrollHeight = 0;
|
|
||||||
this.animationFrameId = 0;
|
|
||||||
|
|
||||||
this.scrollStopAction = new DelayedAction(this.handleScrollStop);
|
|
||||||
|
|
||||||
this.state = {
|
|
||||||
isScrolling: false,
|
|
||||||
fullWidthIntro: PreferenceStore.get(Preferences.CATEGORY_DISPLAY_SETTINGS, Preferences.CHANNEL_DISPLAY_MODE, Preferences.CHANNEL_DISPLAY_MODE_DEFAULT) === Preferences.CHANNEL_DISPLAY_MODE_FULL_SCREEN,
|
|
||||||
topPostId: null,
|
|
||||||
unViewedCount: 0
|
|
||||||
};
|
|
||||||
|
|
||||||
if (props.channel) {
|
|
||||||
this.introText = createChannelIntroMessage(props.channel, this.state.fullWidthIntro);
|
|
||||||
} else {
|
|
||||||
this.introText = this.getArchivesIntroMessage();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
componentWillReceiveProps(nextProps) {
|
|
||||||
if (this.props.channel && this.props.channel.type === Constants.DM_CHANNEL) {
|
|
||||||
const teammateId = Utils.getUserIdFromChannelName(this.props.channel);
|
|
||||||
if (!this.props.profiles[teammateId] && nextProps.profiles[teammateId]) {
|
|
||||||
this.introText = createChannelIntroMessage(this.props.channel, this.state.fullWidthIntro);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const posts = nextProps.postList.posts;
|
|
||||||
const order = nextProps.postList.order;
|
|
||||||
let unViewedCount = 0;
|
|
||||||
|
|
||||||
// Only count if we're not at the bottom, not in highlight view,
|
|
||||||
// or anything else
|
|
||||||
if (nextProps.scrollType === Constants.ScrollTypes.FREE) {
|
|
||||||
unViewedCount = order.reduce((count, orderId) => {
|
|
||||||
const post = posts[orderId];
|
|
||||||
if (post.create_at > nextProps.lastViewedBottom &&
|
|
||||||
post.user_id !== nextProps.currentUser.id &&
|
|
||||||
post.state !== Constants.POST_DELETED) {
|
|
||||||
return count + 1;
|
|
||||||
}
|
|
||||||
return count;
|
|
||||||
}, 0);
|
|
||||||
}
|
|
||||||
this.setState({unViewedCount});
|
|
||||||
|
|
||||||
if (this.props.channelId !== nextProps.channelId) {
|
|
||||||
PostStore.removePostDraftChangeListener(this.props.channelId, this.handlePostDraftChange);
|
|
||||||
PostStore.addPostDraftChangeListener(nextProps.channelId, this.handlePostDraftChange);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
handleKeyDown(e) {
|
|
||||||
if (e.which === Constants.KeyCodes.ESCAPE && $('.popover.in,.modal.in').length === 0) {
|
|
||||||
e.preventDefault();
|
|
||||||
ChannelActions.setChannelAsRead();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
isAtBottom() {
|
|
||||||
if (!this.refs.postlist) {
|
|
||||||
return this.wasAtBottom;
|
|
||||||
}
|
|
||||||
|
|
||||||
// consider the view to be at the bottom if it's within this many pixels of the bottom
|
|
||||||
const atBottomMargin = 10;
|
|
||||||
|
|
||||||
return this.refs.postlist.clientHeight + this.refs.postlist.scrollTop >= this.refs.postlist.scrollHeight - atBottomMargin;
|
|
||||||
}
|
|
||||||
|
|
||||||
handleScroll() {
|
|
||||||
// HACK FOR RHS -- REMOVE WHEN RHS DIES
|
|
||||||
const childNodes = this.refs.postlistcontent.childNodes;
|
|
||||||
for (let i = 0; i < childNodes.length; i++) {
|
|
||||||
// If the node is 1/3 down the page
|
|
||||||
if (childNodes[i].offsetTop >= (this.refs.postlist.scrollTop + (this.refs.postlist.offsetHeight / Constants.SCROLL_PAGE_FRACTION))) {
|
|
||||||
this.jumpToPostNode = childNodes[i];
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!this.jumpToPostNode && childNodes.length > 0) {
|
|
||||||
this.jumpToPostNode = childNodes[childNodes.length - 1];
|
|
||||||
}
|
|
||||||
|
|
||||||
this.updateFloatingTimestamp();
|
|
||||||
|
|
||||||
if (!this.state.isScrolling) {
|
|
||||||
this.setState({
|
|
||||||
isScrolling: true
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Postpone all DOM related calculations to next frame.
|
|
||||||
// scrollHeight etc might return wrong data at this point
|
|
||||||
setTimeout(() => {
|
|
||||||
if (!this.refs.postlist) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.wasAtBottom = this.isAtBottom();
|
|
||||||
this.props.postListScrolled(this.isAtBottom());
|
|
||||||
this.prevScrollHeight = this.refs.postlist.scrollHeight;
|
|
||||||
this.prevOffsetTop = this.jumpToPostNode.offsetTop;
|
|
||||||
}, 0);
|
|
||||||
|
|
||||||
this.scrollStopAction.fireAfter(Constants.SCROLL_DELAY);
|
|
||||||
}
|
|
||||||
|
|
||||||
handleScrollStop() {
|
|
||||||
this.setState({
|
|
||||||
isScrolling: false
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
updateFloatingTimestamp() {
|
|
||||||
// skip this in non-mobile view since that's when the timestamp is visible
|
|
||||||
if (!Utils.isMobile()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.props.postList) {
|
|
||||||
// iterate through posts starting at the bottom since users are more likely to be viewing newer posts
|
|
||||||
for (let i = 0; i < this.props.postList.order.length; i++) {
|
|
||||||
const id = this.props.postList.order[i];
|
|
||||||
const element = this.refs[id];
|
|
||||||
|
|
||||||
if (!element || !element.domNode || element.domNode.offsetTop + element.domNode.clientHeight <= this.refs.postlist.scrollTop) {
|
|
||||||
// this post is off the top of the screen so the last one is at the top of the screen
|
|
||||||
let topPostId;
|
|
||||||
|
|
||||||
if (i > 0) {
|
|
||||||
topPostId = this.props.postList.order[i - 1];
|
|
||||||
} else {
|
|
||||||
// the first post we look at should always be on the screen, but handle that case anyway
|
|
||||||
topPostId = id;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (topPostId !== this.state.topPostId) {
|
|
||||||
this.setState({
|
|
||||||
topPostId
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
loadMorePostsTop(e) {
|
|
||||||
e.preventDefault();
|
|
||||||
|
|
||||||
if (this.props.isFocusPost) {
|
|
||||||
return GlobalActions.emitLoadMorePostsFocusedTopEvent();
|
|
||||||
}
|
|
||||||
return GlobalActions.emitLoadMorePostsEvent();
|
|
||||||
}
|
|
||||||
|
|
||||||
loadMorePostsBottom() {
|
|
||||||
GlobalActions.emitLoadMorePostsFocusedBottomEvent();
|
|
||||||
}
|
|
||||||
|
|
||||||
createPosts(posts, order) {
|
|
||||||
const postCtls = [];
|
|
||||||
let previousPostDay = new Date(0);
|
|
||||||
const userId = this.props.currentUser.id;
|
|
||||||
const profiles = this.props.profiles || {};
|
|
||||||
|
|
||||||
let renderedLastViewed = false;
|
|
||||||
|
|
||||||
for (let i = order.length - 1; i >= 0; i--) {
|
|
||||||
const post = posts[order[i]];
|
|
||||||
const parentPost = posts[post.parent_id];
|
|
||||||
const prevPost = posts[order[i + 1]];
|
|
||||||
const postUserId = PostUtils.isSystemMessage(post) ? '' : post.user_id;
|
|
||||||
|
|
||||||
// If the post is a comment whose parent has been deleted, don't add it to the list.
|
|
||||||
if (parentPost && parentPost.state === Constants.POST_DELETED) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
let sameUser = false;
|
|
||||||
let sameRoot = false;
|
|
||||||
let hideProfilePic = false;
|
|
||||||
|
|
||||||
if (prevPost) {
|
|
||||||
const postIsComment = PostUtils.isComment(post);
|
|
||||||
const prevPostIsComment = PostUtils.isComment(prevPost);
|
|
||||||
const postFromWebhook = Boolean(post.props && post.props.from_webhook);
|
|
||||||
const prevPostFromWebhook = Boolean(prevPost.props && prevPost.props.from_webhook);
|
|
||||||
const prevPostUserId = PostUtils.isSystemMessage(prevPost) ? '' : prevPost.user_id;
|
|
||||||
|
|
||||||
// consider posts from the same user if:
|
|
||||||
// the previous post was made by the same user as the current post,
|
|
||||||
// the previous post was made within 5 minutes of the current post,
|
|
||||||
// the current post is not from a webhook
|
|
||||||
// the previous post is not from a webhook
|
|
||||||
if (prevPostUserId === postUserId &&
|
|
||||||
post.create_at - prevPost.create_at <= Constants.POST_COLLAPSE_TIMEOUT &&
|
|
||||||
!postFromWebhook && !prevPostFromWebhook) {
|
|
||||||
sameUser = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// consider posts from the same root if:
|
|
||||||
// the current post is a comment,
|
|
||||||
// the current post has the same root as the previous post
|
|
||||||
if (postIsComment && (prevPost.id === post.root_id || prevPost.root_id === post.root_id)) {
|
|
||||||
sameRoot = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// consider posts from the same root if:
|
|
||||||
// the current post is not a comment,
|
|
||||||
// the previous post is not a comment,
|
|
||||||
// the previous post is from the same user
|
|
||||||
if (!postIsComment && !prevPostIsComment && sameUser) {
|
|
||||||
sameRoot = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// hide the profile pic if:
|
|
||||||
// the previous post was made by the same user as the current post,
|
|
||||||
// the previous post is not a comment,
|
|
||||||
// the current post is not a comment,
|
|
||||||
// the previous post is not from a webhook
|
|
||||||
// the current post is not from a webhook
|
|
||||||
if (prevPostUserId === postUserId &&
|
|
||||||
!prevPostIsComment &&
|
|
||||||
!postIsComment &&
|
|
||||||
!prevPostFromWebhook &&
|
|
||||||
!postFromWebhook) {
|
|
||||||
hideProfilePic = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// check if it's the last comment in a consecutive string of comments on the same post
|
|
||||||
// it is the last comment if it is last post in the channel or the next post has a different root post
|
|
||||||
const isLastComment = PostUtils.isComment(post) && (i === 0 || posts[order[i - 1]].root_id !== post.root_id);
|
|
||||||
|
|
||||||
const keyPrefix = post.id ? post.id : i;
|
|
||||||
|
|
||||||
const shouldHighlight = this.props.postsToHighlight && this.props.postsToHighlight.hasOwnProperty(post.id);
|
|
||||||
|
|
||||||
let profile;
|
|
||||||
if (userId === post.user_id) {
|
|
||||||
profile = this.props.currentUser;
|
|
||||||
} else {
|
|
||||||
profile = profiles[post.user_id];
|
|
||||||
}
|
|
||||||
|
|
||||||
let commentCount = 0;
|
|
||||||
let isCommentMention = false;
|
|
||||||
let shouldHighlightThreads = false;
|
|
||||||
let commentRootId;
|
|
||||||
if (parentPost) {
|
|
||||||
commentRootId = post.root_id;
|
|
||||||
} else {
|
|
||||||
commentRootId = post.id;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (commentRootId) {
|
|
||||||
for (const postId in posts) {
|
|
||||||
if (posts[postId].root_id === commentRootId && !PostUtils.isSystemMessage(posts[postId])) {
|
|
||||||
commentCount += 1;
|
|
||||||
if (posts[postId].user_id === userId) {
|
|
||||||
shouldHighlightThreads = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (parentPost && commentRootId) {
|
|
||||||
const commentsNotifyLevel = this.props.currentUser.notify_props.comments || 'never';
|
|
||||||
const notCurrentUser = post.user_id !== userId || (post.props && post.props.from_webhook);
|
|
||||||
if (notCurrentUser) {
|
|
||||||
if (commentsNotifyLevel === 'any' && (posts[commentRootId].user_id === userId || shouldHighlightThreads)) {
|
|
||||||
isCommentMention = true;
|
|
||||||
} else if (commentsNotifyLevel === 'root' && posts[commentRootId].user_id === userId) {
|
|
||||||
isCommentMention = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let isFlagged = false;
|
|
||||||
if (this.props.flaggedPosts) {
|
|
||||||
isFlagged = this.props.flaggedPosts.get(post.id) === 'true';
|
|
||||||
}
|
|
||||||
|
|
||||||
let status = '';
|
|
||||||
if (this.props.statuses && profile) {
|
|
||||||
status = this.props.statuses[profile.id] || 'offline';
|
|
||||||
}
|
|
||||||
|
|
||||||
const postCtl = (
|
|
||||||
<Post
|
|
||||||
key={keyPrefix + 'postKey'}
|
|
||||||
ref={post.id}
|
|
||||||
lastPostCount={(i >= 0 && i < Constants.TEST_ID_COUNT) ? i : -1}
|
|
||||||
sameUser={sameUser}
|
|
||||||
sameRoot={sameRoot}
|
|
||||||
post={post}
|
|
||||||
parentPost={parentPost}
|
|
||||||
hideProfilePic={hideProfilePic}
|
|
||||||
isLastComment={isLastComment}
|
|
||||||
shouldHighlight={shouldHighlight}
|
|
||||||
displayNameType={this.props.displayNameType}
|
|
||||||
user={profile}
|
|
||||||
currentUser={this.props.currentUser}
|
|
||||||
center={this.props.displayPostsInCenter}
|
|
||||||
commentCount={commentCount}
|
|
||||||
isCommentMention={isCommentMention}
|
|
||||||
compactDisplay={this.props.compactDisplay}
|
|
||||||
previewCollapsed={this.props.previewsCollapsed}
|
|
||||||
useMilitaryTime={this.props.useMilitaryTime}
|
|
||||||
isFlagged={isFlagged}
|
|
||||||
status={status}
|
|
||||||
isBusy={this.props.isBusy}
|
|
||||||
childComponentDidUpdateFunction={this.childComponentDidUpdate}
|
|
||||||
getPostList={this.getPostList}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
|
|
||||||
const currentPostDay = Utils.getDateForUnixTicks(post.create_at);
|
|
||||||
if (currentPostDay.toDateString() !== previousPostDay.toDateString()) {
|
|
||||||
postCtls.push(
|
|
||||||
<div
|
|
||||||
key={currentPostDay.toDateString()}
|
|
||||||
className='date-separator'
|
|
||||||
>
|
|
||||||
<hr className='separator__hr'/>
|
|
||||||
<div className='separator__text'>
|
|
||||||
<FormattedDate
|
|
||||||
value={currentPostDay}
|
|
||||||
weekday='short'
|
|
||||||
month='short'
|
|
||||||
day='2-digit'
|
|
||||||
year='numeric'
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if ((postUserId !== userId || this.props.ownNewMessage) &&
|
|
||||||
this.props.lastViewed !== 0 &&
|
|
||||||
post.create_at > this.props.lastViewed &&
|
|
||||||
!Utils.isPostEphemeral(post) &&
|
|
||||||
!renderedLastViewed) {
|
|
||||||
renderedLastViewed = true;
|
|
||||||
|
|
||||||
// Temporary fix to solve ie11 rendering issue
|
|
||||||
let newSeparatorId = '';
|
|
||||||
if (!UserAgent.isInternetExplorer()) {
|
|
||||||
newSeparatorId = 'new_message_' + post.id;
|
|
||||||
}
|
|
||||||
postCtls.push(
|
|
||||||
<div
|
|
||||||
id={newSeparatorId}
|
|
||||||
key='unviewed'
|
|
||||||
ref='newMessageSeparator'
|
|
||||||
className='new-separator'
|
|
||||||
>
|
|
||||||
<hr
|
|
||||||
className='separator__hr'
|
|
||||||
/>
|
|
||||||
<div className='separator__text'>
|
|
||||||
<FormattedMessage
|
|
||||||
id='posts_view.newMsg'
|
|
||||||
defaultMessage='New Messages'
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
postCtls.push(postCtl);
|
|
||||||
previousPostDay = currentPostDay;
|
|
||||||
}
|
|
||||||
|
|
||||||
return postCtls;
|
|
||||||
}
|
|
||||||
|
|
||||||
updateScrolling() {
|
|
||||||
if (this.props.scrollType === ScrollTypes.BOTTOM) {
|
|
||||||
this.scrollToBottom();
|
|
||||||
} else if (this.props.scrollType === ScrollTypes.NEW_MESSAGE) {
|
|
||||||
window.requestAnimationFrame(() => {
|
|
||||||
// If separator exists scroll to it. Otherwise scroll to bottom.
|
|
||||||
if (this.refs.newMessageSeparator) {
|
|
||||||
var objDiv = this.refs.postlist;
|
|
||||||
objDiv.scrollTop = this.refs.newMessageSeparator.offsetTop; //scrolls node to top of Div
|
|
||||||
} else if (this.refs.postlist) {
|
|
||||||
this.scrollToBottom();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// This avoids the scroll jumping from top to bottom after the page has rendered (PLT-5025).
|
|
||||||
if (!this.refs.newMessageSeparator) {
|
|
||||||
this.scrollToBottom();
|
|
||||||
}
|
|
||||||
} else if (this.props.scrollType === ScrollTypes.POST && this.props.scrollPostId) {
|
|
||||||
window.requestAnimationFrame(() => {
|
|
||||||
const postNode = ReactDOM.findDOMNode(this.refs[this.props.scrollPostId]);
|
|
||||||
if (postNode == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
postNode.scrollIntoView();
|
|
||||||
if (this.refs.postlist.scrollTop === postNode.offsetTop) {
|
|
||||||
this.refs.postlist.scrollTop -= (this.refs.postlist.offsetHeight / Constants.SCROLL_PAGE_FRACTION);
|
|
||||||
} else {
|
|
||||||
this.refs.postlist.scrollTop -= (this.refs.postlist.offsetHeight / Constants.SCROLL_PAGE_FRACTION) + (this.refs.postlist.scrollTop - postNode.offsetTop);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} else if (this.props.scrollType === ScrollTypes.SIDEBAR_OPEN) {
|
|
||||||
// If we are at the bottom then stay there
|
|
||||||
if (this.wasAtBottom) {
|
|
||||||
this.refs.postlist.scrollTop = this.refs.postlist.scrollHeight;
|
|
||||||
} else {
|
|
||||||
window.requestAnimationFrame(() => {
|
|
||||||
this.jumpToPostNode.scrollIntoView();
|
|
||||||
if (this.refs.postlist.scrollTop === this.jumpToPostNode.offsetTop) {
|
|
||||||
this.refs.postlist.scrollTop -= (this.refs.postlist.offsetHeight / Constants.SCROLL_PAGE_FRACTION);
|
|
||||||
} else {
|
|
||||||
this.refs.postlist.scrollTop -= (this.refs.postlist.offsetHeight / Constants.SCROLL_PAGE_FRACTION) + (this.refs.postlist.scrollTop - this.jumpToPostNode.offsetTop);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} else if (this.refs.postlist.scrollHeight !== this.prevScrollHeight) {
|
|
||||||
window.requestAnimationFrame(() => {
|
|
||||||
if (this.jumpToPostNode && this.refs.postlist) {
|
|
||||||
this.refs.postlist.scrollTop += (this.jumpToPostNode.offsetTop - this.prevOffsetTop);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
handleResize() {
|
|
||||||
this.updateScrolling();
|
|
||||||
}
|
|
||||||
|
|
||||||
scrollToBottom() {
|
|
||||||
this.animationFrameId = window.requestAnimationFrame(() => {
|
|
||||||
if (this.refs.postlist) {
|
|
||||||
this.refs.postlist.scrollTop = this.refs.postlist.scrollHeight;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
scrollToBottomAnimated() {
|
|
||||||
if (UserAgent.isIos()) {
|
|
||||||
// JQuery animation doesn't work on iOS
|
|
||||||
this.refs.postlist.scrollTop = this.refs.postlist.scrollHeight;
|
|
||||||
} else {
|
|
||||||
var postList = $(this.refs.postlist);
|
|
||||||
|
|
||||||
postList.animate({scrollTop: this.refs.postlist.scrollHeight}, '500');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
getArchivesIntroMessage() {
|
|
||||||
return (
|
|
||||||
<div className={'channel-intro'}>
|
|
||||||
<h4 className='channel-intro__title'>
|
|
||||||
<FormattedMessage
|
|
||||||
id='post_focus_view.beginning'
|
|
||||||
defaultMessage='Beginning of Channel Archives'
|
|
||||||
/>
|
|
||||||
</h4>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
checkAndUpdateScrolling() {
|
|
||||||
if (this.props.postList != null && this.refs.postlist) {
|
|
||||||
this.updateScrolling();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidMount() {
|
|
||||||
if (this.props.postList != null) {
|
|
||||||
this.updateScrolling();
|
|
||||||
}
|
|
||||||
|
|
||||||
window.addEventListener('resize', this.handleResize);
|
|
||||||
window.addEventListener('keydown', this.handleKeyDown);
|
|
||||||
|
|
||||||
PostStore.addPostDraftChangeListener(this.props.channelId, this.handlePostDraftChange);
|
|
||||||
ScrollStore.addPostScrollListener(this.checkAndUpdateScrolling);
|
|
||||||
}
|
|
||||||
|
|
||||||
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() {
|
|
||||||
window.cancelAnimationFrame(this.animationFrameId);
|
|
||||||
window.removeEventListener('resize', this.handleResize);
|
|
||||||
window.removeEventListener('keydown', this.handleKeyDown);
|
|
||||||
ScrollStore.removePostScrollListener(this.checkAndUpdateScrolling);
|
|
||||||
this.scrollStopAction.cancel();
|
|
||||||
|
|
||||||
PostStore.removePostDraftChangeListener(this.props.channelId, this.handlePostDraftChange);
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidUpdate() {
|
|
||||||
this.checkAndUpdateScrolling();
|
|
||||||
}
|
|
||||||
|
|
||||||
childComponentDidUpdate() {
|
|
||||||
this.checkAndUpdateScrolling();
|
|
||||||
}
|
|
||||||
|
|
||||||
getPostList = () => {
|
|
||||||
return this.refs.postlist;
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
// Create intro message or top loadmore link
|
|
||||||
let moreMessagesTop;
|
|
||||||
if (this.props.showMoreMessagesTop) {
|
|
||||||
moreMessagesTop = (
|
|
||||||
<a
|
|
||||||
ref='loadmoretop'
|
|
||||||
className='more-messages-text theme'
|
|
||||||
href='#'
|
|
||||||
onClick={this.loadMorePostsTop}
|
|
||||||
>
|
|
||||||
<FormattedMessage
|
|
||||||
id='posts_view.loadMore'
|
|
||||||
defaultMessage='Load more messages'
|
|
||||||
/>
|
|
||||||
</a>
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
moreMessagesTop = this.introText;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Give option to load more posts at bottom if necessary
|
|
||||||
let moreMessagesBottom;
|
|
||||||
if (this.props.showMoreMessagesBottom) {
|
|
||||||
moreMessagesBottom = (
|
|
||||||
<a
|
|
||||||
ref='loadmorebottom'
|
|
||||||
className='more-messages-text theme'
|
|
||||||
href='#'
|
|
||||||
onClick={this.loadMorePostsBottom}
|
|
||||||
>
|
|
||||||
<FormattedMessage id='posts_view.loadMore'/>
|
|
||||||
</a>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create post elements
|
|
||||||
let postElements = null;
|
|
||||||
let topPostCreateAt = 0;
|
|
||||||
if (this.props.postList) {
|
|
||||||
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 (
|
|
||||||
<div>
|
|
||||||
<FloatingTimestamp
|
|
||||||
isScrolling={this.state.isScrolling}
|
|
||||||
isMobile={Utils.isMobile()}
|
|
||||||
createAt={topPostCreateAt}
|
|
||||||
/>
|
|
||||||
<ScrollToBottomArrows
|
|
||||||
isScrolling={this.state.isScrolling}
|
|
||||||
atBottom={this.wasAtBottom}
|
|
||||||
onClick={this.scrollToBottomAnimated}
|
|
||||||
/>
|
|
||||||
<NewMessageIndicator
|
|
||||||
newMessages={this.state.unViewedCount}
|
|
||||||
onClick={this.scrollToBottomAnimated}
|
|
||||||
/>
|
|
||||||
<div
|
|
||||||
ref='postlist'
|
|
||||||
className='post-list-holder-by-time'
|
|
||||||
onScroll={this.handleScroll}
|
|
||||||
>
|
|
||||||
<div className='post-list__table'>
|
|
||||||
<div
|
|
||||||
ref='postlistcontent'
|
|
||||||
className='post-list__content'
|
|
||||||
>
|
|
||||||
{moreMessagesTop}
|
|
||||||
{postElements}
|
|
||||||
{moreMessagesBottom}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
PostList.defaultProps = {
|
|
||||||
lastViewed: 0,
|
|
||||||
lastViewedBottom: Number.MAX_VALUE,
|
|
||||||
ownNewMessage: false
|
|
||||||
};
|
|
||||||
|
|
||||||
PostList.propTypes = {
|
|
||||||
postList: PropTypes.object,
|
|
||||||
profiles: PropTypes.object,
|
|
||||||
channel: PropTypes.object,
|
|
||||||
channelId: PropTypes.string.isRequired,
|
|
||||||
currentUser: PropTypes.object,
|
|
||||||
scrollPostId: PropTypes.string,
|
|
||||||
scrollType: PropTypes.number,
|
|
||||||
postListScrolled: PropTypes.func.isRequired,
|
|
||||||
showMoreMessagesTop: PropTypes.bool,
|
|
||||||
showMoreMessagesBottom: PropTypes.bool,
|
|
||||||
lastViewed: PropTypes.number,
|
|
||||||
lastViewedBottom: PropTypes.number,
|
|
||||||
ownNewMessage: PropTypes.bool,
|
|
||||||
postsToHighlight: PropTypes.object,
|
|
||||||
displayNameType: PropTypes.string,
|
|
||||||
displayPostsInCenter: PropTypes.bool,
|
|
||||||
compactDisplay: PropTypes.bool,
|
|
||||||
previewsCollapsed: PropTypes.string,
|
|
||||||
useMilitaryTime: PropTypes.bool.isRequired,
|
|
||||||
isFocusPost: PropTypes.bool,
|
|
||||||
flaggedPosts: PropTypes.object,
|
|
||||||
statuses: PropTypes.object,
|
|
||||||
isBusy: PropTypes.bool
|
|
||||||
};
|
|
||||||
@@ -1,106 +0,0 @@
|
|||||||
import PropTypes from 'prop-types';
|
|
||||||
|
|
||||||
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
|
|
||||||
// See License.txt for license information.
|
|
||||||
|
|
||||||
import React from 'react';
|
|
||||||
|
|
||||||
import ChannelStore from 'stores/channel_store.jsx';
|
|
||||||
import EmojiStore from 'stores/emoji_store.jsx';
|
|
||||||
import PreferenceStore from 'stores/preference_store.jsx';
|
|
||||||
import {Preferences} from 'utils/constants.jsx';
|
|
||||||
import TeamStore from 'stores/team_store.jsx';
|
|
||||||
import UserStore from 'stores/user_store.jsx';
|
|
||||||
|
|
||||||
import PostMessageView from './post_message_view.jsx';
|
|
||||||
|
|
||||||
export default class PostMessageContainer extends React.Component {
|
|
||||||
static propTypes = {
|
|
||||||
post: PropTypes.object.isRequired,
|
|
||||||
options: PropTypes.object,
|
|
||||||
lastPostCount: PropTypes.number
|
|
||||||
};
|
|
||||||
|
|
||||||
static defaultProps = {
|
|
||||||
options: {}
|
|
||||||
};
|
|
||||||
|
|
||||||
constructor(props) {
|
|
||||||
super(props);
|
|
||||||
|
|
||||||
this.onEmojiChange = this.onEmojiChange.bind(this);
|
|
||||||
this.onPreferenceChange = this.onPreferenceChange.bind(this);
|
|
||||||
this.onUserChange = this.onUserChange.bind(this);
|
|
||||||
this.onChannelChange = this.onChannelChange.bind(this);
|
|
||||||
|
|
||||||
const mentionKeys = UserStore.getCurrentMentionKeys();
|
|
||||||
mentionKeys.push('@here');
|
|
||||||
|
|
||||||
this.state = {
|
|
||||||
emojis: EmojiStore.getEmojis(),
|
|
||||||
enableFormatting: PreferenceStore.getBool(Preferences.CATEGORY_ADVANCED_SETTINGS, 'formatting', true),
|
|
||||||
mentionKeys,
|
|
||||||
usernameMap: UserStore.getProfilesUsernameMap(),
|
|
||||||
channelNamesMap: ChannelStore.getChannelNamesMap(),
|
|
||||||
team: TeamStore.getCurrent()
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidMount() {
|
|
||||||
EmojiStore.addChangeListener(this.onEmojiChange);
|
|
||||||
PreferenceStore.addChangeListener(this.onPreferenceChange);
|
|
||||||
UserStore.addChangeListener(this.onUserChange);
|
|
||||||
ChannelStore.addChangeListener(this.onChannelChange);
|
|
||||||
}
|
|
||||||
|
|
||||||
componentWillUnmount() {
|
|
||||||
EmojiStore.removeChangeListener(this.onEmojiChange);
|
|
||||||
PreferenceStore.removeChangeListener(this.onPreferenceChange);
|
|
||||||
UserStore.removeChangeListener(this.onUserChange);
|
|
||||||
ChannelStore.removeChangeListener(this.onChannelChange);
|
|
||||||
}
|
|
||||||
|
|
||||||
onEmojiChange() {
|
|
||||||
this.setState({
|
|
||||||
emojis: EmojiStore.getEmojis()
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
onPreferenceChange() {
|
|
||||||
this.setState({
|
|
||||||
enableFormatting: PreferenceStore.getBool(Preferences.CATEGORY_ADVANCED_SETTINGS, 'formatting', true)
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
onUserChange() {
|
|
||||||
const mentionKeys = UserStore.getCurrentMentionKeys();
|
|
||||||
mentionKeys.push('@here');
|
|
||||||
|
|
||||||
this.setState({
|
|
||||||
mentionKeys,
|
|
||||||
usernameMap: UserStore.getProfilesUsernameMap()
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
onChannelChange() {
|
|
||||||
this.setState({
|
|
||||||
channelNamesMap: ChannelStore.getChannelNamesMap()
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
return (
|
|
||||||
<PostMessageView
|
|
||||||
options={this.props.options}
|
|
||||||
post={this.props.post}
|
|
||||||
lastPostCount={this.props.lastPostCount}
|
|
||||||
emojis={this.state.emojis}
|
|
||||||
enableFormatting={this.state.enableFormatting}
|
|
||||||
mentionKeys={this.state.mentionKeys}
|
|
||||||
usernameMap={this.state.usernameMap}
|
|
||||||
channelNamesMap={this.state.channelNamesMap}
|
|
||||||
team={this.state.team}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,90 +0,0 @@
|
|||||||
import PropTypes from 'prop-types';
|
|
||||||
|
|
||||||
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
|
|
||||||
// See License.txt for license information.
|
|
||||||
|
|
||||||
import React from 'react';
|
|
||||||
|
|
||||||
import {addReaction, removeReaction} from 'actions/post_actions.jsx';
|
|
||||||
import * as UserActions from 'actions/user_actions.jsx';
|
|
||||||
|
|
||||||
import UserStore from 'stores/user_store.jsx';
|
|
||||||
|
|
||||||
import Reaction from './reaction.jsx';
|
|
||||||
|
|
||||||
export default class ReactionContainer extends React.Component {
|
|
||||||
static propTypes = {
|
|
||||||
post: PropTypes.object.isRequired,
|
|
||||||
emojiName: PropTypes.string.isRequired,
|
|
||||||
reactions: PropTypes.arrayOf(PropTypes.object),
|
|
||||||
emojis: PropTypes.object.isRequired
|
|
||||||
}
|
|
||||||
|
|
||||||
constructor(props) {
|
|
||||||
super(props);
|
|
||||||
|
|
||||||
this.handleUsersChanged = this.handleUsersChanged.bind(this);
|
|
||||||
|
|
||||||
this.getStateFromStore = this.getStateFromStore.bind(this);
|
|
||||||
|
|
||||||
this.getProfilesForReactions = this.getProfilesForReactions.bind(this);
|
|
||||||
this.getMissingProfiles = this.getMissingProfiles.bind(this);
|
|
||||||
|
|
||||||
this.state = this.getStateFromStore(props);
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidMount() {
|
|
||||||
UserStore.addChangeListener(this.handleUsersChanged);
|
|
||||||
}
|
|
||||||
|
|
||||||
componentWillReceiveProps(nextProps) {
|
|
||||||
if (nextProps.reactions !== this.props.reactions) {
|
|
||||||
this.setState(this.getStateFromStore(nextProps));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
componentWillUnmount() {
|
|
||||||
UserStore.removeChangeListener(this.handleUsersChanged);
|
|
||||||
}
|
|
||||||
|
|
||||||
handleUsersChanged() {
|
|
||||||
this.setState(this.getStateFromStore());
|
|
||||||
}
|
|
||||||
|
|
||||||
getStateFromStore(props = this.props) {
|
|
||||||
const profiles = this.getProfilesForReactions(props.reactions);
|
|
||||||
const otherUsers = props.reactions.length - profiles.length;
|
|
||||||
|
|
||||||
return {
|
|
||||||
profiles,
|
|
||||||
otherUsers,
|
|
||||||
currentUserId: UserStore.getCurrentId()
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
getProfilesForReactions(reactions) {
|
|
||||||
return reactions.map((reaction) => {
|
|
||||||
return UserStore.getProfile(reaction.user_id);
|
|
||||||
}).filter((profile) => Boolean(profile));
|
|
||||||
}
|
|
||||||
|
|
||||||
getMissingProfiles() {
|
|
||||||
const ids = this.props.reactions.map((reaction) => reaction.user_id);
|
|
||||||
|
|
||||||
UserActions.getMissingProfiles(ids);
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
return (
|
|
||||||
<Reaction
|
|
||||||
{...this.props}
|
|
||||||
{...this.state}
|
|
||||||
actions={{
|
|
||||||
addReaction,
|
|
||||||
getMissingProfiles: this.getMissingProfiles,
|
|
||||||
removeReaction
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,94 +0,0 @@
|
|||||||
import PropTypes from 'prop-types';
|
|
||||||
|
|
||||||
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
|
|
||||||
// See License.txt for license information.
|
|
||||||
|
|
||||||
import React from 'react';
|
|
||||||
|
|
||||||
import * as AsyncClient from 'utils/async_client.jsx';
|
|
||||||
import EmojiStore from 'stores/emoji_store.jsx';
|
|
||||||
import ReactionStore from 'stores/reaction_store.jsx';
|
|
||||||
|
|
||||||
import ReactionListView from './reaction_list_view.jsx';
|
|
||||||
|
|
||||||
export default class ReactionListContainer extends React.Component {
|
|
||||||
static propTypes = {
|
|
||||||
post: PropTypes.object.isRequired
|
|
||||||
}
|
|
||||||
|
|
||||||
constructor(props) {
|
|
||||||
super(props);
|
|
||||||
|
|
||||||
this.handleReactionsChanged = this.handleReactionsChanged.bind(this);
|
|
||||||
this.handleEmojisChanged = this.handleEmojisChanged.bind(this);
|
|
||||||
|
|
||||||
this.state = {
|
|
||||||
reactions: ReactionStore.getReactions(this.props.post.id),
|
|
||||||
emojis: EmojiStore.getEmojis()
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidMount() {
|
|
||||||
ReactionStore.addChangeListener(this.props.post.id, this.handleReactionsChanged);
|
|
||||||
EmojiStore.addChangeListener(this.handleEmojisChanged);
|
|
||||||
|
|
||||||
if (this.props.post.has_reactions) {
|
|
||||||
AsyncClient.listReactions(this.props.post.channel_id, this.props.post.id);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
componentWillReceiveProps(nextProps) {
|
|
||||||
if (nextProps.post.id !== this.props.post.id) {
|
|
||||||
ReactionStore.removeChangeListener(this.props.post.id, this.handleReactionsChanged);
|
|
||||||
ReactionStore.addChangeListener(nextProps.post.id, this.handleReactionsChanged);
|
|
||||||
|
|
||||||
this.setState({
|
|
||||||
reactions: ReactionStore.getReactions(nextProps.post.id)
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
shouldComponentUpdate(nextProps, nextState) {
|
|
||||||
if (nextProps.post.has_reactions !== this.props.post.has_reactions) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (nextState.reactions !== this.state.reactions) {
|
|
||||||
// this will only work so long as the entries in the ReactionStore are never mutated
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (nextState.emojis !== this.state.emojis) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
componentWillUnmount() {
|
|
||||||
ReactionStore.removeChangeListener(this.props.post.id, this.handleReactionsChanged);
|
|
||||||
EmojiStore.removeChangeListener(this.handleEmojisChanged);
|
|
||||||
}
|
|
||||||
|
|
||||||
handleReactionsChanged() {
|
|
||||||
this.setState({
|
|
||||||
reactions: ReactionStore.getReactions(this.props.post.id)
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
handleEmojisChanged() {
|
|
||||||
this.setState({
|
|
||||||
emojis: EmojiStore.getEmojis()
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
return (
|
|
||||||
<ReactionListView
|
|
||||||
post={this.props.post}
|
|
||||||
reactions={this.state.reactions}
|
|
||||||
emojis={this.state.emojis}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
32
webapp/components/post_view/date_separator.jsx
Обычный файл
32
webapp/components/post_view/date_separator.jsx
Обычный файл
@@ -0,0 +1,32 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import {FormattedDate} from 'react-intl';
|
||||||
|
|
||||||
|
export default class DateSeparator extends React.PureComponent {
|
||||||
|
static propTypes = {
|
||||||
|
|
||||||
|
/*
|
||||||
|
* The date to display in the separator
|
||||||
|
*/
|
||||||
|
date: PropTypes.instanceOf(Date)
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className='date-separator'
|
||||||
|
>
|
||||||
|
<hr className='separator__hr'/>
|
||||||
|
<div className='separator__text'>
|
||||||
|
<FormattedDate
|
||||||
|
value={this.props.date}
|
||||||
|
weekday='short'
|
||||||
|
month='short'
|
||||||
|
day='2-digit'
|
||||||
|
year='numeric'
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,19 +1,28 @@
|
|||||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||||
// See License.txt for license information.
|
// See License.txt for license information.
|
||||||
|
|
||||||
import PostStore from 'stores/post_store.jsx';
|
import {createPost} from 'actions/post_actions.jsx';
|
||||||
|
|
||||||
import {queuePost} from 'actions/post_actions.jsx';
|
|
||||||
|
|
||||||
import Constants from 'utils/constants.jsx';
|
|
||||||
|
|
||||||
import {FormattedMessage} from 'react-intl';
|
|
||||||
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import {FormattedMessage} from 'react-intl';
|
||||||
|
|
||||||
|
export default class FailedPostOptions extends React.Component {
|
||||||
|
static propTypes = {
|
||||||
|
|
||||||
|
/*
|
||||||
|
* The failed post
|
||||||
|
*/
|
||||||
|
post: PropTypes.object.isRequired,
|
||||||
|
actions: PropTypes.shape({
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The function to delete the post
|
||||||
|
*/
|
||||||
|
removePost: PropTypes.func.isRequired
|
||||||
|
}).isRequired
|
||||||
|
}
|
||||||
|
|
||||||
export default class PendingPostOptions extends React.Component {
|
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
|
|
||||||
@@ -24,6 +33,7 @@ export default class PendingPostOptions extends React.Component {
|
|||||||
|
|
||||||
this.state = {};
|
this.state = {};
|
||||||
}
|
}
|
||||||
|
|
||||||
retryPost(e) {
|
retryPost(e) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
||||||
@@ -33,8 +43,12 @@ export default class PendingPostOptions extends React.Component {
|
|||||||
|
|
||||||
this.submitting = true;
|
this.submitting = true;
|
||||||
|
|
||||||
var post = this.props.post;
|
const post = {...this.props.post};
|
||||||
queuePost(post, true, null,
|
Reflect.deleteProperty(post, 'id');
|
||||||
|
createPost(post,
|
||||||
|
() => {
|
||||||
|
this.submitting = false;
|
||||||
|
},
|
||||||
(err) => {
|
(err) => {
|
||||||
if (err.id === 'api.post.create_post.root_id.app_error') {
|
if (err.id === 'api.post.create_post.root_id.app_error') {
|
||||||
this.showPostDeletedModal();
|
this.showPostDeletedModal();
|
||||||
@@ -45,18 +59,13 @@ export default class PendingPostOptions extends React.Component {
|
|||||||
this.submitting = false;
|
this.submitting = false;
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
post.state = Constants.POST_LOADING;
|
|
||||||
PostStore.updatePendingPost(post);
|
|
||||||
this.forceUpdate();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
cancelPost(e) {
|
cancelPost(e) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
this.props.actions.removePost(this.props.post);
|
||||||
var post = this.props.post;
|
|
||||||
PostStore.removePendingPost(post.channel_id, post.pending_post_id);
|
|
||||||
this.forceUpdate();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
return (<span className='pending-post-actions'>
|
return (<span className='pending-post-actions'>
|
||||||
<a
|
<a
|
||||||
@@ -83,7 +92,3 @@ export default class PendingPostOptions extends React.Component {
|
|||||||
</span>);
|
</span>);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
PendingPostOptions.propTypes = {
|
|
||||||
post: PropTypes.object
|
|
||||||
};
|
|
||||||
24
webapp/components/post_view/failed_post_options/index.js
Обычный файл
24
webapp/components/post_view/failed_post_options/index.js
Обычный файл
@@ -0,0 +1,24 @@
|
|||||||
|
// Copyright (c) 2017 Mattermost, Inc. All Rights Reserved.
|
||||||
|
// See License.txt for license information.
|
||||||
|
|
||||||
|
import {connect} from 'react-redux';
|
||||||
|
import {bindActionCreators} from 'redux';
|
||||||
|
import {removePost} from 'mattermost-redux/actions/posts';
|
||||||
|
|
||||||
|
import FailedPostOptions from './failed_post_options.jsx';
|
||||||
|
|
||||||
|
function mapStateToProps(state, ownProps) {
|
||||||
|
return {
|
||||||
|
...ownProps
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function mapDispatchToProps(dispatch) {
|
||||||
|
return {
|
||||||
|
actions: bindActionCreators({
|
||||||
|
removePost
|
||||||
|
}, dispatch)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export default connect(mapStateToProps, mapDispatchToProps)(FailedPostOptions);
|
||||||
@@ -3,16 +3,15 @@
|
|||||||
|
|
||||||
import {FormattedDate} from 'react-intl';
|
import {FormattedDate} from 'react-intl';
|
||||||
|
|
||||||
|
import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
import React from 'react';
|
export default class FloatingTimestamp extends React.PureComponent {
|
||||||
import PureRenderMixin from 'react-addons-pure-render-mixin';
|
static propTypes = {
|
||||||
|
isScrolling: PropTypes.bool.isRequired,
|
||||||
export default class FloatingTimestamp extends React.Component {
|
isMobile: PropTypes.bool,
|
||||||
constructor(props) {
|
createAt: PropTypes.number,
|
||||||
super(props);
|
isRhsPost: PropTypes.bool
|
||||||
|
|
||||||
this.shouldComponentUpdate = PureRenderMixin.shouldComponentUpdate.bind(this);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
@@ -52,10 +51,3 @@ export default class FloatingTimestamp extends React.Component {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
FloatingTimestamp.propTypes = {
|
|
||||||
isScrolling: PropTypes.bool.isRequired,
|
|
||||||
isMobile: PropTypes.bool,
|
|
||||||
createAt: PropTypes.number,
|
|
||||||
isRhsPost: PropTypes.bool
|
|
||||||
};
|
|
||||||
@@ -3,22 +3,52 @@
|
|||||||
|
|
||||||
import {connect} from 'react-redux';
|
import {connect} from 'react-redux';
|
||||||
import {bindActionCreators} from 'redux';
|
import {bindActionCreators} from 'redux';
|
||||||
import {viewChannel} from 'mattermost-redux/actions/channels';
|
|
||||||
|
|
||||||
import PostViewCache from './post_view_cache.jsx';
|
import {makeGetPostsInChannel, makeGetPostsAroundPost} from 'mattermost-redux/selectors/entities/posts';
|
||||||
|
import {get} from 'mattermost-redux/selectors/entities/preferences';
|
||||||
|
import {getChannel} from 'mattermost-redux/selectors/entities/channels';
|
||||||
|
import {getCurrentUserId} from 'mattermost-redux/selectors/entities/users';
|
||||||
|
import {getPosts, getPostsBefore, getPostsAfter, getPostThread} from 'mattermost-redux/actions/posts';
|
||||||
|
import {increasePostVisibility} from 'actions/post_actions.jsx';
|
||||||
|
import {Preferences} from 'utils/constants.jsx';
|
||||||
|
|
||||||
function mapStateToProps(state, ownProps) {
|
import PostList from './post_list.jsx';
|
||||||
return {
|
|
||||||
...ownProps
|
function makeMapStateToProps() {
|
||||||
|
const getPostsInChannel = makeGetPostsInChannel();
|
||||||
|
const getPostsAroundPost = makeGetPostsAroundPost();
|
||||||
|
|
||||||
|
return function mapStateToProps(state, ownProps) {
|
||||||
|
let posts;
|
||||||
|
if (ownProps.focusedPostId) {
|
||||||
|
posts = getPostsAroundPost(state, ownProps.focusedPostId, ownProps.channelId);
|
||||||
|
} else {
|
||||||
|
posts = getPostsInChannel(state, ownProps.channelId);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
channel: getChannel(state, ownProps.channelId),
|
||||||
|
lastViewedAt: state.views.channel.lastChannelViewTime[ownProps.channelId],
|
||||||
|
posts,
|
||||||
|
postVisibility: state.views.channel.postVisibility[ownProps.channelId],
|
||||||
|
loadingPosts: state.views.channel.loadingPosts[ownProps.channelId],
|
||||||
|
focusedPostId: ownProps.focusedPostId,
|
||||||
|
currentUserId: getCurrentUserId(state),
|
||||||
|
fullWidth: get(state, Preferences.CATEGORY_DISPLAY_SETTINGS, Preferences.CHANNEL_DISPLAY_MODE, Preferences.CHANNEL_DISPLAY_MODE_DEFAULT) === Preferences.CHANNEL_DISPLAY_MODE_FULL_SCREEN
|
||||||
|
};
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function mapDispatchToProps(dispatch) {
|
function mapDispatchToProps(dispatch) {
|
||||||
return {
|
return {
|
||||||
actions: bindActionCreators({
|
actions: bindActionCreators({
|
||||||
viewChannel
|
getPosts,
|
||||||
|
getPostsBefore,
|
||||||
|
getPostsAfter,
|
||||||
|
getPostThread,
|
||||||
|
increasePostVisibility
|
||||||
}, dispatch)
|
}, dispatch)
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export default connect(mapStateToProps, mapDispatchToProps)(PostViewCache);
|
export default connect(makeMapStateToProps, mapDispatchToProps)(PostList);
|
||||||
|
|||||||
@@ -1,11 +1,16 @@
|
|||||||
import PropTypes from 'prop-types';
|
|
||||||
|
|
||||||
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
|
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
|
||||||
// See License.txt for license information.
|
// See License.txt for license information.
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
import {FormattedMessage} from 'react-intl';
|
import {FormattedMessage} from 'react-intl';
|
||||||
|
|
||||||
export default class NewMessageIndicator extends React.Component {
|
export default class NewMessageIndicator extends React.PureComponent {
|
||||||
|
static propTypes = {
|
||||||
|
onClick: PropTypes.func.isRequired,
|
||||||
|
newMessages: PropTypes.number
|
||||||
|
}
|
||||||
|
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
this.state = {
|
this.state = {
|
||||||
@@ -13,6 +18,7 @@ export default class NewMessageIndicator extends React.Component {
|
|||||||
rendered: false
|
rendered: false
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillReceiveProps(nextProps) {
|
componentWillReceiveProps(nextProps) {
|
||||||
if (nextProps.newMessages > 0) {
|
if (nextProps.newMessages > 0) {
|
||||||
this.setState({rendered: true}, () => {
|
this.setState({rendered: true}, () => {
|
||||||
@@ -22,6 +28,7 @@ export default class NewMessageIndicator extends React.Component {
|
|||||||
this.setState({visible: false});
|
this.setState({visible: false});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
let className = 'new-messages__button';
|
let className = 'new-messages__button';
|
||||||
if (this.state.visible > 0) {
|
if (this.state.visible > 0) {
|
||||||
@@ -56,11 +63,7 @@ export default class NewMessageIndicator extends React.Component {
|
|||||||
this.setState({rendered: this.state.visible});
|
this.setState({rendered: this.state.visible});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
NewMessageIndicator.defaultProps = {
|
NewMessageIndicator.defaultProps = {
|
||||||
newMessages: 0
|
newMessages: 0
|
||||||
};
|
};
|
||||||
|
|
||||||
NewMessageIndicator.propTypes = {
|
|
||||||
onClick: PropTypes.func.isRequired,
|
|
||||||
newMessages: PropTypes.number
|
|
||||||
};
|
|
||||||
33
webapp/components/post_view/post/index.js
Обычный файл
33
webapp/components/post_view/post/index.js
Обычный файл
@@ -0,0 +1,33 @@
|
|||||||
|
// Copyright (c) 2017 Mattermost, Inc. All Rights Reserved.
|
||||||
|
// See License.txt for license information.
|
||||||
|
|
||||||
|
import {connect} from 'react-redux';
|
||||||
|
|
||||||
|
import {getCurrentUser, getUser, getStatusForUserId} from 'mattermost-redux/selectors/entities/users';
|
||||||
|
import {get} from 'mattermost-redux/selectors/entities/preferences';
|
||||||
|
import {getPost} from 'mattermost-redux/selectors/entities/posts';
|
||||||
|
|
||||||
|
import {Preferences} from 'utils/constants.jsx';
|
||||||
|
|
||||||
|
import Post from './post.jsx';
|
||||||
|
|
||||||
|
function mapStateToProps(state, ownProps) {
|
||||||
|
const detailedPost = ownProps.post;
|
||||||
|
return {
|
||||||
|
post: getPost(state, detailedPost.id),
|
||||||
|
lastPostCount: ownProps.lastPostCount,
|
||||||
|
user: getUser(state, ownProps.post.user_id),
|
||||||
|
status: getStatusForUserId(state, ownProps.post.user_id),
|
||||||
|
currentUser: getCurrentUser(state),
|
||||||
|
isFirstReply: Boolean(detailedPost.isFirstReply && detailedPost.commentedOnPost),
|
||||||
|
highlight: detailedPost.highlight,
|
||||||
|
consecutivePostByUser: detailedPost.consecutivePostByUser,
|
||||||
|
previousPostIsComment: detailedPost.previousPostIsComment,
|
||||||
|
replyCount: detailedPost.replyCount,
|
||||||
|
isCommentMention: detailedPost.isCommentMention,
|
||||||
|
center: get(state, Preferences.CATEGORY_DISPLAY_SETTINGS, Preferences.CHANNEL_DISPLAY_MODE, Preferences.CHANNEL_DISPLAY_MODE_DEFAULT) === Preferences.CHANNEL_DISPLAY_MODE_CENTERED,
|
||||||
|
compactDisplay: get(state, Preferences.CATEGORY_DISPLAY_SETTINGS, Preferences.MESSAGE_DISPLAY, Preferences.MESSAGE_DISPLAY_DEFAULT) === Preferences.MESSAGE_DISPLAY_COMPACT
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export default connect(mapStateToProps)(Post);
|
||||||
@@ -1,46 +1,99 @@
|
|||||||
import PropTypes from 'prop-types';
|
|
||||||
|
|
||||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||||
// See License.txt for license information.
|
// See License.txt for license information.
|
||||||
|
|
||||||
import React, {Component} from 'react';
|
import PostHeader from 'components/post_view/post_header';
|
||||||
|
import PostBody from 'components/post_view/post_body';
|
||||||
import ProfilePicture from 'components/profile_picture.jsx';
|
import ProfilePicture from 'components/profile_picture.jsx';
|
||||||
|
|
||||||
|
import Constants from 'utils/constants.jsx';
|
||||||
|
const ActionTypes = Constants.ActionTypes;
|
||||||
|
import {Posts} from 'mattermost-redux/constants';
|
||||||
|
|
||||||
|
import * as Utils from 'utils/utils.jsx';
|
||||||
|
import * as PostUtils from 'utils/post_utils.jsx';
|
||||||
import AppDispatcher from 'dispatcher/app_dispatcher.jsx';
|
import AppDispatcher from 'dispatcher/app_dispatcher.jsx';
|
||||||
|
|
||||||
import Constants, {ActionTypes} from 'utils/constants.jsx';
|
import React from 'react';
|
||||||
import * as PostUtils from 'utils/post_utils.jsx';
|
import PropTypes from 'prop-types';
|
||||||
import * as Utils from 'utils/utils.jsx';
|
|
||||||
|
|
||||||
import PostBody from './post_body.jsx';
|
export default class Post extends React.PureComponent {
|
||||||
import PostHeader from './post_header.jsx';
|
|
||||||
|
|
||||||
export default class Post extends Component {
|
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The post to render
|
||||||
|
*/
|
||||||
post: PropTypes.object.isRequired,
|
post: PropTypes.object.isRequired,
|
||||||
parentPost: PropTypes.object,
|
|
||||||
|
/**
|
||||||
|
* The user who created the post
|
||||||
|
*/
|
||||||
user: PropTypes.object,
|
user: PropTypes.object,
|
||||||
sameUser: PropTypes.bool,
|
|
||||||
sameRoot: PropTypes.bool,
|
/**
|
||||||
hideProfilePic: PropTypes.bool,
|
* The status of the poster
|
||||||
lastPostCount: PropTypes.number,
|
*/
|
||||||
isLastComment: PropTypes.bool,
|
|
||||||
shouldHighlight: PropTypes.bool,
|
|
||||||
displayNameType: PropTypes.string,
|
|
||||||
currentUser: PropTypes.object.isRequired,
|
|
||||||
center: PropTypes.bool,
|
|
||||||
compactDisplay: PropTypes.bool,
|
|
||||||
previewCollapsed: PropTypes.string,
|
|
||||||
commentCount: PropTypes.number,
|
|
||||||
isCommentMention: PropTypes.bool,
|
|
||||||
useMilitaryTime: PropTypes.bool.isRequired,
|
|
||||||
isFlagged: PropTypes.bool,
|
|
||||||
status: PropTypes.string,
|
status: PropTypes.string,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The logged in user
|
||||||
|
*/
|
||||||
|
currentUser: PropTypes.object.isRequired,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set to center the post
|
||||||
|
*/
|
||||||
|
center: PropTypes.bool,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set to render post compactly
|
||||||
|
*/
|
||||||
|
compactDisplay: PropTypes.bool,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set to render a preview of the parent post above this reply
|
||||||
|
*/
|
||||||
|
isFirstReply: PropTypes.bool,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set to highlight the background of the post
|
||||||
|
*/
|
||||||
|
highlight: PropTypes.bool,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set to render this post as if it was attached to the previous post
|
||||||
|
*/
|
||||||
|
consecutivePostByUser: PropTypes.bool,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set if the previous post is a comment
|
||||||
|
*/
|
||||||
|
previousPostIsComment: PropTypes.bool,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set to render this comment as a mention
|
||||||
|
*/
|
||||||
|
isCommentMention: PropTypes.bool,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The number of replies in the same thread as this post
|
||||||
|
*/
|
||||||
|
replyCount: PropTypes.number,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set to mark the poster as in a webrtc call
|
||||||
|
*/
|
||||||
isBusy: PropTypes.bool,
|
isBusy: PropTypes.bool,
|
||||||
childComponentDidUpdateFunction: PropTypes.func,
|
|
||||||
|
/**
|
||||||
|
* The post count used for selenium tests
|
||||||
|
*/
|
||||||
|
lastPostCount: PropTypes.number,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Function to get the post list HTML element
|
||||||
|
*/
|
||||||
getPostList: PropTypes.func.isRequired
|
getPostList: PropTypes.func.isRequired
|
||||||
};
|
}
|
||||||
|
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
@@ -75,91 +128,23 @@ export default class Post extends Component {
|
|||||||
this.refs.header.forceUpdate();
|
this.refs.header.forceUpdate();
|
||||||
}
|
}
|
||||||
|
|
||||||
shouldComponentUpdate(nextProps, nextState) {
|
|
||||||
if (!Utils.areObjectsEqual(nextProps.post, this.props.post)) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (nextProps.sameRoot !== this.props.sameRoot) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (nextProps.sameUser !== this.props.sameUser) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (nextProps.displayNameType !== this.props.displayNameType) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (nextProps.commentCount !== this.props.commentCount) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (nextProps.isCommentMention !== this.props.isCommentMention) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (nextProps.shouldHighlight !== this.props.shouldHighlight) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (nextProps.center !== this.props.center) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (nextProps.compactDisplay !== this.props.compactDisplay) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (nextProps.previewCollapsed !== this.props.previewCollapsed) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (nextProps.useMilitaryTime !== this.props.useMilitaryTime) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (nextProps.isFlagged !== this.props.isFlagged) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (nextProps.status !== this.props.status) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!Utils.areObjectsEqual(nextProps.user, this.props.user)) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (nextState.dropdownOpened !== this.state.dropdownOpened) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (nextProps.isBusy !== this.props.isBusy) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (nextProps.lastPostCount !== this.props.lastPostCount) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
getClassName = (post, isSystemMessage, fromWebhook) => {
|
getClassName = (post, isSystemMessage, fromWebhook) => {
|
||||||
let className = 'post';
|
let className = 'post';
|
||||||
|
|
||||||
if (post.state === Constants.POST_DELETED || post.state === Constants.POST_FAILED) {
|
if (post.failed || post.state === Posts.POST_DELETED) {
|
||||||
className += ' post--hide-controls';
|
className += ' post--hide-controls';
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.props.shouldHighlight) {
|
if (this.props.highlight) {
|
||||||
className += ' post--highlight';
|
className += ' post--highlight';
|
||||||
}
|
}
|
||||||
|
|
||||||
let rootUser;
|
let rootUser = '';
|
||||||
if (this.props.sameRoot) {
|
if (this.props.isFirstReply) {
|
||||||
|
rootUser = 'other--root';
|
||||||
|
} else if (!post.root_id && !this.props.previousPostIsComment && this.props.consecutivePostByUser) {
|
||||||
|
rootUser = 'same--root';
|
||||||
|
} else if (post.root_id) {
|
||||||
rootUser = 'same--root';
|
rootUser = 'same--root';
|
||||||
} else {
|
} else {
|
||||||
rootUser = 'other--root';
|
rootUser = 'other--root';
|
||||||
@@ -171,14 +156,14 @@ export default class Post extends Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let sameUserClass = '';
|
let sameUserClass = '';
|
||||||
if (this.props.sameUser) {
|
if (this.props.consecutivePostByUser) {
|
||||||
sameUserClass = 'same--user';
|
sameUserClass = 'same--user';
|
||||||
}
|
}
|
||||||
|
|
||||||
let postType = '';
|
let postType = '';
|
||||||
if (post.root_id && post.root_id.length > 0) {
|
if (post.root_id && post.root_id.length > 0) {
|
||||||
postType = 'post--comment';
|
postType = 'post--comment';
|
||||||
} else if (this.props.commentCount > 0) {
|
} else if (this.props.replyCount > 0) {
|
||||||
postType = 'post--root';
|
postType = 'post--root';
|
||||||
sameUserClass = '';
|
sameUserClass = '';
|
||||||
rootUser = '';
|
rootUser = '';
|
||||||
@@ -209,7 +194,6 @@ export default class Post extends Component {
|
|||||||
|
|
||||||
render() {
|
render() {
|
||||||
const post = this.props.post;
|
const post = this.props.post;
|
||||||
const parentPost = this.props.parentPost;
|
|
||||||
const mattermostLogo = Constants.MATTERMOST_ICON_SVG;
|
const mattermostLogo = Constants.MATTERMOST_ICON_SVG;
|
||||||
|
|
||||||
const isSystemMessage = PostUtils.isSystemMessage(post);
|
const isSystemMessage = PostUtils.isSystemMessage(post);
|
||||||
@@ -242,7 +226,7 @@ export default class Post extends Component {
|
|||||||
src={PostUtils.getProfilePicSrcForPost(post, timestamp)}
|
src={PostUtils.getProfilePicSrcForPost(post, timestamp)}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
} else if (isSystemMessage) {
|
} else if (PostUtils.isSystemMessage(post)) {
|
||||||
profilePic = (
|
profilePic = (
|
||||||
<span
|
<span
|
||||||
className='icon'
|
className='icon'
|
||||||
@@ -257,7 +241,7 @@ export default class Post extends Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (this.props.compactDisplay) {
|
if (this.props.compactDisplay) {
|
||||||
if (post.props && post.props.from_webhook) {
|
if (fromWebhook) {
|
||||||
profilePic = (
|
profilePic = (
|
||||||
<ProfilePicture
|
<ProfilePicture
|
||||||
src=''
|
src=''
|
||||||
@@ -294,34 +278,24 @@ export default class Post extends Component {
|
|||||||
<PostHeader
|
<PostHeader
|
||||||
ref='header'
|
ref='header'
|
||||||
post={post}
|
post={post}
|
||||||
sameRoot={this.props.sameRoot}
|
|
||||||
lastPostCount={this.props.lastPostCount}
|
|
||||||
commentCount={this.props.commentCount}
|
|
||||||
handleCommentClick={this.handleCommentClick}
|
handleCommentClick={this.handleCommentClick}
|
||||||
handleDropdownOpened={this.handleDropdownOpened}
|
handleDropdownOpened={this.handleDropdownOpened}
|
||||||
isLastComment={this.props.isLastComment}
|
|
||||||
sameUser={this.props.sameUser}
|
|
||||||
user={this.props.user}
|
user={this.props.user}
|
||||||
currentUser={this.props.currentUser}
|
currentUser={this.props.currentUser}
|
||||||
compactDisplay={this.props.compactDisplay}
|
compactDisplay={this.props.compactDisplay}
|
||||||
displayNameType={this.props.displayNameType}
|
|
||||||
useMilitaryTime={this.props.useMilitaryTime}
|
|
||||||
isFlagged={this.props.isFlagged}
|
|
||||||
status={this.props.status}
|
status={this.props.status}
|
||||||
isBusy={this.props.isBusy}
|
isBusy={this.props.isBusy}
|
||||||
|
lastPostCount={this.props.lastPostCount}
|
||||||
|
replyCount={this.props.replyCount}
|
||||||
|
consecutivePostByUser={this.props.consecutivePostByUser}
|
||||||
getPostList={this.props.getPostList}
|
getPostList={this.props.getPostList}
|
||||||
/>
|
/>
|
||||||
<PostBody
|
<PostBody
|
||||||
post={post}
|
post={post}
|
||||||
currentUser={this.props.currentUser}
|
|
||||||
sameRoot={this.props.sameRoot}
|
|
||||||
lastPostCount={this.props.lastPostCount}
|
|
||||||
parentPost={parentPost}
|
|
||||||
handleCommentClick={this.handleCommentClick}
|
handleCommentClick={this.handleCommentClick}
|
||||||
compactDisplay={this.props.compactDisplay}
|
compactDisplay={this.props.compactDisplay}
|
||||||
previewCollapsed={this.props.previewCollapsed}
|
lastPostCount={this.props.lastPostCount}
|
||||||
isCommentMention={this.props.isCommentMention}
|
isCommentMention={this.props.isCommentMention}
|
||||||
childComponentDidUpdateFunction={this.props.childComponentDidUpdateFunction}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -1,27 +1,22 @@
|
|||||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||||
// See License.txt for license information.
|
// See License.txt for license information.
|
||||||
|
|
||||||
import $ from 'jquery';
|
|
||||||
import * as TextFormatting from 'utils/text_formatting.jsx';
|
import * as TextFormatting from 'utils/text_formatting.jsx';
|
||||||
|
import {localizeMessage} from 'utils/utils.jsx';
|
||||||
|
|
||||||
import {intlShape, injectIntl, defineMessages} from 'react-intl';
|
import $ from 'jquery';
|
||||||
|
import React from 'react';
|
||||||
const holders = defineMessages({
|
|
||||||
collapse: {
|
|
||||||
id: 'post_attachment.collapse',
|
|
||||||
defaultMessage: 'Show less...'
|
|
||||||
},
|
|
||||||
more: {
|
|
||||||
id: 'post_attachment.more',
|
|
||||||
defaultMessage: 'Show more...'
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
import React from 'react';
|
export default class PostAttachment extends React.PureComponent {
|
||||||
|
static propTypes = {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The attachment to render
|
||||||
|
*/
|
||||||
|
attachment: PropTypes.object.isRequired
|
||||||
|
}
|
||||||
|
|
||||||
class PostAttachment extends React.Component {
|
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
|
|
||||||
@@ -46,7 +41,7 @@ class PostAttachment extends React.Component {
|
|||||||
getInitState() {
|
getInitState() {
|
||||||
const shouldCollapse = this.shouldCollapse();
|
const shouldCollapse = this.shouldCollapse();
|
||||||
const text = TextFormatting.formatText(this.props.attachment.text || '');
|
const text = TextFormatting.formatText(this.props.attachment.text || '');
|
||||||
const uncollapsedText = text + (shouldCollapse ? `<div><a class="attachment-link-more" href="#">${this.props.intl.formatMessage(holders.collapse)}</a></div>` : '');
|
const uncollapsedText = text + (shouldCollapse ? `<div><a class="attachment-link-more" href="#">${localizeMessage('post_attachment.collapse', 'Show less...')}</a></div>` : '');
|
||||||
const collapsedText = shouldCollapse ? this.getCollapsedText() : text;
|
const collapsedText = shouldCollapse ? this.getCollapsedText() : text;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -61,10 +56,10 @@ class PostAttachment extends React.Component {
|
|||||||
toggleCollapseState(e) {
|
toggleCollapseState(e) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
||||||
const state = this.state;
|
this.setState({
|
||||||
state.text = state.collapsed ? state.uncollapsedText : state.collapsedText;
|
text: this.state.collapsed ? this.state.uncollapsedText : this.state.collapsedText,
|
||||||
state.collapsed = !state.collapsed;
|
collapsed: !this.state.collapsed
|
||||||
this.setState(state);
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
shouldCollapse() {
|
shouldCollapse() {
|
||||||
@@ -80,7 +75,7 @@ class PostAttachment extends React.Component {
|
|||||||
text = text.substr(0, 700);
|
text = text.substr(0, 700);
|
||||||
}
|
}
|
||||||
|
|
||||||
return TextFormatting.formatText(text) + `<div><a class="attachment-link-more" href="#">${this.props.intl.formatMessage(holders.more)}</a></div>`;
|
return TextFormatting.formatText(text) + `<div><a class="attachment-link-more" href="#">${localizeMessage('post_attachment.more', 'Show more...')}</a></div>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
getFieldsTable() {
|
getFieldsTable() {
|
||||||
@@ -314,10 +309,3 @@ class PostAttachment extends React.Component {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
PostAttachment.propTypes = {
|
|
||||||
intl: intlShape.isRequired,
|
|
||||||
attachment: PropTypes.object.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
export default injectIntl(PostAttachment);
|
|
||||||
35
webapp/components/post_view/post_attachment_list.jsx
Обычный файл
35
webapp/components/post_view/post_attachment_list.jsx
Обычный файл
@@ -0,0 +1,35 @@
|
|||||||
|
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||||
|
// See License.txt for license information.
|
||||||
|
|
||||||
|
import PostAttachment from './post_attachment.jsx';
|
||||||
|
|
||||||
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
|
export default class PostAttachmentList extends React.PureComponent {
|
||||||
|
static propTypes = {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Array of attachments to render
|
||||||
|
*/
|
||||||
|
attachments: PropTypes.array.isRequired
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const content = [];
|
||||||
|
this.props.attachments.forEach((attachment, i) => {
|
||||||
|
content.push(
|
||||||
|
<PostAttachment
|
||||||
|
attachment={attachment}
|
||||||
|
key={'att_' + i}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='attachment_list'>
|
||||||
|
{content}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
26
webapp/components/post_view/post_attachment_opengraph/index.js
Обычный файл
26
webapp/components/post_view/post_attachment_opengraph/index.js
Обычный файл
@@ -0,0 +1,26 @@
|
|||||||
|
// Copyright (c) 2017 Mattermost, Inc. All Rights Reserved.
|
||||||
|
// See License.txt for license information.
|
||||||
|
|
||||||
|
import {connect} from 'react-redux';
|
||||||
|
import {bindActionCreators} from 'redux';
|
||||||
|
import {getOpenGraphMetadata} from 'mattermost-redux/actions/posts';
|
||||||
|
import {getOpenGraphMetadataForUrl} from 'mattermost-redux/selectors/entities/posts';
|
||||||
|
|
||||||
|
import PostAttachmentOpenGraph from './post_attachment_opengraph.jsx';
|
||||||
|
|
||||||
|
function mapStateToProps(state, ownProps) {
|
||||||
|
return {
|
||||||
|
...ownProps,
|
||||||
|
openGraphData: getOpenGraphMetadataForUrl(state, ownProps.link)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function mapDispatchToProps(dispatch) {
|
||||||
|
return {
|
||||||
|
actions: bindActionCreators({
|
||||||
|
getOpenGraphMetadata
|
||||||
|
}, dispatch)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export default connect(mapStateToProps, mapDispatchToProps)(PostAttachmentOpenGraph);
|
||||||
@@ -1,16 +1,38 @@
|
|||||||
import PropTypes from 'prop-types';
|
|
||||||
|
|
||||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||||
// See License.txt for license information.
|
// See License.txt for license information.
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
import OpenGraphStore from 'stores/opengraph_store.jsx';
|
|
||||||
import * as Utils from 'utils/utils.jsx';
|
import * as Utils from 'utils/utils.jsx';
|
||||||
import * as CommonUtils from 'utils/commons.jsx';
|
import * as CommonUtils from 'utils/commons.jsx';
|
||||||
import {requestOpenGraphMetadata} from 'actions/global_actions.jsx';
|
|
||||||
|
|
||||||
export default class PostAttachmentOpenGraph extends React.Component {
|
export default class PostAttachmentOpenGraph extends React.PureComponent {
|
||||||
|
static propTypes = {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The link to display the open graph data for
|
||||||
|
*/
|
||||||
|
link: PropTypes.string.isRequired,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The open graph data to render
|
||||||
|
*/
|
||||||
|
openGraphData: PropTypes.object.isRequired,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set to collapse the preview
|
||||||
|
*/
|
||||||
|
previewCollapsed: PropTypes.string,
|
||||||
|
actions: PropTypes.shape({
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The function to get open graph data for a link
|
||||||
|
*/
|
||||||
|
getOpenGraphMetadata: PropTypes.func.isRequired
|
||||||
|
}).isRequired
|
||||||
|
}
|
||||||
|
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
this.largeImageMinWidth = 150;
|
this.largeImageMinWidth = 150;
|
||||||
@@ -29,7 +51,6 @@ export default class PostAttachmentOpenGraph extends React.Component {
|
|||||||
this.smallImageElement = null;
|
this.smallImageElement = null;
|
||||||
|
|
||||||
this.fetchData = this.fetchData.bind(this);
|
this.fetchData = this.fetchData.bind(this);
|
||||||
this.onOpenGraphMetadataChange = this.onOpenGraphMetadataChange.bind(this);
|
|
||||||
this.toggleImageVisibility = this.toggleImageVisibility.bind(this);
|
this.toggleImageVisibility = this.toggleImageVisibility.bind(this);
|
||||||
this.onImageLoad = this.onImageLoad.bind(this);
|
this.onImageLoad = this.onImageLoad.bind(this);
|
||||||
this.onImageError = this.onImageError.bind(this);
|
this.onImageError = this.onImageError.bind(this);
|
||||||
@@ -44,7 +65,6 @@ export default class PostAttachmentOpenGraph extends React.Component {
|
|||||||
|
|
||||||
componentWillMount() {
|
componentWillMount() {
|
||||||
this.setState({
|
this.setState({
|
||||||
data: {},
|
|
||||||
imageLoaded: this.IMAGE_LOADED.LOADING,
|
imageLoaded: this.IMAGE_LOADED.LOADING,
|
||||||
imageVisible: this.props.previewCollapsed.startsWith('false'),
|
imageVisible: this.props.previewCollapsed.startsWith('false'),
|
||||||
hasLargeImage: false
|
hasLargeImage: false
|
||||||
@@ -53,61 +73,23 @@ export default class PostAttachmentOpenGraph extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
componentWillReceiveProps(nextProps) {
|
componentWillReceiveProps(nextProps) {
|
||||||
if (!Utils.areObjectsEqual(nextProps.link, this.props.link)) {
|
if (nextProps.link !== this.props.link) {
|
||||||
this.fetchData(nextProps.link);
|
this.fetchData(nextProps.link);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
shouldComponentUpdate(nextProps, nextState) {
|
|
||||||
if (nextState.imageVisible !== this.state.imageVisible) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
if (nextState.hasLargeImage !== this.state.hasLargeImage) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
if (nextState.imageLoaded !== this.state.imageLoaded) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
if (!Utils.areObjectsEqual(nextState.data, this.state.data)) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidMount() {
|
|
||||||
OpenGraphStore.addUrlDataChangeListener(this.onOpenGraphMetadataChange);
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidUpdate() {
|
|
||||||
if (this.props.childComponentDidUpdateFunction) {
|
|
||||||
this.props.childComponentDidUpdateFunction();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
componentWillUnmount() {
|
|
||||||
OpenGraphStore.removeUrlDataChangeListener(this.onOpenGraphMetadataChange);
|
|
||||||
}
|
|
||||||
|
|
||||||
onOpenGraphMetadataChange(url) {
|
|
||||||
if (url === this.props.link) {
|
|
||||||
this.fetchData(url);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fetchData(url) {
|
fetchData(url) {
|
||||||
const data = OpenGraphStore.getOgInfo(url);
|
if (!this.props.openGraphData) {
|
||||||
this.setState({data, imageLoaded: this.IMAGE_LOADED.LOADING});
|
this.props.actions.getOpenGraphMetadata(url);
|
||||||
if (Utils.isEmptyObject(data)) {
|
|
||||||
requestOpenGraphMetadata(url);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
getBestImageUrl() {
|
getBestImageUrl() {
|
||||||
if (Utils.isEmptyObject(this.state.data.images)) {
|
if (Utils.isEmptyObject(this.props.openGraphData.images)) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const bestImage = CommonUtils.getNearestPoint(this.imageDimentions, this.state.data.images, 'width', 'height');
|
const bestImage = CommonUtils.getNearestPoint(this.imageDimentions, this.props.openGraphData.images, 'width', 'height');
|
||||||
return bestImage.secure_url || bestImage.url;
|
return bestImage.secure_url || bestImage.url;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -217,11 +199,11 @@ export default class PostAttachmentOpenGraph extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
if (Utils.isEmptyObject(this.state.data) || Utils.isEmptyObject(this.state.data.description)) {
|
if (!this.props.openGraphData || Utils.isEmptyObject(this.props.openGraphData.description)) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const data = this.state.data;
|
const data = this.props.openGraphData;
|
||||||
const imageUrl = this.getBestImageUrl();
|
const imageUrl = this.getBestImageUrl();
|
||||||
|
|
||||||
if (imageUrl) {
|
if (imageUrl) {
|
||||||
@@ -275,13 +257,3 @@ export default class PostAttachmentOpenGraph extends React.Component {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
PostAttachmentOpenGraph.defaultProps = {
|
|
||||||
previewCollapsed: 'false'
|
|
||||||
};
|
|
||||||
|
|
||||||
PostAttachmentOpenGraph.propTypes = {
|
|
||||||
link: PropTypes.string.isRequired,
|
|
||||||
childComponentDidUpdateFunction: PropTypes.func,
|
|
||||||
previewCollapsed: PropTypes.string
|
|
||||||
};
|
|
||||||
30
webapp/components/post_view/post_body/index.js
Обычный файл
30
webapp/components/post_view/post_body/index.js
Обычный файл
@@ -0,0 +1,30 @@
|
|||||||
|
// Copyright (c) 2017 Mattermost, Inc. All Rights Reserved.
|
||||||
|
// See License.txt for license information.
|
||||||
|
|
||||||
|
import {connect} from 'react-redux';
|
||||||
|
|
||||||
|
import {getUser} from 'mattermost-redux/selectors/entities/users';
|
||||||
|
import {get} from 'mattermost-redux/selectors/entities/preferences';
|
||||||
|
import {getPost} from 'mattermost-redux/selectors/entities/posts';
|
||||||
|
|
||||||
|
import {Preferences} from 'utils/constants.jsx';
|
||||||
|
|
||||||
|
import PostBody from './post_body.jsx';
|
||||||
|
|
||||||
|
function mapStateToProps(state, ownProps) {
|
||||||
|
let parentPost;
|
||||||
|
let parentPostUser;
|
||||||
|
if (ownProps.post.root_id) {
|
||||||
|
parentPost = getPost(state, ownProps.post.root_id);
|
||||||
|
parentPostUser = getUser(state, parentPost.user_id);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
...ownProps,
|
||||||
|
parentPost,
|
||||||
|
parentPostUser,
|
||||||
|
previewCollapsed: get(state, Preferences.CATEGORY_DISPLAY_SETTINGS, Preferences.COLLAPSE_DISPLAY, 'false')
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export default connect(mapStateToProps)(PostBody);
|
||||||
@@ -1,67 +1,63 @@
|
|||||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||||
// See License.txt for license information.
|
// See License.txt for license information.
|
||||||
|
|
||||||
import UserStore from 'stores/user_store.jsx';
|
|
||||||
import * as Utils from 'utils/utils.jsx';
|
import * as Utils from 'utils/utils.jsx';
|
||||||
import * as GlobalActions from 'actions/global_actions.jsx';
|
|
||||||
import * as PostUtils from 'utils/post_utils.jsx';
|
import * as PostUtils from 'utils/post_utils.jsx';
|
||||||
import Constants from 'utils/constants.jsx';
|
import {Posts} from 'mattermost-redux/constants';
|
||||||
import CommentedOnFilesMessageContainer from './commented_on_files_message_container.jsx';
|
|
||||||
import FileAttachmentListContainer from 'components/file_attachment_list_container.jsx';
|
|
||||||
import PostBodyAdditionalContent from './post_body_additional_content.jsx';
|
|
||||||
import PostMessageContainer from './post_message_container.jsx';
|
|
||||||
import PendingPostOptions from './pending_post_options.jsx';
|
|
||||||
import ReactionListContainer from './reaction_list_container.jsx';
|
|
||||||
|
|
||||||
import {FormattedMessage} from 'react-intl';
|
import CommentedOnFilesMessage from 'components/post_view/commented_on_files_message';
|
||||||
|
import FileAttachmentListContainer from 'components/file_attachment_list';
|
||||||
import loadingGif from 'images/load.gif';
|
import PostBodyAdditionalContent from 'components/post_view/post_body_additional_content.jsx';
|
||||||
|
import PostMessageContainer from 'components/post_view/post_message_view';
|
||||||
import PropTypes from 'prop-types';
|
import ReactionListContainer from 'components/post_view/reaction_list';
|
||||||
|
import FailedPostOptions from 'components/post_view/failed_post_options';
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import {FormattedMessage} from 'react-intl';
|
||||||
|
|
||||||
export default class PostBody extends React.Component {
|
export default class PostBody extends React.PureComponent {
|
||||||
constructor(props) {
|
static propTypes = {
|
||||||
super(props);
|
|
||||||
|
|
||||||
this.removePost = this.removePost.bind(this);
|
/**
|
||||||
}
|
* The post to render the body of
|
||||||
|
*/
|
||||||
|
post: PropTypes.object.isRequired,
|
||||||
|
|
||||||
shouldComponentUpdate(nextProps) {
|
/**
|
||||||
if (nextProps.isCommentMention !== this.props.isCommentMention) {
|
* The parent post of the thread this post is in
|
||||||
return true;
|
*/
|
||||||
}
|
parentPost: PropTypes.object,
|
||||||
|
|
||||||
if (!Utils.areObjectsEqual(nextProps.post, this.props.post)) {
|
/**
|
||||||
return true;
|
* The poster of the parent post, if exists
|
||||||
}
|
*/
|
||||||
|
parentPostUser: PropTypes.object,
|
||||||
|
|
||||||
if (!Utils.areObjectsEqual(nextProps.parentPost, this.props.parentPost)) {
|
/**
|
||||||
return true;
|
* The function called when the comment icon is clicked
|
||||||
}
|
*/
|
||||||
|
handleCommentClick: PropTypes.func.isRequired,
|
||||||
|
|
||||||
if (nextProps.compactDisplay !== this.props.compactDisplay) {
|
/**
|
||||||
return true;
|
* Set to render post body compactly
|
||||||
}
|
*/
|
||||||
|
compactDisplay: PropTypes.bool,
|
||||||
|
|
||||||
if (nextProps.previewCollapsed !== this.props.previewCollapsed) {
|
/**
|
||||||
return true;
|
* Set to highlight comment as a mention
|
||||||
}
|
*/
|
||||||
|
isCommentMention: PropTypes.bool,
|
||||||
|
|
||||||
if (nextProps.handleCommentClick.toString() !== this.props.handleCommentClick.toString()) {
|
/**
|
||||||
return true;
|
* Set to collapse image and video previews
|
||||||
}
|
*/
|
||||||
|
previewCollapsed: PropTypes.string,
|
||||||
|
|
||||||
if (nextProps.lastPostCount !== this.props.lastPostCount) {
|
/**
|
||||||
return true;
|
* Post identifiers for selenium tests
|
||||||
}
|
*/
|
||||||
|
lastPostCount: PropTypes.number
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
removePost() {
|
|
||||||
GlobalActions.emitRemovePost(this.props.post);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
@@ -71,8 +67,8 @@ export default class PostBody extends React.Component {
|
|||||||
let comment = '';
|
let comment = '';
|
||||||
let postClass = '';
|
let postClass = '';
|
||||||
|
|
||||||
if (parentPost) {
|
if (parentPost && this.props.parentPostUser) {
|
||||||
const profile = UserStore.getProfile(parentPost.user_id);
|
const profile = this.props.parentPostUser;
|
||||||
|
|
||||||
let apostrophe = '';
|
let apostrophe = '';
|
||||||
let name = '...';
|
let name = '...';
|
||||||
@@ -105,8 +101,7 @@ export default class PostBody extends React.Component {
|
|||||||
message = Utils.replaceHtmlEntities(parentPost.message);
|
message = Utils.replaceHtmlEntities(parentPost.message);
|
||||||
} else if (parentPost.file_ids && parentPost.file_ids.length > 0) {
|
} else if (parentPost.file_ids && parentPost.file_ids.length > 0) {
|
||||||
message = (
|
message = (
|
||||||
<CommentedOnFilesMessageContainer
|
<CommentedOnFilesMessage
|
||||||
parentPostChannelId={parentPost.channel_id}
|
|
||||||
parentPostId={parentPost.id}
|
parentPostId={parentPost.id}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
@@ -134,18 +129,10 @@ export default class PostBody extends React.Component {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
let loading;
|
let failedOptions;
|
||||||
if (post.state === Constants.POST_FAILED) {
|
if (this.props.post.failed) {
|
||||||
postClass += ' post--fail';
|
postClass += ' post--fail';
|
||||||
loading = <PendingPostOptions post={this.props.post}/>;
|
failedOptions = <FailedPostOptions post={this.props.post}/>;
|
||||||
} else if (post.state === Constants.POST_LOADING) {
|
|
||||||
postClass += ' post-waiting';
|
|
||||||
loading = (
|
|
||||||
<img
|
|
||||||
className='post-loading-gif pull-right'
|
|
||||||
src={loadingGif}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (PostUtils.isEdited(this.props.post)) {
|
if (PostUtils.isEdited(this.props.post)) {
|
||||||
@@ -153,7 +140,7 @@ export default class PostBody extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let fileAttachmentHolder = null;
|
let fileAttachmentHolder = null;
|
||||||
if (((post.file_ids && post.file_ids.length > 0) || (post.filenames && post.filenames.length > 0)) && this.props.post.state !== Constants.POST_DELETED) {
|
if (((post.file_ids && post.file_ids.length > 0) || (post.filenames && post.filenames.length > 0)) && this.props.post.state !== Posts.POST_DELETED) {
|
||||||
fileAttachmentHolder = (
|
fileAttachmentHolder = (
|
||||||
<FileAttachmentListContainer
|
<FileAttachmentListContainer
|
||||||
post={post}
|
post={post}
|
||||||
@@ -168,7 +155,7 @@ export default class PostBody extends React.Component {
|
|||||||
id={`${post.id}_message`}
|
id={`${post.id}_message`}
|
||||||
className={postClass}
|
className={postClass}
|
||||||
>
|
>
|
||||||
{loading}
|
{failedOptions}
|
||||||
<PostMessageContainer
|
<PostMessageContainer
|
||||||
lastPostCount={this.props.lastPostCount}
|
lastPostCount={this.props.lastPostCount}
|
||||||
post={this.props.post}
|
post={this.props.post}
|
||||||
@@ -177,16 +164,14 @@ export default class PostBody extends React.Component {
|
|||||||
);
|
);
|
||||||
|
|
||||||
let messageWithAdditionalContent;
|
let messageWithAdditionalContent;
|
||||||
if (this.props.post.state === Constants.POST_DELETED) {
|
if (this.props.post.state === Posts.POST_DELETED) {
|
||||||
messageWithAdditionalContent = messageWrapper;
|
messageWithAdditionalContent = messageWrapper;
|
||||||
} else {
|
} else {
|
||||||
messageWithAdditionalContent = (
|
messageWithAdditionalContent = (
|
||||||
<PostBodyAdditionalContent
|
<PostBodyAdditionalContent
|
||||||
post={this.props.post}
|
post={this.props.post}
|
||||||
message={messageWrapper}
|
message={messageWrapper}
|
||||||
compactDisplay={this.props.compactDisplay}
|
|
||||||
previewCollapsed={this.props.previewCollapsed}
|
previewCollapsed={this.props.previewCollapsed}
|
||||||
childComponentDidUpdateFunction={this.props.childComponentDidUpdateFunction}
|
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -208,16 +193,3 @@ export default class PostBody extends React.Component {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
PostBody.propTypes = {
|
|
||||||
post: PropTypes.object.isRequired,
|
|
||||||
currentUser: PropTypes.object.isRequired,
|
|
||||||
parentPost: PropTypes.object,
|
|
||||||
retryPost: PropTypes.func,
|
|
||||||
lastPostCount: PropTypes.number,
|
|
||||||
handleCommentClick: PropTypes.func.isRequired,
|
|
||||||
compactDisplay: PropTypes.bool,
|
|
||||||
previewCollapsed: PropTypes.string,
|
|
||||||
isCommentMention: PropTypes.bool,
|
|
||||||
childComponentDidUpdateFunction: PropTypes.func
|
|
||||||
};
|
|
||||||
@@ -2,18 +2,39 @@
|
|||||||
// See License.txt for license information.
|
// See License.txt for license information.
|
||||||
|
|
||||||
import PostAttachmentList from './post_attachment_list.jsx';
|
import PostAttachmentList from './post_attachment_list.jsx';
|
||||||
import PostAttachmentOpenGraph from './post_attachment_opengraph.jsx';
|
import PostAttachmentOpenGraph from './post_attachment_opengraph';
|
||||||
import PostImage from './post_image.jsx';
|
import PostImage from './post_image.jsx';
|
||||||
import YoutubeVideo from 'components/youtube_video.jsx';
|
import YoutubeVideo from 'components/youtube_video';
|
||||||
|
|
||||||
import Constants from 'utils/constants.jsx';
|
import Constants from 'utils/constants.jsx';
|
||||||
import * as Utils from 'utils/utils.jsx';
|
import * as Utils from 'utils/utils.jsx';
|
||||||
|
|
||||||
|
import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
import React from 'react';
|
export default class PostBodyAdditionalContent extends React.PureComponent {
|
||||||
|
static propTypes = {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The post to render the content of
|
||||||
|
*/
|
||||||
|
post: PropTypes.object.isRequired,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The post's message
|
||||||
|
*/
|
||||||
|
message: PropTypes.element.isRequired,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set to collapse image and video previews
|
||||||
|
*/
|
||||||
|
previewCollapsed: PropTypes.string
|
||||||
|
}
|
||||||
|
|
||||||
|
static defaultProps = {
|
||||||
|
previewCollapsed: ''
|
||||||
|
}
|
||||||
|
|
||||||
export default class PostBodyAdditionalContent extends React.Component {
|
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
|
|
||||||
@@ -40,25 +61,6 @@ export default class PostBodyAdditionalContent extends React.Component {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
shouldComponentUpdate(nextProps, nextState) {
|
|
||||||
if (!Utils.areObjectsEqual(nextProps.post, this.props.post)) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
if (!Utils.areObjectsEqual(nextProps.message, this.props.message)) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
if (nextState.embedVisible !== this.state.embedVisible) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
if (nextState.linkLoadError !== this.state.linkLoadError) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
if (nextState.linkLoaded !== this.state.linkLoaded) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
toggleEmbedVisibility() {
|
toggleEmbedVisibility() {
|
||||||
this.setState({embedVisible: !this.state.embedVisible});
|
this.setState({embedVisible: !this.state.embedVisible});
|
||||||
}
|
}
|
||||||
@@ -138,7 +140,6 @@ export default class PostBodyAdditionalContent extends React.Component {
|
|||||||
link={link}
|
link={link}
|
||||||
onLinkLoadError={this.handleLinkLoadError}
|
onLinkLoadError={this.handleLinkLoadError}
|
||||||
onLinkLoaded={this.handleLinkLoaded}
|
onLinkLoaded={this.handleLinkLoaded}
|
||||||
childComponentDidUpdateFunction={this.props.childComponentDidUpdateFunction}
|
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -156,7 +157,6 @@ export default class PostBodyAdditionalContent extends React.Component {
|
|||||||
return (
|
return (
|
||||||
<PostAttachmentOpenGraph
|
<PostAttachmentOpenGraph
|
||||||
link={link}
|
link={link}
|
||||||
childComponentDidUpdateFunction={this.props.childComponentDidUpdateFunction}
|
|
||||||
previewCollapsed={this.props.previewCollapsed}
|
previewCollapsed={this.props.previewCollapsed}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
@@ -227,14 +227,3 @@ export default class PostBodyAdditionalContent extends React.Component {
|
|||||||
return this.props.message;
|
return this.props.message;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
PostBodyAdditionalContent.defaultProps = {
|
|
||||||
previewCollapsed: 'false'
|
|
||||||
};
|
|
||||||
PostBodyAdditionalContent.propTypes = {
|
|
||||||
post: PropTypes.object.isRequired,
|
|
||||||
message: PropTypes.element.isRequired,
|
|
||||||
compactDisplay: PropTypes.bool,
|
|
||||||
previewCollapsed: PropTypes.string,
|
|
||||||
childComponentDidUpdateFunction: PropTypes.func
|
|
||||||
};
|
|
||||||
@@ -1,9 +1,8 @@
|
|||||||
import PropTypes from 'prop-types';
|
|
||||||
|
|
||||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||||
// See License.txt for license information.
|
// See License.txt for license information.
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
import {FormattedMessage} from 'react-intl';
|
import {FormattedMessage} from 'react-intl';
|
||||||
import {Tooltip, OverlayTrigger} from 'react-bootstrap';
|
import {Tooltip, OverlayTrigger} from 'react-bootstrap';
|
||||||
|
|
||||||
@@ -1,212 +0,0 @@
|
|||||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
|
||||||
// See License.txt for license information.
|
|
||||||
|
|
||||||
import PostList from './components/post_list.jsx';
|
|
||||||
import LoadingScreen from 'components/loading_screen.jsx';
|
|
||||||
|
|
||||||
import EmojiStore from 'stores/emoji_store.jsx';
|
|
||||||
import PostStore from 'stores/post_store.jsx';
|
|
||||||
import UserStore from 'stores/user_store.jsx';
|
|
||||||
import ChannelStore from 'stores/channel_store.jsx';
|
|
||||||
import PreferenceStore from 'stores/preference_store.jsx';
|
|
||||||
import WebrtcStore from 'stores/webrtc_store.jsx';
|
|
||||||
|
|
||||||
import * as Utils from 'utils/utils.jsx';
|
|
||||||
|
|
||||||
import Constants from 'utils/constants.jsx';
|
|
||||||
const Preferences = Constants.Preferences;
|
|
||||||
const ScrollTypes = Constants.ScrollTypes;
|
|
||||||
|
|
||||||
import React from 'react';
|
|
||||||
|
|
||||||
export default class PostFocusView extends React.Component {
|
|
||||||
constructor(props) {
|
|
||||||
super(props);
|
|
||||||
|
|
||||||
this.onChannelChange = this.onChannelChange.bind(this);
|
|
||||||
this.onPostsChange = this.onPostsChange.bind(this);
|
|
||||||
this.onUserChange = this.onUserChange.bind(this);
|
|
||||||
this.onEmojiChange = this.onEmojiChange.bind(this);
|
|
||||||
this.onStatusChange = this.onStatusChange.bind(this);
|
|
||||||
this.onPreferenceChange = this.onPreferenceChange.bind(this);
|
|
||||||
this.onPostListScroll = this.onPostListScroll.bind(this);
|
|
||||||
this.onBusy = this.onBusy.bind(this);
|
|
||||||
|
|
||||||
const focusedPostId = PostStore.getFocusedPostId();
|
|
||||||
|
|
||||||
const channel = ChannelStore.getCurrent();
|
|
||||||
const profiles = UserStore.getProfiles();
|
|
||||||
|
|
||||||
const joinLeaveEnabled = PreferenceStore.getBool(Constants.Preferences.CATEGORY_ADVANCED_SETTINGS, 'join_leave', true);
|
|
||||||
|
|
||||||
let statuses;
|
|
||||||
if (channel) {
|
|
||||||
statuses = Object.assign({}, UserStore.getStatuses());
|
|
||||||
}
|
|
||||||
|
|
||||||
this.state = {
|
|
||||||
postList: PostStore.filterPosts(focusedPostId, joinLeaveEnabled),
|
|
||||||
currentUser: UserStore.getCurrentUser(),
|
|
||||||
isBusy: WebrtcStore.isBusy(),
|
|
||||||
profiles,
|
|
||||||
statuses,
|
|
||||||
scrollType: ScrollTypes.POST,
|
|
||||||
currentChannel: ChannelStore.getCurrentId().slice(),
|
|
||||||
scrollPostId: focusedPostId,
|
|
||||||
atTop: PostStore.getVisibilityAtTop(focusedPostId),
|
|
||||||
atBottom: PostStore.getVisibilityAtBottom(focusedPostId),
|
|
||||||
emojis: EmojiStore.getEmojis(),
|
|
||||||
displayNameType: PreferenceStore.get(Preferences.CATEGORY_DISPLAY_SETTINGS, 'name_format', 'false'),
|
|
||||||
displayPostsInCenter: PreferenceStore.get(Preferences.CATEGORY_DISPLAY_SETTINGS, Preferences.CHANNEL_DISPLAY_MODE, Preferences.CHANNEL_DISPLAY_MODE_DEFAULT) === Preferences.CHANNEL_DISPLAY_MODE_CENTERED,
|
|
||||||
compactDisplay: PreferenceStore.get(Preferences.CATEGORY_DISPLAY_SETTINGS, Preferences.MESSAGE_DISPLAY, Preferences.MESSAGE_DISPLAY_DEFAULT) === Preferences.MESSAGE_DISPLAY_COMPACT,
|
|
||||||
previewsCollapsed: PreferenceStore.get(Preferences.CATEGORY_DISPLAY_SETTINGS, Preferences.COLLAPSE_DISPLAY, 'false'),
|
|
||||||
useMilitaryTime: PreferenceStore.getBool(Constants.Preferences.CATEGORY_DISPLAY_SETTINGS, Preferences.USE_MILITARY_TIME, false),
|
|
||||||
flaggedPosts: PreferenceStore.getCategory(Constants.Preferences.CATEGORY_FLAGGED_POST)
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidMount() {
|
|
||||||
ChannelStore.addChangeListener(this.onChannelChange);
|
|
||||||
PostStore.addChangeListener(this.onPostsChange);
|
|
||||||
UserStore.addChangeListener(this.onUserChange);
|
|
||||||
UserStore.addStatusesChangeListener(this.onStatusChange);
|
|
||||||
EmojiStore.addChangeListener(this.onEmojiChange);
|
|
||||||
PreferenceStore.addChangeListener(this.onPreferenceChange);
|
|
||||||
WebrtcStore.addBusyListener(this.onBusy);
|
|
||||||
}
|
|
||||||
|
|
||||||
componentWillUnmount() {
|
|
||||||
ChannelStore.removeChangeListener(this.onChannelChange);
|
|
||||||
PostStore.removeChangeListener(this.onPostsChange);
|
|
||||||
UserStore.removeChangeListener(this.onUserChange);
|
|
||||||
UserStore.removeStatusesChangeListener(this.onStatusChange);
|
|
||||||
EmojiStore.removeChangeListener(this.onEmojiChange);
|
|
||||||
PreferenceStore.removeChangeListener(this.onPreferenceChange);
|
|
||||||
WebrtcStore.removeBusyListener(this.onBusy);
|
|
||||||
}
|
|
||||||
|
|
||||||
onChannelChange() {
|
|
||||||
const currentChannel = ChannelStore.getCurrentId();
|
|
||||||
if (this.state.currentChannel !== currentChannel) {
|
|
||||||
this.setState({
|
|
||||||
currentChannel: currentChannel.slice(),
|
|
||||||
scrollType: ScrollTypes.POST
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onPostsChange() {
|
|
||||||
const focusedPostId = PostStore.getFocusedPostId();
|
|
||||||
if (focusedPostId == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const joinLeaveEnabled = PreferenceStore.getBool(Constants.Preferences.CATEGORY_ADVANCED_SETTINGS, 'join_leave', true);
|
|
||||||
|
|
||||||
this.setState({
|
|
||||||
scrollPostId: focusedPostId,
|
|
||||||
postList: PostStore.filterPosts(focusedPostId, joinLeaveEnabled),
|
|
||||||
atTop: PostStore.getVisibilityAtTop(focusedPostId),
|
|
||||||
atBottom: PostStore.getVisibilityAtBottom(focusedPostId)
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
onUserChange() {
|
|
||||||
this.setState({currentUser: UserStore.getCurrentUser(), profiles: JSON.parse(JSON.stringify(UserStore.getProfiles()))});
|
|
||||||
}
|
|
||||||
|
|
||||||
onStatusChange() {
|
|
||||||
const channel = ChannelStore.getCurrent();
|
|
||||||
let statuses;
|
|
||||||
if (channel) {
|
|
||||||
statuses = Object.assign({}, UserStore.getStatuses());
|
|
||||||
}
|
|
||||||
|
|
||||||
this.setState({statuses});
|
|
||||||
}
|
|
||||||
|
|
||||||
onEmojiChange() {
|
|
||||||
this.setState({
|
|
||||||
emojis: EmojiStore.getEmojis()
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
onPreferenceChange(category) {
|
|
||||||
// Bit of a hack to force render when this setting is updated
|
|
||||||
// regardless of change
|
|
||||||
let previewSuffix = '';
|
|
||||||
if (category === Preferences.CATEGORY_DISPLAY_SETTINGS) {
|
|
||||||
previewSuffix = '_' + Utils.generateId();
|
|
||||||
}
|
|
||||||
|
|
||||||
const focusedPostId = PostStore.getFocusedPostId();
|
|
||||||
if (focusedPostId == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const joinLeaveEnabled = PreferenceStore.getBool(Constants.Preferences.CATEGORY_ADVANCED_SETTINGS, 'join_leave', true);
|
|
||||||
|
|
||||||
this.setState({
|
|
||||||
postList: PostStore.filterPosts(focusedPostId, joinLeaveEnabled),
|
|
||||||
displayNameType: PreferenceStore.get(Preferences.CATEGORY_DISPLAY_SETTINGS, 'name_format', 'false'),
|
|
||||||
displayPostsInCenter: PreferenceStore.get(Preferences.CATEGORY_DISPLAY_SETTINGS, Preferences.CHANNEL_DISPLAY_MODE, Preferences.CHANNEL_DISPLAY_MODE_DEFAULT) === Preferences.CHANNEL_DISPLAY_MODE_CENTERED,
|
|
||||||
compactDisplay: PreferenceStore.get(Preferences.CATEGORY_DISPLAY_SETTINGS, Preferences.MESSAGE_DISPLAY, Preferences.MESSAGE_DISPLAY_DEFAULT) === Preferences.MESSAGE_DISPLAY_COMPACT,
|
|
||||||
previewsCollapsed: PreferenceStore.get(Preferences.CATEGORY_DISPLAY_SETTINGS, Preferences.COLLAPSE_DISPLAY, 'false') + previewSuffix,
|
|
||||||
useMilitaryTime: PreferenceStore.getBool(Constants.Preferences.CATEGORY_DISPLAY_SETTINGS, Preferences.USE_MILITARY_TIME, false),
|
|
||||||
flaggedPosts: PreferenceStore.getCategory(Constants.Preferences.CATEGORY_FLAGGED_POST)
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
onPostListScroll() {
|
|
||||||
this.setState({scrollType: ScrollTypes.FREE});
|
|
||||||
}
|
|
||||||
|
|
||||||
onBusy(isBusy) {
|
|
||||||
this.setState({isBusy});
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const postsToHighlight = {};
|
|
||||||
postsToHighlight[this.state.scrollPostId] = true;
|
|
||||||
|
|
||||||
let content;
|
|
||||||
if (this.state.postList == null) {
|
|
||||||
content = (
|
|
||||||
<LoadingScreen
|
|
||||||
position='absolute'
|
|
||||||
key='loading'
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
content = (
|
|
||||||
<PostList
|
|
||||||
postList={this.state.postList}
|
|
||||||
currentUser={this.state.currentUser}
|
|
||||||
profiles={this.state.profiles}
|
|
||||||
scrollType={this.state.scrollType}
|
|
||||||
scrollPostId={this.state.scrollPostId}
|
|
||||||
postListScrolled={this.onPostListScroll}
|
|
||||||
displayNameType={this.state.displayNameType}
|
|
||||||
displayPostsInCenter={this.state.displayPostsInCenter}
|
|
||||||
compactDisplay={this.state.compactDisplay}
|
|
||||||
previewsCollapsed={this.state.previewsCollapsed}
|
|
||||||
useMilitaryTime={this.state.useMilitaryTime}
|
|
||||||
showMoreMessagesTop={!this.state.atTop}
|
|
||||||
showMoreMessagesBottom={!this.state.atBottom}
|
|
||||||
postsToHighlight={postsToHighlight}
|
|
||||||
isFocusPost={true}
|
|
||||||
emojis={this.state.emojis}
|
|
||||||
flaggedPosts={this.state.flaggedPosts}
|
|
||||||
statuses={this.state.statuses}
|
|
||||||
isBusy={this.state.isBusy}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div id='post-list'>
|
|
||||||
{content}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
18
webapp/components/post_view/post_header/index.js
Обычный файл
18
webapp/components/post_view/post_header/index.js
Обычный файл
@@ -0,0 +1,18 @@
|
|||||||
|
// Copyright (c) 2017 Mattermost, Inc. All Rights Reserved.
|
||||||
|
// See License.txt for license information.
|
||||||
|
|
||||||
|
import {connect} from 'react-redux';
|
||||||
|
|
||||||
|
import {get} from 'mattermost-redux/selectors/entities/preferences';
|
||||||
|
import {Preferences} from 'mattermost-redux/constants';
|
||||||
|
|
||||||
|
import PostHeader from './post_header.jsx';
|
||||||
|
|
||||||
|
function mapStateToProps(state, ownProps) {
|
||||||
|
return {
|
||||||
|
...ownProps,
|
||||||
|
displayNameType: get(state, Preferences.CATEGORY_DISPLAY_SETTINGS, 'name_format', 'false')
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export default connect(mapStateToProps)(PostHeader);
|
||||||
@@ -2,18 +2,80 @@
|
|||||||
// See License.txt for license information.
|
// See License.txt for license information.
|
||||||
|
|
||||||
import UserProfile from 'components/user_profile.jsx';
|
import UserProfile from 'components/user_profile.jsx';
|
||||||
import PostInfo from './post_info.jsx';
|
import PostInfo from 'components/post_view/post_info';
|
||||||
import {FormattedMessage} from 'react-intl';
|
import {FormattedMessage} from 'react-intl';
|
||||||
|
|
||||||
import * as PostUtils from 'utils/post_utils.jsx';
|
import * as PostUtils from 'utils/post_utils.jsx';
|
||||||
|
|
||||||
import Constants from 'utils/constants.jsx';
|
import Constants from 'utils/constants.jsx';
|
||||||
|
|
||||||
|
import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
import React from 'react';
|
export default class PostHeader extends React.PureComponent {
|
||||||
|
static propTypes = {
|
||||||
|
|
||||||
|
/*
|
||||||
|
* The post to render the header for
|
||||||
|
*/
|
||||||
|
post: PropTypes.object.isRequired,
|
||||||
|
|
||||||
|
/*
|
||||||
|
* The user who created the post
|
||||||
|
*/
|
||||||
|
user: PropTypes.object,
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Function called when the comment icon is clicked
|
||||||
|
*/
|
||||||
|
handleCommentClick: PropTypes.func.isRequired,
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Function called when the post options dropdown is opened
|
||||||
|
*/
|
||||||
|
handleDropdownOpened: PropTypes.func.isRequired,
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Set to render compactly
|
||||||
|
*/
|
||||||
|
compactDisplay: PropTypes.bool,
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Set to render the post as if it was part of the previous post
|
||||||
|
*/
|
||||||
|
consecutivePostByUser: PropTypes.bool,
|
||||||
|
|
||||||
|
/*
|
||||||
|
* The method for displaying the post creator's name
|
||||||
|
*/
|
||||||
|
displayNameType: PropTypes.string,
|
||||||
|
|
||||||
|
/*
|
||||||
|
* The status of the user who created the post
|
||||||
|
*/
|
||||||
|
status: PropTypes.string,
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Set if the post creator is currenlty in a WebRTC call
|
||||||
|
*/
|
||||||
|
isBusy: PropTypes.bool,
|
||||||
|
|
||||||
|
/*
|
||||||
|
* The number of replies in the same thread as this post
|
||||||
|
*/
|
||||||
|
replyCount: PropTypes.number,
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Post identifiers for selenium tests
|
||||||
|
*/
|
||||||
|
lastPostCount: PropTypes.number,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Function to get the post list HTML element
|
||||||
|
*/
|
||||||
|
getPostList: PropTypes.func.isRequired
|
||||||
|
}
|
||||||
|
|
||||||
export default class PostHeader extends React.Component {
|
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
this.state = {};
|
this.state = {};
|
||||||
@@ -81,16 +143,12 @@ export default class PostHeader extends React.Component {
|
|||||||
<div className='col'>
|
<div className='col'>
|
||||||
<PostInfo
|
<PostInfo
|
||||||
post={post}
|
post={post}
|
||||||
lastPostCount={this.props.lastPostCount}
|
|
||||||
commentCount={this.props.commentCount}
|
|
||||||
handleCommentClick={this.props.handleCommentClick}
|
handleCommentClick={this.props.handleCommentClick}
|
||||||
handleDropdownOpened={this.props.handleDropdownOpened}
|
handleDropdownOpened={this.props.handleDropdownOpened}
|
||||||
isLastComment={this.props.isLastComment}
|
|
||||||
sameUser={this.props.sameUser}
|
|
||||||
currentUser={this.props.currentUser}
|
|
||||||
compactDisplay={this.props.compactDisplay}
|
compactDisplay={this.props.compactDisplay}
|
||||||
useMilitaryTime={this.props.useMilitaryTime}
|
lastPostCount={this.props.lastPostCount}
|
||||||
isFlagged={this.props.isFlagged}
|
replyCount={this.props.replyCount}
|
||||||
|
consecutivePostByUser={this.props.consecutivePostByUser}
|
||||||
getPostList={this.props.getPostList}
|
getPostList={this.props.getPostList}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -98,28 +156,3 @@ export default class PostHeader extends React.Component {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
PostHeader.defaultProps = {
|
|
||||||
post: null,
|
|
||||||
commentCount: 0,
|
|
||||||
isLastComment: false,
|
|
||||||
sameUser: false
|
|
||||||
};
|
|
||||||
PostHeader.propTypes = {
|
|
||||||
post: PropTypes.object.isRequired,
|
|
||||||
user: PropTypes.object,
|
|
||||||
currentUser: PropTypes.object.isRequired,
|
|
||||||
lastPostCount: PropTypes.number,
|
|
||||||
commentCount: PropTypes.number.isRequired,
|
|
||||||
isLastComment: PropTypes.bool.isRequired,
|
|
||||||
handleCommentClick: PropTypes.func.isRequired,
|
|
||||||
handleDropdownOpened: PropTypes.func.isRequired,
|
|
||||||
sameUser: PropTypes.bool.isRequired,
|
|
||||||
compactDisplay: PropTypes.bool,
|
|
||||||
displayNameType: PropTypes.string,
|
|
||||||
useMilitaryTime: PropTypes.bool.isRequired,
|
|
||||||
isFlagged: PropTypes.bool.isRequired,
|
|
||||||
status: PropTypes.string,
|
|
||||||
isBusy: PropTypes.bool,
|
|
||||||
getPostList: PropTypes.func.isRequired
|
|
||||||
};
|
|
||||||
@@ -1,11 +1,28 @@
|
|||||||
import PropTypes from 'prop-types';
|
|
||||||
|
|
||||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||||
// See License.txt for license information.
|
// See License.txt for license information.
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
|
export default class PostImageEmbed extends React.PureComponent {
|
||||||
|
static propTypes = {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The link to load the image from
|
||||||
|
*/
|
||||||
|
link: PropTypes.string.isRequired,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Function to call when image is loaded
|
||||||
|
*/
|
||||||
|
onLinkLoaded: PropTypes.func,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The function to call if image load fails
|
||||||
|
*/
|
||||||
|
onLinkLoadError: PropTypes.func
|
||||||
|
}
|
||||||
|
|
||||||
export default class PostImageEmbed extends React.Component {
|
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
|
|
||||||
@@ -32,9 +49,6 @@ export default class PostImageEmbed extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
componentDidUpdate(prevProps) {
|
componentDidUpdate(prevProps) {
|
||||||
if (this.state.loaded && this.props.childComponentDidUpdateFunction) {
|
|
||||||
this.props.childComponentDidUpdateFunction();
|
|
||||||
}
|
|
||||||
if (!this.state.loaded && prevProps.link !== this.props.link) {
|
if (!this.state.loaded && prevProps.link !== this.props.link) {
|
||||||
this.loadImg(this.props.link);
|
this.loadImg(this.props.link);
|
||||||
}
|
}
|
||||||
@@ -84,10 +98,3 @@ export default class PostImageEmbed extends React.Component {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
PostImageEmbed.propTypes = {
|
|
||||||
link: PropTypes.string.isRequired,
|
|
||||||
onLinkLoadError: PropTypes.func,
|
|
||||||
onLinkLoaded: PropTypes.func,
|
|
||||||
childComponentDidUpdateFunction: PropTypes.func
|
|
||||||
};
|
|
||||||
31
webapp/components/post_view/post_info/index.js
Обычный файл
31
webapp/components/post_view/post_info/index.js
Обычный файл
@@ -0,0 +1,31 @@
|
|||||||
|
// Copyright (c) 2017 Mattermost, Inc. All Rights Reserved.
|
||||||
|
// See License.txt for license information.
|
||||||
|
|
||||||
|
import {connect} from 'react-redux';
|
||||||
|
import {bindActionCreators} from 'redux';
|
||||||
|
import {removePost, addReaction} from 'mattermost-redux/actions/posts';
|
||||||
|
|
||||||
|
import {getBool} from 'mattermost-redux/selectors/entities/preferences';
|
||||||
|
|
||||||
|
import {Preferences} from 'utils/constants.jsx';
|
||||||
|
|
||||||
|
import PostInfo from './post_info.jsx';
|
||||||
|
|
||||||
|
function mapStateToProps(state, ownProps) {
|
||||||
|
return {
|
||||||
|
...ownProps,
|
||||||
|
useMilitaryTime: getBool(state, Preferences.CATEGORY_DISPLAY_SETTINGS, Preferences.USE_MILITARY_TIME, false),
|
||||||
|
isFlagged: getBool(state, Preferences.CATEGORY_FLAGGED_POST, ownProps.post.id)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function mapDispatchToProps(dispatch) {
|
||||||
|
return {
|
||||||
|
actions: bindActionCreators({
|
||||||
|
removePost,
|
||||||
|
addReaction
|
||||||
|
}, dispatch)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export default connect(mapStateToProps, mapDispatchToProps)(PostInfo);
|
||||||
@@ -1,26 +1,77 @@
|
|||||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||||
// See License.txt for license information.
|
// See License.txt for license information.
|
||||||
|
|
||||||
import PostTime from './post_time.jsx';
|
import PostTime from 'components/post_view/post_time.jsx';
|
||||||
import PostFlagIcon from 'components/common/post_flag_icon.jsx';
|
import PostFlagIcon from 'components/post_view/post_flag_icon.jsx';
|
||||||
import DotMenu from 'components/dot_menu/dot_menu.jsx';
|
|
||||||
|
|
||||||
import * as GlobalActions from 'actions/global_actions.jsx';
|
|
||||||
import * as PostActions from 'actions/post_actions.jsx';
|
|
||||||
import CommentIcon from 'components/common/comment_icon.jsx';
|
import CommentIcon from 'components/common/comment_icon.jsx';
|
||||||
|
import EmojiPickerOverlay from 'components/emoji_picker/emoji_picker_overlay.jsx';
|
||||||
|
import DotMenu from 'components/dot_menu';
|
||||||
|
|
||||||
import * as Utils from 'utils/utils.jsx';
|
import * as Utils from 'utils/utils.jsx';
|
||||||
import * as PostUtils from 'utils/post_utils.jsx';
|
import * as PostUtils from 'utils/post_utils.jsx';
|
||||||
import Constants from 'utils/constants.jsx';
|
import Constants from 'utils/constants.jsx';
|
||||||
import EmojiPickerOverlay from 'components/emoji_picker/emoji_picker_overlay.jsx';
|
|
||||||
import ChannelStore from 'stores/channel_store.jsx';
|
|
||||||
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
import {FormattedMessage} from 'react-intl';
|
import {FormattedMessage} from 'react-intl';
|
||||||
|
|
||||||
export default class PostInfo extends React.Component {
|
export default class PostInfo extends React.PureComponent {
|
||||||
|
static propTypes = {
|
||||||
|
|
||||||
|
/*
|
||||||
|
* The post to render the info for
|
||||||
|
*/
|
||||||
|
post: PropTypes.object.isRequired,
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Function called when the comment icon is clicked
|
||||||
|
*/
|
||||||
|
handleCommentClick: PropTypes.func.isRequired,
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Funciton called when the post options dropdown is opened
|
||||||
|
*/
|
||||||
|
handleDropdownOpened: PropTypes.func.isRequired,
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Set to display in 24 hour format
|
||||||
|
*/
|
||||||
|
useMilitaryTime: PropTypes.bool.isRequired,
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Set to mark the post as flagged
|
||||||
|
*/
|
||||||
|
isFlagged: PropTypes.bool,
|
||||||
|
|
||||||
|
/*
|
||||||
|
* The number of replies in the same thread as this post
|
||||||
|
*/
|
||||||
|
replyCount: PropTypes.number,
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Post identifiers for selenium tests
|
||||||
|
*/
|
||||||
|
lastPostCount: PropTypes.number,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Function to get the post list HTML element
|
||||||
|
*/
|
||||||
|
getPostList: PropTypes.func.isRequired,
|
||||||
|
|
||||||
|
actions: PropTypes.shape({
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Function to remove the post
|
||||||
|
*/
|
||||||
|
removePost: PropTypes.func.isRequired,
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Function to add a reaction to the post
|
||||||
|
*/
|
||||||
|
addReaction: PropTypes.func.isRequired
|
||||||
|
}).isRequired
|
||||||
|
}
|
||||||
|
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
|
|
||||||
@@ -29,7 +80,8 @@ export default class PostInfo extends React.Component {
|
|||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
showEmojiPicker: false,
|
showEmojiPicker: false,
|
||||||
reactionPickerOffset: 21
|
reactionPickerOffset: 21,
|
||||||
|
canEdit: PostUtils.canEditPost(props.post, this.editDisableAction)
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -46,7 +98,7 @@ export default class PostInfo extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
removePost() {
|
removePost() {
|
||||||
GlobalActions.emitRemovePost(this.props.post);
|
this.props.actions.removePost(this.props.post);
|
||||||
}
|
}
|
||||||
|
|
||||||
createRemovePostButton() {
|
createRemovePostButton() {
|
||||||
@@ -66,7 +118,7 @@ export default class PostInfo extends React.Component {
|
|||||||
const pickerOffset = 21;
|
const pickerOffset = 21;
|
||||||
this.setState({showEmojiPicker: false, reactionPickerOffset: pickerOffset});
|
this.setState({showEmojiPicker: false, reactionPickerOffset: pickerOffset});
|
||||||
const emojiName = emoji.name || emoji.aliases[0];
|
const emojiName = emoji.name || emoji.aliases[0];
|
||||||
PostActions.addReaction(this.props.post.channel_id, this.props.post.id, emojiName);
|
this.props.actions.addReaction(this.props.post.id, emojiName);
|
||||||
}
|
}
|
||||||
|
|
||||||
getDotMenu = () => {
|
getDotMenu = () => {
|
||||||
@@ -74,7 +126,7 @@ export default class PostInfo extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
var post = this.props.post;
|
const post = this.props.post;
|
||||||
|
|
||||||
let idCount = -1;
|
let idCount = -1;
|
||||||
if (this.props.lastPostCount >= 0 && this.props.lastPostCount < Constants.TEST_ID_COUNT) {
|
if (this.props.lastPostCount >= 0 && this.props.lastPostCount < Constants.TEST_ID_COUNT) {
|
||||||
@@ -82,19 +134,18 @@ export default class PostInfo extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const isEphemeral = Utils.isPostEphemeral(post);
|
const isEphemeral = Utils.isPostEphemeral(post);
|
||||||
const isPending = post.state === Constants.POST_FAILED || post.state === Constants.POST_LOADING;
|
|
||||||
const isSystemMessage = PostUtils.isSystemMessage(post);
|
const isSystemMessage = PostUtils.isSystemMessage(post);
|
||||||
|
|
||||||
let comments = null;
|
let comments = null;
|
||||||
let react = null;
|
let react = null;
|
||||||
if (!isEphemeral && !isPending && !isSystemMessage) {
|
if (!isEphemeral && !post.failed && !isSystemMessage) {
|
||||||
comments = (
|
comments = (
|
||||||
<CommentIcon
|
<CommentIcon
|
||||||
idPrefix={'commentIcon'}
|
idPrefix='commentIcon'
|
||||||
idCount={idCount}
|
idCount={idCount}
|
||||||
handleCommentClick={this.props.handleCommentClick}
|
handleCommentClick={this.props.handleCommentClick}
|
||||||
commentCount={this.props.commentCount}
|
commentCount={this.props.replyCount}
|
||||||
id={ChannelStore.getCurrentId() + '_' + post.id}
|
id={post.channel_id + '_' + post.id}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -116,6 +167,7 @@ export default class PostInfo extends React.Component {
|
|||||||
<i className='fa fa-smile-o'/>
|
<i className='fa fa-smile-o'/>
|
||||||
</a>
|
</a>
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -127,13 +179,13 @@ export default class PostInfo extends React.Component {
|
|||||||
{this.createRemovePostButton()}
|
{this.createRemovePostButton()}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
} else if (!isPending) {
|
} else if (!post.failed) {
|
||||||
const dotMenu = (
|
const dotMenu = (
|
||||||
<DotMenu
|
<DotMenu
|
||||||
idPrefix={Constants.CENTER}
|
idPrefix={Constants.CENTER}
|
||||||
idCount={idCount}
|
idCount={idCount}
|
||||||
post={this.props.post}
|
post={this.props.post}
|
||||||
commentCount={this.props.commentCount}
|
commentCount={this.props.replyCount}
|
||||||
isFlagged={this.props.isFlagged}
|
isFlagged={this.props.isFlagged}
|
||||||
handleCommentClick={this.props.handleCommentClick}
|
handleCommentClick={this.props.handleCommentClick}
|
||||||
handleDropdownOpened={this.props.handleDropdownOpened}
|
handleDropdownOpened={this.props.handleDropdownOpened}
|
||||||
@@ -171,8 +223,6 @@ export default class PostInfo extends React.Component {
|
|||||||
<div className='col'>
|
<div className='col'>
|
||||||
<PostTime
|
<PostTime
|
||||||
eventTime={post.create_at}
|
eventTime={post.create_at}
|
||||||
sameUser={this.props.sameUser}
|
|
||||||
compactDisplay={this.props.compactDisplay}
|
|
||||||
useMilitaryTime={this.props.useMilitaryTime}
|
useMilitaryTime={this.props.useMilitaryTime}
|
||||||
postId={post.id}
|
postId={post.id}
|
||||||
/>
|
/>
|
||||||
@@ -191,24 +241,3 @@ export default class PostInfo extends React.Component {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
PostInfo.defaultProps = {
|
|
||||||
post: null,
|
|
||||||
commentCount: 0,
|
|
||||||
isLastComment: false,
|
|
||||||
sameUser: false
|
|
||||||
};
|
|
||||||
PostInfo.propTypes = {
|
|
||||||
post: PropTypes.object.isRequired,
|
|
||||||
lastPostCount: PropTypes.number,
|
|
||||||
commentCount: PropTypes.number.isRequired,
|
|
||||||
isLastComment: PropTypes.bool.isRequired,
|
|
||||||
handleCommentClick: PropTypes.func.isRequired,
|
|
||||||
handleDropdownOpened: PropTypes.func.isRequired,
|
|
||||||
sameUser: PropTypes.bool.isRequired,
|
|
||||||
currentUser: PropTypes.object.isRequired,
|
|
||||||
compactDisplay: PropTypes.bool,
|
|
||||||
useMilitaryTime: PropTypes.bool.isRequired,
|
|
||||||
isFlagged: PropTypes.bool,
|
|
||||||
getPostList: PropTypes.func.isRequired
|
|
||||||
};
|
|
||||||
523
webapp/components/post_view/post_list.jsx
Обычный файл
523
webapp/components/post_view/post_list.jsx
Обычный файл
@@ -0,0 +1,523 @@
|
|||||||
|
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||||
|
// See License.txt for license information.
|
||||||
|
|
||||||
|
import Post from './post';
|
||||||
|
import LoadingScreen from 'components/loading_screen.jsx';
|
||||||
|
import FloatingTimestamp from './floating_timestamp.jsx';
|
||||||
|
import ScrollToBottomArrows from './scroll_to_bottom_arrows.jsx';
|
||||||
|
import NewMessageIndicator from './new_message_indicator.jsx';
|
||||||
|
|
||||||
|
import * as UserAgent from 'utils/user_agent.jsx';
|
||||||
|
import * as Utils from 'utils/utils.jsx';
|
||||||
|
import Constants from 'utils/constants.jsx';
|
||||||
|
import {createChannelIntroMessage} from 'utils/channel_intro_messages.jsx';
|
||||||
|
import DelayedAction from 'utils/delayed_action.jsx';
|
||||||
|
|
||||||
|
import {FormattedDate, FormattedMessage} from 'react-intl';
|
||||||
|
|
||||||
|
import React from 'react';
|
||||||
|
import ReactDOM from 'react-dom';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
|
const CLOSE_TO_BOTTOM_SCROLL_MARGIN = 10;
|
||||||
|
const POSTS_PER_PAGE = Constants.POST_CHUNK_SIZE / 2;
|
||||||
|
|
||||||
|
export default class PostList extends React.PureComponent {
|
||||||
|
static propTypes = {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Array of posts in the channel, ordered from oldest to newest
|
||||||
|
*/
|
||||||
|
posts: PropTypes.array,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The number of posts that should be rendered
|
||||||
|
*/
|
||||||
|
postVisibility: PropTypes.number,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The channel the posts are in
|
||||||
|
*/
|
||||||
|
channel: PropTypes.object,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The last time the channel was viewed, sets the new message separator
|
||||||
|
*/
|
||||||
|
lastViewedAt: PropTypes.number,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set if more posts are being loaded
|
||||||
|
*/
|
||||||
|
loadingPosts: PropTypes.bool,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The user id of the logged in user
|
||||||
|
*/
|
||||||
|
currentUserId: PropTypes.string,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set to focus this post
|
||||||
|
*/
|
||||||
|
focusedPostId: PropTypes.array,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether to display the channel intro at full width
|
||||||
|
*/
|
||||||
|
fullWidth: PropTypes.bool,
|
||||||
|
|
||||||
|
actions: PropTypes.shape({
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Function to get posts in the channel
|
||||||
|
*/
|
||||||
|
getPosts: PropTypes.func.isRequired,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Function to get posts in the channel older than the focused post
|
||||||
|
*/
|
||||||
|
getPostsBefore: PropTypes.func.isRequired,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Function to get posts in the channel newer than the focused post
|
||||||
|
*/
|
||||||
|
getPostsAfter: PropTypes.func.isRequired,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Function to get the post thread for the focused post
|
||||||
|
*/
|
||||||
|
getPostThread: PropTypes.func.isRequired,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Function to increase the number of posts being rendered
|
||||||
|
*/
|
||||||
|
increasePostVisibility: PropTypes.func.isRequired
|
||||||
|
}).isRequired
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
|
||||||
|
this.scrollStopAction = new DelayedAction(this.handleScrollStop);
|
||||||
|
|
||||||
|
this.previousScrollTop = Number.MAX_SAFE_INTEGER;
|
||||||
|
this.previousScrollHeight = 0;
|
||||||
|
this.previousClientHeight = 0;
|
||||||
|
|
||||||
|
this.state = {
|
||||||
|
atEnd: false,
|
||||||
|
unViewedCount: 0,
|
||||||
|
isScrolling: false,
|
||||||
|
lastViewed: Number.MAX_SAFE_INTEGER
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
this.loadPosts(this.props.channel.id, this.props.focusedPostId);
|
||||||
|
|
||||||
|
window.addEventListener('resize', this.handleResize);
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillUnmount() {
|
||||||
|
window.removeEventListener('resize', this.handleResize);
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillReceiveProps(nextProps) {
|
||||||
|
// Focusing on a new post so load posts around it
|
||||||
|
if (nextProps.focusedPostId && this.props.focusedPostId !== nextProps.focusedPostId) {
|
||||||
|
this.hasScrolledToFocusedPost = false;
|
||||||
|
this.hasScrolledToNewMessageSeparator = false;
|
||||||
|
this.setState({atEnd: false});
|
||||||
|
this.loadPosts(nextProps.channel.id, nextProps.focusedPostId);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const channel = this.props.channel || {};
|
||||||
|
const nextChannel = nextProps.channel || {};
|
||||||
|
|
||||||
|
if (nextProps.focusedPostId == null) {
|
||||||
|
// Channel changed so load posts for new channel
|
||||||
|
if (channel.id !== nextChannel.id) {
|
||||||
|
this.hasScrolled = false;
|
||||||
|
this.hasScrolledToFocusedPost = false;
|
||||||
|
this.hasScrolledToNewMessageSeparator = false;
|
||||||
|
this.setState({atEnd: false});
|
||||||
|
|
||||||
|
if (nextChannel.id) {
|
||||||
|
this.loadPosts(nextChannel.id);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.wasAtBottom() && this.props.posts !== nextProps.posts) {
|
||||||
|
const unViewedCount = nextProps.posts.reduce((count, post) => {
|
||||||
|
if (post.create_at > this.state.lastViewed &&
|
||||||
|
post.user_id !== nextProps.currentUserId &&
|
||||||
|
post.state !== Constants.POST_DELETED) {
|
||||||
|
return count + 1;
|
||||||
|
}
|
||||||
|
return count;
|
||||||
|
}, 0);
|
||||||
|
this.setState({unViewedCount});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillUpdate() {
|
||||||
|
if (this.refs.postlist) {
|
||||||
|
this.previousScrollTop = this.refs.postlist.scrollTop;
|
||||||
|
this.previousScrollHeight = this.refs.postlist.scrollHeight;
|
||||||
|
this.previousClientHeight = this.refs.postlist.clientHeight;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidUpdate(prevProps) {
|
||||||
|
// Scroll to focused post on first load
|
||||||
|
const focusedPost = this.refs[this.props.focusedPostId];
|
||||||
|
if (focusedPost) {
|
||||||
|
if (!this.hasScrolledToFocusedPost && this.props.posts) {
|
||||||
|
const element = ReactDOM.findDOMNode(focusedPost);
|
||||||
|
const rect = element.getBoundingClientRect();
|
||||||
|
const listHeight = this.refs.postlist.clientHeight / 2;
|
||||||
|
this.refs.postlist.scrollTop = this.refs.postlist.scrollTop + (rect.top - listHeight);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Scroll to new message indicator or bottom on first load
|
||||||
|
const messageSeparator = this.refs.newMessageSeparator;
|
||||||
|
if (messageSeparator && !this.hasScrolledToNewMessageSeparator) {
|
||||||
|
const element = ReactDOM.findDOMNode(messageSeparator);
|
||||||
|
element.scrollIntoView();
|
||||||
|
return;
|
||||||
|
} else if (this.refs.postlist && !this.hasScrolledToNewMessageSeparator) {
|
||||||
|
this.refs.postlist.scrollTop = this.refs.postlist.scrollHeight;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const prevPosts = prevProps.posts;
|
||||||
|
const posts = this.props.posts;
|
||||||
|
const postList = this.refs.postlist;
|
||||||
|
|
||||||
|
if (postList && prevPosts && posts && posts[0] && prevPosts[0]) {
|
||||||
|
// A new message was posted, so scroll to bottom if it was from current user
|
||||||
|
// or if user was already scrolled close to bottom
|
||||||
|
let doScrollToBottom = false;
|
||||||
|
if (posts[0].id !== prevPosts[0].id && posts[0].pending_post_id !== prevPosts[0].pending_post_id) {
|
||||||
|
// If already scrolled to bottom
|
||||||
|
if (this.wasAtBottom()) {
|
||||||
|
doScrollToBottom = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If new post was by current user
|
||||||
|
if (posts[0].user_id === this.props.currentUserId) {
|
||||||
|
doScrollToBottom = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If new post was ephemeral
|
||||||
|
if (Utils.isPostEphemeral(posts[0])) {
|
||||||
|
doScrollToBottom = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (doScrollToBottom) {
|
||||||
|
postList.scrollTop = postList.scrollHeight;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// New posts added at the top, maintain scroll position
|
||||||
|
if (this.previousScrollHeight !== this.refs.postlist.scrollHeight && posts[0].id === prevPosts[0].id) {
|
||||||
|
this.refs.postlist.scrollTop = this.previousScrollTop + (this.refs.postlist.scrollHeight - this.previousScrollHeight);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
handleScrollStop = () => {
|
||||||
|
this.setState({
|
||||||
|
isScrolling: false
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
wasAtBottom = () => {
|
||||||
|
return this.previousClientHeight + this.previousScrollTop >= this.previousScrollHeight - CLOSE_TO_BOTTOM_SCROLL_MARGIN;
|
||||||
|
}
|
||||||
|
|
||||||
|
handleResize = () => {
|
||||||
|
const postList = this.refs.postlist;
|
||||||
|
|
||||||
|
if (postList && this.wasAtBottom()) {
|
||||||
|
postList.scrollTop = postList.scrollHeight;
|
||||||
|
|
||||||
|
this.previousScrollHeight = postList.scrollHeight;
|
||||||
|
this.previousScrollTop = postList.scrollTop;
|
||||||
|
this.previousClientHeight = postList.clientHeight;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
loadPosts = async (channelId, focusedPostId) => {
|
||||||
|
let posts;
|
||||||
|
if (focusedPostId) {
|
||||||
|
const getPostThreadAsync = this.props.actions.getPostThread(focusedPostId);
|
||||||
|
const getPostsBeforeAsync = this.props.actions.getPostsBefore(channelId, focusedPostId, 0, POSTS_PER_PAGE);
|
||||||
|
const getPostsAfterAsync = this.props.actions.getPostsAfter(channelId, focusedPostId, 0, POSTS_PER_PAGE);
|
||||||
|
|
||||||
|
posts = await getPostsBeforeAsync;
|
||||||
|
await getPostsAfterAsync;
|
||||||
|
await getPostThreadAsync;
|
||||||
|
|
||||||
|
this.hasScrolledToFocusedPost = true;
|
||||||
|
} else {
|
||||||
|
posts = await this.props.actions.getPosts(channelId, 0, POSTS_PER_PAGE);
|
||||||
|
this.hasScrolledToNewMessageSeparator = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (posts && posts.order.length < POSTS_PER_PAGE) {
|
||||||
|
this.setState({atEnd: true});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
loadMorePosts = (e) => {
|
||||||
|
if (e) {
|
||||||
|
e.preventDefault();
|
||||||
|
}
|
||||||
|
|
||||||
|
this.props.actions.increasePostVisibility(this.props.channel.id, this.props.focusedPostId).then((moreToLoad) => {
|
||||||
|
this.setState({atEnd: !moreToLoad && this.props.posts.length < this.props.postVisibility});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
handleScroll = () => {
|
||||||
|
this.hasScrolledToFocusedPost = true;
|
||||||
|
this.hasScrolled = true;
|
||||||
|
this.previousScrollTop = this.refs.postlist.scrollTop;
|
||||||
|
|
||||||
|
this.updateFloatingTimestamp();
|
||||||
|
|
||||||
|
if (!this.state.isScrolling) {
|
||||||
|
this.setState({
|
||||||
|
isScrolling: true
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.wasAtBottom()) {
|
||||||
|
this.setState({
|
||||||
|
lastViewed: new Date().getTime(),
|
||||||
|
unViewedCount: 0,
|
||||||
|
isScrolling: false
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
this.scrollStopAction.fireAfter(Constants.SCROLL_DELAY);
|
||||||
|
}
|
||||||
|
|
||||||
|
updateFloatingTimestamp = () => {
|
||||||
|
// skip this in non-mobile view since that's when the timestamp is visible
|
||||||
|
if (!Utils.isMobile()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.props.posts) {
|
||||||
|
// iterate through posts starting at the bottom since users are more likely to be viewing newer posts
|
||||||
|
for (let i = 0; i < this.props.posts.length; i++) {
|
||||||
|
const post = this.props.posts[i];
|
||||||
|
const element = this.refs[post.id];
|
||||||
|
|
||||||
|
if (!element || !element.domNode || element.domNode.offsetTop + element.domNode.clientHeight <= this.refs.postlist.scrollTop) {
|
||||||
|
// this post is off the top of the screen so the last one is at the top of the screen
|
||||||
|
let topPost;
|
||||||
|
|
||||||
|
if (i > 0) {
|
||||||
|
topPost = this.props.posts[i - 1];
|
||||||
|
} else {
|
||||||
|
// the first post we look at should always be on the screen, but handle that case anyway
|
||||||
|
topPost = post;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.state.topPost || topPost.id !== this.state.topPost.id) {
|
||||||
|
this.setState({
|
||||||
|
topPost
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
scrollToBottom = () => {
|
||||||
|
this.refs.postlist.scrollTop = this.refs.postlist.scrollHeight;
|
||||||
|
}
|
||||||
|
|
||||||
|
createPosts = (posts) => {
|
||||||
|
const postCtls = [];
|
||||||
|
let previousPostDay = new Date(0);
|
||||||
|
const currentUserId = this.props.currentUserId;
|
||||||
|
const lastViewed = this.props.lastViewedAt || 0;
|
||||||
|
|
||||||
|
let renderedLastViewed = false;
|
||||||
|
|
||||||
|
for (let i = posts.length - 1; i >= 0; i--) {
|
||||||
|
const post = posts[i];
|
||||||
|
|
||||||
|
const postCtl = (
|
||||||
|
<Post
|
||||||
|
ref={post.id}
|
||||||
|
key={'post ' + (post.id || post.pending_post_id)}
|
||||||
|
post={post}
|
||||||
|
lastPostCount={(i >= 0 && i < Constants.TEST_ID_COUNT) ? i : -1}
|
||||||
|
getPostList={this.getPostList}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
const currentPostDay = Utils.getDateForUnixTicks(post.create_at);
|
||||||
|
if (currentPostDay.toDateString() !== previousPostDay.toDateString()) {
|
||||||
|
postCtls.push(
|
||||||
|
<div
|
||||||
|
key={currentPostDay.toDateString()}
|
||||||
|
className='date-separator'
|
||||||
|
>
|
||||||
|
<hr className='separator__hr'/>
|
||||||
|
<div className='separator__text'>
|
||||||
|
<FormattedDate
|
||||||
|
value={currentPostDay}
|
||||||
|
weekday='short'
|
||||||
|
month='short'
|
||||||
|
day='2-digit'
|
||||||
|
year='numeric'
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (post.user_id !== currentUserId &&
|
||||||
|
lastViewed !== 0 &&
|
||||||
|
post.create_at > lastViewed &&
|
||||||
|
!Utils.isPostEphemeral(post) &&
|
||||||
|
!renderedLastViewed) {
|
||||||
|
renderedLastViewed = true;
|
||||||
|
|
||||||
|
// Temporary fix to solve ie11 rendering issue
|
||||||
|
let newSeparatorId = '';
|
||||||
|
if (!UserAgent.isInternetExplorer()) {
|
||||||
|
newSeparatorId = 'new_message_' + post.id;
|
||||||
|
}
|
||||||
|
postCtls.push(
|
||||||
|
<div
|
||||||
|
id={newSeparatorId}
|
||||||
|
key='unviewed'
|
||||||
|
ref='newMessageSeparator'
|
||||||
|
className='new-separator'
|
||||||
|
>
|
||||||
|
<hr
|
||||||
|
className='separator__hr'
|
||||||
|
/>
|
||||||
|
<div className='separator__text'>
|
||||||
|
<FormattedMessage
|
||||||
|
id='posts_view.newMsg'
|
||||||
|
defaultMessage='New Messages'
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
postCtls.push(postCtl);
|
||||||
|
previousPostDay = currentPostDay;
|
||||||
|
}
|
||||||
|
|
||||||
|
return postCtls;
|
||||||
|
}
|
||||||
|
|
||||||
|
getPostList = () => {
|
||||||
|
return this.refs.postlist;
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const posts = this.props.posts;
|
||||||
|
const channel = this.props.channel;
|
||||||
|
|
||||||
|
if (posts == null || channel == null) {
|
||||||
|
return (
|
||||||
|
<div id='post-list'>
|
||||||
|
<LoadingScreen
|
||||||
|
position='absolute'
|
||||||
|
key='loading'
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let topRow;
|
||||||
|
if (this.state.atEnd) {
|
||||||
|
topRow = createChannelIntroMessage(channel, this.props.fullWidth);
|
||||||
|
} else if (this.props.postVisibility >= Constants.MAX_POST_VISIBILITY) {
|
||||||
|
topRow = (
|
||||||
|
<div className='post-list__loading post-list__loading-search'>
|
||||||
|
<FormattedMessage
|
||||||
|
id='posts_view.maxLoaded'
|
||||||
|
defaultMessage='Looking for a specific message? Try searching for it'
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
topRow = (
|
||||||
|
<a
|
||||||
|
ref='loadmoretop'
|
||||||
|
className='more-messages-text theme'
|
||||||
|
href='#'
|
||||||
|
onClick={this.loadMorePosts}
|
||||||
|
>
|
||||||
|
<FormattedMessage
|
||||||
|
id='posts_view.loadMore'
|
||||||
|
defaultMessage='Load more messages'
|
||||||
|
/>
|
||||||
|
</a>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const topPostCreateAt = this.state.topPost ? this.state.topPost.create_at : 0;
|
||||||
|
|
||||||
|
let postVisibility = this.props.postVisibility;
|
||||||
|
|
||||||
|
// In focus mode there's an extra (Constants.POST_CHUNK_SIZE / 2) posts to show
|
||||||
|
if (this.props.focusedPostId) {
|
||||||
|
postVisibility += Constants.POST_CHUNK_SIZE / 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div id='post-list'>
|
||||||
|
<FloatingTimestamp
|
||||||
|
isScrolling={this.state.isScrolling}
|
||||||
|
isMobile={Utils.isMobile()}
|
||||||
|
createAt={topPostCreateAt}
|
||||||
|
/>
|
||||||
|
<ScrollToBottomArrows
|
||||||
|
isScrolling={this.state.isScrolling}
|
||||||
|
atBottom={this.wasAtBottom()}
|
||||||
|
onClick={this.scrollToBottom}
|
||||||
|
/>
|
||||||
|
<NewMessageIndicator
|
||||||
|
newMessages={this.state.unViewedCount}
|
||||||
|
onClick={this.scrollToBottom}
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
ref='postlist'
|
||||||
|
className='post-list-holder-by-time'
|
||||||
|
key={'postlist-' + channel.id}
|
||||||
|
onScroll={this.handleScroll}
|
||||||
|
>
|
||||||
|
<div className='post-list__table'>
|
||||||
|
<div
|
||||||
|
ref='postlistcontent'
|
||||||
|
className='post-list__content'
|
||||||
|
>
|
||||||
|
{topRow}
|
||||||
|
{this.createPosts(posts.slice(0, postVisibility))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
41
webapp/components/post_view/post_message_view/index.js
Обычный файл
41
webapp/components/post_view/post_message_view/index.js
Обычный файл
@@ -0,0 +1,41 @@
|
|||||||
|
// Copyright (c) 2017 Mattermost, Inc. All Rights Reserved.
|
||||||
|
// See License.txt for license information.
|
||||||
|
|
||||||
|
import {connect} from 'react-redux';
|
||||||
|
import {getCustomEmojisAsMap} from 'mattermost-redux/selectors/entities/emojis';
|
||||||
|
import {getBool} from 'mattermost-redux/selectors/entities/preferences';
|
||||||
|
import {getCurrentUserMentionKeys, getUsersByUsername} from 'mattermost-redux/selectors/entities/users';
|
||||||
|
|
||||||
|
import {getCurrentTeam} from 'mattermost-redux/selectors/entities/teams';
|
||||||
|
|
||||||
|
import {Preferences} from 'mattermost-redux/constants';
|
||||||
|
import {getSiteURL} from 'utils/url.jsx';
|
||||||
|
|
||||||
|
import {EmojiMap} from 'stores/emoji_store.jsx';
|
||||||
|
|
||||||
|
import PostMessageView from './post_message_view.jsx';
|
||||||
|
|
||||||
|
function makeMapStateToProps() {
|
||||||
|
let emojiMap;
|
||||||
|
let oldCustomEmoji;
|
||||||
|
|
||||||
|
return function mapStateToProps(state, ownProps) {
|
||||||
|
const newCustomEmoji = getCustomEmojisAsMap(state);
|
||||||
|
if (newCustomEmoji !== oldCustomEmoji) {
|
||||||
|
emojiMap = new EmojiMap(newCustomEmoji);
|
||||||
|
}
|
||||||
|
oldCustomEmoji = newCustomEmoji;
|
||||||
|
|
||||||
|
return {
|
||||||
|
...ownProps,
|
||||||
|
emojis: emojiMap,
|
||||||
|
enableFormatting: getBool(state, Preferences.CATEGORY_ADVANCED_SETTINGS, 'formatting', true),
|
||||||
|
mentionKeys: getCurrentUserMentionKeys(state),
|
||||||
|
usernameMap: getUsersByUsername(state),
|
||||||
|
team: getCurrentTeam(state),
|
||||||
|
siteUrl: getSiteURL()
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export default connect(makeMapStateToProps)(PostMessageView);
|
||||||
@@ -1,72 +1,74 @@
|
|||||||
import PropTypes from 'prop-types';
|
|
||||||
|
|
||||||
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
|
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
|
||||||
// See License.txt for license information.
|
// See License.txt for license information.
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
import {FormattedMessage} from 'react-intl';
|
import {FormattedMessage} from 'react-intl';
|
||||||
|
|
||||||
import Constants from 'utils/constants.jsx';
|
|
||||||
import * as PostUtils from 'utils/post_utils.jsx';
|
import * as PostUtils from 'utils/post_utils.jsx';
|
||||||
import * as TextFormatting from 'utils/text_formatting.jsx';
|
import * as TextFormatting from 'utils/text_formatting.jsx';
|
||||||
import * as Utils from 'utils/utils.jsx';
|
import * as Utils from 'utils/utils.jsx';
|
||||||
import {getSiteURL} from 'utils/url.jsx';
|
|
||||||
|
import {getChannelsNameMapInCurrentTeam} from 'mattermost-redux/selectors/entities/channels';
|
||||||
|
import {Posts} from 'mattermost-redux/constants';
|
||||||
|
import store from 'stores/redux_store.jsx';
|
||||||
|
|
||||||
import {renderSystemMessage} from './system_message_helpers.jsx';
|
import {renderSystemMessage} from './system_message_helpers.jsx';
|
||||||
|
|
||||||
export default class PostMessageView extends React.Component {
|
export default class PostMessageView extends React.PureComponent {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
options: PropTypes.object.isRequired,
|
|
||||||
|
/*
|
||||||
|
* The post to render the message for
|
||||||
|
*/
|
||||||
post: PropTypes.object.isRequired,
|
post: PropTypes.object.isRequired,
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Object using emoji names as keys with custom emojis as the values
|
||||||
|
*/
|
||||||
emojis: PropTypes.object.isRequired,
|
emojis: PropTypes.object.isRequired,
|
||||||
enableFormatting: PropTypes.bool.isRequired,
|
|
||||||
mentionKeys: PropTypes.arrayOf(PropTypes.string).isRequired,
|
/*
|
||||||
usernameMap: PropTypes.object.isRequired,
|
* The team the post was made in
|
||||||
channelNamesMap: PropTypes.object.isRequired,
|
*/
|
||||||
team: PropTypes.object.isRequired,
|
team: PropTypes.object.isRequired,
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Set to enable Markdown formatting
|
||||||
|
*/
|
||||||
|
enableFormatting: PropTypes.bool,
|
||||||
|
|
||||||
|
/*
|
||||||
|
* An array of words that can be used to mention a user
|
||||||
|
*/
|
||||||
|
mentionKeys: PropTypes.arrayOf(PropTypes.string),
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Object mapping usernames to users
|
||||||
|
*/
|
||||||
|
usernameMap: PropTypes.object,
|
||||||
|
|
||||||
|
/*
|
||||||
|
* The URL that the app is hosted on
|
||||||
|
*/
|
||||||
|
siteUrl: PropTypes.string,
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Options specific to text formatting
|
||||||
|
*/
|
||||||
|
options: PropTypes.object,
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Post identifiers for selenium tests
|
||||||
|
*/
|
||||||
lastPostCount: PropTypes.number
|
lastPostCount: PropTypes.number
|
||||||
};
|
};
|
||||||
|
|
||||||
shouldComponentUpdate(nextProps) {
|
static defaultProps = {
|
||||||
if (!Utils.areObjectsEqual(nextProps.options, this.props.options)) {
|
options: {},
|
||||||
return true;
|
mentionKeys: [],
|
||||||
}
|
usernameMap: {}
|
||||||
|
};
|
||||||
if (nextProps.post.message !== this.props.post.message) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (nextProps.post.state !== this.props.post.state) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (nextProps.post.type !== this.props.post.type) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// emojis are immutable
|
|
||||||
if (nextProps.emojis !== this.props.emojis) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (nextProps.enableFormatting !== this.props.enableFormatting) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!Utils.areObjectsEqual(nextProps.mentionKeys, this.props.mentionKeys)) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (nextProps.lastPostCount !== this.props.lastPostCount) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Don't check if props.usernameMap changes since it is very large and inefficient to do so.
|
|
||||||
// This mimics previous behaviour, but could be changed if we decide it's worth it.
|
|
||||||
// The same choice (and reasoning) is also applied to the this.props.channelNamesMap.
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
renderDeletedPost() {
|
renderDeletedPost() {
|
||||||
return (
|
return (
|
||||||
@@ -95,7 +97,7 @@ export default class PostMessageView extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
if (this.props.post.state === Constants.POST_DELETED) {
|
if (this.props.post.state === Posts.POST_DELETED) {
|
||||||
return this.renderDeletedPost();
|
return this.renderDeletedPost();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -105,10 +107,10 @@ export default class PostMessageView extends React.Component {
|
|||||||
|
|
||||||
const options = Object.assign({}, this.props.options, {
|
const options = Object.assign({}, this.props.options, {
|
||||||
emojis: this.props.emojis,
|
emojis: this.props.emojis,
|
||||||
siteURL: getSiteURL(),
|
siteURL: this.props.siteUrl,
|
||||||
mentionKeys: this.props.mentionKeys,
|
mentionKeys: this.props.mentionKeys,
|
||||||
usernameMap: this.props.usernameMap,
|
usernameMap: this.props.usernameMap,
|
||||||
channelNamesMap: this.props.channelNamesMap,
|
channelNamesMap: getChannelsNameMapInCurrentTeam(store.getState()),
|
||||||
team: this.props.team
|
team: this.props.team
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -1,23 +1,41 @@
|
|||||||
import PropTypes from 'prop-types';
|
|
||||||
|
|
||||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||||
// See License.txt for license information.
|
// See License.txt for license information.
|
||||||
|
|
||||||
import React from 'react';
|
|
||||||
|
|
||||||
import Constants from 'utils/constants.jsx';
|
import Constants from 'utils/constants.jsx';
|
||||||
import PureRenderMixin from 'react-addons-pure-render-mixin';
|
|
||||||
|
|
||||||
import {getDateForUnixTicks, isMobile, updateWindowDimensions} from 'utils/utils.jsx';
|
import {getDateForUnixTicks, isMobile, updateWindowDimensions} from 'utils/utils.jsx';
|
||||||
|
|
||||||
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
import {Link} from 'react-router/es6';
|
import {Link} from 'react-router/es6';
|
||||||
import TeamStore from 'stores/team_store.jsx';
|
import TeamStore from 'stores/team_store.jsx';
|
||||||
|
|
||||||
export default class PostTime extends React.Component {
|
export default class PostTime extends React.PureComponent {
|
||||||
|
static propTypes = {
|
||||||
|
|
||||||
|
/*
|
||||||
|
* The time to display
|
||||||
|
*/
|
||||||
|
eventTime: PropTypes.number.isRequired,
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Set to display using 24 hour format
|
||||||
|
*/
|
||||||
|
useMilitaryTime: PropTypes.bool,
|
||||||
|
|
||||||
|
/*
|
||||||
|
* The post id of posting being rendered
|
||||||
|
*/
|
||||||
|
postId: PropTypes.string
|
||||||
|
}
|
||||||
|
|
||||||
|
static defaultProps = {
|
||||||
|
eventTime: 0,
|
||||||
|
useMilitaryTime: false
|
||||||
|
}
|
||||||
|
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
|
|
||||||
this.shouldComponentUpdate = PureRenderMixin.shouldComponentUpdate.bind(this);
|
|
||||||
this.state = {
|
this.state = {
|
||||||
currentTeamDisplayName: TeamStore.getCurrent().name,
|
currentTeamDisplayName: TeamStore.getCurrent().name,
|
||||||
width: '',
|
width: '',
|
||||||
@@ -56,29 +74,18 @@ export default class PostTime extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
return isMobile() ?
|
if (isMobile()) {
|
||||||
this.renderTimeTag() :
|
return this.renderTimeTag();
|
||||||
(
|
}
|
||||||
<Link
|
|
||||||
to={`/${this.state.currentTeamDisplayName}/pl/${this.props.postId}`}
|
return (
|
||||||
target='_blank'
|
<Link
|
||||||
className='post__permalink'
|
to={`/${this.state.currentTeamDisplayName}/pl/${this.props.postId}`}
|
||||||
>
|
target='_blank'
|
||||||
{this.renderTimeTag()}
|
className='post__permalink'
|
||||||
</Link>
|
>
|
||||||
);
|
{this.renderTimeTag()}
|
||||||
|
</Link>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
PostTime.defaultProps = {
|
|
||||||
eventTime: 0,
|
|
||||||
sameUser: false
|
|
||||||
};
|
|
||||||
|
|
||||||
PostTime.propTypes = {
|
|
||||||
eventTime: PropTypes.number.isRequired,
|
|
||||||
sameUser: PropTypes.bool,
|
|
||||||
compactDisplay: PropTypes.bool,
|
|
||||||
useMilitaryTime: PropTypes.bool.isRequired,
|
|
||||||
postId: PropTypes.string
|
|
||||||
};
|
|
||||||
@@ -1,98 +0,0 @@
|
|||||||
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
|
|
||||||
// See License.txt for license information
|
|
||||||
|
|
||||||
import PostViewController from './post_view_controller.jsx';
|
|
||||||
|
|
||||||
import ChannelStore from 'stores/channel_store.jsx';
|
|
||||||
import UserStore from 'stores/user_store.jsx';
|
|
||||||
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
|
|
||||||
import React from 'react';
|
|
||||||
|
|
||||||
const MAXIMUM_CACHED_VIEWS = 5;
|
|
||||||
|
|
||||||
export default class PostViewCache extends React.Component {
|
|
||||||
static propTypes = {
|
|
||||||
actions: PropTypes.shape({
|
|
||||||
viewChannel: PropTypes.func.isRequired
|
|
||||||
}).isRequired
|
|
||||||
}
|
|
||||||
|
|
||||||
constructor(props) {
|
|
||||||
super(props);
|
|
||||||
|
|
||||||
this.onChannelChange = this.onChannelChange.bind(this);
|
|
||||||
|
|
||||||
const currentChannelId = ChannelStore.getCurrentId();
|
|
||||||
const channel = ChannelStore.getCurrent();
|
|
||||||
|
|
||||||
this.state = {
|
|
||||||
currentChannelId,
|
|
||||||
channels: channel ? [channel] : []
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidMount() {
|
|
||||||
ChannelStore.addChangeListener(this.onChannelChange);
|
|
||||||
}
|
|
||||||
|
|
||||||
componentWillUnmount() {
|
|
||||||
if (UserStore.getCurrentUser()) {
|
|
||||||
this.props.actions.viewChannel('', this.state.currentChannelId || '');
|
|
||||||
}
|
|
||||||
ChannelStore.removeChangeListener(this.onChannelChange);
|
|
||||||
}
|
|
||||||
|
|
||||||
onChannelChange() {
|
|
||||||
const channels = Object.assign([], this.state.channels);
|
|
||||||
const currentChannel = ChannelStore.getCurrent();
|
|
||||||
|
|
||||||
if (!currentChannel) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// make sure current channel really changed
|
|
||||||
if (currentChannel.id === this.state.currentChannelId) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (channels.length > MAXIMUM_CACHED_VIEWS) {
|
|
||||||
channels.shift();
|
|
||||||
}
|
|
||||||
|
|
||||||
const index = channels.map((c) => c.id).indexOf(currentChannel.id);
|
|
||||||
if (index !== -1) {
|
|
||||||
channels.splice(index, 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
channels.push(currentChannel);
|
|
||||||
|
|
||||||
this.setState({
|
|
||||||
currentChannelId: currentChannel.id,
|
|
||||||
channels
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const channels = this.state.channels;
|
|
||||||
const currentChannelId = this.state.currentChannelId;
|
|
||||||
|
|
||||||
const postViews = [];
|
|
||||||
for (let i = 0; i < channels.length; i++) {
|
|
||||||
postViews.push(
|
|
||||||
<PostViewController
|
|
||||||
key={'postviewcontroller_' + channels[i].id}
|
|
||||||
channel={channels[i]}
|
|
||||||
active={channels[i].id === currentChannelId}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div id='post-list'>
|
|
||||||
{postViews}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,404 +0,0 @@
|
|||||||
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
|
|
||||||
// See License.txt for license information.
|
|
||||||
|
|
||||||
import PostList from './components/post_list.jsx';
|
|
||||||
import LoadingScreen from 'components/loading_screen.jsx';
|
|
||||||
|
|
||||||
import PreferenceStore from 'stores/preference_store.jsx';
|
|
||||||
import UserStore from 'stores/user_store.jsx';
|
|
||||||
import PostStore from 'stores/post_store.jsx';
|
|
||||||
import ChannelStore from 'stores/channel_store.jsx';
|
|
||||||
import TeamStore from 'stores/team_store.jsx';
|
|
||||||
import WebrtcStore from 'stores/webrtc_store.jsx';
|
|
||||||
|
|
||||||
import * as Utils from 'utils/utils.jsx';
|
|
||||||
|
|
||||||
import Constants from 'utils/constants.jsx';
|
|
||||||
const Preferences = Constants.Preferences;
|
|
||||||
const ScrollTypes = Constants.ScrollTypes;
|
|
||||||
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
|
|
||||||
import React from 'react';
|
|
||||||
|
|
||||||
export default class PostViewController extends React.Component {
|
|
||||||
constructor(props) {
|
|
||||||
super(props);
|
|
||||||
|
|
||||||
this.onPreferenceChange = this.onPreferenceChange.bind(this);
|
|
||||||
this.onUserChange = this.onUserChange.bind(this);
|
|
||||||
this.onPostsChange = this.onPostsChange.bind(this);
|
|
||||||
this.onTeamChange = this.onTeamChange.bind(this);
|
|
||||||
this.onStatusChange = this.onStatusChange.bind(this);
|
|
||||||
this.onPostsViewJumpRequest = this.onPostsViewJumpRequest.bind(this);
|
|
||||||
this.onSetNewMessageIndicator = this.onSetNewMessageIndicator.bind(this);
|
|
||||||
this.onPostListScroll = this.onPostListScroll.bind(this);
|
|
||||||
this.onActivate = this.onActivate.bind(this);
|
|
||||||
this.onDeactivate = this.onDeactivate.bind(this);
|
|
||||||
this.onBusy = this.onBusy.bind(this);
|
|
||||||
|
|
||||||
const channel = props.channel;
|
|
||||||
const profiles = UserStore.getProfiles();
|
|
||||||
|
|
||||||
let lastViewed = Number.MAX_VALUE;
|
|
||||||
let lastViewedBottom = Number.MAX_VALUE;
|
|
||||||
const member = ChannelStore.getMyMember(channel.id);
|
|
||||||
if (member != null) {
|
|
||||||
lastViewed = member.last_viewed_at;
|
|
||||||
lastViewedBottom = member.last_viewed_at;
|
|
||||||
}
|
|
||||||
|
|
||||||
const joinLeaveEnabled = PreferenceStore.getBool(Constants.Preferences.CATEGORY_ADVANCED_SETTINGS, 'join_leave', true);
|
|
||||||
|
|
||||||
const statuses = Object.assign({}, UserStore.getStatuses());
|
|
||||||
|
|
||||||
// If we haven't received a page time then we aren't done loading the posts yet
|
|
||||||
const loading = PostStore.getLatestPostFromPageTime(channel.id) === 0;
|
|
||||||
|
|
||||||
this.state = {
|
|
||||||
channel,
|
|
||||||
postList: PostStore.filterPosts(channel.id, joinLeaveEnabled),
|
|
||||||
currentUser: UserStore.getCurrentUser(),
|
|
||||||
currentTeamId: TeamStore.getCurrentId(),
|
|
||||||
isBusy: WebrtcStore.isBusy(),
|
|
||||||
profiles,
|
|
||||||
statuses,
|
|
||||||
atTop: PostStore.getVisibilityAtTop(channel.id),
|
|
||||||
lastViewed,
|
|
||||||
lastViewedBottom,
|
|
||||||
ownNewMessage: false,
|
|
||||||
loading,
|
|
||||||
scrollType: ScrollTypes.NEW_MESSAGE,
|
|
||||||
displayNameType: PreferenceStore.get(Preferences.CATEGORY_DISPLAY_SETTINGS, 'name_format', 'false'),
|
|
||||||
displayPostsInCenter: PreferenceStore.get(Preferences.CATEGORY_DISPLAY_SETTINGS, Preferences.CHANNEL_DISPLAY_MODE, Preferences.CHANNEL_DISPLAY_MODE_DEFAULT) === Preferences.CHANNEL_DISPLAY_MODE_CENTERED,
|
|
||||||
compactDisplay: PreferenceStore.get(Preferences.CATEGORY_DISPLAY_SETTINGS, Preferences.MESSAGE_DISPLAY, Preferences.MESSAGE_DISPLAY_DEFAULT) === Preferences.MESSAGE_DISPLAY_COMPACT,
|
|
||||||
previewsCollapsed: PreferenceStore.get(Preferences.CATEGORY_DISPLAY_SETTINGS, Preferences.COLLAPSE_DISPLAY, 'false'),
|
|
||||||
useMilitaryTime: PreferenceStore.getBool(Constants.Preferences.CATEGORY_DISPLAY_SETTINGS, Preferences.USE_MILITARY_TIME, false),
|
|
||||||
flaggedPosts: PreferenceStore.getCategory(Constants.Preferences.CATEGORY_FLAGGED_POST)
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidMount() {
|
|
||||||
if (this.props.active) {
|
|
||||||
this.onActivate();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
componentWillUnmount() {
|
|
||||||
if (this.props.active) {
|
|
||||||
this.onDeactivate();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onPreferenceChange(category) {
|
|
||||||
// Bit of a hack to force render when this setting is updated
|
|
||||||
// regardless of change
|
|
||||||
let previewSuffix = '';
|
|
||||||
if (category === Preferences.CATEGORY_DISPLAY_SETTINGS) {
|
|
||||||
previewSuffix = '_' + Utils.generateId();
|
|
||||||
}
|
|
||||||
|
|
||||||
const joinLeaveEnabled = PreferenceStore.getBool(Constants.Preferences.CATEGORY_ADVANCED_SETTINGS, 'join_leave', true);
|
|
||||||
|
|
||||||
this.setState({
|
|
||||||
postList: PostStore.filterPosts(this.state.channel.id, joinLeaveEnabled),
|
|
||||||
displayNameType: PreferenceStore.get(Preferences.CATEGORY_DISPLAY_SETTINGS, 'name_format', 'false'),
|
|
||||||
displayPostsInCenter: PreferenceStore.get(Preferences.CATEGORY_DISPLAY_SETTINGS, Preferences.CHANNEL_DISPLAY_MODE, Preferences.CHANNEL_DISPLAY_MODE_DEFAULT) === Preferences.CHANNEL_DISPLAY_MODE_CENTERED,
|
|
||||||
compactDisplay: PreferenceStore.get(Preferences.CATEGORY_DISPLAY_SETTINGS, Preferences.MESSAGE_DISPLAY, Preferences.MESSAGE_DISPLAY_DEFAULT) === Preferences.MESSAGE_DISPLAY_COMPACT,
|
|
||||||
previewsCollapsed: PreferenceStore.get(Preferences.CATEGORY_DISPLAY_SETTINGS, Preferences.COLLAPSE_DISPLAY, 'false') + previewSuffix,
|
|
||||||
useMilitaryTime: PreferenceStore.getBool(Constants.Preferences.CATEGORY_DISPLAY_SETTINGS, Preferences.USE_MILITARY_TIME, false),
|
|
||||||
flaggedPosts: PreferenceStore.getCategory(Constants.Preferences.CATEGORY_FLAGGED_POST)
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
onUserChange() {
|
|
||||||
this.setState({currentUser: UserStore.getCurrentUser(), profiles: JSON.parse(JSON.stringify(UserStore.getProfiles()))});
|
|
||||||
}
|
|
||||||
|
|
||||||
onPostsChange() {
|
|
||||||
const joinLeaveEnabled = PreferenceStore.getBool(Constants.Preferences.CATEGORY_ADVANCED_SETTINGS, 'join_leave', true);
|
|
||||||
const loading = PostStore.getLatestPostFromPageTime(this.state.channel.id) === 0;
|
|
||||||
|
|
||||||
const newState = {
|
|
||||||
postList: PostStore.filterPosts(this.state.channel.id, joinLeaveEnabled),
|
|
||||||
atTop: PostStore.getVisibilityAtTop(this.state.channel.id),
|
|
||||||
loading
|
|
||||||
};
|
|
||||||
|
|
||||||
if (this.state.loading && !loading) {
|
|
||||||
newState.scrollType = ScrollTypes.NEW_MESSAGE;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.setState(newState);
|
|
||||||
}
|
|
||||||
|
|
||||||
onStatusChange() {
|
|
||||||
this.setState({statuses: Object.assign({}, UserStore.getStatuses())});
|
|
||||||
}
|
|
||||||
|
|
||||||
onTeamChange() {
|
|
||||||
const currentTeamId = TeamStore.getCurrentId();
|
|
||||||
if ((this.state.channel.type === Constants.OPEN_CHANNEL || this.state.channel.type === Constants.PRIVATE_CHANNEL) && this.state.channel.team_id !== currentTeamId) {
|
|
||||||
this.setState({
|
|
||||||
currentTeamId,
|
|
||||||
loading: true
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onActivate() {
|
|
||||||
PreferenceStore.addChangeListener(this.onPreferenceChange);
|
|
||||||
UserStore.addChangeListener(this.onUserChange);
|
|
||||||
TeamStore.addChangeListener(this.onTeamChange);
|
|
||||||
UserStore.addStatusesChangeListener(this.onStatusChange);
|
|
||||||
PostStore.addChangeListener(this.onPostsChange);
|
|
||||||
PostStore.addPostsViewJumpListener(this.onPostsViewJumpRequest);
|
|
||||||
ChannelStore.addLastViewedListener(this.onSetNewMessageIndicator);
|
|
||||||
WebrtcStore.addBusyListener(this.onBusy);
|
|
||||||
}
|
|
||||||
|
|
||||||
onDeactivate() {
|
|
||||||
PreferenceStore.removeChangeListener(this.onPreferenceChange);
|
|
||||||
UserStore.removeChangeListener(this.onUserChange);
|
|
||||||
TeamStore.removeChangeListener(this.onTeamChange);
|
|
||||||
UserStore.removeStatusesChangeListener(this.onStatusChange);
|
|
||||||
PostStore.removeChangeListener(this.onPostsChange);
|
|
||||||
PostStore.removePostsViewJumpListener(this.onPostsViewJumpRequest);
|
|
||||||
ChannelStore.removeLastViewedListener(this.onSetNewMessageIndicator);
|
|
||||||
WebrtcStore.removeBusyListener(this.onBusy);
|
|
||||||
}
|
|
||||||
|
|
||||||
componentWillReceiveProps(nextProps) {
|
|
||||||
if (this.props.active && !nextProps.active) {
|
|
||||||
this.onDeactivate();
|
|
||||||
} else if (!this.props.active && nextProps.active) {
|
|
||||||
this.onActivate();
|
|
||||||
|
|
||||||
const channel = nextProps.channel;
|
|
||||||
|
|
||||||
let lastViewed = Number.MAX_VALUE;
|
|
||||||
const member = ChannelStore.getMyMember(channel.id);
|
|
||||||
if (member != null) {
|
|
||||||
lastViewed = member.last_viewed_at;
|
|
||||||
}
|
|
||||||
|
|
||||||
const profiles = UserStore.getProfiles();
|
|
||||||
|
|
||||||
const joinLeaveEnabled = PreferenceStore.getBool(Constants.Preferences.CATEGORY_ADVANCED_SETTINGS, 'join_leave', true);
|
|
||||||
|
|
||||||
const statuses = Object.assign({}, UserStore.getStatuses());
|
|
||||||
|
|
||||||
this.setState({
|
|
||||||
channel,
|
|
||||||
lastViewed,
|
|
||||||
ownNewMessage: false,
|
|
||||||
profiles: JSON.parse(JSON.stringify(profiles)),
|
|
||||||
statuses,
|
|
||||||
postList: PostStore.filterPosts(channel.id, joinLeaveEnabled),
|
|
||||||
displayNameType: PreferenceStore.get(Preferences.CATEGORY_DISPLAY_SETTINGS, 'name_format', 'false'),
|
|
||||||
displayPostsInCenter: PreferenceStore.get(Preferences.CATEGORY_DISPLAY_SETTINGS, Preferences.CHANNEL_DISPLAY_MODE, Preferences.CHANNEL_DISPLAY_MODE_DEFAULT) === Preferences.CHANNEL_DISPLAY_MODE_CENTERED,
|
|
||||||
compactDisplay: PreferenceStore.get(Preferences.CATEGORY_DISPLAY_SETTINGS, Preferences.MESSAGE_DISPLAY, Preferences.MESSAGE_DISPLAY_DEFAULT) === Preferences.MESSAGE_DISPLAY_COMPACT,
|
|
||||||
previewsCollapsed: PreferenceStore.get(Preferences.CATEGORY_DISPLAY_SETTINGS, Preferences.COLLAPSE_DISPLAY, 'false'),
|
|
||||||
useMilitaryTime: PreferenceStore.getBool(Constants.Preferences.CATEGORY_DISPLAY_SETTINGS, Preferences.USE_MILITARY_TIME, false),
|
|
||||||
scrollType: ScrollTypes.NEW_MESSAGE
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onPostsViewJumpRequest(type, postId) {
|
|
||||||
switch (type) {
|
|
||||||
case Constants.PostsViewJumpTypes.BOTTOM: {
|
|
||||||
let lastViewedBottom;
|
|
||||||
const lastPost = PostStore.getLatestPost(this.state.channel.id);
|
|
||||||
|
|
||||||
if (lastPost && lastPost.create_at) {
|
|
||||||
lastViewedBottom = lastPost.create_at;
|
|
||||||
} else {
|
|
||||||
lastViewedBottom = new Date().getTime();
|
|
||||||
}
|
|
||||||
|
|
||||||
this.setState({
|
|
||||||
scrollType: ScrollTypes.BOTTOM,
|
|
||||||
lastViewedBottom
|
|
||||||
});
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case Constants.PostsViewJumpTypes.POST:
|
|
||||||
this.setState({
|
|
||||||
scrollType: ScrollTypes.POST,
|
|
||||||
scrollPostId: postId
|
|
||||||
});
|
|
||||||
break;
|
|
||||||
case Constants.PostsViewJumpTypes.SIDEBAR_OPEN:
|
|
||||||
this.setState({scrollType: ScrollTypes.SIDEBAR_OPEN});
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onSetNewMessageIndicator() {
|
|
||||||
let lastViewed = Number.MAX_VALUE;
|
|
||||||
const member = ChannelStore.getMyMember(this.props.channel.id);
|
|
||||||
if (member != null) {
|
|
||||||
lastViewed = member.last_viewed_at;
|
|
||||||
}
|
|
||||||
this.setState({lastViewed});
|
|
||||||
}
|
|
||||||
|
|
||||||
onPostListScroll(atBottom) {
|
|
||||||
if (atBottom) {
|
|
||||||
let lastViewedBottom;
|
|
||||||
const lastPost = PostStore.getLatestPost(this.state.channel.id);
|
|
||||||
|
|
||||||
if (lastPost && lastPost.create_at) {
|
|
||||||
lastViewedBottom = lastPost.create_at;
|
|
||||||
} else {
|
|
||||||
lastViewedBottom = new Date().getTime();
|
|
||||||
}
|
|
||||||
|
|
||||||
this.setState({scrollType: ScrollTypes.BOTTOM, lastViewedBottom});
|
|
||||||
} else {
|
|
||||||
this.setState({scrollType: ScrollTypes.FREE});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onBusy(isBusy) {
|
|
||||||
this.setState({isBusy});
|
|
||||||
}
|
|
||||||
|
|
||||||
shouldComponentUpdate(nextProps, nextState) {
|
|
||||||
if (nextProps.active !== this.props.active) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (nextState.loading !== this.state.loading) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (nextState.atTop !== this.state.atTop) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (nextState.displayNameType !== this.state.displayNameType) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (nextState.displayPostsInCenter !== this.state.displayPostsInCenter) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (nextState.compactDisplay !== this.state.compactDisplay) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (nextState.previewsCollapsed !== this.state.previewsCollapsed) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (nextState.useMilitaryTime !== this.state.useMilitaryTime) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!Utils.areObjectsEqual(nextState.flaggedPosts, this.state.flaggedPosts)) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (nextState.lastViewed !== this.state.lastViewed) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (nextState.ownNewMessage !== this.state.ownNewMessage) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (nextState.showMoreMessagesTop !== this.state.showMoreMessagesTop) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (nextState.scrollType !== this.state.scrollType) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (nextState.scrollPostId !== this.state.scrollPostId) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (nextProps.channel.id !== this.props.channel.id) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!Utils.areObjectsEqual(nextState.currentUser, this.state.currentUser)) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!Utils.areObjectsEqual(nextState.statuses, this.state.statuses)) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!Utils.areObjectsEqual(nextState.postList, this.state.postList)) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!Utils.areObjectsEqual(nextState.profiles, this.state.profiles)) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (nextState.isBusy !== this.state.isBusy) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
let content;
|
|
||||||
if (this.state.postList == null || this.state.loading) {
|
|
||||||
content = (
|
|
||||||
<LoadingScreen
|
|
||||||
position='absolute'
|
|
||||||
key='loading'
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
content = (
|
|
||||||
<PostList
|
|
||||||
postList={this.state.postList}
|
|
||||||
profiles={this.state.profiles}
|
|
||||||
channelId={this.state.channel.id}
|
|
||||||
channel={this.state.channel}
|
|
||||||
currentUser={this.state.currentUser}
|
|
||||||
showMoreMessagesTop={!this.state.atTop}
|
|
||||||
scrollType={this.state.scrollType}
|
|
||||||
scrollPostId={this.state.scrollPostId}
|
|
||||||
postListScrolled={this.onPostListScroll}
|
|
||||||
displayNameType={this.state.displayNameType}
|
|
||||||
displayPostsInCenter={this.state.displayPostsInCenter}
|
|
||||||
compactDisplay={this.state.compactDisplay}
|
|
||||||
previewsCollapsed={this.state.previewsCollapsed}
|
|
||||||
useMilitaryTime={this.state.useMilitaryTime}
|
|
||||||
flaggedPosts={this.state.flaggedPosts}
|
|
||||||
lastViewed={this.state.lastViewed}
|
|
||||||
lastViewedBottom={this.state.lastViewedBottom}
|
|
||||||
ownNewMessage={this.state.ownNewMessage}
|
|
||||||
statuses={this.state.statuses}
|
|
||||||
isBusy={this.state.isBusy}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
let activeClass = '';
|
|
||||||
if (!this.props.active) {
|
|
||||||
activeClass = 'inactive';
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className={activeClass}>
|
|
||||||
{content}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
PostViewController.propTypes = {
|
|
||||||
channel: PropTypes.object,
|
|
||||||
active: PropTypes.bool
|
|
||||||
};
|
|
||||||
47
webapp/components/post_view/reaction/index.js
Обычный файл
47
webapp/components/post_view/reaction/index.js
Обычный файл
@@ -0,0 +1,47 @@
|
|||||||
|
// Copyright (c) 2017 Mattermost, Inc. All Rights Reserved.
|
||||||
|
// See License.txt for license information.
|
||||||
|
|
||||||
|
import {connect} from 'react-redux';
|
||||||
|
import {bindActionCreators} from 'redux';
|
||||||
|
import {getCurrentUserId, makeGetProfilesForReactions} from 'mattermost-redux/selectors/entities/users';
|
||||||
|
import {getMissingProfilesByIds} from 'mattermost-redux/actions/users';
|
||||||
|
import {addReaction, removeReaction} from 'mattermost-redux/actions/posts';
|
||||||
|
import {getEmojiImageUrl} from 'mattermost-redux/utils/emoji_utils';
|
||||||
|
import * as Emoji from 'utils/emoji.jsx';
|
||||||
|
|
||||||
|
import Reaction from './reaction.jsx';
|
||||||
|
|
||||||
|
function makeMapStateToProps() {
|
||||||
|
const getProfilesForReactions = makeGetProfilesForReactions();
|
||||||
|
|
||||||
|
return function mapStateToProps(state, ownProps) {
|
||||||
|
const profiles = getProfilesForReactions(state, ownProps.reactions);
|
||||||
|
let emoji;
|
||||||
|
if (Emoji.EmojiIndicesByAlias.has(ownProps.emojiName)) {
|
||||||
|
emoji = Emoji.Emojis[Emoji.EmojiIndicesByAlias.get(ownProps.emojiName)];
|
||||||
|
} else {
|
||||||
|
emoji = ownProps.emojis[ownProps.emojiName];
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
...ownProps,
|
||||||
|
profiles,
|
||||||
|
otherUsersCount: ownProps.reactions.length - profiles.length,
|
||||||
|
currentUserId: getCurrentUserId(state),
|
||||||
|
reactionCount: ownProps.reactions.length,
|
||||||
|
emojiImageUrl: getEmojiImageUrl(emoji)
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function mapDispatchToProps(dispatch) {
|
||||||
|
return {
|
||||||
|
actions: bindActionCreators({
|
||||||
|
addReaction,
|
||||||
|
removeReaction,
|
||||||
|
getMissingProfilesByIds
|
||||||
|
}, dispatch)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export default connect(makeMapStateToProps, mapDispatchToProps)(Reaction);
|
||||||
@@ -1,28 +1,66 @@
|
|||||||
import PropTypes from 'prop-types';
|
|
||||||
|
|
||||||
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
|
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
|
||||||
// See License.txt for license information.
|
// See License.txt for license information.
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
import {OverlayTrigger, Tooltip} from 'react-bootstrap';
|
import {OverlayTrigger, Tooltip} from 'react-bootstrap';
|
||||||
import {FormattedMessage} from 'react-intl';
|
import {FormattedMessage} from 'react-intl';
|
||||||
|
|
||||||
import EmojiStore from 'stores/emoji_store.jsx';
|
|
||||||
|
|
||||||
import * as Utils from 'utils/utils.jsx';
|
import * as Utils from 'utils/utils.jsx';
|
||||||
|
|
||||||
export default class Reaction extends React.Component {
|
export default class Reaction extends React.PureComponent {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
|
|
||||||
|
/*
|
||||||
|
* The post to render the reaction for
|
||||||
|
*/
|
||||||
post: PropTypes.object.isRequired,
|
post: PropTypes.object.isRequired,
|
||||||
|
|
||||||
|
/*
|
||||||
|
* The user id of the logged in user
|
||||||
|
*/
|
||||||
currentUserId: PropTypes.string.isRequired,
|
currentUserId: PropTypes.string.isRequired,
|
||||||
|
|
||||||
|
/*
|
||||||
|
* The name of the emoji for the reaction
|
||||||
|
*/
|
||||||
emojiName: PropTypes.string.isRequired,
|
emojiName: PropTypes.string.isRequired,
|
||||||
reactions: PropTypes.arrayOf(PropTypes.object),
|
|
||||||
emojis: PropTypes.object.isRequired,
|
/*
|
||||||
|
* The number of reactions to this post for this emoji
|
||||||
|
*/
|
||||||
|
reactionCount: PropTypes.number.isRequired,
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Array of users who reacted to this post
|
||||||
|
*/
|
||||||
profiles: PropTypes.array.isRequired,
|
profiles: PropTypes.array.isRequired,
|
||||||
otherUsers: PropTypes.number.isRequired,
|
|
||||||
|
/*
|
||||||
|
* The number of users not in the profile list who have reacted with this emoji
|
||||||
|
*/
|
||||||
|
otherUsersCount: PropTypes.number.isRequired,
|
||||||
|
|
||||||
|
/*
|
||||||
|
* The URL of the emoji image
|
||||||
|
*/
|
||||||
|
emojiImageUrl: PropTypes.string.isRequired,
|
||||||
|
|
||||||
actions: PropTypes.shape({
|
actions: PropTypes.shape({
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Function to add a reaction to a post
|
||||||
|
*/
|
||||||
addReaction: PropTypes.func.isRequired,
|
addReaction: PropTypes.func.isRequired,
|
||||||
getMissingProfiles: PropTypes.func.isRequired,
|
|
||||||
|
/*
|
||||||
|
* Function to get non-loaded profiles by id
|
||||||
|
*/
|
||||||
|
getMissingProfilesByIds: PropTypes.func.isRequired,
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Function to remove a reaction from a post
|
||||||
|
*/
|
||||||
removeReaction: PropTypes.func.isRequired
|
removeReaction: PropTypes.func.isRequired
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -36,22 +74,18 @@ export default class Reaction extends React.Component {
|
|||||||
|
|
||||||
addReaction(e) {
|
addReaction(e) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
this.props.actions.addReaction(this.props.post.channel_id, this.props.post.id, this.props.emojiName);
|
this.props.actions.addReaction(this.props.post.id, this.props.emojiName);
|
||||||
}
|
}
|
||||||
|
|
||||||
removeReaction(e) {
|
removeReaction(e) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
this.props.actions.removeReaction(this.props.post.channel_id, this.props.post.id, this.props.emojiName);
|
this.props.actions.removeReaction(this.props.post.id, this.props.emojiName);
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
if (!this.props.emojis.has(this.props.emojiName)) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
let currentUserReacted = false;
|
let currentUserReacted = false;
|
||||||
const users = [];
|
const users = [];
|
||||||
const otherUsers = this.props.otherUsers;
|
const otherUsersCount = this.props.otherUsersCount;
|
||||||
for (const user of this.props.profiles) {
|
for (const user of this.props.profiles) {
|
||||||
if (user.id === this.props.currentUserId) {
|
if (user.id === this.props.currentUserId) {
|
||||||
currentUserReacted = true;
|
currentUserReacted = true;
|
||||||
@@ -67,7 +101,7 @@ export default class Reaction extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let names;
|
let names;
|
||||||
if (otherUsers > 0) {
|
if (otherUsersCount > 0) {
|
||||||
if (users.length > 0) {
|
if (users.length > 0) {
|
||||||
names = (
|
names = (
|
||||||
<FormattedMessage
|
<FormattedMessage
|
||||||
@@ -75,7 +109,7 @@ export default class Reaction extends React.Component {
|
|||||||
defaultMessage='{users} and {otherUsers, number} other {otherUsers, plural, one {user} other {users}}'
|
defaultMessage='{users} and {otherUsers, number} other {otherUsers, plural, one {user} other {users}}'
|
||||||
values={{
|
values={{
|
||||||
users: users.join(', '),
|
users: users.join(', '),
|
||||||
otherUsers
|
otherUsers: otherUsersCount
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
@@ -85,7 +119,7 @@ export default class Reaction extends React.Component {
|
|||||||
id='reaction.othersReacted'
|
id='reaction.othersReacted'
|
||||||
defaultMessage='{otherUsers, number} {otherUsers, plural, one {user} other {users}}'
|
defaultMessage='{otherUsers, number} {otherUsers, plural, one {user} other {users}}'
|
||||||
values={{
|
values={{
|
||||||
otherUsers
|
otherUsers: otherUsersCount
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
@@ -106,7 +140,7 @@ export default class Reaction extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let reactionVerb;
|
let reactionVerb;
|
||||||
if (users.length + otherUsers > 1) {
|
if (users.length + otherUsersCount > 1) {
|
||||||
if (currentUserReacted) {
|
if (currentUserReacted) {
|
||||||
reactionVerb = (
|
reactionVerb = (
|
||||||
<FormattedMessage
|
<FormattedMessage
|
||||||
@@ -185,7 +219,7 @@ export default class Reaction extends React.Component {
|
|||||||
{clickTooltip}
|
{clickTooltip}
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
}
|
}
|
||||||
onEnter={this.props.actions.getMissingProfiles}
|
onEnter={this.props.actions.getMissingProfilesByIds}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className={className}
|
className={className}
|
||||||
@@ -193,10 +227,10 @@ export default class Reaction extends React.Component {
|
|||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
className='post-reaction__emoji emoticon'
|
className='post-reaction__emoji emoticon'
|
||||||
style={{backgroundImage: 'url(' + EmojiStore.getEmojiImageUrl(this.props.emojis.get(this.props.emojiName)) + ')'}}
|
style={{backgroundImage: 'url(' + this.props.emojiImageUrl + ')'}}
|
||||||
/>
|
/>
|
||||||
<span className='post-reaction__count'>
|
<span className='post-reaction__count'>
|
||||||
{this.props.reactions.length}
|
{this.props.reactionCount}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</OverlayTrigger>
|
</OverlayTrigger>
|
||||||
33
webapp/components/post_view/reaction_list/index.js
Обычный файл
33
webapp/components/post_view/reaction_list/index.js
Обычный файл
@@ -0,0 +1,33 @@
|
|||||||
|
// Copyright (c) 2017 Mattermost, Inc. All Rights Reserved.
|
||||||
|
// See License.txt for license information.
|
||||||
|
|
||||||
|
import {connect} from 'react-redux';
|
||||||
|
import {bindActionCreators} from 'redux';
|
||||||
|
import {makeGetReactionsForPost} from 'mattermost-redux/selectors/entities/posts';
|
||||||
|
import {getCustomEmojisAsMap} from 'mattermost-redux/selectors/entities/emojis';
|
||||||
|
|
||||||
|
import * as Actions from 'mattermost-redux/actions/posts';
|
||||||
|
|
||||||
|
import ReactionList from './reaction_list.jsx';
|
||||||
|
|
||||||
|
function makeMapStateToProps() {
|
||||||
|
const getReactionsForPost = makeGetReactionsForPost();
|
||||||
|
|
||||||
|
return function mapStateToProps(state, ownProps) {
|
||||||
|
return {
|
||||||
|
...ownProps,
|
||||||
|
reactions: getReactionsForPost(state, ownProps.post.id),
|
||||||
|
emojis: getCustomEmojisAsMap(state)
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function mapDispatchToProps(dispatch) {
|
||||||
|
return {
|
||||||
|
actions: bindActionCreators({
|
||||||
|
getReactionsForPost: Actions.getReactionsForPost
|
||||||
|
}, dispatch)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export default connect(makeMapStateToProps, mapDispatchToProps)(ReactionList);
|
||||||
@@ -1,17 +1,41 @@
|
|||||||
import PropTypes from 'prop-types';
|
|
||||||
|
|
||||||
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
|
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
|
||||||
// See License.txt for license information.
|
// See License.txt for license information.
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
import Reaction from './reaction_container.jsx';
|
import Reaction from 'components/post_view/reaction';
|
||||||
|
|
||||||
export default class ReactionListView extends React.Component {
|
export default class ReactionListView extends React.PureComponent {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The post to render reactions for
|
||||||
|
*/
|
||||||
post: PropTypes.object.isRequired,
|
post: PropTypes.object.isRequired,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The reactions to render
|
||||||
|
*/
|
||||||
reactions: PropTypes.arrayOf(PropTypes.object),
|
reactions: PropTypes.arrayOf(PropTypes.object),
|
||||||
emojis: PropTypes.object.isRequired
|
|
||||||
|
/**
|
||||||
|
* The emojis for the different reactions
|
||||||
|
*/
|
||||||
|
emojis: PropTypes.object.isRequired,
|
||||||
|
actions: PropTypes.shape({
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Function to get reactions for a post
|
||||||
|
*/
|
||||||
|
getReactionsForPost: PropTypes.func.isRequired
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
if (this.props.post.has_reactions) {
|
||||||
|
this.props.actions.getReactionsForPost(this.props.post.id);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
@@ -41,7 +65,7 @@ export default class ReactionListView extends React.Component {
|
|||||||
key={emojiName}
|
key={emojiName}
|
||||||
post={this.props.post}
|
post={this.props.post}
|
||||||
emojiName={emojiName}
|
emojiName={emojiName}
|
||||||
reactions={reactionsByName.get(emojiName)}
|
reactions={reactionsByName.get(emojiName) || []}
|
||||||
emojis={this.props.emojis}
|
emojis={this.props.emojis}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
@@ -2,41 +2,35 @@
|
|||||||
// See License.txt for license information.
|
// See License.txt for license information.
|
||||||
|
|
||||||
import UserProfile from './user_profile.jsx';
|
import UserProfile from './user_profile.jsx';
|
||||||
import FileAttachmentListContainer from './file_attachment_list_container.jsx';
|
import FileAttachmentListContainer from 'components/file_attachment_list';
|
||||||
import PendingPostOptions from 'components/post_view/components/pending_post_options.jsx';
|
import PostMessageContainer from 'components/post_view/post_message_view';
|
||||||
import PostMessageContainer from 'components/post_view/components/post_message_container.jsx';
|
|
||||||
import ProfilePicture from 'components/profile_picture.jsx';
|
import ProfilePicture from 'components/profile_picture.jsx';
|
||||||
import ReactionListContainer from 'components/post_view/components/reaction_list_container.jsx';
|
import ReactionListContainer from 'components/post_view/reaction_list';
|
||||||
import PostFlagIcon from 'components/common/post_flag_icon.jsx';
|
import PostFlagIcon from 'components/post_view/post_flag_icon.jsx';
|
||||||
import DotMenu from 'components/dot_menu/dot_menu.jsx';
|
import FailedPostOptions from 'components/post_view/failed_post_options';
|
||||||
|
import DotMenu from 'components/dot_menu';
|
||||||
import EmojiPickerOverlay from 'components/emoji_picker/emoji_picker_overlay.jsx';
|
import EmojiPickerOverlay from 'components/emoji_picker/emoji_picker_overlay.jsx';
|
||||||
|
|
||||||
import * as GlobalActions from 'actions/global_actions.jsx';
|
import * as GlobalActions from 'actions/global_actions.jsx';
|
||||||
import {flagPost, unflagPost, pinPost, unpinPost, addReaction} from 'actions/post_actions.jsx';
|
import {addReaction} from 'actions/post_actions.jsx';
|
||||||
|
|
||||||
import TeamStore from 'stores/team_store.jsx';
|
import TeamStore from 'stores/team_store.jsx';
|
||||||
|
|
||||||
import * as Utils from 'utils/utils.jsx';
|
import * as Utils from 'utils/utils.jsx';
|
||||||
import * as PostUtils from 'utils/post_utils.jsx';
|
import * as PostUtils from 'utils/post_utils.jsx';
|
||||||
import Constants from 'utils/constants.jsx';
|
|
||||||
|
|
||||||
import loadingGif from 'images/load.gif';
|
import Constants from 'utils/constants.jsx';
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import {FormattedMessage} from 'react-intl';
|
|
||||||
import {Link} from 'react-router/es6';
|
import {Link} from 'react-router/es6';
|
||||||
|
import {FormattedMessage} from 'react-intl';
|
||||||
|
|
||||||
export default class RhsComment extends React.Component {
|
export default class RhsComment extends React.Component {
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
|
|
||||||
this.handlePermalink = this.handlePermalink.bind(this);
|
|
||||||
this.removePost = this.removePost.bind(this);
|
this.removePost = this.removePost.bind(this);
|
||||||
this.flagPost = this.flagPost.bind(this);
|
|
||||||
this.unflagPost = this.unflagPost.bind(this);
|
|
||||||
this.pinPost = this.pinPost.bind(this);
|
|
||||||
this.unpinPost = this.unpinPost.bind(this);
|
|
||||||
this.reactEmojiClick = this.reactEmojiClick.bind(this);
|
this.reactEmojiClick = this.reactEmojiClick.bind(this);
|
||||||
this.handleDropdownOpened = this.handleDropdownOpened.bind(this);
|
this.handleDropdownOpened = this.handleDropdownOpened.bind(this);
|
||||||
|
|
||||||
@@ -61,11 +55,6 @@ export default class RhsComment extends React.Component {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
handlePermalink(e) {
|
|
||||||
e.preventDefault();
|
|
||||||
GlobalActions.showGetPostLinkModal(this.props.post);
|
|
||||||
}
|
|
||||||
|
|
||||||
removePost() {
|
removePost() {
|
||||||
GlobalActions.emitRemovePost(this.props.post);
|
GlobalActions.emitRemovePost(this.props.post);
|
||||||
}
|
}
|
||||||
@@ -127,26 +116,6 @@ export default class RhsComment extends React.Component {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
flagPost(e) {
|
|
||||||
e.preventDefault();
|
|
||||||
flagPost(this.props.post.id);
|
|
||||||
}
|
|
||||||
|
|
||||||
unflagPost(e) {
|
|
||||||
e.preventDefault();
|
|
||||||
unflagPost(this.props.post.id);
|
|
||||||
}
|
|
||||||
|
|
||||||
pinPost(e) {
|
|
||||||
e.preventDefault();
|
|
||||||
pinPost(this.props.post.channel_id, this.props.post.id);
|
|
||||||
}
|
|
||||||
|
|
||||||
unpinPost(e) {
|
|
||||||
e.preventDefault();
|
|
||||||
unpinPost(this.props.post.channel_id, this.props.post.id);
|
|
||||||
}
|
|
||||||
|
|
||||||
timeTag(post, timeOptions) {
|
timeTag(post, timeOptions) {
|
||||||
return (
|
return (
|
||||||
<time
|
<time
|
||||||
@@ -229,7 +198,6 @@ export default class RhsComment extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const isEphemeral = Utils.isPostEphemeral(post);
|
const isEphemeral = Utils.isPostEphemeral(post);
|
||||||
const isPending = post.state === Constants.POST_FAILED || post.state === Constants.POST_LOADING;
|
|
||||||
const isSystemMessage = PostUtils.isSystemMessage(post);
|
const isSystemMessage = PostUtils.isSystemMessage(post);
|
||||||
|
|
||||||
var timestamp = this.props.currentUser.last_picture_update;
|
var timestamp = this.props.currentUser.last_picture_update;
|
||||||
@@ -283,20 +251,12 @@ export default class RhsComment extends React.Component {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
let loading;
|
let failedPostOptions;
|
||||||
let postClass = '';
|
let postClass = '';
|
||||||
|
|
||||||
if (post.state === Constants.POST_FAILED) {
|
if (post.failed) {
|
||||||
postClass += ' post-fail';
|
postClass += ' post-failed';
|
||||||
loading = <PendingPostOptions post={this.props.post}/>;
|
failedPostOptions = <FailedPostOptions post={this.props.post}/>;
|
||||||
} else if (post.state === Constants.POST_LOADING) {
|
|
||||||
postClass += ' post-waiting';
|
|
||||||
loading = (
|
|
||||||
<img
|
|
||||||
className='post-loading-gif pull-right'
|
|
||||||
src={loadingGif}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (PostUtils.isEdited(this.props.post)) {
|
if (PostUtils.isEdited(this.props.post)) {
|
||||||
@@ -365,7 +325,8 @@ export default class RhsComment extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let react;
|
let react;
|
||||||
if (!isEphemeral && !isPending && !isSystemMessage && Utils.isFeatureEnabled(Constants.PRE_RELEASE_FEATURES.EMOJI_PICKER_PREVIEW)) {
|
|
||||||
|
if (!isEphemeral && !post.failed && !isSystemMessage && Utils.isFeatureEnabled(Constants.PRE_RELEASE_FEATURES.EMOJI_PICKER_PREVIEW)) {
|
||||||
react = (
|
react = (
|
||||||
<span>
|
<span>
|
||||||
<EmojiPickerOverlay
|
<EmojiPickerOverlay
|
||||||
@@ -462,7 +423,7 @@ export default class RhsComment extends React.Component {
|
|||||||
</div>
|
</div>
|
||||||
<div className='post__body' >
|
<div className='post__body' >
|
||||||
<div className={postClass}>
|
<div className={postClass}>
|
||||||
{loading}
|
{failedPostOptions}
|
||||||
<PostMessageContainer post={post}/>
|
<PostMessageContainer post={post}/>
|
||||||
</div>
|
</div>
|
||||||
{fileAttachment}
|
{fileAttachment}
|
||||||
|
|||||||
@@ -2,50 +2,38 @@
|
|||||||
// See License.txt for license information.
|
// See License.txt for license information.
|
||||||
|
|
||||||
import UserProfile from './user_profile.jsx';
|
import UserProfile from './user_profile.jsx';
|
||||||
import PostBodyAdditionalContent from 'components/post_view/components/post_body_additional_content.jsx';
|
import PostBodyAdditionalContent from 'components/post_view/post_body_additional_content.jsx';
|
||||||
import PostMessageContainer from 'components/post_view/components/post_message_container.jsx';
|
import PostMessageContainer from 'components/post_view/post_message_view';
|
||||||
import FileAttachmentListContainer from './file_attachment_list_container.jsx';
|
import FileAttachmentListContainer from 'components/file_attachment_list';
|
||||||
import ProfilePicture from 'components/profile_picture.jsx';
|
import ProfilePicture from 'components/profile_picture.jsx';
|
||||||
import ReactionListContainer from 'components/post_view/components/reaction_list_container.jsx';
|
import ReactionListContainer from 'components/post_view/reaction_list';
|
||||||
import PostFlagIcon from 'components/common/post_flag_icon.jsx';
|
import PostFlagIcon from 'components/post_view/post_flag_icon.jsx';
|
||||||
import DotMenu from 'components/dot_menu/dot_menu.jsx';
|
import DotMenu from 'components/dot_menu';
|
||||||
|
import EmojiPickerOverlay from 'components/emoji_picker/emoji_picker_overlay.jsx';
|
||||||
|
|
||||||
import ChannelStore from 'stores/channel_store.jsx';
|
import ChannelStore from 'stores/channel_store.jsx';
|
||||||
import UserStore from 'stores/user_store.jsx';
|
import UserStore from 'stores/user_store.jsx';
|
||||||
import TeamStore from 'stores/team_store.jsx';
|
import TeamStore from 'stores/team_store.jsx';
|
||||||
|
|
||||||
import * as GlobalActions from 'actions/global_actions.jsx';
|
import {addReaction} from 'actions/post_actions.jsx';
|
||||||
import {flagPost, unflagPost, pinPost, unpinPost, addReaction} from 'actions/post_actions.jsx';
|
|
||||||
|
|
||||||
import * as Utils from 'utils/utils.jsx';
|
import * as Utils from 'utils/utils.jsx';
|
||||||
import * as PostUtils from 'utils/post_utils.jsx';
|
import * as PostUtils from 'utils/post_utils.jsx';
|
||||||
|
|
||||||
import EmojiPickerOverlay from 'components/emoji_picker/emoji_picker_overlay.jsx';
|
|
||||||
|
|
||||||
import Constants from 'utils/constants.jsx';
|
import Constants from 'utils/constants.jsx';
|
||||||
import DelayedAction from 'utils/delayed_action.jsx';
|
|
||||||
|
|
||||||
import {FormattedMessage} from 'react-intl';
|
|
||||||
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
import {Link} from 'react-router/es6';
|
import {Link} from 'react-router/es6';
|
||||||
|
import {FormattedMessage} from 'react-intl';
|
||||||
|
|
||||||
export default class RhsRootPost extends React.Component {
|
export default class RhsRootPost extends React.Component {
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
|
|
||||||
this.handlePermalink = this.handlePermalink.bind(this);
|
|
||||||
this.flagPost = this.flagPost.bind(this);
|
|
||||||
this.unflagPost = this.unflagPost.bind(this);
|
|
||||||
this.pinPost = this.pinPost.bind(this);
|
|
||||||
this.unpinPost = this.unpinPost.bind(this);
|
|
||||||
this.reactEmojiClick = this.reactEmojiClick.bind(this);
|
this.reactEmojiClick = this.reactEmojiClick.bind(this);
|
||||||
this.handleDropdownOpened = this.handleDropdownOpened.bind(this);
|
this.handleDropdownOpened = this.handleDropdownOpened.bind(this);
|
||||||
|
|
||||||
this.editDisableAction = new DelayedAction(this.handleEditDisable);
|
|
||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
currentTeamDisplayName: TeamStore.getCurrent().name,
|
currentTeamDisplayName: TeamStore.getCurrent().name,
|
||||||
width: '',
|
width: '',
|
||||||
@@ -68,11 +56,6 @@ export default class RhsRootPost extends React.Component {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
handlePermalink(e) {
|
|
||||||
e.preventDefault();
|
|
||||||
GlobalActions.showGetPostLinkModal(this.props.post);
|
|
||||||
}
|
|
||||||
|
|
||||||
shouldComponentUpdate(nextProps, nextState) {
|
shouldComponentUpdate(nextProps, nextState) {
|
||||||
if (nextProps.status !== this.props.status) {
|
if (nextProps.status !== this.props.status) {
|
||||||
return true;
|
return true;
|
||||||
@@ -121,16 +104,6 @@ export default class RhsRootPost extends React.Component {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
flagPost(e) {
|
|
||||||
e.preventDefault();
|
|
||||||
flagPost(this.props.post.id);
|
|
||||||
}
|
|
||||||
|
|
||||||
unflagPost(e) {
|
|
||||||
e.preventDefault();
|
|
||||||
unflagPost(this.props.post.id);
|
|
||||||
}
|
|
||||||
|
|
||||||
timeTag(post, timeOptions) {
|
timeTag(post, timeOptions) {
|
||||||
return (
|
return (
|
||||||
<time
|
<time
|
||||||
@@ -156,16 +129,6 @@ export default class RhsRootPost extends React.Component {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
pinPost(e) {
|
|
||||||
e.preventDefault();
|
|
||||||
pinPost(this.props.post.channel_id, this.props.post.id);
|
|
||||||
}
|
|
||||||
|
|
||||||
unpinPost(e) {
|
|
||||||
e.preventDefault();
|
|
||||||
unpinPost(this.props.post.channel_id, this.props.post.id);
|
|
||||||
}
|
|
||||||
|
|
||||||
toggleEmojiPicker = () => {
|
toggleEmojiPicker = () => {
|
||||||
const showEmojiPicker = !this.state.showEmojiPicker;
|
const showEmojiPicker = !this.state.showEmojiPicker;
|
||||||
|
|
||||||
@@ -220,7 +183,6 @@ export default class RhsRootPost extends React.Component {
|
|||||||
var channel = ChannelStore.get(post.channel_id);
|
var channel = ChannelStore.get(post.channel_id);
|
||||||
|
|
||||||
const isEphemeral = Utils.isPostEphemeral(post);
|
const isEphemeral = Utils.isPostEphemeral(post);
|
||||||
const isPending = post.state === Constants.POST_FAILED || post.state === Constants.POST_LOADING;
|
|
||||||
const isSystemMessage = PostUtils.isSystemMessage(post);
|
const isSystemMessage = PostUtils.isSystemMessage(post);
|
||||||
|
|
||||||
var channelName;
|
var channelName;
|
||||||
@@ -238,7 +200,8 @@ export default class RhsRootPost extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let react;
|
let react;
|
||||||
if (!isEphemeral && !isPending && !isSystemMessage && Utils.isFeatureEnabled(Constants.PRE_RELEASE_FEATURES.EMOJI_PICKER_PREVIEW)) {
|
|
||||||
|
if (!isEphemeral && !post.failed && !isSystemMessage && Utils.isFeatureEnabled(Constants.PRE_RELEASE_FEATURES.EMOJI_PICKER_PREVIEW)) {
|
||||||
react = (
|
react = (
|
||||||
<span>
|
<span>
|
||||||
<EmojiPickerOverlay
|
<EmojiPickerOverlay
|
||||||
@@ -454,10 +417,8 @@ RhsRootPost.defaultProps = {
|
|||||||
};
|
};
|
||||||
RhsRootPost.propTypes = {
|
RhsRootPost.propTypes = {
|
||||||
post: PropTypes.object.isRequired,
|
post: PropTypes.object.isRequired,
|
||||||
lastPostCount: PropTypes.number,
|
|
||||||
user: PropTypes.object.isRequired,
|
user: PropTypes.object.isRequired,
|
||||||
currentUser: PropTypes.object.isRequired,
|
currentUser: PropTypes.object.isRequired,
|
||||||
commentCount: PropTypes.number,
|
|
||||||
compactDisplay: PropTypes.bool,
|
compactDisplay: PropTypes.bool,
|
||||||
useMilitaryTime: PropTypes.bool.isRequired,
|
useMilitaryTime: PropTypes.bool.isRequired,
|
||||||
isFlagged: PropTypes.bool,
|
isFlagged: PropTypes.bool,
|
||||||
|
|||||||
27
webapp/components/rhs_thread/index.js
Обычный файл
27
webapp/components/rhs_thread/index.js
Обычный файл
@@ -0,0 +1,27 @@
|
|||||||
|
// Copyright (c) 2017 Mattermost, Inc. All Rights Reserved.
|
||||||
|
// See License.txt for license information.
|
||||||
|
|
||||||
|
import {connect} from 'react-redux';
|
||||||
|
import {getPost, makeGetPostsForThread} from 'mattermost-redux/selectors/entities/posts';
|
||||||
|
|
||||||
|
import RhsThread from './rhs_thread.jsx';
|
||||||
|
|
||||||
|
function makeMapStateToProps() {
|
||||||
|
const getPostsForThread = makeGetPostsForThread();
|
||||||
|
|
||||||
|
return function mapStateToProps(state, ownProps) {
|
||||||
|
const selected = getPost(state, state.views.rhs.selectedPostId);
|
||||||
|
let posts = [];
|
||||||
|
if (selected) {
|
||||||
|
posts = getPostsForThread(state, {rootId: selected.id, channelId: selected.channel_id});
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
...ownProps,
|
||||||
|
selected,
|
||||||
|
posts
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export default connect(makeMapStateToProps)(RhsThread);
|
||||||
@@ -1,14 +1,13 @@
|
|||||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||||
// See License.txt for license information.
|
// See License.txt for license information.
|
||||||
|
|
||||||
import CreateComment from './create_comment.jsx';
|
import CreateComment from 'components/create_comment.jsx';
|
||||||
import RhsHeaderPost from './rhs_header_post.jsx';
|
import RhsHeaderPost from 'components/rhs_header_post.jsx';
|
||||||
import RootPost from './rhs_root_post.jsx';
|
import RootPost from 'components/rhs_root_post.jsx';
|
||||||
import Comment from './rhs_comment.jsx';
|
import Comment from 'components/rhs_comment.jsx';
|
||||||
import FloatingTimestamp from './post_view/components/floating_timestamp.jsx';
|
import FloatingTimestamp from 'components/post_view/floating_timestamp.jsx';
|
||||||
import DateSeparator from './post_view/components/date_separator.jsx';
|
import DateSeparator from 'components/post_view/date_separator.jsx';
|
||||||
|
|
||||||
import PostStore from 'stores/post_store.jsx';
|
|
||||||
import UserStore from 'stores/user_store.jsx';
|
import UserStore from 'stores/user_store.jsx';
|
||||||
import PreferenceStore from 'stores/preference_store.jsx';
|
import PreferenceStore from 'stores/preference_store.jsx';
|
||||||
import WebrtcStore from 'stores/webrtc_store.jsx';
|
import WebrtcStore from 'stores/webrtc_store.jsx';
|
||||||
@@ -49,14 +48,31 @@ export function renderThumbVertical(props) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default class RhsThread extends React.Component {
|
export default class RhsThread extends React.Component {
|
||||||
|
static propTypes = {
|
||||||
|
posts: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||||
|
selected: PropTypes.object.isRequired,
|
||||||
|
fromSearch: PropTypes.string,
|
||||||
|
fromFlaggedPosts: PropTypes.bool,
|
||||||
|
fromPinnedPosts: PropTypes.bool,
|
||||||
|
isWebrtc: PropTypes.bool,
|
||||||
|
isMentionSearch: PropTypes.bool,
|
||||||
|
currentUser: PropTypes.object.isRequired,
|
||||||
|
useMilitaryTime: PropTypes.bool.isRequired,
|
||||||
|
toggleSize: PropTypes.func,
|
||||||
|
shrink: PropTypes.func
|
||||||
|
}
|
||||||
|
|
||||||
|
static defaultProps = {
|
||||||
|
fromSearch: '',
|
||||||
|
isMentionSearch: false
|
||||||
|
}
|
||||||
|
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
|
|
||||||
this.mounted = false;
|
this.mounted = false;
|
||||||
|
|
||||||
this.onPostChange = this.onPostChange.bind(this);
|
|
||||||
this.onUserChange = this.onUserChange.bind(this);
|
this.onUserChange = this.onUserChange.bind(this);
|
||||||
this.onSelectedChange = this.onSelectedChange.bind(this);
|
|
||||||
this.forceUpdateInfo = this.forceUpdateInfo.bind(this);
|
this.forceUpdateInfo = this.forceUpdateInfo.bind(this);
|
||||||
this.onPreferenceChange = this.onPreferenceChange.bind(this);
|
this.onPreferenceChange = this.onPreferenceChange.bind(this);
|
||||||
this.onStatusChange = this.onStatusChange.bind(this);
|
this.onStatusChange = this.onStatusChange.bind(this);
|
||||||
@@ -67,7 +83,7 @@ export default class RhsThread extends React.Component {
|
|||||||
this.scrollStopAction = new DelayedAction(this.handleScrollStop);
|
this.scrollStopAction = new DelayedAction(this.handleScrollStop);
|
||||||
|
|
||||||
const openTime = (new Date()).getTime();
|
const openTime = (new Date()).getTime();
|
||||||
const state = this.getPosts(openTime);
|
const state = {};
|
||||||
state.windowWidth = Utils.windowWidth();
|
state.windowWidth = Utils.windowWidth();
|
||||||
state.windowHeight = Utils.windowHeight();
|
state.windowHeight = Utils.windowHeight();
|
||||||
state.profiles = JSON.parse(JSON.stringify(UserStore.getProfiles()));
|
state.profiles = JSON.parse(JSON.stringify(UserStore.getProfiles()));
|
||||||
@@ -86,9 +102,6 @@ export default class RhsThread extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
PostStore.addSelectedPostChangeListener(this.onSelectedChange);
|
|
||||||
PostStore.addSelectedPostChangeListener(this.onPostChange);
|
|
||||||
PostStore.addChangeListener(this.onPostChange);
|
|
||||||
PreferenceStore.addChangeListener(this.onPreferenceChange);
|
PreferenceStore.addChangeListener(this.onPreferenceChange);
|
||||||
UserStore.addChangeListener(this.onUserChange);
|
UserStore.addChangeListener(this.onUserChange);
|
||||||
UserStore.addStatusesChangeListener(this.onStatusChange);
|
UserStore.addStatusesChangeListener(this.onStatusChange);
|
||||||
@@ -101,9 +114,6 @@ export default class RhsThread extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
componentWillUnmount() {
|
componentWillUnmount() {
|
||||||
PostStore.addSelectedPostChangeListener(this.onSelectedChange);
|
|
||||||
PostStore.removeSelectedPostChangeListener(this.onPostChange);
|
|
||||||
PostStore.removeChangeListener(this.onPostChange);
|
|
||||||
PreferenceStore.removeChangeListener(this.onPreferenceChange);
|
PreferenceStore.removeChangeListener(this.onPreferenceChange);
|
||||||
UserStore.removeChangeListener(this.onUserChange);
|
UserStore.removeChangeListener(this.onUserChange);
|
||||||
UserStore.removeStatusesChangeListener(this.onStatusChange);
|
UserStore.removeStatusesChangeListener(this.onStatusChange);
|
||||||
@@ -116,7 +126,7 @@ export default class RhsThread extends React.Component {
|
|||||||
|
|
||||||
componentDidUpdate(prevProps, prevState) {
|
componentDidUpdate(prevProps, prevState) {
|
||||||
const prevPostsArray = prevState.postsArray || [];
|
const prevPostsArray = prevState.postsArray || [];
|
||||||
const curPostsArray = this.state.postsArray || [];
|
const curPostsArray = this.props.posts || [];
|
||||||
|
|
||||||
if (prevPostsArray.length >= curPostsArray.length) {
|
if (prevPostsArray.length >= curPostsArray.length) {
|
||||||
return;
|
return;
|
||||||
@@ -134,11 +144,11 @@ export default class RhsThread extends React.Component {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!Utils.areObjectsEqual(nextState.postsArray, this.state.postsArray)) {
|
if (!Utils.areObjectsEqual(nextState.postsArray, this.props.posts)) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!Utils.areObjectsEqual(nextState.selected, this.state.selected)) {
|
if (!Utils.areObjectsEqual(nextState.selected, this.props.selected)) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -198,10 +208,16 @@ export default class RhsThread extends React.Component {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
onSelectedChange() {
|
componentWillReceiveProps(nextProps) {
|
||||||
this.setState({
|
if (!this.props.selected || !nextProps.selected) {
|
||||||
openTime: (new Date()).getTime()
|
return;
|
||||||
});
|
}
|
||||||
|
|
||||||
|
if (this.props.selected.id !== nextProps.selected.id) {
|
||||||
|
this.setState({
|
||||||
|
openTime: (new Date()).getTime()
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onPreferenceChange(category) {
|
onPreferenceChange(category) {
|
||||||
@@ -218,12 +234,6 @@ export default class RhsThread extends React.Component {
|
|||||||
this.forceUpdateInfo();
|
this.forceUpdateInfo();
|
||||||
}
|
}
|
||||||
|
|
||||||
onPostChange() {
|
|
||||||
if (this.mounted) {
|
|
||||||
this.setState(this.getPosts(this.state.openTime));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onStatusChange() {
|
onStatusChange() {
|
||||||
this.setState({statuses: Object.assign({}, UserStore.getStatuses())});
|
this.setState({statuses: Object.assign({}, UserStore.getStatuses())});
|
||||||
}
|
}
|
||||||
@@ -232,53 +242,21 @@ export default class RhsThread extends React.Component {
|
|||||||
this.setState({isBusy});
|
this.setState({isBusy});
|
||||||
}
|
}
|
||||||
|
|
||||||
getPosts(openTime) {
|
filterPosts(posts, selected, openTime) {
|
||||||
const selected = PostStore.getSelectedPost();
|
|
||||||
const posts = PostStore.getSelectedPostThread();
|
|
||||||
|
|
||||||
const postsArray = [];
|
const postsArray = [];
|
||||||
|
|
||||||
for (const id in posts) {
|
posts.forEach((cpost) => {
|
||||||
if (posts.hasOwnProperty(id)) {
|
// Do not show empherals created before sidebar has been opened
|
||||||
const cpost = posts[id];
|
if (cpost.type === 'system_ephemeral' && cpost.create_at < openTime) {
|
||||||
|
return;
|
||||||
// Do not show empherals created before sidebar has been opened
|
|
||||||
if (cpost.type === 'system_ephemeral' && cpost.create_at < openTime) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (cpost.root_id === selected.id) {
|
|
||||||
postsArray.push(cpost);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// sort failed posts to bottom, followed by pending, and then regular posts
|
|
||||||
postsArray.sort((a, b) => {
|
|
||||||
if ((a.state === Constants.POST_LOADING || a.state === Constants.POST_FAILED) && (b.state !== Constants.POST_LOADING && b.state !== Constants.POST_FAILED)) {
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
if ((a.state !== Constants.POST_LOADING && a.state !== Constants.POST_FAILED) && (b.state === Constants.POST_LOADING || b.state === Constants.POST_FAILED)) {
|
|
||||||
return -1;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (a.state === Constants.POST_LOADING && b.state === Constants.POST_FAILED) {
|
if (cpost.root_id === selected.id) {
|
||||||
return -1;
|
postsArray.unshift(cpost);
|
||||||
}
|
}
|
||||||
if (a.state === Constants.POST_FAILED && b.state === Constants.POST_LOADING) {
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (a.create_at < b.create_at) {
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
if (a.create_at > b.create_at) {
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
return 0;
|
|
||||||
});
|
});
|
||||||
|
|
||||||
return {postsArray, selected};
|
return postsArray;
|
||||||
}
|
}
|
||||||
|
|
||||||
onUserChange() {
|
onUserChange() {
|
||||||
@@ -298,7 +276,7 @@ export default class RhsThread extends React.Component {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.state.postsArray) {
|
if (this.props.posts) {
|
||||||
const childNodes = this.refs.rhspostlist.childNodes;
|
const childNodes = this.refs.rhspostlist.childNodes;
|
||||||
const viewPort = this.refs.rhspostlist.getBoundingClientRect();
|
const viewPort = this.refs.rhspostlist.getBoundingClientRect();
|
||||||
let topRhsPostCreateAt = 0;
|
let topRhsPostCreateAt = 0;
|
||||||
@@ -307,7 +285,7 @@ export default class RhsThread extends React.Component {
|
|||||||
// determine the top rhs comment assuming that childNodes and postsArray are of same length
|
// determine the top rhs comment assuming that childNodes and postsArray are of same length
|
||||||
for (let i = 0; i < childNodes.length; i++) {
|
for (let i = 0; i < childNodes.length; i++) {
|
||||||
if ((childNodes[i].offsetTop + viewPort.top) - offset > 0) {
|
if ((childNodes[i].offsetTop + viewPort.top) - offset > 0) {
|
||||||
topRhsPostCreateAt = this.state.postsArray[i].create_at;
|
topRhsPostCreateAt = this.props.posts[i].create_at;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -343,14 +321,14 @@ export default class RhsThread extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
if (this.state.postsArray == null || this.state.selected == null) {
|
if (this.props.posts == null || this.props.selected == null) {
|
||||||
return (
|
return (
|
||||||
<div/>
|
<div/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const postsArray = this.state.postsArray;
|
const postsArray = this.filterPosts(this.props.posts, this.props.selected, this.state.openTime);
|
||||||
const selected = this.state.selected;
|
const selected = this.props.selected;
|
||||||
const profiles = this.state.profiles || {};
|
const profiles = this.state.profiles || {};
|
||||||
|
|
||||||
let profile;
|
let profile;
|
||||||
@@ -490,20 +468,3 @@ export default class RhsThread extends React.Component {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
RhsThread.defaultProps = {
|
|
||||||
fromSearch: '',
|
|
||||||
isMentionSearch: false
|
|
||||||
};
|
|
||||||
|
|
||||||
RhsThread.propTypes = {
|
|
||||||
fromSearch: PropTypes.string,
|
|
||||||
fromFlaggedPosts: PropTypes.bool,
|
|
||||||
fromPinnedPosts: PropTypes.bool,
|
|
||||||
isWebrtc: PropTypes.bool,
|
|
||||||
isMentionSearch: PropTypes.bool,
|
|
||||||
currentUser: PropTypes.object.isRequired,
|
|
||||||
useMilitaryTime: PropTypes.bool.isRequired,
|
|
||||||
toggleSize: PropTypes.func,
|
|
||||||
shrink: PropTypes.func
|
|
||||||
};
|
|
||||||
@@ -2,9 +2,9 @@
|
|||||||
// See License.txt for license information.
|
// See License.txt for license information.
|
||||||
|
|
||||||
import $ from 'jquery';
|
import $ from 'jquery';
|
||||||
import PostMessageContainer from 'components/post_view/components/post_message_container.jsx';
|
import PostMessageContainer from 'components/post_view/post_message_view';
|
||||||
import UserProfile from './user_profile.jsx';
|
import UserProfile from './user_profile.jsx';
|
||||||
import FileAttachmentListContainer from './file_attachment_list_container.jsx';
|
import FileAttachmentListContainer from 'components/file_attachment_list';
|
||||||
import ProfilePicture from './profile_picture.jsx';
|
import ProfilePicture from './profile_picture.jsx';
|
||||||
import CommentIcon from 'components/common/comment_icon.jsx';
|
import CommentIcon from 'components/common/comment_icon.jsx';
|
||||||
|
|
||||||
@@ -14,17 +14,17 @@ import UserStore from 'stores/user_store.jsx';
|
|||||||
import AppDispatcher from '../dispatcher/app_dispatcher.jsx';
|
import AppDispatcher from '../dispatcher/app_dispatcher.jsx';
|
||||||
import * as GlobalActions from 'actions/global_actions.jsx';
|
import * as GlobalActions from 'actions/global_actions.jsx';
|
||||||
import {flagPost, unflagPost} from 'actions/post_actions.jsx';
|
import {flagPost, unflagPost} from 'actions/post_actions.jsx';
|
||||||
import PostFlagIcon from 'components/common/post_flag_icon.jsx';
|
import PostFlagIcon from 'components/post_view/post_flag_icon.jsx';
|
||||||
|
|
||||||
import * as Utils from 'utils/utils.jsx';
|
import * as Utils from 'utils/utils.jsx';
|
||||||
import * as PostUtils from 'utils/post_utils.jsx';
|
import * as PostUtils from 'utils/post_utils.jsx';
|
||||||
|
|
||||||
import Constants from 'utils/constants.jsx';
|
import Constants from 'utils/constants.jsx';
|
||||||
const ActionTypes = Constants.ActionTypes;
|
const ActionTypes = Constants.ActionTypes;
|
||||||
|
import {Posts} from 'mattermost-redux/constants';
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
import {FormattedMessage, FormattedDate} from 'react-intl';
|
import {FormattedMessage, FormattedDate} from 'react-intl';
|
||||||
import {browserHistory, Link} from 'react-router/es6';
|
import {browserHistory, Link} from 'react-router/es6';
|
||||||
|
|
||||||
@@ -187,7 +187,7 @@ export default class SearchResultsItem extends React.Component {
|
|||||||
let message;
|
let message;
|
||||||
let flagContent;
|
let flagContent;
|
||||||
let rhsControls;
|
let rhsControls;
|
||||||
if (post.state === Constants.POST_DELETED) {
|
if (post.state === Posts.POST_DELETED) {
|
||||||
message = (
|
message = (
|
||||||
<p>
|
<p>
|
||||||
<FormattedMessage
|
<FormattedMessage
|
||||||
|
|||||||
17
webapp/components/sidebar_right/index.js
Обычный файл
17
webapp/components/sidebar_right/index.js
Обычный файл
@@ -0,0 +1,17 @@
|
|||||||
|
// Copyright (c) 2017 Mattermost, Inc. All Rights Reserved.
|
||||||
|
// See License.txt for license information.
|
||||||
|
|
||||||
|
import {connect} from 'react-redux';
|
||||||
|
import SidebarRight from './sidebar_right.jsx';
|
||||||
|
|
||||||
|
function mapStateToProps(state, ownProps) {
|
||||||
|
return {
|
||||||
|
...ownProps,
|
||||||
|
postRightVisible: Boolean(state.views.rhs.selectedPostId),
|
||||||
|
fromSearch: state.views.rhs.fromSearch,
|
||||||
|
fromFlaggedPosts: state.views.rhs.fromFlaggedPosts,
|
||||||
|
fromPinnedPosts: state.views.rhs.fromPinnedPosts
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export default connect(mapStateToProps)(SidebarRight);
|
||||||
@@ -3,10 +3,10 @@
|
|||||||
|
|
||||||
import $ from 'jquery';
|
import $ from 'jquery';
|
||||||
|
|
||||||
import SearchResults from './search_results.jsx';
|
import SearchResults from 'components/search_results.jsx';
|
||||||
import RhsThread from './rhs_thread.jsx';
|
import RhsThread from 'components/rhs_thread';
|
||||||
import SearchBox from './search_bar.jsx';
|
import SearchBox from 'components/search_bar.jsx';
|
||||||
import FileUploadOverlay from './file_upload_overlay.jsx';
|
import FileUploadOverlay from 'components/file_upload_overlay.jsx';
|
||||||
import SearchStore from 'stores/search_store.jsx';
|
import SearchStore from 'stores/search_store.jsx';
|
||||||
import PostStore from 'stores/post_store.jsx';
|
import PostStore from 'stores/post_store.jsx';
|
||||||
import UserStore from 'stores/user_store.jsx';
|
import UserStore from 'stores/user_store.jsx';
|
||||||
@@ -19,18 +19,24 @@ import {trackEvent} from 'actions/diagnostics_actions.jsx';
|
|||||||
import * as Utils from 'utils/utils.jsx';
|
import * as Utils from 'utils/utils.jsx';
|
||||||
import Constants from 'utils/constants.jsx';
|
import Constants from 'utils/constants.jsx';
|
||||||
|
|
||||||
|
import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
import React from 'react';
|
|
||||||
|
|
||||||
export default class SidebarRight extends React.Component {
|
export default class SidebarRight extends React.Component {
|
||||||
|
static propTypes = {
|
||||||
|
channel: PropTypes.object,
|
||||||
|
postRightVisible: PropTypes.bool,
|
||||||
|
fromSearch: PropTypes.string,
|
||||||
|
fromFlaggedPosts: PropTypes.bool,
|
||||||
|
fromPinnedPosts: PropTypes.bool
|
||||||
|
}
|
||||||
|
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
|
|
||||||
this.plScrolledToBottom = true;
|
this.plScrolledToBottom = true;
|
||||||
|
|
||||||
this.onPreferenceChange = this.onPreferenceChange.bind(this);
|
this.onPreferenceChange = this.onPreferenceChange.bind(this);
|
||||||
this.onSelectedChange = this.onSelectedChange.bind(this);
|
|
||||||
this.onPostPinnedChange = this.onPostPinnedChange.bind(this);
|
this.onPostPinnedChange = this.onPostPinnedChange.bind(this);
|
||||||
this.onSearchChange = this.onSearchChange.bind(this);
|
this.onSearchChange = this.onSearchChange.bind(this);
|
||||||
this.onUserChange = this.onUserChange.bind(this);
|
this.onUserChange = this.onUserChange.bind(this);
|
||||||
@@ -45,7 +51,6 @@ export default class SidebarRight extends React.Component {
|
|||||||
isMentionSearch: SearchStore.getIsMentionSearch(),
|
isMentionSearch: SearchStore.getIsMentionSearch(),
|
||||||
isFlaggedPosts: SearchStore.getIsFlaggedPosts(),
|
isFlaggedPosts: SearchStore.getIsFlaggedPosts(),
|
||||||
isPinnedPosts: SearchStore.getIsPinnedPosts(),
|
isPinnedPosts: SearchStore.getIsPinnedPosts(),
|
||||||
postRightVisible: Boolean(PostStore.getSelectedPost()),
|
|
||||||
expanded: false,
|
expanded: false,
|
||||||
fromSearch: false,
|
fromSearch: false,
|
||||||
currentUser: UserStore.getCurrentUser(),
|
currentUser: UserStore.getCurrentUser(),
|
||||||
@@ -55,7 +60,6 @@ export default class SidebarRight extends React.Component {
|
|||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
SearchStore.addSearchChangeListener(this.onSearchChange);
|
SearchStore.addSearchChangeListener(this.onSearchChange);
|
||||||
PostStore.addSelectedPostChangeListener(this.onSelectedChange);
|
|
||||||
PostStore.addPostPinnedChangeListener(this.onPostPinnedChange);
|
PostStore.addPostPinnedChangeListener(this.onPostPinnedChange);
|
||||||
SearchStore.addShowSearchListener(this.onShowSearch);
|
SearchStore.addShowSearchListener(this.onShowSearch);
|
||||||
UserStore.addChangeListener(this.onUserChange);
|
UserStore.addChangeListener(this.onUserChange);
|
||||||
@@ -65,7 +69,6 @@ export default class SidebarRight extends React.Component {
|
|||||||
|
|
||||||
componentWillUnmount() {
|
componentWillUnmount() {
|
||||||
SearchStore.removeSearchChangeListener(this.onSearchChange);
|
SearchStore.removeSearchChangeListener(this.onSearchChange);
|
||||||
PostStore.removeSelectedPostChangeListener(this.onSelectedChange);
|
|
||||||
PostStore.removePostPinnedChangeListener(this.onPostPinnedChange);
|
PostStore.removePostPinnedChangeListener(this.onPostPinnedChange);
|
||||||
SearchStore.removeShowSearchListener(this.onShowSearch);
|
SearchStore.removeShowSearchListener(this.onShowSearch);
|
||||||
UserStore.removeChangeListener(this.onUserChange);
|
UserStore.removeChangeListener(this.onUserChange);
|
||||||
@@ -73,21 +76,17 @@ export default class SidebarRight extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
shouldComponentUpdate(nextProps, nextState) {
|
shouldComponentUpdate(nextProps, nextState) {
|
||||||
return !Utils.areObjectsEqual(nextState, this.state);
|
return !Utils.areObjectsEqual(nextState, this.state) || !Utils.areObjectsEqual(nextProps, this.props);
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillUpdate(nextProps, nextState) {
|
componentWillUpdate(nextProps, nextState) {
|
||||||
const isOpen = this.state.searchVisible || this.state.postRightVisible;
|
const isOpen = this.state.searchVisible || this.props.postRightVisible;
|
||||||
const willOpen = nextState.searchVisible || nextState.postRightVisible;
|
const willOpen = nextState.searchVisible || nextState.postRightVisible;
|
||||||
|
|
||||||
if (!isOpen && willOpen) {
|
if (!isOpen && willOpen) {
|
||||||
trackEvent('ui', 'ui_rhs_opened');
|
trackEvent('ui', 'ui_rhs_opened');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isOpen !== willOpen) {
|
|
||||||
PostStore.jumpPostsViewSidebarOpen();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!isOpen && willOpen) {
|
if (!isOpen && willOpen) {
|
||||||
this.setState({
|
this.setState({
|
||||||
expanded: false
|
expanded: false
|
||||||
@@ -105,7 +104,7 @@ export default class SidebarRight extends React.Component {
|
|||||||
$('.app__body .sidebar--right').addClass('move--left');
|
$('.app__body .sidebar--right').addClass('move--left');
|
||||||
|
|
||||||
//$('.sidebar--right').prepend('<div class="sidebar__overlay"></div>');
|
//$('.sidebar--right').prepend('<div class="sidebar__overlay"></div>');
|
||||||
if (!this.state.searchVisible && !this.state.postRightVisible) {
|
if (!this.state.searchVisible && !this.props.postRightVisible) {
|
||||||
$('.app__body .inner-wrap').removeClass('move--left').removeClass('move--right');
|
$('.app__body .inner-wrap').removeClass('move--left').removeClass('move--right');
|
||||||
$('.app__body .sidebar--right').removeClass('move--left');
|
$('.app__body .sidebar--right').removeClass('move--left');
|
||||||
return (
|
return (
|
||||||
@@ -122,7 +121,7 @@ export default class SidebarRight extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
componentDidUpdate() {
|
componentDidUpdate() {
|
||||||
const isOpen = this.state.searchVisible || this.state.postRightVisible;
|
const isOpen = this.state.searchVisible || this.props.postRightVisible;
|
||||||
WebrtcStore.emitRhsChanged(isOpen);
|
WebrtcStore.emitRhsChanged(isOpen);
|
||||||
this.doStrangeThings();
|
this.doStrangeThings();
|
||||||
}
|
}
|
||||||
@@ -137,15 +136,6 @@ export default class SidebarRight extends React.Component {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
onSelectedChange(fromSearch, fromFlaggedPosts, fromPinnedPosts) {
|
|
||||||
this.setState({
|
|
||||||
postRightVisible: Boolean(PostStore.getSelectedPost()),
|
|
||||||
fromSearch,
|
|
||||||
fromFlaggedPosts,
|
|
||||||
fromPinnedPosts
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
onPostPinnedChange() {
|
onPostPinnedChange() {
|
||||||
if (this.props.channel && this.state.isPinnedPosts) {
|
if (this.props.channel && this.state.isPinnedPosts) {
|
||||||
getPinnedPosts(this.props.channel.id);
|
getPinnedPosts(this.props.channel.id);
|
||||||
@@ -225,15 +215,15 @@ export default class SidebarRight extends React.Component {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
} else if (this.state.postRightVisible) {
|
} else if (this.props.postRightVisible) {
|
||||||
content = (
|
content = (
|
||||||
<div className='post-right__container'>
|
<div className='post-right__container'>
|
||||||
<FileUploadOverlay overlayType='right'/>
|
<FileUploadOverlay overlayType='right'/>
|
||||||
<div className='search-bar__container sidebar--right__search-header'>{searchForm}</div>
|
<div className='search-bar__container sidebar--right__search-header'>{searchForm}</div>
|
||||||
<RhsThread
|
<RhsThread
|
||||||
fromFlaggedPosts={this.state.fromFlaggedPosts}
|
fromFlaggedPosts={this.props.fromFlaggedPosts}
|
||||||
fromSearch={this.state.fromSearch}
|
fromSearch={this.props.fromSearch}
|
||||||
fromPinnedPosts={this.state.fromPinnedPosts}
|
fromPinnedPosts={this.props.fromPinnedPosts}
|
||||||
isWebrtc={WebrtcStore.isBusy()}
|
isWebrtc={WebrtcStore.isBusy()}
|
||||||
isMentionSearch={this.state.isMentionSearch}
|
isMentionSearch={this.state.isMentionSearch}
|
||||||
currentUser={this.state.currentUser}
|
currentUser={this.state.currentUser}
|
||||||
@@ -265,7 +255,3 @@ export default class SidebarRight extends React.Component {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
SidebarRight.propTypes = {
|
|
||||||
channel: PropTypes.object
|
|
||||||
};
|
|
||||||
@@ -9,9 +9,8 @@ import ViewImagePopoverBar from './view_image_popover_bar.jsx';
|
|||||||
|
|
||||||
import * as GlobalActions from 'actions/global_actions.jsx';
|
import * as GlobalActions from 'actions/global_actions.jsx';
|
||||||
|
|
||||||
import FileStore from 'stores/file_store.jsx';
|
|
||||||
|
|
||||||
import * as Utils from 'utils/utils.jsx';
|
import * as Utils from 'utils/utils.jsx';
|
||||||
|
import {getFileUrl, getFilePreviewUrl} from 'mattermost-redux/utils/file_utils';
|
||||||
|
|
||||||
import Constants from 'utils/constants.jsx';
|
import Constants from 'utils/constants.jsx';
|
||||||
const KeyCodes = Constants.KeyCodes;
|
const KeyCodes = Constants.KeyCodes;
|
||||||
@@ -127,10 +126,10 @@ export default class ViewImageModal extends React.Component {
|
|||||||
if (fileType === 'image') {
|
if (fileType === 'image') {
|
||||||
let previewUrl;
|
let previewUrl;
|
||||||
if (fileInfo.has_image_preview) {
|
if (fileInfo.has_image_preview) {
|
||||||
previewUrl = FileStore.getFilePreviewUrl(fileInfo.id);
|
previewUrl = getFilePreviewUrl(fileInfo.id);
|
||||||
} else {
|
} else {
|
||||||
// some images (eg animated gifs) just show the file itself and not a preview
|
// some images (eg animated gifs) just show the file itself and not a preview
|
||||||
previewUrl = FileStore.getFileUrl(fileInfo.id);
|
previewUrl = getFileUrl(fileInfo.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
const img = new Image();
|
const img = new Image();
|
||||||
@@ -175,7 +174,7 @@ export default class ViewImageModal extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const fileInfo = this.props.fileInfos[this.state.imgId];
|
const fileInfo = this.props.fileInfos[this.state.imgId];
|
||||||
const fileUrl = FileStore.getFileUrl(fileInfo.id);
|
const fileUrl = getFileUrl(fileInfo.id);
|
||||||
|
|
||||||
let content;
|
let content;
|
||||||
if (this.state.loaded[this.state.imgId]) {
|
if (this.state.loaded[this.state.imgId]) {
|
||||||
@@ -346,7 +345,7 @@ LoadingImagePreview.propTypes = {
|
|||||||
function ImagePreview({fileInfo, fileUrl}) {
|
function ImagePreview({fileInfo, fileUrl}) {
|
||||||
let previewUrl;
|
let previewUrl;
|
||||||
if (fileInfo.has_preview_image) {
|
if (fileInfo.has_preview_image) {
|
||||||
previewUrl = FileStore.getFilePreviewUrl(fileInfo.id);
|
previewUrl = getFilePreviewUrl(fileInfo.id);
|
||||||
} else {
|
} else {
|
||||||
previewUrl = fileUrl;
|
previewUrl = fileUrl;
|
||||||
}
|
}
|
||||||
|
|||||||
16
webapp/components/youtube_video/index.js
Обычный файл
16
webapp/components/youtube_video/index.js
Обычный файл
@@ -0,0 +1,16 @@
|
|||||||
|
// Copyright (c) 2017 Mattermost, Inc. All Rights Reserved.
|
||||||
|
// See License.txt for license information.
|
||||||
|
|
||||||
|
import {connect} from 'react-redux';
|
||||||
|
import {getCurrentChannelId} from 'mattermost-redux/selectors/entities/channels';
|
||||||
|
|
||||||
|
import YoutubeVideo from './youtube_video.jsx';
|
||||||
|
|
||||||
|
function mapStateToProps(state, ownProps) {
|
||||||
|
return {
|
||||||
|
...ownProps,
|
||||||
|
currentChannelId: getCurrentChannelId(state)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export default connect(mapStateToProps)(YoutubeVideo);
|
||||||
@@ -1,17 +1,22 @@
|
|||||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||||
// See License.txt for license information.
|
// See License.txt for license information.
|
||||||
|
|
||||||
import ChannelStore from 'stores/channel_store.jsx';
|
|
||||||
import WebClient from 'client/web_client.jsx';
|
import WebClient from 'client/web_client.jsx';
|
||||||
import * as Utils from 'utils/utils.jsx';
|
import * as Utils from 'utils/utils.jsx';
|
||||||
|
|
||||||
const ytRegex = /(?:http|https):\/\/(?:www\.|m\.)?(?:(?:youtube\.com\/(?:(?:v\/)|(?:(?:watch|embed\/watch)(?:\/|.*v=))|(?:embed\/)|(?:user\/[^/]+\/u\/[0-9]\/)))|(?:youtu\.be\/))([^#&?]*)/;
|
const ytRegex = /(?:http|https):\/\/(?:www\.|m\.)?(?:(?:youtube\.com\/(?:(?:v\/)|(?:(?:watch|embed\/watch)(?:\/|.*v=))|(?:embed\/)|(?:user\/[^/]+\/u\/[0-9]\/)))|(?:youtu\.be\/))([^#&?]*)/;
|
||||||
|
|
||||||
|
import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
import React from 'react';
|
export default class YoutubeVideo extends React.PureComponent {
|
||||||
|
static propTypes = {
|
||||||
|
channelId: PropTypes.string.isRequired,
|
||||||
|
currentChannelId: PropTypes.string.isRequired,
|
||||||
|
link: PropTypes.string.isRequired,
|
||||||
|
show: PropTypes.bool.isRequired
|
||||||
|
}
|
||||||
|
|
||||||
export default class YoutubeVideo extends React.Component {
|
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
|
|
||||||
@@ -22,7 +27,6 @@ export default class YoutubeVideo extends React.Component {
|
|||||||
|
|
||||||
this.play = this.play.bind(this);
|
this.play = this.play.bind(this);
|
||||||
this.stop = this.stop.bind(this);
|
this.stop = this.stop.bind(this);
|
||||||
this.stopOnChannelChange = this.stopOnChannelChange.bind(this);
|
|
||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
loaded: false,
|
loaded: false,
|
||||||
@@ -52,6 +56,10 @@ export default class YoutubeVideo extends React.Component {
|
|||||||
this.stop();
|
this.stop();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (props.channelId !== props.currentChannelId) {
|
||||||
|
this.stop();
|
||||||
|
}
|
||||||
|
|
||||||
this.setState({
|
this.setState({
|
||||||
videoId: match[1],
|
videoId: match[1],
|
||||||
time: this.handleYoutubeTime(link)
|
time: this.handleYoutubeTime(link)
|
||||||
@@ -138,22 +146,12 @@ export default class YoutubeVideo extends React.Component {
|
|||||||
|
|
||||||
play() {
|
play() {
|
||||||
this.setState({playing: true});
|
this.setState({playing: true});
|
||||||
|
|
||||||
if (ChannelStore.getCurrentId() === this.props.channelId) {
|
|
||||||
ChannelStore.addChangeListener(this.stopOnChannelChange);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
stop() {
|
stop() {
|
||||||
this.setState({playing: false});
|
this.setState({playing: false});
|
||||||
}
|
}
|
||||||
|
|
||||||
stopOnChannelChange() {
|
|
||||||
if (ChannelStore.getCurrentId() !== this.props.channelId) {
|
|
||||||
this.stop();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
if (!this.state.loaded) {
|
if (!this.state.loaded) {
|
||||||
return (
|
return (
|
||||||
@@ -243,9 +241,3 @@ export default class YoutubeVideo extends React.Component {
|
|||||||
return link.trim().match(ytRegex);
|
return link.trim().match(ytRegex);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
YoutubeVideo.propTypes = {
|
|
||||||
channelId: PropTypes.string.isRequired,
|
|
||||||
link: PropTypes.string.isRequired,
|
|
||||||
show: PropTypes.bool.isRequired
|
|
||||||
};
|
|
||||||
@@ -1887,6 +1887,7 @@
|
|||||||
"post_info.unpin": "Un-pin from channel",
|
"post_info.unpin": "Un-pin from channel",
|
||||||
"post_message_view.edited": "(edited)",
|
"post_message_view.edited": "(edited)",
|
||||||
"posts_view.loadMore": "Load more messages",
|
"posts_view.loadMore": "Load more messages",
|
||||||
|
"posts_view.loadingMore": "Loading more messages...",
|
||||||
"posts_view.newMsg": "New Messages",
|
"posts_view.newMsg": "New Messages",
|
||||||
"posts_view.newMsgBelow": "New {count, plural, one {message} other {messages}} below",
|
"posts_view.newMsgBelow": "New {count, plural, one {message} other {messages}} below",
|
||||||
"quick_switch_modal.channels": "Channels",
|
"quick_switch_modal.channels": "Channels",
|
||||||
|
|||||||
8
webapp/reducers/index.js
Обычный файл
8
webapp/reducers/index.js
Обычный файл
@@ -0,0 +1,8 @@
|
|||||||
|
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
|
||||||
|
// See License.txt for license information.
|
||||||
|
|
||||||
|
import views from './views';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
views
|
||||||
|
};
|
||||||
69
webapp/reducers/views/channel.js
Обычный файл
69
webapp/reducers/views/channel.js
Обычный файл
@@ -0,0 +1,69 @@
|
|||||||
|
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
|
||||||
|
// See License.txt for license information.
|
||||||
|
|
||||||
|
import {combineReducers} from 'redux';
|
||||||
|
import {ActionTypes, Constants} from 'utils/constants.jsx';
|
||||||
|
import {ChannelTypes, PostTypes} from 'mattermost-redux/action_types';
|
||||||
|
|
||||||
|
function postVisibility(state = {}, action) {
|
||||||
|
switch (action.type) {
|
||||||
|
case ChannelTypes.SELECT_CHANNEL: {
|
||||||
|
const nextState = {...state};
|
||||||
|
nextState[action.data] = Constants.POST_CHUNK_SIZE / 2;
|
||||||
|
return nextState;
|
||||||
|
}
|
||||||
|
case ActionTypes.INCREASE_POST_VISIBILITY: {
|
||||||
|
const nextState = {...state};
|
||||||
|
nextState[action.data] += action.amount;
|
||||||
|
return nextState;
|
||||||
|
}
|
||||||
|
case ActionTypes.RECEIVED_FOCUSED_POST: {
|
||||||
|
const nextState = {...state};
|
||||||
|
nextState[action.channelId] = Constants.POST_CHUNK_SIZE / 2;
|
||||||
|
return nextState;
|
||||||
|
}
|
||||||
|
case PostTypes.RECEIVED_POST: {
|
||||||
|
if (action.data && state[action.data.channel_id]) {
|
||||||
|
const nextState = {...state};
|
||||||
|
nextState[action.data.channel_id] += 1;
|
||||||
|
return nextState;
|
||||||
|
}
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function lastChannelViewTime(state = {}, action) {
|
||||||
|
switch (action.type) {
|
||||||
|
case ChannelTypes.SELECT_CHANNEL: {
|
||||||
|
if (action.member) {
|
||||||
|
const nextState = {...state};
|
||||||
|
nextState[action.data] = action.member.last_viewed_at;
|
||||||
|
return nextState;
|
||||||
|
}
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadingPosts(state = {}, action) {
|
||||||
|
switch (action.type) {
|
||||||
|
case ActionTypes.LOADING_POSTS: {
|
||||||
|
const nextState = {...state};
|
||||||
|
nextState[action.channelId] = action.data;
|
||||||
|
return nextState;
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default combineReducers({
|
||||||
|
postVisibility,
|
||||||
|
lastChannelViewTime,
|
||||||
|
loadingPosts
|
||||||
|
});
|
||||||
12
webapp/reducers/views/index.js
Обычный файл
12
webapp/reducers/views/index.js
Обычный файл
@@ -0,0 +1,12 @@
|
|||||||
|
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
|
||||||
|
// See License.txt for license information.
|
||||||
|
|
||||||
|
import {combineReducers} from 'redux';
|
||||||
|
|
||||||
|
import rhs from './rhs';
|
||||||
|
import channel from './channel';
|
||||||
|
|
||||||
|
export default combineReducers({
|
||||||
|
rhs,
|
||||||
|
channel
|
||||||
|
});
|
||||||
63
webapp/reducers/views/rhs.js
Обычный файл
63
webapp/reducers/views/rhs.js
Обычный файл
@@ -0,0 +1,63 @@
|
|||||||
|
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
|
||||||
|
// See License.txt for license information.
|
||||||
|
|
||||||
|
import {combineReducers} from 'redux';
|
||||||
|
import {ActionTypes} from 'utils/constants.jsx';
|
||||||
|
import {PostTypes} from 'mattermost-redux/action_types';
|
||||||
|
|
||||||
|
function selectedPostId(state = '', action) {
|
||||||
|
switch (action.type) {
|
||||||
|
case ActionTypes.SELECT_POST:
|
||||||
|
return action.postId;
|
||||||
|
case PostTypes.REMOVE_POST:
|
||||||
|
if (action.data && action.data.id === state) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
return state;
|
||||||
|
default:
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function fromSearch(state = '', action) {
|
||||||
|
switch (action.type) {
|
||||||
|
case ActionTypes.SELECT_POST:
|
||||||
|
if (action.from_search) {
|
||||||
|
return action.from_search;
|
||||||
|
}
|
||||||
|
return '';
|
||||||
|
default:
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function fromFlaggedPosts(state = false, action) {
|
||||||
|
switch (action.type) {
|
||||||
|
case ActionTypes.SELECT_POST:
|
||||||
|
if (action.from_flagged_posts) {
|
||||||
|
return action.from_flagged_posts;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
default:
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function fromPinnedPosts(state = false, action) {
|
||||||
|
switch (action.type) {
|
||||||
|
case ActionTypes.SELECT_POST:
|
||||||
|
if (action.from_pinned_posts) {
|
||||||
|
return action.from_pinned_posts;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
default:
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default combineReducers({
|
||||||
|
selectedPostId,
|
||||||
|
fromSearch,
|
||||||
|
fromFlaggedPosts,
|
||||||
|
fromPinnedPosts
|
||||||
|
});
|
||||||
@@ -236,6 +236,26 @@
|
|||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.post-list__loading {
|
||||||
|
@include opacity(.5);
|
||||||
|
font-size: .9em;
|
||||||
|
font-style: italic;
|
||||||
|
padding: 1em 0;
|
||||||
|
text-align: center;
|
||||||
|
|
||||||
|
i {
|
||||||
|
margin-right: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.post-list__loading-search {
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.post-list-holder-by-time {
|
.post-list-holder-by-time {
|
||||||
-webkit-overflow-scrolling: touch;
|
-webkit-overflow-scrolling: touch;
|
||||||
background: #ffffff;
|
background: #ffffff;
|
||||||
|
|||||||
@@ -7,6 +7,11 @@ import {General, RequestStatus} from 'mattermost-redux/constants';
|
|||||||
import reduxInitialState from 'mattermost-redux/store/initial_state';
|
import reduxInitialState from 'mattermost-redux/store/initial_state';
|
||||||
import {createTransform, persistStore} from 'redux-persist';
|
import {createTransform, persistStore} from 'redux-persist';
|
||||||
import localForage from 'localforage';
|
import localForage from 'localforage';
|
||||||
|
import appReducer from 'reducers';
|
||||||
|
|
||||||
|
function getAppReducer() {
|
||||||
|
return require('../reducers'); // eslint-disable-line global-require
|
||||||
|
}
|
||||||
|
|
||||||
import {transformSet} from './utils';
|
import {transformSet} from './utils';
|
||||||
|
|
||||||
@@ -99,7 +104,7 @@ export default function configureStore(initialState) {
|
|||||||
autoRehydrate: {
|
autoRehydrate: {
|
||||||
log: false
|
log: false
|
||||||
},
|
},
|
||||||
blacklist: ['errors', 'offline', 'requests', 'entities'],
|
blacklist: ['errors', 'offline', 'requests', 'entities', 'views'],
|
||||||
debounce: 500,
|
debounce: 500,
|
||||||
transforms: [
|
transforms: [
|
||||||
setTransformer
|
setTransformer
|
||||||
@@ -107,6 +112,6 @@ export default function configureStore(initialState) {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return configureServiceStore({}, {}, offlineOptions, null, false);
|
return configureServiceStore({}, appReducer, offlineOptions, getAppReducer, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -159,7 +159,8 @@ class ChannelStoreClass extends EventEmitter {
|
|||||||
setCurrentId(id) {
|
setCurrentId(id) {
|
||||||
store.dispatch({
|
store.dispatch({
|
||||||
type: ChannelTypes.SELECT_CHANNEL,
|
type: ChannelTypes.SELECT_CHANNEL,
|
||||||
data: id
|
data: id,
|
||||||
|
member: this.getMyMember(id)
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ const MAXIMUM_RECENT_EMOJI = 27;
|
|||||||
// Wrap the contents of the store so that we don't need to construct an ES6 map where most of the content
|
// Wrap the contents of the store so that we don't need to construct an ES6 map where most of the content
|
||||||
// (the system emojis) will never change. It provides the get/has functions of a map and an iterator so
|
// (the system emojis) will never change. It provides the get/has functions of a map and an iterator so
|
||||||
// that it can be used in for..of loops
|
// that it can be used in for..of loops
|
||||||
class EmojiMap {
|
export class EmojiMap {
|
||||||
constructor(customEmojis) {
|
constructor(customEmojis) {
|
||||||
this.customEmojis = customEmojis;
|
this.customEmojis = customEmojis;
|
||||||
|
|
||||||
|
|||||||
@@ -1,73 +0,0 @@
|
|||||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
|
||||||
// See License.txt for license information.
|
|
||||||
|
|
||||||
import AppDispatcher from '../dispatcher/app_dispatcher.jsx';
|
|
||||||
import Constants from 'utils/constants.jsx';
|
|
||||||
import EventEmitter from 'events';
|
|
||||||
|
|
||||||
const ActionTypes = Constants.ActionTypes;
|
|
||||||
|
|
||||||
const CHANGE_EVENT = 'changed';
|
|
||||||
|
|
||||||
class FileStore extends EventEmitter {
|
|
||||||
constructor() {
|
|
||||||
super();
|
|
||||||
|
|
||||||
this.handleEventPayload = this.handleEventPayload.bind(this);
|
|
||||||
this.dispatchToken = AppDispatcher.register(this.handleEventPayload);
|
|
||||||
|
|
||||||
this.setMaxListeners(600);
|
|
||||||
|
|
||||||
this.fileInfosByPost = new Map();
|
|
||||||
}
|
|
||||||
|
|
||||||
addChangeListener(callback) {
|
|
||||||
this.on(CHANGE_EVENT, callback);
|
|
||||||
}
|
|
||||||
|
|
||||||
removeChangeListener(callback) {
|
|
||||||
this.removeListener(CHANGE_EVENT, callback);
|
|
||||||
}
|
|
||||||
|
|
||||||
emitChange() {
|
|
||||||
this.emit(CHANGE_EVENT);
|
|
||||||
}
|
|
||||||
|
|
||||||
hasInfosForPost(postId) {
|
|
||||||
return this.fileInfosByPost.has(postId);
|
|
||||||
}
|
|
||||||
|
|
||||||
getInfosForPost(postId) {
|
|
||||||
return this.fileInfosByPost.get(postId);
|
|
||||||
}
|
|
||||||
|
|
||||||
saveInfos(postId, infos) {
|
|
||||||
this.fileInfosByPost.set(postId, infos);
|
|
||||||
}
|
|
||||||
|
|
||||||
getFileUrl(fileId) {
|
|
||||||
return `/api/v3/files/${fileId}/get`;
|
|
||||||
}
|
|
||||||
|
|
||||||
getFileThumbnailUrl(fileId) {
|
|
||||||
return `/api/v3/files/${fileId}/get_thumbnail`;
|
|
||||||
}
|
|
||||||
|
|
||||||
getFilePreviewUrl(fileId) {
|
|
||||||
return `/api/v3/files/${fileId}/get_preview`;
|
|
||||||
}
|
|
||||||
|
|
||||||
handleEventPayload(payload) {
|
|
||||||
const action = payload.action;
|
|
||||||
|
|
||||||
switch (action.type) {
|
|
||||||
case ActionTypes.RECEIVED_FILE_INFOS:
|
|
||||||
// This assumes that all received file infos are for a single post
|
|
||||||
this.saveInfos(action.postId, action.infos);
|
|
||||||
this.emitChange(action.postId);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default new FileStore();
|
|
||||||
@@ -4,40 +4,29 @@
|
|||||||
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';
|
||||||
|
|
||||||
import {Constants, PostTypes} from 'utils/constants.jsx';
|
import {Constants} from 'utils/constants.jsx';
|
||||||
const ActionTypes = Constants.ActionTypes;
|
const ActionTypes = Constants.ActionTypes;
|
||||||
|
|
||||||
const CHANGE_EVENT = 'change';
|
|
||||||
const FOCUSED_POST_CHANGE = 'focused_post_change';
|
const FOCUSED_POST_CHANGE = 'focused_post_change';
|
||||||
const EDIT_POST_EVENT = 'edit_post';
|
const EDIT_POST_EVENT = 'edit_post';
|
||||||
const POSTS_VIEW_JUMP_EVENT = 'post_list_jump';
|
|
||||||
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';
|
|
||||||
|
import store from 'stores/redux_store.jsx';
|
||||||
|
const dispatch = store.dispatch;
|
||||||
|
const getState = store.getState;
|
||||||
|
|
||||||
|
import * as Selectors from 'mattermost-redux/selectors/entities/posts';
|
||||||
|
|
||||||
class PostStoreClass extends EventEmitter {
|
class PostStoreClass extends EventEmitter {
|
||||||
constructor() {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
this.selectedPostId = null;
|
this.selectedPostId = null;
|
||||||
this.postsInfo = {};
|
|
||||||
this.latestPageTime = {};
|
|
||||||
this.earliestPostFromPage = {};
|
|
||||||
this.currentFocusedPostId = null;
|
this.currentFocusedPostId = null;
|
||||||
}
|
}
|
||||||
emitChange() {
|
|
||||||
this.emit(CHANGE_EVENT);
|
|
||||||
}
|
|
||||||
|
|
||||||
addChangeListener(callback) {
|
|
||||||
this.on(CHANGE_EVENT, callback);
|
|
||||||
}
|
|
||||||
|
|
||||||
removeChangeListener(callback) {
|
|
||||||
this.removeListener(CHANGE_EVENT, callback);
|
|
||||||
}
|
|
||||||
|
|
||||||
emitPostFocused() {
|
emitPostFocused() {
|
||||||
this.emit(FOCUSED_POST_CHANGE);
|
this.emit(FOCUSED_POST_CHANGE);
|
||||||
@@ -63,490 +52,6 @@ class PostStoreClass extends EventEmitter {
|
|||||||
this.removeListener(EDIT_POST_EVENT, callback);
|
this.removeListener(EDIT_POST_EVENT, callback);
|
||||||
}
|
}
|
||||||
|
|
||||||
emitPostsViewJump(type, post) {
|
|
||||||
this.emit(POSTS_VIEW_JUMP_EVENT, type, post);
|
|
||||||
}
|
|
||||||
|
|
||||||
addPostsViewJumpListener(callback) {
|
|
||||||
this.on(POSTS_VIEW_JUMP_EVENT, callback);
|
|
||||||
}
|
|
||||||
|
|
||||||
removePostsViewJumpListener(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() {
|
|
||||||
this.emitPostsViewJump(Constants.PostsViewJumpTypes.BOTTOM, null);
|
|
||||||
}
|
|
||||||
|
|
||||||
jumpPostsViewToPost(post) {
|
|
||||||
this.emitPostsViewJump(Constants.PostsViewJumpTypes.POST, post);
|
|
||||||
}
|
|
||||||
|
|
||||||
jumpPostsViewSidebarOpen() {
|
|
||||||
this.emitPostsViewJump(Constants.PostsViewJumpTypes.SIDEBAR_OPEN, null);
|
|
||||||
}
|
|
||||||
|
|
||||||
// All this does is makes sure the postsInfo is not null for the specified channel
|
|
||||||
makePostsInfo(id) {
|
|
||||||
if (!this.postsInfo.hasOwnProperty(id)) {
|
|
||||||
this.postsInfo[id] = {};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
getPost(channelId, postId) {
|
|
||||||
const postInfo = this.postsInfo[channelId];
|
|
||||||
if (postInfo == null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const postList = postInfo.postList;
|
|
||||||
let post = null;
|
|
||||||
|
|
||||||
if (postList && postList.posts && postList.posts.hasOwnProperty(postId)) {
|
|
||||||
post = postList.posts[postId];
|
|
||||||
}
|
|
||||||
|
|
||||||
return post;
|
|
||||||
}
|
|
||||||
|
|
||||||
getAllPosts(id) {
|
|
||||||
if (this.postsInfo.hasOwnProperty(id)) {
|
|
||||||
return this.postsInfo[id].postList;
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
getEarliestPostFromPage(id) {
|
|
||||||
return this.earliestPostFromPage[id];
|
|
||||||
}
|
|
||||||
|
|
||||||
getLatestPost(id) {
|
|
||||||
if (this.postsInfo.hasOwnProperty(id)) {
|
|
||||||
const postList = this.postsInfo[id].postList;
|
|
||||||
|
|
||||||
for (const postId of postList.order) {
|
|
||||||
if (postList.posts[postId].state !== Constants.POST_DELETED) {
|
|
||||||
return postList.posts[postId];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
getLatestNonEphemeralPost(id) {
|
|
||||||
if (this.postsInfo.hasOwnProperty(id)) {
|
|
||||||
const postList = this.postsInfo[id].postList;
|
|
||||||
|
|
||||||
for (const postId of postList.order) {
|
|
||||||
if (postList.posts[postId].state !== Constants.POST_DELETED && postList.posts[postId].type !== Constants.PostTypes.EPHEMERAL) {
|
|
||||||
return postList.posts[postId];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
getLatestPostFromPageTime(id) {
|
|
||||||
if (this.latestPageTime.hasOwnProperty(id)) {
|
|
||||||
return this.latestPageTime[id];
|
|
||||||
}
|
|
||||||
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
getVisiblePosts(id) {
|
|
||||||
if (this.postsInfo.hasOwnProperty(id) && this.postsInfo[id].hasOwnProperty('postList')) {
|
|
||||||
const postList = JSON.parse(JSON.stringify(this.postsInfo[id].postList));
|
|
||||||
|
|
||||||
// Only limit visibility if we are not focused on a post
|
|
||||||
if (this.currentFocusedPostId === null) {
|
|
||||||
postList.order = postList.order.slice(0, this.postsInfo[id].endVisible);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add pending posts
|
|
||||||
if (this.postsInfo[id].hasOwnProperty('pendingPosts')) {
|
|
||||||
Object.assign(postList.posts, this.postsInfo[id].pendingPosts.posts);
|
|
||||||
postList.order = this.postsInfo[id].pendingPosts.order.concat(postList.order);
|
|
||||||
}
|
|
||||||
|
|
||||||
return postList;
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
getVisibilityAtTop(id) {
|
|
||||||
if (this.postsInfo.hasOwnProperty(id)) {
|
|
||||||
return this.postsInfo[id].atTop && this.postsInfo[id].endVisible >= this.postsInfo[id].postList.order.length;
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
getVisibilityAtBottom(id) {
|
|
||||||
if (this.postsInfo.hasOwnProperty(id)) {
|
|
||||||
return this.postsInfo[id].atBottom;
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Returns true if posts need to be fetched
|
|
||||||
requestVisibilityIncrease(id, amount) {
|
|
||||||
const endVisible = this.postsInfo[id].endVisible;
|
|
||||||
const postList = this.postsInfo[id].postList;
|
|
||||||
if (this.getVisibilityAtTop(id)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
this.postsInfo[id].endVisible += amount;
|
|
||||||
this.emitChange();
|
|
||||||
return endVisible + amount > postList.order.length;
|
|
||||||
}
|
|
||||||
|
|
||||||
getFocusedPostId() {
|
|
||||||
return this.currentFocusedPostId;
|
|
||||||
}
|
|
||||||
|
|
||||||
storePosts(id, newPosts, checkLatest, checkEarliest) {
|
|
||||||
if (isPostListNull(newPosts)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (checkLatest) {
|
|
||||||
const currentLatest = this.latestPageTime[id] || 0;
|
|
||||||
if (newPosts.order.length >= 1) {
|
|
||||||
const newLatest = newPosts.posts[newPosts.order[0]].create_at || 0;
|
|
||||||
if (newLatest > currentLatest) {
|
|
||||||
this.latestPageTime[id] = newLatest;
|
|
||||||
}
|
|
||||||
} else if (currentLatest === 0) {
|
|
||||||
// Mark that an empty page was received
|
|
||||||
this.latestPageTime[id] = 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (checkEarliest) {
|
|
||||||
const currentEarliest = this.earliestPostFromPage[id] || {create_at: Number.MAX_SAFE_INTEGER};
|
|
||||||
const orderLength = newPosts.order.length;
|
|
||||||
if (orderLength >= 1) {
|
|
||||||
const newEarliestPost = newPosts.posts[newPosts.order[orderLength - 1]];
|
|
||||||
if (newEarliestPost.create_at < currentEarliest.create_at) {
|
|
||||||
this.earliestPostFromPage[id] = newEarliestPost;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const combinedPosts = makePostListNonNull(this.getAllPosts(id));
|
|
||||||
|
|
||||||
for (const pid in newPosts.posts) {
|
|
||||||
if (newPosts.posts.hasOwnProperty(pid)) {
|
|
||||||
const np = newPosts.posts[pid];
|
|
||||||
if (np.delete_at === 0) {
|
|
||||||
combinedPosts.posts[pid] = np;
|
|
||||||
if (combinedPosts.order.indexOf(pid) === -1 && newPosts.order.indexOf(pid) !== -1) {
|
|
||||||
combinedPosts.order.push(pid);
|
|
||||||
}
|
|
||||||
} else if (combinedPosts.posts.hasOwnProperty(pid)) {
|
|
||||||
combinedPosts.posts[pid] = Object.assign({}, np, {
|
|
||||||
state: Constants.POST_DELETED,
|
|
||||||
fileIds: []
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
combinedPosts.order.sort((a, b) => {
|
|
||||||
if (combinedPosts.posts[a].create_at > combinedPosts.posts[b].create_at) {
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
if (combinedPosts.posts[a].create_at < combinedPosts.posts[b].create_at) {
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
return 0;
|
|
||||||
});
|
|
||||||
|
|
||||||
this.makePostsInfo(id);
|
|
||||||
this.postsInfo[id].postList = combinedPosts;
|
|
||||||
}
|
|
||||||
|
|
||||||
focusedPostListHasPost(id) {
|
|
||||||
const focusedPostId = this.getFocusedPostId();
|
|
||||||
if (focusedPostId == null) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
const focusedPostList = makePostListNonNull(this.getAllPosts(focusedPostId));
|
|
||||||
return focusedPostList.posts.hasOwnProperty(id);
|
|
||||||
}
|
|
||||||
|
|
||||||
storePost(post, isNewPost = false) {
|
|
||||||
const ids = [
|
|
||||||
post.channel_id
|
|
||||||
];
|
|
||||||
|
|
||||||
// update the post in the permalink view if it's there
|
|
||||||
if (!isNewPost && this.focusedPostListHasPost(post.id)) {
|
|
||||||
ids.push(this.getFocusedPostId());
|
|
||||||
}
|
|
||||||
|
|
||||||
ids.forEach((id) => {
|
|
||||||
const postList = makePostListNonNull(this.getAllPosts(id));
|
|
||||||
if (post.pending_post_id !== '') {
|
|
||||||
this.removePendingPost(post.channel_id, post.pending_post_id);
|
|
||||||
}
|
|
||||||
|
|
||||||
post.pending_post_id = '';
|
|
||||||
|
|
||||||
postList.posts[post.id] = post;
|
|
||||||
if (isNewPost && postList.order.indexOf(post.id) === -1) {
|
|
||||||
postList.order.unshift(post.id);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.makePostsInfo(post.channel_id);
|
|
||||||
this.postsInfo[id].postList = postList;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
storeFocusedPost(postId, channelId, postList) {
|
|
||||||
const focusedPost = postList.posts[postId];
|
|
||||||
if (!focusedPost) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
this.currentFocusedPostId = postId;
|
|
||||||
this.storePosts(postId, postList);
|
|
||||||
this.storePosts(channelId, postList);
|
|
||||||
}
|
|
||||||
|
|
||||||
checkBounds(id, numRequested, postList, before) {
|
|
||||||
if (numRequested > postList.order.length) {
|
|
||||||
if (before) {
|
|
||||||
this.postsInfo[id].atTop = true;
|
|
||||||
} else {
|
|
||||||
this.postsInfo[id].atBottom = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
clearFocusedPost() {
|
|
||||||
if (this.currentFocusedPostId != null) {
|
|
||||||
Reflect.deleteProperty(this.postsInfo, this.currentFocusedPostId);
|
|
||||||
this.currentFocusedPostId = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
clearChannelVisibility(id, atBottom) {
|
|
||||||
this.makePostsInfo(id);
|
|
||||||
this.postsInfo[id].endVisible = Constants.POST_CHUNK_SIZE;
|
|
||||||
if (this.postsInfo[id].postList) {
|
|
||||||
this.postsInfo[id].atTop = this.postsInfo[id].atTop && Constants.POST_CHUNK_SIZE >= this.postsInfo[id].postList.order.length;
|
|
||||||
} else {
|
|
||||||
this.postsInfo[id].atTop = false;
|
|
||||||
}
|
|
||||||
this.postsInfo[id].atBottom = atBottom;
|
|
||||||
}
|
|
||||||
|
|
||||||
deletePost(post) {
|
|
||||||
let postInfo = null;
|
|
||||||
if (this.currentFocusedPostId == null) {
|
|
||||||
postInfo = this.postsInfo[post.channel_id];
|
|
||||||
} else {
|
|
||||||
postInfo = this.postsInfo[this.currentFocusedPostId];
|
|
||||||
}
|
|
||||||
if (!postInfo) {
|
|
||||||
// the post that has been deleted is in a channel that we haven't seen so just ignore it
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const postList = postInfo.postList;
|
|
||||||
|
|
||||||
if (isPostListNull(postList)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (post.id in postList.posts) {
|
|
||||||
// make sure to copy the post so that component state changes work properly
|
|
||||||
postList.posts[post.id] = Object.assign({}, post, {
|
|
||||||
state: Constants.POST_DELETED,
|
|
||||||
file_ids: [],
|
|
||||||
has_reactions: false
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
removePost(post) {
|
|
||||||
const channelId = post.channel_id;
|
|
||||||
this.makePostsInfo(channelId);
|
|
||||||
const postList = this.postsInfo[channelId].postList;
|
|
||||||
if (isPostListNull(postList)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (post.id in postList.posts) {
|
|
||||||
Reflect.deleteProperty(postList.posts, post.id);
|
|
||||||
}
|
|
||||||
|
|
||||||
const index = postList.order.indexOf(post.id);
|
|
||||||
if (index !== -1) {
|
|
||||||
postList.order.splice(index, 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const pid in postList.posts) {
|
|
||||||
if (!postList.posts.hasOwnProperty(pid)) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (postList.posts[pid].root_id === post.id) {
|
|
||||||
Reflect.deleteProperty(postList.posts, pid);
|
|
||||||
const commentIndex = postList.order.indexOf(pid);
|
|
||||||
if (commentIndex !== -1) {
|
|
||||||
postList.order.splice(commentIndex, 1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
this.postsInfo[channelId].postList = postList;
|
|
||||||
}
|
|
||||||
|
|
||||||
getPendingPosts(channelId) {
|
|
||||||
if (this.postsInfo.hasOwnProperty(channelId)) {
|
|
||||||
return this.postsInfo[channelId].pendingPosts;
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
storePendingPost(post) {
|
|
||||||
const copyPost = JSON.parse(JSON.stringify(post));
|
|
||||||
copyPost.state = Constants.POST_LOADING;
|
|
||||||
|
|
||||||
const postList = makePostListNonNull(this.getPendingPosts(copyPost.channel_id));
|
|
||||||
|
|
||||||
postList.posts[copyPost.pending_post_id] = copyPost;
|
|
||||||
postList.order.unshift(copyPost.pending_post_id);
|
|
||||||
|
|
||||||
this.makePostsInfo(copyPost.channel_id);
|
|
||||||
this.postsInfo[copyPost.channel_id].pendingPosts = postList;
|
|
||||||
this.emitChange();
|
|
||||||
}
|
|
||||||
|
|
||||||
removePendingPost(channelId, pendingPostId) {
|
|
||||||
const postList = makePostListNonNull(this.getPendingPosts(channelId));
|
|
||||||
|
|
||||||
Reflect.deleteProperty(postList.posts, pendingPostId);
|
|
||||||
const index = postList.order.indexOf(pendingPostId);
|
|
||||||
if (index === -1) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
postList.order.splice(index, 1);
|
|
||||||
|
|
||||||
this.postsInfo[channelId].pendingPosts = postList;
|
|
||||||
this.emitChange();
|
|
||||||
}
|
|
||||||
|
|
||||||
clearPendingPosts(channelId) {
|
|
||||||
if (this.postsInfo.hasOwnProperty(channelId)) {
|
|
||||||
Reflect.deleteProperty(this.postsInfo[channelId], 'pendingPosts');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
updatePendingPost(post) {
|
|
||||||
const copyPost = JSON.parse(JSON.stringify(post));
|
|
||||||
const postList = makePostListNonNull(this.getPendingPosts(copyPost.channel_id));
|
|
||||||
|
|
||||||
if (postList.order.indexOf(copyPost.pending_post_id) === -1) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
postList.posts[copyPost.pending_post_id] = copyPost;
|
|
||||||
this.postsInfo[copyPost.channel_id].pendingPosts = postList;
|
|
||||||
this.emitChange();
|
|
||||||
}
|
|
||||||
|
|
||||||
storeSelectedPostId(postId) {
|
|
||||||
this.selectedPostId = postId;
|
|
||||||
}
|
|
||||||
|
|
||||||
getSelectedPostId() {
|
|
||||||
return this.selectedPostId;
|
|
||||||
}
|
|
||||||
|
|
||||||
getSelectedPost() {
|
|
||||||
if (this.selectedPostId == null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const k in this.postsInfo) {
|
|
||||||
if (this.postsInfo[k].postList.posts.hasOwnProperty(this.selectedPostId)) {
|
|
||||||
return this.postsInfo[k].postList.posts[this.selectedPostId];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
getSelectedPostThread() {
|
|
||||||
if (this.selectedPostId == null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const posts = {};
|
|
||||||
let pendingPosts;
|
|
||||||
for (const k in this.postsInfo) {
|
|
||||||
if (this.postsInfo[k].postList && this.postsInfo[k].postList.posts.hasOwnProperty(this.selectedPostId)) {
|
|
||||||
Object.assign(posts, this.postsInfo[k].postList.posts);
|
|
||||||
if (this.postsInfo[k].pendingPosts != null) {
|
|
||||||
pendingPosts = this.postsInfo[k].pendingPosts.posts;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const threadPosts = {};
|
|
||||||
const rootId = this.selectedPostId;
|
|
||||||
for (const k in posts) {
|
|
||||||
if (posts[k].root_id === rootId) {
|
|
||||||
threadPosts[k] = JSON.parse(JSON.stringify(posts[k]));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const k in pendingPosts) {
|
|
||||||
if (pendingPosts[k].root_id === rootId) {
|
|
||||||
threadPosts[k] = JSON.parse(JSON.stringify(pendingPosts[k]));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return threadPosts;
|
|
||||||
}
|
|
||||||
|
|
||||||
emitSelectedPostChange(fromSearch, fromFlaggedPosts, fromPinnedPosts) {
|
|
||||||
this.emit(SELECTED_POST_CHANGE_EVENT, fromSearch, fromFlaggedPosts, fromPinnedPosts);
|
|
||||||
}
|
|
||||||
|
|
||||||
addSelectedPostChangeListener(callback) {
|
|
||||||
this.on(SELECTED_POST_CHANGE_EVENT, callback);
|
|
||||||
}
|
|
||||||
|
|
||||||
removeSelectedPostChangeListener(callback) {
|
|
||||||
this.removeListener(SELECTED_POST_CHANGE_EVENT, callback);
|
|
||||||
}
|
|
||||||
|
|
||||||
emitPostPinnedChange() {
|
emitPostPinnedChange() {
|
||||||
this.emit(POST_PINNED_CHANGE_EVENT);
|
this.emit(POST_PINNED_CHANGE_EVENT);
|
||||||
}
|
}
|
||||||
@@ -559,16 +64,52 @@ class PostStoreClass extends EventEmitter {
|
|||||||
this.removeListener(POST_PINNED_CHANGE_EVENT, callback);
|
this.removeListener(POST_PINNED_CHANGE_EVENT, callback);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getLatestPostId(channelId) {
|
||||||
|
const postsInChannel = getState().entities.posts.postsInChannel[channelId] || [];
|
||||||
|
return postsInChannel[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
getLatestNonEphemeralPost(channelId) {
|
||||||
|
const postIds = getState().entities.posts.postsInChannel[channelId];
|
||||||
|
const posts = getState().entities.posts.posts;
|
||||||
|
|
||||||
|
for (const postId of postIds) {
|
||||||
|
const post = posts[postId] || {};
|
||||||
|
if (post.state !== Constants.POST_DELETED && post.type !== Constants.PostTypes.EPHEMERAL) {
|
||||||
|
return post;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
getVisiblePosts() {
|
||||||
|
const posts = Selectors.getPostsInCurrentChannel(getState());
|
||||||
|
const currentChannelId = getState().entities.channels.currentChannelId;
|
||||||
|
return posts.slice(0, getState().views.channel.postVisibility[currentChannelId]);
|
||||||
|
}
|
||||||
|
|
||||||
|
getFocusedPostId() {
|
||||||
|
return this.currentFocusedPostId;
|
||||||
|
}
|
||||||
|
|
||||||
|
storeFocusedPostId(postId) {
|
||||||
|
this.currentFocusedPostId = postId;
|
||||||
|
}
|
||||||
|
|
||||||
|
clearFocusedPost() {
|
||||||
|
this.currentFocusedPostId = null;
|
||||||
|
}
|
||||||
|
|
||||||
getCurrentUsersLatestPost(channelId, rootId) {
|
getCurrentUsersLatestPost(channelId, rootId) {
|
||||||
const userId = UserStore.getCurrentId();
|
const userId = UserStore.getCurrentId();
|
||||||
|
|
||||||
const postList = makePostListNonNull(this.getAllPosts(channelId));
|
const postIds = getState().entities.posts.postsInChannel[channelId] || [];
|
||||||
const len = postList.order.length;
|
|
||||||
|
|
||||||
let lastPost = null;
|
let lastPost = null;
|
||||||
|
|
||||||
for (let i = 0; i < len; i++) {
|
for (const id of postIds) {
|
||||||
const post = postList.posts[postList.order[i]];
|
const post = Selectors.getPost(getState(), id) || {};
|
||||||
|
|
||||||
// don't edit webhook posts, deleted posts, or system messages
|
// don't edit webhook posts, deleted posts, or system messages
|
||||||
if (post.user_id !== userId ||
|
if (post.user_id !== userId ||
|
||||||
@@ -611,11 +152,21 @@ class PostStoreClass extends EventEmitter {
|
|||||||
return draft;
|
return draft;
|
||||||
}
|
}
|
||||||
|
|
||||||
storePostDraft(channelId, draft) {
|
storeCurrentDraft(draft) {
|
||||||
|
var channelId = ChannelStore.getCurrentId();
|
||||||
BrowserStore.setGlobalItem('draft_' + channelId, draft);
|
BrowserStore.setGlobalItem('draft_' + channelId, draft);
|
||||||
}
|
}
|
||||||
|
|
||||||
getPostDraft(channelId) {
|
getCurrentDraft() {
|
||||||
|
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));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -645,40 +196,19 @@ class PostStoreClass extends EventEmitter {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
getCommentCount(post) {
|
getCommentCount(rootPost) {
|
||||||
const posts = this.getAllPosts(post.channel_id).posts;
|
const postIds = getState().entities.posts.postsInChannel[rootPost.channel_id] || [];
|
||||||
|
|
||||||
let commentCount = 0;
|
let commentCount = 0;
|
||||||
for (const id in posts) {
|
for (const postId of postIds) {
|
||||||
if (posts.hasOwnProperty(id)) {
|
const post = Selectors.getPost(getState(), postId) || {};
|
||||||
if (posts[id].root_id === post.id) {
|
if (post.root_id === rootPost.id) {
|
||||||
commentCount += 1;
|
commentCount += 1;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return commentCount;
|
return commentCount;
|
||||||
}
|
}
|
||||||
|
|
||||||
filterPosts(channelId, joinLeave) {
|
|
||||||
const postsList = JSON.parse(JSON.stringify(this.getVisiblePosts(channelId)));
|
|
||||||
|
|
||||||
if (!joinLeave && postsList) {
|
|
||||||
postsList.order = postsList.order.filter((id) => {
|
|
||||||
const post = postsList.posts[id];
|
|
||||||
|
|
||||||
if (post.type === PostTypes.JOIN_LEAVE || post.type === PostTypes.JOIN_CHANNEL || post.type === PostTypes.LEAVE_CHANNEL) {
|
|
||||||
Reflect.deleteProperty(postsList.posts, id);
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return postsList;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var PostStore = new PostStoreClass();
|
var PostStore = new PostStoreClass();
|
||||||
@@ -687,97 +217,25 @@ PostStore.dispatchToken = AppDispatcher.register((payload) => {
|
|||||||
var action = payload.action;
|
var action = payload.action;
|
||||||
|
|
||||||
switch (action.type) {
|
switch (action.type) {
|
||||||
case ActionTypes.RECEIVED_POSTS: {
|
|
||||||
if (PostStore.currentFocusedPostId !== null && action.isPost) {
|
|
||||||
PostStore.storePosts(PostStore.currentFocusedPostId, makePostListNonNull(action.post_list), action.checkLatest, action.checkEarliest);
|
|
||||||
PostStore.checkBounds(PostStore.currentFocusedPostId, action.numRequested, makePostListNonNull(action.post_list), action.before);
|
|
||||||
}
|
|
||||||
PostStore.storePosts(action.id, makePostListNonNull(action.post_list), action.checkLatest, action.checkEarliest);
|
|
||||||
PostStore.checkBounds(action.id, action.numRequested, makePostListNonNull(action.post_list), action.before);
|
|
||||||
PostStore.emitChange();
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case ActionTypes.RECEIVED_FOCUSED_POST:
|
case ActionTypes.RECEIVED_FOCUSED_POST:
|
||||||
PostStore.clearChannelVisibility(action.postId, false);
|
PostStore.storeFocusedPostId(action.postId);
|
||||||
PostStore.storeFocusedPost(action.postId, action.channelId, makePostListNonNull(action.post_list));
|
PostStore.emitPostFocused();
|
||||||
PostStore.emitChange();
|
|
||||||
break;
|
|
||||||
case ActionTypes.RECEIVED_POST:
|
|
||||||
PostStore.storePost(action.post, true);
|
|
||||||
PostStore.emitChange();
|
|
||||||
break;
|
|
||||||
case ActionTypes.RECEIVED_EDIT_POST:
|
|
||||||
PostStore.emitEditPost(action);
|
|
||||||
PostStore.emitChange();
|
|
||||||
break;
|
break;
|
||||||
case ActionTypes.CLICK_CHANNEL:
|
case ActionTypes.CLICK_CHANNEL:
|
||||||
PostStore.clearFocusedPost();
|
PostStore.clearFocusedPost();
|
||||||
PostStore.clearChannelVisibility(action.id, true);
|
|
||||||
break;
|
break;
|
||||||
case ActionTypes.CREATE_POST:
|
case ActionTypes.RECEIVED_EDIT_POST:
|
||||||
PostStore.storePendingPost(action.post);
|
PostStore.emitEditPost(action);
|
||||||
PostStore.storePostDraft(action.post.channel_id, null);
|
|
||||||
PostStore.jumpPostsViewToBottom();
|
|
||||||
break;
|
|
||||||
case ActionTypes.CREATE_COMMENT:
|
|
||||||
PostStore.storePendingPost(action.post);
|
|
||||||
PostStore.storeCommentDraft(action.post.root_id, null);
|
|
||||||
break;
|
|
||||||
case ActionTypes.POST_DELETED:
|
|
||||||
PostStore.deletePost(action.post);
|
|
||||||
PostStore.emitChange();
|
|
||||||
break;
|
|
||||||
case ActionTypes.REMOVE_POST:
|
|
||||||
PostStore.removePost(action.post);
|
|
||||||
PostStore.emitChange();
|
|
||||||
break;
|
break;
|
||||||
case ActionTypes.RECEIVED_POST_SELECTED:
|
case ActionTypes.RECEIVED_POST_SELECTED:
|
||||||
PostStore.storeSelectedPostId(action.postId);
|
dispatch({...action, type: ActionTypes.SELECT_POST});
|
||||||
PostStore.emitSelectedPostChange(action.from_search, action.from_flagged_posts, action.from_pinned_posts);
|
|
||||||
break;
|
break;
|
||||||
case ActionTypes.RECEIVED_POST_PINNED:
|
case ActionTypes.RECEIVED_POST_PINNED:
|
||||||
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:
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
export default PostStore;
|
export default PostStore;
|
||||||
|
|
||||||
function makePostListNonNull(pl) {
|
|
||||||
var postList = pl;
|
|
||||||
if (postList == null) {
|
|
||||||
postList = {order: [], posts: {}};
|
|
||||||
}
|
|
||||||
|
|
||||||
if (postList.order == null) {
|
|
||||||
postList.order = [];
|
|
||||||
}
|
|
||||||
|
|
||||||
if (postList.posts == null) {
|
|
||||||
postList.posts = {};
|
|
||||||
}
|
|
||||||
|
|
||||||
return postList;
|
|
||||||
}
|
|
||||||
|
|
||||||
function isPostListNull(pl) {
|
|
||||||
if (pl == null) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (pl.posts == null) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (pl.order == null) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,92 +0,0 @@
|
|||||||
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
|
|
||||||
// See License.txt for license information.
|
|
||||||
|
|
||||||
import AppDispatcher from '../dispatcher/app_dispatcher.jsx';
|
|
||||||
import Constants from 'utils/constants.jsx';
|
|
||||||
import EventEmitter from 'events';
|
|
||||||
|
|
||||||
const ActionTypes = Constants.ActionTypes;
|
|
||||||
|
|
||||||
const CHANGE_EVENT = 'changed';
|
|
||||||
|
|
||||||
class ReactionStore extends EventEmitter {
|
|
||||||
constructor() {
|
|
||||||
super();
|
|
||||||
|
|
||||||
this.dispatchToken = AppDispatcher.register(this.handleEventPayload.bind(this));
|
|
||||||
|
|
||||||
this.reactions = new Map();
|
|
||||||
|
|
||||||
this.setMaxListeners(600);
|
|
||||||
}
|
|
||||||
|
|
||||||
addChangeListener(postId, callback) {
|
|
||||||
this.on(CHANGE_EVENT + postId, callback);
|
|
||||||
}
|
|
||||||
|
|
||||||
removeChangeListener(postId, callback) {
|
|
||||||
this.removeListener(CHANGE_EVENT + postId, callback);
|
|
||||||
}
|
|
||||||
|
|
||||||
emitChange(postId) {
|
|
||||||
this.emit(CHANGE_EVENT + postId, postId);
|
|
||||||
}
|
|
||||||
|
|
||||||
setReactions(postId, reactions) {
|
|
||||||
this.reactions.set(postId, reactions);
|
|
||||||
}
|
|
||||||
|
|
||||||
addReaction(postId, reaction) {
|
|
||||||
let reactions = this.getReactions(postId) || [];
|
|
||||||
|
|
||||||
// Make sure not to add duplicates
|
|
||||||
const existingIndex = reactions.findIndex((existing) => {
|
|
||||||
return existing.user_id === reaction.user_id && existing.post_id === reaction.post_id && existing.emoji_name === reaction.emoji_name;
|
|
||||||
});
|
|
||||||
|
|
||||||
if (existingIndex === -1) {
|
|
||||||
reactions = [...reactions, reaction];
|
|
||||||
}
|
|
||||||
|
|
||||||
this.setReactions(postId, reactions);
|
|
||||||
}
|
|
||||||
|
|
||||||
removeReaction(postId, reaction) {
|
|
||||||
let reactions = this.getReactions(postId) || [];
|
|
||||||
|
|
||||||
const existingIndex = reactions.findIndex((existing) => {
|
|
||||||
return existing.user_id === reaction.user_id && existing.post_id === reaction.post_id && existing.emoji_name === reaction.emoji_name;
|
|
||||||
});
|
|
||||||
|
|
||||||
if (existingIndex !== -1) {
|
|
||||||
reactions = reactions.slice(0, existingIndex).concat(reactions.slice(existingIndex + 1));
|
|
||||||
}
|
|
||||||
|
|
||||||
this.setReactions(postId, reactions);
|
|
||||||
}
|
|
||||||
|
|
||||||
getReactions(postId) {
|
|
||||||
return this.reactions.get(postId);
|
|
||||||
}
|
|
||||||
|
|
||||||
handleEventPayload(payload) {
|
|
||||||
const action = payload.action;
|
|
||||||
|
|
||||||
switch (action.type) {
|
|
||||||
case ActionTypes.RECEIVED_REACTIONS:
|
|
||||||
this.setReactions(action.postId, action.reactions);
|
|
||||||
this.emitChange(action.postId);
|
|
||||||
break;
|
|
||||||
case ActionTypes.ADDED_REACTION:
|
|
||||||
this.addReaction(action.postId, action.reaction);
|
|
||||||
this.emitChange(action.postId);
|
|
||||||
break;
|
|
||||||
case ActionTypes.REMOVED_REACTION:
|
|
||||||
this.removeReaction(action.postId, action.reaction);
|
|
||||||
this.emitChange(action.postId);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default new ReactionStore();
|
|
||||||
@@ -87,12 +87,14 @@ export const ActionTypes = keyMirror({
|
|||||||
RECEIVED_EDIT_POST: null,
|
RECEIVED_EDIT_POST: null,
|
||||||
RECEIVED_SEARCH: null,
|
RECEIVED_SEARCH: null,
|
||||||
RECEIVED_SEARCH_TERM: null,
|
RECEIVED_SEARCH_TERM: null,
|
||||||
|
SELECT_POST: null,
|
||||||
RECEIVED_POST_SELECTED: null,
|
RECEIVED_POST_SELECTED: null,
|
||||||
RECEIVED_MENTION_DATA: null,
|
RECEIVED_MENTION_DATA: null,
|
||||||
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,
|
INCREASE_POST_VISIBILITY: null,
|
||||||
|
LOADING_POSTS: null,
|
||||||
|
|
||||||
RECEIVED_PROFILES: null,
|
RECEIVED_PROFILES: null,
|
||||||
RECEIVED_PROFILES_IN_TEAM: null,
|
RECEIVED_PROFILES_IN_TEAM: null,
|
||||||
@@ -309,6 +311,8 @@ export const Constants = {
|
|||||||
ErrorPageTypes,
|
ErrorPageTypes,
|
||||||
ErrorBarTypes,
|
ErrorBarTypes,
|
||||||
|
|
||||||
|
MAX_POST_VISIBILITY: 1000000,
|
||||||
|
|
||||||
IGNORE_POST_TYPES: [PostTypes.JOIN_LEAVE, PostTypes.JOIN_CHANNEL, PostTypes.LEAVE_CHANNEL, PostTypes.REMOVE_FROM_CHANNEL, PostTypes.ADD_TO_CHANNEL, PostTypes.ADD_REMOVE],
|
IGNORE_POST_TYPES: [PostTypes.JOIN_LEAVE, PostTypes.JOIN_CHANNEL, PostTypes.LEAVE_CHANNEL, PostTypes.REMOVE_FROM_CHANNEL, PostTypes.ADD_TO_CHANNEL, PostTypes.ADD_REMOVE],
|
||||||
|
|
||||||
PayloadSources: keyMirror({
|
PayloadSources: keyMirror({
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import Constants from 'utils/constants.jsx';
|
|||||||
var ActionTypes = Constants.ActionTypes;
|
var ActionTypes = Constants.ActionTypes;
|
||||||
import Client from 'client/web_client.jsx';
|
import Client from 'client/web_client.jsx';
|
||||||
import * as UserAgent from 'utils/user_agent.jsx';
|
import * as UserAgent from 'utils/user_agent.jsx';
|
||||||
|
import {Posts} from 'mattermost-redux/constants';
|
||||||
|
|
||||||
import {browserHistory} from 'react-router/es6';
|
import {browserHistory} from 'react-router/es6';
|
||||||
import {FormattedMessage} from 'react-intl';
|
import {FormattedMessage} from 'react-intl';
|
||||||
@@ -1194,7 +1195,7 @@ export function clearFileInput(elm) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function isPostEphemeral(post) {
|
export function isPostEphemeral(post) {
|
||||||
return post.type === Constants.PostTypes.EPHEMERAL || post.state === Constants.POST_DELETED;
|
return post.type === Constants.PostTypes.EPHEMERAL || post.state === Posts.POST_DELETED;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getRootId(post) {
|
export function getRootId(post) {
|
||||||
|
|||||||
@@ -4884,7 +4884,7 @@ math-expression-evaluator@^1.2.14:
|
|||||||
|
|
||||||
mattermost-redux@mattermost/mattermost-redux#webapp-master:
|
mattermost-redux@mattermost/mattermost-redux#webapp-master:
|
||||||
version "0.0.1"
|
version "0.0.1"
|
||||||
resolved "https://codeload.github.com/mattermost/mattermost-redux/tar.gz/1ba6245017789943d286443b95fd75d651437df7"
|
resolved "https://codeload.github.com/mattermost/mattermost-redux/tar.gz/f19e3ea5487dcd172d177d271f1b596b23ef6ea5"
|
||||||
dependencies:
|
dependencies:
|
||||||
deep-equal "1.0.1"
|
deep-equal "1.0.1"
|
||||||
harmony-reflect "1.5.1"
|
harmony-reflect "1.5.1"
|
||||||
@@ -6019,6 +6019,13 @@ rc@^1.1.2, rc@^1.1.7:
|
|||||||
minimist "^1.2.0"
|
minimist "^1.2.0"
|
||||||
strip-json-comments "~2.0.1"
|
strip-json-comments "~2.0.1"
|
||||||
|
|
||||||
|
react-addons-perf@^15.4.2:
|
||||||
|
version "15.4.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/react-addons-perf/-/react-addons-perf-15.4.2.tgz#110bdcf5c459c4f77cb85ed634bcd3397536383b"
|
||||||
|
dependencies:
|
||||||
|
fbjs "^0.8.4"
|
||||||
|
object-assign "^4.1.0"
|
||||||
|
|
||||||
react-addons-pure-render-mixin@15.5.2:
|
react-addons-pure-render-mixin@15.5.2:
|
||||||
version "15.5.2"
|
version "15.5.2"
|
||||||
resolved "https://registry.yarnpkg.com/react-addons-pure-render-mixin/-/react-addons-pure-render-mixin-15.5.2.tgz#ebb846aeb2fd771336c232822923108f87d5bff2"
|
resolved "https://registry.yarnpkg.com/react-addons-pure-render-mixin/-/react-addons-pure-render-mixin-15.5.2.tgz#ebb846aeb2fd771336c232822923108f87d5bff2"
|
||||||
|
|||||||
Ссылка в новой задаче
Block a user