mattermost-mobile/app/store/middleware.js
Harrison Healey 0ff6dba3c8
MM-16477 Check for modified users on reconnect (#2936)
* MM-16477 Move websocket status to redux

* MM-16477 Check for modified users on reconnect

* Update mattermost-redux

* Update mattermost-redux
2019-07-02 14:20:48 -04:00

427 lines
14 KiB
JavaScript

// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import DeviceInfo from 'react-native-device-info';
import {ViewTypes} from 'app/constants';
import initialState from 'app/initial_state';
import {
captureException,
LOGGER_JAVASCRIPT_WARNING,
} from 'app/utils/sentry';
export function messageRetention(store) {
return (next) => (action) => {
if (action.type === 'persist/REHYDRATE') {
const {app} = action.payload;
const {entities, views} = action.payload;
if (!entities || !views) {
return next(action);
}
// When a new version of the app has been detected
if (!app || !app.version || app.version !== DeviceInfo.getVersion() || app.build !== DeviceInfo.getBuildNumber()) {
return next(resetStateForNewVersion(action));
}
// Keep only the last 60 messages for the last 5 viewed channels in each team
// and apply data retention on those posts if applies
let nextAction;
try {
nextAction = cleanUpState(action);
} catch (e) {
// Sometimes, the payload is incomplete so log the error to Sentry and skip the cleanup
console.warn(e); // eslint-disable-line no-console
captureException(e, LOGGER_JAVASCRIPT_WARNING, store);
nextAction = action;
}
return next(nextAction);
} else if (action.type === ViewTypes.DATA_CLEANUP) {
const nextAction = cleanUpState(action, true);
return next(nextAction);
}
return next(action);
};
}
function resetStateForNewVersion(action) {
const {payload} = action;
const lastChannelForTeam = getLastChannelForTeam(payload);
let general = initialState.entities.general;
if (payload.entities.general) {
general = payload.entities.general;
}
let teams = initialState.entities.teams;
if (payload.entities.teams) {
teams = {
currentTeamId: payload.entities.teams.currentTeamId,
teams: payload.entities.teams.teams,
myMembers: payload.entities.teams.myMembers,
};
}
let users = initialState.entities.users;
if (payload.entities.users) {
const currentUserId = payload.entities.users.currentUserId;
if (currentUserId) {
users = {
currentUserId,
profiles: {
[currentUserId]: payload.entities.users.profiles[currentUserId],
},
};
}
}
let preferences = initialState.entities.preferences;
if (payload.entities.preferences) {
preferences = payload.entities.preferences;
}
let roles = initialState.entities.roles;
if (payload.entities.roles) {
roles = payload.entities.roles;
}
let search = initialState.entities.search;
if (payload.entities.search && payload.entities.search.recent) {
search = {
recent: payload.entities.search.recent,
};
}
let channelDrafts = initialState.views.channel.drafts;
if (payload.views.channel && payload.views.channel.drafts) {
channelDrafts = payload.views.channel.drafts;
}
let i18n = initialState.views.i18n;
if (payload.views.i18n) {
i18n = payload.views.i18n;
}
let lastTeamId = initialState.views.team.lastTeamId;
if (payload.views.team && payload.views.team.lastTeamId) {
lastTeamId = payload.views.team.lastTeamId;
}
const currentChannelId = lastChannelForTeam[lastTeamId] && lastChannelForTeam[lastTeamId].length ? lastChannelForTeam[lastTeamId][0] : '';
let channels = initialState.entities.channels;
if (payload.entities.channels && currentChannelId) {
channels = {
currentChannelId,
channels: {
[currentChannelId]: payload.entities.channels.channels[currentChannelId],
},
myMembers: {
[currentChannelId]: payload.entities.channels.myMembers[currentChannelId],
},
};
}
let threadDrafts = initialState.views.thread.drafts;
if (payload.views.thread && payload.views.thread.drafts) {
threadDrafts = payload.views.thread.drafts;
}
let selectServer = initialState.views.selectServer;
if (payload.views.selectServer) {
selectServer = payload.views.selectServer;
}
let recentEmojis = initialState.views.recentEmojis;
if (payload.views.recentEmojis) {
recentEmojis = payload.views.recentEmojis;
}
const nextState = {
app: {
build: DeviceInfo.getBuildNumber(),
version: DeviceInfo.getVersion(),
},
entities: {
channels,
general,
teams,
users,
preferences,
search,
roles,
},
views: {
channel: {
drafts: channelDrafts,
},
i18n,
team: {
lastTeamId,
lastChannelForTeam,
},
thread: {
drafts: threadDrafts,
},
root: {
hydrationComplete: true,
},
selectServer,
recentEmojis,
},
websocket: {
lastConnectAt: payload.websocket?.lastConnectAt,
lastDisconnectAt: payload.websocket?.lastDisconnectAt,
},
};
return {
type: action.type,
payload: nextState,
error: action.error,
};
}
function getLastChannelForTeam(payload) {
const lastChannelForTeam = {...payload.views.team.lastChannelForTeam};
const convertLastChannelForTeam = Object.values(lastChannelForTeam).some((value) => !Array.isArray(value));
if (convertLastChannelForTeam) {
Object.keys(lastChannelForTeam).forEach((id) => {
lastChannelForTeam[id] = [lastChannelForTeam[id]];
});
}
return lastChannelForTeam;
}
export function cleanUpState(action, keepCurrent = false) {
const {payload: resetPayload} = resetStateForNewVersion(action);
const {payload} = action;
const {currentChannelId} = payload.entities.channels;
const {lastChannelForTeam} = resetPayload.views.team;
const nextEntities = {
posts: {
posts: {},
postsInChannel: {},
postsInThread: {},
reactions: {},
openGraph: payload.entities.posts.openGraph,
selectedPostId: payload.entities.posts.selectedPostId,
currentFocusedPostId: payload.entities.posts.currentFocusedPostId,
},
files: {
files: {},
fileIdsByPostId: {},
},
};
let retentionPeriod = 0;
if (resetPayload.entities.general && resetPayload.entities.general.dataRetentionPolicy &&
resetPayload.entities.general.dataRetentionPolicy.message_deletion_enabled) {
retentionPeriod = resetPayload.entities.general.dataRetentionPolicy.message_retention_cutoff;
}
const postIdsToKeep = [];
// Keep the last 60 posts in each recently viewed channel
nextEntities.posts.postsInChannel = cleanUpPostsInChannel(payload.entities.posts.postsInChannel, lastChannelForTeam, keepCurrent ? currentChannelId : '');
postIdsToKeep.push(...getAllFromPostsInChannel(nextEntities.posts.postsInChannel));
// Keep any posts that appear in search results
let searchResults = [];
let flaggedPosts = [];
if (payload.entities.search) {
if (payload.entities.search.results?.length) {
const {results} = payload.entities.search;
searchResults = results;
postIdsToKeep.push(...results);
}
if (payload.entities.search.flagged?.length) {
const {flagged} = payload.entities.search;
flaggedPosts = flagged;
postIdsToKeep.push(...flagged);
}
}
postIdsToKeep.forEach((postId) => {
const post = payload.entities.posts.posts[postId];
if (post) {
if (retentionPeriod && post.create_at < retentionPeriod) {
// This post has been removed by data retention, so don't keep it
removeFromPostsInChannel(nextEntities.posts.postsInChannel, post.channel_id, postId);
return;
}
// Keep the post
nextEntities.posts.posts[postId] = post;
// And its reactions
const reaction = payload.entities.posts.reactions[postId];
if (reaction) {
nextEntities.posts.reactions[postId] = reaction;
}
// And its files
const fileIds = payload.entities.files.fileIdsByPostId[postId];
if (fileIds) {
nextEntities.files.fileIdsByPostId[postId] = fileIds;
fileIds.forEach((fileId) => {
nextEntities.files.files[fileId] = payload.entities.files.files[fileId];
});
}
// And its comments
const postsInThread = payload.entities.posts.postsInThread[postId];
if (postsInThread) {
nextEntities.posts.postsInThread[postId] = postsInThread;
}
}
});
// Remove any pending posts that haven't failed
if (payload.entities.posts && payload.entities.posts.pendingPostIds && payload.entities.posts.pendingPostIds.length) {
const nextPendingPostIds = [...payload.entities.posts.pendingPostIds];
payload.entities.posts.pendingPostIds.forEach((id) => {
const posts = nextEntities.posts.posts;
const post = posts[id];
if (post && !post.failed) {
Reflect.deleteProperty(posts, id);
removeFromPostsInChannel(nextEntities.posts.postsInChannel, post.channel_id, id);
removePendingPost(nextPendingPostIds, id);
} else if (!post) {
removePendingPost(nextPendingPostIds, id);
}
});
nextEntities.posts.pendingPostIds = nextPendingPostIds;
}
const nextState = {
app: resetPayload.app,
entities: {
...nextEntities,
channels: payload.entities.channels,
emojis: payload.entities.emojis,
general: resetPayload.entities.general,
preferences: resetPayload.entities.preferences,
search: {
...resetPayload.entities.search,
results: searchResults,
flagged: flaggedPosts,
},
teams: resetPayload.entities.teams,
users: payload.entities.users,
roles: resetPayload.entities.roles,
},
views: {
announcement: payload.views.announcement,
...resetPayload.views,
channel: {
...resetPayload.views.channel,
...payload.views.channel,
},
},
websocket: {
lastConnectAt: payload.websocket?.lastConnectAt,
lastDisconnectAt: payload.websocket?.lastDisconnectAt,
},
};
nextState.errors = payload.errors;
return {
type: action.type,
payload: nextState,
error: action.error,
};
}
// cleanUpPostsInChannel returns a copy of postsInChannel where only the most recent posts in each channel are kept
export function cleanUpPostsInChannel(postsInChannel, lastChannelForTeam, currentChannelId, recentPostCount = 60) {
const nextPostsInChannel = {};
for (const channelIds of Object.values(lastChannelForTeam)) {
for (const channelId of channelIds) {
if (nextPostsInChannel[channelId]) {
// This is a DM or GM channel that we've already seen on another team
continue;
}
const postsForChannel = postsInChannel[channelId];
if (!postsForChannel) {
// We don't have anything to keep for this channel
continue;
}
let nextPostsForChannel;
if (channelId === currentChannelId) {
// Keep all of the posts for this channel
nextPostsForChannel = postsForChannel;
} else {
// Only keep the most recent posts for this channel
const recentBlock = postsForChannel.find((block) => block.recent);
if (!recentBlock) {
// We don't have recent posts for this channel
continue;
}
nextPostsForChannel = [{
...recentBlock,
order: recentBlock.order.slice(0, recentPostCount),
}];
}
nextPostsInChannel[channelId] = nextPostsForChannel;
}
}
return nextPostsInChannel;
}
// getAllFromPostsInChannel returns an array of all post IDs found in postsInChannel
export function getAllFromPostsInChannel(postsInChannel) {
const postIds = [];
for (const postsForChannel of Object.values(postsInChannel)) {
for (const block of postsForChannel) {
postIds.push(...block.order);
}
}
return postIds;
}
function removeFromPostsInChannel(postsInChannel, channelId, postId) {
const postsForChannel = postsInChannel[channelId];
if (!postsForChannel) {
return;
}
// Since this has already gone through cleanUpPostsInChannel, we know that there can only be one block to look at
const index = postsForChannel[0].order.indexOf(postId);
if (index !== -1) {
postsForChannel[0].order.splice(index, 1);
}
}
function removePendingPost(pendingPostIds, id) {
const pendingIndex = pendingPostIds.indexOf(id);
if (pendingIndex !== -1) {
pendingPostIds.splice(pendingIndex, 1);
}
}