Compare commits

...

10 Commits

Author SHA1 Message Date
Miguel Alatzar
41afce3e1d Bump app build number to 287 (#4229) (#4230) 2020-04-24 11:59:19 -07:00
Elias Nahum
87abcce7fa MM-24451 Set prev app version to current on logout/reset cache (#4224)
* MM-24451 Set prev app version to current on logout/reset cache

* Fix unit tests for release-1.30
2020-04-24 11:11:52 -07:00
Elias Nahum
8388ae1a30 Bump app build number to 286 and version to 1.30.1 (#4221)
* Bump app build number to 286

* Bump app version number to 1.30.1
2020-04-23 13:38:29 -04:00
Amit Uttam
605cc2afd8 Run message retention cleanup off of pre-existing state (#4211)
Instead of a reconstructed "zero" state.

Only posts in channels, searched posts and flag posts are recalculated (as per data retention policy, if applicable). The rest of state is cloned from existing state.
2020-04-23 12:57:58 -03:00
Mattermost Build
7f66fe84a7 Automated cherry pick of #4207 (#4213)
* MM-24385 Fix ExperimentalStrictCSRFEnforcement

* Fix ESLint errors

Co-authored-by: Elias Nahum <nahumhbl@gmail.com>
2020-04-23 07:55:49 -04:00
Miguel Alatzar
da701268d5 [MM-24451] Handle setting previousVersion on logout and clearing data (#4205)
* Handle setting previousVersion on login and clearing data

* Fix unused import error

* Update test

* Just add previous version on logout
2020-04-22 21:04:24 -07:00
Miguel Alatzar
318cd13064 Check canPost permissions for v5.22+ (#4193) (#4201) 2020-04-22 20:32:12 -07:00
Mattermost Build
2fa572bcc7 Automated cherry pick of #4195 (#4206)
* Patch react-native-image-picker to allow selecting video

* Improve patch

* Fix Attaching a Photo capture

Co-authored-by: Elias Nahum <nahumhbl@gmail.com>
2020-04-22 19:10:28 -04:00
Elias Nahum
976050ecc2 MM-24446 Fix Crash on iOS with EMM enabled (1.30) (#4204)
* Fix Crash on iOS with EMM enabled

* Fix Android Passcode authentication

* Fix Login screen header when EMM does not allow other servers
2020-04-22 19:09:32 -04:00
Miguel Alatzar
27411cb119 [MM-24426] Set previous app version in redux store (1.30) (#4196)
* Set previous app version in redux store

* Update state

* Log error
2020-04-22 08:23:45 -07:00
26 changed files with 643 additions and 271 deletions

View File

@@ -130,8 +130,8 @@ android {
applicationId "com.mattermost.rnbeta"
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
versionCode 285
versionName "1.30.0"
versionCode 287
versionName "1.30.1"
multiDexEnabled = true
ndk {
abiFilters 'armeabi-v7a','arm64-v8a','x86','x86_64'

View File

@@ -26,7 +26,6 @@ export function completeLogin(user, deviceToken) {
const token = Client4.getToken();
const url = Client4.getUrl();
setCSRFFromCookie(url);
setAppCredentials(deviceToken, user.id, token, url);
// Set timezone
@@ -125,6 +124,7 @@ export function login(loginId, password, mfaToken, ldapOnly = false) {
try {
user = await Client4.login(loginId, password, mfaToken, deviceToken, ldapOnly);
await setCSRFFromCookie(Client4.getUrl());
} catch (error) {
return {error};
}
@@ -142,6 +142,7 @@ export function login(loginId, password, mfaToken, ldapOnly = false) {
export function ssoLogin(token) {
return async (dispatch) => {
Client4.setToken(token);
await setCSRFFromCookie(Client4.getUrl());
const result = await dispatch(loadMe());
if (!result.error) {
@@ -167,11 +168,8 @@ export function logout(skipServerLogout = false) {
}
export function forceLogoutIfNecessary(error) {
return async (dispatch, getState) => {
const state = getState();
const currentUserId = getCurrentUserId(state);
if (currentUserId && error.status_code === HTTP_UNAUTHORIZED && error.url && !error.url.includes('/login')) {
return async (dispatch) => {
if (error.status_code === HTTP_UNAUTHORIZED && error.url && !error.url.includes('/login')) {
dispatch(logout(true));
return true;
}

View File

@@ -30,7 +30,7 @@ import PostTextbox from './post_textbox';
const MAX_MESSAGE_LENGTH = 4000;
function mapStateToProps(state, ownProps) {
export function mapStateToProps(state, ownProps) {
const currentDraft = ownProps.rootId ? getThreadDraft(state, ownProps.rootId) : getCurrentChannelDraft(state);
const config = getConfig(state);
@@ -50,17 +50,19 @@ function mapStateToProps(state, ownProps) {
const currentChannelStats = getCurrentChannelStats(state);
const currentChannelMembersCount = currentChannelStats?.member_count || 0; // eslint-disable-line camelcase
const isTimezoneEnabled = config?.ExperimentalTimezone === 'true';
const canPost = haveIChannelPermission(
state,
{
channel: currentChannel.id,
team: currentChannel.team_id,
permission: Permissions.CREATE_POST,
},
);
let canPost = true;
let useChannelMentions = true;
if (isMinimumServerVersion(state.entities.general.serverVersion, 5, 22)) {
canPost = haveIChannelPermission(
state,
{
channel: currentChannel.id,
team: currentChannel.team_id,
permission: Permissions.CREATE_POST,
},
);
useChannelMentions = haveIChannelPermission(
state,
{

View File

@@ -0,0 +1,100 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {Permissions} from 'mattermost-redux/constants';
import * as channelSelectors from 'mattermost-redux/selectors/entities/channels';
import * as userSelectors from 'mattermost-redux/selectors/entities/users';
import * as generalSelectors from 'mattermost-redux/selectors/entities/general';
import * as preferenceSelectors from 'mattermost-redux/selectors/entities/preferences';
import * as roleSelectors from 'mattermost-redux/selectors/entities/roles';
import * as deviceSelectors from 'app/selectors/device';
import {isMinimumServerVersion} from 'mattermost-redux/utils/helpers';
import {mapStateToProps} from './index';
jest.mock('./post_textbox', () => ({
__esModule: true,
default: jest.fn(),
}));
channelSelectors.getCurrentChannel = jest.fn().mockReturnValue({});
channelSelectors.isCurrentChannelReadOnly = jest.fn();
channelSelectors.getCurrentChannelStats = jest.fn();
userSelectors.getStatusForUserId = jest.fn();
generalSelectors.canUploadFilesOnMobile = jest.fn();
preferenceSelectors.getTheme = jest.fn();
roleSelectors.haveIChannelPermission = jest.fn();
deviceSelectors.isLandscape = jest.fn();
describe('mapStateToProps', () => {
const baseState = {
entities: {
general: {
config: {},
serverVersion: '',
},
users: {
currentUserId: '',
},
channels: {
currentChannelId: '',
},
preferences: {
myPreferences: {},
},
},
views: {
channel: {
drafts: {},
},
},
requests: {
files: {
uploadFiles: {
status: '',
},
},
},
};
const baseOwnProps = {};
test('haveIChannelPermission is not called when isMinimumServerVersion is not 5.22v', () => {
const state = {...baseState};
state.entities.general.serverVersion = '5.21';
mapStateToProps(state, baseOwnProps);
expect(isMinimumServerVersion(state.entities.general.serverVersion, 5, 22)).toBe(false);
expect(roleSelectors.haveIChannelPermission).not.toHaveBeenCalledWith(state, {
channel: undefined,
team: undefined,
permission: Permissions.CREATE_POST,
});
expect(roleSelectors.haveIChannelPermission).not.toHaveBeenCalledWith(state, {
channel: undefined,
permission: Permissions.USE_CHANNEL_MENTIONS,
});
});
test('haveIChannelPermission is called when isMinimumServerVersion is 5.22v', () => {
const state = {...baseState};
state.entities.general.serverVersion = '5.22';
mapStateToProps(state, baseOwnProps);
expect(isMinimumServerVersion(state.entities.general.serverVersion, 5, 22)).toBe(true);
expect(roleSelectors.haveIChannelPermission).toHaveBeenCalledWith(state, {
channel: undefined,
team: undefined,
permission: Permissions.CREATE_POST,
});
expect(roleSelectors.haveIChannelPermission).toHaveBeenCalledWith(state, {
channel: undefined,
permission: Permissions.USE_CHANNEL_MENTIONS,
});
});
});

View File

@@ -280,6 +280,7 @@ class GlobalEventHandler {
app: {
build: DeviceInfo.getBuildNumber(),
version: DeviceInfo.getVersion(),
previousVersion: DeviceInfo.getVersion(),
},
},
},

View File

@@ -7,7 +7,7 @@ import {Provider} from 'react-redux';
import EventEmitter from 'mattermost-redux/utils/event_emitter';
import {loadMe} from 'app/actions/views/user';
import {loadMe, logout} from 'app/actions/views/user';
import {resetToChannel, resetToSelectServer} from 'app/actions/navigation';
import {setDeepLinkURL} from 'app/actions/views/root';
@@ -24,6 +24,7 @@ import EphemeralStore from 'app/store/ephemeral_store';
import telemetry from 'app/telemetry';
import {validatePreviousVersion} from 'app/utils/general';
import pushNotificationsUtils from 'app/utils/push_notifications';
import {captureJSException} from 'app/utils/sentry';
const init = async () => {
const credentials = await getAppCredentials();
@@ -53,10 +54,15 @@ const launchApp = (credentials) => {
if (credentials) {
waitForHydration(store, () => {
const valid = validatePreviousVersion(store);
const {previousVersion} = store.getState().app;
const valid = validatePreviousVersion(previousVersion);
if (valid) {
store.dispatch(loadMe());
resetToChannel({skipMetrics: true});
} else {
const error = new Error(`Previous app version "${previousVersion}" is invalid.`);
captureJSException(error, false, store);
store.dispatch(logout());
}
});
} else {

View File

@@ -36,11 +36,12 @@ export default {
listeners.splice(index, 1);
}
},
authenticate: () => {
authenticate: (opts) => {
if (!LocalAuth) {
LocalAuth = require('react-native-local-auth');
}
return LocalAuth.auth;
return LocalAuth.auth(opts);
},
blurAppScreen: emptyFunction,
appGroupIdentifier: null,

View File

@@ -5,8 +5,10 @@ import {combineReducers} from 'redux';
import build from './build';
import version from './version';
import previousVersion from './previousVersion';
export default combineReducers({
build,
version,
previousVersion,
});

View File

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

View File

@@ -56,14 +56,18 @@ export function makeMapStateToProps() {
let {canDelete} = ownProps;
let canFlag = true;
let canPin = true;
const canPost = haveIChannelPermission(
state,
{
channel: post.channel_id,
team: channel.team_id,
permission: Permissions.CREATE_POST,
},
);
let canPost = true;
if (isMinimumServerVersion(serverVersion, 5, 22)) {
canPost = haveIChannelPermission(
state,
{
channel: post.channel_id,
team: channel.team_id,
permission: Permissions.CREATE_POST,
},
);
}
if (hasNewPermissions(state)) {
canAddReaction = haveIChannelPermission(state, {

View File

@@ -2,11 +2,13 @@
// See LICENSE.txt for license information.
import {makeMapStateToProps} from './index';
import {Permissions} from 'mattermost-redux/constants';
import * as channelSelectors from 'mattermost-redux/selectors/entities/channels';
import * as generalSelectors from 'mattermost-redux/selectors/entities/general';
import * as userSelectors from 'mattermost-redux/selectors/entities/users';
import * as commonSelectors from 'mattermost-redux/selectors/entities/common';
import * as teamSelectors from 'mattermost-redux/selectors/entities/teams';
import * as roleSelectors from 'mattermost-redux/selectors/entities/roles';
import * as deviceSelectors from 'app/selectors/device';
import * as preferencesSelectors from 'mattermost-redux/selectors/entities/preferences';
import {isMinimumServerVersion} from 'mattermost-redux/utils/helpers';
@@ -26,6 +28,7 @@ teamSelectors.getCurrentTeamUrl = jest.fn();
deviceSelectors.getDimensions = jest.fn();
deviceSelectors.isLandscape = jest.fn();
preferencesSelectors.getTheme = jest.fn();
roleSelectors.haveIChannelPermission = jest.fn();
describe('makeMapStateToProps', () => {
const baseState = {
@@ -135,4 +138,44 @@ describe('makeMapStateToProps', () => {
expect(isMinimumServerVersion(state.entities.general.serverVersion, 5, 18)).toBe(false);
expect(props.canMarkAsUnread).toBe(false);
});
test('haveIChannelPermission for canPost is not called when isMinimumServerVersion is not 5.22v', () => {
const state = {
entities: {
...baseState.entities,
general: {
serverVersion: '5.21',
},
},
};
const mapStateToProps = makeMapStateToProps();
mapStateToProps(state, baseOwnProps);
expect(isMinimumServerVersion(state.entities.general.serverVersion, 5, 22)).toBe(false);
expect(roleSelectors.haveIChannelPermission).not.toHaveBeenCalledWith(state, {
channel: undefined,
team: undefined,
permission: Permissions.CREATE_POST,
});
});
test('haveIChannelPermission for canPost is called when isMinimumServerVersion is 5.22v', () => {
const state = {
entities: {
...baseState.entities,
general: {
serverVersion: '5.22',
},
},
};
const mapStateToProps = makeMapStateToProps();
mapStateToProps(state, baseOwnProps);
expect(isMinimumServerVersion(state.entities.general.serverVersion, 5, 22)).toBe(true);
expect(roleSelectors.haveIChannelPermission).toHaveBeenCalledWith(state, {
channel: undefined,
team: undefined,
permission: Permissions.CREATE_POST,
});
});
});

View File

@@ -67,6 +67,10 @@ export default class SelectServer extends PureComponent {
serverUrl: PropTypes.string.isRequired,
};
static defaultProps = {
allowOtherServers: true,
};
static contextTypes = {
intl: intlShape.isRequired,
};
@@ -159,11 +163,18 @@ export default class SelectServer extends PureComponent {
};
goToNextScreen = (screen, title, passProps = {}, navOptions = {}) => {
const {allowOtherServers} = this.props;
let visible = !LocalConfig.AutoSelectServerUrl;
if (!allowOtherServers) {
visible = false;
}
const defaultOptions = {
popGesture: !LocalConfig.AutoSelectServerUrl,
popGesture: visible,
topBar: {
visible: !LocalConfig.AutoSelectServerUrl,
height: LocalConfig.AutoSelectServerUrl ? 0 : null,
visible,
height: visible ? null : 0,
},
};
const options = merge(defaultOptions, navOptions);

View File

@@ -4,9 +4,6 @@
import DeviceInfo from 'react-native-device-info';
import {ViewTypes} from 'app/constants';
import initialState from 'app/initial_state';
import EphemeralStore from './ephemeral_store';
import {
captureException,
@@ -19,23 +16,21 @@ export function messageRetention(store) {
const {app} = action.payload;
const {entities, views} = action.payload;
if (!entities || !views) {
const version = DeviceInfo.getVersion();
EphemeralStore.prevAppVersion = version;
action.payload = {
...action.payload,
app: {
build: DeviceInfo.getBuildNumber(),
version,
},
};
return next(action);
}
const build = DeviceInfo.getBuildNumber();
const version = DeviceInfo.getVersion();
const previousVersion = app?.version;
EphemeralStore.prevAppVersion = app?.version;
if (app?.version !== DeviceInfo.getVersion() || app?.build !== DeviceInfo.getBuildNumber()) {
// When a new version of the app has been detected
return next(resetStateForNewVersion(action));
action.payload = {
...action.payload,
app: {
build,
version,
previousVersion,
},
};
if (!entities || !views) {
return next(action);
}
// Keep only the last 60 messages for the last 5 viewed channels in each team
@@ -60,140 +55,6 @@ export function messageRetention(store) {
};
}
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,
},
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));
@@ -208,11 +69,14 @@ function getLastChannelForTeam(payload) {
}
export function cleanUpState(action, keepCurrent = false) {
const {payload: resetPayload} = resetStateForNewVersion(action);
const {payload} = action;
const {currentChannelId} = payload.entities.channels;
const nextState = Object.assign({}, payload);
const lastTeamId = payload.views?.team?.lastTeamId;
const lastChannelForTeam = getLastChannelForTeam(payload);
const currentChannelId = lastChannelForTeam[lastTeamId] && lastChannelForTeam[lastTeamId].length ? lastChannelForTeam[lastTeamId][0] : '';
const {lastChannelForTeam} = resetPayload.views.team;
const nextEntities = {
posts: {
posts: {},
@@ -230,9 +94,9 @@ export function cleanUpState(action, keepCurrent = false) {
};
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;
if (payload.entities.general && payload.entities.general.dataRetentionPolicy &&
payload.entities.general.dataRetentionPolicy.message_deletion_enabled) {
retentionPeriod = payload.entities.general.dataRetentionPolicy.message_retention_cutoff;
}
const postIdsToKeep = [];
@@ -258,6 +122,12 @@ export function cleanUpState(action, keepCurrent = false) {
}
}
const nextSearch = {
...payload.entities.search,
results: searchResults,
flagged: flaggedPosts,
};
postIdsToKeep.forEach((postId) => {
const post = payload.entities.posts.posts[postId];
@@ -316,39 +186,12 @@ export function cleanUpState(action, keepCurrent = false) {
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.entities = {
...nextState.entities,
...nextEntities,
search: nextSearch,
};
nextState.errors = payload.errors;
return {
type: action.type,
payload: nextState,

View File

@@ -3,6 +3,8 @@
/* eslint-disable max-nested-callbacks */
import DeviceInfo from 'react-native-device-info';
import assert from 'assert';
import {ViewTypes} from 'app/constants';
@@ -55,6 +57,122 @@ describe('messageRetention', () => {
});
});
});
describe('should add build, version, and previousVersion to payload.app on persist/REHYDRATE', () => {
const next = (a) => a;
const store = {};
const build = 'build';
const version = 'version';
const previousBuild = 'previous-build';
const previousVersion = 'previous-version';
DeviceInfo.getBuildNumber = jest.fn().mockReturnValue('build');
DeviceInfo.getVersion = jest.fn().mockReturnValue('version');
const rehydrateAction = {
type: 'persist/REHYDRATE',
payload: {
app: {
build: previousBuild,
version: previousVersion,
},
},
};
const expectedPayloadApp = {
build,
version,
previousVersion,
};
const entities = {
channels: {},
posts: {},
};
const views = {
team: {
lastChannelForTeam: {},
},
};
test('when entities is missing', () => {
const action = {...rehydrateAction};
const nextAction = messageRetention(store)(next)(action);
expect(nextAction.payload.app).toStrictEqual(expectedPayloadApp);
});
test('when views is missing', () => {
const action = {
...rehydrateAction,
payload: {
...rehydrateAction.payload,
entities,
},
};
const nextAction = messageRetention(store)(next)(action);
expect(nextAction.payload.app).toStrictEqual(expectedPayloadApp);
});
test('when previousVersion !== version', () => {
const action = {
...rehydrateAction,
payload: {
...rehydrateAction.payload,
entities,
views,
},
};
expect(action.payload.app.version).not.toEqual(DeviceInfo.getVersion());
const nextAction = messageRetention(store)(next)(action);
expect(nextAction.payload.app).toStrictEqual(expectedPayloadApp);
});
test('when previousBuild !== build', () => {
const action = {
...rehydrateAction,
payload: {
...rehydrateAction.payload,
app: {
...rehydrateAction.payload.app,
version: DeviceInfo.getVersion(),
},
entities,
views,
},
};
expect(action.payload.app.version).toEqual(DeviceInfo.getVersion());
expect(action.payload.app.build).not.toEqual(DeviceInfo.getBuildNumber());
const nextAction = messageRetention(store)(next)(action);
expect(nextAction.payload.app).toStrictEqual({
...expectedPayloadApp,
previousVersion: DeviceInfo.getVersion(),
});
});
test('when cleanUpState', () => {
const action = {
...rehydrateAction,
payload: {
...rehydrateAction.payload,
app: {
...rehydrateAction.payload.app,
version: DeviceInfo.getVersion(),
build: DeviceInfo.getBuildNumber(),
},
entities,
views,
},
};
expect(action.payload.app.version).toEqual(DeviceInfo.getVersion());
expect(action.payload.app.build).toEqual(DeviceInfo.getBuildNumber());
const nextAction = messageRetention(store)(next)(action);
expect(nextAction.payload.app).toStrictEqual({
...expectedPayloadApp,
previousVersion: DeviceInfo.getVersion(),
});
});
});
});
describe('cleanUpState', () => {

View File

@@ -67,6 +67,7 @@ export function waitForHydration(store, callback) {
}
export function getStateForReset(initialState, currentState) {
const {app} = currentState;
const {currentUserId} = currentState.entities.users;
const currentUserProfile = currentState.entities.users.profiles[currentUserId];
const {currentTeamId} = currentState.entities.teams;
@@ -79,8 +80,8 @@ export function getStateForReset(initialState, currentState) {
const resetState = merge(initialState, {
app: {
build: DeviceInfo.getBuildNumber(),
version: DeviceInfo.getVersion(),
...app,
previousVersion: DeviceInfo.getVersion(),
},
entities: {
users: {

View File

@@ -1,6 +1,8 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import DeviceInfo from 'react-native-device-info';
import initialState from 'app/initial_state';
import {getStateForReset} from 'app/store/utils';
@@ -20,6 +22,11 @@ describe('getStateForReset', () => {
const otherUserId = 'other-user-id';
const currentTeamId = 'current-team-id';
const currentState = {
app: {
build: DeviceInfo.getBuildNumber(),
version: DeviceInfo.getVersion(),
previousVersion: 'previousVersion',
},
entities: {
users: {
currentUserId,
@@ -76,4 +83,10 @@ describe('getStateForReset', () => {
expect(themeKeys.length).not.toEqual(0);
expect(themeKeys.length).toEqual(preferenceKeys.length);
});
it('should set previous version as current', () => {
const resetState = getStateForReset(initialState, currentState);
const {app} = resetState;
expect(app.previousVersion).toStrictEqual(currentState.app.version);
});
});

View File

@@ -6,9 +6,6 @@ import ReactNativeHapticFeedback from 'react-native-haptic-feedback';
import {Posts} from 'mattermost-redux/constants';
import {logout} from 'app/actions/views/user';
import EphemeralStore from 'app/store/ephemeral_store';
const INVALID_VERSIONS = ['1.29.0'];
export function fromAutoResponder(post) {
@@ -88,12 +85,9 @@ export function isPendingPost(postId, userId) {
return postId.startsWith(userId);
}
export function validatePreviousVersion(store) {
const version = EphemeralStore.prevAppVersion;
if (!version || INVALID_VERSIONS.includes(version)) {
console.log('Previous version is no longer valid'); //eslint-disable-line no-console
store.dispatch(logout());
export function validatePreviousVersion(previousVersion) {
if (!previousVersion || INVALID_VERSIONS.includes(previousVersion)) {
console.log(`Previous version "${previousVersion}" is no longer valid`); //eslint-disable-line no-console
return false;
}

View File

@@ -1118,7 +1118,7 @@
CODE_SIGN_ENTITLEMENTS = Mattermost/Mattermost.entitlements;
CODE_SIGN_IDENTITY = "iPhone Developer";
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
CURRENT_PROJECT_VERSION = 285;
CURRENT_PROJECT_VERSION = 287;
DEAD_CODE_STRIPPING = NO;
DEVELOPMENT_TEAM = UQ8HT4Q2XM;
ENABLE_BITCODE = NO;
@@ -1156,7 +1156,7 @@
CODE_SIGN_ENTITLEMENTS = Mattermost/Mattermost.entitlements;
CODE_SIGN_IDENTITY = "iPhone Developer";
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
CURRENT_PROJECT_VERSION = 285;
CURRENT_PROJECT_VERSION = 287;
DEAD_CODE_STRIPPING = NO;
DEVELOPMENT_TEAM = UQ8HT4Q2XM;
ENABLE_BITCODE = NO;

View File

@@ -19,7 +19,7 @@
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>1.30.0</string>
<string>1.30.1</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleURLTypes</key>
@@ -34,7 +34,7 @@
</dict>
</array>
<key>CFBundleVersion</key>
<string>285</string>
<string>287</string>
<key>ITSAppUsesNonExemptEncryption</key>
<false/>
<key>LSRequiresIPhoneOS</key>

View File

@@ -17,9 +17,9 @@
<key>CFBundlePackageType</key>
<string>XPC!</string>
<key>CFBundleShortVersionString</key>
<string>1.30.0</string>
<string>1.30.1</string>
<key>CFBundleVersion</key>
<string>285</string>
<string>287</string>
<key>NSAppTransportSecurity</key>
<dict>
<key>NSAllowsArbitraryLoads</key>

View File

@@ -17,9 +17,9 @@
<key>CFBundlePackageType</key>
<string>XPC!</string>
<key>CFBundleShortVersionString</key>
<string>1.30.0</string>
<string>1.30.1</string>
<key>CFBundleVersion</key>
<string>285</string>
<string>287</string>
<key>NSExtension</key>
<dict>
<key>NSExtensionPointIdentifier</key>

View File

@@ -212,6 +212,8 @@ PODS:
- React
- react-native-image-picker (2.3.1):
- React
- react-native-local-auth (1.6.0):
- React
- react-native-netinfo (4.4.0):
- React
- react-native-notifications (2.0.6):
@@ -338,6 +340,7 @@ DEPENDENCIES:
- react-native-document-picker (from `../node_modules/react-native-document-picker`)
- react-native-hw-keyboard-event (from `../node_modules/react-native-hw-keyboard-event`)
- react-native-image-picker (from `../node_modules/react-native-image-picker`)
- react-native-local-auth (from `../node_modules/react-native-local-auth`)
- "react-native-netinfo (from `../node_modules/@react-native-community/netinfo`)"
- react-native-notifications (from `../node_modules/react-native-notifications`)
- react-native-passcode-status (from `../node_modules/react-native-passcode-status`)
@@ -434,6 +437,8 @@ EXTERNAL SOURCES:
:path: "../node_modules/react-native-hw-keyboard-event"
react-native-image-picker:
:path: "../node_modules/react-native-image-picker"
react-native-local-auth:
:path: "../node_modules/react-native-local-auth"
react-native-netinfo:
:path: "../node_modules/@react-native-community/netinfo"
react-native-notifications:
@@ -526,6 +531,7 @@ SPEC CHECKSUMS:
react-native-document-picker: 0573c02d742d4bef38a5d16b5f039754cfa69888
react-native-hw-keyboard-event: b517cefb8d5c659a38049c582de85ff43337dc53
react-native-image-picker: 668e72d0277dc8c12ae90e835507c1eddd2e4f85
react-native-local-auth: 359af242caa1e5c501ac9dfe33b1e238ad8f08c6
react-native-netinfo: 892a5130be97ff8bb69c523739c424a2ffc296d1
react-native-notifications: d5cb54ef8bf3004dcb56c887650dea08ecbddee7
react-native-passcode-status: 88c4f6e074328bc278bd127646b6c694ad5a530a

2
package-lock.json generated
View File

@@ -1,6 +1,6 @@
{
"name": "mattermost-mobile",
"version": "1.30.0",
"version": "1.30.1",
"lockfileVersion": 1,
"requires": true,
"dependencies": {

View File

@@ -1,6 +1,6 @@
{
"name": "mattermost-mobile",
"version": "1.30.0",
"version": "1.30.1",
"description": "Mattermost Mobile with React Native",
"repository": "git@github.com:mattermost/mattermost-mobile.git",
"author": "Mattermost, Inc.",

View File

@@ -1,8 +1,16 @@
diff --git a/node_modules/react-native-image-picker/android/src/main/java/com/imagepicker/ImagePickerModule.java b/node_modules/react-native-image-picker/android/src/main/java/com/imagepicker/ImagePickerModule.java
index 48fb5c1..5085663 100644
index 48fb5c1..3def244 100644
--- a/node_modules/react-native-image-picker/android/src/main/java/com/imagepicker/ImagePickerModule.java
+++ b/node_modules/react-native-image-picker/android/src/main/java/com/imagepicker/ImagePickerModule.java
@@ -49,6 +49,7 @@ import java.io.InputStream;
@@ -3,6 +3,7 @@ package com.imagepicker;
import android.Manifest;
import android.app.Activity;
import android.content.ActivityNotFoundException;
+import android.content.ContentResolver;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
@@ -49,6 +50,7 @@ import java.io.InputStream;
import java.io.OutputStream;
import java.lang.ref.WeakReference;
import java.util.List;
@@ -10,7 +18,7 @@ index 48fb5c1..5085663 100644
import com.facebook.react.modules.core.PermissionListener;
import com.facebook.react.modules.core.PermissionAwareActivity;
@@ -69,6 +70,7 @@ public class ImagePickerModule extends ReactContextBaseJavaModule
@@ -69,6 +71,7 @@ public class ImagePickerModule extends ReactContextBaseJavaModule
public static final int REQUEST_LAUNCH_IMAGE_LIBRARY = 13002;
public static final int REQUEST_LAUNCH_VIDEO_LIBRARY = 13003;
public static final int REQUEST_LAUNCH_VIDEO_CAPTURE = 13004;
@@ -18,7 +26,7 @@ index 48fb5c1..5085663 100644
public static final int REQUEST_PERMISSIONS_FOR_CAMERA = 14001;
public static final int REQUEST_PERMISSIONS_FOR_LIBRARY = 14002;
@@ -266,26 +268,24 @@ public class ImagePickerModule extends ReactContextBaseJavaModule
@@ -266,26 +269,24 @@ public class ImagePickerModule extends ReactContextBaseJavaModule
cameraIntent.putExtra(MediaStore.EXTRA_DURATION_LIMIT, videoDurationLimit);
}
}
@@ -58,35 +66,159 @@ index 48fb5c1..5085663 100644
}
if (cameraIntent.resolveActivity(reactContext.getPackageManager()) == null)
@@ -444,14 +444,20 @@ public class ImagePickerModule extends ReactContextBaseJavaModule
callback = null;
return;
@@ -350,16 +351,19 @@ public class ImagePickerModule extends ReactContextBaseJavaModule
libraryIntent = new Intent(Intent.ACTION_PICK);
libraryIntent.setType("video/*");
}
+ else if (pickBoth) {
+ libraryIntent = new Intent(Intent.ACTION_GET_CONTENT);
+ libraryIntent.addCategory(Intent.CATEGORY_OPENABLE);
+ libraryIntent.setType("image/*");
+ libraryIntent.putExtra(Intent.EXTRA_MIME_TYPES, new String[] {"image/*", "video/*"});
+ requestCode = REQUEST_LAUNCH_MIXED_CAPTURE;
+ }
else
{
requestCode = REQUEST_LAUNCH_IMAGE_LIBRARY;
libraryIntent = new Intent(Intent.ACTION_PICK,
MediaStore.Images.Media.EXTERNAL_CONTENT_URI);
-
- if (pickBoth)
- {
- libraryIntent.setType("image/* video/*");
- }
+ libraryIntent.setType("image/*");
}
+ case REQUEST_LAUNCH_MIXED_CAPTURE:
case REQUEST_LAUNCH_VIDEO_CAPTURE:
if (libraryIntent.resolveActivity(reactContext.getPackageManager()) == null)
@@ -385,75 +389,47 @@ public class ImagePickerModule extends ReactContextBaseJavaModule
}
}
- @Override
- public void onActivityResult(Activity activity, int requestCode, int resultCode, Intent data) {
- //robustness code
- if (passResult(requestCode))
- {
- return;
- }
-
- responseHelper.cleanResponse();
-
- // user cancel
- if (resultCode != Activity.RESULT_OK)
- {
- removeUselessFiles(requestCode, imageConfig);
- responseHelper.invokeCancel(callback);
- callback = null;
- return;
- }
+ protected String getMimeType(Activity activity, Uri uri) {
+ String mimeType = null;
+ if (uri.getScheme().equals(ContentResolver.SCHEME_CONTENT)) {
+ ContentResolver cr = activity.getApplicationContext().getContentResolver();
+ mimeType = cr.getType(uri);
+ } else {
+ String fileExtension = MimeTypeMap.getFileExtensionFromUrl(uri
+ .toString());
+ mimeType = MimeTypeMap.getSingleton().getMimeTypeFromExtension(
+ fileExtension.toLowerCase());
+ }
+ return mimeType;
+ }
- Uri uri = null;
- switch (requestCode)
+ protected void extractImageFromResult(Activity activity, Uri uri, int requestCode) {
+ String realPath = getRealPathFromURI(uri);
+ String mime = getMimeType(activity, uri);
+ final boolean isUrl = !TextUtils.isEmpty(realPath) &&
+ Patterns.WEB_URL.matcher(realPath).matches();
+ if (realPath == null || isUrl)
{
- case REQUEST_LAUNCH_IMAGE_CAPTURE:
- uri = cameraCaptureURI;
- break;
-
- case REQUEST_LAUNCH_IMAGE_LIBRARY:
- uri = data.getData();
- String realPath = getRealPathFromURI(uri);
- final boolean isUrl = !TextUtils.isEmpty(realPath) &&
- Patterns.WEB_URL.matcher(realPath).matches();
- if (realPath == null || isUrl)
- {
- try
- {
- File file = createFileFromURI(uri);
- realPath = file.getAbsolutePath();
- uri = Uri.fromFile(file);
- }
- catch (Exception e)
- {
- // image not in cache
- responseHelper.putString("error", "Could not read photo");
- responseHelper.putString("uri", uri.toString());
- responseHelper.invokeResponse(callback);
- callback = null;
- return;
- }
- }
- imageConfig = imageConfig.withOriginalFile(new File(realPath));
- break;
-
- case REQUEST_LAUNCH_VIDEO_LIBRARY:
- responseHelper.putString("uri", data.getData().toString());
- responseHelper.putString("path", getRealPathFromURI(data.getData()));
- responseHelper.invokeResponse(callback);
- callback = null;
- return;
-
- case REQUEST_LAUNCH_VIDEO_CAPTURE:
- final String path = getRealPathFromURI(data.getData());
- responseHelper.putString("uri", data.getData().toString());
- responseHelper.putString("path", path);
- fileScan(reactContext, path);
- responseHelper.invokeResponse(callback);
- callback = null;
- return;
+ if (data == null || data.getData() == null) {
+ uri = cameraCaptureURI;
+ break;
+ } else {
+ final String path = getRealPathFromURI(data.getData());
+ responseHelper.putString("uri", data.getData().toString());
+ responseHelper.putString("path", path);
+ fileScan(reactContext, path);
+ responseHelper.invokeResponse(callback);
+ callback = null;
+ return;
+ }
+ try
+ {
+ File file = createFileFromURI(uri);
+ realPath = file.getAbsolutePath();
+ uri = Uri.fromFile(file);
+ }
+ catch (Exception e)
+ {
+ // image not in cache
+ responseHelper.putString("error", "Could not read photo");
+ responseHelper.putString("uri", uri.toString());
responseHelper.invokeResponse(callback);
callback = null;
+ this.options = null;
return;
+ }
}
+ imageConfig = imageConfig.withOriginalFile(new File(realPath));
+
final ReadExifResult result = readExifInterface(responseHelper, imageConfig);
@@ -481,6 +487,13 @@ public class ImagePickerModule extends ReactContextBaseJavaModule
if (result.error != null)
@@ -461,6 +437,7 @@ public class ImagePickerModule extends ReactContextBaseJavaModule
removeUselessFiles(requestCode, imageConfig);
responseHelper.invokeError(callback, result.error.getMessage());
callback = null;
+ this.options = null;
return;
}
@@ -472,7 +449,7 @@ public class ImagePickerModule extends ReactContextBaseJavaModule
updatedResultResponse(uri, imageConfig.original.getAbsolutePath());
// don't create a new file if contraint are respected
- if (imageConfig.useOriginal(initialWidth, initialHeight, result.currentRotation))
+ if (imageConfig.useOriginal(initialWidth, initialHeight, result.currentRotation) || mime.equals("image/gif"))
{
responseHelper.putInt("width", initialWidth);
responseHelper.putInt("height", initialHeight);
@@ -481,6 +458,14 @@ public class ImagePickerModule extends ReactContextBaseJavaModule
else
{
imageConfig = getResizedImage(reactContext, this.options, imageConfig, initialWidth, initialHeight, requestCode);
@@ -94,13 +226,78 @@ index 48fb5c1..5085663 100644
+ {
+ responseHelper.invokeError(callback, "Could not read image");
+ callback = null;
+ this.options = null;
+ return;
+ }
+
if (imageConfig.resized == null)
{
removeUselessFiles(requestCode, imageConfig);
@@ -551,7 +564,8 @@ public class ImagePickerModule extends ReactContextBaseJavaModule
@@ -523,6 +508,63 @@ public class ImagePickerModule extends ReactContextBaseJavaModule
this.options = null;
}
+ protected void extractVideoFromResult(Uri uri) {
+ responseHelper.putString("uri", uri.toString());
+ responseHelper.putString("path", getRealPathFromURI(uri));
+ responseHelper.invokeResponse(callback);
+ callback = null;
+ this.options = null;
+ }
+
+ @Override
+ public void onActivityResult(Activity activity, int requestCode, int resultCode, Intent data) {
+ //robustness code
+ if (passResult(requestCode))
+ {
+ return;
+ }
+
+ responseHelper.cleanResponse();
+
+ // user cancel
+ if (resultCode != Activity.RESULT_OK)
+ {
+ removeUselessFiles(requestCode, imageConfig);
+ responseHelper.invokeCancel(callback);
+ callback = null;
+ return;
+ }
+
+ switch (requestCode)
+ {
+ case REQUEST_LAUNCH_IMAGE_CAPTURE:
+ extractImageFromResult(activity, cameraCaptureURI, requestCode);
+ break;
+
+ case REQUEST_LAUNCH_IMAGE_LIBRARY:
+ extractImageFromResult(activity, data.getData(), requestCode);
+ break;
+
+ case REQUEST_LAUNCH_VIDEO_LIBRARY:
+ extractVideoFromResult(data.getData());
+ break;
+
+ case REQUEST_LAUNCH_MIXED_CAPTURE:
+ case REQUEST_LAUNCH_VIDEO_CAPTURE:
+ if (data == null || data.getData() == null) {
+ extractImageFromResult(activity, cameraCaptureURI, requestCode);
+ } else {
+ Uri selectedMediaUri = data.getData();
+ if (selectedMediaUri.toString().contains("image")) {
+ extractImageFromResult(activity, selectedMediaUri, requestCode);
+ } else {
+ extractVideoFromResult(selectedMediaUri);
+ }
+ }
+ break;
+ }
+ }
+
public void invokeCustomButton(@NonNull final String action)
{
responseHelper.invokeCustomButton(this.callback, action);
@@ -551,7 +593,8 @@ public class ImagePickerModule extends ReactContextBaseJavaModule
{
return callback == null || (cameraCaptureURI == null && requestCode == REQUEST_LAUNCH_IMAGE_CAPTURE)
|| (requestCode != REQUEST_LAUNCH_IMAGE_CAPTURE && requestCode != REQUEST_LAUNCH_IMAGE_LIBRARY
@@ -110,7 +307,7 @@ index 48fb5c1..5085663 100644
}
private void updatedResultResponse(@Nullable final Uri uri,
@@ -571,22 +585,23 @@ public class ImagePickerModule extends ReactContextBaseJavaModule
@@ -571,22 +614,23 @@ public class ImagePickerModule extends ReactContextBaseJavaModule
@NonNull final Callback callback,
@NonNull final int requestCode)
{
@@ -143,7 +340,7 @@ index 48fb5c1..5085663 100644
if (!permissionsGranted)
{
final Boolean dontAskAgain = ActivityCompat.shouldShowRequestPermissionRationale(activity, Manifest.permission.WRITE_EXTERNAL_STORAGE) && ActivityCompat.shouldShowRequestPermissionRationale(activity, Manifest.permission.CAMERA);
@@ -787,4 +802,22 @@ public class ImagePickerModule extends ReactContextBaseJavaModule
@@ -787,4 +831,22 @@ public class ImagePickerModule extends ReactContextBaseJavaModule
videoDurationLimit = options.getInt("durationLimit");
}
}

View File

@@ -170,3 +170,29 @@ index 38b78f1..a47afea 100644
} catch (Exception e) {
authPromise.reject(E_FAILED_TO_SHOW_AUTH, e);
authPromise = null;
diff --git a/node_modules/react-native-local-auth/react-native-local-auth.podspec b/node_modules/react-native-local-auth/react-native-local-auth.podspec
new file mode 100644
index 0000000..5f9a6b4
--- /dev/null
+++ b/node_modules/react-native-local-auth/react-native-local-auth.podspec
@@ -0,0 +1,20 @@
+require "json"
+package = JSON.parse(File.read(File.join(__dir__, '/package.json')))
+
+Pod::Spec.new do |s|
+ s.name = package['name']
+ s.version = package['version']
+ s.summary = package['description']
+ s.description = package['description']
+ s.homepage = package['homepage']
+ s.license = package['license']
+ s.author = package['author']
+ s.source = { :git => 'https://github.com/tradle/react-native-local-auth.git' }
+
+ s.platform = :ios, '9.0'
+ s.ios.deployment_target = '9.0'
+
+ s.source_files = "*.{h,m}"
+
+ s.dependency 'React'
+end