Data cleanup (#1070)

* Add message retention middleware and config

* Data cleanup middleware
This commit is contained in:
enahum
2017-10-30 12:23:19 -03:00
committed by Harrison Healey
parent 898d02c5d4
commit 3fdfe92800
14 changed files with 279 additions and 32 deletions

View File

@@ -223,7 +223,8 @@ export function selectInitialChannel(teamId) {
const {channels, myMembers} = state.entities.channels;
const {currentUserId} = state.entities.users;
const {myPreferences} = state.entities.preferences;
const lastChannelId = state.views.team.lastChannelForTeam[teamId] || '';
const lastChannelForTeam = state.views.team.lastChannelForTeam[teamId];
const lastChannelId = lastChannelForTeam && lastChannelForTeam.length ? lastChannelForTeam[0] : '';
const lastChannel = channels[lastChannelId];
const isDMVisible = lastChannel && lastChannel.type === General.DM_CHANNEL &&

View File

@@ -10,6 +10,7 @@ export const UpgradeTypes = {
};
const ViewTypes = keyMirror({
DATA_CLEANUP: null,
SERVER_URL_CHANGED: null,
LOGIN_ID_CHANGED: null,

View File

@@ -194,8 +194,10 @@ export default class Mattermost {
try {
if (!isActive && !this.inBackgroundSince) {
this.inBackgroundSince = Date.now();
dispatch({type: ViewTypes.DATA_CLEANUP, payload: getState()});
} else if (isActive && this.inBackgroundSince && (Date.now() - this.inBackgroundSince) >= AUTHENTICATION_TIMEOUT) {
this.inBackgroundSince = null;
if (this.mdmEnabled) {
const config = await mattermostManaged.getConfig();
const authNeeded = config.inAppPinCode && config.inAppPinCode === 'true';
@@ -206,6 +208,8 @@ export default class Mattermost {
}
}
}
} else if (isActive) {
this.inBackgroundSince = null;
}
} catch (error) {
// do nothing

View File

@@ -0,0 +1,7 @@
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
export default function build(state = '') {
return state;
}

12
app/reducers/app/index.js Normal file
View File

@@ -0,0 +1,12 @@
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import {combineReducers} from 'redux';
import build from './build';
import version from './version';
export default combineReducers({
build,
version
});

View File

@@ -0,0 +1,7 @@
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
export default function version(state = '') {
return state;
}

View File

@@ -1,11 +1,13 @@
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import app from './app';
import device from './device';
import navigation from './navigation';
import views from './views';
export default {
app,
device,
navigation,
views

View File

@@ -4,7 +4,8 @@
import {combineReducers} from 'redux';
import {
ChannelTypes,
FileTypes
FileTypes,
PostTypes
} from 'mattermost-redux/action_types';
import {ViewTypes} from 'app/constants';
@@ -238,6 +239,14 @@ function retryFailed(state = false, action) {
switch (action.type) {
case ViewTypes.SET_CHANNEL_RETRY_FAILED:
return action.failed;
case PostTypes.GET_POSTS_SUCCESS:
case PostTypes.GET_POSTS_SINCE_SUCCESS: {
if (state) {
return false;
}
return state;
}
default:
return state;
}

View File

@@ -17,11 +17,33 @@ function lastTeamId(state = '', action) {
function lastChannelForTeam(state = {}, action) {
switch (action.type) {
case ViewTypes.SET_LAST_CHANNEL_FOR_TEAM:
case ViewTypes.SET_LAST_CHANNEL_FOR_TEAM: {
const team = state[action.teamId];
const channelIds = [];
if (!action.channelId) {
return state;
}
if (team) {
channelIds.push(...team);
const index = channelIds.indexOf(action.channelId);
if (index === -1) {
channelIds.unshift(action.channelId);
channelIds.slice(0, 5);
} else {
channelIds.splice(index, 1);
channelIds.unshift(action.channelId);
}
} else {
channelIds.push(action.channelId);
}
return {
...state,
[action.teamId]: action.channelId
[action.teamId]: channelIds
};
}
default:
return state;
}

View File

@@ -51,27 +51,4 @@ function mapDispatchToProps(dispatch) {
};
}
function areStatesEqual(next, prev) {
// When switching teams
if (next.entities.teams.currentTeamId !== prev.entities.teams.currentTeamId) {
return false;
}
// When we have a new channel after switching teams
const prevChannelId = prev.entities.channels.currentChannelId;
const nextChannelId = next.entities.channels.currentChannelId;
if (nextChannelId !== prevChannelId) {
return false;
}
// When getting the channels for a team and the request fails
const prevStatus = prev.requests.channels.myChannels.status;
const nextStatus = next.requests.channels.myChannels.status;
if (!nextChannelId && prevStatus === RequestStatus.STARTED && nextStatus === RequestStatus.FAILURE) {
return false;
}
return true;
}
export default connect(mapStateToProps, mapDispatchToProps, null, {pure: true, areStatesEqual})(Channel);
export default connect(mapStateToProps, mapDispatchToProps)(Channel);

View File

@@ -16,6 +16,7 @@ import appReducer from 'app/reducers';
import networkConnectionListener from 'app/utils/network';
import {createSentryMiddleware} from 'app/utils/sentry/middleware';
import {messageRetention} from './middleware';
import {transformSet} from './utils';
function getAppReducer() {
@@ -193,7 +194,8 @@ export default function configureAppStore(initialState) {
}
};
const additionalMiddleware = [createSentryMiddleware(), messageRetention];
return configureStore(initialState, appReducer, offlineOptions, getAppReducer, {
additionalMiddleware: createSentryMiddleware()
additionalMiddleware
});
}

201
app/store/middleware.js Normal file
View File

@@ -0,0 +1,201 @@
// Copyright (c) 2017-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 Config from 'assets/config';
export function messageRetention() {
return (next) => (action) => {
if (action.type === 'persist/REHYDRATE') {
const {app} = action.payload;
const {entities} = action.payload;
if (!entities) {
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
return next(cleanupState(action));
} 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);
const currentUserId = payload.entities.users.currentUserId;
const nextState = {
app: {
build: DeviceInfo.getBuildNumber(),
version: DeviceInfo.getVersion()
},
entities: {
general: payload.entities.general,
teams: {
currentTeamId: payload.entities.teams.currentTeamId,
teams: payload.entities.teams.teams,
myMembers: payload.entities.teams.myMembers
},
users: {
currentUserId,
profiles: {
[currentUserId]: payload.entities.users.profiles[currentUserId]
}
},
preferences: payload.entities.preferences,
search: {
recent: payload.entities.search.recent
}
},
views: {
channel: {
drafts: payload.views.channel.drafts
},
i18n: payload.views.i18n,
fetchCache: payload.views.fetchCache,
team: {
lastTeamId: payload.views.team.lastTeamId,
lastChannelForTeam
},
thread: {
drafts: payload.views.thread.drafts
},
selectServer: payload.views.selectServer
}
};
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;
}
function cleanupState(action, keepCurrent = false) {
const {payload: resetPayload} = resetStateForNewVersion(action);
const {payload} = action;
const {currentChannelId} = payload.entities.channels;
const {statuses, ...otherUsers} = payload.entities.users; //eslint-disable-line no-unused-vars
const {lastChannelForTeam} = resetPayload.views.team;
const nextEntitites = {
posts: {
posts: {},
postsInChannel: {},
reactions: {},
selectedPostId: payload.entities.posts.selectedPostId,
currentFocusedPostId: payload.entities.posts.currentFocusedPostId
},
files: {
files: {},
fileIdsByPostId: {}
}
};
const retentionPeriod = Config.EnableMessageRetention ? Config.MessageRetentionPeriod + 1 : 0;
const postIdsToKeep = Object.values(lastChannelForTeam).reduce((array, channelIds) => {
const ids = channelIds.reduce((result, id) => {
// we need to check that the channel id is not already included
// the reason it can be included is cause at least one of the last channels viewed
// in a team can be a DM or GM and the id can be duplicate
if (!nextEntitites.posts.postsInChannel[id]) {
let postIds;
if (keepCurrent && currentChannelId === id) {
postIds = payload.entities.posts.postsInChannel[id];
} else {
postIds = payload.entities.posts.postsInChannel[id].slice(0, 60);
}
nextEntitites.posts.postsInChannel[id] = postIds;
return result.concat(postIds);
}
return result;
}, []);
return array.concat(ids);
}, []);
postIdsToKeep.forEach((postId) => {
const post = payload.entities.posts.posts[postId];
const skip = keepCurrent && currentChannelId === post.channel_id;
if (!skip && retentionPeriod && (Date.now() - post.create_at) / (1000 * 3600 * 24) > retentionPeriod) {
const postsInChannel = nextEntitites.posts.postsInChannel[post.channel_id];
const index = postsInChannel.indexOf(postId);
if (index !== -1) {
postsInChannel.splice(index, 1);
}
return;
}
nextEntitites.posts.posts[postId] = post;
const reaction = payload.entities.posts.reactions[postId];
if (reaction) {
nextEntitites.posts.reactions[postId] = reaction;
}
const fileIds = payload.entities.files.fileIdsByPostId[postId];
if (fileIds) {
nextEntitites.files.fileIdsByPostId[postId] = fileIds;
fileIds.forEach((fileId) => {
nextEntitites.files.files[fileId] = payload.entities.files.files[fileId];
});
}
});
const nextState = {
app: resetPayload.app,
entities: {
...nextEntitites,
channels: payload.entities.channels,
emojis: payload.entities.emojis,
general: resetPayload.entities.general,
preferences: resetPayload.entities.preferences,
search: resetPayload.entities.search,
teams: resetPayload.entities.teams,
users: {
...otherUsers
}
},
views: {
...resetPayload.views
}
};
if (keepCurrent) {
nextState.errors = payload.errors;
}
return {
type: 'persist/REHYDRATE',
payload: nextState,
error: action.error
};
}

View File

@@ -1,9 +1,9 @@
import {NetInfo} from 'react-native';
import {Client4} from 'mattermost-redux/client';
export async function checkConnection() {
const server = Client4.getUrl() || 'https://www.google.com';
// If the websocket cannot connect probably is because the Mattermost server
// is down and we don't want to make the app think the device is offline
const server = 'https://www.google.com';
try {
await fetch(server);

View File

@@ -1,5 +1,7 @@
{
"DefaultServerUrl": "",
"EnableMessageRetention": false,
"MessageRetentionPeriod": 30,
"TestServerUrl": "http://localhost:8065",
"DefaultTheme": "default",
"ShowErrorsList": false,