forked from Ivasoft/mattermost-mobile
Compare commits
75 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c9e5da40ec | ||
|
|
51050e31a9 | ||
|
|
9c00f51058 | ||
|
|
bfd50568cc | ||
|
|
5bef5c2da8 | ||
|
|
b064312e70 | ||
|
|
e82db7840f | ||
|
|
c4d8308802 | ||
|
|
b8b7d54185 | ||
|
|
8416525487 | ||
|
|
d7a85fe2de | ||
|
|
c1957ac6a0 | ||
|
|
90dba40a92 | ||
|
|
f8efcf73b7 | ||
|
|
ff7548251b | ||
|
|
e03b72861c | ||
|
|
94aa1be435 | ||
|
|
7bf3020490 | ||
|
|
880cbd9f3d | ||
|
|
390c30af00 | ||
|
|
e3fd88b7fb | ||
|
|
3ed078f4ce | ||
|
|
e7042e4907 | ||
|
|
9ae69d9b43 | ||
|
|
eb314d714a | ||
|
|
cdd7a54ef4 | ||
|
|
41ddb5cc1a | ||
|
|
aceef20257 | ||
|
|
a5f8b9bdcc | ||
|
|
18b7a3c0a1 | ||
|
|
7b75868101 | ||
|
|
24bd57ad3f | ||
|
|
b931733695 | ||
|
|
088d375ff2 | ||
|
|
4b06636d9b | ||
|
|
05db1aaa71 | ||
|
|
e94dfb5389 | ||
|
|
58b95e7609 | ||
|
|
da911a2b34 | ||
|
|
d842d7881a | ||
|
|
98dc141ee3 | ||
|
|
8d0cb0663b | ||
|
|
612d284cbb | ||
|
|
be727fec9e | ||
|
|
d0f059e1f9 | ||
|
|
df74521ff3 | ||
|
|
eff961d109 | ||
|
|
59f3633d94 | ||
|
|
31bd391e56 | ||
|
|
810f73e3ad | ||
|
|
14421ba8e9 | ||
|
|
5c4405278b | ||
|
|
067a5481ff | ||
|
|
c267b8dd13 | ||
|
|
ce325a4ab1 | ||
|
|
1f8e853e41 | ||
|
|
8887319324 | ||
|
|
e214039cee | ||
|
|
0a93ec134c | ||
|
|
a0b021d21d | ||
|
|
75aedb8aa1 | ||
|
|
60030defb8 | ||
|
|
50cc6f827e | ||
|
|
ac11b7fec3 | ||
|
|
c9575b464d | ||
|
|
a4284666a3 | ||
|
|
4d5422e98b | ||
|
|
a3783b1bf5 | ||
|
|
c31ff56149 | ||
|
|
93498a3ab5 | ||
|
|
6d10915aad | ||
|
|
79653ad814 | ||
|
|
13bb83c11c | ||
|
|
3f6e706fa1 | ||
|
|
5492cd5e46 |
99
NOTICE.txt
99
NOTICE.txt
@@ -312,6 +312,37 @@ SOFTWARE.
|
||||
|
||||
---
|
||||
|
||||
## core-js
|
||||
|
||||
This product contains 'core-js' by Denis Pushkarev.
|
||||
|
||||
Modular standard library for JavaScript.
|
||||
|
||||
* HOMEPAGE:
|
||||
* https://github.com/zloirock/core-js
|
||||
|
||||
* LICENSE: Copyright (c) 2014-2019 Denis Pushkarev
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
|
||||
---
|
||||
|
||||
## deep-equal
|
||||
|
||||
This product contains 'deep-equal' by James Halliday.
|
||||
@@ -344,6 +375,39 @@ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
---
|
||||
|
||||
## deepmerge
|
||||
|
||||
This product contains 'deepmerge' by Josh Duff.
|
||||
|
||||
A library for deep (recursive) merging of Javascript objects.
|
||||
|
||||
* HOMEPAGE:
|
||||
* https://github.com/TehShrike/deepmerge
|
||||
|
||||
* LICENSE: The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2012 James Halliday, Josh Duff, and other contributors
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
|
||||
---
|
||||
|
||||
## emoji-regex
|
||||
|
||||
This product contains 'emoji-regex' by Mathias Bynens.
|
||||
@@ -1216,41 +1280,6 @@ SOFTWARE.
|
||||
|
||||
---
|
||||
|
||||
## react-native-bottom-sheet
|
||||
|
||||
This product contains 'react-native-bottom-sheet' by WhatAKitty.
|
||||
|
||||
React Native Bottom sheet for android
|
||||
|
||||
* HOMEPAGE:
|
||||
* https://github.com/WhatAKitty/react-native-bottom-sheet#readme
|
||||
|
||||
* LICENSE: MIT
|
||||
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2016 WhatAKitty
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
|
||||
---
|
||||
|
||||
## react-native-button
|
||||
|
||||
This product contains 'react-native-button' by James Ide.
|
||||
|
||||
@@ -123,8 +123,8 @@ android {
|
||||
minSdkVersion rootProject.ext.minSdkVersion
|
||||
targetSdkVersion rootProject.ext.targetSdkVersion
|
||||
missingDimensionStrategy "RNN.reactNativeVersion", "reactNative57_5"
|
||||
versionCode 208
|
||||
versionName "1.21.0"
|
||||
versionCode 221
|
||||
versionName "1.22.0"
|
||||
multiDexEnabled = true
|
||||
ndk {
|
||||
abiFilters 'armeabi-v7a','arm64-v8a','x86','x86_64'
|
||||
|
||||
@@ -32,7 +32,7 @@ import com.reactnativedocumentpicker.DocumentPicker;
|
||||
import com.oblador.keychain.KeychainModule;
|
||||
import com.reactnativecommunity.asyncstorage.AsyncStorageModule;
|
||||
import com.reactnativecommunity.netinfo.NetInfoModule;
|
||||
import com.reactnativecommunity.webview.RNCWebViewModule;
|
||||
import com.reactnativecommunity.webview.RNCWebViewPackage;
|
||||
|
||||
import com.brentvatne.react.ReactVideoPackage;
|
||||
import com.BV.LinearGradient.LinearGradientPackage;
|
||||
@@ -151,8 +151,6 @@ public class MainApplication extends NavigationApplication implements INotificat
|
||||
return new AsyncStorageModule(reactContext);
|
||||
case NetInfoModule.NAME:
|
||||
return new NetInfoModule(reactContext);
|
||||
case RNCWebViewModule.MODULE_NAME:
|
||||
return new RNCWebViewModule(reactContext);
|
||||
default:
|
||||
throw new IllegalArgumentException("Could not find module " + name);
|
||||
}
|
||||
@@ -186,12 +184,12 @@ public class MainApplication extends NavigationApplication implements INotificat
|
||||
map.put("RNKeychainManager", new ReactModuleInfo("RNKeychainManager", "com.oblador.keychain.KeychainModule", false, false, true, false, false));
|
||||
map.put(AsyncStorageModule.NAME, new ReactModuleInfo(AsyncStorageModule.NAME, "com.reactnativecommunity.asyncstorage.AsyncStorageModule", false, false, false, false, false));
|
||||
map.put(NetInfoModule.NAME, new ReactModuleInfo(NetInfoModule.NAME, "com.reactnativecommunity.netinfo.NetInfoModule", false, false, false, false, false));
|
||||
map.put(RNCWebViewModule.MODULE_NAME, new ReactModuleInfo(RNCWebViewModule.MODULE_NAME, "com.reactnativecommunity.webview.RNCWebViewModule", false, false, false, false, false));
|
||||
return map;
|
||||
}
|
||||
};
|
||||
}
|
||||
},
|
||||
new RNCWebViewPackage(),
|
||||
new SvgPackage(),
|
||||
new LinearGradientPackage(),
|
||||
new ReactVideoPackage(),
|
||||
|
||||
@@ -133,7 +133,7 @@ export function resetToTeams(name, title, passProps = {}, options = {}) {
|
||||
export function goToScreen(name, title, passProps = {}, options = {}) {
|
||||
return (dispatch, getState) => {
|
||||
const state = getState();
|
||||
const componentId = EphemeralStore.getTopComponentId();
|
||||
const componentId = EphemeralStore.getNavigationTopComponentId();
|
||||
const theme = getTheme(state);
|
||||
const defaultOptions = {
|
||||
layout: {
|
||||
@@ -166,17 +166,20 @@ export function goToScreen(name, title, passProps = {}, options = {}) {
|
||||
};
|
||||
}
|
||||
|
||||
export function popTopScreen() {
|
||||
export function popTopScreen(screenId) {
|
||||
return () => {
|
||||
const componentId = EphemeralStore.getTopComponentId();
|
||||
|
||||
Navigation.pop(componentId);
|
||||
if (screenId) {
|
||||
Navigation.pop(screenId);
|
||||
} else {
|
||||
const componentId = EphemeralStore.getNavigationTopComponentId();
|
||||
Navigation.pop(componentId);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export function popToRoot() {
|
||||
return () => {
|
||||
const componentId = EphemeralStore.getTopComponentId();
|
||||
const componentId = EphemeralStore.getNavigationTopComponentId();
|
||||
|
||||
Navigation.popToRoot(componentId).catch(() => {
|
||||
// RNN returns a promise rejection if there are no screens
|
||||
@@ -286,9 +289,13 @@ export function showSearchModal(initialValue = '') {
|
||||
|
||||
export function dismissModal(options = {}) {
|
||||
return () => {
|
||||
const componentId = EphemeralStore.getTopComponentId();
|
||||
const componentId = EphemeralStore.getNavigationTopComponentId();
|
||||
|
||||
Navigation.dismissModal(componentId, options);
|
||||
Navigation.dismissModal(componentId, options).catch(() => {
|
||||
// RNN returns a promise rejection if there is no modal to
|
||||
// dismiss. We'll do nothing in this case but we will catch
|
||||
// the rejection here so that the caller doesn't have to.
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
@@ -304,7 +311,7 @@ export function dismissAllModals(options = {}) {
|
||||
|
||||
export function peek(name, passProps = {}, options = {}) {
|
||||
return () => {
|
||||
const componentId = EphemeralStore.getTopComponentId();
|
||||
const componentId = EphemeralStore.getNavigationTopComponentId();
|
||||
const defaultOptions = {
|
||||
preview: {
|
||||
commit: false,
|
||||
@@ -359,3 +366,29 @@ export function dismissOverlay(componentId) {
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
export function applyTheme(componentId, skipBackButtonStyle = false) {
|
||||
return (dispatch, getState) => {
|
||||
const theme = getTheme(getState());
|
||||
|
||||
let backButton = {
|
||||
color: theme.sidebarHeaderTextColor,
|
||||
};
|
||||
|
||||
if (skipBackButtonStyle && Platform.OS === 'android') {
|
||||
backButton = null;
|
||||
}
|
||||
|
||||
Navigation.mergeOptions(componentId, {
|
||||
topBar: {
|
||||
backButton,
|
||||
background: {
|
||||
color: theme.sidebarHeaderBg,
|
||||
},
|
||||
title: {
|
||||
color: theme.sidebarHeaderTextColor,
|
||||
},
|
||||
},
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
@@ -198,7 +198,7 @@ export function loadPostsIfNecessaryWithRetry(channelId) {
|
||||
});
|
||||
}
|
||||
} else {
|
||||
const {lastConnectAt} = state.websocket;
|
||||
const lastConnectAt = state.websocket?.lastConnectAt || 0;
|
||||
const lastGetPosts = state.views.channel.lastGetPosts[channelId];
|
||||
|
||||
let since;
|
||||
|
||||
@@ -4,7 +4,15 @@
|
||||
import configureStore from 'redux-mock-store';
|
||||
import thunk from 'redux-thunk';
|
||||
|
||||
import {handleSelectChannelByName} from 'app/actions/views/channel';
|
||||
import initialState from 'app/initial_state';
|
||||
import testHelper from 'test/test_helper';
|
||||
|
||||
import {
|
||||
handleSelectChannelByName,
|
||||
loadPostsIfNecessaryWithRetry,
|
||||
} from 'app/actions/views/channel';
|
||||
|
||||
import postReducer from 'mattermost-redux/reducers/entities/posts';
|
||||
|
||||
jest.mock('mattermost-redux/selectors/entities/channels', () => ({
|
||||
getChannel: () => ({data: 'received-channel-id'}),
|
||||
@@ -19,6 +27,9 @@ describe('Actions.Views.Channel', () => {
|
||||
|
||||
const MOCK_SELECT_CHANNEL_TYPE = 'MOCK_SELECT_CHANNEL_TYPE';
|
||||
const MOCK_RECEIVE_CHANNEL_TYPE = 'MOCK_RECEIVE_CHANNEL_TYPE';
|
||||
const MOCK_RECEIVED_POSTS = 'RECEIVED_POSTS';
|
||||
const MOCK_RECEIVED_POSTS_IN_CHANNEL = 'RECEIVED_POSTS_IN_CHANNEL';
|
||||
const MOCK_RECEIVED_POSTS_SINCE = 'MOCK_RECEIVED_POSTS_SINCE';
|
||||
|
||||
const actions = require('mattermost-redux/actions/channels');
|
||||
actions.getChannelByNameAndTeamName = jest.fn((teamName) => {
|
||||
@@ -38,14 +49,51 @@ describe('Actions.Views.Channel', () => {
|
||||
type: MOCK_SELECT_CHANNEL_TYPE,
|
||||
data: 'selected-channel-id',
|
||||
});
|
||||
const postActions = require('mattermost-redux/actions/posts');
|
||||
postActions.getPostsSince = jest.fn(() => {
|
||||
return {
|
||||
type: MOCK_RECEIVED_POSTS_SINCE,
|
||||
data: {
|
||||
order: [],
|
||||
posts: {},
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
postActions.getPosts = jest.fn((channelId) => {
|
||||
const order = [];
|
||||
const posts = {};
|
||||
|
||||
for (let i = 0; i < 60; i++) {
|
||||
const p = testHelper.fakePost(channelId);
|
||||
order.push(p.id);
|
||||
posts[p.id] = p;
|
||||
}
|
||||
|
||||
return {
|
||||
type: MOCK_RECEIVED_POSTS,
|
||||
data: {
|
||||
order,
|
||||
posts,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
const postUtils = require('mattermost-redux/utils/post_utils');
|
||||
postUtils.getLastCreateAt = jest.fn((array) => {
|
||||
return array[0].create_at;
|
||||
});
|
||||
|
||||
let nextPostState = {};
|
||||
const currentUserId = 'current-user-id';
|
||||
const currentChannelId = 'channel-id';
|
||||
const currentChannelName = 'channel-name';
|
||||
const currentTeamId = 'current-team-id';
|
||||
const currentTeamName = 'current-team-name';
|
||||
const storeObj = {
|
||||
...initialState,
|
||||
entities: {
|
||||
...initialState.entities,
|
||||
users: {
|
||||
currentUserId,
|
||||
},
|
||||
@@ -93,4 +141,84 @@ describe('Actions.Views.Channel', () => {
|
||||
const storeBatchActions = storeActions.some(({type}) => type === 'BATCHING_REDUCER.BATCH');
|
||||
expect(storeBatchActions).toBe(false);
|
||||
});
|
||||
|
||||
test('loadPostsIfNecessaryWithRetry for the first time', async () => {
|
||||
store = mockStore(storeObj);
|
||||
|
||||
await store.dispatch(loadPostsIfNecessaryWithRetry(currentChannelId));
|
||||
expect(postActions.getPosts).toBeCalled();
|
||||
|
||||
const storeActions = store.getActions();
|
||||
const storeBatchActions = storeActions.filter(({type}) => type === 'BATCHING_REDUCER.BATCH');
|
||||
const receivedPosts = storeActions.find(({type}) => type === MOCK_RECEIVED_POSTS);
|
||||
const receivedPostsAtAction = storeBatchActions[0].payload.some((action) => action.type === 'RECEIVED_POSTS_FOR_CHANNEL_AT_TIME');
|
||||
|
||||
nextPostState = postReducer(store.getState().entities.posts, receivedPosts);
|
||||
nextPostState = postReducer(nextPostState, {
|
||||
type: MOCK_RECEIVED_POSTS_IN_CHANNEL,
|
||||
channelId: currentChannelId,
|
||||
data: receivedPosts.data,
|
||||
recent: true,
|
||||
});
|
||||
|
||||
expect(receivedPostsAtAction).toBe(true);
|
||||
});
|
||||
|
||||
test('loadPostsIfNecessaryWithRetry get posts since', async () => {
|
||||
store = mockStore({
|
||||
...storeObj,
|
||||
entities: {
|
||||
...storeObj.entities,
|
||||
posts: nextPostState,
|
||||
},
|
||||
views: {
|
||||
...storeObj.views,
|
||||
channel: {
|
||||
...storeObj.views.channel,
|
||||
lastGetPosts: {
|
||||
[currentChannelId]: Date.now(),
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
await store.dispatch(loadPostsIfNecessaryWithRetry(currentChannelId));
|
||||
const storeActions = store.getActions();
|
||||
const receivedPostsSince = storeActions.find(({type}) => type === MOCK_RECEIVED_POSTS_SINCE);
|
||||
|
||||
expect(postUtils.getLastCreateAt).toBeCalled();
|
||||
expect(postActions.getPostsSince).toHaveBeenCalledWith(currentChannelId, Object.values(store.getState().entities.posts.posts)[0].create_at);
|
||||
expect(receivedPostsSince).not.toBe(null);
|
||||
});
|
||||
|
||||
test('loadPostsIfNecessaryWithRetry get posts since the websocket reconnected', async () => {
|
||||
const time = Date.now();
|
||||
store = mockStore({
|
||||
...storeObj,
|
||||
entities: {
|
||||
...storeObj.entities,
|
||||
posts: nextPostState,
|
||||
},
|
||||
views: {
|
||||
...storeObj.views,
|
||||
channel: {
|
||||
...storeObj.views.channel,
|
||||
lastGetPosts: {
|
||||
[currentChannelId]: time,
|
||||
},
|
||||
},
|
||||
},
|
||||
websocket: {
|
||||
lastConnectAt: time + (1 * 60 * 1000),
|
||||
},
|
||||
});
|
||||
|
||||
await store.dispatch(loadPostsIfNecessaryWithRetry(currentChannelId));
|
||||
const storeActions = store.getActions();
|
||||
const receivedPostsSince = storeActions.find(({type}) => type === MOCK_RECEIVED_POSTS_SINCE);
|
||||
|
||||
expect(postUtils.getLastCreateAt).not.toBeCalled();
|
||||
expect(postActions.getPostsSince).toHaveBeenCalledWith(currentChannelId, store.getState().views.channel.lastGetPosts[currentChannelId]);
|
||||
expect(receivedPostsSince).not.toBe(null);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -16,6 +16,10 @@ export function handleServerUrlChanged(serverUrl) {
|
||||
};
|
||||
}
|
||||
|
||||
export function setServerUrl(serverUrl) {
|
||||
return {type: ViewTypes.SERVER_URL_CHANGED, serverUrl};
|
||||
}
|
||||
|
||||
export default {
|
||||
handleServerUrlChanged,
|
||||
};
|
||||
|
||||
@@ -7,6 +7,7 @@ exports[`ChannelLoader should match snapshot 1`] = `
|
||||
Array [
|
||||
Object {
|
||||
"flex": 1,
|
||||
"overflow": "hidden",
|
||||
},
|
||||
undefined,
|
||||
Object {
|
||||
|
||||
@@ -149,6 +149,7 @@ const getStyleSheet = makeStyleSheetFromTheme((theme) => {
|
||||
return {
|
||||
container: {
|
||||
flex: 1,
|
||||
overflow: 'hidden',
|
||||
},
|
||||
section: {
|
||||
backgroundColor: theme.centerChannelBg,
|
||||
|
||||
@@ -16,4 +16,4 @@ function mapDispatchToProps(dispatch) {
|
||||
};
|
||||
}
|
||||
|
||||
export default connect(null, mapDispatchToProps)(FileAttachmentDocument);
|
||||
export default connect(null, mapDispatchToProps, null, {forwardRef: true})(FileAttachmentDocument);
|
||||
|
||||
@@ -3,35 +3,33 @@
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {injectIntl, intlShape} from 'react-intl';
|
||||
import moment from 'moment-timezone';
|
||||
import {Text} from 'react-native';
|
||||
|
||||
class FormattedDate extends React.PureComponent {
|
||||
export default class FormattedDate extends React.PureComponent {
|
||||
static propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
value: PropTypes.any.isRequired,
|
||||
format: PropTypes.string,
|
||||
children: PropTypes.func,
|
||||
timeZone: PropTypes.string,
|
||||
value: PropTypes.any.isRequired,
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
format: 'ddd, MMM DD, YYYY',
|
||||
};
|
||||
|
||||
render() {
|
||||
const {
|
||||
intl,
|
||||
format,
|
||||
timeZone,
|
||||
value,
|
||||
children,
|
||||
...props
|
||||
} = this.props;
|
||||
|
||||
Reflect.deleteProperty(props, 'format');
|
||||
|
||||
const formattedDate = intl.formatDate(value, this.props);
|
||||
|
||||
if (typeof children === 'function') {
|
||||
return children(formattedDate);
|
||||
let formattedDate = moment(value).format(format);
|
||||
if (timeZone) {
|
||||
formattedDate = moment.tz(value, timeZone).format(format);
|
||||
}
|
||||
|
||||
return <Text {...props}>{formattedDate}</Text>;
|
||||
}
|
||||
}
|
||||
|
||||
export default injectIntl(FormattedDate);
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {intlShape} from 'react-intl';
|
||||
import {Text} from 'react-native';
|
||||
import moment from 'moment-timezone';
|
||||
|
||||
@@ -15,30 +14,19 @@ export default class FormattedTime extends React.PureComponent {
|
||||
hour12: PropTypes.bool,
|
||||
};
|
||||
|
||||
static contextTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
};
|
||||
|
||||
getFormattedTime = () => {
|
||||
const {intl} = this.context;
|
||||
|
||||
const {
|
||||
value,
|
||||
timeZone,
|
||||
hour12,
|
||||
} = this.props;
|
||||
|
||||
const format = hour12 ? 'hh:mm A' : 'HH:mm';
|
||||
if (timeZone) {
|
||||
const format = hour12 ? 'hh:mm A' : 'HH:mm';
|
||||
return moment.tz(value, timeZone).format(format);
|
||||
}
|
||||
|
||||
// If no timezone is defined fallback to the previous implementation
|
||||
return intl.formatDate(new Date(value), {
|
||||
hour: 'numeric',
|
||||
minute: 'numeric',
|
||||
hour12,
|
||||
});
|
||||
return moment(value).format(format);
|
||||
};
|
||||
|
||||
render() {
|
||||
|
||||
@@ -4,6 +4,8 @@
|
||||
import {bindActionCreators} from 'redux';
|
||||
import {connect} from 'react-redux';
|
||||
|
||||
import {submitInteractiveDialog} from 'mattermost-redux/actions/integrations';
|
||||
|
||||
import {showModal} from 'app/actions/navigation';
|
||||
|
||||
import InteractiveDialogController from './interactive_dialog_controller';
|
||||
@@ -11,7 +13,7 @@ import InteractiveDialogController from './interactive_dialog_controller';
|
||||
function mapStateToProps(state) {
|
||||
return {
|
||||
triggerId: state.entities.integrations.dialogTriggerId,
|
||||
dialog: state.entities.integrations.dialog || {},
|
||||
dialogData: state.entities.integrations.dialog || {},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -19,6 +21,7 @@ function mapDispatchToProps(dispatch) {
|
||||
return {
|
||||
actions: bindActionCreators({
|
||||
showModal,
|
||||
submitInteractiveDialog,
|
||||
}, dispatch),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -4,14 +4,17 @@
|
||||
import {PureComponent} from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import MaterialIcon from 'react-native-vector-icons/MaterialIcons';
|
||||
import {Alert} from 'react-native';
|
||||
import {intlShape} from 'react-intl';
|
||||
|
||||
export default class InteractiveDialogController extends PureComponent {
|
||||
static propTypes = {
|
||||
actions: PropTypes.shape({
|
||||
showModal: PropTypes.func.isRequired,
|
||||
submitInteractiveDialog: PropTypes.func.isRequired,
|
||||
}).isRequired,
|
||||
triggerId: PropTypes.string,
|
||||
dialog: PropTypes.object,
|
||||
dialogData: PropTypes.object,
|
||||
theme: PropTypes.object,
|
||||
};
|
||||
|
||||
@@ -23,14 +26,18 @@ export default class InteractiveDialogController extends PureComponent {
|
||||
});
|
||||
}
|
||||
|
||||
static contextTypes = {
|
||||
intl: intlShape,
|
||||
};
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
const {actions, triggerId} = this.props;
|
||||
const {triggerId} = this.props;
|
||||
if (!triggerId) {
|
||||
return;
|
||||
}
|
||||
|
||||
const dialogData = this.props.dialog || {};
|
||||
const prevDialogData = prevProps.dialog || {};
|
||||
const dialogData = this.props.dialogData || {};
|
||||
const prevDialogData = prevProps.dialogData || {};
|
||||
if (prevProps.triggerId === triggerId && dialogData.trigger_id === prevDialogData.trigger_id) {
|
||||
return;
|
||||
}
|
||||
@@ -43,9 +50,30 @@ export default class InteractiveDialogController extends PureComponent {
|
||||
return;
|
||||
}
|
||||
|
||||
const screen = 'InteractiveDialog';
|
||||
const title = dialogData.dialog.title;
|
||||
const passProps = {};
|
||||
if (dialogData.dialog.elements && dialogData.dialog.elements.length > 0) {
|
||||
this.showInteractiveDialogScreen(dialogData.dialog);
|
||||
} else {
|
||||
this.showAlertDialog(dialogData.dialog, dialogData.url);
|
||||
}
|
||||
}
|
||||
|
||||
showAlertDialog(dialog, url) {
|
||||
const {formatMessage} = this.context.intl;
|
||||
|
||||
Alert.alert(
|
||||
dialog.title,
|
||||
'',
|
||||
[{
|
||||
text: formatMessage({id: 'mobile.alert_dialog.alertCancel', defaultMessage: 'Cancel'}),
|
||||
onPress: () => this.handleCancel(dialog, url),
|
||||
}, {
|
||||
text: dialog.submit_label,
|
||||
onPress: () => this.props.actions.submitInteractiveDialog({...dialog, url}),
|
||||
}],
|
||||
);
|
||||
}
|
||||
|
||||
showInteractiveDialogScreen = (dialog) => {
|
||||
const options = {
|
||||
topBar: {
|
||||
leftButtons: [{
|
||||
@@ -55,12 +83,18 @@ export default class InteractiveDialogController extends PureComponent {
|
||||
rightButtons: [{
|
||||
id: 'submit-dialog',
|
||||
showAsAction: 'always',
|
||||
text: dialogData.dialog.submit_label,
|
||||
text: dialog.submit_label,
|
||||
}],
|
||||
},
|
||||
};
|
||||
|
||||
actions.showModal(screen, title, passProps, options);
|
||||
this.props.actions.showModal('InteractiveDialog', dialog.title, null, options);
|
||||
}
|
||||
|
||||
handleCancel = (dialog, url) => {
|
||||
if (dialog.notify_on_cancel) {
|
||||
this.props.actions.submitInteractiveDialog({...dialog, url, cancelled: true});
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
import {shallow} from 'enzyme';
|
||||
|
||||
import InteractiveDialogController from './interactive_dialog_controller';
|
||||
|
||||
jest.mock('react-intl');
|
||||
jest.mock('react-native-vector-icons/MaterialIcons', () => ({
|
||||
getImageSource: jest.fn().mockResolvedValue({}),
|
||||
}));
|
||||
|
||||
describe('InteractiveDialogController', () => {
|
||||
test('should open interactive dialog as alert or screen depending on with or without element', () => {
|
||||
let baseProps = getBaseProps('trigger_id_1');
|
||||
const wrapper = shallow(
|
||||
<InteractiveDialogController {...baseProps}/>,
|
||||
{context: {intl: {formatMessage: jest.fn()}}},
|
||||
);
|
||||
|
||||
const instance = wrapper.instance();
|
||||
instance.showAlertDialog = jest.fn();
|
||||
instance.showInteractiveDialogScreen = jest.fn();
|
||||
|
||||
expect(instance.showAlertDialog).toHaveBeenCalledTimes(0);
|
||||
expect(instance.showInteractiveDialogScreen).toHaveBeenCalledTimes(0);
|
||||
|
||||
baseProps = getBaseProps('trigger_id_2');
|
||||
wrapper.setProps({...baseProps});
|
||||
expect(instance.showAlertDialog).toHaveBeenCalledTimes(1);
|
||||
expect(instance.showAlertDialog).toHaveBeenCalledWith(baseProps.dialogData.dialog, baseProps.dialogData.url);
|
||||
expect(instance.showInteractiveDialogScreen).toHaveBeenCalledTimes(0);
|
||||
|
||||
const elements = [{
|
||||
data_source: '',
|
||||
default: '',
|
||||
display_name: 'Number',
|
||||
help_text: '',
|
||||
max_length: 0,
|
||||
min_length: 0,
|
||||
name: 'somenumber',
|
||||
optional: false,
|
||||
options: null,
|
||||
placeholder: '',
|
||||
subtype: 'number',
|
||||
type: 'text',
|
||||
}];
|
||||
|
||||
baseProps = getBaseProps('trigger_id_3', elements);
|
||||
wrapper.setProps({...baseProps});
|
||||
expect(instance.showAlertDialog).toHaveBeenCalledTimes(1);
|
||||
expect(instance.showInteractiveDialogScreen).toHaveBeenCalledTimes(1);
|
||||
expect(instance.showInteractiveDialogScreen).toHaveBeenCalledWith(baseProps.dialogData.dialog);
|
||||
});
|
||||
});
|
||||
|
||||
function getBaseProps(triggerId, elements) {
|
||||
const dialogData = {
|
||||
dialog: {
|
||||
callback_id: 'somecallbackid',
|
||||
elements,
|
||||
icon_url: 'icon_url',
|
||||
notify_on_cancel: true,
|
||||
state: 'somestate',
|
||||
submit_label: 'Submit Test',
|
||||
title: 'Dialog Test',
|
||||
},
|
||||
trigger_id: triggerId,
|
||||
url: 'https://localhost:8065/dialog_submit',
|
||||
};
|
||||
|
||||
return {
|
||||
actions: {
|
||||
showModal: jest.fn(),
|
||||
submitInteractiveDialog: jest.fn(),
|
||||
},
|
||||
triggerId,
|
||||
dialogData,
|
||||
theme: {},
|
||||
};
|
||||
}
|
||||
@@ -18,6 +18,7 @@ import {
|
||||
import FormattedText from 'app/components/formatted_text';
|
||||
import ProgressiveImage from 'app/components/progressive_image';
|
||||
import CustomPropTypes from 'app/constants/custom_prop_types';
|
||||
import EphemeralStore from 'app/store/ephemeral_store';
|
||||
import mattermostManaged from 'app/mattermost_managed';
|
||||
import BottomSheet from 'app/utils/bottom_sheet';
|
||||
import ImageCacheManager from 'app/utils/image_cache_manager';
|
||||
@@ -42,7 +43,6 @@ export default class MarkdownImage extends React.Component {
|
||||
imagesMetadata: PropTypes.object,
|
||||
linkDestination: PropTypes.string,
|
||||
isReplyPost: PropTypes.bool,
|
||||
serverURL: PropTypes.string.isRequired,
|
||||
source: PropTypes.string.isRequired,
|
||||
errorTextStyle: CustomPropTypes.Style,
|
||||
};
|
||||
@@ -99,7 +99,7 @@ export default class MarkdownImage extends React.Component {
|
||||
let source = this.props.source;
|
||||
|
||||
if (source.startsWith('/')) {
|
||||
source = this.props.serverURL + '/' + source;
|
||||
source = EphemeralStore.currentServerUrl + source;
|
||||
}
|
||||
|
||||
return source;
|
||||
|
||||
@@ -9,6 +9,7 @@ import {intlShape} from 'react-intl';
|
||||
|
||||
import CustomPropTypes from 'app/constants/custom_prop_types';
|
||||
import {DeepLinkTypes} from 'app/constants';
|
||||
import {getCurrentServerUrl} from 'app/init/credentials';
|
||||
import mattermostManaged from 'app/mattermost_managed';
|
||||
import BottomSheet from 'app/utils/bottom_sheet';
|
||||
import {preventDoubleTap} from 'app/utils/tap';
|
||||
@@ -24,7 +25,7 @@ export default class MarkdownLink extends PureComponent {
|
||||
children: CustomPropTypes.Children.isRequired,
|
||||
href: PropTypes.string.isRequired,
|
||||
onPermalinkPress: PropTypes.func,
|
||||
serverURL: PropTypes.string.isRequired,
|
||||
serverURL: PropTypes.string,
|
||||
siteURL: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
@@ -38,7 +39,7 @@ export default class MarkdownLink extends PureComponent {
|
||||
intl: intlShape.isRequired,
|
||||
};
|
||||
|
||||
handlePress = preventDoubleTap(() => {
|
||||
handlePress = preventDoubleTap(async () => {
|
||||
const {href, onPermalinkPress, serverURL, siteURL} = this.props;
|
||||
const url = normalizeProtocol(href);
|
||||
|
||||
@@ -46,7 +47,12 @@ export default class MarkdownLink extends PureComponent {
|
||||
return;
|
||||
}
|
||||
|
||||
const match = matchDeepLink(url, serverURL, siteURL);
|
||||
let serverUrl = serverURL;
|
||||
if (!serverUrl) {
|
||||
serverUrl = await getCurrentServerUrl();
|
||||
}
|
||||
|
||||
const match = matchDeepLink(url, serverUrl, siteURL);
|
||||
if (match) {
|
||||
if (match.type === DeepLinkTypes.CHANNEL) {
|
||||
this.props.actions.handleSelectChannelByName(match.channelName, match.teamName);
|
||||
|
||||
@@ -7,6 +7,7 @@ import {intlShape} from 'react-intl';
|
||||
import {Text} from 'react-native';
|
||||
|
||||
import CustomPropTypes from 'app/constants/custom_prop_types';
|
||||
import {getCurrentServerUrl} from 'app/init/credentials';
|
||||
import {preventDoubleTap} from 'app/utils/tap';
|
||||
|
||||
export default class MarkdownTableImage extends React.PureComponent {
|
||||
@@ -17,7 +18,7 @@ export default class MarkdownTableImage extends React.PureComponent {
|
||||
children: PropTypes.node.isRequired,
|
||||
source: PropTypes.string.isRequired,
|
||||
textStyle: CustomPropTypes.Style.isRequired,
|
||||
serverURL: PropTypes.string.isRequired,
|
||||
serverURL: PropTypes.string,
|
||||
theme: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
@@ -40,11 +41,16 @@ export default class MarkdownTableImage extends React.PureComponent {
|
||||
actions.goToScreen(screen, title, passProps);
|
||||
});
|
||||
|
||||
getImageSource = () => {
|
||||
getImageSource = async () => {
|
||||
let source = this.props.source;
|
||||
let serverUrl = this.props.serverURL;
|
||||
|
||||
if (!serverUrl) {
|
||||
serverUrl = await getCurrentServerUrl();
|
||||
}
|
||||
|
||||
if (source.startsWith('/')) {
|
||||
source = `${this.props.serverURL}/${source}`;
|
||||
source = serverUrl + source;
|
||||
}
|
||||
|
||||
return source;
|
||||
|
||||
@@ -9,7 +9,7 @@ import {init as initWebSocket, close as closeWebSocket} from 'mattermost-redux/a
|
||||
import {getCurrentChannelId} from 'mattermost-redux/selectors/entities/channels';
|
||||
|
||||
import {connection} from 'app/actions/device';
|
||||
import {markChannelViewedAndRead} from 'app/actions/views/channel';
|
||||
import {markChannelViewedAndRead, setChannelRetryFailed} from 'app/actions/views/channel';
|
||||
import {setCurrentUserStatusOffline} from 'app/actions/views/user';
|
||||
import {getConnection, isLandscape} from 'app/selectors/device';
|
||||
|
||||
@@ -36,6 +36,7 @@ function mapDispatchToProps(dispatch) {
|
||||
initWebSocket,
|
||||
logout,
|
||||
markChannelViewedAndRead,
|
||||
setChannelRetryFailed,
|
||||
setCurrentUserStatusOffline,
|
||||
startPeriodicStatusUpdates,
|
||||
stopPeriodicStatusUpdates,
|
||||
|
||||
@@ -45,6 +45,7 @@ export default class NetworkIndicator extends PureComponent {
|
||||
initWebSocket: PropTypes.func.isRequired,
|
||||
markChannelViewedAndRead: PropTypes.func.isRequired,
|
||||
logout: PropTypes.func.isRequired,
|
||||
setChannelRetryFailed: PropTypes.func.isRequired,
|
||||
setCurrentUserStatusOffline: PropTypes.func.isRequired,
|
||||
startPeriodicStatusUpdates: PropTypes.func.isRequired,
|
||||
stopPeriodicStatusUpdates: PropTypes.func.isRequired,
|
||||
@@ -77,6 +78,7 @@ export default class NetworkIndicator extends PureComponent {
|
||||
|
||||
this.backgroundColor = new Animated.Value(0);
|
||||
this.firstRun = true;
|
||||
this.statusUpdates = false;
|
||||
|
||||
this.networkListener = networkConnectionListener(this.handleConnectionChange);
|
||||
}
|
||||
@@ -137,7 +139,7 @@ export default class NetworkIndicator extends PureComponent {
|
||||
}
|
||||
|
||||
connect = (displayBar = false) => {
|
||||
const {connection} = this.props.actions;
|
||||
const {connection, startPeriodicStatusUpdates} = this.props.actions;
|
||||
clearTimeout(this.connectionRetryTimeout);
|
||||
|
||||
NetInfo.fetch().then(async ({isConnected}) => {
|
||||
@@ -148,7 +150,9 @@ export default class NetworkIndicator extends PureComponent {
|
||||
this.serverReachable = serverReachable;
|
||||
|
||||
if (serverReachable) {
|
||||
this.statusUpdates = true;
|
||||
this.initializeWebSocket();
|
||||
startPeriodicStatusUpdates();
|
||||
} else {
|
||||
if (displayBar) {
|
||||
this.show();
|
||||
@@ -165,6 +169,7 @@ export default class NetworkIndicator extends PureComponent {
|
||||
};
|
||||
|
||||
connected = () => {
|
||||
this.props.actions.setChannelRetryFailed(false);
|
||||
Animated.sequence([
|
||||
Animated.timing(
|
||||
this.backgroundColor, {
|
||||
@@ -218,9 +223,11 @@ export default class NetworkIndicator extends PureComponent {
|
||||
} = actions;
|
||||
|
||||
if (open) {
|
||||
this.statusUpdates = true;
|
||||
this.initializeWebSocket();
|
||||
startPeriodicStatusUpdates();
|
||||
} else {
|
||||
} else if (this.statusUpdates) {
|
||||
this.statusUpdates = false;
|
||||
closeWebSocket(true);
|
||||
stopPeriodicStatusUpdates();
|
||||
}
|
||||
@@ -247,13 +254,12 @@ export default class NetworkIndicator extends PureComponent {
|
||||
};
|
||||
|
||||
handleConnectionChange = ({hasInternet, serverReachable}) => {
|
||||
const {connection, startPeriodicStatusUpdates} = this.props.actions;
|
||||
const {connection} = this.props.actions;
|
||||
|
||||
// On first run always initialize the WebSocket
|
||||
// if we have internet connection
|
||||
if (hasInternet && this.firstRun) {
|
||||
this.initializeWebSocket();
|
||||
startPeriodicStatusUpdates();
|
||||
this.firstRun = false;
|
||||
|
||||
// if the state of the internet connection was previously known to be false,
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`PostAttachmentImage should match snapshot 1`] = `
|
||||
<TouchableWithoutFeedback
|
||||
onPress={[Function]}
|
||||
style={
|
||||
Array [
|
||||
Object {
|
||||
"alignItems": "flex-start",
|
||||
"justifyContent": "flex-start",
|
||||
"marginBottom": 6,
|
||||
"marginTop": 10,
|
||||
},
|
||||
Object {
|
||||
"height": 100,
|
||||
},
|
||||
]
|
||||
}
|
||||
>
|
||||
<View>
|
||||
<ForwardRef(forwardConnectRef)
|
||||
imageUri="uri"
|
||||
onError={[MockFunction]}
|
||||
resizeMode="contain"
|
||||
style={
|
||||
Array [
|
||||
Object {
|
||||
"alignItems": "center",
|
||||
"borderRadius": 3,
|
||||
"justifyContent": "center",
|
||||
"marginVertical": 1,
|
||||
},
|
||||
Object {
|
||||
"height": 100,
|
||||
"width": 100,
|
||||
},
|
||||
]
|
||||
}
|
||||
/>
|
||||
</View>
|
||||
</TouchableWithoutFeedback>
|
||||
`;
|
||||
@@ -47,7 +47,7 @@ export default class PostAttachmentImage extends React.PureComponent {
|
||||
<View ref={this.image}>
|
||||
<ProgressiveImage
|
||||
style={[styles.image, {width: this.props.width, height: this.props.height}]}
|
||||
defaultSource={{uri: this.props.uri}}
|
||||
imageUri={this.props.uri}
|
||||
resizeMode='contain'
|
||||
onError={this.props.onError}
|
||||
/>
|
||||
|
||||
23
app/components/post_attachment_image/index.test.js
Normal file
23
app/components/post_attachment_image/index.test.js
Normal file
@@ -0,0 +1,23 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
import {shallow} from 'enzyme';
|
||||
|
||||
import PostAttachmentImage from './index';
|
||||
|
||||
describe('PostAttachmentImage', () => {
|
||||
const baseProps = {
|
||||
height: 100,
|
||||
width: 100,
|
||||
onError: jest.fn(),
|
||||
onImagePress: jest.fn(),
|
||||
uri: 'uri',
|
||||
};
|
||||
|
||||
it('should match snapshot', () => {
|
||||
const wrapper = shallow(<PostAttachmentImage {...baseProps}/>);
|
||||
|
||||
expect(wrapper.getElement()).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
@@ -202,7 +202,10 @@ export default class PostHeader extends PureComponent {
|
||||
dateComponent = (
|
||||
<View style={style.datetime}>
|
||||
<Text style={style.time}>
|
||||
<FormattedDate value={createAt}/>
|
||||
<FormattedDate
|
||||
timeZone={userTimezone}
|
||||
value={createAt}
|
||||
/>
|
||||
</Text>
|
||||
<Text style={style.time}>
|
||||
<FormattedTime
|
||||
@@ -240,8 +243,8 @@ export default class PostHeader extends PureComponent {
|
||||
style={style.replyIconContainer}
|
||||
>
|
||||
<ReplyIcon
|
||||
height={15}
|
||||
width={15}
|
||||
height={16}
|
||||
width={16}
|
||||
color={theme.linkColor}
|
||||
/>
|
||||
{!isSearchResult &&
|
||||
|
||||
@@ -31,9 +31,8 @@ exports[`DateHeader component should match snapshot when timezone is set 1`] = `
|
||||
}
|
||||
}
|
||||
>
|
||||
<InjectIntl(FormattedDate)
|
||||
day="2-digit"
|
||||
month="short"
|
||||
<FormattedDate
|
||||
format="ddd, MMM DD, YYYY"
|
||||
style={
|
||||
Object {
|
||||
"color": "#3d3c40",
|
||||
@@ -41,9 +40,8 @@ exports[`DateHeader component should match snapshot when timezone is set 1`] = `
|
||||
"fontWeight": "600",
|
||||
}
|
||||
}
|
||||
value={1970-01-18T17:19:12.392Z}
|
||||
weekday="short"
|
||||
year="numeric"
|
||||
timeZone="America/New_York"
|
||||
value={1531152392}
|
||||
/>
|
||||
</View>
|
||||
<View
|
||||
@@ -90,9 +88,8 @@ exports[`DateHeader component should match snapshot with suffix 1`] = `
|
||||
}
|
||||
}
|
||||
>
|
||||
<InjectIntl(FormattedDate)
|
||||
day="2-digit"
|
||||
month="short"
|
||||
<FormattedDate
|
||||
format="ddd, MMM DD, YYYY"
|
||||
style={
|
||||
Object {
|
||||
"color": "#3d3c40",
|
||||
@@ -100,9 +97,8 @@ exports[`DateHeader component should match snapshot with suffix 1`] = `
|
||||
"fontWeight": "600",
|
||||
}
|
||||
}
|
||||
value={1970-01-18T17:19:12.392Z}
|
||||
weekday="short"
|
||||
year="numeric"
|
||||
timeZone={null}
|
||||
value={1531152392}
|
||||
/>
|
||||
</View>
|
||||
<View
|
||||
@@ -149,9 +145,8 @@ exports[`DateHeader component should match snapshot without suffix 1`] = `
|
||||
}
|
||||
}
|
||||
>
|
||||
<InjectIntl(FormattedDate)
|
||||
day="2-digit"
|
||||
month="short"
|
||||
<FormattedDate
|
||||
format="ddd, MMM DD, YYYY"
|
||||
style={
|
||||
Object {
|
||||
"color": "#3d3c40",
|
||||
@@ -159,9 +154,8 @@ exports[`DateHeader component should match snapshot without suffix 1`] = `
|
||||
"fontWeight": "600",
|
||||
}
|
||||
}
|
||||
value={1970-01-18T17:19:12.392Z}
|
||||
weekday="short"
|
||||
year="numeric"
|
||||
timeZone={null}
|
||||
value={1531152392}
|
||||
/>
|
||||
</View>
|
||||
<View
|
||||
|
||||
@@ -7,7 +7,6 @@ import {
|
||||
View,
|
||||
ViewPropTypes,
|
||||
} from 'react-native';
|
||||
import moment from 'moment-timezone';
|
||||
|
||||
import FormattedDate from 'app/components/formatted_date';
|
||||
import {makeStyleSheetFromTheme} from 'app/utils/theme';
|
||||
@@ -25,25 +24,14 @@ export default class DateHeader extends PureComponent {
|
||||
const {theme, timeZone, date} = this.props;
|
||||
const style = getStyleSheet(theme);
|
||||
|
||||
const dateFormatProps = {
|
||||
weekday: 'short',
|
||||
day: '2-digit',
|
||||
month: 'short',
|
||||
year: 'numeric',
|
||||
value: new Date(date),
|
||||
};
|
||||
|
||||
if (timeZone) {
|
||||
dateFormatProps.value = moment.tz(date, timeZone).toDate();
|
||||
}
|
||||
|
||||
return (
|
||||
<View style={[style.container, this.props.style]}>
|
||||
<View style={style.line}/>
|
||||
<View style={style.dateContainer}>
|
||||
<FormattedDate
|
||||
style={style.date}
|
||||
{...dateFormatProps}
|
||||
timeZone={timeZone}
|
||||
value={date}
|
||||
/>
|
||||
</View>
|
||||
<View style={style.line}/>
|
||||
|
||||
@@ -100,6 +100,7 @@ export default class PostList extends PureComponent {
|
||||
if (this.props.channelId !== nextProps.channelId) {
|
||||
this.contentOffsetY = 0;
|
||||
this.hasDoneInitialScroll = false;
|
||||
this.setState({contentHeight: 0});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -115,10 +116,22 @@ export default class PostList extends PureComponent {
|
||||
this.scrollToBottom();
|
||||
this.shouldScrollToBottom = false;
|
||||
}
|
||||
|
||||
if (!this.hasDoneInitialScroll && this.props.initialIndex > 0 && this.state.contentHeight) {
|
||||
this.scrollToInitialIndexIfNeeded(this.props.initialIndex);
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
EventEmitter.off('scroll-to-bottom', this.handleSetScrollToBottom);
|
||||
|
||||
if (this.animationFrameIndexFailed) {
|
||||
cancelAnimationFrame(this.animationFrameIndexFailed);
|
||||
}
|
||||
|
||||
if (this.animationFrameInitialIndex) {
|
||||
cancelAnimationFrame(this.animationFrameInitialIndex);
|
||||
}
|
||||
}
|
||||
|
||||
handleClosePermalink = () => {
|
||||
@@ -128,12 +141,12 @@ export default class PostList extends PureComponent {
|
||||
};
|
||||
|
||||
handleContentSizeChange = (contentWidth, contentHeight) => {
|
||||
this.scrollToInitialIndexIfNeeded(contentWidth, contentHeight);
|
||||
|
||||
if (this.state.postListHeight && contentHeight < this.state.postListHeight && this.props.extraData) {
|
||||
// We still have less than 1 screen of posts loaded with more to get, so load more
|
||||
this.props.onLoadMoreUp();
|
||||
}
|
||||
this.setState({contentHeight}, () => {
|
||||
if (this.state.postListHeight && contentHeight < this.state.postListHeight && this.props.extraData) {
|
||||
// We still have less than 1 screen of posts loaded with more to get, so load more
|
||||
this.props.onLoadMoreUp();
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
handleDeepLink = (url) => {
|
||||
@@ -201,9 +214,11 @@ export default class PostList extends PureComponent {
|
||||
};
|
||||
|
||||
handleScrollToIndexFailed = () => {
|
||||
requestAnimationFrame(() => {
|
||||
this.hasDoneInitialScroll = false;
|
||||
this.scrollToInitialIndexIfNeeded(1, 1);
|
||||
this.animationFrameIndexFailed = requestAnimationFrame(() => {
|
||||
if (this.props.initialIndex > 0 && this.state.contentHeight > 0) {
|
||||
this.hasDoneInitialScroll = false;
|
||||
this.scrollToInitialIndexIfNeeded(this.props.initialIndex);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
@@ -286,23 +301,17 @@ export default class PostList extends PureComponent {
|
||||
}, 250);
|
||||
};
|
||||
|
||||
scrollToInitialIndexIfNeeded = (width, height) => {
|
||||
if (
|
||||
width > 0 &&
|
||||
height > 0 &&
|
||||
this.props.initialIndex > 0 &&
|
||||
!this.hasDoneInitialScroll &&
|
||||
this.flatListRef?.current
|
||||
) {
|
||||
requestAnimationFrame(() => {
|
||||
scrollToInitialIndexIfNeeded = (index) => {
|
||||
if (!this.hasDoneInitialScroll && this.flatListRef?.current) {
|
||||
this.hasDoneInitialScroll = true;
|
||||
this.animationFrameInitialIndex = requestAnimationFrame(() => {
|
||||
this.flatListRef.current.scrollToIndex({
|
||||
animated: false,
|
||||
index: this.props.initialIndex,
|
||||
index,
|
||||
viewOffset: 0,
|
||||
viewPosition: 1, // 0 is at bottom
|
||||
});
|
||||
});
|
||||
this.hasDoneInitialScroll = true;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
import {connect} from 'react-redux';
|
||||
|
||||
import {isSystemMessage} from 'mattermost-redux/utils/post_utils';
|
||||
import {Client4} from 'mattermost-redux/client';
|
||||
|
||||
import {getTheme} from 'mattermost-redux/selectors/entities/preferences';
|
||||
import {getConfig} from 'mattermost-redux/selectors/entities/general';
|
||||
@@ -19,14 +20,17 @@ function mapStateToProps(state, ownProps) {
|
||||
const post = ownProps.post;
|
||||
const user = getUser(state, post.user_id);
|
||||
|
||||
const overrideIconUrl = Client4.getAbsoluteUrl(post?.props?.override_icon_url); // eslint-disable-line camelcase
|
||||
|
||||
return {
|
||||
enablePostIconOverride: config.EnablePostIconOverride === 'true' && post?.props?.use_user_icon !== 'true', // eslint-disable-line camelcase
|
||||
fromWebHook: post?.props?.from_webhook === 'true', // eslint-disable-line camelcase
|
||||
isSystemMessage: isSystemMessage(post),
|
||||
fromAutoResponder: fromAutoResponder(post),
|
||||
overrideIconUrl: post?.props?.override_icon_url, // eslint-disable-line camelcase
|
||||
overrideIconUrl,
|
||||
userId: post.user_id,
|
||||
isBot: (user ? user.is_bot : false),
|
||||
isEmoji: Boolean(post?.props?.override_icon_emoji), // eslint-disable-line camelcase
|
||||
theme: getTheme(state),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -22,6 +22,7 @@ export default class PostProfilePicture extends PureComponent {
|
||||
theme: PropTypes.object,
|
||||
userId: PropTypes.string,
|
||||
isBot: PropTypes.bool,
|
||||
isEmoji: PropTypes.bool,
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
@@ -39,6 +40,7 @@ export default class PostProfilePicture extends PureComponent {
|
||||
theme,
|
||||
userId,
|
||||
isBot,
|
||||
isEmoji,
|
||||
} = this.props;
|
||||
|
||||
if (isSystemMessage && !fromAutoResponder && !isBot) {
|
||||
@@ -55,15 +57,26 @@ export default class PostProfilePicture extends PureComponent {
|
||||
|
||||
if (fromWebHook && enablePostIconOverride) {
|
||||
const icon = overrideIconUrl ? {uri: overrideIconUrl} : webhookIcon;
|
||||
const frameSize = ViewTypes.PROFILE_PICTURE_SIZE;
|
||||
const pictureSize = isEmoji ? ViewTypes.PROFILE_PICTURE_EMOJI_SIZE : ViewTypes.PROFILE_PICTURE_SIZE;
|
||||
const borderRadius = isEmoji ? 0 : ViewTypes.PROFILE_PICTURE_SIZE / 2;
|
||||
|
||||
return (
|
||||
<View>
|
||||
<View
|
||||
style={{
|
||||
borderRadius,
|
||||
overflow: 'hidden',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
height: frameSize,
|
||||
width: frameSize,
|
||||
}}
|
||||
>
|
||||
<Image
|
||||
source={icon}
|
||||
style={{
|
||||
height: ViewTypes.PROFILE_PICTURE_SIZE,
|
||||
width: ViewTypes.PROFILE_PICTURE_SIZE,
|
||||
borderRadius: ViewTypes.PROFILE_PICTURE_SIZE / 2,
|
||||
height: pictureSize,
|
||||
width: pictureSize,
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
|
||||
@@ -98,7 +98,7 @@ exports[`PostTextBox should match, full snapshot 1`] = `
|
||||
visible={false}
|
||||
>
|
||||
<SendButton
|
||||
disabled={false}
|
||||
disabled={true}
|
||||
handleSendMessage={[Function]}
|
||||
theme={
|
||||
Object {
|
||||
|
||||
@@ -6,6 +6,9 @@ import {shallowWithIntl} from 'test/intl-test-helper';
|
||||
|
||||
import Preferences from 'mattermost-redux/constants/preferences';
|
||||
|
||||
import Fade from 'app/components/fade';
|
||||
import SendButton from 'app/components/send_button';
|
||||
|
||||
import PostTextbox from './post_textbox.ios';
|
||||
|
||||
jest.mock('NativeEventEmitter');
|
||||
@@ -84,4 +87,73 @@ describe('PostTextBox', () => {
|
||||
expect(baseProps.actions.handlePostDraftChanged).toHaveBeenCalledWith(baseProps.channelId, value);
|
||||
expect(baseProps.actions.handlePostDraftChanged).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
describe('send button', () => {
|
||||
test('should initially disable and hide the send button', () => {
|
||||
const wrapper = shallowWithIntl(
|
||||
<PostTextbox {...baseProps}/>
|
||||
);
|
||||
|
||||
expect(wrapper.find(Fade).prop('visible')).toBe(false);
|
||||
expect(wrapper.find(SendButton).prop('disabled')).toBe(true);
|
||||
});
|
||||
|
||||
test('should show a disabled send button when uploading a file', () => {
|
||||
const props = {
|
||||
...baseProps,
|
||||
files: [{loading: true}],
|
||||
};
|
||||
|
||||
const wrapper = shallowWithIntl(
|
||||
<PostTextbox {...props}/>
|
||||
);
|
||||
|
||||
expect(wrapper.find(Fade).prop('visible')).toBe(true);
|
||||
expect(wrapper.find(SendButton).prop('disabled')).toBe(true);
|
||||
});
|
||||
|
||||
test('should show an enabled send button after uploading a file', () => {
|
||||
const props = {
|
||||
...baseProps,
|
||||
files: [{loading: false}],
|
||||
};
|
||||
|
||||
const wrapper = shallowWithIntl(
|
||||
<PostTextbox {...props}/>
|
||||
);
|
||||
|
||||
expect(wrapper.find(Fade).prop('visible')).toBe(true);
|
||||
expect(wrapper.find(SendButton).prop('disabled')).toBe(false);
|
||||
});
|
||||
|
||||
test('should show an enabled send button with a message', () => {
|
||||
const props = {
|
||||
...baseProps,
|
||||
value: 'test',
|
||||
};
|
||||
|
||||
const wrapper = shallowWithIntl(
|
||||
<PostTextbox {...props}/>
|
||||
);
|
||||
|
||||
expect(wrapper.find(Fade).prop('visible')).toBe(true);
|
||||
expect(wrapper.find(SendButton).prop('disabled')).toBe(false);
|
||||
});
|
||||
|
||||
test('should show a disabled send button while sending the message', () => {
|
||||
const props = {
|
||||
...baseProps,
|
||||
value: 'test',
|
||||
};
|
||||
|
||||
const wrapper = shallowWithIntl(
|
||||
<PostTextbox {...props}/>
|
||||
);
|
||||
|
||||
wrapper.setState({sendingMessage: true});
|
||||
|
||||
expect(wrapper.find(Fade).prop('visible')).toBe(true);
|
||||
expect(wrapper.find(SendButton).prop('disabled')).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -91,6 +91,7 @@ export default class PostTextBoxBase extends PureComponent {
|
||||
this.state = {
|
||||
cursorPosition: 0,
|
||||
keyboardType: 'default',
|
||||
sendingMessage: false,
|
||||
top: 0,
|
||||
value: props.value,
|
||||
};
|
||||
@@ -278,10 +279,12 @@ export default class PostTextBoxBase extends PureComponent {
|
||||
};
|
||||
|
||||
handleSendMessage = () => {
|
||||
if (!this.canSend()) {
|
||||
if (!this.canSend() || this.state.sendingMessage) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.setState({sendingMessage: true});
|
||||
|
||||
const {actions, channelId, files, rootId} = this.props;
|
||||
const {value} = this.state;
|
||||
|
||||
@@ -307,6 +310,9 @@ export default class PostTextBoxBase extends PureComponent {
|
||||
}),
|
||||
[{
|
||||
text: intl.formatMessage({id: 'mobile.channel_info.alertNo', defaultMessage: 'No'}),
|
||||
onPress: () => {
|
||||
this.setState({sendingMessage: false});
|
||||
},
|
||||
}, {
|
||||
text: intl.formatMessage({id: 'mobile.channel_info.alertYes', defaultMessage: 'Yes'}),
|
||||
onPress: () => {
|
||||
@@ -388,81 +394,72 @@ export default class PostTextBoxBase extends PureComponent {
|
||||
return this.canSend() || this.isFileLoading();
|
||||
}
|
||||
|
||||
isSendButtonEnabled() {
|
||||
return this.canSend() && !this.state.sendingMessage;
|
||||
}
|
||||
|
||||
sendMessage = () => {
|
||||
const {actions, currentUserId, channelId, files, rootId} = this.props;
|
||||
const {intl} = this.context;
|
||||
const {value} = this.state;
|
||||
|
||||
if (files.length === 0 && !value) {
|
||||
Alert.alert(
|
||||
intl.formatMessage({
|
||||
id: 'mobile.post_textbox.empty.title',
|
||||
defaultMessage: 'Empty Message',
|
||||
}),
|
||||
intl.formatMessage({
|
||||
id: 'mobile.post_textbox.empty.message',
|
||||
defaultMessage: 'You are trying to send an empty message.\nPlease make sure you have a message or at least one attached file.',
|
||||
}),
|
||||
[{
|
||||
text: intl.formatMessage({id: 'mobile.post_textbox.empty.ok', defaultMessage: 'OK'}),
|
||||
}],
|
||||
);
|
||||
if (value.indexOf('/') === 0) {
|
||||
this.sendCommand(value);
|
||||
} else {
|
||||
if (value.indexOf('/') === 0) {
|
||||
this.sendCommand(value);
|
||||
} else {
|
||||
const postFiles = files.filter((f) => !f.failed);
|
||||
const post = {
|
||||
user_id: currentUserId,
|
||||
channel_id: channelId,
|
||||
root_id: rootId,
|
||||
parent_id: rootId,
|
||||
message: value,
|
||||
};
|
||||
const postFiles = files.filter((f) => !f.failed);
|
||||
const post = {
|
||||
user_id: currentUserId,
|
||||
channel_id: channelId,
|
||||
root_id: rootId,
|
||||
parent_id: rootId,
|
||||
message: value,
|
||||
};
|
||||
|
||||
actions.createPost(post, postFiles);
|
||||
actions.createPost(post, postFiles);
|
||||
|
||||
if (postFiles.length) {
|
||||
actions.handleClearFiles(channelId, rootId);
|
||||
}
|
||||
if (postFiles.length) {
|
||||
actions.handleClearFiles(channelId, rootId);
|
||||
}
|
||||
|
||||
if (Platform.OS === 'ios') {
|
||||
// On iOS, if the PostTextbox height increases from its
|
||||
// initial height (due to a multiline post or a post whose
|
||||
// message wraps, for example), then when the text is cleared
|
||||
// the PostTextbox height decrease will be animated. This
|
||||
// animation in conjunction with the PostList animation as it
|
||||
// receives the newly created post is causing issues in the iOS
|
||||
// PostList component as it fails to properly react to its content
|
||||
// size changes. While a proper fix is determined for the PostList
|
||||
// component, a small delay in triggering the height decrease
|
||||
// animation gives the PostList enough time to first handle content
|
||||
// size changes from the new post.
|
||||
setTimeout(() => {
|
||||
this.handleTextChange('');
|
||||
}, 250);
|
||||
} else {
|
||||
this.handleTextChange('');
|
||||
}
|
||||
|
||||
this.changeDraft('');
|
||||
|
||||
let callback;
|
||||
if (Platform.OS === 'android') {
|
||||
// Fixes the issue where Android predictive text would prepend suggestions to the post draft when messages
|
||||
// are typed successively without blurring the input
|
||||
const nextState = {
|
||||
keyboardType: 'email-address',
|
||||
};
|
||||
|
||||
callback = () => this.setState({keyboardType: 'default'});
|
||||
|
||||
this.setState(nextState, callback);
|
||||
}
|
||||
|
||||
EventEmitter.emit('scroll-to-bottom');
|
||||
}
|
||||
|
||||
if (Platform.OS === 'ios') {
|
||||
// On iOS, if the PostTextbox height increases from its
|
||||
// initial height (due to a multiline post or a post whose
|
||||
// message wraps, for example), then when the text is cleared
|
||||
// the PostTextbox height decrease will be animated. This
|
||||
// animation in conjunction with the PostList animation as it
|
||||
// receives the newly created post is causing issues in the iOS
|
||||
// PostList component as it fails to properly react to its content
|
||||
// size changes. While a proper fix is determined for the PostList
|
||||
// component, a small delay in triggering the height decrease
|
||||
// animation gives the PostList enough time to first handle content
|
||||
// size changes from the new post.
|
||||
setTimeout(() => {
|
||||
this.handleTextChange('');
|
||||
|
||||
this.setState({sendingMessage: false});
|
||||
}, 250);
|
||||
} else {
|
||||
this.handleTextChange('');
|
||||
|
||||
this.setState({sendingMessage: false});
|
||||
}
|
||||
|
||||
this.changeDraft('');
|
||||
|
||||
let callback;
|
||||
if (Platform.OS === 'android') {
|
||||
// Fixes the issue where Android predictive text would prepend suggestions to the post draft when messages
|
||||
// are typed successively without blurring the input
|
||||
const nextState = {
|
||||
keyboardType: 'email-address',
|
||||
};
|
||||
|
||||
callback = () => this.setState({keyboardType: 'default'});
|
||||
|
||||
this.setState(nextState, callback);
|
||||
}
|
||||
|
||||
EventEmitter.emit('scroll-to-bottom');
|
||||
};
|
||||
|
||||
getStatusFromSlashCommand = (message) => {
|
||||
@@ -516,6 +513,7 @@ export default class PostTextBoxBase extends PureComponent {
|
||||
const {actions, rootId} = this.props;
|
||||
actions.addReactionToLatestPost(emoji, rootId);
|
||||
this.handleTextChange('');
|
||||
this.setState({sendingMessage: false});
|
||||
this.changeDraft('');
|
||||
};
|
||||
|
||||
@@ -628,7 +626,7 @@ export default class PostTextBoxBase extends PureComponent {
|
||||
/>
|
||||
<Fade visible={this.isSendButtonVisible()}>
|
||||
<SendButton
|
||||
disabled={this.isFileLoading()}
|
||||
disabled={!this.isSendButtonEnabled()}
|
||||
handleSendMessage={this.handleSendMessage}
|
||||
theme={theme}
|
||||
/>
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`MainSidebar should match, full snapshot 1`] = `
|
||||
<DrawerLayout
|
||||
drawerPosition="left"
|
||||
drawerWidth={-30}
|
||||
isTablet={false}
|
||||
onDrawerClose={[Function]}
|
||||
onDrawerOpen={[Function]}
|
||||
renderNavigationView={[Function]}
|
||||
useNativeAnimations={true}
|
||||
/>
|
||||
`;
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
View,
|
||||
} from 'react-native';
|
||||
import {intlShape} from 'react-intl';
|
||||
import AsyncStorage from '@react-native-community/async-storage';
|
||||
|
||||
import {General, WebsocketEvents} from 'mattermost-redux/constants';
|
||||
import EventEmitter from 'mattermost-redux/utils/event_emitter';
|
||||
@@ -79,9 +80,11 @@ export default class ChannelSidebar extends Component {
|
||||
this.mounted = true;
|
||||
this.props.actions.getTeams();
|
||||
this.handleDimensions();
|
||||
this.handlePermanentSidebar();
|
||||
EventEmitter.on('close_channel_drawer', this.closeChannelDrawer);
|
||||
EventEmitter.on('renderDrawer', this.handleShowDrawerContent);
|
||||
EventEmitter.on(WebsocketEvents.CHANNEL_UPDATED, this.handleUpdateTitle);
|
||||
EventEmitter.on(DeviceTypes.PERMANENT_SIDEBAR_SETTINGS, this.handlePermanentSidebar);
|
||||
BackHandler.addEventListener('hardwareBackPress', this.handleAndroidBack);
|
||||
Dimensions.addEventListener('change', this.handleDimensions);
|
||||
}
|
||||
@@ -103,7 +106,7 @@ export default class ChannelSidebar extends Component {
|
||||
|
||||
shouldComponentUpdate(nextProps, nextState) {
|
||||
const {currentTeamId, deviceWidth, isLandscape, teamsCount} = this.props;
|
||||
const {openDrawerOffset, isSplitView, show, searching} = this.state;
|
||||
const {openDrawerOffset, isSplitView, permanentSidebar, show, searching} = this.state;
|
||||
|
||||
if (nextState.openDrawerOffset !== openDrawerOffset || nextState.show !== show || nextState.searching !== searching) {
|
||||
return true;
|
||||
@@ -112,7 +115,8 @@ export default class ChannelSidebar extends Component {
|
||||
return nextProps.currentTeamId !== currentTeamId ||
|
||||
nextProps.isLandscape !== isLandscape || nextProps.deviceWidth !== deviceWidth ||
|
||||
nextProps.teamsCount !== teamsCount ||
|
||||
nextState.isSplitView !== isSplitView;
|
||||
nextState.isSplitView !== isSplitView ||
|
||||
nextState.permanentSidebar !== permanentSidebar;
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
@@ -120,6 +124,7 @@ export default class ChannelSidebar extends Component {
|
||||
EventEmitter.off('close_channel_drawer', this.closeChannelDrawer);
|
||||
EventEmitter.off(WebsocketEvents.CHANNEL_UPDATED, this.handleUpdateTitle);
|
||||
EventEmitter.off('renderDrawer', this.handleShowDrawerContent);
|
||||
EventEmitter.off(DeviceTypes.PERMANENT_SIDEBAR_SETTINGS, this.handlePermanentSidebar);
|
||||
BackHandler.removeEventListener('hardwareBackPress', this.handleAndroidBack);
|
||||
Dimensions.addEventListener('change', this.handleDimensions);
|
||||
}
|
||||
@@ -142,6 +147,13 @@ export default class ChannelSidebar extends Component {
|
||||
}
|
||||
};
|
||||
|
||||
handlePermanentSidebar = async () => {
|
||||
if (DeviceTypes.IS_TABLET && this.mounted) {
|
||||
const enabled = await AsyncStorage.getItem(DeviceTypes.PERMANENT_SIDEBAR_SETTINGS);
|
||||
this.setState({permanentSidebar: enabled === 'true'});
|
||||
}
|
||||
};
|
||||
|
||||
handleShowDrawerContent = () => {
|
||||
requestAnimationFrame(() => this.setState({show: true}));
|
||||
};
|
||||
@@ -378,7 +390,7 @@ export default class ChannelSidebar extends Component {
|
||||
<SafeAreaView
|
||||
navBarBackgroundColor={theme.sidebarBg}
|
||||
backgroundColor={theme.sidebarHeaderBg}
|
||||
footerColor={theme.sidebarHeaderBg}
|
||||
footerColor={theme.sidebarBg}
|
||||
>
|
||||
<DrawerSwiper
|
||||
ref={this.drawerSwiperRef}
|
||||
@@ -396,7 +408,7 @@ export default class ChannelSidebar extends Component {
|
||||
render() {
|
||||
const {children, deviceWidth} = this.props;
|
||||
const {openDrawerOffset} = this.state;
|
||||
const isTablet = DeviceTypes.IS_TABLET && !this.state.isSplitView;
|
||||
const isTablet = DeviceTypes.IS_TABLET && !this.state.isSplitView && this.state.permanentSidebar;
|
||||
const drawerWidth = DeviceTypes.IS_TABLET ? TABLET_WIDTH : (deviceWidth - openDrawerOffset);
|
||||
|
||||
return (
|
||||
|
||||
61
app/components/sidebars/main/main_sidebar.test.js
Normal file
61
app/components/sidebars/main/main_sidebar.test.js
Normal file
@@ -0,0 +1,61 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
import {shallow} from 'enzyme';
|
||||
|
||||
import Preferences from 'mattermost-redux/constants/preferences';
|
||||
|
||||
import {DeviceTypes} from 'app/constants';
|
||||
|
||||
import MainSidebar from './main_sidebar';
|
||||
|
||||
jest.mock('react-intl');
|
||||
|
||||
describe('MainSidebar', () => {
|
||||
const baseProps = {
|
||||
actions: {
|
||||
getTeams: jest.fn(),
|
||||
logChannelSwitch: jest.fn(),
|
||||
makeDirectChannel: jest.fn(),
|
||||
setChannelDisplayName: jest.fn(),
|
||||
setChannelLoading: jest.fn(),
|
||||
},
|
||||
blurPostTextBox: jest.fn(),
|
||||
currentTeamId: 'current-team-id',
|
||||
currentUserId: 'current-user-id',
|
||||
deviceWidth: 10,
|
||||
isLandscape: false,
|
||||
teamsCount: 2,
|
||||
theme: Preferences.THEMES.default,
|
||||
};
|
||||
|
||||
test('should match, full snapshot', () => {
|
||||
const wrapper = shallow(
|
||||
<MainSidebar {...baseProps}/>
|
||||
);
|
||||
|
||||
expect(wrapper.getElement()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('should not set the permanentSidebar state if not Tablet', () => {
|
||||
const wrapper = shallow(
|
||||
<MainSidebar {...baseProps}/>
|
||||
);
|
||||
|
||||
wrapper.instance().handlePermanentSidebar();
|
||||
expect(wrapper.state('permanentSidebar')).toBeUndefined();
|
||||
});
|
||||
|
||||
test('should set the permanentSidebar state if Tablet', async () => {
|
||||
const wrapper = shallow(
|
||||
<MainSidebar {...baseProps}/>
|
||||
);
|
||||
|
||||
DeviceTypes.IS_TABLET = true;
|
||||
|
||||
await wrapper.instance().handlePermanentSidebar();
|
||||
|
||||
expect(wrapper.state('permanentSidebar')).toBeDefined();
|
||||
});
|
||||
});
|
||||
@@ -4,14 +4,12 @@
|
||||
import {bindActionCreators} from 'redux';
|
||||
import {connect} from 'react-redux';
|
||||
|
||||
import {getCurrentUrl} from 'mattermost-redux/selectors/entities/general';
|
||||
import {getCurrentTeamId, getMySortedTeamIds, getJoinableTeamIds} from 'mattermost-redux/selectors/entities/teams';
|
||||
import {getTheme} from 'mattermost-redux/selectors/entities/preferences';
|
||||
|
||||
import {showModal} from 'app/actions/navigation';
|
||||
import {handleTeamChange} from 'app/actions/views/select_team';
|
||||
import {getCurrentLocale} from 'app/selectors/i18n';
|
||||
import {removeProtocol} from 'app/utils/url';
|
||||
|
||||
import TeamsList from './teams_list';
|
||||
|
||||
@@ -20,7 +18,6 @@ function mapStateToProps(state) {
|
||||
|
||||
return {
|
||||
currentTeamId: getCurrentTeamId(state),
|
||||
currentUrl: removeProtocol(getCurrentUrl(state)),
|
||||
hasOtherJoinableTeams: getJoinableTeamIds(state).length > 0,
|
||||
teamIds: getMySortedTeamIds(state, locale),
|
||||
theme: getTheme(state),
|
||||
|
||||
@@ -17,8 +17,10 @@ import MaterialIcon from 'react-native-vector-icons/MaterialIcons';
|
||||
|
||||
import FormattedText from 'app/components/formatted_text';
|
||||
import {DeviceTypes, ListTypes, ViewTypes} from 'app/constants';
|
||||
import {getCurrentServerUrl} from 'app/init/credentials';
|
||||
import {preventDoubleTap} from 'app/utils/tap';
|
||||
import {changeOpacity, makeStyleSheetFromTheme} from 'app/utils/theme';
|
||||
import {removeProtocol} from 'app/utils/url';
|
||||
import tracker from 'app/utils/time_tracker';
|
||||
import telemetry from 'app/telemetry';
|
||||
|
||||
@@ -38,7 +40,6 @@ export default class TeamsList extends PureComponent {
|
||||
}).isRequired,
|
||||
closeChannelDrawer: PropTypes.func.isRequired,
|
||||
currentTeamId: PropTypes.string.isRequired,
|
||||
currentUrl: PropTypes.string.isRequired,
|
||||
hasOtherJoinableTeams: PropTypes.bool,
|
||||
teamIds: PropTypes.array.isRequired,
|
||||
theme: PropTypes.object.isRequired,
|
||||
@@ -51,9 +52,17 @@ export default class TeamsList extends PureComponent {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
serverUrl: '',
|
||||
};
|
||||
|
||||
MaterialIcon.getImageSource('close', 20, props.theme.sidebarHeaderTextColor).then((source) => {
|
||||
this.closeButton = source;
|
||||
});
|
||||
|
||||
getCurrentServerUrl().then((url) => {
|
||||
this.setState({serverUrl: removeProtocol(url)});
|
||||
});
|
||||
}
|
||||
|
||||
selectTeam = (teamId) => {
|
||||
@@ -75,13 +84,14 @@ export default class TeamsList extends PureComponent {
|
||||
});
|
||||
};
|
||||
|
||||
goToSelectTeam = preventDoubleTap(() => {
|
||||
goToSelectTeam = preventDoubleTap(async () => {
|
||||
const {intl} = this.context;
|
||||
const {currentUrl, theme, actions} = this.props;
|
||||
const {theme, actions} = this.props;
|
||||
const {serverUrl} = this.state;
|
||||
const screen = 'SelectTeam';
|
||||
const title = intl.formatMessage({id: 'mobile.routes.selectTeam', defaultMessage: 'Select Team'});
|
||||
const passProps = {
|
||||
currentUrl,
|
||||
currentUrl: serverUrl,
|
||||
theme,
|
||||
};
|
||||
const options = {
|
||||
@@ -117,6 +127,7 @@ export default class TeamsList extends PureComponent {
|
||||
renderItem = ({item}) => {
|
||||
return (
|
||||
<TeamsListItem
|
||||
currentUrl={this.state.serverUrl}
|
||||
selectTeam={this.selectTeam}
|
||||
teamId={item}
|
||||
/>
|
||||
@@ -155,6 +166,7 @@ export default class TeamsList extends PureComponent {
|
||||
{moreAction}
|
||||
</View>
|
||||
<FlatList
|
||||
extraData={this.state.serverUrl}
|
||||
contentContainerStyle={this.listContentPadding()}
|
||||
data={teamIds}
|
||||
renderItem={this.renderItem}
|
||||
|
||||
@@ -3,12 +3,9 @@
|
||||
|
||||
import {connect} from 'react-redux';
|
||||
|
||||
import {getCurrentUrl} from 'mattermost-redux/selectors/entities/general';
|
||||
import {getTheme} from 'mattermost-redux/selectors/entities/preferences';
|
||||
import {getCurrentTeamId, getTeam, makeGetBadgeCountForTeamId} from 'mattermost-redux/selectors/entities/teams';
|
||||
|
||||
import {removeProtocol} from 'app/utils/url';
|
||||
|
||||
import TeamsListItem from './teams_list_item.js';
|
||||
|
||||
function makeMapStateToProps() {
|
||||
@@ -19,7 +16,6 @@ function makeMapStateToProps() {
|
||||
|
||||
return {
|
||||
currentTeamId: getCurrentTeamId(state),
|
||||
currentUrl: removeProtocol(getCurrentUrl(state)),
|
||||
displayName: team.display_name,
|
||||
mentionCount: getMentionCount(state, ownProps.teamId),
|
||||
name: team.name,
|
||||
|
||||
@@ -130,12 +130,12 @@ export default class SlideUpPanel extends PureComponent {
|
||||
}).start();
|
||||
}
|
||||
|
||||
closeWithAnimation = () => {
|
||||
closeWithAnimation = (cb) => {
|
||||
Animated.timing(this.translateYOffset, {
|
||||
duration: 200,
|
||||
toValue: this.snapPoints[2],
|
||||
useNativeDriver: true,
|
||||
}).start(() => this.props.onRequestClose());
|
||||
}).start(() => this.props.onRequestClose(cb));
|
||||
};
|
||||
|
||||
onHeaderHandlerStateChange = ({nativeEvent}) => {
|
||||
|
||||
@@ -14,6 +14,10 @@ export default class TextInputWithLocalizedPlaceholder extends PureComponent {
|
||||
placeholder: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
placeholderTextColor: changeOpacity('#000', 0.5),
|
||||
};
|
||||
|
||||
static contextTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
};
|
||||
@@ -39,7 +43,6 @@ export default class TextInputWithLocalizedPlaceholder extends PureComponent {
|
||||
ref='input'
|
||||
{...otherProps}
|
||||
placeholder={placeholderString}
|
||||
placeholderTextColor={changeOpacity('#000', 0.5)}
|
||||
disableFullscreenUI={true}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -20,4 +20,5 @@ export default {
|
||||
IS_IPHONE_X: DeviceInfo.getModel().includes('iPhone X'),
|
||||
IS_TABLET: DeviceInfo.isTablet(),
|
||||
VIDEOS_PATH: `${RNFetchBlobFS.dirs.CacheDir}/Videos`,
|
||||
PERMANENT_SIDEBAR_SETTINGS: '@PERMANENT_SIDEBAR_SETTINGS',
|
||||
};
|
||||
|
||||
@@ -110,6 +110,7 @@ export default {
|
||||
IOSX_TOP_PORTRAIT: 88,
|
||||
STATUS_BAR_HEIGHT: 20,
|
||||
PROFILE_PICTURE_SIZE: 32,
|
||||
PROFILE_PICTURE_EMOJI_SIZE: 28,
|
||||
DATA_SOURCE_USERS: 'users',
|
||||
DATA_SOURCE_CHANNELS: 'channels',
|
||||
NotificationLevels,
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
import 'intl';
|
||||
import {addLocaleData} from 'react-intl';
|
||||
import enLocaleData from 'react-intl/locale-data/en';
|
||||
import moment from 'moment';
|
||||
|
||||
import en from 'assets/i18n/en.json';
|
||||
|
||||
@@ -16,72 +17,92 @@ addLocaleData(enLocaleData);
|
||||
function loadTranslation(locale) {
|
||||
try {
|
||||
let localeData;
|
||||
let momentData;
|
||||
switch (locale) {
|
||||
case 'de':
|
||||
TRANSLATIONS.de = require('assets/i18n/de.json');
|
||||
localeData = require('react-intl/locale-data/de');
|
||||
momentData = require('moment/locale/de');
|
||||
break;
|
||||
case 'es':
|
||||
TRANSLATIONS.es = require('assets/i18n/es.json');
|
||||
localeData = require('react-intl/locale-data/es');
|
||||
momentData = require('moment/locale/es');
|
||||
break;
|
||||
case 'fr':
|
||||
TRANSLATIONS.fr = require('assets/i18n/fr.json');
|
||||
localeData = require('react-intl/locale-data/fr');
|
||||
momentData = require('moment/locale/fr');
|
||||
break;
|
||||
case 'it':
|
||||
TRANSLATIONS.it = require('assets/i18n/it.json');
|
||||
localeData = require('react-intl/locale-data/it');
|
||||
momentData = require('moment/locale/it');
|
||||
break;
|
||||
case 'ja':
|
||||
TRANSLATIONS.ja = require('assets/i18n/ja.json');
|
||||
localeData = require('react-intl/locale-data/ja');
|
||||
momentData = require('moment/locale/ja');
|
||||
break;
|
||||
case 'ko':
|
||||
TRANSLATIONS.ko = require('assets/i18n/ko.json');
|
||||
localeData = require('react-intl/locale-data/ko');
|
||||
momentData = require('moment/locale/ko');
|
||||
break;
|
||||
case 'nl':
|
||||
TRANSLATIONS.nl = require('assets/i18n/nl.json');
|
||||
localeData = require('react-intl/locale-data/nl');
|
||||
momentData = require('moment/locale/nl');
|
||||
break;
|
||||
case 'pl':
|
||||
TRANSLATIONS.pl = require('assets/i18n/pl.json');
|
||||
localeData = require('react-intl/locale-data/pl');
|
||||
momentData = require('moment/locale/pl');
|
||||
break;
|
||||
case 'pt-BR':
|
||||
TRANSLATIONS[locale] = require('assets/i18n/pt-BR.json');
|
||||
localeData = require('react-intl/locale-data/pt');
|
||||
momentData = require('moment/locale/pt-br');
|
||||
break;
|
||||
case 'ro':
|
||||
TRANSLATIONS.ro = require('assets/i18n/ro.json');
|
||||
localeData = require('react-intl/locale-data/ro');
|
||||
momentData = require('moment/locale/ro');
|
||||
break;
|
||||
case 'ru':
|
||||
TRANSLATIONS.ru = require('assets/i18n/ru.json');
|
||||
localeData = require('react-intl/locale-data/ru');
|
||||
momentData = require('moment/locale/ru');
|
||||
break;
|
||||
case 'tr':
|
||||
TRANSLATIONS.tr = require('assets/i18n/tr.json');
|
||||
localeData = require('react-intl/locale-data/tr');
|
||||
momentData = require('moment/locale/tr');
|
||||
break;
|
||||
case 'uk':
|
||||
TRANSLATIONS.tr = require('assets/i18n/uk.json');
|
||||
localeData = require('react-intl/locale-data/uk');
|
||||
momentData = require('moment/locale/uk');
|
||||
break;
|
||||
case 'zh-CN':
|
||||
TRANSLATIONS[locale] = require('assets/i18n/zh-CN.json');
|
||||
localeData = require('react-intl/locale-data/zh');
|
||||
momentData = require('moment/locale/zh-cn');
|
||||
break;
|
||||
case 'zh-TW':
|
||||
TRANSLATIONS[locale] = require('assets/i18n/zh-TW.json');
|
||||
localeData = require('react-intl/locale-data/zh');
|
||||
momentData = require('moment/locale/zh-tw');
|
||||
break;
|
||||
}
|
||||
|
||||
if (localeData) {
|
||||
addLocaleData(localeData);
|
||||
}
|
||||
|
||||
if (momentData) {
|
||||
moment.updateLocale(locale, momentData);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('NO Translation found', e); //eslint-disable-line no-console
|
||||
}
|
||||
|
||||
@@ -27,6 +27,7 @@ export const setAppCredentials = (deviceToken, currentUserId, token, url) => {
|
||||
const username = `${deviceToken}, ${currentUserId}`;
|
||||
|
||||
EphemeralStore.deviceToken = deviceToken;
|
||||
EphemeralStore.currentServerUrl = url;
|
||||
AsyncStorage.setItem(CURRENT_SERVER, url);
|
||||
KeyChain.setInternetCredentials(url, username, token, {accessGroup: mattermostManaged.appGroupIdentifier});
|
||||
} catch (e) {
|
||||
@@ -39,6 +40,7 @@ export const getAppCredentials = async () => {
|
||||
const serverUrl = await AsyncStorage.getItem(CURRENT_SERVER);
|
||||
|
||||
if (serverUrl) {
|
||||
EphemeralStore.currentServerUrl = serverUrl;
|
||||
return getInternetCredentials(serverUrl);
|
||||
}
|
||||
|
||||
@@ -59,6 +61,7 @@ export const removeAppCredentials = async () => {
|
||||
}
|
||||
|
||||
KeyChain.resetGenericPassword();
|
||||
EphemeralStore.currentServerUrl = null;
|
||||
AsyncStorage.removeItem(CURRENT_SERVER);
|
||||
};
|
||||
|
||||
|
||||
13
app/init/device.js
Normal file
13
app/init/device.js
Normal file
@@ -0,0 +1,13 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import AsyncStorage from '@react-native-community/async-storage';
|
||||
import {DeviceTypes} from 'app/constants';
|
||||
|
||||
if (DeviceTypes.IS_TABLET) {
|
||||
AsyncStorage.getItem(DeviceTypes.PERMANENT_SIDEBAR_SETTINGS).then((value) => {
|
||||
if (!value) {
|
||||
AsyncStorage.setItem(DeviceTypes.PERMANENT_SIDEBAR_SETTINGS, 'true');
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -4,7 +4,7 @@
|
||||
import {Alert, Platform} from 'react-native';
|
||||
|
||||
import {handleLoginIdChanged} from 'app/actions/views/login';
|
||||
import {handleServerUrlChanged} from 'app/actions/views/select_server';
|
||||
import {setServerUrl} from 'app/actions/views/select_server';
|
||||
import {getTranslations} from 'app/i18n';
|
||||
import mattermostBucket from 'app/mattermost_bucket';
|
||||
import mattermostManaged from 'app/mattermost_managed';
|
||||
@@ -93,7 +93,7 @@ class EMMProvider {
|
||||
const {dispatch} = store;
|
||||
|
||||
if (LocalConfig.AutoSelectServerUrl) {
|
||||
dispatch(handleServerUrlChanged(LocalConfig.DefaultServerUrl));
|
||||
dispatch(setServerUrl(LocalConfig.DefaultServerUrl));
|
||||
this.allowOtherServers = false;
|
||||
}
|
||||
|
||||
@@ -122,7 +122,7 @@ class EMMProvider {
|
||||
}
|
||||
|
||||
if (this.emmServerUrl) {
|
||||
dispatch(handleServerUrlChanged(this.emmServerUrl));
|
||||
dispatch(setServerUrl(this.emmServerUrl));
|
||||
}
|
||||
|
||||
if (this.emmUsername) {
|
||||
|
||||
@@ -143,8 +143,7 @@ class GlobalEventHandler {
|
||||
deleteFileCache();
|
||||
removeAppCredentials();
|
||||
|
||||
PushNotifications.setApplicationIconBadgeNumber(0);
|
||||
PushNotifications.cancelAllLocalNotifications(); // TODO: Only cancel the notification that belongs to this server
|
||||
PushNotifications.clearNotifications();
|
||||
|
||||
if (this.launchApp) {
|
||||
this.launchApp();
|
||||
@@ -225,8 +224,6 @@ class GlobalEventHandler {
|
||||
|
||||
dispatch(setServerVersion(''));
|
||||
Client4.serverVersion = '';
|
||||
PushNotifications.setApplicationIconBadgeNumber(0);
|
||||
PushNotifications.cancelAllLocalNotifications(); // TODO: Only cancel the notification that belongs to this server
|
||||
|
||||
const credentials = await getAppCredentials();
|
||||
|
||||
|
||||
38
app/init/global_event_handler.test.js
Normal file
38
app/init/global_event_handler.test.js
Normal file
@@ -0,0 +1,38 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import configureMockStore from 'redux-mock-store';
|
||||
import thunk from 'redux-thunk';
|
||||
|
||||
import intitialState from 'app/initial_state';
|
||||
import PushNotification from 'app/push_notifications';
|
||||
|
||||
import GlobalEventHandler from './global_event_handler';
|
||||
|
||||
jest.mock('app/init/credentials', () => ({
|
||||
getCurrentServerUrl: jest.fn().mockResolvedValue(''),
|
||||
getAppCredentials: jest.fn(),
|
||||
removeAppCredentials: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock('react-native-notifications', () => ({
|
||||
addEventListener: jest.fn(),
|
||||
cancelAllLocalNotifications: jest.fn(),
|
||||
setBadgesCount: jest.fn(),
|
||||
NotificationAction: jest.fn(),
|
||||
NotificationCategory: jest.fn(),
|
||||
}));
|
||||
|
||||
const mockStore = configureMockStore([thunk]);
|
||||
const store = mockStore(intitialState);
|
||||
GlobalEventHandler.store = store;
|
||||
|
||||
// TODO: Add Android test as part of https://mattermost.atlassian.net/browse/MM-17110
|
||||
describe('GlobalEventHandler', () => {
|
||||
it('should clear notifications on logout', async () => {
|
||||
const clearNotifications = jest.spyOn(PushNotification, 'clearNotifications');
|
||||
|
||||
await GlobalEventHandler.onLogout();
|
||||
expect(clearNotifications).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@@ -12,6 +12,7 @@ import {setDeepLinkURL} from 'app/actions/views/root';
|
||||
import initialState from 'app/initial_state';
|
||||
import {getAppCredentials} from 'app/init/credentials';
|
||||
import emmProvider from 'app/init/emm_provider';
|
||||
import 'app/init/device';
|
||||
import 'app/init/fetch';
|
||||
import globalEventHandler from 'app/init/global_event_handler';
|
||||
import {registerScreens} from 'app/screens';
|
||||
@@ -87,11 +88,12 @@ const launchAppAndAuthenticateIfNeeded = async () => {
|
||||
Navigation.events().registerAppLaunchedListener(() => {
|
||||
init();
|
||||
|
||||
// Keep track of the latest componentId to appear and disappear
|
||||
// Keep track of the latest componentId to appear
|
||||
Navigation.events().registerComponentDidAppearListener(({componentId}) => {
|
||||
EphemeralStore.addComponentIdToStack(componentId);
|
||||
EphemeralStore.addNavigationComponentId(componentId);
|
||||
});
|
||||
|
||||
Navigation.events().registerComponentDidDisappearListener(({componentId}) => {
|
||||
EphemeralStore.removeComponentIdFromStack(componentId);
|
||||
EphemeralStore.removeNavigationComponentId(componentId);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -113,6 +113,16 @@ class PushNotification {
|
||||
NotificationPreferences.removeDeliveredNotifications(notificationForChannel.identifier, channelId);
|
||||
}
|
||||
}
|
||||
|
||||
clearForegroundNotifications = () => {
|
||||
// TODO: Implement as part of https://mattermost.atlassian.net/browse/MM-17110
|
||||
};
|
||||
|
||||
clearNotifications = () => {
|
||||
this.setApplicationIconBadgeNumber(0);
|
||||
this.cancelAllLocalNotifications(); // TODO: Only cancel the local notifications that belong to this server
|
||||
this.clearForegroundNotifications(); // TODO: Only clear the foreground notifications that belong to this server
|
||||
}
|
||||
}
|
||||
|
||||
export default new PushNotification();
|
||||
|
||||
@@ -2,12 +2,14 @@
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {AppState} from 'react-native';
|
||||
import AsyncStorage from '@react-native-community/async-storage';
|
||||
import NotificationsIOS, {NotificationAction, NotificationCategory} from 'react-native-notifications';
|
||||
|
||||
import ephemeralStore from 'app/store/ephemeral_store';
|
||||
|
||||
const CATEGORY = 'CAN_REPLY';
|
||||
const REPLY_ACTION = 'REPLY_ACTION';
|
||||
export const FOREGROUND_NOTIFICATIONS_KEY = '@FOREGROUND_NOTIFICATIONS';
|
||||
|
||||
let replyCategory;
|
||||
const replies = new Set();
|
||||
@@ -51,6 +53,10 @@ class PushNotification {
|
||||
if (this.onNotification) {
|
||||
this.onNotification(this.deviceNotification);
|
||||
}
|
||||
|
||||
if (foreground) {
|
||||
this.trackForegroundNotification(data.channel_id);
|
||||
}
|
||||
};
|
||||
|
||||
handleReply = (action, completed) => {
|
||||
@@ -96,13 +102,13 @@ class PushNotification {
|
||||
}
|
||||
|
||||
localNotification(notification) {
|
||||
const deviceNotification = {
|
||||
this.deviceNotification = {
|
||||
alertBody: notification.message,
|
||||
alertAction: '',
|
||||
userInfo: notification.userInfo,
|
||||
};
|
||||
|
||||
NotificationsIOS.localNotification(deviceNotification);
|
||||
NotificationsIOS.localNotification(this.deviceNotification);
|
||||
}
|
||||
|
||||
cancelAllLocalNotifications() {
|
||||
@@ -130,6 +136,10 @@ class PushNotification {
|
||||
message: notification.getMessage(),
|
||||
};
|
||||
this.handleNotification(info, true, false);
|
||||
|
||||
NotificationsIOS.getBadgesCount((count) => {
|
||||
this.setApplicationIconBadgeNumber(count + 1);
|
||||
});
|
||||
};
|
||||
|
||||
onNotificationOpened = (notification) => {
|
||||
@@ -160,10 +170,22 @@ class PushNotification {
|
||||
}
|
||||
|
||||
clearChannelNotifications(channelId) {
|
||||
NotificationsIOS.getDeliveredNotifications((notifications) => {
|
||||
const ids = [];
|
||||
let badgeCount = notifications.length;
|
||||
NotificationsIOS.getDeliveredNotifications(async (notifications) => {
|
||||
let foregroundNotifications;
|
||||
try {
|
||||
const value = await AsyncStorage.getItem(FOREGROUND_NOTIFICATIONS_KEY);
|
||||
foregroundNotifications = JSON.parse(value) || {};
|
||||
} catch (e) {
|
||||
foregroundNotifications = {};
|
||||
}
|
||||
|
||||
Reflect.deleteProperty(foregroundNotifications, channelId);
|
||||
AsyncStorage.setItem(FOREGROUND_NOTIFICATIONS_KEY, JSON.stringify(foregroundNotifications));
|
||||
|
||||
const foregroundCount = Object.values(foregroundNotifications).reduce((a, b) => a + b, 0);
|
||||
let badgeCount = notifications.length + foregroundCount;
|
||||
|
||||
const ids = [];
|
||||
for (let i = 0; i < notifications.length; i++) {
|
||||
const notification = notifications[i];
|
||||
|
||||
@@ -180,6 +202,26 @@ class PushNotification {
|
||||
this.setApplicationIconBadgeNumber(badgeCount);
|
||||
});
|
||||
}
|
||||
|
||||
trackForegroundNotification = async (channelId) => {
|
||||
const value = await AsyncStorage.getItem(FOREGROUND_NOTIFICATIONS_KEY);
|
||||
const foregroundNotifications = value ? JSON.parse(value) : {};
|
||||
if (!foregroundNotifications.hasOwnProperty(channelId)) {
|
||||
foregroundNotifications[channelId] = 0;
|
||||
}
|
||||
foregroundNotifications[channelId] += 1;
|
||||
await AsyncStorage.setItem(FOREGROUND_NOTIFICATIONS_KEY, JSON.stringify(foregroundNotifications));
|
||||
}
|
||||
|
||||
clearForegroundNotifications = () => {
|
||||
AsyncStorage.removeItem(FOREGROUND_NOTIFICATIONS_KEY);
|
||||
};
|
||||
|
||||
clearNotifications = () => {
|
||||
this.setApplicationIconBadgeNumber(0);
|
||||
this.cancelAllLocalNotifications(); // TODO: Only cancel the local notifications that belong to this server
|
||||
this.clearForegroundNotifications(); // TODO: Only clear the foreground notifications that belong to this server
|
||||
}
|
||||
}
|
||||
|
||||
export default new PushNotification();
|
||||
|
||||
164
app/push_notifications/push_notifications.ios.test.js
Normal file
164
app/push_notifications/push_notifications.ios.test.js
Normal file
@@ -0,0 +1,164 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import AsyncStorage from '@react-native-community/async-storage';
|
||||
import NotificationsIOS from 'react-native-notifications';
|
||||
import PushNotification, {FOREGROUND_NOTIFICATIONS_KEY} from './push_notifications.ios';
|
||||
|
||||
jest.mock('react-native-notifications', () => {
|
||||
let badgesCount = 0;
|
||||
let deliveredNotifications = {};
|
||||
|
||||
return {
|
||||
getBadgesCount: jest.fn((callback) => callback(badgesCount)),
|
||||
setBadgesCount: jest.fn((count) => {
|
||||
badgesCount = count;
|
||||
}),
|
||||
addEventListener: jest.fn(),
|
||||
setDeliveredNotifications: jest.fn((notifications) => {
|
||||
deliveredNotifications = notifications;
|
||||
}),
|
||||
getDeliveredNotifications: jest.fn(async (callback) => {
|
||||
await callback(deliveredNotifications);
|
||||
}),
|
||||
removeDeliveredNotifications: jest.fn((ids) => {
|
||||
deliveredNotifications = deliveredNotifications.filter((n) => !ids.includes(n.identifier));
|
||||
}),
|
||||
cancelAllLocalNotifications: jest.fn(),
|
||||
NotificationAction: jest.fn(),
|
||||
NotificationCategory: jest.fn(),
|
||||
};
|
||||
});
|
||||
|
||||
describe('PushNotification', () => {
|
||||
const channel1ID = 'channel-1-id';
|
||||
const channel2ID = 'channel-2-id';
|
||||
const notification = {
|
||||
getData: jest.fn(),
|
||||
getMessage: jest.fn(),
|
||||
};
|
||||
|
||||
afterEach(() => AsyncStorage.clear());
|
||||
|
||||
it('should track foreground notifications for channel', async () => {
|
||||
let item = await AsyncStorage.getItem(FOREGROUND_NOTIFICATIONS_KEY);
|
||||
expect(item).toBe(null);
|
||||
|
||||
await PushNotification.trackForegroundNotification(channel1ID);
|
||||
item = await AsyncStorage.getItem(FOREGROUND_NOTIFICATIONS_KEY);
|
||||
expect(item).not.toBe(null);
|
||||
let foregroundNotifications = JSON.parse(item);
|
||||
expect(foregroundNotifications[channel1ID]).toBe(1);
|
||||
expect(foregroundNotifications[channel2ID]).toBe(undefined);
|
||||
|
||||
await PushNotification.trackForegroundNotification(channel1ID);
|
||||
item = await AsyncStorage.getItem(FOREGROUND_NOTIFICATIONS_KEY);
|
||||
expect(item).not.toBe(null);
|
||||
foregroundNotifications = JSON.parse(item);
|
||||
expect(foregroundNotifications[channel1ID]).toBe(2);
|
||||
expect(foregroundNotifications[channel2ID]).toBe(undefined);
|
||||
});
|
||||
|
||||
it('should NOT track foreground notifications for channel when opened', async () => {
|
||||
let item = await AsyncStorage.getItem(FOREGROUND_NOTIFICATIONS_KEY);
|
||||
expect(item).toBe(null);
|
||||
|
||||
PushNotification.trackForegroundNotification = jest.fn();
|
||||
PushNotification.onNotificationOpened(notification);
|
||||
expect(PushNotification.trackForegroundNotification).not.toBeCalled();
|
||||
item = await AsyncStorage.getItem(FOREGROUND_NOTIFICATIONS_KEY);
|
||||
expect(item).toBe(null);
|
||||
});
|
||||
|
||||
it('should increment badge number when foreground notification is received', () => {
|
||||
const setApplicationIconBadgeNumber = jest.spyOn(PushNotification, 'setApplicationIconBadgeNumber');
|
||||
|
||||
NotificationsIOS.getBadgesCount((count) => expect(count).toBe(0));
|
||||
|
||||
PushNotification.onNotificationReceivedForeground(notification);
|
||||
expect(setApplicationIconBadgeNumber).toHaveBeenCalledWith(1);
|
||||
NotificationsIOS.getBadgesCount((count) => expect(count).toBe(1));
|
||||
|
||||
PushNotification.onNotificationReceivedForeground(notification);
|
||||
expect(setApplicationIconBadgeNumber).toHaveBeenCalledWith(2);
|
||||
NotificationsIOS.getBadgesCount((count) => expect(count).toBe(2));
|
||||
});
|
||||
|
||||
it('should clear channel notifications and set correct badge number', async () => {
|
||||
const deliveredNotifications = [
|
||||
|
||||
// Three channel1 delivered notifications
|
||||
{
|
||||
identifier: 'channel1-1',
|
||||
userInfo: {channel_id: channel1ID},
|
||||
},
|
||||
{
|
||||
identifier: 'channel1-2',
|
||||
userInfo: {channel_id: channel1ID},
|
||||
},
|
||||
{
|
||||
identifier: 'channel1-3',
|
||||
userInfo: {channel_id: channel1ID},
|
||||
},
|
||||
|
||||
// Two channel2 delivered notifications
|
||||
{
|
||||
identifier: 'channel2-1',
|
||||
userInfo: {channel_id: channel2ID},
|
||||
},
|
||||
{
|
||||
identifier: 'channel2-2',
|
||||
userInfo: {channel_id: channel2ID},
|
||||
},
|
||||
];
|
||||
NotificationsIOS.setDeliveredNotifications(deliveredNotifications);
|
||||
|
||||
const foregroundNotifications = {
|
||||
[channel1ID]: 1,
|
||||
[channel2ID]: 1,
|
||||
};
|
||||
await AsyncStorage.setItem(FOREGROUND_NOTIFICATIONS_KEY, JSON.stringify(foregroundNotifications));
|
||||
|
||||
const notificationCount = deliveredNotifications.length + Object.values(foregroundNotifications).reduce((a, b) => a + b);
|
||||
expect(notificationCount).toBe(7);
|
||||
|
||||
NotificationsIOS.setBadgesCount(notificationCount);
|
||||
NotificationsIOS.getBadgesCount((count) => expect(count).toBe(notificationCount));
|
||||
|
||||
// Clear channel1 notifications
|
||||
const setApplicationIconBadgeNumber = jest.spyOn(PushNotification, 'setApplicationIconBadgeNumber');
|
||||
await PushNotification.clearChannelNotifications(channel1ID);
|
||||
|
||||
await NotificationsIOS.getDeliveredNotifications(async (deliveredNotifs) => {
|
||||
expect(deliveredNotifs.length).toBe(2);
|
||||
const channel1DeliveredNotifications = deliveredNotifs.filter((n) => n.userInfo.channel_id === channel1ID);
|
||||
const channel2DeliveredNotifications = deliveredNotifs.filter((n) => n.userInfo.channel_id === channel2ID);
|
||||
expect(channel1DeliveredNotifications.length).toBe(0);
|
||||
expect(channel2DeliveredNotifications.length).toBe(2);
|
||||
|
||||
const item = await AsyncStorage.getItem(FOREGROUND_NOTIFICATIONS_KEY);
|
||||
const foregroundNotifs = JSON.parse(item);
|
||||
|
||||
const channel1ForegroundNotifications = foregroundNotifs[channel1ID];
|
||||
const channel2ForegroundNotifications = foregroundNotifs[channel2ID];
|
||||
expect(channel1ForegroundNotifications).toBe(undefined);
|
||||
expect(channel2ForegroundNotifications).toBe(1);
|
||||
|
||||
const badgeNumber = channel2DeliveredNotifications.length + channel2ForegroundNotifications;
|
||||
expect(setApplicationIconBadgeNumber).toHaveBeenCalledWith(badgeNumber);
|
||||
});
|
||||
});
|
||||
|
||||
it('should clear all notifications', () => {
|
||||
const setApplicationIconBadgeNumber = jest.spyOn(PushNotification, 'setApplicationIconBadgeNumber');
|
||||
const cancelAllLocalNotifications = jest.spyOn(PushNotification, 'cancelAllLocalNotifications');
|
||||
const clearForegroundNotifications = jest.spyOn(PushNotification, 'clearForegroundNotifications');
|
||||
|
||||
PushNotification.clearNotifications();
|
||||
expect(setApplicationIconBadgeNumber).toHaveBeenCalledWith(0);
|
||||
expect(NotificationsIOS.setBadgesCount).toHaveBeenCalledWith(0);
|
||||
expect(cancelAllLocalNotifications).toHaveBeenCalled();
|
||||
expect(NotificationsIOS.cancelAllLocalNotifications).toHaveBeenCalled();
|
||||
expect(clearForegroundNotifications).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@@ -169,16 +169,24 @@ export default class ChannelBase extends PureComponent {
|
||||
};
|
||||
|
||||
showTermsOfServiceModal = async () => {
|
||||
const {intl} = this.context;
|
||||
const {actions, theme} = this.props;
|
||||
const closeButton = await MaterialIcon.getImageSource('close', 20, theme.sidebarHeaderTextColor);
|
||||
const screen = 'TermsOfService';
|
||||
const passProps = {
|
||||
closeButton,
|
||||
};
|
||||
const passProps = {closeButton};
|
||||
const title = intl.formatMessage({id: 'mobile.tos_link', defaultMessage: 'Terms of Service'});
|
||||
const options = {
|
||||
layout: {
|
||||
backgroundColor: theme.centerChannelBg,
|
||||
},
|
||||
topBar: {
|
||||
visible: true,
|
||||
height: null,
|
||||
title: {
|
||||
color: theme.sidebarHeaderTextColor,
|
||||
text: title,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
actions.showModalOverCurrentContext(screen, passProps, options);
|
||||
@@ -310,6 +318,6 @@ export const style = StyleSheet.create({
|
||||
flex: 1,
|
||||
},
|
||||
iOSHomeIndicator: {
|
||||
paddingBottom: 5,
|
||||
paddingBottom: 50,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`ChannelNavBar should match, full snapshot 1`] = `
|
||||
<View
|
||||
style={
|
||||
Array [
|
||||
Object {
|
||||
"backgroundColor": "#1153ab",
|
||||
"flexDirection": "row",
|
||||
"justifyContent": "flex-start",
|
||||
"width": "100%",
|
||||
"zIndex": 10,
|
||||
},
|
||||
Object {
|
||||
"paddingHorizontal": 0,
|
||||
},
|
||||
Object {
|
||||
"height": 44,
|
||||
},
|
||||
]
|
||||
}
|
||||
>
|
||||
<Connect(ChannelDrawerButton)
|
||||
openDrawer={[MockFunction]}
|
||||
visible={true}
|
||||
/>
|
||||
<Connect(ChannelTitle)
|
||||
onPress={[MockFunction]}
|
||||
/>
|
||||
<Connect(ChannelSearchButton)
|
||||
theme={
|
||||
Object {
|
||||
"awayIndicator": "#ffbc42",
|
||||
"buttonBg": "#166de0",
|
||||
"buttonColor": "#ffffff",
|
||||
"centerChannelBg": "#ffffff",
|
||||
"centerChannelColor": "#3d3c40",
|
||||
"codeTheme": "github",
|
||||
"dndIndicator": "#f74343",
|
||||
"errorTextColor": "#fd5960",
|
||||
"linkColor": "#2389d7",
|
||||
"mentionBj": "#ffffff",
|
||||
"mentionColor": "#145dbf",
|
||||
"mentionHighlightBg": "#ffe577",
|
||||
"mentionHighlightLink": "#166de0",
|
||||
"newMessageSeparator": "#ff8800",
|
||||
"onlineIndicator": "#06d6a0",
|
||||
"sidebarBg": "#145dbf",
|
||||
"sidebarHeaderBg": "#1153ab",
|
||||
"sidebarHeaderTextColor": "#ffffff",
|
||||
"sidebarText": "#ffffff",
|
||||
"sidebarTextActiveBorder": "#579eff",
|
||||
"sidebarTextActiveColor": "#ffffff",
|
||||
"sidebarTextHoverBg": "#4578bf",
|
||||
"sidebarUnreadText": "#ffffff",
|
||||
"type": "Mattermost",
|
||||
}
|
||||
}
|
||||
/>
|
||||
<Connect(SettingDrawerButton)
|
||||
openDrawer={[MockFunction]}
|
||||
/>
|
||||
</View>
|
||||
`;
|
||||
@@ -4,6 +4,9 @@
|
||||
import React, {PureComponent} from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {Dimensions, Platform, View} from 'react-native';
|
||||
import AsyncStorage from '@react-native-community/async-storage';
|
||||
|
||||
import EventEmitter from 'mattermost-redux/utils/event_emitter';
|
||||
|
||||
import {DeviceTypes, ViewTypes} from 'app/constants';
|
||||
import mattermostManaged from 'app/mattermost_managed';
|
||||
@@ -38,12 +41,15 @@ export default class ChannelNavBar extends PureComponent {
|
||||
componentDidMount() {
|
||||
this.mounted = true;
|
||||
this.handleDimensions();
|
||||
this.handlePermanentSidebar();
|
||||
Dimensions.addEventListener('change', this.handleDimensions);
|
||||
EventEmitter.on(DeviceTypes.PERMANENT_SIDEBAR_SETTINGS, this.handlePermanentSidebar);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this.mounted = false;
|
||||
Dimensions.removeEventListener('change', this.handleDimensions);
|
||||
EventEmitter.off(DeviceTypes.PERMANENT_SIDEBAR_SETTINGS, this.handlePermanentSidebar);
|
||||
}
|
||||
|
||||
handleDimensions = () => {
|
||||
@@ -55,6 +61,14 @@ export default class ChannelNavBar extends PureComponent {
|
||||
}
|
||||
};
|
||||
|
||||
handlePermanentSidebar = () => {
|
||||
if (DeviceTypes.IS_TABLET && this.mounted) {
|
||||
AsyncStorage.getItem(DeviceTypes.PERMANENT_SIDEBAR_SETTINGS).then((enabled) => {
|
||||
this.setState({permanentSidebar: enabled === 'true'});
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
const {isLandscape, onPress, theme} = this.props;
|
||||
const {openChannelDrawer, openSettingsDrawer} = this.props;
|
||||
@@ -84,7 +98,7 @@ export default class ChannelNavBar extends PureComponent {
|
||||
}
|
||||
|
||||
let drawerButtonVisible = false;
|
||||
if (!DeviceTypes.IS_TABLET || this.state.isSplitView) {
|
||||
if (!DeviceTypes.IS_TABLET || this.state.isSplitView || !this.state.permanentSidebar) {
|
||||
drawerButtonVisible = true;
|
||||
}
|
||||
|
||||
|
||||
52
app/screens/channel/channel_nav_bar/channel_nav_bar.test.js
Normal file
52
app/screens/channel/channel_nav_bar/channel_nav_bar.test.js
Normal file
@@ -0,0 +1,52 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
import {shallow} from 'enzyme';
|
||||
|
||||
import Preferences from 'mattermost-redux/constants/preferences';
|
||||
|
||||
import {DeviceTypes} from 'app/constants';
|
||||
|
||||
import ChannelNavBar from './channel_nav_bar';
|
||||
|
||||
jest.mock('react-intl');
|
||||
|
||||
describe('ChannelNavBar', () => {
|
||||
const baseProps = {
|
||||
isLandscape: false,
|
||||
openChannelDrawer: jest.fn(),
|
||||
openSettingsDrawer: jest.fn(),
|
||||
onPress: jest.fn(),
|
||||
theme: Preferences.THEMES.default,
|
||||
};
|
||||
|
||||
test('should match, full snapshot', () => {
|
||||
const wrapper = shallow(
|
||||
<ChannelNavBar {...baseProps}/>
|
||||
);
|
||||
|
||||
expect(wrapper.getElement()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('should not set the permanentSidebar state if not Tablet', () => {
|
||||
const wrapper = shallow(
|
||||
<ChannelNavBar {...baseProps}/>
|
||||
);
|
||||
|
||||
wrapper.instance().handlePermanentSidebar();
|
||||
expect(wrapper.state('permanentSidebar')).toBeUndefined();
|
||||
});
|
||||
|
||||
test('should set the permanentSidebar state if Tablet', async () => {
|
||||
const wrapper = shallow(
|
||||
<ChannelNavBar {...baseProps}/>
|
||||
);
|
||||
|
||||
DeviceTypes.IS_TABLET = true;
|
||||
|
||||
await wrapper.instance().handlePermanentSidebar();
|
||||
|
||||
expect(wrapper.state('permanentSidebar')).toBeDefined();
|
||||
});
|
||||
});
|
||||
@@ -6,7 +6,6 @@ import PropTypes from 'prop-types';
|
||||
import {intlShape} from 'react-intl';
|
||||
import {
|
||||
Alert,
|
||||
Platform,
|
||||
ScrollView,
|
||||
View,
|
||||
} from 'react-native';
|
||||
@@ -113,11 +112,8 @@ export default class ChannelInfo extends PureComponent {
|
||||
if (redirect) {
|
||||
actions.setChannelDisplayName('');
|
||||
}
|
||||
if (Platform.OS === 'android') {
|
||||
actions.dismissModal();
|
||||
} else {
|
||||
actions.popTopScreen();
|
||||
}
|
||||
|
||||
actions.popTopScreen();
|
||||
};
|
||||
|
||||
goToChannelAddMembers = preventDoubleTap(() => {
|
||||
|
||||
@@ -4,15 +4,20 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {
|
||||
Text,
|
||||
View,
|
||||
Clipboard,
|
||||
Platform,
|
||||
Text,
|
||||
TouchableHighlight,
|
||||
View,
|
||||
} from 'react-native';
|
||||
import {intlShape} from 'react-intl';
|
||||
|
||||
import ChannelIcon from 'app/components/channel_icon';
|
||||
import FormattedDate from 'app/components/formatted_date';
|
||||
import FormattedText from 'app/components/formatted_text';
|
||||
import Markdown from 'app/components/markdown';
|
||||
import mattermostManaged from 'app/mattermost_managed';
|
||||
import BottomSheet from 'app/utils/bottom_sheet';
|
||||
import {getMarkdownTextStyles, getMarkdownBlockStyles} from 'app/utils/markdown';
|
||||
import {changeOpacity, makeStyleSheetFromTheme} from 'app/utils/theme';
|
||||
|
||||
@@ -31,8 +36,54 @@ export default class ChannelInfoHeader extends React.PureComponent {
|
||||
isArchived: PropTypes.bool.isRequired,
|
||||
isBot: PropTypes.bool.isRequired,
|
||||
isGroupConstrained: PropTypes.bool,
|
||||
timeZone: PropTypes.string,
|
||||
};
|
||||
|
||||
static contextTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
};
|
||||
|
||||
handleLongPress = (text, actionText) => {
|
||||
const {formatMessage} = this.context.intl;
|
||||
|
||||
const config = mattermostManaged.getCachedConfig();
|
||||
|
||||
if (config?.copyAndPasteProtection !== 'true') {
|
||||
const cancelText = formatMessage({id: 'mobile.post.cancel', defaultMessage: 'Cancel'});
|
||||
|
||||
BottomSheet.showBottomSheetWithOptions({
|
||||
options: [actionText, cancelText],
|
||||
cancelButtonIndex: 1,
|
||||
}, (value) => {
|
||||
if (value === 0) {
|
||||
this.handleCopy(text);
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
handleCopy = (text) => {
|
||||
Clipboard.setString(text);
|
||||
}
|
||||
|
||||
handleHeaderLongPress = () => {
|
||||
const {formatMessage} = this.context.intl;
|
||||
const {header} = this.props;
|
||||
this.handleLongPress(
|
||||
header,
|
||||
formatMessage({id: 'mobile.channel_info.copy_header', defaultMessage: 'Copy Header'})
|
||||
);
|
||||
}
|
||||
|
||||
handlePurposeLongPress = () => {
|
||||
const {formatMessage} = this.context.intl;
|
||||
const {purpose} = this.props;
|
||||
this.handleLongPress(
|
||||
purpose,
|
||||
formatMessage({id: 'mobile.channel_info.copy_purpose', defaultMessage: 'Copy Purpose'})
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
createAt,
|
||||
@@ -48,6 +99,7 @@ export default class ChannelInfoHeader extends React.PureComponent {
|
||||
isArchived,
|
||||
isBot,
|
||||
isGroupConstrained,
|
||||
timeZone,
|
||||
} = this.props;
|
||||
|
||||
const style = getStyleSheet(theme);
|
||||
@@ -59,7 +111,7 @@ export default class ChannelInfoHeader extends React.PureComponent {
|
||||
|
||||
return (
|
||||
<View style={style.container}>
|
||||
<View style={style.channelNameContainer}>
|
||||
<View style={[style.channelNameContainer, style.row]}>
|
||||
<ChannelIcon
|
||||
isInfo={true}
|
||||
membersCount={memberCount - 1}
|
||||
@@ -79,61 +131,74 @@ export default class ChannelInfoHeader extends React.PureComponent {
|
||||
</Text>
|
||||
</View>
|
||||
{purpose.length > 0 &&
|
||||
<View style={style.section}>
|
||||
<FormattedText
|
||||
style={style.header}
|
||||
id='channel_info.purpose'
|
||||
defaultMessage='Purpose'
|
||||
/>
|
||||
<Markdown
|
||||
onPermalinkPress={onPermalinkPress}
|
||||
baseTextStyle={baseTextStyle}
|
||||
textStyles={textStyles}
|
||||
blockStyles={blockStyles}
|
||||
value={purpose}
|
||||
/>
|
||||
</View>
|
||||
<View style={style.section}>
|
||||
<TouchableHighlight
|
||||
underlayColor={changeOpacity(theme.centerChannelColor, 0.1)}
|
||||
onLongPress={this.handlePurposeLongPress}
|
||||
>
|
||||
<View style={style.row}>
|
||||
<FormattedText
|
||||
style={style.header}
|
||||
id='channel_info.purpose'
|
||||
defaultMessage='Purpose'
|
||||
/>
|
||||
<Markdown
|
||||
onPermalinkPress={onPermalinkPress}
|
||||
baseTextStyle={baseTextStyle}
|
||||
textStyles={textStyles}
|
||||
blockStyles={blockStyles}
|
||||
value={purpose}
|
||||
/>
|
||||
</View>
|
||||
</TouchableHighlight>
|
||||
</View>
|
||||
}
|
||||
{header.length > 0 &&
|
||||
<View style={style.section}>
|
||||
<FormattedText
|
||||
style={style.header}
|
||||
id='channel_info.header'
|
||||
defaultMessage='Header'
|
||||
/>
|
||||
<Markdown
|
||||
onPermalinkPress={onPermalinkPress}
|
||||
baseTextStyle={baseTextStyle}
|
||||
textStyles={textStyles}
|
||||
blockStyles={blockStyles}
|
||||
value={header}
|
||||
/>
|
||||
</View>
|
||||
<View style={style.section}>
|
||||
<TouchableHighlight
|
||||
underlayColor={changeOpacity(theme.centerChannelColor, 0.1)}
|
||||
onLongPress={this.handleHeaderLongPress}
|
||||
>
|
||||
<View style={style.row}>
|
||||
<FormattedText
|
||||
style={style.header}
|
||||
id='channel_info.header'
|
||||
defaultMessage='Header'
|
||||
/>
|
||||
<Markdown
|
||||
onPermalinkPress={onPermalinkPress}
|
||||
baseTextStyle={baseTextStyle}
|
||||
textStyles={textStyles}
|
||||
blockStyles={blockStyles}
|
||||
value={header}
|
||||
/>
|
||||
</View>
|
||||
</TouchableHighlight>
|
||||
</View>
|
||||
}
|
||||
{isGroupConstrained &&
|
||||
<Text style={style.createdBy}>
|
||||
<FormattedText
|
||||
id='mobile.routes.channelInfo.groupManaged'
|
||||
defaultMessage='Members are managed by linked groups'
|
||||
/>
|
||||
</Text>
|
||||
<Text style={[style.createdBy, style.row]}>
|
||||
<FormattedText
|
||||
id='mobile.routes.channelInfo.groupManaged'
|
||||
defaultMessage='Members are managed by linked groups'
|
||||
/>
|
||||
</Text>
|
||||
}
|
||||
{creator &&
|
||||
<Text style={style.createdBy}>
|
||||
<FormattedText
|
||||
id='mobile.routes.channelInfo.createdBy'
|
||||
defaultMessage='Created by {creator} on '
|
||||
values={{
|
||||
creator,
|
||||
}}
|
||||
/>
|
||||
<FormattedDate
|
||||
value={new Date(createAt)}
|
||||
year='numeric'
|
||||
month='long'
|
||||
day='2-digit'
|
||||
/>
|
||||
</Text>
|
||||
<Text style={[style.createdBy, style.row]}>
|
||||
<FormattedText
|
||||
id='mobile.routes.channelInfo.createdBy'
|
||||
defaultMessage='Created by {creator} on '
|
||||
values={{
|
||||
creator,
|
||||
}}
|
||||
/>
|
||||
<FormattedDate
|
||||
format='LL'
|
||||
timeZone={timeZone}
|
||||
value={createAt}
|
||||
/>
|
||||
</Text>
|
||||
}
|
||||
</View>
|
||||
);
|
||||
@@ -145,7 +210,7 @@ const getStyleSheet = makeStyleSheetFromTheme((theme) => {
|
||||
container: {
|
||||
backgroundColor: theme.centerChannelBg,
|
||||
marginBottom: 40,
|
||||
padding: 15,
|
||||
paddingVertical: 15,
|
||||
borderBottomWidth: 1,
|
||||
borderBottomColor: changeOpacity(theme.centerChannelColor, 0.1),
|
||||
},
|
||||
@@ -180,5 +245,8 @@ const getStyleSheet = makeStyleSheetFromTheme((theme) => {
|
||||
section: {
|
||||
marginTop: 15,
|
||||
},
|
||||
row: {
|
||||
paddingHorizontal: 15,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
@@ -29,6 +29,8 @@ import {getCurrentUserId, getUser, getStatusForUserId, getCurrentUserRoles} from
|
||||
import {areChannelMentionsIgnored, getUserIdFromChannelName, isChannelMuted, showDeleteOption, showManagementOptions} from 'mattermost-redux/utils/channel_utils';
|
||||
import {isAdmin as checkIsAdmin, isChannelAdmin as checkIsChannelAdmin, isSystemAdmin as checkIsSystemAdmin} from 'mattermost-redux/utils/user_utils';
|
||||
import {getConfig, getLicense} from 'mattermost-redux/selectors/entities/general';
|
||||
import {isTimezoneEnabled} from 'mattermost-redux/selectors/entities/timezone';
|
||||
import {getUserCurrentTimezone} from 'mattermost-redux/utils/timezone_utils';
|
||||
|
||||
import {
|
||||
popTopScreen,
|
||||
@@ -45,6 +47,7 @@ import {
|
||||
selectPenultimateChannel,
|
||||
setChannelDisplayName,
|
||||
} from 'app/actions/views/channel';
|
||||
import {isLandscape} from 'app/selectors/device';
|
||||
|
||||
import ChannelInfo from './channel_info';
|
||||
|
||||
@@ -87,6 +90,12 @@ function mapStateToProps(state) {
|
||||
const canEditChannel = !channelIsReadOnly && showManagementOptions(state, config, license, currentChannel, isAdmin, isSystemAdmin, isChannelAdmin);
|
||||
const viewArchivedChannels = config.ExperimentalViewArchivedChannels === 'true';
|
||||
|
||||
const enableTimezone = isTimezoneEnabled(state);
|
||||
let timeZone = null;
|
||||
if (enableTimezone) {
|
||||
timeZone = getUserCurrentTimezone(currentUser.timezone);
|
||||
}
|
||||
|
||||
return {
|
||||
canDeleteChannel: showDeleteOption(state, config, license, currentChannel, isAdmin, isSystemAdmin, isChannelAdmin),
|
||||
viewArchivedChannels,
|
||||
@@ -103,6 +112,8 @@ function mapStateToProps(state) {
|
||||
theme: getTheme(state),
|
||||
canManageUsers,
|
||||
isBot,
|
||||
isLandscape: isLandscape(state),
|
||||
timeZone,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -63,20 +63,11 @@ export default class CreateChannel extends PureComponent {
|
||||
};
|
||||
|
||||
this.rightButton.text = context.intl.formatMessage({id: 'mobile.create_channel', defaultMessage: 'Create'});
|
||||
this.rightButton.color = props.theme.sidebarHeaderTextColor;
|
||||
|
||||
if (props.closeButton) {
|
||||
this.left = {...this.leftButton, icon: props.closeButton};
|
||||
}
|
||||
|
||||
const buttons = {
|
||||
rightButtons: [this.rightButton],
|
||||
};
|
||||
|
||||
if (this.left) {
|
||||
buttons.leftButtons = [this.left];
|
||||
}
|
||||
|
||||
props.actions.setButtons(props.componentId, buttons);
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
|
||||
@@ -99,6 +99,7 @@ export default class EditChannel extends PureComponent {
|
||||
header,
|
||||
};
|
||||
|
||||
this.rightButton.color = props.theme.sidebarHeaderTextColor;
|
||||
this.rightButton.text = context.intl.formatMessage({id: 'mobile.edit_channel', defaultMessage: 'Save'});
|
||||
|
||||
const buttons = {
|
||||
|
||||
@@ -51,6 +51,7 @@ export default class EditPost extends PureComponent {
|
||||
super(props);
|
||||
|
||||
this.state = {message: props.post.message};
|
||||
this.rightButton.color = props.theme.sidebarHeaderTextColor;
|
||||
this.rightButton.text = context.intl.formatMessage({id: 'edit_post.save', defaultMessage: 'Save'});
|
||||
|
||||
props.actions.setButtons(props.componentId, {
|
||||
|
||||
@@ -119,6 +119,7 @@ export default class EditProfile extends PureComponent {
|
||||
const buttons = {
|
||||
rightButtons: [this.rightButton],
|
||||
};
|
||||
this.rightButton.color = props.theme.sidebarHeaderTextColor;
|
||||
this.rightButton.text = context.intl.formatMessage({id: t('mobile.account.settings.save'), defaultMessage: 'Save'});
|
||||
|
||||
props.actions.setButtons(props.componentId, buttons);
|
||||
|
||||
@@ -28,7 +28,7 @@ export function registerScreens(store, Provider) {
|
||||
Navigation.registerComponent('ChannelMembers', () => wrapper(require('app/screens/channel_members').default), () => require('app/screens/channel_members').default);
|
||||
Navigation.registerComponent('ChannelPeek', () => wrapper(require('app/screens/channel_peek').default), () => require('app/screens/channel_peek').default);
|
||||
Navigation.registerComponent('ClientUpgrade', () => wrapper(require('app/screens/client_upgrade').default), () => require('app/screens/client_upgrade').default);
|
||||
Navigation.registerComponent('ClockDisplay', () => wrapper(require('app/screens/clock_display').default), () => require('app/screens/clock_display').default);
|
||||
Navigation.registerComponent('ClockDisplaySettings', () => wrapper(require('app/screens/settings/clock_display').default), () => require('app/screens/settings/clock_display').default);
|
||||
Navigation.registerComponent('Code', () => wrapper(require('app/screens/code').default), () => require('app/screens/code').default);
|
||||
Navigation.registerComponent('CreateChannel', () => wrapper(require('app/screens/create_channel').default), () => require('app/screens/create_channel').default);
|
||||
Navigation.registerComponent('DisplaySettings', () => wrapper(require('app/screens/settings/display_settings').default), () => require('app/screens/settings/display_settings').default);
|
||||
@@ -64,16 +64,17 @@ export function registerScreens(store, Provider) {
|
||||
Navigation.registerComponent('SelectorScreen', () => wrapper(require('app/screens/selector_screen').default), () => require('app/screens/selector_screen').default);
|
||||
Navigation.registerComponent('SelectServer', () => wrapper(SelectServer), () => SelectServer);
|
||||
Navigation.registerComponent('SelectTeam', () => wrapper(require('app/screens/select_team').default), () => require('app/screens/select_team').default);
|
||||
Navigation.registerComponent('SelectTimezone', () => wrapper(require('app/screens/timezone/select_timezone').default), () => require('app/screens/timezone/select_timezone').default);
|
||||
Navigation.registerComponent('SelectTimezone', () => wrapper(require('app/screens/settings/timezone/select_timezone').default), () => require('app/screens/settings/timezone/select_timezone').default);
|
||||
Navigation.registerComponent('Settings', () => wrapper(require('app/screens/settings/general').default), () => require('app/screens/settings/general').default);
|
||||
Navigation.registerComponent('SidebarSettings', () => wrapper(require('app/screens/settings/sidebar').default), () => require('app/screens/settings/sidebar').default);
|
||||
Navigation.registerComponent('SSO', () => wrapper(require('app/screens/sso').default), () => require('app/screens/sso').default);
|
||||
Navigation.registerComponent('Table', () => wrapper(require('app/screens/table').default), () => require('app/screens/table').default);
|
||||
Navigation.registerComponent('TableImage', () => wrapper(require('app/screens/table_image').default), () => require('app/screens/table_image').default);
|
||||
Navigation.registerComponent('TermsOfService', () => wrapper(require('app/screens/terms_of_service').default), () => require('app/screens/terms_of_service').default);
|
||||
Navigation.registerComponent('TextPreview', () => wrapper(require('app/screens/text_preview').default), () => require('app/screens/text_preview').default);
|
||||
Navigation.registerComponent('ThemeSettings', () => wrapper(require('app/screens/theme').default), () => require('app/screens/theme').default);
|
||||
Navigation.registerComponent('ThemeSettings', () => wrapper(require('app/screens/settings/theme').default), () => require('app/screens/settings/theme').default);
|
||||
Navigation.registerComponent('Thread', () => wrapper(require('app/screens/thread').default), () => require('app/screens/thread').default);
|
||||
Navigation.registerComponent('TimezoneSettings', () => wrapper(require('app/screens/timezone').default), () => require('app/screens/timezone').default);
|
||||
Navigation.registerComponent('TimezoneSettings', () => wrapper(require('app/screens/settings/timezone').default), () => require('app/screens/settings/timezone').default);
|
||||
Navigation.registerComponent('ErrorTeamsList', () => wrapper(require('app/screens/error_teams_list').default), () => require('app/screens/error_teams_list').default);
|
||||
Navigation.registerComponent('UserProfile', () => wrapper(require('app/screens/user_profile').default), () => require('app/screens/user_profile').default);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,7 +19,7 @@ export default class InteractiveDialog extends PureComponent {
|
||||
static propTypes = {
|
||||
url: PropTypes.string.isRequired,
|
||||
callbackId: PropTypes.string,
|
||||
elements: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
elements: PropTypes.arrayOf(PropTypes.object),
|
||||
notifyOnCancel: PropTypes.bool,
|
||||
state: PropTypes.string,
|
||||
theme: PropTypes.object,
|
||||
@@ -38,9 +38,11 @@ export default class InteractiveDialog extends PureComponent {
|
||||
super(props);
|
||||
|
||||
const values = {};
|
||||
props.elements.forEach((e) => {
|
||||
values[e.name] = e.default || null;
|
||||
});
|
||||
if (props.elements != null) {
|
||||
props.elements.forEach((e) => {
|
||||
values[e.name] = e.default || null;
|
||||
});
|
||||
}
|
||||
|
||||
this.state = {
|
||||
values,
|
||||
@@ -69,18 +71,21 @@ export default class InteractiveDialog extends PureComponent {
|
||||
const {elements} = this.props;
|
||||
const values = this.state.values;
|
||||
const errors = {};
|
||||
elements.forEach((elem) => {
|
||||
const error = checkDialogElementForError(elem, values[elem.name]);
|
||||
if (error) {
|
||||
errors[elem.name] = (
|
||||
<FormattedText
|
||||
id={error.id}
|
||||
defaultMessage={error.defaultMessage}
|
||||
values={error.values}
|
||||
/>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
if (elements) {
|
||||
elements.forEach((elem) => {
|
||||
const error = checkDialogElementForError(elem, values[elem.name]);
|
||||
if (error) {
|
||||
errors[elem.name] = (
|
||||
<FormattedText
|
||||
id={error.id}
|
||||
defaultMessage={error.defaultMessage}
|
||||
values={error.values}
|
||||
/>
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
this.setState({errors});
|
||||
|
||||
@@ -152,7 +157,7 @@ export default class InteractiveDialog extends PureComponent {
|
||||
<View style={style.container}>
|
||||
<ScrollView style={style.scrollView}>
|
||||
<StatusBar/>
|
||||
{elements.map((e) => {
|
||||
{elements && elements.map((e) => {
|
||||
return (
|
||||
<DialogElement
|
||||
key={'dialogelement' + e.name}
|
||||
@@ -190,4 +195,4 @@ const getStyleFromTheme = makeStyleSheetFromTheme((theme) => {
|
||||
marginTop: 10,
|
||||
},
|
||||
};
|
||||
});
|
||||
});
|
||||
|
||||
@@ -84,4 +84,26 @@ describe('InteractiveDialog', () => {
|
||||
wrapper.instance().navigationButtonPressed({buttonId: 'close-dialog'});
|
||||
expect(baseProps.actions.submitInteractiveDialog).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('should handle display with no elements', async () => {
|
||||
baseProps.elements = null;
|
||||
|
||||
const wrapper = shallow(
|
||||
<InteractiveDialog
|
||||
{...baseProps}
|
||||
notifyOnCancel={false}
|
||||
/>,
|
||||
);
|
||||
|
||||
const dialog = {
|
||||
url: baseProps.url,
|
||||
callback_id: baseProps.callbackId,
|
||||
state: baseProps.state,
|
||||
submission: {},
|
||||
};
|
||||
|
||||
wrapper.instance().handleSubmit();
|
||||
expect(baseProps.actions.submitInteractiveDialog).toHaveBeenCalledTimes(1);
|
||||
expect(baseProps.actions.submitInteractiveDialog).toHaveBeenCalledWith(dialog);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -75,21 +75,12 @@ export default class MoreChannels extends PureComponent {
|
||||
id: 'close-more-channels',
|
||||
icon: props.closeButton,
|
||||
};
|
||||
|
||||
const buttons = {
|
||||
leftButtons: [this.leftButton],
|
||||
};
|
||||
|
||||
if (props.canCreateChannels) {
|
||||
buttons.rightButtons = [this.rightButton];
|
||||
}
|
||||
|
||||
props.actions.setButtons(props.componentId, buttons);
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.navigationEventListener = Navigation.events().bindComponent(this);
|
||||
this.mounted = true;
|
||||
this.setHeaderButtons(this.props.canCreateChannels);
|
||||
this.doGetChannels();
|
||||
}
|
||||
|
||||
@@ -165,7 +156,7 @@ export default class MoreChannels extends PureComponent {
|
||||
|
||||
getChannels = debounce(this.doGetChannels, 100);
|
||||
|
||||
headerButtons = (createEnabled) => {
|
||||
setHeaderButtons = (createEnabled) => {
|
||||
const {actions, canCreateChannels, componentId} = this.props;
|
||||
const buttons = {
|
||||
leftButtons: [this.leftButton],
|
||||
@@ -194,7 +185,7 @@ export default class MoreChannels extends PureComponent {
|
||||
const {actions, currentTeamId, currentUserId} = this.props;
|
||||
const {channels} = this.state;
|
||||
|
||||
this.headerButtons(false);
|
||||
this.setHeaderButtons(false);
|
||||
this.setState({adding: true});
|
||||
|
||||
const channel = channels.find((c) => c.id === id);
|
||||
@@ -212,7 +203,7 @@ export default class MoreChannels extends PureComponent {
|
||||
displayName: channel ? channel.display_name : '',
|
||||
}
|
||||
);
|
||||
this.headerButtons(true);
|
||||
this.setHeaderButtons(true);
|
||||
this.setState({adding: false});
|
||||
} else {
|
||||
if (channel) {
|
||||
|
||||
@@ -52,14 +52,14 @@ describe('MoreChannels', () => {
|
||||
expect(baseProps.actions.dismissModal).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
test('should call props.actions.setButtons on headerButtons', () => {
|
||||
test('should call props.actions.setButtons on setHeaderButtons', () => {
|
||||
const wrapper = shallow(
|
||||
<MoreChannels {...baseProps}/>,
|
||||
{context: {intl: {formatMessage: jest.fn()}}},
|
||||
);
|
||||
|
||||
expect(baseProps.actions.setButtons).toHaveBeenCalledTimes(1);
|
||||
wrapper.instance().headerButtons(true);
|
||||
wrapper.instance().setHeaderButtons(true);
|
||||
expect(baseProps.actions.setButtons).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
|
||||
@@ -73,13 +73,12 @@ export default class MoreDirectMessages extends PureComponent {
|
||||
selectedIds: {},
|
||||
selectedCount: 0,
|
||||
};
|
||||
|
||||
this.updateNavigationButtons(false, context);
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.navigationEventListener = Navigation.events().bindComponent(this);
|
||||
this.mounted = true;
|
||||
this.updateNavigationButtons(false);
|
||||
|
||||
this.getProfiles();
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@ import {Navigation} from 'react-native-navigation';
|
||||
import * as Animatable from 'react-native-animatable';
|
||||
import {PanGestureHandler} from 'react-native-gesture-handler';
|
||||
|
||||
import {Client4} from 'mattermost-redux/client';
|
||||
import {isDirectChannel} from 'mattermost-redux/utils/channel_utils';
|
||||
import EventEmitter from 'mattermost-redux/utils/event_emitter';
|
||||
import {displayUsername} from 'mattermost-redux/utils/user_utils';
|
||||
@@ -167,7 +168,8 @@ export default class Notification extends PureComponent {
|
||||
);
|
||||
|
||||
if (data.from_webhook && config.EnablePostIconOverride === 'true' && data.use_user_icon !== 'true') {
|
||||
const wsIcon = data.override_icon_url ? {uri: data.override_icon_url} : webhookIcon;
|
||||
const overrideIconURL = Client4.getAbsoluteUrl(data.override_icon_url); // eslint-disable-line camelcase
|
||||
const wsIcon = data.override_icon_url ? {uri: overrideIconURL} : webhookIcon;
|
||||
icon = (
|
||||
<Image
|
||||
source={wsIcon}
|
||||
|
||||
@@ -5,6 +5,7 @@ import React, {PureComponent} from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {
|
||||
Animated,
|
||||
Platform,
|
||||
StyleSheet,
|
||||
TouchableWithoutFeedback,
|
||||
View,
|
||||
@@ -74,7 +75,11 @@ export default class OptionsModal extends PureComponent {
|
||||
};
|
||||
|
||||
onItemPress = () => {
|
||||
this.close();
|
||||
if (Platform.OS === 'android') {
|
||||
this.close();
|
||||
} else {
|
||||
this.props.actions.dismissModal();
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
|
||||
@@ -267,41 +267,43 @@ export default class Permalink extends PureComponent {
|
||||
const {formatMessage} = intl;
|
||||
let focusChannelId = channelId;
|
||||
|
||||
const post = await actions.getPostThread(focusedPostId, false);
|
||||
if (post.error && (!postIds || !postIds.length)) {
|
||||
if (this.mounted && isPermalink && post.error.message.toLowerCase() !== 'network request failed') {
|
||||
this.setState({
|
||||
error: formatMessage({
|
||||
id: 'permalink.error.access',
|
||||
defaultMessage: 'Permalink belongs to a deleted message or to a channel to which you do not have access.',
|
||||
}),
|
||||
title: formatMessage({
|
||||
id: 'mobile.search.no_results',
|
||||
defaultMessage: 'No Results Found',
|
||||
}),
|
||||
});
|
||||
} else if (this.mounted) {
|
||||
this.setState({error: post.error.message, retry: true});
|
||||
if (focusedPostId) {
|
||||
const post = await actions.getPostThread(focusedPostId, false);
|
||||
if (post.error && (!postIds || !postIds.length)) {
|
||||
if (this.mounted && isPermalink && post.error.message.toLowerCase() !== 'network request failed') {
|
||||
this.setState({
|
||||
error: formatMessage({
|
||||
id: 'permalink.error.access',
|
||||
defaultMessage: 'Permalink belongs to a deleted message or to a channel to which you do not have access.',
|
||||
}),
|
||||
title: formatMessage({
|
||||
id: 'mobile.search.no_results',
|
||||
defaultMessage: 'No Results Found',
|
||||
}),
|
||||
});
|
||||
} else if (this.mounted) {
|
||||
this.setState({error: post.error.message, retry: true});
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (!channelId) {
|
||||
const focusedPost = post.data && post.data.posts ? post.data.posts[focusedPostId] : null;
|
||||
focusChannelId = focusedPost ? focusedPost.channel_id : '';
|
||||
if (focusChannelId) {
|
||||
const {data: channel} = await actions.getChannel(focusChannelId);
|
||||
if (!this.props.myMembers[focusChannelId] && channel && channel.type === General.OPEN_CHANNEL) {
|
||||
await actions.joinChannel(currentUserId, channel.team_id, channel.id);
|
||||
if (!channelId) {
|
||||
const focusedPost = post.data && post.data.posts ? post.data.posts[focusedPostId] : null;
|
||||
focusChannelId = focusedPost ? focusedPost.channel_id : '';
|
||||
if (focusChannelId) {
|
||||
const {data: channel} = await actions.getChannel(focusChannelId);
|
||||
if (!this.props.myMembers[focusChannelId] && channel && channel.type === General.OPEN_CHANNEL) {
|
||||
await actions.joinChannel(currentUserId, channel.team_id, channel.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
await actions.getPostsAround(focusChannelId, focusedPostId, 10);
|
||||
await actions.getPostsAround(focusChannelId, focusedPostId, 10);
|
||||
|
||||
if (this.mounted) {
|
||||
this.setState({loading: false});
|
||||
if (this.mounted) {
|
||||
this.setState({loading: false});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -49,15 +49,19 @@ export default class PostOptions extends PureComponent {
|
||||
intl: intlShape.isRequired,
|
||||
};
|
||||
|
||||
close = () => {
|
||||
this.props.actions.dismissModal();
|
||||
close = async (cb) => {
|
||||
await this.props.actions.dismissModal();
|
||||
|
||||
if (typeof cb === 'function') {
|
||||
setTimeout(cb, 300);
|
||||
}
|
||||
};
|
||||
|
||||
closeWithAnimation = () => {
|
||||
closeWithAnimation = (cb) => {
|
||||
if (this.slideUpPanel) {
|
||||
this.slideUpPanel.closeWithAnimation();
|
||||
this.slideUpPanel.closeWithAnimation(cb);
|
||||
} else {
|
||||
this.close();
|
||||
this.close(cb);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -268,8 +272,7 @@ export default class PostOptions extends PureComponent {
|
||||
const {actions, theme} = this.props;
|
||||
const {formatMessage} = this.context.intl;
|
||||
|
||||
this.close();
|
||||
setTimeout(() => {
|
||||
this.close(() => {
|
||||
MaterialIcon.getImageSource('close', 20, theme.sidebarHeaderTextColor).then((source) => {
|
||||
const screen = 'AddReaction';
|
||||
const title = formatMessage({id: 'mobile.post_info.add_reaction', defaultMessage: 'Add Reaction'});
|
||||
@@ -280,15 +283,14 @@ export default class PostOptions extends PureComponent {
|
||||
|
||||
actions.showModal(screen, title, passProps);
|
||||
});
|
||||
}, 300);
|
||||
});
|
||||
};
|
||||
|
||||
handleReply = () => {
|
||||
const {post} = this.props;
|
||||
this.closeWithAnimation();
|
||||
setTimeout(() => {
|
||||
this.closeWithAnimation(() => {
|
||||
EventEmitter.emit('goToThread', post);
|
||||
}, 250);
|
||||
});
|
||||
};
|
||||
|
||||
handleAddReactionToPost = (emoji) => {
|
||||
@@ -347,9 +349,10 @@ export default class PostOptions extends PureComponent {
|
||||
text: formatMessage({id: 'post_info.del', defaultMessage: 'Delete'}),
|
||||
style: 'destructive',
|
||||
onPress: () => {
|
||||
actions.deletePost(post);
|
||||
actions.removePost(post);
|
||||
this.closeWithAnimation();
|
||||
this.closeWithAnimation(() => {
|
||||
actions.deletePost(post);
|
||||
actions.removePost(post);
|
||||
});
|
||||
},
|
||||
}]
|
||||
);
|
||||
@@ -359,8 +362,7 @@ export default class PostOptions extends PureComponent {
|
||||
const {actions, theme, post} = this.props;
|
||||
const {intl} = this.context;
|
||||
|
||||
this.close();
|
||||
setTimeout(() => {
|
||||
this.close(() => {
|
||||
MaterialIcon.getImageSource('close', 20, theme.sidebarHeaderTextColor).then((source) => {
|
||||
const screen = 'EditPost';
|
||||
const title = intl.formatMessage({id: 'mobile.edit_post.title', defaultMessage: 'Editing Message'});
|
||||
@@ -371,7 +373,7 @@ export default class PostOptions extends PureComponent {
|
||||
|
||||
actions.showModal(screen, title, passProps);
|
||||
});
|
||||
}, 300);
|
||||
});
|
||||
};
|
||||
|
||||
handleUnflagPost = () => {
|
||||
|
||||
@@ -110,8 +110,14 @@ describe('PostOptions', () => {
|
||||
expect(Alert.alert).toBeCalled();
|
||||
|
||||
// Trigger on press of Delete in the Alert
|
||||
const closeWithAnimation = jest.spyOn(wrapper.instance(), 'closeWithAnimation');
|
||||
Alert.alert.mock.calls[0][2][1].onPress();
|
||||
expect(closeWithAnimation).toBeCalled();
|
||||
|
||||
// get the callback that gets called by closeWithAnimation
|
||||
const callback = closeWithAnimation.mock.calls[0][0];
|
||||
|
||||
callback();
|
||||
expect(actions.deletePost).toBeCalled();
|
||||
expect(actions.removePost).toBeCalled();
|
||||
});
|
||||
|
||||
@@ -107,15 +107,15 @@ export default class SelectServer extends PureComponent {
|
||||
if (this.state.connected && this.props.hasConfigAndLicense && !(prevState.connected && prevProps.hasConfigAndLicense)) {
|
||||
if (LocalConfig.EnableMobileClientUpgrade) {
|
||||
this.props.actions.setLastUpgradeCheck();
|
||||
const {currentVersion, minVersion, latestVersion} = prevProps;
|
||||
const {currentVersion, minVersion, latestVersion} = this.props;
|
||||
const upgradeType = checkUpgradeType(currentVersion, minVersion, latestVersion);
|
||||
if (isUpgradeAvailable(upgradeType)) {
|
||||
this.handleShowClientUpgrade(upgradeType);
|
||||
} else {
|
||||
this.handleLoginOptions(prevProps);
|
||||
this.handleLoginOptions(this.props);
|
||||
}
|
||||
} else {
|
||||
this.handleLoginOptions(prevProps);
|
||||
this.handleLoginOptions(this.props);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,6 +28,7 @@ import Config from 'assets/config';
|
||||
class AdvancedSettings extends PureComponent {
|
||||
static propTypes = {
|
||||
actions: PropTypes.shape({
|
||||
dismissAllModals: PropTypes.func.isRequired,
|
||||
purgeOfflineStore: PropTypes.func.isRequired,
|
||||
}).isRequired,
|
||||
intl: intlShape.isRequired,
|
||||
@@ -73,6 +74,10 @@ class AdvancedSettings extends PureComponent {
|
||||
await deleteFileCache();
|
||||
this.setState({cacheSize: 0, cacheSizedFetched: true});
|
||||
actions.purgeOfflineStore();
|
||||
|
||||
if (Platform.OS === 'android') {
|
||||
actions.dismissAllModals();
|
||||
}
|
||||
});
|
||||
|
||||
renderCacheFileSize = () => {
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
import {bindActionCreators} from 'redux';
|
||||
import {connect} from 'react-redux';
|
||||
|
||||
import {dismissAllModals} from 'app/actions/navigation';
|
||||
import {purgeOfflineStore} from 'app/actions/views/root';
|
||||
import {getTheme} from 'mattermost-redux/selectors/entities/preferences';
|
||||
|
||||
@@ -18,6 +19,7 @@ function mapStateToProps(state) {
|
||||
function mapDispatchToProps(dispatch) {
|
||||
return {
|
||||
actions: bindActionCreators({
|
||||
dismissAllModals,
|
||||
purgeOfflineStore,
|
||||
}, dispatch),
|
||||
};
|
||||
|
||||
@@ -19,7 +19,7 @@ import ClockDisplayBase from './clock_display_base';
|
||||
export default class ClockDisplay extends ClockDisplayBase {
|
||||
static propTypes = {
|
||||
showModal: PropTypes.bool.isRequired,
|
||||
militaryTime: PropTypes.bool.isRequired,
|
||||
militaryTime: PropTypes.string.isRequired,
|
||||
onClose: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
@@ -76,3 +76,118 @@ exports[`DisplaySettings should match snapshot 1`] = `
|
||||
</View>
|
||||
</View>
|
||||
`;
|
||||
|
||||
exports[`DisplaySettings should match snapshot on Tablet devices 1`] = `
|
||||
<View
|
||||
style={
|
||||
Object {
|
||||
"backgroundColor": "#ffffff",
|
||||
"flex": 1,
|
||||
}
|
||||
}
|
||||
>
|
||||
<Connect(StatusBar) />
|
||||
<View
|
||||
style={
|
||||
Object {
|
||||
"backgroundColor": "rgba(61,60,64,0.06)",
|
||||
"flex": 1,
|
||||
"paddingTop": 35,
|
||||
}
|
||||
}
|
||||
>
|
||||
<View
|
||||
style={
|
||||
Object {
|
||||
"backgroundColor": "rgba(61,60,64,0.1)",
|
||||
"height": 1,
|
||||
}
|
||||
}
|
||||
/>
|
||||
<SettingsItem
|
||||
defaultMessage="Sidebar"
|
||||
i18nId="mobile.display_settings.sidebar"
|
||||
iconName="columns"
|
||||
iconType="fontawesome"
|
||||
isDestructor={false}
|
||||
onPress={[Function]}
|
||||
separator={true}
|
||||
showArrow={false}
|
||||
theme={
|
||||
Object {
|
||||
"awayIndicator": "#ffbc42",
|
||||
"buttonBg": "#166de0",
|
||||
"buttonColor": "#ffffff",
|
||||
"centerChannelBg": "#ffffff",
|
||||
"centerChannelColor": "#3d3c40",
|
||||
"codeTheme": "github",
|
||||
"dndIndicator": "#f74343",
|
||||
"errorTextColor": "#fd5960",
|
||||
"linkColor": "#2389d7",
|
||||
"mentionBj": "#ffffff",
|
||||
"mentionColor": "#145dbf",
|
||||
"mentionHighlightBg": "#ffe577",
|
||||
"mentionHighlightLink": "#166de0",
|
||||
"newMessageSeparator": "#ff8800",
|
||||
"onlineIndicator": "#06d6a0",
|
||||
"sidebarBg": "#145dbf",
|
||||
"sidebarHeaderBg": "#1153ab",
|
||||
"sidebarHeaderTextColor": "#ffffff",
|
||||
"sidebarText": "#ffffff",
|
||||
"sidebarTextActiveBorder": "#579eff",
|
||||
"sidebarTextActiveColor": "#ffffff",
|
||||
"sidebarTextHoverBg": "#4578bf",
|
||||
"sidebarUnreadText": "#ffffff",
|
||||
"type": "Mattermost",
|
||||
}
|
||||
}
|
||||
/>
|
||||
<SettingsItem
|
||||
defaultMessage="Clock Display"
|
||||
i18nId="mobile.advanced_settings.clockDisplay"
|
||||
iconName="ios-time"
|
||||
iconType="ion"
|
||||
isDestructor={false}
|
||||
onPress={[Function]}
|
||||
separator={false}
|
||||
showArrow={false}
|
||||
theme={
|
||||
Object {
|
||||
"awayIndicator": "#ffbc42",
|
||||
"buttonBg": "#166de0",
|
||||
"buttonColor": "#ffffff",
|
||||
"centerChannelBg": "#ffffff",
|
||||
"centerChannelColor": "#3d3c40",
|
||||
"codeTheme": "github",
|
||||
"dndIndicator": "#f74343",
|
||||
"errorTextColor": "#fd5960",
|
||||
"linkColor": "#2389d7",
|
||||
"mentionBj": "#ffffff",
|
||||
"mentionColor": "#145dbf",
|
||||
"mentionHighlightBg": "#ffe577",
|
||||
"mentionHighlightLink": "#166de0",
|
||||
"newMessageSeparator": "#ff8800",
|
||||
"onlineIndicator": "#06d6a0",
|
||||
"sidebarBg": "#145dbf",
|
||||
"sidebarHeaderBg": "#1153ab",
|
||||
"sidebarHeaderTextColor": "#ffffff",
|
||||
"sidebarText": "#ffffff",
|
||||
"sidebarTextActiveBorder": "#579eff",
|
||||
"sidebarTextActiveColor": "#ffffff",
|
||||
"sidebarTextHoverBg": "#4578bf",
|
||||
"sidebarUnreadText": "#ffffff",
|
||||
"type": "Mattermost",
|
||||
}
|
||||
}
|
||||
/>
|
||||
<View
|
||||
style={
|
||||
Object {
|
||||
"backgroundColor": "rgba(61,60,64,0.1)",
|
||||
"height": 1,
|
||||
}
|
||||
}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
`;
|
||||
|
||||
@@ -9,13 +9,13 @@ import {
|
||||
View,
|
||||
} from 'react-native';
|
||||
|
||||
import SettingsItem from 'app/screens/settings/settings_item';
|
||||
import {DeviceTypes} from 'app/constants';
|
||||
import StatusBar from 'app/components/status_bar';
|
||||
import ClockDisplay from 'app/screens/settings/clock_display';
|
||||
import SettingsItem from 'app/screens/settings/settings_item';
|
||||
import {preventDoubleTap} from 'app/utils/tap';
|
||||
import {changeOpacity, makeStyleSheetFromTheme} from 'app/utils/theme';
|
||||
|
||||
import ClockDisplay from 'app/screens/clock_display';
|
||||
|
||||
export default class DisplaySettings extends PureComponent {
|
||||
static propTypes = {
|
||||
actions: PropTypes.shape({
|
||||
@@ -44,7 +44,7 @@ export default class DisplaySettings extends PureComponent {
|
||||
const {intl} = this.context;
|
||||
|
||||
if (Platform.OS === 'ios') {
|
||||
const screen = 'ClockDisplay';
|
||||
const screen = 'ClockDisplaySettings';
|
||||
const title = intl.formatMessage({id: 'user.settings.display.clockDisplay', defaultMessage: 'Clock Display'});
|
||||
actions.goToScreen(screen, title);
|
||||
return;
|
||||
@@ -53,6 +53,15 @@ export default class DisplaySettings extends PureComponent {
|
||||
this.setState({showClockDisplaySettings: true});
|
||||
});
|
||||
|
||||
goToSidebarSettings = preventDoubleTap(() => {
|
||||
const {actions, theme} = this.props;
|
||||
const {intl} = this.context;
|
||||
const screen = 'SidebarSettings';
|
||||
const title = intl.formatMessage({id: 'mobile.display_settings.sidebar', defaultMessage: 'Sidebar'});
|
||||
|
||||
actions.goToScreen(screen, title, {theme});
|
||||
});
|
||||
|
||||
goToTimezoneSettings = preventDoubleTap(() => {
|
||||
const {actions} = this.props;
|
||||
const {intl} = this.context;
|
||||
@@ -104,11 +113,28 @@ export default class DisplaySettings extends PureComponent {
|
||||
);
|
||||
}
|
||||
|
||||
let sidebar;
|
||||
if (DeviceTypes.IS_TABLET) {
|
||||
sidebar = (
|
||||
<SettingsItem
|
||||
defaultMessage='Sidebar'
|
||||
i18nId='mobile.display_settings.sidebar'
|
||||
iconName='columns'
|
||||
iconType='fontawesome'
|
||||
onPress={this.goToSidebarSettings}
|
||||
separator={true}
|
||||
showArrow={false}
|
||||
theme={theme}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<View style={style.container}>
|
||||
<StatusBar/>
|
||||
<View style={style.wrapper}>
|
||||
<View style={style.divider}/>
|
||||
{sidebar}
|
||||
{enableTheme && (
|
||||
<SettingsItem
|
||||
defaultMessage='Theme'
|
||||
|
||||
@@ -3,9 +3,12 @@
|
||||
|
||||
import React from 'react';
|
||||
import {shallow} from 'enzyme';
|
||||
import SettingsItem from 'app/screens/settings/settings_item';
|
||||
|
||||
import Preferences from 'mattermost-redux/constants/preferences';
|
||||
|
||||
import {DeviceTypes} from 'app/constants';
|
||||
import SettingsItem from 'app/screens/settings/settings_item';
|
||||
|
||||
import DisplaySettings from './display_settings';
|
||||
|
||||
jest.mock('react-intl');
|
||||
@@ -33,4 +36,19 @@ describe('DisplaySettings', () => {
|
||||
wrapper.setProps({enableTimezone: true});
|
||||
expect(wrapper.find(SettingsItem).length).toBe(3);
|
||||
});
|
||||
|
||||
test('should match snapshot on Tablet devices', () => {
|
||||
DeviceTypes.IS_TABLET = true;
|
||||
|
||||
const wrapper = shallow(
|
||||
<DisplaySettings {...baseProps}/>,
|
||||
);
|
||||
|
||||
expect(wrapper.getElement()).toMatchSnapshot();
|
||||
expect(wrapper.find(SettingsItem).length).toBe(2);
|
||||
wrapper.setProps({enableTheme: true});
|
||||
expect(wrapper.find(SettingsItem).length).toBe(3);
|
||||
wrapper.setProps({enableTimezone: true});
|
||||
expect(wrapper.find(SettingsItem).length).toBe(4);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,254 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`NotificationSettingsMentionsKeywords should match snapshot 1`] = `
|
||||
NotificationSettingsMentionsKeywords {
|
||||
"context": Object {},
|
||||
"handleSubmit": [Function],
|
||||
"keywordsRef": [Function],
|
||||
"navigationEventListener": Object {
|
||||
"remove": [Function],
|
||||
},
|
||||
"onKeywordsChangeText": [Function],
|
||||
"props": Object {
|
||||
"actions": Object {
|
||||
"popTopScreen": [MockFunction],
|
||||
},
|
||||
"componentId": "component-id",
|
||||
"intl": Object {
|
||||
"defaultFormats": Object {},
|
||||
"defaultLocale": "en",
|
||||
"formatDate": [Function],
|
||||
"formatHTMLMessage": [Function],
|
||||
"formatMessage": [Function],
|
||||
"formatNumber": [Function],
|
||||
"formatPlural": [Function],
|
||||
"formatRelative": [Function],
|
||||
"formatTime": [Function],
|
||||
"formats": Object {},
|
||||
"formatters": Object {
|
||||
"getDateTimeFormat": [Function],
|
||||
"getMessageFormat": [Function],
|
||||
"getNumberFormat": [Function],
|
||||
"getPluralFormat": [Function],
|
||||
"getRelativeFormat": [Function],
|
||||
},
|
||||
"locale": "en",
|
||||
"messages": Object {},
|
||||
"now": [Function],
|
||||
"onError": [Function],
|
||||
"textComponent": "span",
|
||||
"timeZone": null,
|
||||
},
|
||||
"isLandscape": false,
|
||||
"keywords": "",
|
||||
"onBack": [MockFunction],
|
||||
"theme": Object {
|
||||
"awayIndicator": "#ffbc42",
|
||||
"buttonBg": "#166de0",
|
||||
"buttonColor": "#ffffff",
|
||||
"centerChannelBg": "#ffffff",
|
||||
"centerChannelColor": "#3d3c40",
|
||||
"codeTheme": "github",
|
||||
"dndIndicator": "#f74343",
|
||||
"errorTextColor": "#fd5960",
|
||||
"linkColor": "#2389d7",
|
||||
"mentionBj": "#ffffff",
|
||||
"mentionColor": "#145dbf",
|
||||
"mentionHighlightBg": "#ffe577",
|
||||
"mentionHighlightLink": "#166de0",
|
||||
"newMessageSeparator": "#ff8800",
|
||||
"onlineIndicator": "#06d6a0",
|
||||
"sidebarBg": "#145dbf",
|
||||
"sidebarHeaderBg": "#1153ab",
|
||||
"sidebarHeaderTextColor": "#ffffff",
|
||||
"sidebarText": "#ffffff",
|
||||
"sidebarTextActiveBorder": "#579eff",
|
||||
"sidebarTextActiveColor": "#ffffff",
|
||||
"sidebarTextHoverBg": "#4578bf",
|
||||
"sidebarUnreadText": "#ffffff",
|
||||
"type": "Mattermost",
|
||||
},
|
||||
},
|
||||
"refs": Object {},
|
||||
"setState": [Function],
|
||||
"state": Object {
|
||||
"keywords": "",
|
||||
},
|
||||
"updater": Updater {
|
||||
"_callbacks": Array [],
|
||||
"_renderer": ReactShallowRenderer {
|
||||
"_context": Object {},
|
||||
"_didScheduleRenderPhaseUpdate": false,
|
||||
"_dispatcher": Object {
|
||||
"readContext": [Function],
|
||||
"useCallback": [Function],
|
||||
"useContext": [Function],
|
||||
"useDebugValue": [Function],
|
||||
"useEffect": [Function],
|
||||
"useImperativeHandle": [Function],
|
||||
"useLayoutEffect": [Function],
|
||||
"useMemo": [Function],
|
||||
"useReducer": [Function],
|
||||
"useRef": [Function],
|
||||
"useState": [Function],
|
||||
},
|
||||
"_element": <NotificationSettingsMentionsKeywords
|
||||
actions={
|
||||
Object {
|
||||
"popTopScreen": [MockFunction],
|
||||
}
|
||||
}
|
||||
componentId="component-id"
|
||||
intl={
|
||||
Object {
|
||||
"defaultFormats": Object {},
|
||||
"defaultLocale": "en",
|
||||
"formatDate": [Function],
|
||||
"formatHTMLMessage": [Function],
|
||||
"formatMessage": [Function],
|
||||
"formatNumber": [Function],
|
||||
"formatPlural": [Function],
|
||||
"formatRelative": [Function],
|
||||
"formatTime": [Function],
|
||||
"formats": Object {},
|
||||
"formatters": Object {
|
||||
"getDateTimeFormat": [Function],
|
||||
"getMessageFormat": [Function],
|
||||
"getNumberFormat": [Function],
|
||||
"getPluralFormat": [Function],
|
||||
"getRelativeFormat": [Function],
|
||||
},
|
||||
"locale": "en",
|
||||
"messages": Object {},
|
||||
"now": [Function],
|
||||
"onError": [Function],
|
||||
"textComponent": "span",
|
||||
"timeZone": null,
|
||||
}
|
||||
}
|
||||
isLandscape={false}
|
||||
keywords=""
|
||||
onBack={[MockFunction]}
|
||||
theme={
|
||||
Object {
|
||||
"awayIndicator": "#ffbc42",
|
||||
"buttonBg": "#166de0",
|
||||
"buttonColor": "#ffffff",
|
||||
"centerChannelBg": "#ffffff",
|
||||
"centerChannelColor": "#3d3c40",
|
||||
"codeTheme": "github",
|
||||
"dndIndicator": "#f74343",
|
||||
"errorTextColor": "#fd5960",
|
||||
"linkColor": "#2389d7",
|
||||
"mentionBj": "#ffffff",
|
||||
"mentionColor": "#145dbf",
|
||||
"mentionHighlightBg": "#ffe577",
|
||||
"mentionHighlightLink": "#166de0",
|
||||
"newMessageSeparator": "#ff8800",
|
||||
"onlineIndicator": "#06d6a0",
|
||||
"sidebarBg": "#145dbf",
|
||||
"sidebarHeaderBg": "#1153ab",
|
||||
"sidebarHeaderTextColor": "#ffffff",
|
||||
"sidebarText": "#ffffff",
|
||||
"sidebarTextActiveBorder": "#579eff",
|
||||
"sidebarTextActiveColor": "#ffffff",
|
||||
"sidebarTextHoverBg": "#4578bf",
|
||||
"sidebarUnreadText": "#ffffff",
|
||||
"type": "Mattermost",
|
||||
}
|
||||
}
|
||||
/>,
|
||||
"_firstWorkInProgressHook": null,
|
||||
"_forcedUpdate": false,
|
||||
"_instance": [Circular],
|
||||
"_isReRender": false,
|
||||
"_newState": null,
|
||||
"_numberOfReRenders": 0,
|
||||
"_renderPhaseUpdates": null,
|
||||
"_rendered": <View
|
||||
style={
|
||||
Object {
|
||||
"backgroundColor": "#ffffff",
|
||||
"flex": 1,
|
||||
}
|
||||
}
|
||||
>
|
||||
<Connect(StatusBar) />
|
||||
<ScrollViewMock
|
||||
alwaysBounceVertical={false}
|
||||
contentContainerStyle={
|
||||
Object {
|
||||
"backgroundColor": "rgba(61,60,64,0.06)",
|
||||
"flex": 1,
|
||||
"paddingTop": 35,
|
||||
}
|
||||
}
|
||||
>
|
||||
<View
|
||||
style={
|
||||
Object {
|
||||
"backgroundColor": "#ffffff",
|
||||
"borderBottomColor": "rgba(61,60,64,0.1)",
|
||||
"borderBottomWidth": 1,
|
||||
"borderTopColor": "rgba(61,60,64,0.1)",
|
||||
"borderTopWidth": 1,
|
||||
}
|
||||
}
|
||||
>
|
||||
<TextInputWithLocalizedPlaceholder
|
||||
autoCapitalize="none"
|
||||
autoCorrect={false}
|
||||
autoFocus={true}
|
||||
blurOnSubmit={true}
|
||||
multiline={true}
|
||||
numberOfLines={1}
|
||||
onChangeText={[Function]}
|
||||
onSubmitEditing={[Function]}
|
||||
placeholder={
|
||||
Object {
|
||||
"defaultMessage": "Other words that trigger a mention",
|
||||
"id": "mobile.notification_settings_mentions.keywordsDescription",
|
||||
}
|
||||
}
|
||||
placeholderTextColor="rgba(61,60,64,0.4)"
|
||||
returnKeyType="done"
|
||||
style={
|
||||
Object {
|
||||
"color": "#3d3c40",
|
||||
"fontSize": 15,
|
||||
"height": 150,
|
||||
"paddingHorizontal": 15,
|
||||
"paddingVertical": 10,
|
||||
}
|
||||
}
|
||||
value=""
|
||||
/>
|
||||
</View>
|
||||
<View
|
||||
style={
|
||||
Object {
|
||||
"marginTop": 10,
|
||||
"paddingHorizontal": 15,
|
||||
}
|
||||
}
|
||||
>
|
||||
<FormattedText
|
||||
defaultMessage="Keywords are non-case sensitive and should be separated by a comma."
|
||||
id="mobile.notification_settings_mentions.keywordsHelp"
|
||||
style={
|
||||
Object {
|
||||
"color": "rgba(61,60,64,0.4)",
|
||||
"fontSize": 13,
|
||||
}
|
||||
}
|
||||
/>
|
||||
</View>
|
||||
</ScrollViewMock>
|
||||
</View>,
|
||||
"_rendering": false,
|
||||
"_updater": [Circular],
|
||||
"_workInProgressHook": null,
|
||||
},
|
||||
},
|
||||
}
|
||||
`;
|
||||
@@ -107,6 +107,7 @@ const getStyleSheet = makeStyleSheetFromTheme((theme) => {
|
||||
},
|
||||
wrapper: {
|
||||
backgroundColor: changeOpacity(theme.centerChannelColor, 0.06),
|
||||
flex: 1,
|
||||
paddingTop: 35,
|
||||
},
|
||||
inputContainer: {
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import Preferences from 'mattermost-redux/constants/preferences';
|
||||
|
||||
import {shallowWithIntl} from 'test/intl-test-helper';
|
||||
|
||||
import NotificationSettingsMentionsKeywords from './notification_settings_mentions_keywords';
|
||||
|
||||
describe('NotificationSettingsMentionsKeywords', () => {
|
||||
const baseProps = {
|
||||
actions: {
|
||||
popTopScreen: jest.fn(),
|
||||
},
|
||||
componentId: 'component-id',
|
||||
keywords: '',
|
||||
isLandscape: false,
|
||||
onBack: jest.fn(),
|
||||
theme: Preferences.THEMES.default,
|
||||
};
|
||||
|
||||
test('should match snapshot', () => {
|
||||
const wrapper = shallowWithIntl(
|
||||
<NotificationSettingsMentionsKeywords {...baseProps}/>
|
||||
);
|
||||
|
||||
expect(wrapper.instance()).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
119
app/screens/settings/sidebar/__snapshots__/sidebar.test.js.snap
Normal file
119
app/screens/settings/sidebar/__snapshots__/sidebar.test.js.snap
Normal file
@@ -0,0 +1,119 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`SidebarSettings should match, full snapshot 1`] = `null`;
|
||||
|
||||
exports[`SidebarSettings should match, full snapshot 2`] = `
|
||||
<View
|
||||
style={
|
||||
Object {
|
||||
"backgroundColor": "#ffffff",
|
||||
"flex": 1,
|
||||
}
|
||||
}
|
||||
>
|
||||
<Connect(StatusBar) />
|
||||
<View
|
||||
style={
|
||||
Object {
|
||||
"backgroundColor": "rgba(61,60,64,0.06)",
|
||||
"flex": 1,
|
||||
"paddingTop": 35,
|
||||
}
|
||||
}
|
||||
>
|
||||
<section
|
||||
disableHeader={true}
|
||||
theme={
|
||||
Object {
|
||||
"awayIndicator": "#ffbc42",
|
||||
"buttonBg": "#166de0",
|
||||
"buttonColor": "#ffffff",
|
||||
"centerChannelBg": "#ffffff",
|
||||
"centerChannelColor": "#3d3c40",
|
||||
"codeTheme": "github",
|
||||
"dndIndicator": "#f74343",
|
||||
"errorTextColor": "#fd5960",
|
||||
"linkColor": "#2389d7",
|
||||
"mentionBj": "#ffffff",
|
||||
"mentionColor": "#145dbf",
|
||||
"mentionHighlightBg": "#ffe577",
|
||||
"mentionHighlightLink": "#166de0",
|
||||
"newMessageSeparator": "#ff8800",
|
||||
"onlineIndicator": "#06d6a0",
|
||||
"sidebarBg": "#145dbf",
|
||||
"sidebarHeaderBg": "#1153ab",
|
||||
"sidebarHeaderTextColor": "#ffffff",
|
||||
"sidebarText": "#ffffff",
|
||||
"sidebarTextActiveBorder": "#579eff",
|
||||
"sidebarTextActiveColor": "#ffffff",
|
||||
"sidebarTextHoverBg": "#4578bf",
|
||||
"sidebarUnreadText": "#ffffff",
|
||||
"type": "Mattermost",
|
||||
}
|
||||
}
|
||||
>
|
||||
<View
|
||||
style={
|
||||
Object {
|
||||
"backgroundColor": "rgba(61,60,64,0.1)",
|
||||
"height": 1,
|
||||
}
|
||||
}
|
||||
/>
|
||||
<sectionItem
|
||||
action={[Function]}
|
||||
actionType="toggle"
|
||||
description={
|
||||
<FormattedText
|
||||
defaultMessage="Keep the sidebar open permanently"
|
||||
id="mobile.sidebar_settings.permanent_description"
|
||||
/>
|
||||
}
|
||||
label={
|
||||
<FormattedText
|
||||
defaultMessage="Permanent Sidebar"
|
||||
id="mobile.sidebar_settings.permanent"
|
||||
/>
|
||||
}
|
||||
selected={false}
|
||||
theme={
|
||||
Object {
|
||||
"awayIndicator": "#ffbc42",
|
||||
"buttonBg": "#166de0",
|
||||
"buttonColor": "#ffffff",
|
||||
"centerChannelBg": "#ffffff",
|
||||
"centerChannelColor": "#3d3c40",
|
||||
"codeTheme": "github",
|
||||
"dndIndicator": "#f74343",
|
||||
"errorTextColor": "#fd5960",
|
||||
"linkColor": "#2389d7",
|
||||
"mentionBj": "#ffffff",
|
||||
"mentionColor": "#145dbf",
|
||||
"mentionHighlightBg": "#ffe577",
|
||||
"mentionHighlightLink": "#166de0",
|
||||
"newMessageSeparator": "#ff8800",
|
||||
"onlineIndicator": "#06d6a0",
|
||||
"sidebarBg": "#145dbf",
|
||||
"sidebarHeaderBg": "#1153ab",
|
||||
"sidebarHeaderTextColor": "#ffffff",
|
||||
"sidebarText": "#ffffff",
|
||||
"sidebarTextActiveBorder": "#579eff",
|
||||
"sidebarTextActiveColor": "#ffffff",
|
||||
"sidebarTextHoverBg": "#4578bf",
|
||||
"sidebarUnreadText": "#ffffff",
|
||||
"type": "Mattermost",
|
||||
}
|
||||
}
|
||||
/>
|
||||
<View
|
||||
style={
|
||||
Object {
|
||||
"backgroundColor": "rgba(61,60,64,0.1)",
|
||||
"height": 1,
|
||||
}
|
||||
}
|
||||
/>
|
||||
</section>
|
||||
</View>
|
||||
</View>
|
||||
`;
|
||||
119
app/screens/settings/sidebar/index.js
Normal file
119
app/screens/settings/sidebar/index.js
Normal file
@@ -0,0 +1,119 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React, {PureComponent} from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {
|
||||
View,
|
||||
Platform,
|
||||
} from 'react-native';
|
||||
import {intlShape} from 'react-intl';
|
||||
import AsyncStorage from '@react-native-community/async-storage';
|
||||
|
||||
import EventEmitter from 'mattermost-redux/utils/event_emitter';
|
||||
|
||||
import {DeviceTypes} from 'app/constants';
|
||||
import FormattedText from 'app/components/formatted_text';
|
||||
import StatusBar from 'app/components/status_bar';
|
||||
import Section from 'app/screens/settings/section';
|
||||
import SectionItem from 'app/screens/settings/section_item';
|
||||
import {changeOpacity, makeStyleSheetFromTheme} from 'app/utils/theme';
|
||||
|
||||
export default class SidebarSettings extends PureComponent {
|
||||
static propTypes = {
|
||||
theme: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
static contextTypes = {
|
||||
intl: intlShape,
|
||||
};
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.loadSetting();
|
||||
}
|
||||
|
||||
loadSetting = async () => {
|
||||
const value = await AsyncStorage.getItem(DeviceTypes.PERMANENT_SIDEBAR_SETTINGS);
|
||||
const enabled = Boolean(value === 'true');
|
||||
this.setState({enabled});
|
||||
};
|
||||
|
||||
saveSetting = (enabled) => {
|
||||
AsyncStorage.setItem(DeviceTypes.PERMANENT_SIDEBAR_SETTINGS, enabled.toString());
|
||||
this.setState({enabled}, () => EventEmitter.emit(DeviceTypes.PERMANENT_SIDEBAR_SETTINGS));
|
||||
};
|
||||
|
||||
render() {
|
||||
if (!this.state) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const {
|
||||
theme,
|
||||
} = this.props;
|
||||
const {enabled} = this.state;
|
||||
const style = getStyleSheet(theme);
|
||||
|
||||
return (
|
||||
<View style={style.container}>
|
||||
<StatusBar/>
|
||||
<View style={style.wrapper}>
|
||||
<Section
|
||||
disableHeader={true}
|
||||
theme={theme}
|
||||
>
|
||||
<View style={style.divider}/>
|
||||
<SectionItem
|
||||
label={(
|
||||
<FormattedText
|
||||
id='mobile.sidebar_settings.permanent'
|
||||
defaultMessage='Permanent Sidebar'
|
||||
/>
|
||||
)}
|
||||
description={(
|
||||
<FormattedText
|
||||
id='mobile.sidebar_settings.permanent_description'
|
||||
defaultMessage='Keep the sidebar open permanently'
|
||||
/>
|
||||
)}
|
||||
action={this.saveSetting}
|
||||
actionType='toggle'
|
||||
selected={enabled}
|
||||
theme={theme}
|
||||
/>
|
||||
<View style={style.divider}/>
|
||||
</Section>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const getStyleSheet = makeStyleSheetFromTheme((theme) => {
|
||||
return {
|
||||
container: {
|
||||
flex: 1,
|
||||
backgroundColor: theme.centerChannelBg,
|
||||
},
|
||||
wrapper: {
|
||||
backgroundColor: changeOpacity(theme.centerChannelColor, 0.06),
|
||||
flex: 1,
|
||||
...Platform.select({
|
||||
ios: {
|
||||
paddingTop: 35,
|
||||
},
|
||||
}),
|
||||
},
|
||||
divider: {
|
||||
backgroundColor: changeOpacity(theme.centerChannelColor, 0.1),
|
||||
height: 1,
|
||||
},
|
||||
separator: {
|
||||
backgroundColor: changeOpacity(theme.centerChannelColor, 0.1),
|
||||
height: 1,
|
||||
marginLeft: 15,
|
||||
},
|
||||
};
|
||||
});
|
||||
79
app/screens/settings/sidebar/sidebar.test.js
Normal file
79
app/screens/settings/sidebar/sidebar.test.js
Normal file
@@ -0,0 +1,79 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
import {shallow} from 'enzyme';
|
||||
|
||||
import Preferences from 'mattermost-redux/constants/preferences';
|
||||
|
||||
import {DeviceTypes} from 'app/constants';
|
||||
import MainSidebar from 'app/components/sidebars/main/main_sidebar';
|
||||
import SidebarSettings from './index';
|
||||
|
||||
jest.mock('react-intl');
|
||||
jest.mock('app/mattermost_managed', () => ({
|
||||
isRunningInSplitView: jest.fn().mockResolvedValue(false),
|
||||
}));
|
||||
|
||||
describe('SidebarSettings', () => {
|
||||
const baseProps = {
|
||||
theme: Preferences.THEMES.default,
|
||||
};
|
||||
|
||||
test('should match, full snapshot', async () => {
|
||||
const wrapper = shallow(
|
||||
<SidebarSettings {...baseProps}/>
|
||||
);
|
||||
|
||||
expect(wrapper.getElement()).toMatchSnapshot();
|
||||
|
||||
await wrapper.instance().loadSetting();
|
||||
expect(wrapper.getElement()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('should set the Permanent Sidebar value to false', async () => {
|
||||
const wrapper = shallow(
|
||||
<SidebarSettings {...baseProps}/>
|
||||
);
|
||||
|
||||
await wrapper.instance().loadSetting();
|
||||
expect(wrapper.state('enabled')).toBe(false);
|
||||
});
|
||||
|
||||
test('should set the Permanent Sidebar value to true and update the sidebar', async () => {
|
||||
DeviceTypes.IS_TABLET = true;
|
||||
|
||||
const wrapper = shallow(
|
||||
<SidebarSettings {...baseProps}/>
|
||||
);
|
||||
|
||||
const mainProps = {
|
||||
actions: {
|
||||
getTeams: jest.fn(),
|
||||
logChannelSwitch: jest.fn(),
|
||||
makeDirectChannel: jest.fn(),
|
||||
setChannelDisplayName: jest.fn(),
|
||||
setChannelLoading: jest.fn(),
|
||||
},
|
||||
blurPostTextBox: jest.fn(),
|
||||
currentTeamId: 'current-team-id',
|
||||
currentUserId: 'current-user-id',
|
||||
deviceWidth: 10,
|
||||
isLandscape: false,
|
||||
teamsCount: 2,
|
||||
theme: Preferences.THEMES.default,
|
||||
};
|
||||
|
||||
const mainSidebar = shallow(
|
||||
<MainSidebar {...mainProps}/>
|
||||
);
|
||||
|
||||
await wrapper.instance().loadSetting();
|
||||
expect(wrapper.state('enabled')).toBe(false);
|
||||
|
||||
await wrapper.instance().saveSetting(true);
|
||||
|
||||
expect(wrapper.state('enabled')).toBe(true);
|
||||
expect(mainSidebar.state('permanentSidebar')).toBe(true);
|
||||
});
|
||||
});
|
||||
@@ -3,10 +3,12 @@
|
||||
|
||||
import {connect} from 'react-redux';
|
||||
import {bindActionCreators} from 'redux';
|
||||
|
||||
import {getTheme} from 'mattermost-redux/selectors/entities/preferences';
|
||||
import {getCurrentUserId} from 'mattermost-redux/selectors/entities/users';
|
||||
import {getCurrentTeamId} from 'mattermost-redux/selectors/entities/teams';
|
||||
import {savePreferences} from 'mattermost-redux/actions/preferences';
|
||||
|
||||
import {getAllowedThemes, getCustomTheme} from 'app/selectors/theme';
|
||||
import {isLandscape, isTablet} from 'app/selectors/device';
|
||||
|
||||
@@ -6,14 +6,16 @@ import {Text, View} from 'react-native';
|
||||
import PropTypes from 'prop-types';
|
||||
import {intlShape} from 'react-intl';
|
||||
|
||||
import Preferences from 'mattermost-redux/constants/preferences';
|
||||
|
||||
import StatusBar from 'app/components/status_bar';
|
||||
import Section from 'app/screens/settings/section';
|
||||
import SectionItem from 'app/screens/settings/section_item';
|
||||
import ThemeTile from './theme_tile';
|
||||
import FormattedText from 'app/components/formatted_text';
|
||||
|
||||
import {changeOpacity, makeStyleSheetFromTheme, setNavigatorStyles} from 'app/utils/theme';
|
||||
import Preferences from 'mattermost-redux/constants/preferences';
|
||||
import EphemeralStore from 'app/store/ephemeral_store';
|
||||
|
||||
import ThemeTile from './theme_tile';
|
||||
|
||||
const thumbnailImages = {
|
||||
default: require('assets/images/themes/mattermost.png'),
|
||||
@@ -56,12 +58,19 @@ export default class Theme extends React.PureComponent {
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
if (prevProps.theme !== this.props.theme) {
|
||||
setNavigatorStyles(this.props.componentId, this.props.theme);
|
||||
EphemeralStore.allNavigationComponentIds.forEach((componentId) => {
|
||||
setNavigatorStyles(componentId, this.props.theme);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
setTheme = (key) => {
|
||||
const {userId, teamId, actions: {savePreferences}, allowedThemes} = this.props;
|
||||
const {
|
||||
userId,
|
||||
teamId,
|
||||
allowedThemes,
|
||||
actions: {savePreferences},
|
||||
} = this.props;
|
||||
const {customTheme} = this.state;
|
||||
const selectedTheme = allowedThemes.concat(customTheme).find((theme) => theme.key === key);
|
||||
|
||||
@@ -71,7 +80,7 @@ export default class Theme extends React.PureComponent {
|
||||
name: teamId,
|
||||
value: JSON.stringify(selectedTheme),
|
||||
}]);
|
||||
}
|
||||
};
|
||||
|
||||
renderAllowedThemeTiles = () => {
|
||||
const {theme, allowedThemes, isLandscape, isTablet} = this.props;
|
||||
@@ -3,15 +3,23 @@
|
||||
|
||||
import React from 'react';
|
||||
import {shallow} from 'enzyme';
|
||||
|
||||
import ThemeTile from './theme_tile';
|
||||
import {Navigation} from 'react-native-navigation';
|
||||
|
||||
import Preferences from 'mattermost-redux/constants/preferences';
|
||||
|
||||
import EphemeralStore from 'app/store/ephemeral_store';
|
||||
|
||||
import Theme from './theme';
|
||||
import ThemeTile from './theme_tile';
|
||||
|
||||
jest.mock('react-intl');
|
||||
|
||||
jest.mock('react-native-navigation', () => ({
|
||||
Navigation: {
|
||||
mergeOptions: jest.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
const allowedThemes = [
|
||||
{
|
||||
type: 'Mattermost',
|
||||
@@ -144,4 +152,43 @@ describe('Theme', () => {
|
||||
expect(wrapper.getElement()).toMatchSnapshot();
|
||||
expect(wrapper.find(ThemeTile)).toHaveLength(4);
|
||||
});
|
||||
|
||||
test('should call Navigation.mergeOptions on all navigation components when theme changes', () => {
|
||||
const componentIds = ['component-1', 'component-2', 'component-3'];
|
||||
componentIds.forEach((componentId) => {
|
||||
EphemeralStore.addNavigationComponentId(componentId);
|
||||
});
|
||||
|
||||
const wrapper = shallow(
|
||||
<Theme {...baseProps}/>,
|
||||
);
|
||||
|
||||
const newTheme = allowedThemes[1];
|
||||
wrapper.setProps({theme: newTheme});
|
||||
|
||||
const options = {
|
||||
topBar: {
|
||||
backButton: {
|
||||
color: newTheme.sidebarHeaderTextColor,
|
||||
},
|
||||
background: {
|
||||
color: newTheme.sidebarHeaderBg,
|
||||
},
|
||||
title: {
|
||||
color: newTheme.sidebarHeaderTextColor,
|
||||
},
|
||||
leftButtonColor: newTheme.sidebarHeaderTextColor,
|
||||
rightButtonColor: newTheme.sidebarHeaderTextColor,
|
||||
},
|
||||
layout: {
|
||||
backgroundColor: newTheme.centerChannelBg,
|
||||
},
|
||||
};
|
||||
|
||||
expect(Navigation.mergeOptions.mock.calls).toEqual([
|
||||
[componentIds[2], options],
|
||||
[componentIds[1], options],
|
||||
[componentIds[0], options],
|
||||
]);
|
||||
});
|
||||
});
|
||||
@@ -12,8 +12,6 @@ import {
|
||||
|
||||
import {makeStyleSheetFromTheme} from 'app/utils/theme';
|
||||
|
||||
const {width: deviceWidth, height: deviceHeight} = Dimensions.get('window');
|
||||
|
||||
const checkmark = require('assets/images/themes/check.png');
|
||||
|
||||
const tilePadding = 8;
|
||||
@@ -38,7 +36,8 @@ const ThemeTile = (props) => {
|
||||
);
|
||||
|
||||
const tilesPerLine = isLandscape || isTablet ? 4 : 2;
|
||||
const fullWidth = isLandscape ? deviceHeight - 40 : deviceWidth;
|
||||
const {width: deviceWidth} = Dimensions.get('window');
|
||||
const fullWidth = isLandscape ? deviceWidth - 40 : deviceWidth;
|
||||
const layoutStyle = {
|
||||
container: {
|
||||
width: (fullWidth / tilesPerLine) - tilePadding,
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user