* objective-C share extension * MattermostBucket module to share data between the main app and the extension * middleware that shares the data between the main app and the extension * Fix setState when safe area in unmounted * Share extension for iOS * Fastlane changes to include iOS share extension * Fix unit test * Feedback review * define proptypes for icons
407 lines
14 KiB
JavaScript
407 lines
14 KiB
JavaScript
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
|
|
// See License.txt for license information.
|
|
|
|
import DeviceInfo from 'react-native-device-info';
|
|
|
|
import {ChannelTypes, GeneralTypes, TeamTypes, UserTypes} from 'mattermost-redux/action_types';
|
|
import {General} from 'mattermost-redux/constants';
|
|
import {getTeammateNameDisplaySetting} from 'mattermost-redux/selectors/entities/preferences';
|
|
import {getUserIdFromChannelName, getGroupDisplayNameFromUserIds} from 'mattermost-redux/utils/channel_utils';
|
|
import {displayUsername} from 'mattermost-redux/utils/user_utils';
|
|
|
|
import {ViewTypes} from 'app/constants';
|
|
import initialState from 'app/initial_state';
|
|
import mattermostBucket from 'app/mattermost_bucket';
|
|
import Config from 'assets/config';
|
|
|
|
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 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 fetchCache = initialState.views.fetchCache;
|
|
if (payload.views.fetchCache) {
|
|
fetchCache = payload.views.fetchCache;
|
|
}
|
|
|
|
let lastTeamId = initialState.views.team.lastTeamId;
|
|
if (payload.views.team && payload.views.team.lastTeamId) {
|
|
lastTeamId = payload.views.team.lastTeamId;
|
|
}
|
|
|
|
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: {
|
|
general,
|
|
teams,
|
|
users,
|
|
preferences,
|
|
search
|
|
},
|
|
views: {
|
|
channel: {
|
|
drafts: channelDrafts
|
|
},
|
|
i18n,
|
|
fetchCache,
|
|
team: {
|
|
lastTeamId,
|
|
lastChannelForTeam
|
|
},
|
|
thread: {
|
|
drafts: threadDrafts
|
|
},
|
|
selectServer,
|
|
recentEmojis
|
|
}
|
|
};
|
|
|
|
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 {lastChannelForTeam} = resetPayload.views.team;
|
|
const nextEntitites = {
|
|
posts: {
|
|
posts: {},
|
|
postsInChannel: {},
|
|
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 = 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] && payload.entities.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);
|
|
}, []);
|
|
|
|
let searchResults = [];
|
|
if (payload.entities.search && payload.entities.search.results.length) {
|
|
const {results} = payload.entities.search;
|
|
searchResults = results;
|
|
postIdsToKeep.push(...results);
|
|
}
|
|
|
|
postIdsToKeep.forEach((postId) => {
|
|
const post = payload.entities.posts.posts[postId];
|
|
|
|
if (post) {
|
|
if (retentionPeriod && post.create_at < 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];
|
|
});
|
|
}
|
|
} else {
|
|
// If the post is not in the store we need to remove it from the postsInChannel
|
|
const channelIds = Object.keys(nextEntitites.posts.postsInChannel);
|
|
for (let i = 0; i < channelIds.length; i++) {
|
|
const channelId = channelIds[i];
|
|
const posts = nextEntitites.posts.postsInChannel[channelId];
|
|
const index = posts.indexOf(postId);
|
|
if (index !== -1) {
|
|
posts.splice(index, 1);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
});
|
|
|
|
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,
|
|
results: searchResults
|
|
},
|
|
teams: resetPayload.entities.teams,
|
|
users: payload.entities.users
|
|
},
|
|
views: {
|
|
...resetPayload.views,
|
|
channel: {
|
|
...resetPayload.views.channel,
|
|
...payload.views.channel
|
|
}
|
|
}
|
|
};
|
|
|
|
nextState.errors = payload.errors;
|
|
|
|
return {
|
|
type: 'persist/REHYDRATE',
|
|
payload: nextState,
|
|
error: action.error
|
|
};
|
|
}
|
|
|
|
export function shareExtensionData(store) {
|
|
return (next) => (action) => {
|
|
// allow other middleware to do their things
|
|
const nextAction = next(action);
|
|
|
|
switch (action.type) {
|
|
case 'persist/REHYDRATE': {
|
|
const {entities} = action.payload;
|
|
if (entities) {
|
|
if (entities.general && entities.general.credentials && entities.general.credentials.token) {
|
|
mattermostBucket.set('credentials', JSON.stringify(entities.general.credentials), Config.AppGroupId);
|
|
}
|
|
|
|
if (entities.teams) {
|
|
const {currentTeamId, teams} = entities.teams;
|
|
if (currentTeamId) {
|
|
const team = teams[currentTeamId];
|
|
const teamToSave = {
|
|
id: currentTeamId,
|
|
name: team.name,
|
|
display_name: team.display_name
|
|
};
|
|
mattermostBucket.set('selectedTeam', JSON.stringify(teamToSave), Config.AppGroupId);
|
|
}
|
|
}
|
|
|
|
if (entities.users) {
|
|
const {currentUserId} = entities.users;
|
|
if (currentUserId) {
|
|
mattermostBucket.set('currentUserId', currentUserId, Config.AppGroupId);
|
|
}
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
case GeneralTypes.RECEIVED_APP_CREDENTIALS:
|
|
mattermostBucket.set('credentials', JSON.stringify(action.data), Config.AppGroupId);
|
|
break;
|
|
case ChannelTypes.SELECT_CHANNEL: {
|
|
const state = store.getState();
|
|
const {channels} = state.entities.channels;
|
|
const {currentUserId, profiles, profilesInChannel} = state.entities.users;
|
|
const channel = {...channels[action.data]};
|
|
if (channel.type === General.DM_CHANNEL) {
|
|
const teammateId = getUserIdFromChannelName(currentUserId, channel.name);
|
|
channel.display_name = displayUsername(profiles[teammateId], getTeammateNameDisplaySetting(state));
|
|
} else if (channel.type === General.GM_CHANNEL) {
|
|
channel.display_name = getGroupDisplayNameFromUserIds(
|
|
profilesInChannel[channel.id],
|
|
profiles,
|
|
currentUserId,
|
|
getTeammateNameDisplaySetting(state)
|
|
);
|
|
}
|
|
|
|
const channelToSave = {
|
|
id: channel.id,
|
|
name: channel.name,
|
|
display_name: channel.display_name,
|
|
type: channel.type
|
|
};
|
|
mattermostBucket.set('selectedChannel', JSON.stringify(channelToSave), Config.AppGroupId);
|
|
break;
|
|
}
|
|
case 'BATCH_SELECT_TEAM': {
|
|
const teamData = action.payload.find((data) => data.type === TeamTypes.SELECT_TEAM);
|
|
if (teamData && teamData.data) {
|
|
const team = store.getState().entities.teams.teams[teamData.data];
|
|
const teamToSave = {
|
|
id: team.id,
|
|
name: team.name,
|
|
display_name: team.display_name
|
|
};
|
|
mattermostBucket.set('selectedTeam', JSON.stringify(teamToSave), Config.AppGroupId);
|
|
}
|
|
break;
|
|
}
|
|
case UserTypes.RECEIVED_ME:
|
|
mattermostBucket.set('currentUserId', action.data.id, Config.AppGroupId);
|
|
break;
|
|
case UserTypes.LOGOUT_SUCCESS:
|
|
mattermostBucket.remove('credentials', Config.AppGroupId);
|
|
mattermostBucket.remove('selectedChannel', Config.AppGroupId);
|
|
mattermostBucket.remove('selectedTeam', Config.AppGroupId);
|
|
mattermostBucket.remove('currentUserId', Config.AppGroupId);
|
|
mattermostBucket.remove('emm', Config.AppGroupId);
|
|
break;
|
|
}
|
|
return nextAction;
|
|
};
|
|
}
|