forked from Ivasoft/mattermost-mobile
Data cleanup (#1070)
* Add message retention middleware and config * Data cleanup middleware
This commit is contained in:
@@ -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 &&
|
||||
|
||||
@@ -10,6 +10,7 @@ export const UpgradeTypes = {
|
||||
};
|
||||
|
||||
const ViewTypes = keyMirror({
|
||||
DATA_CLEANUP: null,
|
||||
SERVER_URL_CHANGED: null,
|
||||
|
||||
LOGIN_ID_CHANGED: null,
|
||||
|
||||
@@ -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
|
||||
|
||||
7
app/reducers/app/build.js
Normal file
7
app/reducers/app/build.js
Normal 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
12
app/reducers/app/index.js
Normal 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
|
||||
});
|
||||
7
app/reducers/app/version.js
Normal file
7
app/reducers/app/version.js
Normal 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;
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
201
app/store/middleware.js
Normal 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
|
||||
};
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
{
|
||||
"DefaultServerUrl": "",
|
||||
"EnableMessageRetention": false,
|
||||
"MessageRetentionPeriod": 30,
|
||||
"TestServerUrl": "http://localhost:8065",
|
||||
"DefaultTheme": "default",
|
||||
"ShowErrorsList": false,
|
||||
|
||||
Reference in New Issue
Block a user