forked from Ivasoft/mattermost-mobile
* Fix infinite skeleton in different use cases * Apply suggestions from code review Co-authored-by: Miguel Alatzar <migbot@users.noreply.github.com> * Update app/utils/teams.js Co-authored-by: Miguel Alatzar <migbot@users.noreply.github.com>
484 lines
15 KiB
JavaScript
484 lines
15 KiB
JavaScript
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
|
// See LICENSE.txt for license information.
|
|
|
|
import {batchActions} from 'redux-batched-actions';
|
|
|
|
import {UserTypes} from '@mm-redux/action_types';
|
|
import {
|
|
doPostAction,
|
|
getNeededAtMentionedUsernames,
|
|
receivedNewPost,
|
|
receivedPost,
|
|
receivedPosts,
|
|
receivedPostsBefore,
|
|
receivedPostsInChannel,
|
|
receivedPostsSince,
|
|
receivedPostsInThread,
|
|
} from '@mm-redux/actions/posts';
|
|
import {Client4} from '@mm-redux/client';
|
|
import {Posts} from '@mm-redux/constants';
|
|
import {getPost as selectPost, getPostIdsInChannel} from '@mm-redux/selectors/entities/posts';
|
|
import {getCurrentChannelId} from '@mm-redux/selectors/entities/channels';
|
|
import {removeUserFromList} from '@mm-redux/utils/user_utils';
|
|
import {isUnreadChannel, isArchivedChannel} from '@mm-redux/utils/channel_utils';
|
|
|
|
import {ViewTypes} from '@constants';
|
|
import {generateId} from '@utils/file';
|
|
import {getChannelSinceValue} from '@utils/channels';
|
|
|
|
import {getEmojisInPosts} from './emoji';
|
|
|
|
export function sendAddToChannelEphemeralPost(user, addedUsername, message, channelId, postRootId = '') {
|
|
return async (dispatch) => {
|
|
const timestamp = Date.now();
|
|
const post = {
|
|
id: generateId(),
|
|
user_id: user.id,
|
|
channel_id: channelId,
|
|
message,
|
|
type: Posts.POST_TYPES.EPHEMERAL_ADD_TO_CHANNEL,
|
|
create_at: timestamp,
|
|
update_at: timestamp,
|
|
root_id: postRootId,
|
|
parent_id: postRootId,
|
|
props: {
|
|
username: user.username,
|
|
addedUsername,
|
|
},
|
|
};
|
|
|
|
dispatch(receivedNewPost(post));
|
|
};
|
|
}
|
|
|
|
export function setAutocompleteSelector(dataSource, onSelect, options) {
|
|
return {
|
|
type: ViewTypes.SELECTED_ACTION_MENU,
|
|
data: {
|
|
dataSource,
|
|
onSelect,
|
|
options,
|
|
},
|
|
};
|
|
}
|
|
|
|
export function selectAttachmentMenuAction(postId, actionId, text, value) {
|
|
return (dispatch) => {
|
|
dispatch({
|
|
type: ViewTypes.SUBMIT_ATTACHMENT_MENU_ACTION,
|
|
postId,
|
|
data: {
|
|
[actionId]: {
|
|
text,
|
|
value,
|
|
},
|
|
},
|
|
});
|
|
|
|
dispatch(doPostAction(postId, actionId, value));
|
|
};
|
|
}
|
|
|
|
export function getPosts(channelId, page = 0, perPage = Posts.POST_CHUNK_SIZE) {
|
|
return async (dispatch, getState) => {
|
|
try {
|
|
const state = getState();
|
|
const {postsInChannel} = state.entities.posts;
|
|
const postForChannel = postsInChannel[channelId];
|
|
const data = await Client4.getPosts(channelId, page, perPage);
|
|
const posts = Object.values(data.posts);
|
|
const actions = [{
|
|
type: ViewTypes.SET_CHANNEL_RETRY_FAILED,
|
|
failed: false,
|
|
}];
|
|
|
|
if (posts?.length) {
|
|
actions.push(receivedPosts(data));
|
|
const additional = await dispatch(getPostsAdditionalDataBatch(posts));
|
|
if (additional.data.length) {
|
|
actions.push(...additional.data);
|
|
}
|
|
}
|
|
|
|
if (posts?.length || !postForChannel) {
|
|
actions.push(receivedPostsInChannel(data, channelId, page === 0, data.prev_post_id === ''));
|
|
}
|
|
|
|
dispatch(batchActions(actions, 'BATCH_GET_POSTS'));
|
|
|
|
return {data};
|
|
} catch (error) {
|
|
return {error};
|
|
}
|
|
};
|
|
}
|
|
|
|
export function getPost(postId) {
|
|
return async (dispatch) => {
|
|
try {
|
|
const data = await Client4.getPost(postId);
|
|
|
|
if (data) {
|
|
const actions = [
|
|
receivedPost(data),
|
|
];
|
|
|
|
const additional = await dispatch(getPostsAdditionalDataBatch([data]));
|
|
if (additional.data.length) {
|
|
actions.push(...additional.data);
|
|
}
|
|
|
|
dispatch(batchActions(actions, 'BATCH_GET_POST'));
|
|
}
|
|
|
|
return {data};
|
|
} catch (error) {
|
|
return {error};
|
|
}
|
|
};
|
|
}
|
|
|
|
export function getPostsSince(channelId, since) {
|
|
return async (dispatch) => {
|
|
try {
|
|
const data = await Client4.getPostsSince(channelId, since);
|
|
const posts = Object.values(data.posts);
|
|
|
|
if (posts?.length) {
|
|
const actions = [
|
|
receivedPosts(data),
|
|
receivedPostsSince(data, channelId),
|
|
];
|
|
|
|
const additional = await dispatch(getPostsAdditionalDataBatch(posts));
|
|
if (additional.data.length) {
|
|
actions.push(...additional.data);
|
|
}
|
|
|
|
dispatch(batchActions(actions, 'BATCH_GET_POSTS_SINCE'));
|
|
}
|
|
|
|
return {data};
|
|
} catch (error) {
|
|
return {error};
|
|
}
|
|
};
|
|
}
|
|
|
|
export function getPostsBefore(channelId, postId, page = 0, perPage = Posts.POST_CHUNK_SIZE) {
|
|
return async (dispatch) => {
|
|
try {
|
|
const data = await Client4.getPostsBefore(channelId, postId, page, perPage);
|
|
const posts = Object.values(data.posts);
|
|
|
|
if (posts?.length) {
|
|
const actions = [
|
|
receivedPosts(data),
|
|
receivedPostsBefore(data, channelId, postId, data.prev_post_id === ''),
|
|
];
|
|
|
|
const additional = await dispatch(getPostsAdditionalDataBatch(posts));
|
|
if (additional.data.length) {
|
|
actions.push(...additional.data);
|
|
}
|
|
|
|
dispatch(batchActions(actions, 'BATCH_GET_POSTS_BEFORE'));
|
|
}
|
|
|
|
return {data};
|
|
} catch (error) {
|
|
return {error};
|
|
}
|
|
};
|
|
}
|
|
|
|
export function getPostThread(rootId, skipDispatch = false) {
|
|
return async (dispatch) => {
|
|
try {
|
|
const data = await Client4.getPostThread(rootId);
|
|
const posts = Object.values(data.posts);
|
|
|
|
if (posts.length) {
|
|
const actions = [
|
|
receivedPosts(data),
|
|
receivedPostsInThread(data, rootId),
|
|
];
|
|
|
|
const additional = await dispatch(getPostsAdditionalDataBatch(posts));
|
|
if (additional.data.length) {
|
|
actions.push(...additional.data);
|
|
}
|
|
|
|
if (skipDispatch) {
|
|
return {data: actions};
|
|
}
|
|
|
|
dispatch(batchActions(actions, 'BATCH_GET_POSTS_THREAD'));
|
|
}
|
|
|
|
return {data};
|
|
} catch (error) {
|
|
return {error};
|
|
}
|
|
};
|
|
}
|
|
|
|
export function getPostsAround(channelId, postId, perPage = Posts.POST_CHUNK_SIZE / 2) {
|
|
return async (dispatch) => {
|
|
try {
|
|
const [before, thread, after] = await Promise.all([
|
|
Client4.getPostsBefore(channelId, postId, 0, perPage),
|
|
Client4.getPostThread(postId),
|
|
Client4.getPostsAfter(channelId, postId, 0, perPage),
|
|
]);
|
|
|
|
const data = {
|
|
posts: {
|
|
...after.posts,
|
|
...thread.posts,
|
|
...before.posts,
|
|
},
|
|
order: [ // Remember that the order is newest posts first
|
|
...after.order,
|
|
postId,
|
|
...before.order,
|
|
],
|
|
next_post_id: after.next_post_id,
|
|
prev_post_id: before.prev_post_id,
|
|
};
|
|
|
|
const posts = Object.values(data.posts);
|
|
|
|
if (posts?.length) {
|
|
const actions = [
|
|
receivedPosts(data),
|
|
receivedPostsInChannel(data, channelId, after.next_post_id === '', before.prev_post_id === ''),
|
|
];
|
|
|
|
const additional = await dispatch(getPostsAdditionalDataBatch(posts));
|
|
if (additional.data.length) {
|
|
actions.push(...additional.data);
|
|
}
|
|
|
|
dispatch(batchActions(actions, 'BATCH_GET_POSTS_AROUND'));
|
|
}
|
|
|
|
return {data};
|
|
} catch (error) {
|
|
return {error};
|
|
}
|
|
};
|
|
}
|
|
|
|
export function handleNewPostBatch(WebSocketMessage) {
|
|
return async (dispatch, getState) => {
|
|
const state = getState();
|
|
const post = JSON.parse(WebSocketMessage.data.post);
|
|
const actions = [receivedNewPost(post)];
|
|
|
|
// If we don't have the thread for this post, fetch it from the server
|
|
// and include the actions in the batch
|
|
if (post.root_id) {
|
|
const rootPost = selectPost(state, post.root_id);
|
|
|
|
if (!rootPost) {
|
|
const thread = await dispatch(getPostThread(post.root_id, true));
|
|
if (thread.actions?.length) {
|
|
actions.push(...thread.actions);
|
|
}
|
|
}
|
|
}
|
|
|
|
const additional = await dispatch(getPostsAdditionalDataBatch([post]));
|
|
if (additional.data.length) {
|
|
actions.push(...additional.data);
|
|
}
|
|
|
|
return actions;
|
|
};
|
|
}
|
|
|
|
export function getPostsAdditionalDataBatch(posts = []) {
|
|
return async (dispatch, getState) => {
|
|
const data = [];
|
|
|
|
if (!posts.length) {
|
|
return {data};
|
|
}
|
|
|
|
// Custom Emojis used in the posts
|
|
// Do not wait for this as they need to be loaded one by one
|
|
dispatch(getEmojisInPosts(posts));
|
|
|
|
try {
|
|
const state = getState();
|
|
const promises = [];
|
|
const promiseTrace = [];
|
|
const extra = userMetadataToLoadFromPosts(state, posts);
|
|
|
|
if (extra?.userIds.length) {
|
|
promises.push(Client4.getProfilesByIds(extra.userIds));
|
|
promiseTrace.push('ids');
|
|
}
|
|
|
|
if (extra?.usernames.length) {
|
|
promises.push(Client4.getProfilesByUsernames(extra.usernames));
|
|
promiseTrace.push('usernames');
|
|
}
|
|
|
|
if (extra?.statuses.length) {
|
|
promises.push(Client4.getStatusesByIds(extra.statuses));
|
|
promiseTrace.push('statuses');
|
|
}
|
|
|
|
if (promises.length) {
|
|
const result = await Promise.all(promises);
|
|
result.forEach((p, index) => {
|
|
if (p.length) {
|
|
const type = promiseTrace[index];
|
|
switch (type) {
|
|
case 'statuses':
|
|
data.push({
|
|
type: UserTypes.RECEIVED_STATUSES,
|
|
data: p,
|
|
});
|
|
break;
|
|
default: {
|
|
const {currentUserId} = state.entities.users;
|
|
|
|
removeUserFromList(currentUserId, p);
|
|
data.push({
|
|
type: UserTypes.RECEIVED_PROFILES_LIST,
|
|
data: p,
|
|
});
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
});
|
|
}
|
|
} catch (error) {
|
|
// do nothing
|
|
}
|
|
|
|
return {data};
|
|
};
|
|
}
|
|
|
|
function userMetadataToLoadFromPosts(state, posts = []) {
|
|
const {currentUserId, profiles, statuses} = state.entities.users;
|
|
|
|
// Profiles of users mentioned in the posts
|
|
const usernamesToLoad = getNeededAtMentionedUsernames(state, posts);
|
|
|
|
// Statuses and profiles of the users who made the posts
|
|
const userIdsToLoad = new Set();
|
|
const statusesToLoad = new Set();
|
|
|
|
posts.forEach((post) => {
|
|
const userId = post.user_id;
|
|
|
|
if (!statuses[userId]) {
|
|
statusesToLoad.add(userId);
|
|
}
|
|
|
|
if (userId === currentUserId) {
|
|
return;
|
|
}
|
|
|
|
if (!profiles[userId]) {
|
|
userIdsToLoad.add(userId);
|
|
}
|
|
});
|
|
|
|
return {
|
|
usernames: Array.from(usernamesToLoad),
|
|
userIds: Array.from(userIdsToLoad),
|
|
statuses: Array.from(statusesToLoad),
|
|
};
|
|
}
|
|
|
|
export function loadUnreadChannelPosts(channels, channelMembers) {
|
|
return async (dispatch, getState) => {
|
|
const state = getState();
|
|
const currentChannelId = getCurrentChannelId(state);
|
|
|
|
const promises = [];
|
|
const promiseTrace = [];
|
|
|
|
const channelMembersByChannel = {};
|
|
channelMembers.forEach((member) => {
|
|
channelMembersByChannel[member.channel_id] = member;
|
|
});
|
|
|
|
channels.forEach((channel) => {
|
|
if (channel.id === currentChannelId || isArchivedChannel(channel)) {
|
|
return;
|
|
}
|
|
|
|
const isUnread = isUnreadChannel(channelMembersByChannel, channel);
|
|
if (!isUnread) {
|
|
return;
|
|
}
|
|
|
|
const postIds = getPostIdsInChannel(state, channel.id);
|
|
|
|
let promise;
|
|
const trace = {
|
|
channelId: channel.id,
|
|
since: false,
|
|
};
|
|
if (!postIds || !postIds.length) {
|
|
// Get the first page of posts if it appears we haven't gotten it yet, like the webapp
|
|
promise = Client4.getPosts(channel.id);
|
|
} else {
|
|
const since = getChannelSinceValue(state, channel.id, postIds);
|
|
promise = Client4.getPostsSince(channel.id, since);
|
|
trace.since = since;
|
|
}
|
|
|
|
promises.push(promise);
|
|
promiseTrace.push(trace);
|
|
});
|
|
|
|
let posts = [];
|
|
const actions = [];
|
|
if (promises.length) {
|
|
const results = await Promise.all(promises);
|
|
results.forEach((data, index) => {
|
|
const channelPosts = Object.values(data.posts);
|
|
if (channelPosts.length) {
|
|
posts = posts.concat(channelPosts);
|
|
|
|
const trace = promiseTrace[index];
|
|
if (trace.since) {
|
|
actions.push(receivedPostsSince(data, trace.channelId));
|
|
} else {
|
|
actions.push(receivedPostsInChannel(data, trace.channelId, true, data.prev_post_id === ''));
|
|
}
|
|
|
|
actions.push({
|
|
type: ViewTypes.RECEIVED_POSTS_FOR_CHANNEL_AT_TIME,
|
|
channelId: trace.channelId,
|
|
time: Date.now(),
|
|
});
|
|
}
|
|
});
|
|
}
|
|
|
|
console.log(`Fetched ${posts.length} posts from ${promises.length} unread channels`); //eslint-disable-line no-console
|
|
if (posts.length) {
|
|
// receivedPosts should be the first action dispatched as
|
|
// receivedPostsSince and receivedPostsInChannel reducers are
|
|
// dependent on it.
|
|
actions.unshift(receivedPosts({posts}));
|
|
const additional = await dispatch(getPostsAdditionalDataBatch(posts));
|
|
if (additional.data.length) {
|
|
actions.push(...additional.data);
|
|
}
|
|
|
|
dispatch(batchActions(actions));
|
|
}
|
|
};
|
|
}
|