forked from Ivasoft/mattermost-mobile
Compare commits
53 Commits
release-1.
...
release-1.
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
15e081f572 | ||
|
|
1be535cc22 | ||
|
|
6a2f02be62 | ||
|
|
4f28e61632 | ||
|
|
c7cf32ebb8 | ||
|
|
142a04fac5 | ||
|
|
60b82ea5a8 | ||
|
|
64989a728c | ||
|
|
7317ffeb21 | ||
|
|
49031c26d4 | ||
|
|
c133dab50f | ||
|
|
47c0ff2655 | ||
|
|
55fc50d7c2 | ||
|
|
8b0c831814 | ||
|
|
971d5990e8 | ||
|
|
e91af36780 | ||
|
|
70e5cace11 | ||
|
|
7aebfd0b13 | ||
|
|
8d9ed361f8 | ||
|
|
25d5b48db1 | ||
|
|
68d76af4dd | ||
|
|
0ee7b60e84 | ||
|
|
ad6d3f42c3 | ||
|
|
f0598dde54 | ||
|
|
170ef360c1 | ||
|
|
b3796e162c | ||
|
|
9abc89129b | ||
|
|
7088481ac6 | ||
|
|
c106e9f973 | ||
|
|
47b62daccb | ||
|
|
77bc6257ac | ||
|
|
6e2936e2e9 | ||
|
|
20f210cb03 | ||
|
|
47deea650e | ||
|
|
b27076b06f | ||
|
|
c3b3d0239f | ||
|
|
5874e58dd1 | ||
|
|
f3baaa6aa3 | ||
|
|
fc71c686b2 | ||
|
|
9a08f57155 | ||
|
|
6ebfe6d1c7 | ||
|
|
757a673416 | ||
|
|
0360ceeb6e | ||
|
|
04bb204191 | ||
|
|
77d63dfd96 | ||
|
|
67398d83cb | ||
|
|
e719f43485 | ||
|
|
5f6fd6df7a | ||
|
|
dcaaaee44c | ||
|
|
05beb6c64b | ||
|
|
16bc98bbce | ||
|
|
91c08143a8 | ||
|
|
b226d451f3 |
@@ -22,6 +22,7 @@
|
||||
"__DEV__": true
|
||||
},
|
||||
"rules": {
|
||||
"eol-last": ["error", "always"],
|
||||
"global-require": 0,
|
||||
"no-undefined": 0,
|
||||
"react/display-name": [2, { "ignoreTranspilerName": false }],
|
||||
|
||||
90
NOTICE.txt
90
NOTICE.txt
@@ -1855,30 +1855,6 @@ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLI
|
||||
|
||||
---
|
||||
|
||||
## react-native-image-gallery
|
||||
|
||||
This product contains a modified version of 'react-native-image-gallery' by Archriss.
|
||||
|
||||
Pure JavaScript image gallery component for iOS and Android
|
||||
|
||||
* HOMEPAGE:
|
||||
* https://github.com/archriss/react-native-image-gallery#readme
|
||||
|
||||
* LICENSE: ISC
|
||||
|
||||
Note: An original license file for this dependency is not available. We determined the type of license based on the package registry entry for this project. The following text has been prepared using a template from the SPDX Workgroup (https://spdx.org) for this type of license.
|
||||
|
||||
ISC License:
|
||||
|
||||
Copyright (c) 2004-2010 by Internet Systems Consortium, Inc. ("ISC")
|
||||
Copyright (c) 1995-2003 by Internet Software Consortium
|
||||
|
||||
Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted, provided that the above copyright notice and this permission notice appear in all copies.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
|
||||
---
|
||||
|
||||
## react-native-image-picker
|
||||
|
||||
This product contains 'react-native-image-picker' by Marc Shilling.
|
||||
@@ -2313,6 +2289,39 @@ SOFTWARE.
|
||||
|
||||
---
|
||||
|
||||
## react-native-redash
|
||||
|
||||
This product contains 'react-native-redash' by William Candillon.
|
||||
|
||||
The React Native Reanimated and Gesture Handler Toolbelt.
|
||||
|
||||
* HOMEPAGE:
|
||||
* https://github.com/wcandillon/react-native-redash
|
||||
|
||||
* LICENSE: MIT License
|
||||
|
||||
Copyright (c) 2020 William Candillon
|
||||
|
||||
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-safe-area
|
||||
|
||||
This product contains 'react-native-safe-area' by Masayuki Iwai.
|
||||
@@ -2441,6 +2450,39 @@ limitations under the License.
|
||||
|
||||
---
|
||||
|
||||
## react-native-share
|
||||
|
||||
This product contains 'react-native-share' by react-native-share.
|
||||
|
||||
React Native Share, a simple tool for share message and file to other apps.
|
||||
|
||||
* HOMEPAGE:
|
||||
* https://github.com/react-native-share/react-native-share
|
||||
|
||||
* LICENSE: The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2015 Esteban Fuentealba
|
||||
|
||||
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-slider
|
||||
|
||||
This product contains 'react-native-slider' by Jean Regisser.
|
||||
|
||||
@@ -63,7 +63,7 @@ We plan to add support for tablets in the future, but the timeline depends on ho
|
||||
|
||||
### I keep getting a message "Cannot connect to the server. Please check your server URL and internet connection."
|
||||
|
||||
This sometimes appears when there is an issue with the SSL certitificate configuration.
|
||||
This sometimes appears when there is an issue with the SSL certificate configuration.
|
||||
|
||||
To check that your SSL certificate is set up correctly, test the SSL certificate by visiting a site such as https://www.ssllabs.com/ssltest/index.html. If there’s an error about the missing chain or certificate path, there is likely an intermediate certificate missing that needs to be included.
|
||||
|
||||
|
||||
@@ -132,8 +132,8 @@ android {
|
||||
minSdkVersion rootProject.ext.minSdkVersion
|
||||
targetSdkVersion rootProject.ext.targetSdkVersion
|
||||
missingDimensionStrategy "RNNotifications.reactNativeVersion", "reactNative60"
|
||||
versionCode 334
|
||||
versionName "1.37.0"
|
||||
versionCode 341
|
||||
versionName "1.39.0"
|
||||
multiDexEnabled = true
|
||||
testBuildType System.getProperty('testBuildType', 'debug')
|
||||
testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner'
|
||||
@@ -250,7 +250,7 @@ dependencies {
|
||||
implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
|
||||
|
||||
implementation project(':reactnativenotifications')
|
||||
implementation 'com.google.firebase:firebase-messaging:17.3.4'
|
||||
implementation "com.google.firebase:firebase-messaging:$firebaseVersion"
|
||||
|
||||
// For animated GIF support
|
||||
implementation 'com.facebook.fresco:fresco:2.0.0'
|
||||
|
||||
@@ -26,8 +26,6 @@
|
||||
<meta-data android:name="firebase_analytics_collection_deactivated" android:value="true" />
|
||||
<meta-data android:name="android.content.APP_RESTRICTIONS"
|
||||
android:resource="@xml/app_restrictions" />
|
||||
|
||||
<meta-data android:name="com.wix.reactnativenotifications.gcmSenderId" android:value="184930218130\"/>
|
||||
<activity
|
||||
android:name=".MainActivity"
|
||||
android:label="@string/app_name"
|
||||
|
||||
@@ -300,11 +300,11 @@ public class CustomPushNotification extends PushNotification {
|
||||
|
||||
private Notification.MessagingStyle getMessagingStyle(Bundle bundle) {
|
||||
Notification.MessagingStyle messagingStyle;
|
||||
String senderId = bundle.getString("sender_id");
|
||||
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.P) {
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.P || senderId == null) {
|
||||
messagingStyle = new Notification.MessagingStyle("");
|
||||
} else {
|
||||
String senderId = bundle.getString("sender_id");
|
||||
Person sender = new Person.Builder()
|
||||
.setKey(senderId)
|
||||
.setName("")
|
||||
@@ -364,7 +364,7 @@ public class CustomPushNotification extends PushNotification {
|
||||
int bundleCount = bundleList.size() - 1;
|
||||
for (int i = bundleCount; i >= 0; i--) {
|
||||
Bundle data = bundleList.get(i);
|
||||
String message = data.getString("message");
|
||||
String message = data.getString("message", data.getString("body"));
|
||||
String senderId = data.getString("sender_id");
|
||||
if (senderId == null) {
|
||||
senderId = "sender_id";
|
||||
@@ -372,7 +372,7 @@ public class CustomPushNotification extends PushNotification {
|
||||
Bundle userInfoBundle = data.getBundle("userInfo");
|
||||
String senderName = getSenderName(data);
|
||||
if (userInfoBundle != null) {
|
||||
boolean localPushNotificationTest = userInfoBundle.getBoolean("localTest");
|
||||
boolean localPushNotificationTest = userInfoBundle.getBoolean("test");
|
||||
if (localPushNotificationTest) {
|
||||
senderName = "Test";
|
||||
}
|
||||
@@ -403,13 +403,15 @@ public class CustomPushNotification extends PushNotification {
|
||||
|
||||
NotificationChannel notificationChannel = mHighImportanceChannel;
|
||||
|
||||
boolean localPushNotificationTest = false;
|
||||
boolean testNotification = false;
|
||||
boolean localNotification = false;
|
||||
Bundle userInfoBundle = bundle.getBundle("userInfo");
|
||||
if (userInfoBundle != null) {
|
||||
localPushNotificationTest = userInfoBundle.getBoolean("localTest");
|
||||
testNotification = userInfoBundle.getBoolean("test");
|
||||
localNotification = userInfoBundle.getBoolean("local");
|
||||
}
|
||||
|
||||
if (mAppLifecycleFacade.isAppVisible() && !localPushNotificationTest) {
|
||||
if (mAppLifecycleFacade.isAppVisible() && !testNotification && !localNotification) {
|
||||
notificationChannel = mMinImportanceChannel;
|
||||
}
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ buildscript {
|
||||
targetSdkVersion = 29
|
||||
supportLibVersion = "28.0.0"
|
||||
kotlinVersion = "1.3.61"
|
||||
firebaseVersion = "21.0.0"
|
||||
RNNKotlinVersion = kotlinVersion
|
||||
|
||||
}
|
||||
@@ -20,7 +21,7 @@ buildscript {
|
||||
dependencies {
|
||||
classpath 'com.android.tools.build:gradle:3.5.3'
|
||||
classpath 'com.google.gms:google-services:4.2.0'
|
||||
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlinVersion"
|
||||
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlinVersion"
|
||||
|
||||
// NOTE: Do not place your application dependencies here; they belong
|
||||
// in the individual module build.gradle files
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
rootProject.name = 'Mattermost'
|
||||
include ':reactnativenotifications'
|
||||
project(':reactnativenotifications').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-notifications/android/app')
|
||||
project(':reactnativenotifications').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-notifications/lib/android/app')
|
||||
apply from: file("../node_modules/@react-native-community/cli-platform-android/native_modules.gradle"); applyNativeModulesSettingsGradle(settings)
|
||||
include ':app'
|
||||
|
||||
@@ -419,4 +419,4 @@ async function getProfilesFromPromises(promises: Array<Promise<ActionResult>>):
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,9 +9,15 @@ import {Preferences} from '@mm-redux/constants';
|
||||
import {getTheme} from '@mm-redux/selectors/entities/preferences';
|
||||
import EventEmmiter from '@mm-redux/utils/event_emitter';
|
||||
|
||||
import {DeviceTypes, NavigationTypes} from '@constants';
|
||||
import EphemeralStore from '@store/ephemeral_store';
|
||||
import Store from '@store/store';
|
||||
import {NavigationTypes} from '@constants';
|
||||
|
||||
Navigation.setDefaultOptions({
|
||||
layout: {
|
||||
orientation: [DeviceTypes.IS_TABLET ? 'all' : 'portrait'],
|
||||
},
|
||||
});
|
||||
|
||||
function getThemeFromState() {
|
||||
const state = Store.redux?.getState() || {};
|
||||
|
||||
@@ -213,6 +213,8 @@ export function handleSelectChannel(channelId) {
|
||||
|
||||
console.log('channel switch to', channel?.display_name, channelId, (Date.now() - dt), 'ms'); //eslint-disable-line
|
||||
}
|
||||
|
||||
return {data: true};
|
||||
};
|
||||
}
|
||||
|
||||
@@ -222,7 +224,7 @@ export function handleSelectChannelByName(channelName, teamName, errorHandler) {
|
||||
const {teams: currentTeams, currentTeamId} = state.entities.teams;
|
||||
const currentTeam = currentTeams[currentTeamId];
|
||||
const currentTeamName = currentTeam?.name;
|
||||
const response = await dispatch(getChannelByNameAndTeamName(teamName || currentTeamName, channelName));
|
||||
const response = await dispatch(getChannelByNameAndTeamName(teamName || currentTeamName, channelName, true));
|
||||
const {error, data: channel} = response;
|
||||
const currentChannelId = getCurrentChannelId(state);
|
||||
|
||||
@@ -249,16 +251,16 @@ export function handleSelectChannelByName(channelName, teamName, errorHandler) {
|
||||
if (!myMemberships[channel.id]) {
|
||||
const currentUserId = getCurrentUserId(state);
|
||||
console.log('joining channel', channel?.display_name, channel.id); //eslint-disable-line
|
||||
const result = await dispatch(joinChannel(currentUserId, teamName, channel.id));
|
||||
const result = await dispatch(joinChannel(currentUserId, '', channel.id));
|
||||
if (result.error || !result.data || !result.data.channel) {
|
||||
return {error};
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
||||
dispatch(handleSelectChannel(channel.id));
|
||||
}
|
||||
|
||||
return null;
|
||||
return {data: true};
|
||||
};
|
||||
}
|
||||
|
||||
@@ -293,6 +295,8 @@ export function markChannelViewedAndRead(channelId, previousChannelId, markOnSer
|
||||
const actions = markAsViewedAndReadBatch(state, channelId, previousChannelId, markOnServer);
|
||||
|
||||
dispatch(batchActions(actions, 'BATCH_MARK_CHANNEL_VIEWED_AND_READ'));
|
||||
|
||||
return {data: true};
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@ import {ViewTypes} from '@constants';
|
||||
import {ChannelTypes} from '@mm-redux/action_types';
|
||||
import postReducer from '@mm-redux/reducers/entities/posts';
|
||||
import initialState from '@store/initial_state';
|
||||
import {General} from '@mm-redux/constants';
|
||||
|
||||
const {
|
||||
handleSelectChannel,
|
||||
@@ -67,6 +68,10 @@ describe('Actions.Views.Channel', () => {
|
||||
type: MOCK_SELECT_CHANNEL_TYPE,
|
||||
data: 'selected-channel-id',
|
||||
});
|
||||
actions.joinChannel = jest.fn((userId, teamId, channelId) => ({
|
||||
type: 'MOCK_JOIN_CHANNEL',
|
||||
data: {channel: {id: channelId}},
|
||||
}));
|
||||
const postActions = require('./post');
|
||||
postActions.getPostsSince = jest.fn(() => {
|
||||
return {
|
||||
@@ -145,6 +150,7 @@ describe('Actions.Views.Channel', () => {
|
||||
channelSelectors.getMyChannelMember = jest.fn(() => ({data: {member: {}}}));
|
||||
|
||||
const appChannelSelectors = require('app/selectors/channel');
|
||||
const getChannelReachableOriginal = appChannelSelectors.getChannelReachable;
|
||||
appChannelSelectors.getChannelReachable = jest.fn(() => true);
|
||||
|
||||
test('handleSelectChannelByName success', async () => {
|
||||
@@ -205,6 +211,82 @@ describe('Actions.Views.Channel', () => {
|
||||
expect(receivedChannel).toBe(false);
|
||||
});
|
||||
|
||||
test('handleSelectChannelByName select channel that user is not a member of', async () => {
|
||||
actions.getChannelByNameAndTeamName = jest.fn(() => {
|
||||
return {
|
||||
type: MOCK_RECEIVE_CHANNEL_TYPE,
|
||||
data: {id: 'channel-id-3', name: 'channel-id-3', display_name: 'Test Channel', type: General.OPEN_CHANNEL},
|
||||
};
|
||||
});
|
||||
|
||||
store = mockStore(storeObj);
|
||||
|
||||
await store.dispatch(handleSelectChannelByName('channel-id-3', currentTeamName));
|
||||
|
||||
const storeActions = store.getActions();
|
||||
const receivedChannel = storeActions.some((action) => action.type === MOCK_RECEIVE_CHANNEL_TYPE);
|
||||
expect(receivedChannel).toBe(true);
|
||||
|
||||
expect(actions.joinChannel).toBeCalled();
|
||||
|
||||
const joinedChannel = storeActions.some((action) => action.type === 'MOCK_JOIN_CHANNEL' && action.data.channel.id === 'channel-id-3');
|
||||
expect(joinedChannel).toBe(true);
|
||||
});
|
||||
|
||||
test('handleSelectChannelByName select archived channel with ExperimentalViewArchivedChannels enabled', async () => {
|
||||
const archivedChannelStoreObj = {...storeObj};
|
||||
archivedChannelStoreObj.entities.general.config.ExperimentalViewArchivedChannels = 'true';
|
||||
store = mockStore(archivedChannelStoreObj);
|
||||
|
||||
appChannelSelectors.getChannelReachable = getChannelReachableOriginal;
|
||||
actions.getChannelByNameAndTeamName = jest.fn(() => {
|
||||
return {
|
||||
type: MOCK_RECEIVE_CHANNEL_TYPE,
|
||||
data: {id: 'channel-id-3', name: 'channel-id-3', display_name: 'Test Channel', type: General.OPEN_CHANNEL, delete_at: 100},
|
||||
};
|
||||
});
|
||||
channelSelectors.getChannelByName = jest.fn(() => {
|
||||
return {
|
||||
data: {id: 'channel-id-3', name: 'channel-id-3', display_name: 'Test Channel', type: General.OPEN_CHANNEL, delete_at: 100},
|
||||
};
|
||||
});
|
||||
const errorHandler = jest.fn();
|
||||
|
||||
await store.dispatch(handleSelectChannelByName('channel-id-3', currentTeamName, errorHandler));
|
||||
|
||||
const storeActions = store.getActions();
|
||||
const receivedChannel = storeActions.some((action) => action.type === MOCK_RECEIVE_CHANNEL_TYPE);
|
||||
expect(receivedChannel).toBe(true);
|
||||
expect(errorHandler).not.toBeCalled();
|
||||
});
|
||||
|
||||
test('handleSelectChannelByName select archived channel with ExperimentalViewArchivedChannels disabled', async () => {
|
||||
const noArchivedChannelStoreObj = {...storeObj};
|
||||
noArchivedChannelStoreObj.entities.general.config.ExperimentalViewArchivedChannels = 'false';
|
||||
store = mockStore(noArchivedChannelStoreObj);
|
||||
|
||||
appChannelSelectors.getChannelReachable = getChannelReachableOriginal;
|
||||
actions.getChannelByNameAndTeamName = jest.fn(() => {
|
||||
return {
|
||||
type: MOCK_RECEIVE_CHANNEL_TYPE,
|
||||
data: {id: 'channel-id-3', name: 'channel-id-3', display_name: 'Test Channel', type: General.OPEN_CHANNEL, delete_at: 100},
|
||||
};
|
||||
});
|
||||
channelSelectors.getChannelByName = jest.fn(() => {
|
||||
return {
|
||||
data: {id: 'channel-id-3', name: 'channel-id-3', display_name: 'Test Channel', type: General.OPEN_CHANNEL, delete_at: 100},
|
||||
};
|
||||
});
|
||||
const errorHandler = jest.fn();
|
||||
|
||||
await store.dispatch(handleSelectChannelByName('channel-id-3', currentTeamName, errorHandler));
|
||||
|
||||
const storeActions = store.getActions();
|
||||
const receivedChannel = storeActions.some((action) => action.type === MOCK_RECEIVE_CHANNEL_TYPE);
|
||||
expect(receivedChannel).toBe(true);
|
||||
expect(errorHandler).toBeCalled();
|
||||
});
|
||||
|
||||
test('loadPostsIfNecessaryWithRetry for the first time', async () => {
|
||||
store = mockStore(storeObj);
|
||||
|
||||
|
||||
@@ -9,6 +9,8 @@ import {Client4} from '@mm-redux/client';
|
||||
import {getPostIdsInCurrentChannel, makeGetPostIdsForThread} from '@mm-redux/selectors/entities/posts';
|
||||
|
||||
import {ViewTypes} from 'app/constants';
|
||||
import {EmojiIndicesByAlias, EmojiIndicesByUnicode, Emojis} from '@utils/emojis';
|
||||
import emojiRegex from 'emoji-regex';
|
||||
|
||||
const getPostIdsForThread = makeGetPostIdsForThread();
|
||||
|
||||
@@ -97,4 +99,42 @@ async function getCustomEmojiByName(name) {
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export function addRecentUsedEmojisInMessage(message) {
|
||||
return (dispatch) => {
|
||||
const RE_UNICODE_EMOJI = emojiRegex();
|
||||
const RE_NAMED_EMOJI = /(:([a-zA-Z0-9_-]+):)/g;
|
||||
const emojis = message.match(RE_UNICODE_EMOJI);
|
||||
const namedEmojis = message.match(RE_NAMED_EMOJI);
|
||||
function emojiUnicode(input) {
|
||||
const emoji = [];
|
||||
for (const i of input) {
|
||||
emoji.push(i.codePointAt(0).toString(16));
|
||||
}
|
||||
return emoji.join('-');
|
||||
}
|
||||
const emojisAvailableWithMattermost = [];
|
||||
if (emojis) {
|
||||
for (const emoji of emojis) {
|
||||
const unicode = emojiUnicode(emoji);
|
||||
const index = EmojiIndicesByUnicode.get(unicode || '');
|
||||
if (index) {
|
||||
emojisAvailableWithMattermost.push(Emojis[index].aliases[0]);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (namedEmojis) {
|
||||
for (const emoji of namedEmojis) {
|
||||
const index = EmojiIndicesByAlias.get(emoji.slice(1, -1));
|
||||
if (index) {
|
||||
emojisAvailableWithMattermost.push(Emojis[index].aliases[0]);
|
||||
}
|
||||
}
|
||||
}
|
||||
dispatch({
|
||||
type: ViewTypes.ADD_RECENT_EMOJI_ARRAY,
|
||||
emojis: emojisAvailableWithMattermost,
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@ import {isTimezoneEnabled} from '@mm-redux/selectors/entities/timezone';
|
||||
import {getCurrentUserId} from '@mm-redux/selectors/entities/users';
|
||||
|
||||
import {setAppCredentials} from 'app/init/credentials';
|
||||
import PushNotifications from 'app/push_notifications';
|
||||
import PushNotifications from '@init/push_notifications';
|
||||
import {getDeviceTimezone} from 'app/utils/timezone';
|
||||
import {setCSRFFromCookie} from 'app/utils/security';
|
||||
import {loadConfigAndLicense} from 'app/actions/views/root';
|
||||
@@ -94,11 +94,11 @@ export function scheduleExpiredNotification(intl) {
|
||||
});
|
||||
|
||||
if (expiresAt) {
|
||||
PushNotifications.localNotificationSchedule({
|
||||
date: new Date(expiresAt),
|
||||
message,
|
||||
PushNotifications.scheduleNotification({
|
||||
fireDate: expiresAt,
|
||||
body: message,
|
||||
userInfo: {
|
||||
localNotification: true,
|
||||
local: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -40,6 +40,7 @@ export function showPermalink(intl: typeof intlShape, teamName: string, postId:
|
||||
showModalOverCurrentContext(screen, passProps, options);
|
||||
}
|
||||
}
|
||||
return {};
|
||||
};
|
||||
}
|
||||
|
||||
@@ -48,4 +49,4 @@ export function closePermalink() {
|
||||
showingPermalink = false;
|
||||
return dispatch(selectFocusedPostId(''));
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -185,4 +185,4 @@ describe('Actions.Views.Post', () => {
|
||||
const receivedStatuses = actionTypes.filter((type) => type === UserTypes.RECEIVED_STATUSES);
|
||||
expect(receivedStatuses.length).toBe(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -66,17 +66,17 @@ export function loadConfigAndLicense() {
|
||||
export function loadFromPushNotification(notification) {
|
||||
return async (dispatch, getState) => {
|
||||
const state = getState();
|
||||
const {data} = notification;
|
||||
const {payload} = notification;
|
||||
const {currentTeamId, teams, myMembers: myTeamMembers} = state.entities.teams;
|
||||
const {channels} = state.entities.channels;
|
||||
|
||||
let channelId = '';
|
||||
let teamId = currentTeamId;
|
||||
if (data) {
|
||||
channelId = data.channel_id;
|
||||
if (payload) {
|
||||
channelId = payload.channel_id;
|
||||
|
||||
// when the notification does not have a team id is because its from a DM or GM
|
||||
teamId = data.team_id || currentTeamId;
|
||||
teamId = payload.team_id || currentTeamId;
|
||||
}
|
||||
|
||||
// load any missing data
|
||||
@@ -96,6 +96,8 @@ export function loadFromPushNotification(notification) {
|
||||
}
|
||||
|
||||
dispatch(handleSelectTeamAndChannel(teamId, channelId));
|
||||
|
||||
return {data: true};
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -7,8 +7,10 @@ import {lastChannelIdForTeam} from '@actions/helpers/channels';
|
||||
import {NavigationTypes} from '@constants';
|
||||
import {ChannelTypes, TeamTypes} from '@mm-redux/action_types';
|
||||
import {getMyTeams} from '@mm-redux/actions/teams';
|
||||
import {RequestStatus} from '@mm-redux/constants';
|
||||
import {Preferences, RequestStatus} from '@mm-redux/constants';
|
||||
import {getConfig} from '@mm-redux/selectors/entities/general';
|
||||
import {get as getPreference} from '@mm-redux/selectors/entities/preferences';
|
||||
import {getCurrentLocale} from 'app/selectors/i18n';
|
||||
import EventEmitter from '@mm-redux/utils/event_emitter';
|
||||
import {selectFirstAvailableTeam} from '@utils/teams';
|
||||
|
||||
@@ -50,6 +52,8 @@ export function selectDefaultTeam() {
|
||||
const state = getState();
|
||||
|
||||
const {ExperimentalPrimaryTeam} = getConfig(state);
|
||||
const locale = getCurrentLocale(state);
|
||||
const userTeamOrderPreference = getPreference(state, Preferences.TEAMS_ORDER, '', '');
|
||||
const {teams, myMembers} = state.entities.teams;
|
||||
const myTeams = Object.keys(teams).reduce((result, id) => {
|
||||
if (myMembers[id]) {
|
||||
@@ -59,7 +63,7 @@ export function selectDefaultTeam() {
|
||||
return result;
|
||||
}, []);
|
||||
|
||||
let defaultTeam = selectFirstAvailableTeam(myTeams, ExperimentalPrimaryTeam);
|
||||
let defaultTeam = selectFirstAvailableTeam(myTeams, locale, userTeamOrderPreference, ExperimentalPrimaryTeam);
|
||||
|
||||
if (defaultTeam) {
|
||||
dispatch(handleTeamChange(defaultTeam.id));
|
||||
@@ -75,7 +79,7 @@ export function selectDefaultTeam() {
|
||||
}
|
||||
|
||||
if (data) {
|
||||
defaultTeam = selectFirstAvailableTeam(data, ExperimentalPrimaryTeam);
|
||||
defaultTeam = selectFirstAvailableTeam(data, locale, userTeamOrderPreference, ExperimentalPrimaryTeam);
|
||||
}
|
||||
|
||||
if (defaultTeam) {
|
||||
|
||||
@@ -8,7 +8,7 @@ import {Client4} from '@mm-redux/client';
|
||||
import {getConfig} from '@mm-redux/selectors/entities/general';
|
||||
import {isMinimumServerVersion} from '@mm-redux/utils/helpers';
|
||||
|
||||
import PushNotifications from 'app/push_notifications';
|
||||
import PushNotifications from '@init/push_notifications';
|
||||
|
||||
const sortByNewest = (a, b) => {
|
||||
if (a.create_at > b.create_at) {
|
||||
@@ -56,11 +56,11 @@ export function scheduleExpiredNotification(intl) {
|
||||
if (expiresAt) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log('Schedule Session Expiry Local Push Notification', expiresAt);
|
||||
PushNotifications.localNotificationSchedule({
|
||||
date: new Date(expiresAt),
|
||||
message,
|
||||
PushNotifications.scheduleNotification({
|
||||
fireDate: expiresAt,
|
||||
body: message,
|
||||
userInfo: {
|
||||
localNotification: true,
|
||||
local: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -242,4 +242,4 @@ export function setCurrentUserStatusOffline() {
|
||||
}
|
||||
|
||||
/* eslint-disable no-import-assign */
|
||||
HelperActions.forceLogoutIfNecessary = forceLogoutIfNecessary;
|
||||
HelperActions.forceLogoutIfNecessary = forceLogoutIfNecessary;
|
||||
|
||||
@@ -176,4 +176,4 @@ describe('Websocket Chanel Events', () => {
|
||||
const {channels} = store.getState().entities.channels;
|
||||
assert.ok(Object.keys(channels).length);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -57,4 +57,4 @@ describe('Websocket General Events', () => {
|
||||
assert.ok(config.EnableCustomEmoji === 'true');
|
||||
assert.ok(config.EnableLinkPreviews === 'false');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -24,4 +24,4 @@ export function handleLicenseChangedEvent(msg: WebSocketMessage): GenericAction
|
||||
type: GeneralTypes.CLIENT_LICENSE_RECEIVED,
|
||||
data,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,4 +20,4 @@ export function handleGroupUpdatedEvent(msg: WebSocketMessage) {
|
||||
]));
|
||||
return {data: true};
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {loadChannelsForTeam} from '@actions/views/channel';
|
||||
import {getPosts} from '@actions/views/post';
|
||||
import {getPostsSince} from '@actions/views/post';
|
||||
import {loadMe} from '@actions/views/user';
|
||||
import {WebsocketEvents} from '@constants';
|
||||
import {ChannelTypes, GeneralTypes, PreferenceTypes, TeamTypes, UserTypes, RoleTypes} from '@mm-redux/action_types';
|
||||
@@ -44,6 +44,8 @@ import {handleAddEmoji, handleReactionAddedEvent, handleReactionRemovedEvent} fr
|
||||
import {handleRoleAddedEvent, handleRoleRemovedEvent, handleRoleUpdatedEvent} from './roles';
|
||||
import {handleLeaveTeamEvent, handleUpdateTeamEvent, handleTeamAddedEvent} from './teams';
|
||||
import {handleStatusChangedEvent, handleUserAddedEvent, handleUserRemovedEvent, handleUserRoleUpdated, handleUserUpdatedEvent} from './users';
|
||||
import {getChannelSinceValue} from '@utils/channels';
|
||||
import {getPostIdsInChannel} from '@mm-redux/selectors/entities/posts';
|
||||
|
||||
export function init(additionalOptions: any = {}) {
|
||||
return async (dispatch: DispatchFunc, getState: GetStateFunc) => {
|
||||
@@ -168,7 +170,9 @@ export function doReconnect(now: number) {
|
||||
if (!stillMemberOfCurrentChannel || !channelStillExists || (!viewArchivedChannels && channelStillExists.delete_at !== 0)) {
|
||||
EventEmitter.emit(General.SWITCH_TO_DEFAULT_CHANNEL, currentTeamId);
|
||||
} else {
|
||||
dispatch(getPosts(currentChannelId));
|
||||
const postIds = getPostIdsInChannel(state, currentChannelId);
|
||||
const since = getChannelSinceValue(state, currentChannelId, postIds);
|
||||
dispatch(getPostsSince(currentChannelId, since));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -44,4 +44,4 @@ describe('Websocket Integration Events', () => {
|
||||
assert.ok(dialog.trigger_id === 'sometriggerid');
|
||||
assert.ok(dialog.dialog);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -11,4 +11,4 @@ export function handleOpenDialogEvent(msg: WebSocketMessage) {
|
||||
dispatch({type: IntegrationTypes.RECEIVED_DIALOG, data: JSON.parse(data)});
|
||||
return {data: true};
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -206,4 +206,4 @@ export function handlePostUnread(msg: WebSocketMessage) {
|
||||
|
||||
return {data: null};
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -57,4 +57,4 @@ export function handlePreferencesDeletedEvent(msg: WebSocketMessage): GenericAct
|
||||
const preferences = JSON.parse(msg.data.preferences);
|
||||
|
||||
return {type: PreferenceTypes.DELETED_PREFERENCES, data: preferences};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -57,4 +57,4 @@ describe('Websocket Reaction Events', () => {
|
||||
assert.ok(emojis);
|
||||
assert.ok(emojis[created.id]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -38,4 +38,4 @@ export function handleReactionRemovedEvent(msg: WebSocketMessage): GenericAction
|
||||
type: PostTypes.REACTION_DELETED,
|
||||
data: reaction,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,4 +30,4 @@ export function handleRoleUpdatedEvent(msg: WebSocketMessage): GenericAction {
|
||||
type: RoleTypes.RECEIVED_ROLE,
|
||||
data: role,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -103,4 +103,4 @@ describe('Websocket Team Events', () => {
|
||||
const {myMembers} = store.getState().entities.teams;
|
||||
assert.ifError(myMembers[team.id]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -103,4 +103,4 @@ export function handleTeamAddedEvent(msg: WebSocketMessage) {
|
||||
|
||||
return {data: true};
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -91,4 +91,4 @@ describe('Websocket User Events', () => {
|
||||
assert.strictEqual(profiles[user.id].first_name, 'tester4');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -202,4 +202,4 @@ export function handleUserUpdatedEvent(msg: WebSocketMessage) {
|
||||
}
|
||||
return {data: true};
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -177,8 +177,9 @@ describe('Actions.Websocket doReconnect', () => {
|
||||
const timestamp = 1000;
|
||||
const expectedActions = [
|
||||
GeneralTypes.WEBSOCKET_SUCCESS,
|
||||
];
|
||||
const expectedMissingActions = [
|
||||
'BATCH_WS_RECONNECT',
|
||||
'BATCH_GET_POSTS',
|
||||
];
|
||||
|
||||
mockConfigRequest();
|
||||
@@ -197,6 +198,7 @@ describe('Actions.Websocket doReconnect', () => {
|
||||
await TestHelper.wait(300);
|
||||
const actionTypes = testStore.getActions().map((a) => a.type);
|
||||
expect(actionTypes).toEqual(expectedActions);
|
||||
expect(actionTypes).not.toEqual(expect.arrayContaining(expectedMissingActions));
|
||||
});
|
||||
|
||||
it('handle doReconnect after the current channel was archived or the user left it', async () => {
|
||||
@@ -217,7 +219,7 @@ describe('Actions.Websocket doReconnect', () => {
|
||||
'BATCH_WS_RECONNECT',
|
||||
];
|
||||
const expectedMissingActions = [
|
||||
'BATCH_GET_POSTS',
|
||||
'BATCH_GET_POSTS_SINCE',
|
||||
];
|
||||
|
||||
mockConfigRequest();
|
||||
@@ -259,6 +261,8 @@ describe('Actions.Websocket doReconnect', () => {
|
||||
const timestamp = 1000;
|
||||
const expectedActions = [
|
||||
GeneralTypes.WEBSOCKET_SUCCESS,
|
||||
];
|
||||
const expectedMissingActions = [
|
||||
'BATCH_WS_RECONNECT',
|
||||
];
|
||||
|
||||
@@ -279,6 +283,7 @@ describe('Actions.Websocket doReconnect', () => {
|
||||
|
||||
const actions = testStore.getActions().map((a) => a.type);
|
||||
expect(actions).toEqual(expect.arrayContaining(expectedActions));
|
||||
expect(actions).not.toEqual(expect.arrayContaining(expectedMissingActions));
|
||||
});
|
||||
|
||||
it('handle doReconnect after the current channel was archived and setting is off', async () => {
|
||||
@@ -303,7 +308,7 @@ describe('Actions.Websocket doReconnect', () => {
|
||||
'BATCH_WS_RECONNECT',
|
||||
];
|
||||
const expectedMissingActions = [
|
||||
'BATCH_GET_POSTS',
|
||||
'BATCH_GET_POSTS_SINCE',
|
||||
];
|
||||
|
||||
mockConfigRequest({ExperimentalViewArchivedChannels: 'false'});
|
||||
@@ -337,7 +342,7 @@ describe('Actions.Websocket doReconnect', () => {
|
||||
'BATCH_WS_RECONNECT',
|
||||
];
|
||||
const expectedMissingActions = [
|
||||
'BATCH_GET_POSTS',
|
||||
'BATCH_GET_POSTS_SINCE',
|
||||
];
|
||||
|
||||
mockConfigRequest();
|
||||
|
||||
@@ -15,6 +15,7 @@ exports[`Badge should match snapshot 1`] = `
|
||||
},
|
||||
]
|
||||
}
|
||||
testID="badge"
|
||||
>
|
||||
<View
|
||||
style={
|
||||
@@ -70,6 +71,7 @@ exports[`Badge should match snapshot 1`] = `
|
||||
},
|
||||
]
|
||||
}
|
||||
testID="badge.unread_count"
|
||||
>
|
||||
99+
|
||||
</Text>
|
||||
|
||||
@@ -1,121 +1,133 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`AnnouncementBanner should match snapshot 1`] = `
|
||||
<ForwardRef(AnimatedComponentWrapper)
|
||||
style={
|
||||
Array [
|
||||
Object {
|
||||
"overflow": "hidden",
|
||||
"paddingHorizontal": 10,
|
||||
"position": "absolute",
|
||||
"top": 0,
|
||||
"width": "100%",
|
||||
<Unknown
|
||||
bannerColor="#ddd"
|
||||
bannerDismissed={false}
|
||||
bannerEnabled={true}
|
||||
bannerText="Banner Text"
|
||||
bannerTextColor="#fff"
|
||||
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],
|
||||
},
|
||||
Object {
|
||||
"backgroundColor": "#ddd",
|
||||
"height": 0,
|
||||
},
|
||||
]
|
||||
}
|
||||
>
|
||||
<ForwardRef
|
||||
onPress={[Function]}
|
||||
style={
|
||||
Array [
|
||||
Object {
|
||||
"alignItems": "center",
|
||||
"flex": 1,
|
||||
"flexDirection": "row",
|
||||
},
|
||||
null,
|
||||
]
|
||||
"locale": "en",
|
||||
"messages": Object {},
|
||||
"now": [Function],
|
||||
"onError": [Function],
|
||||
"textComponent": "span",
|
||||
"timeZone": null,
|
||||
}
|
||||
>
|
||||
<Text
|
||||
ellipsizeMode="tail"
|
||||
numberOfLines={1}
|
||||
style={
|
||||
Array [
|
||||
Object {
|
||||
"flex": 1,
|
||||
"fontSize": 14,
|
||||
"marginRight": 5,
|
||||
},
|
||||
Object {
|
||||
"color": "#fff",
|
||||
},
|
||||
]
|
||||
}
|
||||
>
|
||||
<RemoveMarkdown
|
||||
value="Banner Text"
|
||||
/>
|
||||
</Text>
|
||||
<CompassIcon
|
||||
color="#fff"
|
||||
name="info-outline"
|
||||
size={16}
|
||||
/>
|
||||
</ForwardRef>
|
||||
</ForwardRef(AnimatedComponentWrapper)>
|
||||
}
|
||||
theme={
|
||||
Object {
|
||||
"awayIndicator": "#ffbc42",
|
||||
"buttonBg": "#166de0",
|
||||
"buttonColor": "#ffffff",
|
||||
"centerChannelBg": "#ffffff",
|
||||
"centerChannelColor": "#3d3c40",
|
||||
"codeTheme": "github",
|
||||
"dndIndicator": "#f74343",
|
||||
"errorTextColor": "#fd5960",
|
||||
"linkColor": "#2389d7",
|
||||
"mentionBg": "#ffffff",
|
||||
"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",
|
||||
}
|
||||
}
|
||||
/>
|
||||
`;
|
||||
|
||||
exports[`AnnouncementBanner should match snapshot 2`] = `
|
||||
<ForwardRef(AnimatedComponentWrapper)
|
||||
style={
|
||||
Array [
|
||||
Object {
|
||||
"overflow": "hidden",
|
||||
"paddingHorizontal": 10,
|
||||
"position": "absolute",
|
||||
"top": 0,
|
||||
"width": "100%",
|
||||
<Unknown
|
||||
bannerColor="#ddd"
|
||||
bannerDismissed={false}
|
||||
bannerEnabled={false}
|
||||
bannerText="Banner Text"
|
||||
bannerTextColor="#fff"
|
||||
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],
|
||||
},
|
||||
Object {
|
||||
"backgroundColor": "#ddd",
|
||||
"height": 0,
|
||||
},
|
||||
]
|
||||
}
|
||||
>
|
||||
<ForwardRef
|
||||
onPress={[Function]}
|
||||
style={
|
||||
Array [
|
||||
Object {
|
||||
"alignItems": "center",
|
||||
"flex": 1,
|
||||
"flexDirection": "row",
|
||||
},
|
||||
null,
|
||||
]
|
||||
"locale": "en",
|
||||
"messages": Object {},
|
||||
"now": [Function],
|
||||
"onError": [Function],
|
||||
"textComponent": "span",
|
||||
"timeZone": null,
|
||||
}
|
||||
>
|
||||
<Text
|
||||
ellipsizeMode="tail"
|
||||
numberOfLines={1}
|
||||
style={
|
||||
Array [
|
||||
Object {
|
||||
"flex": 1,
|
||||
"fontSize": 14,
|
||||
"marginRight": 5,
|
||||
},
|
||||
Object {
|
||||
"color": "#fff",
|
||||
},
|
||||
]
|
||||
}
|
||||
>
|
||||
<RemoveMarkdown
|
||||
value="Banner Text"
|
||||
/>
|
||||
</Text>
|
||||
<CompassIcon
|
||||
color="#fff"
|
||||
name="info-outline"
|
||||
size={16}
|
||||
/>
|
||||
</ForwardRef>
|
||||
</ForwardRef(AnimatedComponentWrapper)>
|
||||
}
|
||||
theme={
|
||||
Object {
|
||||
"awayIndicator": "#ffbc42",
|
||||
"buttonBg": "#166de0",
|
||||
"buttonColor": "#ffffff",
|
||||
"centerChannelBg": "#ffffff",
|
||||
"centerChannelColor": "#3d3c40",
|
||||
"codeTheme": "github",
|
||||
"dndIndicator": "#f74343",
|
||||
"errorTextColor": "#fd5960",
|
||||
"linkColor": "#2389d7",
|
||||
"mentionBg": "#ffffff",
|
||||
"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",
|
||||
}
|
||||
}
|
||||
/>
|
||||
`;
|
||||
|
||||
@@ -1,62 +1,33 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React, {PureComponent} from 'react';
|
||||
import React, {useEffect, useRef, useState} from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {
|
||||
Animated,
|
||||
InteractionManager,
|
||||
StyleSheet,
|
||||
Text,
|
||||
TouchableOpacity,
|
||||
} from 'react-native';
|
||||
import {intlShape} from 'react-intl';
|
||||
import {injectIntl} from 'react-intl';
|
||||
import {useSafeAreaInsets} from 'react-native-safe-area-context';
|
||||
|
||||
import {goToScreen} from '@actions/navigation';
|
||||
import CompassIcon from '@components/compass_icon';
|
||||
import RemoveMarkdown from '@components/remove_markdown';
|
||||
import {paddingHorizontal as padding} from '@components/safe_area_view/iphone_x_spacing';
|
||||
import {goToScreen} from '@actions/navigation';
|
||||
import {ViewTypes} from '@constants';
|
||||
import EventEmitter from '@mm-redux/utils/event_emitter';
|
||||
|
||||
const {View: AnimatedView} = Animated;
|
||||
|
||||
export default class AnnouncementBanner extends PureComponent {
|
||||
static propTypes = {
|
||||
bannerColor: PropTypes.string,
|
||||
bannerDismissed: PropTypes.bool,
|
||||
bannerEnabled: PropTypes.bool,
|
||||
bannerText: PropTypes.string,
|
||||
bannerTextColor: PropTypes.string,
|
||||
theme: PropTypes.object.isRequired,
|
||||
isLandscape: PropTypes.bool.isRequired,
|
||||
};
|
||||
|
||||
static contextTypes = {
|
||||
intl: intlShape,
|
||||
};
|
||||
|
||||
state = {
|
||||
bannerHeight: new Animated.Value(0),
|
||||
};
|
||||
|
||||
componentDidMount() {
|
||||
const {bannerDismissed, bannerEnabled, bannerText} = this.props;
|
||||
const showBanner = bannerEnabled && !bannerDismissed && Boolean(bannerText);
|
||||
this.toggleBanner(showBanner);
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
if (this.props.bannerText !== prevProps.bannerText ||
|
||||
this.props.bannerEnabled !== prevProps.bannerEnabled ||
|
||||
this.props.bannerDismissed !== prevProps.bannerDismissed
|
||||
) {
|
||||
const showBanner = this.props.bannerEnabled && !this.props.bannerDismissed && Boolean(this.props.bannerText);
|
||||
this.toggleBanner(showBanner);
|
||||
}
|
||||
}
|
||||
|
||||
handlePress = () => {
|
||||
const {intl} = this.context;
|
||||
const AnnouncementBanner = injectIntl((props) => {
|
||||
const {bannerColor, bannerDismissed, bannerEnabled, bannerText, bannerTextColor, intl} = props;
|
||||
const insets = useSafeAreaInsets();
|
||||
const translateY = useRef(new Animated.Value(0)).current;
|
||||
const [visible, setVisible] = useState(false);
|
||||
const [navHeight, setNavHeight] = useState(0);
|
||||
|
||||
const handlePress = () => {
|
||||
const screen = 'ExpandedAnnouncementBanner';
|
||||
const title = intl.formatMessage({
|
||||
id: 'mobile.announcement_banner.title',
|
||||
@@ -66,80 +37,88 @@ export default class AnnouncementBanner extends PureComponent {
|
||||
goToScreen(screen, title);
|
||||
};
|
||||
|
||||
toggleBanner = (show = true) => {
|
||||
const value = show ? 38 : 0;
|
||||
if (show && !this.state.visible) {
|
||||
this.setState({visible: show});
|
||||
}
|
||||
useEffect(() => {
|
||||
const handleNavbarHeight = (height) => {
|
||||
setNavHeight(height);
|
||||
};
|
||||
|
||||
InteractionManager.runAfterInteractions(() => {
|
||||
Animated.timing(this.state.bannerHeight, {
|
||||
toValue: value,
|
||||
duration: 350,
|
||||
useNativeDriver: false,
|
||||
}).start(() => {
|
||||
if (this.state.visible !== show) {
|
||||
this.setState({visible: show});
|
||||
}
|
||||
});
|
||||
});
|
||||
EventEmitter.on(ViewTypes.CHANNEL_NAV_BAR_CHANGED, handleNavbarHeight);
|
||||
|
||||
return () => EventEmitter.off(ViewTypes.CHANNEL_NAV_BAR_CHANGED, handleNavbarHeight);
|
||||
}, [insets]);
|
||||
|
||||
useEffect(() => {
|
||||
const showBanner = bannerEnabled && !bannerDismissed && Boolean(bannerText);
|
||||
setVisible(showBanner);
|
||||
EventEmitter.emit(ViewTypes.INDICATOR_BAR_VISIBLE, showBanner);
|
||||
}, [bannerDismissed, bannerEnabled, bannerText]);
|
||||
|
||||
useEffect(() => {
|
||||
Animated.timing(translateY, {
|
||||
toValue: visible ? navHeight : insets.top,
|
||||
duration: 50,
|
||||
useNativeDriver: true,
|
||||
}).start();
|
||||
}, [visible, navHeight]);
|
||||
|
||||
if (!visible) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const bannerStyle = {
|
||||
backgroundColor: bannerColor,
|
||||
height: ViewTypes.INDICATOR_BAR_HEIGHT,
|
||||
transform: [{translateY}],
|
||||
};
|
||||
|
||||
render() {
|
||||
if (!this.state.visible) {
|
||||
return null;
|
||||
}
|
||||
const bannerTextStyle = {
|
||||
color: bannerTextColor,
|
||||
};
|
||||
|
||||
const {bannerHeight} = this.state;
|
||||
const {
|
||||
bannerColor,
|
||||
bannerText,
|
||||
bannerTextColor,
|
||||
isLandscape,
|
||||
} = this.props;
|
||||
|
||||
const bannerStyle = {
|
||||
backgroundColor: bannerColor,
|
||||
height: bannerHeight,
|
||||
};
|
||||
|
||||
const bannerTextStyle = {
|
||||
color: bannerTextColor,
|
||||
};
|
||||
|
||||
return (
|
||||
<AnimatedView
|
||||
style={[style.bannerContainer, bannerStyle]}
|
||||
return (
|
||||
<AnimatedView
|
||||
style={[style.bannerContainer, bannerStyle]}
|
||||
>
|
||||
<TouchableOpacity
|
||||
onPress={handlePress}
|
||||
style={[style.wrapper, {marginLeft: insets.left, marginRight: insets.right}]}
|
||||
>
|
||||
<TouchableOpacity
|
||||
onPress={this.handlePress}
|
||||
style={[style.wrapper, padding(isLandscape)]}
|
||||
<Text
|
||||
ellipsizeMode='tail'
|
||||
numberOfLines={1}
|
||||
style={[style.bannerText, bannerTextStyle]}
|
||||
>
|
||||
<Text
|
||||
ellipsizeMode='tail'
|
||||
numberOfLines={1}
|
||||
style={[style.bannerText, bannerTextStyle]}
|
||||
>
|
||||
<RemoveMarkdown value={bannerText}/>
|
||||
</Text>
|
||||
<CompassIcon
|
||||
color={bannerTextColor}
|
||||
name='info-outline'
|
||||
size={16}
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
</AnimatedView>
|
||||
);
|
||||
}
|
||||
}
|
||||
<RemoveMarkdown value={bannerText}/>
|
||||
</Text>
|
||||
<CompassIcon
|
||||
color={bannerTextColor}
|
||||
name='information-outline'
|
||||
size={16}
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
</AnimatedView>
|
||||
);
|
||||
});
|
||||
|
||||
AnnouncementBanner.propTypes = {
|
||||
bannerColor: PropTypes.string,
|
||||
bannerDismissed: PropTypes.bool,
|
||||
bannerEnabled: PropTypes.bool,
|
||||
bannerText: PropTypes.string,
|
||||
bannerTextColor: PropTypes.string,
|
||||
};
|
||||
|
||||
export default AnnouncementBanner;
|
||||
|
||||
const style = StyleSheet.create({
|
||||
bannerContainer: {
|
||||
elevation: 2,
|
||||
paddingHorizontal: 10,
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
overflow: 'hidden',
|
||||
width: '100%',
|
||||
zIndex: 2,
|
||||
},
|
||||
wrapper: {
|
||||
alignItems: 'center',
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
import {shallow} from 'enzyme';
|
||||
import {shallowWithIntl} from 'test/intl-test-helper';
|
||||
|
||||
import Preferences from '@mm-redux/constants/preferences';
|
||||
|
||||
@@ -18,11 +18,10 @@ describe('AnnouncementBanner', () => {
|
||||
bannerText: 'Banner Text',
|
||||
bannerTextColor: '#fff',
|
||||
theme: Preferences.THEMES.default,
|
||||
isLandscape: false,
|
||||
};
|
||||
|
||||
test('should match snapshot', () => {
|
||||
const wrapper = shallow(
|
||||
const wrapper = shallowWithIntl(
|
||||
<AnnouncementBanner {...baseProps}/>,
|
||||
);
|
||||
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
import {connect} from 'react-redux';
|
||||
|
||||
import {getConfig, getLicense} from '@mm-redux/selectors/entities/general';
|
||||
import {getTheme} from '@mm-redux/selectors/entities/preferences';
|
||||
|
||||
import {isLandscape} from 'app/selectors/device';
|
||||
|
||||
@@ -21,7 +20,6 @@ function mapStateToProps(state) {
|
||||
bannerEnabled: config.EnableBanner === 'true' && license.IsLicensed === 'true',
|
||||
bannerText: config.BannerText,
|
||||
bannerTextColor: config.BannerTextColor || '#000',
|
||||
theme: getTheme(state),
|
||||
isLandscape: isLandscape(state),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -35,4 +35,4 @@ const AppVersion = () => {
|
||||
);
|
||||
};
|
||||
|
||||
export default AppVersion;
|
||||
export default AppVersion;
|
||||
|
||||
@@ -41,4 +41,4 @@ describe('AtMention', () => {
|
||||
wrapper.setState({user: {username: 'Victor.Welch'}});
|
||||
expect(wrapper.getElement()).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -35,7 +35,6 @@ export default class AtMention extends PureComponent {
|
||||
teamMembers: PropTypes.array,
|
||||
theme: PropTypes.object.isRequired,
|
||||
value: PropTypes.string,
|
||||
isLandscape: PropTypes.bool.isRequired,
|
||||
nestedScrollEnabled: PropTypes.bool,
|
||||
useChannelMentions: PropTypes.bool.isRequired,
|
||||
groups: PropTypes.array,
|
||||
@@ -55,51 +54,38 @@ export default class AtMention extends PureComponent {
|
||||
sections: [],
|
||||
};
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps) {
|
||||
const {groups, inChannel, outChannel, teamMembers, isSearch, matchTerm, requestStatus} = nextProps;
|
||||
|
||||
// Not invoked, render nothing.
|
||||
if (matchTerm === null) {
|
||||
this.setState({
|
||||
sections: [],
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (matchTerm !== this.props.matchTerm) {
|
||||
const sections = this.buildSections(nextProps);
|
||||
this.setState({
|
||||
sections,
|
||||
});
|
||||
|
||||
this.props.onResultCountChange(sections.reduce((total, section) => total + section.data.length, 0));
|
||||
|
||||
// Update user autocomplete list with results of server request
|
||||
const {currentTeamId, currentChannelId} = this.props;
|
||||
const channelId = isSearch ? '' : currentChannelId;
|
||||
this.props.actions.autocompleteUsers(matchTerm, currentTeamId, channelId);
|
||||
return;
|
||||
}
|
||||
|
||||
// Server request is complete
|
||||
if (
|
||||
groups !== this.props.groups ||
|
||||
(
|
||||
requestStatus !== RequestStatus.STARTED &&
|
||||
(inChannel !== this.props.inChannel || outChannel !== this.props.outChannel || teamMembers !== this.props.teamMembers)
|
||||
)
|
||||
) {
|
||||
const sections = this.buildSections(nextProps);
|
||||
this.setState({
|
||||
sections,
|
||||
});
|
||||
|
||||
this.props.onResultCountChange(sections.reduce((total, section) => total + section.data.length, 0));
|
||||
}
|
||||
updateSections(sections) {
|
||||
this.setState({sections});
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps, prevState) {
|
||||
if (this.props.matchTerm !== prevProps.matchTerm) {
|
||||
if (this.props.matchTerm === null) {
|
||||
this.updateSections([]);
|
||||
} else {
|
||||
const sections = this.buildSections(this.props);
|
||||
this.updateSections(sections);
|
||||
|
||||
this.props.onResultCountChange(sections.reduce((total, section) => total + section.data.length, 0));
|
||||
|
||||
// Update user autocomplete list with results of server request
|
||||
const {currentTeamId, currentChannelId} = this.props;
|
||||
const channelId = this.props.isSearch ? '' : currentChannelId;
|
||||
this.props.actions.autocompleteUsers(this.props.matchTerm, currentTeamId, channelId);
|
||||
}
|
||||
}
|
||||
if (this.props.matchTerm !== null && this.props.matchTerm === prevProps.matchTerm) {
|
||||
if (
|
||||
this.props.groups !== prevProps.groups ||
|
||||
(
|
||||
this.props.requestStatus !== RequestStatus.STARTED &&
|
||||
(this.props.inChannel !== prevProps.inChannel || this.props.outChannel !== prevProps.outChannel || this.props.teamMembers !== prevProps.teamMembers)
|
||||
)
|
||||
) {
|
||||
const sections = this.buildSections(this.props);
|
||||
this.updateSections(sections);
|
||||
this.props.onResultCountChange(sections.reduce((total, section) => total + section.data.length, 0));
|
||||
}
|
||||
}
|
||||
if (prevState.sections.length !== this.state.sections.length && this.state.sections.length === 0) {
|
||||
this.props.onResultCountChange(0);
|
||||
}
|
||||
@@ -213,7 +199,6 @@ export default class AtMention extends PureComponent {
|
||||
id={section.id}
|
||||
defaultMessage={section.defaultMessage}
|
||||
theme={this.props.theme}
|
||||
isLandscape={this.props.isLandscape}
|
||||
isFirstSection={isFirstSection}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -10,7 +10,6 @@ import {getLicense} from '@mm-redux/selectors/entities/general';
|
||||
import {getCurrentChannelId, getDefaultChannel} from '@mm-redux/selectors/entities/channels';
|
||||
import {getAssociatedGroupsForReference, searchAssociatedGroupsForReferenceLocal} from '@mm-redux/selectors/entities/groups';
|
||||
import {getCurrentTeamId} from '@mm-redux/selectors/entities/teams';
|
||||
import {isLandscape} from 'app/selectors/device';
|
||||
|
||||
import {
|
||||
filterMembersInChannel,
|
||||
@@ -76,7 +75,6 @@ function mapStateToProps(state, ownProps) {
|
||||
outChannel,
|
||||
requestStatus: state.requests.users.autocompleteUsers.status,
|
||||
theme: getTheme(state),
|
||||
isLandscape: isLandscape(state),
|
||||
useChannelMentions,
|
||||
groups,
|
||||
};
|
||||
|
||||
@@ -1,56 +1,18 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React, {PureComponent} from 'react';
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {
|
||||
Text,
|
||||
View,
|
||||
} from 'react-native';
|
||||
import {useSafeAreaInsets} from 'react-native-safe-area-context';
|
||||
|
||||
import CompassIcon from '@components/compass_icon';
|
||||
import TouchableWithFeedback from '@components/touchable_with_feedback';
|
||||
import {makeStyleSheetFromTheme, changeOpacity} from '@utils/theme';
|
||||
|
||||
export default class GroupMentionItem extends PureComponent {
|
||||
static propTypes = {
|
||||
completeHandle: PropTypes.string.isRequired,
|
||||
onPress: PropTypes.func.isRequired,
|
||||
theme: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
completeMention = () => {
|
||||
const {onPress, completeHandle} = this.props;
|
||||
onPress(completeHandle);
|
||||
};
|
||||
|
||||
render() {
|
||||
const {
|
||||
completeHandle,
|
||||
theme,
|
||||
} = this.props;
|
||||
|
||||
const style = getStyleFromTheme(theme);
|
||||
|
||||
return (
|
||||
<TouchableWithFeedback
|
||||
onPress={this.completeMention}
|
||||
style={style.row}
|
||||
type={'opacity'}
|
||||
>
|
||||
<View style={style.rowPicture}>
|
||||
<CompassIcon
|
||||
name='account-group-outline'
|
||||
style={style.rowIcon}
|
||||
/>
|
||||
</View>
|
||||
<Text style={style.rowUsername}>{`@${completeHandle}`}</Text>
|
||||
<Text style={style.rowUsername}>{' - '}</Text>
|
||||
<Text style={style.rowFullname}>{`${completeHandle}`}</Text>
|
||||
</TouchableWithFeedback>
|
||||
);
|
||||
}
|
||||
}
|
||||
const getStyleFromTheme = makeStyleSheetFromTheme((theme) => {
|
||||
return {
|
||||
row: {
|
||||
@@ -85,3 +47,40 @@ const getStyleFromTheme = makeStyleSheetFromTheme((theme) => {
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
const GroupMentionItem = (props) => {
|
||||
const insets = useSafeAreaInsets();
|
||||
const {onPress, completeHandle, theme} = props;
|
||||
|
||||
const completeMention = () => {
|
||||
onPress(completeHandle);
|
||||
};
|
||||
|
||||
const style = getStyleFromTheme(theme);
|
||||
|
||||
return (
|
||||
<TouchableWithFeedback
|
||||
onPress={completeMention}
|
||||
style={[style.row, {marginLeft: insets.left, marginRight: insets.right}]}
|
||||
type={'opacity'}
|
||||
>
|
||||
<View style={style.rowPicture}>
|
||||
<CompassIcon
|
||||
name='account-group-outline'
|
||||
style={style.rowIcon}
|
||||
/>
|
||||
</View>
|
||||
<Text style={style.rowUsername}>{`@${completeHandle}`}</Text>
|
||||
<Text style={style.rowUsername}>{' - '}</Text>
|
||||
<Text style={style.rowFullname}>{`${completeHandle}`}</Text>
|
||||
</TouchableWithFeedback>
|
||||
);
|
||||
};
|
||||
|
||||
GroupMentionItem.propTypes = {
|
||||
completeHandle: PropTypes.string.isRequired,
|
||||
onPress: PropTypes.func.isRequired,
|
||||
theme: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
export default GroupMentionItem;
|
||||
|
||||
@@ -1,129 +1,16 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React, {PureComponent} from 'react';
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {
|
||||
Text,
|
||||
View,
|
||||
} from 'react-native';
|
||||
import {Text, View} from 'react-native';
|
||||
import {useSafeAreaInsets} from 'react-native-safe-area-context';
|
||||
|
||||
import ProfilePicture from 'app/components/profile_picture';
|
||||
import {paddingHorizontal as padding} from 'app/components/safe_area_view/iphone_x_spacing';
|
||||
import {BotTag, GuestTag} from 'app/components/tag';
|
||||
import TouchableWithFeedback from 'app/components/touchable_with_feedback';
|
||||
import {makeStyleSheetFromTheme, changeOpacity} from 'app/utils/theme';
|
||||
import FormattedText from 'app/components/formatted_text';
|
||||
|
||||
export default class AtMentionItem extends PureComponent {
|
||||
static propTypes = {
|
||||
firstName: PropTypes.string,
|
||||
lastName: PropTypes.string,
|
||||
nickname: PropTypes.string,
|
||||
onPress: PropTypes.func.isRequired,
|
||||
userId: PropTypes.string.isRequired,
|
||||
username: PropTypes.string,
|
||||
isGuest: PropTypes.bool,
|
||||
isBot: PropTypes.bool,
|
||||
theme: PropTypes.object.isRequired,
|
||||
isLandscape: PropTypes.bool.isRequired,
|
||||
isCurrentUser: PropTypes.bool.isRequired,
|
||||
showFullName: PropTypes.string,
|
||||
testID: PropTypes.string,
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
firstName: '',
|
||||
lastName: '',
|
||||
};
|
||||
|
||||
completeMention = () => {
|
||||
const {onPress, username} = this.props;
|
||||
onPress(username);
|
||||
};
|
||||
|
||||
renderNameBlock = () => {
|
||||
let name = '';
|
||||
const {showFullName, firstName, lastName, nickname} = this.props;
|
||||
const hasNickname = nickname.length > 0;
|
||||
|
||||
if (showFullName === 'true') {
|
||||
name += `${firstName} ${lastName} `;
|
||||
}
|
||||
|
||||
if (hasNickname) {
|
||||
name += `(${nickname})`;
|
||||
}
|
||||
|
||||
return name.trim();
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
userId,
|
||||
username,
|
||||
theme,
|
||||
isBot,
|
||||
isLandscape,
|
||||
isGuest,
|
||||
isCurrentUser,
|
||||
testID,
|
||||
} = this.props;
|
||||
|
||||
const style = getStyleFromTheme(theme);
|
||||
const name = this.renderNameBlock();
|
||||
|
||||
return (
|
||||
<TouchableWithFeedback
|
||||
testID={testID}
|
||||
key={userId}
|
||||
onPress={this.completeMention}
|
||||
style={padding(isLandscape)}
|
||||
underlayColor={changeOpacity(theme.buttonBg, 0.08)}
|
||||
type={'native'}
|
||||
>
|
||||
<View style={style.row}>
|
||||
<View style={style.rowPicture}>
|
||||
<ProfilePicture
|
||||
userId={userId}
|
||||
theme={theme}
|
||||
size={24}
|
||||
status={null}
|
||||
showStatus={false}
|
||||
/>
|
||||
</View>
|
||||
<BotTag
|
||||
show={isBot}
|
||||
theme={theme}
|
||||
/>
|
||||
<GuestTag
|
||||
show={isGuest}
|
||||
theme={theme}
|
||||
/>
|
||||
{Boolean(name.length) &&
|
||||
<Text
|
||||
style={style.rowFullname}
|
||||
numberOfLines={1}
|
||||
>
|
||||
{name}
|
||||
{isCurrentUser &&
|
||||
<FormattedText
|
||||
id='suggestion.mention.you'
|
||||
defaultMessage='(you)'
|
||||
/>}
|
||||
</Text>
|
||||
}
|
||||
<Text
|
||||
style={style.rowUsername}
|
||||
numberOfLines={1}
|
||||
>
|
||||
{` @${username}`}
|
||||
</Text>
|
||||
</View>
|
||||
</TouchableWithFeedback>
|
||||
);
|
||||
}
|
||||
}
|
||||
import FormattedText from '@components/formatted_text';
|
||||
import ProfilePicture from '@components/profile_picture';
|
||||
import {BotTag, GuestTag} from '@components/tag';
|
||||
import TouchableWithFeedback from '@components/touchable_with_feedback';
|
||||
import {makeStyleSheetFromTheme, changeOpacity} from '@utils/theme';
|
||||
|
||||
const getStyleFromTheme = makeStyleSheetFromTheme((theme) => {
|
||||
return {
|
||||
@@ -155,3 +42,115 @@ const getStyleFromTheme = makeStyleSheetFromTheme((theme) => {
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
const AtMentionItem = (props) => {
|
||||
const insets = useSafeAreaInsets();
|
||||
const {
|
||||
firstName,
|
||||
isBot,
|
||||
isCurrentUser,
|
||||
isGuest,
|
||||
lastName,
|
||||
nickname,
|
||||
onPress,
|
||||
showFullName,
|
||||
testID,
|
||||
theme,
|
||||
userId,
|
||||
username,
|
||||
} = props;
|
||||
|
||||
const completeMention = () => {
|
||||
onPress(username);
|
||||
};
|
||||
|
||||
const renderNameBlock = () => {
|
||||
let name = '';
|
||||
const hasNickname = nickname.length > 0;
|
||||
|
||||
if (showFullName === 'true') {
|
||||
name += `${firstName} ${lastName} `;
|
||||
}
|
||||
|
||||
if (hasNickname) {
|
||||
name += `(${nickname})`;
|
||||
}
|
||||
|
||||
return name.trim();
|
||||
};
|
||||
|
||||
const style = getStyleFromTheme(theme);
|
||||
const name = renderNameBlock();
|
||||
|
||||
return (
|
||||
<TouchableWithFeedback
|
||||
testID={testID}
|
||||
key={userId}
|
||||
onPress={completeMention}
|
||||
underlayColor={changeOpacity(theme.buttonBg, 0.08)}
|
||||
style={{marginLeft: insets.left, marginRight: insets.right}}
|
||||
type={'native'}
|
||||
>
|
||||
<View style={style.row}>
|
||||
<View style={style.rowPicture}>
|
||||
<ProfilePicture
|
||||
userId={userId}
|
||||
theme={theme}
|
||||
size={24}
|
||||
status={null}
|
||||
showStatus={false}
|
||||
/>
|
||||
</View>
|
||||
<BotTag
|
||||
show={isBot}
|
||||
theme={theme}
|
||||
/>
|
||||
<GuestTag
|
||||
show={isGuest}
|
||||
theme={theme}
|
||||
/>
|
||||
{Boolean(name.length) &&
|
||||
<Text
|
||||
style={style.rowFullname}
|
||||
numberOfLines={1}
|
||||
>
|
||||
{name}
|
||||
{isCurrentUser &&
|
||||
<FormattedText
|
||||
id='suggestion.mention.you'
|
||||
defaultMessage='(you)'
|
||||
/>}
|
||||
</Text>
|
||||
}
|
||||
<Text
|
||||
style={style.rowUsername}
|
||||
numberOfLines={1}
|
||||
>
|
||||
{` @${username}`}
|
||||
</Text>
|
||||
</View>
|
||||
</TouchableWithFeedback>
|
||||
);
|
||||
};
|
||||
|
||||
AtMentionItem.propTypes = {
|
||||
firstName: PropTypes.string,
|
||||
lastName: PropTypes.string,
|
||||
nickname: PropTypes.string,
|
||||
onPress: PropTypes.func.isRequired,
|
||||
userId: PropTypes.string.isRequired,
|
||||
username: PropTypes.string,
|
||||
isGuest: PropTypes.bool,
|
||||
isBot: PropTypes.bool,
|
||||
theme: PropTypes.object.isRequired,
|
||||
isCurrentUser: PropTypes.bool.isRequired,
|
||||
showFullName: PropTypes.string,
|
||||
testID: PropTypes.string,
|
||||
};
|
||||
|
||||
AtMentionItem.defaultProps = {
|
||||
firstName: '',
|
||||
lastName: '',
|
||||
};
|
||||
|
||||
export default AtMentionItem;
|
||||
|
||||
@@ -3,16 +3,13 @@
|
||||
|
||||
import {connect} from 'react-redux';
|
||||
|
||||
import {getCurrentUserId, getUser} from '@mm-redux/selectors/entities/users';
|
||||
|
||||
import {getTheme} from '@mm-redux/selectors/entities/preferences';
|
||||
import {getConfig} from '@mm-redux/selectors/entities/general';
|
||||
import {getTheme} from '@mm-redux/selectors/entities/preferences';
|
||||
import {getCurrentUserId, getUser} from '@mm-redux/selectors/entities/users';
|
||||
import {isGuest} from '@utils/users';
|
||||
|
||||
import AtMentionItem from './at_mention_item';
|
||||
|
||||
import {isLandscape} from 'app/selectors/device';
|
||||
import {isGuest} from 'app/utils/users';
|
||||
|
||||
function mapStateToProps(state, ownProps) {
|
||||
const user = getUser(state, ownProps.userId);
|
||||
const config = getConfig(state);
|
||||
@@ -25,7 +22,6 @@ function mapStateToProps(state, ownProps) {
|
||||
isBot: Boolean(user.is_bot),
|
||||
isGuest: isGuest(user),
|
||||
theme: getTheme(state),
|
||||
isLandscape: isLandscape(state),
|
||||
isCurrentUser: getCurrentUserId(state) === user.id,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -10,12 +10,11 @@ import {
|
||||
ViewPropTypes,
|
||||
} from 'react-native';
|
||||
|
||||
import {DeviceTypes} from '@constants';
|
||||
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
|
||||
import {emptyFunction} from '@utils/general';
|
||||
import EventEmitter from '@mm-redux/utils/event_emitter';
|
||||
|
||||
import {DeviceTypes} from 'app/constants';
|
||||
import {changeOpacity, makeStyleSheetFromTheme} from 'app/utils/theme';
|
||||
import {emptyFunction} from 'app/utils/general';
|
||||
|
||||
import AtMention from './at_mention';
|
||||
import ChannelMention from './channel_mention';
|
||||
import EmojiSuggestion from './emoji_suggestion';
|
||||
@@ -203,7 +202,10 @@ export default class Autocomplete extends PureComponent {
|
||||
}
|
||||
|
||||
return (
|
||||
<View style={wrapperStyles}>
|
||||
<View
|
||||
style={wrapperStyles}
|
||||
edges={['left', 'right']}
|
||||
>
|
||||
<View
|
||||
testID='autocomplete'
|
||||
ref={this.containerRef}
|
||||
|
||||
@@ -1,56 +1,13 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React, {PureComponent} from 'react';
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {ActivityIndicator, View} from 'react-native';
|
||||
import {useSafeAreaInsets} from 'react-native-safe-area-context';
|
||||
|
||||
import FormattedText from 'app/components/formatted_text';
|
||||
import {makeStyleSheetFromTheme, changeOpacity} from 'app/utils/theme';
|
||||
import {paddingHorizontal as padding} from 'app/components/safe_area_view/iphone_x_spacing';
|
||||
|
||||
export default class AutocompleteSectionHeader extends PureComponent {
|
||||
static propTypes = {
|
||||
defaultMessage: PropTypes.string.isRequired,
|
||||
id: PropTypes.string.isRequired,
|
||||
loading: PropTypes.bool,
|
||||
theme: PropTypes.object.isRequired,
|
||||
isLandscape: PropTypes.bool.isRequired,
|
||||
isFirstSection: PropTypes.bool,
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
isLandscape: false,
|
||||
};
|
||||
|
||||
render() {
|
||||
const {defaultMessage, id, loading, theme, isLandscape, isFirstSection} = this.props;
|
||||
const style = getStyleFromTheme(theme);
|
||||
const sectionStyles = [style.section, padding(isLandscape)];
|
||||
|
||||
if (!isFirstSection) {
|
||||
sectionStyles.push(style.borderTop);
|
||||
}
|
||||
|
||||
return (
|
||||
<View style={style.sectionWrapper}>
|
||||
<View style={sectionStyles}>
|
||||
<FormattedText
|
||||
id={id}
|
||||
defaultMessage={defaultMessage}
|
||||
style={style.sectionText}
|
||||
/>
|
||||
{loading &&
|
||||
<ActivityIndicator
|
||||
color={theme.centerChannelColor}
|
||||
size='small'
|
||||
/>
|
||||
}
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
}
|
||||
import FormattedText from '@components/formatted_text';
|
||||
import {makeStyleSheetFromTheme, changeOpacity} from '@utils/theme';
|
||||
|
||||
const getStyleFromTheme = makeStyleSheetFromTheme((theme) => {
|
||||
return {
|
||||
@@ -79,3 +36,43 @@ const getStyleFromTheme = makeStyleSheetFromTheme((theme) => {
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
const AutocompleteSectionHeader = (props) => {
|
||||
const insets = useSafeAreaInsets();
|
||||
const {defaultMessage, id, loading, theme, isFirstSection} = props;
|
||||
|
||||
const style = getStyleFromTheme(theme);
|
||||
const sectionStyles = [style.section, {marginLeft: insets.left, marginRight: insets.right}];
|
||||
|
||||
if (!isFirstSection) {
|
||||
sectionStyles.push(style.borderTop);
|
||||
}
|
||||
|
||||
return (
|
||||
<View style={style.sectionWrapper}>
|
||||
<View style={sectionStyles}>
|
||||
<FormattedText
|
||||
id={id}
|
||||
defaultMessage={defaultMessage}
|
||||
style={style.sectionText}
|
||||
/>
|
||||
{loading &&
|
||||
<ActivityIndicator
|
||||
color={theme.centerChannelColor}
|
||||
size='small'
|
||||
/>
|
||||
}
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
AutocompleteSectionHeader.propTypes = {
|
||||
defaultMessage: PropTypes.string.isRequired,
|
||||
id: PropTypes.string.isRequired,
|
||||
loading: PropTypes.bool,
|
||||
theme: PropTypes.object.isRequired,
|
||||
isFirstSection: PropTypes.bool,
|
||||
};
|
||||
|
||||
export default AutocompleteSectionHeader;
|
||||
|
||||
@@ -36,7 +36,6 @@ export default class ChannelMention extends PureComponent {
|
||||
requestStatus: PropTypes.string.isRequired,
|
||||
theme: PropTypes.object.isRequired,
|
||||
value: PropTypes.string,
|
||||
isLandscape: PropTypes.bool.isRequired,
|
||||
nestedScrollEnabled: PropTypes.bool,
|
||||
};
|
||||
|
||||
@@ -62,15 +61,23 @@ export default class ChannelMention extends PureComponent {
|
||||
this.props.actions.autocompleteChannelsForSearch(currentTeamId, matchTerm);
|
||||
}, 200);
|
||||
|
||||
componentWillReceiveProps(nextProps) {
|
||||
const {isSearch, matchTerm, myChannels, otherChannels, privateChannels, publicChannels, directAndGroupMessages, requestStatus, myMembers} = nextProps;
|
||||
resetState() {
|
||||
this.setState({
|
||||
mentionComplete: false,
|
||||
sections: [],
|
||||
});
|
||||
}
|
||||
|
||||
if ((matchTerm !== this.props.matchTerm && matchTerm === null) || this.state.mentionComplete) {
|
||||
updateSections(sections) {
|
||||
this.setState({sections});
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps, prevState) {
|
||||
const {isSearch, matchTerm, myChannels, otherChannels, privateChannels, publicChannels, directAndGroupMessages, requestStatus, myMembers} = this.props;
|
||||
|
||||
if ((matchTerm !== prevProps.matchTerm && matchTerm === null) || (this.state.mentionComplete !== prevState.mentionComplete && this.state.mentionComplete)) {
|
||||
// if the term changes but is null or the mention has been completed we render this component as null
|
||||
this.setState({
|
||||
mentionComplete: false,
|
||||
sections: [],
|
||||
});
|
||||
this.resetState();
|
||||
|
||||
this.props.onResultCountChange(0);
|
||||
|
||||
@@ -80,15 +87,15 @@ export default class ChannelMention extends PureComponent {
|
||||
return;
|
||||
}
|
||||
|
||||
if (matchTerm !== this.props.matchTerm) {
|
||||
if (matchTerm !== prevProps.matchTerm) {
|
||||
const {currentTeamId} = this.props;
|
||||
this.runSearch(currentTeamId, matchTerm);
|
||||
}
|
||||
|
||||
if (matchTerm === '' || (myChannels !== this.props.myChannels || otherChannels !== this.props.otherChannels ||
|
||||
privateChannels !== this.props.privateChannels || publicChannels !== this.props.publicChannels ||
|
||||
directAndGroupMessages !== this.props.directAndGroupMessages ||
|
||||
myMembers !== this.props.myMembers)) {
|
||||
if ((matchTerm !== prevProps.matchTerm && matchTerm === '') || (myChannels !== prevProps.myChannels || otherChannels !== prevProps.otherChannels ||
|
||||
privateChannels !== prevProps.privateChannels || publicChannels !== prevProps.publicChannels ||
|
||||
directAndGroupMessages !== prevProps.directAndGroupMessages ||
|
||||
myMembers !== prevProps.myMembers)) {
|
||||
const sections = [];
|
||||
if (isSearch) {
|
||||
if (publicChannels.length) {
|
||||
@@ -140,9 +147,7 @@ export default class ChannelMention extends PureComponent {
|
||||
}
|
||||
}
|
||||
|
||||
this.setState({
|
||||
sections,
|
||||
});
|
||||
this.updateSections(sections);
|
||||
this.props.onResultCountChange(sections.reduce((total, section) => total + section.data.length, 0));
|
||||
}
|
||||
}
|
||||
@@ -191,7 +196,6 @@ export default class ChannelMention extends PureComponent {
|
||||
defaultMessage={section.defaultMessage}
|
||||
loading={!section.hideLoadingIndicator && this.props.requestStatus === RequestStatus.STARTED}
|
||||
theme={this.props.theme}
|
||||
isLandscape={this.props.isLandscape}
|
||||
isFirstSection={isFirstSection}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -6,9 +6,8 @@ import {connect} from 'react-redux';
|
||||
|
||||
import {searchChannels, autocompleteChannelsForSearch} from '@mm-redux/actions/channels';
|
||||
import {getMyChannelMemberships} from '@mm-redux/selectors/entities/channels';
|
||||
import {getTheme} from '@mm-redux/selectors/entities/preferences';
|
||||
import {getCurrentTeamId} from '@mm-redux/selectors/entities/teams';
|
||||
import {isLandscape} from 'app/selectors/device';
|
||||
|
||||
import {
|
||||
filterMyChannels,
|
||||
filterOtherChannels,
|
||||
@@ -16,8 +15,7 @@ import {
|
||||
filterPrivateChannels,
|
||||
filterDirectAndGroupMessages,
|
||||
getMatchTermForChannelMention,
|
||||
} from 'app/selectors/autocomplete';
|
||||
import {getTheme} from '@mm-redux/selectors/entities/preferences';
|
||||
} from '@selectors/autocomplete';
|
||||
|
||||
import ChannelMention from './channel_mention';
|
||||
|
||||
@@ -52,7 +50,6 @@ function mapStateToProps(state, ownProps) {
|
||||
matchTerm,
|
||||
requestStatus: state.requests.channels.getChannels.status,
|
||||
theme: getTheme(state),
|
||||
isLandscape: isLandscape(state),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -1,113 +1,17 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React, {PureComponent} from 'react';
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {
|
||||
Text,
|
||||
View,
|
||||
} from 'react-native';
|
||||
import {Text, View} from 'react-native';
|
||||
import {useSafeAreaInsets} from 'react-native-safe-area-context';
|
||||
|
||||
import {General} from '@mm-redux/constants';
|
||||
import CompassIcon from '@components/compass_icon';
|
||||
import {BotTag, GuestTag} from '@components/tag';
|
||||
import {paddingHorizontal as padding} from '@components/safe_area_view/iphone_x_spacing';
|
||||
import TouchableWithFeedback from '@components/touchable_with_feedback';
|
||||
import {makeStyleSheetFromTheme, changeOpacity} from '@utils/theme';
|
||||
|
||||
export default class ChannelMentionItem extends PureComponent {
|
||||
static propTypes = {
|
||||
channelId: PropTypes.string.isRequired,
|
||||
displayName: PropTypes.string,
|
||||
name: PropTypes.string,
|
||||
type: PropTypes.string,
|
||||
isBot: PropTypes.bool.isRequired,
|
||||
isGuest: PropTypes.bool.isRequired,
|
||||
onPress: PropTypes.func.isRequired,
|
||||
theme: PropTypes.object.isRequired,
|
||||
isLandscape: PropTypes.bool.isRequired,
|
||||
};
|
||||
|
||||
completeMention = () => {
|
||||
const {onPress, displayName, name, type} = this.props;
|
||||
if (type === General.DM_CHANNEL || type === General.GM_CHANNEL) {
|
||||
onPress('@' + displayName.replace(/ /g, ''));
|
||||
} else {
|
||||
onPress(name);
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
const {
|
||||
channelId,
|
||||
displayName,
|
||||
name,
|
||||
theme,
|
||||
type,
|
||||
isBot,
|
||||
isLandscape,
|
||||
isGuest,
|
||||
} = this.props;
|
||||
|
||||
const style = getStyleFromTheme(theme);
|
||||
let iconName = 'globe';
|
||||
let component;
|
||||
if (type === General.PRIVATE_CHANNEL) {
|
||||
iconName = 'lock';
|
||||
}
|
||||
|
||||
if (type === General.DM_CHANNEL || type === General.GM_CHANNEL) {
|
||||
if (!displayName) {
|
||||
return null;
|
||||
}
|
||||
|
||||
component = (
|
||||
<TouchableWithFeedback
|
||||
key={channelId}
|
||||
onPress={this.completeMention}
|
||||
style={[style.row, padding(isLandscape)]}
|
||||
type={'opacity'}
|
||||
>
|
||||
<Text style={style.rowDisplayName}>{'@' + displayName}</Text>
|
||||
<BotTag
|
||||
show={isBot}
|
||||
theme={theme}
|
||||
/>
|
||||
<GuestTag
|
||||
show={isGuest}
|
||||
theme={theme}
|
||||
/>
|
||||
</TouchableWithFeedback>
|
||||
);
|
||||
} else {
|
||||
component = (
|
||||
<TouchableWithFeedback
|
||||
key={channelId}
|
||||
onPress={this.completeMention}
|
||||
style={padding(isLandscape)}
|
||||
underlayColor={changeOpacity(theme.buttonBg, 0.08)}
|
||||
type={'native'}
|
||||
>
|
||||
<View style={style.row}>
|
||||
<CompassIcon
|
||||
name={iconName}
|
||||
style={style.icon}
|
||||
/>
|
||||
<Text style={style.rowDisplayName}>{displayName}</Text>
|
||||
<Text style={style.rowName}>{` ~${name}`}</Text>
|
||||
</View>
|
||||
</TouchableWithFeedback>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
{component}
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const getStyleFromTheme = makeStyleSheetFromTheme((theme) => {
|
||||
return {
|
||||
icon: {
|
||||
@@ -133,3 +37,92 @@ const getStyleFromTheme = makeStyleSheetFromTheme((theme) => {
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
const ChannelMentionItem = (props) => {
|
||||
const insets = useSafeAreaInsets();
|
||||
const {
|
||||
channelId,
|
||||
displayName,
|
||||
isBot,
|
||||
isGuest,
|
||||
name,
|
||||
onPress,
|
||||
theme,
|
||||
type,
|
||||
} = props;
|
||||
|
||||
const completeMention = () => {
|
||||
if (type === General.DM_CHANNEL || type === General.GM_CHANNEL) {
|
||||
onPress('@' + displayName.replace(/ /g, ''));
|
||||
} else {
|
||||
onPress(name);
|
||||
}
|
||||
};
|
||||
|
||||
const style = getStyleFromTheme(theme);
|
||||
const margins = {marginLeft: insets.left, marginRight: insets.right};
|
||||
let iconName = 'globe';
|
||||
let component;
|
||||
if (type === General.PRIVATE_CHANNEL) {
|
||||
iconName = 'lock';
|
||||
}
|
||||
|
||||
if (type === General.DM_CHANNEL || type === General.GM_CHANNEL) {
|
||||
if (!displayName) {
|
||||
return null;
|
||||
}
|
||||
|
||||
component = (
|
||||
<TouchableWithFeedback
|
||||
key={channelId}
|
||||
onPress={completeMention}
|
||||
style={[style.row, margins]}
|
||||
type={'opacity'}
|
||||
>
|
||||
<Text style={style.rowDisplayName}>{'@' + displayName}</Text>
|
||||
<BotTag
|
||||
show={isBot}
|
||||
theme={theme}
|
||||
/>
|
||||
<GuestTag
|
||||
show={isGuest}
|
||||
theme={theme}
|
||||
/>
|
||||
</TouchableWithFeedback>
|
||||
);
|
||||
} else {
|
||||
component = (
|
||||
<TouchableWithFeedback
|
||||
key={channelId}
|
||||
onPress={completeMention}
|
||||
style={margins}
|
||||
underlayColor={changeOpacity(theme.buttonBg, 0.08)}
|
||||
type={'native'}
|
||||
>
|
||||
<View style={style.row}>
|
||||
<CompassIcon
|
||||
name={iconName}
|
||||
style={style.icon}
|
||||
/>
|
||||
<Text style={style.rowDisplayName}>{displayName}</Text>
|
||||
<Text style={style.rowName}>{` ~${name}`}</Text>
|
||||
</View>
|
||||
</TouchableWithFeedback>
|
||||
);
|
||||
}
|
||||
|
||||
return component;
|
||||
};
|
||||
|
||||
ChannelMentionItem.propTypes = {
|
||||
channelId: PropTypes.string.isRequired,
|
||||
displayName: PropTypes.string,
|
||||
name: PropTypes.string,
|
||||
type: PropTypes.string,
|
||||
isBot: PropTypes.bool.isRequired,
|
||||
isGuest: PropTypes.bool.isRequired,
|
||||
onPress: PropTypes.func.isRequired,
|
||||
theme: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
export default ChannelMentionItem;
|
||||
|
||||
@@ -7,10 +7,8 @@ import {General} from '@mm-redux/constants';
|
||||
import {getChannel} from '@mm-redux/selectors/entities/channels';
|
||||
import {getTheme} from '@mm-redux/selectors/entities/preferences';
|
||||
import {getUser} from '@mm-redux/selectors/entities/users';
|
||||
|
||||
import {getChannelNameForSearchAutocomplete} from 'app/selectors/channel';
|
||||
import {isLandscape} from 'app/selectors/device';
|
||||
import {isGuest as isGuestUser} from 'app/utils/users';
|
||||
import {getChannelNameForSearchAutocomplete} from '@selectors/channel';
|
||||
import {isGuest as isGuestUser} from '@utils/users';
|
||||
|
||||
import ChannelMentionItem from './channel_mention_item';
|
||||
|
||||
@@ -36,7 +34,6 @@ function mapStateToProps(state, ownProps) {
|
||||
isBot,
|
||||
isGuest,
|
||||
theme: getTheme(state),
|
||||
isLandscape: isLandscape(state),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -72,4 +72,4 @@ describe('components/autocomplete/emoji_suggestion', () => {
|
||||
expect(wrapper.state('dataSource')).toEqual(output3);
|
||||
}, 100);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -17,6 +17,4 @@ function mapStateToProps(state) {
|
||||
};
|
||||
}
|
||||
|
||||
export const AUTOCOMPLETE_MAX_HEIGHT = 200;
|
||||
|
||||
export default connect(mapStateToProps, null, null, {forwardRef: true})(Autocomplete);
|
||||
|
||||
@@ -9,7 +9,6 @@ import {getAutocompleteCommands, getCommandAutocompleteSuggestions} from '@mm-re
|
||||
import {getAutocompleteCommandsList, getCommandAutocompleteSuggestionsList} from '@mm-redux/selectors/entities/integrations';
|
||||
import {getTheme} from '@mm-redux/selectors/entities/preferences';
|
||||
import {getCurrentTeamId} from '@mm-redux/selectors/entities/teams';
|
||||
import {isLandscape} from 'app/selectors/device';
|
||||
|
||||
import SlashSuggestion from './slash_suggestion';
|
||||
|
||||
@@ -31,7 +30,6 @@ function mapStateToProps(state) {
|
||||
commands: mobileCommandsSelector(state),
|
||||
currentTeamId: getCurrentTeamId(state),
|
||||
theme: getTheme(state),
|
||||
isLandscape: isLandscape(state),
|
||||
suggestions: getCommandAutocompleteSuggestionsList(state),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -8,12 +8,12 @@ import {
|
||||
Platform,
|
||||
} from 'react-native';
|
||||
|
||||
import {makeStyleSheetFromTheme} from 'app/utils/theme';
|
||||
|
||||
import SlashSuggestionItem from './slash_suggestion_item';
|
||||
import {analytics} from '@init/analytics.ts';
|
||||
import {Client4} from '@mm-redux/client';
|
||||
import {isMinimumServerVersion} from '@mm-redux/utils/helpers';
|
||||
import {analytics} from '@init/analytics.ts';
|
||||
import {makeStyleSheetFromTheme} from '@utils/theme';
|
||||
|
||||
import SlashSuggestionItem from './slash_suggestion_item';
|
||||
|
||||
const TIME_BEFORE_NEXT_COMMAND_REQUEST = 1000 * 60 * 5;
|
||||
|
||||
@@ -31,7 +31,6 @@ export default class SlashSuggestion extends PureComponent {
|
||||
onChangeText: PropTypes.func.isRequired,
|
||||
onResultCountChange: PropTypes.func.isRequired,
|
||||
value: PropTypes.string,
|
||||
isLandscape: PropTypes.bool.isRequired,
|
||||
nestedScrollEnabled: PropTypes.bool,
|
||||
suggestions: PropTypes.array,
|
||||
rootId: PropTypes.string,
|
||||
@@ -49,51 +48,53 @@ export default class SlashSuggestion extends PureComponent {
|
||||
lastCommandRequest: 0,
|
||||
};
|
||||
|
||||
componentWillReceiveProps(nextProps) {
|
||||
if ((nextProps.value === this.props.value && nextProps.suggestions === this.props.suggestions && nextProps.commands === this.props.commands) ||
|
||||
nextProps.isSearch || nextProps.value.startsWith('//') || !nextProps.channelId) {
|
||||
setActive(active) {
|
||||
this.setState({active});
|
||||
}
|
||||
|
||||
setLastCommandRequest(lastCommandRequest) {
|
||||
this.setState({lastCommandRequest});
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
if ((this.props.value === prevProps.value && this.props.suggestions === prevProps.suggestions && this.props.commands === prevProps.commands) ||
|
||||
this.props.isSearch || this.props.value.startsWith('//') || !this.props.channelId) {
|
||||
return;
|
||||
}
|
||||
|
||||
const {currentTeamId} = this.props;
|
||||
const {currentTeamId} = prevProps;
|
||||
const {
|
||||
commands: nextCommands,
|
||||
currentTeamId: nextTeamId,
|
||||
value: nextValue,
|
||||
suggestions: nextSuggestions,
|
||||
} = nextProps;
|
||||
} = this.props;
|
||||
|
||||
if (nextValue[0] !== '/') {
|
||||
this.setState({
|
||||
active: false,
|
||||
});
|
||||
this.setActive(false);
|
||||
this.props.onResultCountChange(0);
|
||||
return;
|
||||
}
|
||||
|
||||
if (nextValue.indexOf(' ') === -1) { // return suggestions for a top level cached commands
|
||||
if (currentTeamId !== nextTeamId) {
|
||||
this.setState({
|
||||
lastCommandRequest: 0,
|
||||
});
|
||||
this.setLastCommandRequest(0);
|
||||
}
|
||||
|
||||
const dataIsStale = Date.now() - this.state.lastCommandRequest > TIME_BEFORE_NEXT_COMMAND_REQUEST;
|
||||
|
||||
if ((!nextCommands.length || dataIsStale)) {
|
||||
this.props.actions.getAutocompleteCommands(nextProps.currentTeamId);
|
||||
this.setState({
|
||||
lastCommandRequest: Date.now(),
|
||||
});
|
||||
this.props.actions.getAutocompleteCommands(this.props.currentTeamId);
|
||||
this.setLastCommandRequest(Date.now());
|
||||
}
|
||||
|
||||
const matches = this.filterSlashSuggestions(nextValue.substring(1), nextCommands);
|
||||
this.updateSuggestions(matches);
|
||||
} else if (isMinimumServerVersion(Client4.getServerVersion(), 5, 24)) {
|
||||
if (nextSuggestions === this.props.suggestions) {
|
||||
if (nextSuggestions === prevProps.suggestions) {
|
||||
const args = {
|
||||
channel_id: this.props.channelId,
|
||||
...(this.props.rootId && {root_id: this.props.rootId, parent_id: this.props.rootId}),
|
||||
channel_id: prevProps.channelId,
|
||||
...(prevProps.rootId && {root_id: prevProps.rootId, parent_id: prevProps.rootId}),
|
||||
};
|
||||
this.props.actions.getCommandAutocompleteSuggestions(nextValue, nextTeamId, args);
|
||||
} else {
|
||||
@@ -111,9 +112,7 @@ export default class SlashSuggestion extends PureComponent {
|
||||
this.updateSuggestions(matches);
|
||||
}
|
||||
} else {
|
||||
this.setState({
|
||||
active: false,
|
||||
});
|
||||
this.setActive(false);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -187,7 +186,6 @@ export default class SlashSuggestion extends PureComponent {
|
||||
theme={this.props.theme}
|
||||
suggestion={item.Suggestion}
|
||||
complete={item.Complete}
|
||||
isLandscape={this.props.isLandscape}
|
||||
/>
|
||||
)
|
||||
|
||||
|
||||
@@ -1,79 +1,14 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React, {PureComponent} from 'react';
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {Image, Text, View} from 'react-native';
|
||||
import {useSafeAreaInsets} from 'react-native-safe-area-context';
|
||||
|
||||
import {paddingHorizontal as padding} from '@components/safe_area_view/iphone_x_spacing';
|
||||
import slashIcon from '@assets/images/autocomplete/slash_command.png';
|
||||
import TouchableWithFeedback from '@components/touchable_with_feedback';
|
||||
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
|
||||
import slashIcon from '@assets/images/autocomplete/slash_command.png';
|
||||
|
||||
export default class SlashSuggestionItem extends PureComponent {
|
||||
static propTypes = {
|
||||
description: PropTypes.string,
|
||||
hint: PropTypes.string,
|
||||
onPress: PropTypes.func.isRequired,
|
||||
theme: PropTypes.object.isRequired,
|
||||
suggestion: PropTypes.string,
|
||||
complete: PropTypes.string,
|
||||
isLandscape: PropTypes.bool.isRequired,
|
||||
};
|
||||
|
||||
completeSuggestion = () => {
|
||||
const {onPress, complete} = this.props;
|
||||
onPress(complete);
|
||||
};
|
||||
|
||||
render() {
|
||||
const {
|
||||
description,
|
||||
hint,
|
||||
theme,
|
||||
suggestion,
|
||||
complete,
|
||||
isLandscape,
|
||||
} = this.props;
|
||||
|
||||
const style = getStyleFromTheme(theme);
|
||||
|
||||
let suggestionText = suggestion;
|
||||
if (suggestionText[0] === '/' && complete.split(' ').length === 1) {
|
||||
suggestionText = suggestionText.substring(1);
|
||||
}
|
||||
|
||||
return (
|
||||
<TouchableWithFeedback
|
||||
onPress={this.completeSuggestion}
|
||||
style={padding(isLandscape)}
|
||||
underlayColor={changeOpacity(theme.buttonBg, 0.08)}
|
||||
type={'native'}
|
||||
>
|
||||
<View style={style.container}>
|
||||
<View style={style.icon}>
|
||||
<Image
|
||||
style={style.iconColor}
|
||||
width={10}
|
||||
height={16}
|
||||
source={slashIcon}
|
||||
/>
|
||||
</View>
|
||||
<View style={style.suggestionContainer}>
|
||||
<Text style={style.suggestionName}>{`${suggestionText} ${hint}`}</Text>
|
||||
<Text
|
||||
ellipsizeMode='tail'
|
||||
numberOfLines={1}
|
||||
style={style.suggestionDescription}
|
||||
>
|
||||
{description}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
</TouchableWithFeedback>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const getStyleFromTheme = makeStyleSheetFromTheme((theme) => {
|
||||
return {
|
||||
@@ -112,3 +47,67 @@ const getStyleFromTheme = makeStyleSheetFromTheme((theme) => {
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
const SlashSuggestionItem = (props) => {
|
||||
const insets = useSafeAreaInsets();
|
||||
const {
|
||||
complete,
|
||||
description,
|
||||
hint,
|
||||
onPress,
|
||||
suggestion,
|
||||
theme,
|
||||
} = props;
|
||||
|
||||
const completeSuggestion = () => {
|
||||
onPress(complete);
|
||||
};
|
||||
|
||||
const style = getStyleFromTheme(theme);
|
||||
|
||||
let suggestionText = suggestion;
|
||||
if (suggestionText[0] === '/' && complete.split(' ').length === 1) {
|
||||
suggestionText = suggestionText.substring(1);
|
||||
}
|
||||
|
||||
return (
|
||||
<TouchableWithFeedback
|
||||
onPress={completeSuggestion}
|
||||
style={{marginLeft: insets.left, marginRight: insets.right}}
|
||||
underlayColor={changeOpacity(theme.buttonBg, 0.08)}
|
||||
type={'native'}
|
||||
>
|
||||
<View style={style.container}>
|
||||
<View style={style.icon}>
|
||||
<Image
|
||||
style={style.iconColor}
|
||||
width={10}
|
||||
height={16}
|
||||
source={slashIcon}
|
||||
/>
|
||||
</View>
|
||||
<View style={style.suggestionContainer}>
|
||||
<Text style={style.suggestionName}>{`${suggestionText} ${hint}`}</Text>
|
||||
<Text
|
||||
ellipsizeMode='tail'
|
||||
numberOfLines={1}
|
||||
style={style.suggestionDescription}
|
||||
>
|
||||
{description}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
</TouchableWithFeedback>
|
||||
);
|
||||
};
|
||||
|
||||
SlashSuggestionItem.propTypes = {
|
||||
description: PropTypes.string,
|
||||
hint: PropTypes.string,
|
||||
onPress: PropTypes.func.isRequired,
|
||||
theme: PropTypes.object.isRequired,
|
||||
suggestion: PropTypes.string,
|
||||
complete: PropTypes.string,
|
||||
};
|
||||
|
||||
export default SlashSuggestionItem;
|
||||
|
||||
@@ -10,7 +10,6 @@ import {displayUsername} from '@mm-redux/utils/user_utils';
|
||||
|
||||
import CompassIcon from '@components/compass_icon';
|
||||
import FormattedText from '@components/formatted_text';
|
||||
import {paddingHorizontal as padding} from '@components/safe_area_view/iphone_x_spacing';
|
||||
import TouchableWithFeedback from '@components/touchable_with_feedback';
|
||||
import {preventDoubleTap} from '@utils/tap';
|
||||
import {makeStyleSheetFromTheme, changeOpacity} from '@utils/theme';
|
||||
@@ -35,7 +34,6 @@ export default class AutocompleteSelector extends PureComponent {
|
||||
helpText: PropTypes.node,
|
||||
errorText: PropTypes.node,
|
||||
roundedBorders: PropTypes.bool,
|
||||
isLandscape: PropTypes.bool.isRequired,
|
||||
disabled: PropTypes.bool,
|
||||
};
|
||||
|
||||
@@ -119,7 +117,6 @@ export default class AutocompleteSelector extends PureComponent {
|
||||
optional,
|
||||
showRequiredAsterisk,
|
||||
roundedBorders,
|
||||
isLandscape,
|
||||
disabled,
|
||||
} = this.props;
|
||||
const {selectedText} = this.state;
|
||||
@@ -186,9 +183,7 @@ export default class AutocompleteSelector extends PureComponent {
|
||||
|
||||
return (
|
||||
<View style={style.container}>
|
||||
<View style={padding(isLandscape)}>
|
||||
{labelContent}
|
||||
</View>
|
||||
{labelContent}
|
||||
<TouchableWithFeedback
|
||||
style={disabled ? style.disabled : null}
|
||||
onPress={this.goToSelectorScreen}
|
||||
@@ -197,7 +192,7 @@ export default class AutocompleteSelector extends PureComponent {
|
||||
>
|
||||
<View style={inputStyle}>
|
||||
<Text
|
||||
style={[selectedStyle, padding(isLandscape)]}
|
||||
style={selectedStyle}
|
||||
numberOfLines={1}
|
||||
>
|
||||
{text}
|
||||
@@ -205,14 +200,12 @@ export default class AutocompleteSelector extends PureComponent {
|
||||
<CompassIcon
|
||||
name='chevron-down'
|
||||
color={changeOpacity(theme.centerChannelColor, 0.5)}
|
||||
style={[style.icon, padding(isLandscape)]}
|
||||
style={style.icon}
|
||||
/>
|
||||
</View>
|
||||
</TouchableWithFeedback>
|
||||
<View style={padding(isLandscape)}>
|
||||
{helpTextContent}
|
||||
{errorTextContent}
|
||||
</View>
|
||||
{helpTextContent}
|
||||
{errorTextContent}
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -9,13 +9,11 @@ import {getTeammateNameDisplaySetting, getTheme} from '@mm-redux/selectors/entit
|
||||
import {setAutocompleteSelector} from 'app/actions/views/post';
|
||||
|
||||
import AutocompleteSelector from './autocomplete_selector';
|
||||
import {isLandscape} from 'app/selectors/device';
|
||||
|
||||
function mapStateToProps(state) {
|
||||
return {
|
||||
teammateNameDisplay: getTeammateNameDisplaySetting(state),
|
||||
theme: getTheme(state),
|
||||
isLandscape: isLandscape(state),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -21,6 +21,7 @@ export default class Badge extends PureComponent {
|
||||
};
|
||||
|
||||
static propTypes = {
|
||||
testID: PropTypes.string,
|
||||
containerStyle: ViewPropTypes.style,
|
||||
count: PropTypes.number.isRequired,
|
||||
extraPaddingHorizontal: PropTypes.number,
|
||||
@@ -108,12 +109,15 @@ export default class Badge extends PureComponent {
|
||||
};
|
||||
|
||||
renderText = () => {
|
||||
const {containerStyle, count, style} = this.props;
|
||||
const {testID, containerStyle, count, style} = this.props;
|
||||
const unreadCountTestID = `${testID}.unread_count`;
|
||||
const unreadIndicatorID = `${testID}.unread_indicator`;
|
||||
let unreadCount = null;
|
||||
let unreadIndicator = null;
|
||||
if (count < 0) {
|
||||
unreadIndicator = (
|
||||
<View
|
||||
testID={unreadIndicatorID}
|
||||
style={[styles.text, this.props.countStyle]}
|
||||
onLayout={this.onLayout}
|
||||
/>
|
||||
@@ -127,6 +131,7 @@ export default class Badge extends PureComponent {
|
||||
unreadCount = (
|
||||
<View style={styles.verticalAlign}>
|
||||
<Text
|
||||
testID={unreadCountTestID}
|
||||
style={[styles.text, this.props.countStyle]}
|
||||
onLayout={this.onLayout}
|
||||
>
|
||||
@@ -137,7 +142,10 @@ export default class Badge extends PureComponent {
|
||||
}
|
||||
|
||||
return (
|
||||
<View style={[styles.badgeContainer, containerStyle]}>
|
||||
<View
|
||||
testID={testID}
|
||||
style={[styles.badgeContainer, containerStyle]}
|
||||
>
|
||||
<View
|
||||
ref={this.setBadgeRef}
|
||||
style={[styles.badge, style, {opacity: 0}]}
|
||||
|
||||
@@ -9,6 +9,7 @@ import Badge from './badge';
|
||||
|
||||
describe('Badge', () => {
|
||||
const baseProps = {
|
||||
testID: 'badge',
|
||||
count: 100,
|
||||
countStyle: {color: '#145dbf', fontSize: 10},
|
||||
style: {backgroundColor: '#ffffff'},
|
||||
|
||||
@@ -15,7 +15,6 @@ import {General} from '@mm-redux/constants';
|
||||
import {showModal} from '@actions/navigation';
|
||||
import CompassIcon from '@components/compass_icon';
|
||||
import ProfilePicture from '@components/profile_picture';
|
||||
import {paddingHorizontal as padding} from '@components/safe_area_view/iphone_x_spacing';
|
||||
import {BotTag, GuestTag} from '@components/tag';
|
||||
import TouchableWithFeedback from '@components/touchable_with_feedback';
|
||||
import {preventDoubleTap} from '@utils/tap';
|
||||
@@ -30,7 +29,6 @@ class ChannelIntro extends PureComponent {
|
||||
currentChannelMembers: PropTypes.array.isRequired,
|
||||
intl: intlShape.isRequired,
|
||||
theme: PropTypes.object.isRequired,
|
||||
isLandscape: PropTypes.bool.isRequired,
|
||||
teammateNameDisplay: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
@@ -329,7 +327,7 @@ class ChannelIntro extends PureComponent {
|
||||
};
|
||||
|
||||
render() {
|
||||
const {currentChannel, theme, isLandscape} = this.props;
|
||||
const {currentChannel, theme} = this.props;
|
||||
const style = getStyleSheet(theme);
|
||||
const channelType = currentChannel.type;
|
||||
|
||||
@@ -337,10 +335,10 @@ class ChannelIntro extends PureComponent {
|
||||
if (channelType === General.DM_CHANNEL || channelType === General.GM_CHANNEL) {
|
||||
profiles = (
|
||||
<View>
|
||||
<View style={[style.profilesContainer, padding(isLandscape)]}>
|
||||
<View style={style.profilesContainer}>
|
||||
{this.buildProfiles()}
|
||||
</View>
|
||||
<View style={[style.namesContainer, padding(isLandscape)]}>
|
||||
<View style={style.namesContainer}>
|
||||
{this.buildNames()}
|
||||
</View>
|
||||
</View>
|
||||
@@ -350,7 +348,7 @@ class ChannelIntro extends PureComponent {
|
||||
return (
|
||||
<View style={style.container}>
|
||||
{profiles}
|
||||
<View style={[style.contentContainer, padding(isLandscape)]}>
|
||||
<View style={style.contentContainer}>
|
||||
{this.buildContent()}
|
||||
</View>
|
||||
</View>
|
||||
|
||||
@@ -9,7 +9,6 @@ import {makeGetChannel} from '@mm-redux/selectors/entities/channels';
|
||||
import {getCurrentUserId, getUser, makeGetProfilesInChannel} from '@mm-redux/selectors/entities/users';
|
||||
|
||||
import {getTeammateNameDisplaySetting, getTheme} from '@mm-redux/selectors/entities/preferences';
|
||||
import {isLandscape} from 'app/selectors/device';
|
||||
import {getChannelMembersForDm} from 'app/selectors/channel';
|
||||
|
||||
import ChannelIntro from './channel_intro';
|
||||
@@ -48,7 +47,6 @@ function makeMapStateToProps() {
|
||||
currentChannel,
|
||||
currentChannelMembers,
|
||||
theme: getTheme(state),
|
||||
isLandscape: isLandscape(state),
|
||||
teammateNameDisplay: getTeammateNameDisplaySetting(state),
|
||||
};
|
||||
};
|
||||
|
||||
@@ -10,13 +10,74 @@ exports[`ChannelLoader should match snapshot 1`] = `
|
||||
"overflow": "hidden",
|
||||
},
|
||||
undefined,
|
||||
null,
|
||||
Object {
|
||||
"backgroundColor": "#ffffff",
|
||||
},
|
||||
]
|
||||
}
|
||||
>
|
||||
<ForwardRef(AnimatedComponentWrapper)
|
||||
style={
|
||||
Array [
|
||||
Object {
|
||||
"backgroundColor": "#3D3C40",
|
||||
"height": 38,
|
||||
"position": "absolute",
|
||||
"width": "100%",
|
||||
"zIndex": 9,
|
||||
},
|
||||
Object {
|
||||
"top": -38,
|
||||
},
|
||||
]
|
||||
}
|
||||
>
|
||||
<ForwardRef(AnimatedComponentWrapper)
|
||||
edges={
|
||||
Array [
|
||||
"left",
|
||||
"right",
|
||||
]
|
||||
}
|
||||
style={
|
||||
Object {
|
||||
"alignItems": "center",
|
||||
"flexDirection": "row",
|
||||
"height": 38,
|
||||
"paddingLeft": 12,
|
||||
"paddingRight": 5,
|
||||
}
|
||||
}
|
||||
>
|
||||
<View
|
||||
style={
|
||||
Object {
|
||||
"alignItems": "flex-start",
|
||||
"height": 24,
|
||||
"justifyContent": "center",
|
||||
"paddingRight": 10,
|
||||
}
|
||||
}
|
||||
>
|
||||
<ActivityIndicator
|
||||
animating={true}
|
||||
color="#FFFFFF"
|
||||
hidesWhenStopped={true}
|
||||
size="small"
|
||||
/>
|
||||
</View>
|
||||
<FormattedText
|
||||
defaultMessage="Still trying to load your content..."
|
||||
id="mobile.channel_loader.still_loading"
|
||||
style={
|
||||
Object {
|
||||
"color": "#fff",
|
||||
"fontWeight": "bold",
|
||||
}
|
||||
}
|
||||
/>
|
||||
</ForwardRef(AnimatedComponentWrapper)>
|
||||
</ForwardRef(AnimatedComponentWrapper)>
|
||||
<View
|
||||
style={
|
||||
Array [
|
||||
|
||||
@@ -4,15 +4,21 @@
|
||||
import React, {PureComponent} from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {
|
||||
ActivityIndicator,
|
||||
Animated,
|
||||
View,
|
||||
Dimensions,
|
||||
Platform,
|
||||
View,
|
||||
} from 'react-native';
|
||||
import * as RNPlaceholder from 'rn-placeholder';
|
||||
import {SafeAreaView} from 'react-native-safe-area-context';
|
||||
|
||||
import CustomPropTypes from 'app/constants/custom_prop_types';
|
||||
import {changeOpacity, makeStyleSheetFromTheme} from 'app/utils/theme';
|
||||
import {paddingHorizontal as padding} from 'app/components/safe_area_view/iphone_x_spacing';
|
||||
import FormattedText from '@components/formatted_text';
|
||||
import CustomPropTypes from '@constants/custom_prop_types';
|
||||
import {INDICATOR_BAR_HEIGHT} from '@constants/view';
|
||||
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
|
||||
|
||||
const AnimatedSafeAreaView = Animated.createAnimatedComponent(SafeAreaView);
|
||||
|
||||
function calculateMaxRows(height) {
|
||||
return Math.round(height / 100);
|
||||
@@ -36,7 +42,7 @@ export default class ChannelLoader extends PureComponent {
|
||||
style: CustomPropTypes.Style,
|
||||
theme: PropTypes.object.isRequired,
|
||||
height: PropTypes.number,
|
||||
isLandscape: PropTypes.bool.isRequired,
|
||||
retryLoad: PropTypes.func,
|
||||
};
|
||||
|
||||
constructor(props) {
|
||||
@@ -50,6 +56,8 @@ export default class ChannelLoader extends PureComponent {
|
||||
switch: false,
|
||||
maxRows,
|
||||
};
|
||||
|
||||
this.top = new Animated.Value(-INDICATOR_BAR_HEIGHT);
|
||||
}
|
||||
|
||||
static getDerivedStateFromProps(nextProps, prevState) {
|
||||
@@ -66,6 +74,27 @@ export default class ChannelLoader extends PureComponent {
|
||||
return Object.keys(state) ? state : null;
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
if (this.props.retryLoad) {
|
||||
this.stillLoadingTimeout = setTimeout(this.showIndicator, 10000);
|
||||
this.retryLoadInterval = setInterval(this.props.retryLoad, 10000);
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
clearTimeout(this.stillLoadingTimeout);
|
||||
clearInterval(this.retryLoadInterval);
|
||||
}
|
||||
|
||||
showIndicator = () => {
|
||||
Animated.timing(this.top, {
|
||||
toValue: 0,
|
||||
duration: 300,
|
||||
delay: 500,
|
||||
useNativeDriver: false,
|
||||
}).start();
|
||||
}
|
||||
|
||||
buildSections({key, style, bg, color}) {
|
||||
return (
|
||||
<View
|
||||
@@ -107,7 +136,6 @@ export default class ChannelLoader extends PureComponent {
|
||||
channelIsLoading,
|
||||
style: styleProp,
|
||||
theme,
|
||||
isLandscape,
|
||||
} = this.props;
|
||||
|
||||
if (!channelIsLoading) {
|
||||
@@ -119,9 +147,29 @@ export default class ChannelLoader extends PureComponent {
|
||||
|
||||
return (
|
||||
<View
|
||||
style={[style.container, styleProp, padding(isLandscape), {backgroundColor: bg}]}
|
||||
style={[style.container, styleProp, {backgroundColor: bg}]}
|
||||
onLayout={this.handleLayout}
|
||||
>
|
||||
<Animated.View
|
||||
style={[style.indicator, {top: this.top}]}
|
||||
>
|
||||
<AnimatedSafeAreaView
|
||||
edges={['left', 'right']}
|
||||
style={style.indicatorWrapper}
|
||||
>
|
||||
<View style={style.activityIndicator}>
|
||||
<ActivityIndicator
|
||||
color='#FFFFFF'
|
||||
size='small'
|
||||
/>
|
||||
</View>
|
||||
<FormattedText
|
||||
id='mobile.channel_loader.still_loading'
|
||||
defaultMessage='Still trying to load your content...'
|
||||
style={style.indicatorText}
|
||||
/>
|
||||
</AnimatedSafeAreaView>
|
||||
</Animated.View>
|
||||
{Array(this.state.maxRows).fill().map((item, index) => this.buildSections({
|
||||
key: index,
|
||||
style,
|
||||
@@ -146,5 +194,36 @@ const getStyleSheet = makeStyleSheetFromTheme((theme) => {
|
||||
paddingRight: 20,
|
||||
marginVertical: 10,
|
||||
},
|
||||
indicator: {
|
||||
position: 'absolute',
|
||||
height: INDICATOR_BAR_HEIGHT,
|
||||
width: '100%',
|
||||
...Platform.select({
|
||||
android: {
|
||||
elevation: 9,
|
||||
},
|
||||
ios: {
|
||||
zIndex: 9,
|
||||
},
|
||||
}),
|
||||
backgroundColor: '#3D3C40',
|
||||
},
|
||||
indicatorWrapper: {
|
||||
alignItems: 'center',
|
||||
height: INDICATOR_BAR_HEIGHT,
|
||||
flexDirection: 'row',
|
||||
paddingLeft: 12,
|
||||
paddingRight: 5,
|
||||
},
|
||||
indicatorText: {
|
||||
color: '#fff',
|
||||
fontWeight: 'bold',
|
||||
},
|
||||
activityIndicator: {
|
||||
alignItems: 'flex-start',
|
||||
height: 24,
|
||||
justifyContent: 'center',
|
||||
paddingRight: 10,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
@@ -8,15 +8,44 @@ import Preferences from '@mm-redux/constants/preferences';
|
||||
|
||||
import ChannelLoader from './channel_loader';
|
||||
|
||||
jest.useFakeTimers();
|
||||
|
||||
describe('ChannelLoader', () => {
|
||||
const baseProps = {
|
||||
channelIsLoading: true,
|
||||
theme: Preferences.THEMES.default,
|
||||
isLandscape: false,
|
||||
};
|
||||
|
||||
test('should match snapshot', () => {
|
||||
const wrapper = shallow(<ChannelLoader {...baseProps}/>);
|
||||
expect(wrapper.getElement()).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
|
||||
test('should call setTimeout and setInterval for showIndicator and retryLoad on mount', () => {
|
||||
shallow(<ChannelLoader {...baseProps}/>);
|
||||
expect(setTimeout).not.toHaveBeenCalled();
|
||||
expect(setInterval).not.toHaveBeenCalled();
|
||||
|
||||
const props = {
|
||||
...baseProps,
|
||||
retryLoad: jest.fn(),
|
||||
};
|
||||
const wrapper = shallow(<ChannelLoader {...props}/>);
|
||||
const instance = wrapper.instance();
|
||||
expect(setTimeout).toHaveBeenCalledWith(instance.showIndicator, 10000);
|
||||
expect(setInterval).toHaveBeenCalledWith(props.retryLoad, 10000);
|
||||
});
|
||||
|
||||
test('should clear timer and interval on unmount', () => {
|
||||
const props = {
|
||||
...baseProps,
|
||||
retryLoad: jest.fn(),
|
||||
};
|
||||
const wrapper = shallow(<ChannelLoader {...props}/>);
|
||||
const instance = wrapper.instance();
|
||||
instance.componentWillUnmount();
|
||||
|
||||
expect(clearTimeout).toHaveBeenCalledWith(instance.stillLoadingTimeout);
|
||||
expect(clearInterval).toHaveBeenCalledWith(instance.retryLoadInterval);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -5,8 +5,6 @@ import {connect} from 'react-redux';
|
||||
|
||||
import {getTheme} from '@mm-redux/selectors/entities/preferences';
|
||||
|
||||
import {isLandscape} from 'app/selectors/device';
|
||||
|
||||
import ChannelLoader from './channel_loader';
|
||||
|
||||
function mapStateToProps(state, ownProps) {
|
||||
@@ -15,7 +13,6 @@ function mapStateToProps(state, ownProps) {
|
||||
return {
|
||||
channelIsLoading,
|
||||
theme: getTheme(state),
|
||||
isLandscape: isLandscape(state),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -6,7 +6,6 @@ import PropTypes from 'prop-types';
|
||||
import {
|
||||
Alert,
|
||||
Animated,
|
||||
Linking,
|
||||
TouchableOpacity,
|
||||
View,
|
||||
} from 'react-native';
|
||||
@@ -17,6 +16,7 @@ import FormattedText from '@components/formatted_text';
|
||||
import {DeviceTypes} from '@constants';
|
||||
import {checkUpgradeType, isUpgradeAvailable} from '@utils/client_upgrade';
|
||||
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
|
||||
import {tryOpenURL} from '@utils/url';
|
||||
import {showModal, dismissModal} from '@actions/navigation';
|
||||
|
||||
const {View: AnimatedView} = Animated;
|
||||
@@ -62,17 +62,21 @@ export default class ClientUpgradeListener extends PureComponent {
|
||||
}
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps) {
|
||||
const {forceUpgrade, latestVersion, minVersion} = this.props;
|
||||
const {latestVersion: nextLatestVersion, minVersion: nextMinVersion, lastUpgradeCheck} = nextProps;
|
||||
setTop(top) {
|
||||
this.setState({top});
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
const {forceUpgrade, latestVersion, minVersion} = prevProps;
|
||||
const {latestVersion: nextLatestVersion, minVersion: nextMinVersion, lastUpgradeCheck} = this.props;
|
||||
|
||||
const versionMismatch = latestVersion !== nextLatestVersion || minVersion !== nextMinVersion;
|
||||
if (versionMismatch && (forceUpgrade || Date.now() - lastUpgradeCheck > UPDATE_TIMEOUT)) {
|
||||
this.checkUpgrade(minVersion, latestVersion, nextProps.isLandscape);
|
||||
} else if (this.props.isLandscape !== nextProps.isLandscape &&
|
||||
this.checkUpgrade(minVersion, latestVersion, this.props.isLandscape);
|
||||
} else if (prevProps.isLandscape !== this.props.isLandscape &&
|
||||
isUpgradeAvailable(this.state.upgradeType) && DeviceTypes.IS_IPHONE_WITH_INSETS) {
|
||||
const newTop = nextProps.isLandscape ? 45 : 100;
|
||||
this.setState({top: new Animated.Value(newTop)});
|
||||
const newTop = this.props.isLandscape ? 45 : 100;
|
||||
this.setTop(new Animated.Value(newTop));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -117,7 +121,7 @@ export default class ClientUpgradeListener extends PureComponent {
|
||||
const {downloadLink} = this.props;
|
||||
const {intl} = this.context;
|
||||
|
||||
Linking.openURL(downloadLink).catch(() => {
|
||||
const onError = () => {
|
||||
Alert.alert(
|
||||
intl.formatMessage({
|
||||
id: 'mobile.client_upgrade.download_error.title',
|
||||
@@ -128,7 +132,8 @@ export default class ClientUpgradeListener extends PureComponent {
|
||||
defaultMessage: 'An error occurred while trying to open the download link.',
|
||||
}),
|
||||
);
|
||||
});
|
||||
};
|
||||
tryOpenURL(downloadLink, onError);
|
||||
|
||||
this.toggleUpgradeMessage(false);
|
||||
};
|
||||
|
||||
@@ -4,4 +4,4 @@
|
||||
import {createIconSetFromFontello} from 'react-native-vector-icons';
|
||||
import fontelloConfig from '@assets/compass-icons.json';
|
||||
|
||||
export default createIconSetFromFontello(fontelloConfig, 'compass-icons', 'compass-icons.ttf');
|
||||
export default createIconSetFromFontello(fontelloConfig, 'compass-icons', 'compass-icons.ttf');
|
||||
|
||||
@@ -117,13 +117,10 @@ exports[`CustomList should match snapshot, renderSectionHeader 1`] = `
|
||||
>
|
||||
<Text
|
||||
style={
|
||||
Array [
|
||||
Object {
|
||||
"color": "#3d3c40",
|
||||
"fontWeight": "600",
|
||||
},
|
||||
null,
|
||||
]
|
||||
Object {
|
||||
"color": "#3d3c40",
|
||||
"fontWeight": "600",
|
||||
}
|
||||
}
|
||||
>
|
||||
section_id
|
||||
|
||||
@@ -19,7 +19,6 @@ export default class ChannelListRow extends React.PureComponent {
|
||||
theme: PropTypes.object.isRequired,
|
||||
channel: PropTypes.object.isRequired,
|
||||
...CustomListRow.propTypes,
|
||||
isLandscape: PropTypes.bool.isRequired,
|
||||
};
|
||||
|
||||
onPress = () => {
|
||||
@@ -49,7 +48,6 @@ export default class ChannelListRow extends React.PureComponent {
|
||||
enabled={this.props.enabled}
|
||||
selectable={this.props.selectable}
|
||||
selected={this.props.selected}
|
||||
isLandscape={this.props.isLandscape}
|
||||
>
|
||||
<View style={style.container}>
|
||||
<View style={style.titleContainer}>
|
||||
|
||||
@@ -6,7 +6,6 @@ import {connect} from 'react-redux';
|
||||
import {getTheme} from '@mm-redux/selectors/entities/preferences';
|
||||
|
||||
import {makeGetChannel} from '@mm-redux/selectors/entities/channels';
|
||||
import {isLandscape} from 'app/selectors/device';
|
||||
import ChannelListRow from './channel_list_row';
|
||||
|
||||
function makeMapStateToProps() {
|
||||
@@ -16,7 +15,6 @@ function makeMapStateToProps() {
|
||||
return {
|
||||
theme: getTheme(state),
|
||||
channel: getChannel(state, ownProps),
|
||||
isLandscape: isLandscape(state),
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
@@ -9,7 +9,6 @@ import {
|
||||
} from 'react-native';
|
||||
|
||||
import CompassIcon from '@components/compass_icon';
|
||||
import {paddingLeft as padding} from '@components/safe_area_view/iphone_x_spacing';
|
||||
import ConditionalTouchable from '@components/conditional_touchable';
|
||||
import CustomPropTypes from '@constants/custom_prop_types';
|
||||
|
||||
@@ -20,14 +19,11 @@ export default class CustomListRow extends React.PureComponent {
|
||||
selectable: PropTypes.bool,
|
||||
selected: PropTypes.bool,
|
||||
children: CustomPropTypes.Children,
|
||||
item: PropTypes.object,
|
||||
isLandscape: PropTypes.bool.isRequired,
|
||||
testID: PropTypes.string,
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
enabled: true,
|
||||
isLandscape: false,
|
||||
};
|
||||
|
||||
render() {
|
||||
@@ -38,7 +34,7 @@ export default class CustomListRow extends React.PureComponent {
|
||||
style={style.touchable}
|
||||
testID={this.props.testID}
|
||||
>
|
||||
<View style={[style.container, padding(this.props.isLandscape)]}>
|
||||
<View style={style.container}>
|
||||
{this.props.selectable &&
|
||||
<View style={style.selectorContainer}>
|
||||
<View style={[style.selector, (this.props.selected && style.selectorFilled), (!this.props.enabled && style.selectorDisabled)]}>
|
||||
|
||||
@@ -7,7 +7,6 @@ import {FlatList, Keyboard, Platform, RefreshControl, SectionList, Text, View} f
|
||||
|
||||
import {ListTypes} from 'app/constants';
|
||||
import {makeStyleSheetFromTheme, changeOpacity} from 'app/utils/theme';
|
||||
import {paddingLeft as padding} from 'app/components/safe_area_view/iphone_x_spacing';
|
||||
|
||||
export const FLATLIST = 'flat';
|
||||
export const SECTIONLIST = 'section';
|
||||
@@ -32,13 +31,11 @@ export default class CustomList extends PureComponent {
|
||||
selectable: PropTypes.bool,
|
||||
theme: PropTypes.object.isRequired,
|
||||
shouldRenderSeparator: PropTypes.bool,
|
||||
isLandscape: PropTypes.bool.isRequired,
|
||||
testID: PropTypes.string,
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
canRefresh: true,
|
||||
isLandscape: false,
|
||||
listType: FLATLIST,
|
||||
showNoResults: true,
|
||||
shouldRenderSeparator: true,
|
||||
@@ -165,13 +162,13 @@ export default class CustomList extends PureComponent {
|
||||
};
|
||||
|
||||
renderSectionHeader = ({section}) => {
|
||||
const {theme, isLandscape} = this.props;
|
||||
const {theme} = this.props;
|
||||
const style = getStyleFromTheme(theme);
|
||||
|
||||
return (
|
||||
<View style={style.sectionWrapper}>
|
||||
<View style={style.sectionContainer}>
|
||||
<Text style={[style.sectionText, padding(isLandscape)]}>{section.id}</Text>
|
||||
<Text style={style.sectionText}>{section.id}</Text>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
|
||||
@@ -4,13 +4,11 @@
|
||||
import {connect} from 'react-redux';
|
||||
|
||||
import {getTheme} from '@mm-redux/selectors/entities/preferences';
|
||||
import {isLandscape} from 'app/selectors/device';
|
||||
import OptionListRow from './option_list_row';
|
||||
|
||||
function mapStateToProps(state) {
|
||||
return {
|
||||
theme: getTheme(state),
|
||||
isLandscape: isLandscape(state),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -17,17 +17,12 @@ export default class OptionListRow extends React.PureComponent {
|
||||
id: PropTypes.string,
|
||||
theme: PropTypes.object.isRequired,
|
||||
...CustomListRow.propTypes,
|
||||
isLandscape: PropTypes.bool.isRequired,
|
||||
};
|
||||
|
||||
static contextTypes = {
|
||||
intl: intlShape,
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
isLandscape: false,
|
||||
};
|
||||
|
||||
onPress = () => {
|
||||
if (this.props.onPress) {
|
||||
this.props.onPress(this.props.id, this.props.item);
|
||||
@@ -41,7 +36,6 @@ export default class OptionListRow extends React.PureComponent {
|
||||
selected,
|
||||
theme,
|
||||
item,
|
||||
isLandscape,
|
||||
} = this.props;
|
||||
|
||||
const {text, value} = item;
|
||||
@@ -54,7 +48,6 @@ export default class OptionListRow extends React.PureComponent {
|
||||
enabled={enabled}
|
||||
selectable={selectable}
|
||||
selected={selected}
|
||||
isLandscape={isLandscape}
|
||||
>
|
||||
<View style={style.textContainer}>
|
||||
<View>
|
||||
|
||||
@@ -14,7 +14,6 @@ exports[`UserListRow should match snapshot 1`] = `
|
||||
<CustomListRow
|
||||
enabled={true}
|
||||
id="21345"
|
||||
isLandscape={false}
|
||||
onPress={[Function]}
|
||||
>
|
||||
<View
|
||||
@@ -147,7 +146,6 @@ exports[`UserListRow should match snapshot for currentUser with (you) populated
|
||||
<CustomListRow
|
||||
enabled={true}
|
||||
id="21345"
|
||||
isLandscape={false}
|
||||
onPress={[Function]}
|
||||
>
|
||||
<View
|
||||
@@ -278,7 +276,6 @@ exports[`UserListRow should match snapshot for deactivated user 1`] = `
|
||||
<CustomListRow
|
||||
enabled={true}
|
||||
id="21345"
|
||||
isLandscape={false}
|
||||
onPress={[Function]}
|
||||
>
|
||||
<View
|
||||
@@ -422,7 +419,6 @@ exports[`UserListRow should match snapshot for guest user 1`] = `
|
||||
<CustomListRow
|
||||
enabled={true}
|
||||
id="21345"
|
||||
isLandscape={false}
|
||||
onPress={[Function]}
|
||||
>
|
||||
<View
|
||||
|
||||
@@ -5,7 +5,6 @@ import {connect} from 'react-redux';
|
||||
|
||||
import {getTeammateNameDisplaySetting, getTheme} from '@mm-redux/selectors/entities/preferences';
|
||||
import {getCurrentUserId, getUser} from '@mm-redux/selectors/entities/users';
|
||||
import {isLandscape} from 'app/selectors/device';
|
||||
import UserListRow from './user_list_row';
|
||||
|
||||
function mapStateToProps(state, ownProps) {
|
||||
@@ -14,7 +13,6 @@ function mapStateToProps(state, ownProps) {
|
||||
theme: getTheme(state),
|
||||
user: getUser(state, ownProps.id),
|
||||
teammateNameDisplay: getTeammateNameDisplaySetting(state),
|
||||
isLandscape: isLandscape(state),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -48,7 +48,6 @@ export default class UserListRow extends React.PureComponent {
|
||||
teammateNameDisplay,
|
||||
theme,
|
||||
user,
|
||||
isLandscape,
|
||||
} = this.props;
|
||||
|
||||
const {id, username} = user;
|
||||
@@ -73,7 +72,6 @@ export default class UserListRow extends React.PureComponent {
|
||||
enabled={enabled}
|
||||
selectable={selectable}
|
||||
selected={selected}
|
||||
isLandscape={isLandscape}
|
||||
testID={this.props.testID}
|
||||
>
|
||||
<View style={style.profileContainer}>
|
||||
|
||||
@@ -29,7 +29,6 @@ describe('UserListRow', () => {
|
||||
},
|
||||
theme: Preferences.THEMES.default,
|
||||
teammateNameDisplay: 'test',
|
||||
isLandscape: false,
|
||||
};
|
||||
|
||||
test('should match snapshot', () => {
|
||||
|
||||
@@ -79,4 +79,4 @@ const getStyleSheet = makeStyleSheetFromTheme((theme) => {
|
||||
};
|
||||
});
|
||||
|
||||
export default DeletedPost;
|
||||
export default DeletedPost;
|
||||
|
||||
@@ -1,7 +1,20 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`EditChannelInfo should match snapshot 1`] = `
|
||||
<React.Fragment>
|
||||
<RNCSafeAreaView
|
||||
edges={
|
||||
Array [
|
||||
"bottom",
|
||||
"left",
|
||||
"right",
|
||||
]
|
||||
}
|
||||
style={
|
||||
Object {
|
||||
"flex": 1,
|
||||
}
|
||||
}
|
||||
>
|
||||
<Connect(StatusBar) />
|
||||
<KeyboardAwareScrollView
|
||||
enableAutomaticScroll={true}
|
||||
@@ -17,22 +30,18 @@ exports[`EditChannelInfo should match snapshot 1`] = `
|
||||
"flex": 1,
|
||||
}
|
||||
}
|
||||
testID="edit_channel_info"
|
||||
>
|
||||
<TouchableWithoutFeedback
|
||||
onPress={[Function]}
|
||||
>
|
||||
<View
|
||||
style={
|
||||
Array [
|
||||
Object {
|
||||
"backgroundColor": "rgba(61,60,64,0.06)",
|
||||
"flex": 1,
|
||||
"paddingTop": 30,
|
||||
},
|
||||
Object {
|
||||
"height": 600,
|
||||
},
|
||||
]
|
||||
Object {
|
||||
"backgroundColor": "rgba(61,60,64,0.06)",
|
||||
"flex": 1,
|
||||
"paddingTop": 30,
|
||||
}
|
||||
}
|
||||
>
|
||||
<View>
|
||||
@@ -41,30 +50,20 @@ exports[`EditChannelInfo should match snapshot 1`] = `
|
||||
defaultMessage="Name"
|
||||
id="channel_modal.name"
|
||||
style={
|
||||
Array [
|
||||
Object {
|
||||
"color": "#3d3c40",
|
||||
"fontSize": 14,
|
||||
"marginLeft": 15,
|
||||
},
|
||||
Object {
|
||||
"paddingHorizontal": 44,
|
||||
},
|
||||
]
|
||||
Object {
|
||||
"color": "#3d3c40",
|
||||
"fontSize": 14,
|
||||
"marginLeft": 15,
|
||||
}
|
||||
}
|
||||
/>
|
||||
</View>
|
||||
<View
|
||||
style={
|
||||
Array [
|
||||
Object {
|
||||
"backgroundColor": "#ffffff",
|
||||
"marginTop": 10,
|
||||
},
|
||||
Object {
|
||||
"paddingHorizontal": 44,
|
||||
},
|
||||
]
|
||||
Object {
|
||||
"backgroundColor": "#ffffff",
|
||||
"marginTop": 10,
|
||||
}
|
||||
}
|
||||
>
|
||||
<TextInputWithLocalizedPlaceholder
|
||||
@@ -89,7 +88,7 @@ exports[`EditChannelInfo should match snapshot 1`] = `
|
||||
"paddingHorizontal": 15,
|
||||
}
|
||||
}
|
||||
testID="edit_channel.name.input"
|
||||
testID="edit_channel_info.name.input"
|
||||
underlineColorAndroid="transparent"
|
||||
value="display_name"
|
||||
/>
|
||||
@@ -98,15 +97,10 @@ exports[`EditChannelInfo should match snapshot 1`] = `
|
||||
<View>
|
||||
<View
|
||||
style={
|
||||
Array [
|
||||
Object {
|
||||
"flexDirection": "row",
|
||||
"marginTop": 30,
|
||||
},
|
||||
Object {
|
||||
"paddingHorizontal": 44,
|
||||
},
|
||||
]
|
||||
Object {
|
||||
"flexDirection": "row",
|
||||
"marginTop": 30,
|
||||
}
|
||||
}
|
||||
>
|
||||
<FormattedText
|
||||
@@ -134,15 +128,10 @@ exports[`EditChannelInfo should match snapshot 1`] = `
|
||||
</View>
|
||||
<View
|
||||
style={
|
||||
Array [
|
||||
Object {
|
||||
"backgroundColor": "#ffffff",
|
||||
"marginTop": 10,
|
||||
},
|
||||
Object {
|
||||
"paddingHorizontal": 44,
|
||||
},
|
||||
]
|
||||
Object {
|
||||
"backgroundColor": "#ffffff",
|
||||
"marginTop": 10,
|
||||
}
|
||||
}
|
||||
>
|
||||
<TextInputWithLocalizedPlaceholder
|
||||
@@ -173,7 +162,7 @@ exports[`EditChannelInfo should match snapshot 1`] = `
|
||||
},
|
||||
]
|
||||
}
|
||||
testID="edit_channel.purpose.input"
|
||||
testID="edit_channel_info.purpose.input"
|
||||
textAlignVertical="top"
|
||||
underlineColorAndroid="transparent"
|
||||
value="purpose"
|
||||
@@ -184,17 +173,12 @@ exports[`EditChannelInfo should match snapshot 1`] = `
|
||||
defaultMessage="Describe how this channel should be used."
|
||||
id="channel_modal.descriptionHelp"
|
||||
style={
|
||||
Array [
|
||||
Object {
|
||||
"color": "rgba(61,60,64,0.5)",
|
||||
"fontSize": 14,
|
||||
"marginHorizontal": 15,
|
||||
"marginTop": 10,
|
||||
},
|
||||
Object {
|
||||
"paddingHorizontal": 44,
|
||||
},
|
||||
]
|
||||
Object {
|
||||
"color": "rgba(61,60,64,0.5)",
|
||||
"fontSize": 14,
|
||||
"marginHorizontal": 15,
|
||||
"marginTop": 10,
|
||||
}
|
||||
}
|
||||
/>
|
||||
</View>
|
||||
@@ -202,15 +186,10 @@ exports[`EditChannelInfo should match snapshot 1`] = `
|
||||
<View
|
||||
onLayout={[Function]}
|
||||
style={
|
||||
Array [
|
||||
Object {
|
||||
"flexDirection": "row",
|
||||
"marginTop": 15,
|
||||
},
|
||||
Object {
|
||||
"paddingHorizontal": 44,
|
||||
},
|
||||
]
|
||||
Object {
|
||||
"flexDirection": "row",
|
||||
"marginTop": 15,
|
||||
}
|
||||
}
|
||||
>
|
||||
<FormattedText
|
||||
@@ -238,15 +217,10 @@ exports[`EditChannelInfo should match snapshot 1`] = `
|
||||
</View>
|
||||
<View
|
||||
style={
|
||||
Array [
|
||||
Object {
|
||||
"backgroundColor": "#ffffff",
|
||||
"marginTop": 10,
|
||||
},
|
||||
Object {
|
||||
"paddingHorizontal": 44,
|
||||
},
|
||||
]
|
||||
Object {
|
||||
"backgroundColor": "#ffffff",
|
||||
"marginTop": 10,
|
||||
}
|
||||
}
|
||||
>
|
||||
<TextInputWithLocalizedPlaceholder
|
||||
@@ -278,7 +252,7 @@ exports[`EditChannelInfo should match snapshot 1`] = `
|
||||
},
|
||||
]
|
||||
}
|
||||
testID="edit_channel.header.input"
|
||||
testID="edit_channel_info.header.input"
|
||||
textAlignVertical="top"
|
||||
underlineColorAndroid="transparent"
|
||||
value="header"
|
||||
@@ -295,17 +269,12 @@ exports[`EditChannelInfo should match snapshot 1`] = `
|
||||
defaultMessage="Set text that will appear in the header of the channel beside the channel name. For example, include frequently used links by typing [Link Title](http://example.com)."
|
||||
id="channel_modal.headerHelp"
|
||||
style={
|
||||
Array [
|
||||
Object {
|
||||
"color": "rgba(61,60,64,0.5)",
|
||||
"fontSize": 14,
|
||||
"marginHorizontal": 15,
|
||||
"marginTop": 10,
|
||||
},
|
||||
Object {
|
||||
"paddingHorizontal": 44,
|
||||
},
|
||||
]
|
||||
Object {
|
||||
"color": "rgba(61,60,64,0.5)",
|
||||
"fontSize": 14,
|
||||
"marginHorizontal": 15,
|
||||
"marginTop": 10,
|
||||
}
|
||||
}
|
||||
/>
|
||||
</View>
|
||||
@@ -342,5 +311,5 @@ exports[`EditChannelInfo should match snapshot 1`] = `
|
||||
value="header"
|
||||
/>
|
||||
</View>
|
||||
</React.Fragment>
|
||||
</RNCSafeAreaView>
|
||||
`;
|
||||
|
||||
@@ -1,453 +0,0 @@
|
||||
// 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 {
|
||||
Platform,
|
||||
TouchableWithoutFeedback,
|
||||
View,
|
||||
} from 'react-native';
|
||||
import {KeyboardAwareScrollView} from 'react-native-keyboard-aware-scrollview';
|
||||
|
||||
import {General} from '@mm-redux/constants';
|
||||
|
||||
import Autocomplete, {AUTOCOMPLETE_MAX_HEIGHT} from 'app/components/autocomplete';
|
||||
import ErrorText from 'app/components/error_text';
|
||||
import FormattedText from 'app/components/formatted_text';
|
||||
import Loading from 'app/components/loading';
|
||||
import StatusBar from 'app/components/status_bar';
|
||||
import TextInputWithLocalizedPlaceholder from 'app/components/text_input_with_localized_placeholder';
|
||||
import {paddingHorizontal as padding} from 'app/components/safe_area_view/iphone_x_spacing';
|
||||
|
||||
import {
|
||||
changeOpacity,
|
||||
makeStyleSheetFromTheme,
|
||||
getKeyboardAppearanceFromTheme,
|
||||
} from 'app/utils/theme';
|
||||
|
||||
import {t} from 'app/utils/i18n';
|
||||
import {popTopScreen, dismissModal} from 'app/actions/navigation';
|
||||
|
||||
export default class EditChannelInfo extends PureComponent {
|
||||
static propTypes = {
|
||||
theme: PropTypes.object.isRequired,
|
||||
deviceWidth: PropTypes.number.isRequired,
|
||||
deviceHeight: PropTypes.number.isRequired,
|
||||
channelType: PropTypes.string,
|
||||
enableRightButton: PropTypes.func,
|
||||
saving: PropTypes.bool.isRequired,
|
||||
editing: PropTypes.bool,
|
||||
error: PropTypes.oneOfType([PropTypes.string, PropTypes.object]),
|
||||
displayName: PropTypes.string,
|
||||
currentTeamUrl: PropTypes.string,
|
||||
channelURL: PropTypes.string,
|
||||
purpose: PropTypes.string,
|
||||
header: PropTypes.string,
|
||||
onDisplayNameChange: PropTypes.func,
|
||||
onChannelURLChange: PropTypes.func,
|
||||
onPurposeChange: PropTypes.func,
|
||||
onHeaderChange: PropTypes.func,
|
||||
oldDisplayName: PropTypes.string,
|
||||
oldChannelURL: PropTypes.string,
|
||||
oldHeader: PropTypes.string,
|
||||
oldPurpose: PropTypes.string,
|
||||
isLandscape: PropTypes.bool.isRequired,
|
||||
testID: PropTypes.string,
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
editing: false,
|
||||
};
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.nameInput = React.createRef();
|
||||
this.urlInput = React.createRef();
|
||||
this.purposeInput = React.createRef();
|
||||
this.headerInput = React.createRef();
|
||||
this.scroll = React.createRef();
|
||||
|
||||
this.state = {
|
||||
keyboardVisible: false,
|
||||
keyboardPosition: 0,
|
||||
};
|
||||
}
|
||||
|
||||
blur = () => {
|
||||
if (this.nameInput?.current) {
|
||||
this.nameInput.current.blur();
|
||||
}
|
||||
|
||||
// TODO: uncomment below once the channel URL field is added
|
||||
// if (this.urlInput?.current) {
|
||||
// this.urlInput.current.blur();
|
||||
// }
|
||||
|
||||
if (this.purposeInput?.current) {
|
||||
this.purposeInput.current.blur();
|
||||
}
|
||||
if (this.headerInput?.current) {
|
||||
this.headerInput.current.blur();
|
||||
}
|
||||
|
||||
if (this.scroll?.current) {
|
||||
this.scroll.current.scrollTo({x: 0, y: 0, animated: true});
|
||||
}
|
||||
};
|
||||
|
||||
close = (goBack = false) => {
|
||||
if (goBack) {
|
||||
popTopScreen();
|
||||
} else {
|
||||
dismissModal();
|
||||
}
|
||||
};
|
||||
|
||||
canUpdate = (displayName, channelURL, purpose, header) => {
|
||||
const {
|
||||
oldDisplayName,
|
||||
oldChannelURL,
|
||||
oldPurpose,
|
||||
oldHeader,
|
||||
} = this.props;
|
||||
|
||||
return displayName !== oldDisplayName || channelURL !== oldChannelURL ||
|
||||
purpose !== oldPurpose || header !== oldHeader;
|
||||
};
|
||||
|
||||
enableRightButton = (enable = false) => {
|
||||
this.props.enableRightButton(enable);
|
||||
};
|
||||
|
||||
onDisplayNameChangeText = (displayName) => {
|
||||
const {editing, onDisplayNameChange} = this.props;
|
||||
onDisplayNameChange(displayName);
|
||||
|
||||
if (editing) {
|
||||
const {channelURL, purpose, header} = this.props;
|
||||
const canUpdate = this.canUpdate(displayName, channelURL, purpose, header);
|
||||
this.enableRightButton(canUpdate);
|
||||
return;
|
||||
}
|
||||
|
||||
const displayNameExists = displayName && displayName.length >= 2;
|
||||
this.props.enableRightButton(displayNameExists);
|
||||
};
|
||||
|
||||
onPurposeChangeText = (purpose) => {
|
||||
const {editing, onPurposeChange} = this.props;
|
||||
onPurposeChange(purpose);
|
||||
|
||||
if (editing) {
|
||||
const {displayName, channelURL, header} = this.props;
|
||||
const canUpdate = this.canUpdate(displayName, channelURL, purpose, header);
|
||||
this.enableRightButton(canUpdate);
|
||||
}
|
||||
};
|
||||
|
||||
onHeaderChangeText = (header) => {
|
||||
const {editing, onHeaderChange} = this.props;
|
||||
onHeaderChange(header);
|
||||
|
||||
if (editing) {
|
||||
const {displayName, channelURL, purpose} = this.props;
|
||||
const canUpdate = this.canUpdate(displayName, channelURL, purpose, header);
|
||||
this.enableRightButton(canUpdate);
|
||||
}
|
||||
};
|
||||
|
||||
onHeaderLayout = ({nativeEvent}) => {
|
||||
this.setState({headerPosition: nativeEvent.layout.y});
|
||||
}
|
||||
|
||||
onKeyboardDidShow = () => {
|
||||
this.setState({keyboardVisible: true});
|
||||
|
||||
if (this.state.headerHasFocus) {
|
||||
this.setState({headerHasFocus: false});
|
||||
this.scrollHeaderToTop();
|
||||
}
|
||||
}
|
||||
|
||||
onKeyboardDidHide = () => {
|
||||
this.setState({keyboardVisible: false});
|
||||
}
|
||||
|
||||
onKeyboardOffsetChanged = (keyboardPosition) => {
|
||||
this.setState({keyboardPosition});
|
||||
}
|
||||
|
||||
onHeaderFocus = () => {
|
||||
if (this.state.keyboardVisible) {
|
||||
this.scrollHeaderToTop();
|
||||
} else {
|
||||
this.setState({headerHasFocus: true});
|
||||
}
|
||||
};
|
||||
|
||||
scrollHeaderToTop = () => {
|
||||
if (this.scroll.current) {
|
||||
this.scroll.current.scrollTo({x: 0, y: this.state.headerPosition});
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
theme,
|
||||
channelType,
|
||||
deviceWidth,
|
||||
deviceHeight,
|
||||
displayName,
|
||||
header,
|
||||
purpose,
|
||||
isLandscape,
|
||||
error,
|
||||
saving,
|
||||
testID,
|
||||
} = this.props;
|
||||
const {keyboardVisible, keyboardPosition} = this.state;
|
||||
const bottomStyle = {
|
||||
bottom: Platform.select({
|
||||
ios: keyboardPosition,
|
||||
android: 0,
|
||||
}),
|
||||
};
|
||||
const style = getStyleSheet(theme);
|
||||
|
||||
const displayHeaderOnly = channelType === General.DM_CHANNEL ||
|
||||
channelType === General.GM_CHANNEL;
|
||||
|
||||
if (saving) {
|
||||
return (
|
||||
<View style={style.container}>
|
||||
<StatusBar/>
|
||||
<Loading color={theme.centerChannelColor}/>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
let displayError;
|
||||
if (error) {
|
||||
displayError = (
|
||||
<View style={[style.errorContainer, {width: deviceWidth}]}>
|
||||
<View style={[style.errorWrapper, padding(isLandscape)]}>
|
||||
<ErrorText error={error}/>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
<StatusBar/>
|
||||
<KeyboardAwareScrollView
|
||||
testID={testID}
|
||||
ref={this.scroll}
|
||||
style={style.container}
|
||||
keyboardShouldPersistTaps={'always'}
|
||||
onKeyboardDidShow={this.onKeyboardDidShow}
|
||||
onKeyboardDidHide={this.onKeyboardDidHide}
|
||||
enableAutomaticScroll={!keyboardVisible}
|
||||
>
|
||||
{displayError}
|
||||
<TouchableWithoutFeedback onPress={this.blur}>
|
||||
<View style={[style.scrollView, {height: deviceHeight + (Platform.OS === 'android' ? 200 : 0)}]}>
|
||||
{!displayHeaderOnly && (
|
||||
<View>
|
||||
<View>
|
||||
<FormattedText
|
||||
style={[style.title, padding(isLandscape)]}
|
||||
id='channel_modal.name'
|
||||
defaultMessage='Name'
|
||||
/>
|
||||
</View>
|
||||
<View style={[style.inputContainer, padding(isLandscape)]}>
|
||||
<TextInputWithLocalizedPlaceholder
|
||||
testID='edit_channel.name.input'
|
||||
ref={this.nameInput}
|
||||
value={displayName}
|
||||
onChangeText={this.onDisplayNameChangeText}
|
||||
style={style.input}
|
||||
autoCapitalize='none'
|
||||
autoCorrect={false}
|
||||
placeholder={{id: t('channel_modal.nameEx'), defaultMessage: 'E.g.: "Bugs", "Marketing", "客户支持"'}}
|
||||
placeholderTextColor={changeOpacity(theme.centerChannelColor, 0.5)}
|
||||
underlineColorAndroid='transparent'
|
||||
disableFullscreenUI={true}
|
||||
maxLength={64}
|
||||
keyboardAppearance={getKeyboardAppearanceFromTheme(theme)}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
)}
|
||||
{!displayHeaderOnly && (
|
||||
<View>
|
||||
<View style={[style.titleContainer30, padding(isLandscape)]}>
|
||||
<FormattedText
|
||||
style={style.title}
|
||||
id='channel_modal.purpose'
|
||||
defaultMessage='Purpose'
|
||||
/>
|
||||
<FormattedText
|
||||
style={style.optional}
|
||||
id='channel_modal.optional'
|
||||
defaultMessage='(optional)'
|
||||
/>
|
||||
</View>
|
||||
<View style={[style.inputContainer, padding(isLandscape)]}>
|
||||
<TextInputWithLocalizedPlaceholder
|
||||
testID='edit_channel.purpose.input'
|
||||
ref={this.purposeInput}
|
||||
value={purpose}
|
||||
onChangeText={this.onPurposeChangeText}
|
||||
style={[style.input, {height: 110}]}
|
||||
autoCapitalize='none'
|
||||
autoCorrect={false}
|
||||
placeholder={{id: t('channel_modal.purposeEx'), defaultMessage: 'E.g.: "A channel to file bugs and improvements"'}}
|
||||
placeholderTextColor={changeOpacity(theme.centerChannelColor, 0.5)}
|
||||
multiline={true}
|
||||
blurOnSubmit={false}
|
||||
textAlignVertical='top'
|
||||
underlineColorAndroid='transparent'
|
||||
disableFullscreenUI={true}
|
||||
keyboardAppearance={getKeyboardAppearanceFromTheme(theme)}
|
||||
/>
|
||||
</View>
|
||||
<View>
|
||||
<FormattedText
|
||||
style={[style.helpText, padding(isLandscape)]}
|
||||
id='channel_modal.descriptionHelp'
|
||||
defaultMessage='Describe how this channel should be used.'
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
)}
|
||||
<View
|
||||
onLayout={this.onHeaderLayout}
|
||||
style={[style.titleContainer15, padding(isLandscape)]}
|
||||
>
|
||||
<FormattedText
|
||||
style={style.title}
|
||||
id='channel_modal.header'
|
||||
defaultMessage='Header'
|
||||
/>
|
||||
<FormattedText
|
||||
style={style.optional}
|
||||
id='channel_modal.optional'
|
||||
defaultMessage='(optional)'
|
||||
/>
|
||||
</View>
|
||||
<View style={[style.inputContainer, padding(isLandscape)]}>
|
||||
<TextInputWithLocalizedPlaceholder
|
||||
testID={'edit_channel.header.input'}
|
||||
ref={this.headerInput}
|
||||
value={header}
|
||||
onChangeText={this.onHeaderChangeText}
|
||||
style={[style.input, {height: 110}]}
|
||||
autoCapitalize='none'
|
||||
autoCorrect={false}
|
||||
placeholder={{id: t('channel_modal.headerEx'), defaultMessage: 'E.g.: "[Link Title](http://example.com)"'}}
|
||||
placeholderTextColor={changeOpacity(theme.centerChannelColor, 0.5)}
|
||||
multiline={true}
|
||||
blurOnSubmit={false}
|
||||
onFocus={this.onHeaderFocus}
|
||||
textAlignVertical='top'
|
||||
underlineColorAndroid='transparent'
|
||||
disableFullscreenUI={true}
|
||||
keyboardAppearance={getKeyboardAppearanceFromTheme(theme)}
|
||||
/>
|
||||
</View>
|
||||
<View style={style.headerHelpText}>
|
||||
<FormattedText
|
||||
style={[style.helpText, padding(isLandscape)]}
|
||||
id='channel_modal.headerHelp'
|
||||
defaultMessage={'Set text that will appear in the header of the channel beside the channel name. For example, include frequently used links by typing [Link Title](http://example.com).'}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
</TouchableWithoutFeedback>
|
||||
</KeyboardAwareScrollView>
|
||||
<View style={[style.autocompleteContainer, bottomStyle]}>
|
||||
<Autocomplete
|
||||
cursorPosition={header.length}
|
||||
maxHeight={AUTOCOMPLETE_MAX_HEIGHT}
|
||||
onChangeText={this.onHeaderChangeText}
|
||||
value={header}
|
||||
nestedScrollEnabled={true}
|
||||
onKeyboardOffsetChanged={this.onKeyboardOffsetChanged}
|
||||
offsetY={8}
|
||||
style={style.autocomplete}
|
||||
/>
|
||||
</View>
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const getStyleSheet = makeStyleSheetFromTheme((theme) => {
|
||||
return {
|
||||
autocomplete: {
|
||||
position: undefined,
|
||||
},
|
||||
autocompleteContainer: {
|
||||
position: 'absolute',
|
||||
width: '100%',
|
||||
flex: 1,
|
||||
justifyContent: 'flex-end',
|
||||
},
|
||||
container: {
|
||||
flex: 1,
|
||||
},
|
||||
scrollView: {
|
||||
flex: 1,
|
||||
backgroundColor: changeOpacity(theme.centerChannelColor, 0.06),
|
||||
paddingTop: 30,
|
||||
},
|
||||
errorContainer: {
|
||||
backgroundColor: changeOpacity(theme.centerChannelColor, 0.06),
|
||||
},
|
||||
errorWrapper: {
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
},
|
||||
inputContainer: {
|
||||
marginTop: 10,
|
||||
backgroundColor: theme.centerChannelBg,
|
||||
},
|
||||
input: {
|
||||
color: theme.centerChannelColor,
|
||||
fontSize: 14,
|
||||
height: 40,
|
||||
paddingHorizontal: 15,
|
||||
},
|
||||
titleContainer30: {
|
||||
flexDirection: 'row',
|
||||
marginTop: 30,
|
||||
},
|
||||
titleContainer15: {
|
||||
flexDirection: 'row',
|
||||
marginTop: 15,
|
||||
},
|
||||
title: {
|
||||
fontSize: 14,
|
||||
color: theme.centerChannelColor,
|
||||
marginLeft: 15,
|
||||
},
|
||||
optional: {
|
||||
color: changeOpacity(theme.centerChannelColor, 0.5),
|
||||
fontSize: 14,
|
||||
marginLeft: 5,
|
||||
},
|
||||
helpText: {
|
||||
fontSize: 14,
|
||||
color: changeOpacity(theme.centerChannelColor, 0.5),
|
||||
marginTop: 10,
|
||||
marginHorizontal: 15,
|
||||
},
|
||||
headerHelpText: {
|
||||
zIndex: -1,
|
||||
},
|
||||
};
|
||||
});
|
||||
@@ -7,10 +7,11 @@ import {shallow} from 'enzyme';
|
||||
import Preferences from '@mm-redux/constants/preferences';
|
||||
|
||||
import Autocomplete from 'app/components/autocomplete';
|
||||
import EditChannelInfo from './edit_channel_info';
|
||||
import EditChannelInfo from './index';
|
||||
|
||||
describe('EditChannelInfo', () => {
|
||||
const baseProps = {
|
||||
testID: 'edit_channel_info',
|
||||
theme: Preferences.THEMES.default,
|
||||
deviceWidth: 400,
|
||||
deviceHeight: 600,
|
||||
@@ -32,7 +33,6 @@ describe('EditChannelInfo', () => {
|
||||
oldChannelURL: '/team_a/channels/channel_old',
|
||||
oldHeader: 'old_header',
|
||||
oldPurpose: 'old_purpose',
|
||||
isLandscape: true,
|
||||
};
|
||||
|
||||
test('should match snapshot', () => {
|
||||
@@ -91,4 +91,4 @@ describe('EditChannelInfo', () => {
|
||||
instance.onKeyboardDidShow();
|
||||
expect(instance.scrollHeaderToTop).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,15 +1,456 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {connect} from 'react-redux';
|
||||
import React, {PureComponent} from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {
|
||||
Platform,
|
||||
TouchableWithoutFeedback,
|
||||
View,
|
||||
} from 'react-native';
|
||||
import {KeyboardAwareScrollView} from 'react-native-keyboard-aware-scrollview';
|
||||
import {SafeAreaView} from 'react-native-safe-area-context';
|
||||
|
||||
import {isLandscape} from 'app/selectors/device';
|
||||
import EditChannelInfo from './edit_channel_info';
|
||||
import {General} from '@mm-redux/constants';
|
||||
|
||||
function mapStateToProps(state) {
|
||||
return {
|
||||
isLandscape: isLandscape(state),
|
||||
import Autocomplete from 'app/components/autocomplete';
|
||||
import ErrorText from 'app/components/error_text';
|
||||
import FormattedText from 'app/components/formatted_text';
|
||||
import Loading from 'app/components/loading';
|
||||
import StatusBar from 'app/components/status_bar';
|
||||
import TextInputWithLocalizedPlaceholder from 'app/components/text_input_with_localized_placeholder';
|
||||
import DEVICE from '@constants/device';
|
||||
|
||||
import {
|
||||
changeOpacity,
|
||||
makeStyleSheetFromTheme,
|
||||
getKeyboardAppearanceFromTheme,
|
||||
} from 'app/utils/theme';
|
||||
|
||||
import {t} from 'app/utils/i18n';
|
||||
import {popTopScreen, dismissModal} from 'app/actions/navigation';
|
||||
|
||||
export default class EditChannelInfo extends PureComponent {
|
||||
static propTypes = {
|
||||
theme: PropTypes.object.isRequired,
|
||||
channelType: PropTypes.string,
|
||||
enableRightButton: PropTypes.func,
|
||||
saving: PropTypes.bool.isRequired,
|
||||
editing: PropTypes.bool,
|
||||
error: PropTypes.oneOfType([PropTypes.string, PropTypes.object]),
|
||||
displayName: PropTypes.string,
|
||||
channelURL: PropTypes.string,
|
||||
purpose: PropTypes.string,
|
||||
header: PropTypes.string,
|
||||
onDisplayNameChange: PropTypes.func,
|
||||
onPurposeChange: PropTypes.func,
|
||||
onHeaderChange: PropTypes.func,
|
||||
oldDisplayName: PropTypes.string,
|
||||
oldChannelURL: PropTypes.string,
|
||||
oldHeader: PropTypes.string,
|
||||
oldPurpose: PropTypes.string,
|
||||
testID: PropTypes.string,
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
editing: false,
|
||||
};
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.nameInput = React.createRef();
|
||||
this.urlInput = React.createRef();
|
||||
this.purposeInput = React.createRef();
|
||||
this.headerInput = React.createRef();
|
||||
this.scroll = React.createRef();
|
||||
|
||||
this.state = {
|
||||
keyboardVisible: false,
|
||||
keyboardPosition: 0,
|
||||
};
|
||||
}
|
||||
|
||||
blur = () => {
|
||||
if (this.nameInput?.current) {
|
||||
this.nameInput.current.blur();
|
||||
}
|
||||
|
||||
// TODO: uncomment below once the channel URL field is added
|
||||
// if (this.urlInput?.current) {
|
||||
// this.urlInput.current.blur();
|
||||
// }
|
||||
|
||||
if (this.purposeInput?.current) {
|
||||
this.purposeInput.current.blur();
|
||||
}
|
||||
if (this.headerInput?.current) {
|
||||
this.headerInput.current.blur();
|
||||
}
|
||||
|
||||
if (this.scroll?.current) {
|
||||
this.scroll.current.scrollTo({x: 0, y: 0, animated: true});
|
||||
}
|
||||
};
|
||||
|
||||
close = (goBack = false) => {
|
||||
if (goBack) {
|
||||
popTopScreen();
|
||||
} else {
|
||||
dismissModal();
|
||||
}
|
||||
};
|
||||
|
||||
canUpdate = (displayName, channelURL, purpose, header) => {
|
||||
const {
|
||||
oldDisplayName,
|
||||
oldChannelURL,
|
||||
oldPurpose,
|
||||
oldHeader,
|
||||
} = this.props;
|
||||
|
||||
return displayName !== oldDisplayName || channelURL !== oldChannelURL ||
|
||||
purpose !== oldPurpose || header !== oldHeader;
|
||||
};
|
||||
|
||||
enableRightButton = (enable = false) => {
|
||||
this.props.enableRightButton(enable);
|
||||
};
|
||||
|
||||
onDisplayNameChangeText = (displayName) => {
|
||||
const {editing, onDisplayNameChange} = this.props;
|
||||
onDisplayNameChange(displayName);
|
||||
|
||||
if (editing) {
|
||||
const {channelURL, purpose, header} = this.props;
|
||||
const canUpdate = this.canUpdate(displayName, channelURL, purpose, header);
|
||||
this.enableRightButton(canUpdate);
|
||||
return;
|
||||
}
|
||||
|
||||
const displayNameExists = displayName && displayName.length >= 2;
|
||||
this.props.enableRightButton(displayNameExists);
|
||||
};
|
||||
|
||||
onPurposeChangeText = (purpose) => {
|
||||
const {editing, onPurposeChange} = this.props;
|
||||
onPurposeChange(purpose);
|
||||
|
||||
if (editing) {
|
||||
const {displayName, channelURL, header} = this.props;
|
||||
const canUpdate = this.canUpdate(displayName, channelURL, purpose, header);
|
||||
this.enableRightButton(canUpdate);
|
||||
}
|
||||
};
|
||||
|
||||
onHeaderChangeText = (header) => {
|
||||
const {editing, onHeaderChange} = this.props;
|
||||
onHeaderChange(header);
|
||||
|
||||
if (editing) {
|
||||
const {displayName, channelURL, purpose} = this.props;
|
||||
const canUpdate = this.canUpdate(displayName, channelURL, purpose, header);
|
||||
this.enableRightButton(canUpdate);
|
||||
}
|
||||
};
|
||||
|
||||
onHeaderLayout = ({nativeEvent}) => {
|
||||
this.setState({headerPosition: nativeEvent.layout.y});
|
||||
}
|
||||
|
||||
onKeyboardDidShow = () => {
|
||||
this.setState({keyboardVisible: true});
|
||||
|
||||
if (this.state.headerHasFocus) {
|
||||
this.setState({headerHasFocus: false});
|
||||
this.scrollHeaderToTop();
|
||||
}
|
||||
}
|
||||
|
||||
onKeyboardDidHide = () => {
|
||||
this.setState({keyboardVisible: false});
|
||||
}
|
||||
|
||||
onKeyboardOffsetChanged = (keyboardPosition) => {
|
||||
this.setState({keyboardPosition});
|
||||
}
|
||||
|
||||
onHeaderFocus = () => {
|
||||
if (this.state.keyboardVisible) {
|
||||
this.scrollHeaderToTop();
|
||||
} else {
|
||||
this.setState({headerHasFocus: true});
|
||||
}
|
||||
};
|
||||
|
||||
scrollHeaderToTop = () => {
|
||||
if (this.scroll.current) {
|
||||
this.scroll.current.scrollTo({x: 0, y: this.state.headerPosition});
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
theme,
|
||||
channelType,
|
||||
displayName,
|
||||
header,
|
||||
purpose,
|
||||
error,
|
||||
saving,
|
||||
testID,
|
||||
} = this.props;
|
||||
const {keyboardVisible, keyboardPosition} = this.state;
|
||||
const bottomStyle = {
|
||||
bottom: Platform.select({
|
||||
ios: keyboardPosition,
|
||||
android: 0,
|
||||
}),
|
||||
};
|
||||
const style = getStyleSheet(theme);
|
||||
|
||||
const displayHeaderOnly = channelType === General.DM_CHANNEL ||
|
||||
channelType === General.GM_CHANNEL;
|
||||
|
||||
if (saving) {
|
||||
return (
|
||||
<View style={style.container}>
|
||||
<StatusBar/>
|
||||
<Loading color={theme.centerChannelColor}/>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
let displayError;
|
||||
if (error) {
|
||||
displayError = (
|
||||
<SafeAreaView
|
||||
edges={['bottom', 'left', 'right']}
|
||||
style={style.errorContainer}
|
||||
>
|
||||
<View style={style.errorWrapper}>
|
||||
<ErrorText
|
||||
testID='edit_channel_info.error.text'
|
||||
error={error}
|
||||
/>
|
||||
</View>
|
||||
</SafeAreaView>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<SafeAreaView
|
||||
edges={['bottom', 'left', 'right']}
|
||||
style={style.container}
|
||||
>
|
||||
<StatusBar/>
|
||||
<KeyboardAwareScrollView
|
||||
testID={testID}
|
||||
ref={this.scroll}
|
||||
style={style.container}
|
||||
keyboardShouldPersistTaps={'always'}
|
||||
onKeyboardDidShow={this.onKeyboardDidShow}
|
||||
onKeyboardDidHide={this.onKeyboardDidHide}
|
||||
enableAutomaticScroll={!keyboardVisible}
|
||||
>
|
||||
{displayError}
|
||||
<TouchableWithoutFeedback onPress={this.blur}>
|
||||
<View style={style.scrollView}>
|
||||
{!displayHeaderOnly && (
|
||||
<View>
|
||||
<View>
|
||||
<FormattedText
|
||||
style={style.title}
|
||||
id='channel_modal.name'
|
||||
defaultMessage='Name'
|
||||
/>
|
||||
</View>
|
||||
<View style={style.inputContainer}>
|
||||
<TextInputWithLocalizedPlaceholder
|
||||
testID='edit_channel_info.name.input'
|
||||
ref={this.nameInput}
|
||||
value={displayName}
|
||||
onChangeText={this.onDisplayNameChangeText}
|
||||
style={style.input}
|
||||
autoCapitalize='none'
|
||||
autoCorrect={false}
|
||||
placeholder={{id: t('channel_modal.nameEx'), defaultMessage: 'E.g.: "Bugs", "Marketing", "客户支持"'}}
|
||||
placeholderTextColor={changeOpacity(theme.centerChannelColor, 0.5)}
|
||||
underlineColorAndroid='transparent'
|
||||
disableFullscreenUI={true}
|
||||
maxLength={64}
|
||||
keyboardAppearance={getKeyboardAppearanceFromTheme(theme)}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
)}
|
||||
{!displayHeaderOnly && (
|
||||
<View>
|
||||
<View style={style.titleContainer30}>
|
||||
<FormattedText
|
||||
style={style.title}
|
||||
id='channel_modal.purpose'
|
||||
defaultMessage='Purpose'
|
||||
/>
|
||||
<FormattedText
|
||||
style={style.optional}
|
||||
id='channel_modal.optional'
|
||||
defaultMessage='(optional)'
|
||||
/>
|
||||
</View>
|
||||
<View style={style.inputContainer}>
|
||||
<TextInputWithLocalizedPlaceholder
|
||||
testID='edit_channel_info.purpose.input'
|
||||
ref={this.purposeInput}
|
||||
value={purpose}
|
||||
onChangeText={this.onPurposeChangeText}
|
||||
style={[style.input, {height: 110}]}
|
||||
autoCapitalize='none'
|
||||
autoCorrect={false}
|
||||
placeholder={{id: t('channel_modal.purposeEx'), defaultMessage: 'E.g.: "A channel to file bugs and improvements"'}}
|
||||
placeholderTextColor={changeOpacity(theme.centerChannelColor, 0.5)}
|
||||
multiline={true}
|
||||
blurOnSubmit={false}
|
||||
textAlignVertical='top'
|
||||
underlineColorAndroid='transparent'
|
||||
disableFullscreenUI={true}
|
||||
keyboardAppearance={getKeyboardAppearanceFromTheme(theme)}
|
||||
/>
|
||||
</View>
|
||||
<View>
|
||||
<FormattedText
|
||||
style={style.helpText}
|
||||
id='channel_modal.descriptionHelp'
|
||||
defaultMessage='Describe how this channel should be used.'
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
)}
|
||||
<View
|
||||
onLayout={this.onHeaderLayout}
|
||||
style={style.titleContainer15}
|
||||
>
|
||||
<FormattedText
|
||||
style={style.title}
|
||||
id='channel_modal.header'
|
||||
defaultMessage='Header'
|
||||
/>
|
||||
<FormattedText
|
||||
style={style.optional}
|
||||
id='channel_modal.optional'
|
||||
defaultMessage='(optional)'
|
||||
/>
|
||||
</View>
|
||||
<View style={style.inputContainer}>
|
||||
<TextInputWithLocalizedPlaceholder
|
||||
testID='edit_channel_info.header.input'
|
||||
ref={this.headerInput}
|
||||
value={header}
|
||||
onChangeText={this.onHeaderChangeText}
|
||||
style={[style.input, {height: 110}]}
|
||||
autoCapitalize='none'
|
||||
autoCorrect={false}
|
||||
placeholder={{id: t('channel_modal.headerEx'), defaultMessage: 'E.g.: "[Link Title](http://example.com)"'}}
|
||||
placeholderTextColor={changeOpacity(theme.centerChannelColor, 0.5)}
|
||||
multiline={true}
|
||||
blurOnSubmit={false}
|
||||
onFocus={this.onHeaderFocus}
|
||||
textAlignVertical='top'
|
||||
underlineColorAndroid='transparent'
|
||||
disableFullscreenUI={true}
|
||||
keyboardAppearance={getKeyboardAppearanceFromTheme(theme)}
|
||||
/>
|
||||
</View>
|
||||
<View style={style.headerHelpText}>
|
||||
<FormattedText
|
||||
style={style.helpText}
|
||||
id='channel_modal.headerHelp'
|
||||
defaultMessage={'Set text that will appear in the header of the channel beside the channel name. For example, include frequently used links by typing [Link Title](http://example.com).'}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
</TouchableWithoutFeedback>
|
||||
</KeyboardAwareScrollView>
|
||||
<View style={[style.autocompleteContainer, bottomStyle]}>
|
||||
<Autocomplete
|
||||
cursorPosition={header.length}
|
||||
maxHeight={DEVICE.AUTOCOMPLETE_MAX_HEIGHT}
|
||||
onChangeText={this.onHeaderChangeText}
|
||||
value={header}
|
||||
nestedScrollEnabled={true}
|
||||
onKeyboardOffsetChanged={this.onKeyboardOffsetChanged}
|
||||
offsetY={8}
|
||||
style={style.autocomplete}
|
||||
/>
|
||||
</View>
|
||||
</SafeAreaView>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps)(EditChannelInfo);
|
||||
const getStyleSheet = makeStyleSheetFromTheme((theme) => {
|
||||
return {
|
||||
autocomplete: {
|
||||
position: undefined,
|
||||
},
|
||||
autocompleteContainer: {
|
||||
position: 'absolute',
|
||||
width: '100%',
|
||||
flex: 1,
|
||||
justifyContent: 'flex-end',
|
||||
},
|
||||
container: {
|
||||
flex: 1,
|
||||
},
|
||||
scrollView: {
|
||||
flex: 1,
|
||||
backgroundColor: changeOpacity(theme.centerChannelColor, 0.06),
|
||||
paddingTop: 30,
|
||||
},
|
||||
errorContainer: {
|
||||
backgroundColor: changeOpacity(theme.centerChannelColor, 0.06),
|
||||
width: '100%',
|
||||
},
|
||||
errorWrapper: {
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
},
|
||||
inputContainer: {
|
||||
marginTop: 10,
|
||||
backgroundColor: theme.centerChannelBg,
|
||||
},
|
||||
input: {
|
||||
color: theme.centerChannelColor,
|
||||
fontSize: 14,
|
||||
height: 40,
|
||||
paddingHorizontal: 15,
|
||||
},
|
||||
titleContainer30: {
|
||||
flexDirection: 'row',
|
||||
marginTop: 30,
|
||||
},
|
||||
titleContainer15: {
|
||||
flexDirection: 'row',
|
||||
marginTop: 15,
|
||||
},
|
||||
title: {
|
||||
fontSize: 14,
|
||||
color: theme.centerChannelColor,
|
||||
marginLeft: 15,
|
||||
},
|
||||
optional: {
|
||||
color: changeOpacity(theme.centerChannelColor, 0.5),
|
||||
fontSize: 14,
|
||||
marginLeft: 5,
|
||||
},
|
||||
helpText: {
|
||||
fontSize: 14,
|
||||
color: changeOpacity(theme.centerChannelColor, 0.5),
|
||||
marginTop: 10,
|
||||
marginHorizontal: 15,
|
||||
},
|
||||
headerHelpText: {
|
||||
zIndex: -1,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
@@ -1,9 +1,18 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`components/emoji_picker/emoji_picker.ios should match snapshot 1`] = `
|
||||
<Connect(SafeAreaIos)
|
||||
excludeFooter={true}
|
||||
excludeHeader={true}
|
||||
<RNCSafeAreaView
|
||||
edges={
|
||||
Array [
|
||||
"left",
|
||||
"right",
|
||||
]
|
||||
}
|
||||
style={
|
||||
Object {
|
||||
"flex": 1,
|
||||
}
|
||||
}
|
||||
>
|
||||
<KeyboardAvoidingView
|
||||
behavior="padding"
|
||||
@@ -24,48 +33,46 @@ exports[`components/emoji_picker/emoji_picker.ios should match snapshot 1`] = `
|
||||
"paddingVertical": 5,
|
||||
}
|
||||
}
|
||||
testID="emoji_picker"
|
||||
>
|
||||
<View
|
||||
style={null}
|
||||
>
|
||||
<Search
|
||||
autoCapitalize="none"
|
||||
backArrowSize={24}
|
||||
backgroundColor="transparent"
|
||||
blurOnSubmit={false}
|
||||
cancelTitle="Cancel"
|
||||
containerHeight={40}
|
||||
deleteIconSize={20}
|
||||
editable={true}
|
||||
inputHeight={33}
|
||||
inputStyle={
|
||||
Object {
|
||||
"backgroundColor": "#ffffff",
|
||||
"color": "#3d3c40",
|
||||
"fontSize": 13,
|
||||
}
|
||||
<Search
|
||||
autoCapitalize="none"
|
||||
backArrowSize={24}
|
||||
backgroundColor="transparent"
|
||||
blurOnSubmit={false}
|
||||
cancelTitle="Cancel"
|
||||
containerHeight={40}
|
||||
deleteIconSize={20}
|
||||
editable={true}
|
||||
inputHeight={33}
|
||||
inputStyle={
|
||||
Object {
|
||||
"backgroundColor": "#ffffff",
|
||||
"color": "#3d3c40",
|
||||
"fontSize": 13,
|
||||
}
|
||||
keyboardAppearance="light"
|
||||
keyboardShouldPersist={false}
|
||||
keyboardType="default"
|
||||
onAnimationComplete={[Function]}
|
||||
onBlur={[Function]}
|
||||
onCancelButtonPress={[Function]}
|
||||
onChangeText={[Function]}
|
||||
onSelectionChange={[Function]}
|
||||
placeholder="Search"
|
||||
placeholderTextColor="rgba(61,60,64,0.5)"
|
||||
returnKeyType="search"
|
||||
searchBarRightMargin={0}
|
||||
searchIconSize={24}
|
||||
showArrow={false}
|
||||
showCancel={true}
|
||||
tintColorDelete="rgba(61,60,64,0.5)"
|
||||
tintColorSearch="rgba(61,60,64,0.8)"
|
||||
titleCancelColor="#3d3c40"
|
||||
value=""
|
||||
/>
|
||||
</View>
|
||||
}
|
||||
keyboardAppearance="light"
|
||||
keyboardShouldPersist={false}
|
||||
keyboardType="default"
|
||||
onAnimationComplete={[Function]}
|
||||
onBlur={[Function]}
|
||||
onCancelButtonPress={[Function]}
|
||||
onChangeText={[Function]}
|
||||
onSelectionChange={[Function]}
|
||||
placeholder="Search"
|
||||
placeholderTextColor="rgba(61,60,64,0.5)"
|
||||
returnKeyType="search"
|
||||
searchBarRightMargin={0}
|
||||
searchIconSize={24}
|
||||
showArrow={false}
|
||||
showCancel={true}
|
||||
testID="emoji_picker.search_bar"
|
||||
tintColorDelete="rgba(61,60,64,0.5)"
|
||||
tintColorSearch="rgba(61,60,64,0.8)"
|
||||
titleCancelColor="#3d3c40"
|
||||
value=""
|
||||
/>
|
||||
</View>
|
||||
<View
|
||||
style={
|
||||
@@ -10220,5 +10227,5 @@ exports[`components/emoji_picker/emoji_picker.ios should match snapshot 1`] = `
|
||||
</KeyboardTrackingView>
|
||||
</View>
|
||||
</KeyboardAvoidingView>
|
||||
</Connect(SafeAreaIos)>
|
||||
</RNCSafeAreaView>
|
||||
`;
|
||||
|
||||
@@ -12,8 +12,9 @@ import EmojiPickerBase, {getStyleSheetFromTheme} from './emoji_picker_base';
|
||||
export default class EmojiPicker extends EmojiPickerBase {
|
||||
render() {
|
||||
const {formatMessage} = this.context.intl;
|
||||
const {theme} = this.props;
|
||||
const {testID, theme} = this.props;
|
||||
const {searchTerm} = this.state;
|
||||
const searchBarTestID = `${testID}.search_bar`;
|
||||
const styles = getStyleSheetFromTheme(theme);
|
||||
|
||||
const searchBarInput = {
|
||||
@@ -24,8 +25,12 @@ export default class EmojiPicker extends EmojiPickerBase {
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
<View style={styles.searchBar}>
|
||||
<View
|
||||
testID={testID}
|
||||
style={styles.searchBar}
|
||||
>
|
||||
<SearchBar
|
||||
testID={searchBarTestID}
|
||||
ref={this.setSearchBarRef}
|
||||
placeholder={formatMessage({id: 'search_bar.search', defaultMessage: 'Search'})}
|
||||
cancelTitle={formatMessage({id: 'mobile.post.cancel', defaultMessage: 'Cancel'})}
|
||||
|
||||
@@ -7,20 +7,20 @@ import {
|
||||
View,
|
||||
} from 'react-native';
|
||||
import {KeyboardTrackingView} from 'react-native-keyboard-tracking-view';
|
||||
import {SafeAreaView} from 'react-native-safe-area-context';
|
||||
|
||||
import SafeAreaView from 'app/components/safe_area_view';
|
||||
import {paddingHorizontal as padding} from 'app/components/safe_area_view/iphone_x_spacing';
|
||||
import SearchBar from 'app/components/search_bar';
|
||||
import {DeviceTypes} from 'app/constants';
|
||||
import {changeOpacity, getKeyboardAppearanceFromTheme} from 'app/utils/theme';
|
||||
import SearchBar from '@components/search_bar';
|
||||
import {DeviceTypes} from '@constants';
|
||||
import {changeOpacity, getKeyboardAppearanceFromTheme} from '@utils/theme';
|
||||
|
||||
import EmojiPickerBase, {getStyleSheetFromTheme, SCROLLVIEW_NATIVE_ID} from './emoji_picker_base';
|
||||
|
||||
export default class EmojiPicker extends EmojiPickerBase {
|
||||
render() {
|
||||
const {formatMessage} = this.context.intl;
|
||||
const {isLandscape, theme} = this.props;
|
||||
const {testID, isLandscape, theme} = this.props;
|
||||
const {searchTerm} = this.state;
|
||||
const searchBarTestID = `${testID}.search_bar`;
|
||||
const styles = getStyleSheetFromTheme(theme);
|
||||
|
||||
const shorten = DeviceTypes.IS_IPHONE_WITH_INSETS && isLandscape ? 6 : 2;
|
||||
@@ -38,8 +38,8 @@ export default class EmojiPicker extends EmojiPickerBase {
|
||||
|
||||
return (
|
||||
<SafeAreaView
|
||||
excludeHeader={true}
|
||||
excludeFooter={true}
|
||||
style={{flex: 1}}
|
||||
edges={['left', 'right']}
|
||||
>
|
||||
<KeyboardAvoidingView
|
||||
behavior='padding'
|
||||
@@ -47,27 +47,29 @@ export default class EmojiPicker extends EmojiPickerBase {
|
||||
keyboardVerticalOffset={keyboardOffset}
|
||||
style={styles.flex}
|
||||
>
|
||||
<View style={styles.searchBar}>
|
||||
<View style={padding(isLandscape)}>
|
||||
<SearchBar
|
||||
ref={this.setSearchBarRef}
|
||||
placeholder={formatMessage({id: 'search_bar.search', defaultMessage: 'Search'})}
|
||||
cancelTitle={formatMessage({id: 'mobile.post.cancel', defaultMessage: 'Cancel'})}
|
||||
backgroundColor='transparent'
|
||||
inputHeight={33}
|
||||
inputStyle={searchBarInput}
|
||||
placeholderTextColor={changeOpacity(theme.centerChannelColor, 0.5)}
|
||||
tintColorSearch={changeOpacity(theme.centerChannelColor, 0.8)}
|
||||
tintColorDelete={changeOpacity(theme.centerChannelColor, 0.5)}
|
||||
titleCancelColor={theme.centerChannelColor}
|
||||
onChangeText={this.changeSearchTerm}
|
||||
onCancelButtonPress={this.cancelSearch}
|
||||
autoCapitalize='none'
|
||||
value={searchTerm}
|
||||
keyboardAppearance={getKeyboardAppearanceFromTheme(theme)}
|
||||
onAnimationComplete={this.setRebuiltEmojis}
|
||||
/>
|
||||
</View>
|
||||
<View
|
||||
testID={testID}
|
||||
style={styles.searchBar}
|
||||
>
|
||||
<SearchBar
|
||||
testID={searchBarTestID}
|
||||
ref={this.setSearchBarRef}
|
||||
placeholder={formatMessage({id: 'search_bar.search', defaultMessage: 'Search'})}
|
||||
cancelTitle={formatMessage({id: 'mobile.post.cancel', defaultMessage: 'Cancel'})}
|
||||
backgroundColor='transparent'
|
||||
inputHeight={33}
|
||||
inputStyle={searchBarInput}
|
||||
placeholderTextColor={changeOpacity(theme.centerChannelColor, 0.5)}
|
||||
tintColorSearch={changeOpacity(theme.centerChannelColor, 0.8)}
|
||||
tintColorDelete={changeOpacity(theme.centerChannelColor, 0.5)}
|
||||
titleCancelColor={theme.centerChannelColor}
|
||||
onChangeText={this.changeSearchTerm}
|
||||
onCancelButtonPress={this.cancelSearch}
|
||||
autoCapitalize='none'
|
||||
value={searchTerm}
|
||||
keyboardAppearance={getKeyboardAppearanceFromTheme(theme)}
|
||||
onAnimationComplete={this.setRebuiltEmojis}
|
||||
/>
|
||||
</View>
|
||||
<View style={[styles.container]}>
|
||||
{this.renderListComponent(shorten)}
|
||||
|
||||
@@ -34,6 +34,7 @@ describe('components/emoji_picker/emoji_picker.ios', () => {
|
||||
const fuse = new Fuse(emojis, options);
|
||||
|
||||
const baseProps = {
|
||||
testID: 'emoji_picker',
|
||||
actions: {
|
||||
getCustomEmojis: jest.fn(),
|
||||
incrementEmojiPickerPage: jest.fn(),
|
||||
@@ -80,18 +81,6 @@ describe('components/emoji_picker/emoji_picker.ios', () => {
|
||||
expect(result).toEqual(output);
|
||||
});
|
||||
|
||||
test('should set rebuildEmojis to true when deviceWidth changes', async () => {
|
||||
const wrapper = shallowWithIntl(<EmojiPicker {...baseProps}/>);
|
||||
const instance = wrapper.instance();
|
||||
|
||||
expect(instance.rebuildEmojis).toBe(undefined);
|
||||
|
||||
const newDeviceWidth = baseProps.deviceWidth * 2;
|
||||
wrapper.setProps({deviceWidth: newDeviceWidth});
|
||||
|
||||
expect(instance.rebuildEmojis).toBe(true);
|
||||
});
|
||||
|
||||
test('should rebuild emojis emojis when emojis change', async () => {
|
||||
const wrapper = shallowWithIntl(<EmojiPicker {...baseProps}/>);
|
||||
const instance = wrapper.instance();
|
||||
|
||||
@@ -19,7 +19,6 @@ import sectionListGetItemLayout from 'react-native-section-list-get-item-layout'
|
||||
import CompassIcon from '@components/compass_icon';
|
||||
import Emoji from '@components/emoji';
|
||||
import FormattedText from '@components/formatted_text';
|
||||
import {paddingHorizontal as padding} from '@components/safe_area_view/iphone_x_spacing';
|
||||
import {DeviceTypes} from '@constants';
|
||||
import {emptyFunction} from '@utils/general';
|
||||
import {
|
||||
@@ -42,6 +41,7 @@ export function filterEmojiSearchInput(searchText) {
|
||||
|
||||
export default class EmojiPicker extends PureComponent {
|
||||
static propTypes = {
|
||||
testID: PropTypes.string,
|
||||
customEmojisEnabled: PropTypes.bool.isRequired,
|
||||
customEmojiPage: PropTypes.number.isRequired,
|
||||
deviceWidth: PropTypes.number.isRequired,
|
||||
@@ -101,8 +101,9 @@ export default class EmojiPicker extends PureComponent {
|
||||
|
||||
if (this.props.emojis !== prevProps.emojis) {
|
||||
this.rebuildEmojis = true;
|
||||
this.setRebuiltEmojis();
|
||||
}
|
||||
|
||||
this.setRebuiltEmojis();
|
||||
}
|
||||
|
||||
setSearchBarRef = (ref) => {
|
||||
@@ -231,15 +232,17 @@ export default class EmojiPicker extends PureComponent {
|
||||
return Math.floor(Number(((deviceWidth - (SECTION_MARGIN * shorten)) / ((EMOJI_SIZE + 7) + (EMOJI_GUTTER * shorten)))));
|
||||
};
|
||||
|
||||
renderItem = ({item}) => {
|
||||
renderItem = ({item, section}) => {
|
||||
return (
|
||||
<EmojiPickerRow
|
||||
key={item.key}
|
||||
emojiGutter={EMOJI_GUTTER}
|
||||
emojiSize={EMOJI_SIZE}
|
||||
items={item.items}
|
||||
onEmojiPress={this.props.onEmojiPress}
|
||||
/>
|
||||
<View testID={section.defaultMessage}>
|
||||
<EmojiPickerRow
|
||||
key={item.key}
|
||||
emojiGutter={EMOJI_GUTTER}
|
||||
emojiSize={EMOJI_SIZE}
|
||||
items={item.items}
|
||||
onEmojiPress={this.props.onEmojiPress}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -305,7 +308,7 @@ export default class EmojiPicker extends PureComponent {
|
||||
onPress={() => this.props.onEmojiPress(item)}
|
||||
style={style.flatListRow}
|
||||
>
|
||||
<View style={[style.flatListEmoji, padding(this.props.isLandscape)]}>
|
||||
<View style={style.flatListEmoji}>
|
||||
<Emoji
|
||||
emojiName={item}
|
||||
textStyle={style.emojiText}
|
||||
|
||||
@@ -9,7 +9,7 @@ import {incrementEmojiPickerPage} from '@actions/views/emoji';
|
||||
import {getCustomEmojis} from '@mm-redux/actions/emojis';
|
||||
import {getTheme} from '@mm-redux/selectors/entities/preferences';
|
||||
import {getConfig} from '@mm-redux/selectors/entities/general';
|
||||
import {getDimensions, isLandscape} from '@selectors/device';
|
||||
import {isLandscape} from '@selectors/device';
|
||||
import {selectEmojisByName, selectEmojisBySection} from '@selectors/emojis';
|
||||
|
||||
import EmojiPicker from './emoji_picker';
|
||||
@@ -17,7 +17,6 @@ import EmojiPicker from './emoji_picker';
|
||||
function mapStateToProps(state) {
|
||||
const emojisBySection = selectEmojisBySection(state);
|
||||
const emojis = selectEmojisByName(state);
|
||||
const {deviceWidth} = getDimensions(state);
|
||||
const options = {
|
||||
shouldSort: false,
|
||||
threshold: 0.3,
|
||||
@@ -34,7 +33,6 @@ function mapStateToProps(state) {
|
||||
fuse,
|
||||
emojis,
|
||||
emojisBySection,
|
||||
deviceWidth,
|
||||
isLandscape: isLandscape(state),
|
||||
theme: getTheme(state),
|
||||
customEmojisEnabled: getConfig(state).EnableCustomEmoji === 'true',
|
||||
|
||||
@@ -12,13 +12,14 @@ import {makeStyleSheetFromTheme} from 'app/utils/theme';
|
||||
|
||||
export default class ErrorText extends PureComponent {
|
||||
static propTypes = {
|
||||
testID: PropTypes.string,
|
||||
error: PropTypes.oneOfType([PropTypes.string, PropTypes.object]),
|
||||
textStyle: CustomPropTypes.Style,
|
||||
theme: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
render() {
|
||||
const {error, textStyle, theme} = this.props;
|
||||
const {testID, error, textStyle, theme} = this.props;
|
||||
if (!error) {
|
||||
return null;
|
||||
}
|
||||
@@ -29,7 +30,7 @@ export default class ErrorText extends PureComponent {
|
||||
if (intl) {
|
||||
return (
|
||||
<FormattedText
|
||||
testID='error.text'
|
||||
testID={testID}
|
||||
id={intl.id}
|
||||
defaultMessage={intl.defaultMessage}
|
||||
values={intl.values}
|
||||
@@ -40,7 +41,7 @@ export default class ErrorText extends PureComponent {
|
||||
|
||||
return (
|
||||
<Text
|
||||
testID='error.text'
|
||||
testID={testID}
|
||||
style={[GlobalStyles.errorLabel, style.errorLabel, textStyle]}
|
||||
>
|
||||
{error.message || error}
|
||||
|
||||
@@ -9,6 +9,7 @@ import ErrorText from './error_text.js';
|
||||
|
||||
describe('ErrorText', () => {
|
||||
const baseProps = {
|
||||
testID: 'error.text',
|
||||
textStyle: {
|
||||
fontSize: 14,
|
||||
marginHorizontal: 15,
|
||||
|
||||
@@ -52,4 +52,4 @@ export default class ImageViewPort extends PureComponent {
|
||||
render() {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -160,13 +160,24 @@ export default class Markdown extends PureComponent {
|
||||
renderText = ({context, literal}) => {
|
||||
if (context.indexOf('image') !== -1) {
|
||||
// If this text is displayed, it will be styled by the image component
|
||||
return <Text>{literal}</Text>;
|
||||
return (
|
||||
<Text testID='markdown_text'>
|
||||
{literal}
|
||||
</Text>
|
||||
);
|
||||
}
|
||||
|
||||
// Construct the text style based off of the parents of this node since RN's inheritance is limited
|
||||
const style = this.computeTextStyle(this.props.baseTextStyle, context);
|
||||
|
||||
return <Text style={style}>{literal}</Text>;
|
||||
return (
|
||||
<Text
|
||||
testID='markdown_text'
|
||||
style={style}
|
||||
>
|
||||
{literal}
|
||||
</Text>
|
||||
);
|
||||
};
|
||||
|
||||
renderCodeSpan = ({context, literal}) => {
|
||||
|
||||
@@ -6,7 +6,6 @@ import React from 'react';
|
||||
import {intlShape} from 'react-intl';
|
||||
import {
|
||||
Alert,
|
||||
Linking,
|
||||
Platform,
|
||||
StyleSheet,
|
||||
Text,
|
||||
@@ -25,7 +24,7 @@ import EphemeralStore from '@store/ephemeral_store';
|
||||
import BottomSheet from '@utils/bottom_sheet';
|
||||
import {generateId} from '@utils/file';
|
||||
import {calculateDimensions, getViewPortWidth, isGifTooLarge, openGalleryAtIndex} from '@utils/images';
|
||||
import {normalizeProtocol} from '@utils/url';
|
||||
import {normalizeProtocol, tryOpenURL} from '@utils/url';
|
||||
|
||||
import mattermostManaged from 'app/mattermost_managed';
|
||||
|
||||
@@ -126,7 +125,7 @@ export default class MarkdownImage extends ImageViewPort {
|
||||
const url = normalizeProtocol(this.props.linkDestination);
|
||||
const {intl} = this.context;
|
||||
|
||||
Linking.openURL(url).catch(() => {
|
||||
const onError = () => {
|
||||
Alert.alert(
|
||||
intl.formatMessage({
|
||||
id: 'mobile.link.error.title',
|
||||
@@ -137,7 +136,9 @@ export default class MarkdownImage extends ImageViewPort {
|
||||
defaultMessage: 'Unable to open the link.',
|
||||
}),
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
tryOpenURL(url, onError);
|
||||
};
|
||||
|
||||
handleLinkLongPress = async () => {
|
||||
|
||||
@@ -3,20 +3,19 @@
|
||||
|
||||
import React, {Children, PureComponent} from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {Alert, Linking, Text} from 'react-native';
|
||||
import {Alert, Text} from 'react-native';
|
||||
import Clipboard from '@react-native-community/clipboard';
|
||||
import urlParse from 'url-parse';
|
||||
import {intlShape} from 'react-intl';
|
||||
import urlParse from 'url-parse';
|
||||
|
||||
import Config from '@assets/config';
|
||||
import {DeepLinkTypes} from '@constants';
|
||||
import CustomPropTypes from '@constants/custom_prop_types';
|
||||
import {getCurrentServerUrl} from '@init/credentials';
|
||||
import BottomSheet from '@utils/bottom_sheet';
|
||||
import {alertErrorWithFallback} from '@utils/general';
|
||||
import {t} from '@utils/i18n';
|
||||
import {errorBadChannel} from '@utils/draft';
|
||||
import {preventDoubleTap} from '@utils/tap';
|
||||
import {matchDeepLink, normalizeProtocol} from '@utils/url';
|
||||
import {matchDeepLink, normalizeProtocol, tryOpenURL} from '@utils/url';
|
||||
|
||||
import mattermostManaged from 'app/mattermost_managed';
|
||||
|
||||
@@ -59,37 +58,30 @@ export default class MarkdownLink extends PureComponent {
|
||||
|
||||
if (match) {
|
||||
if (match.type === DeepLinkTypes.CHANNEL) {
|
||||
this.props.actions.handleSelectChannelByName(match.channelName, match.teamName, this.errorBadChannel);
|
||||
const {intl} = this.context;
|
||||
this.props.actions.handleSelectChannelByName(match.channelName, match.teamName, errorBadChannel.bind(null, intl));
|
||||
} else if (match.type === DeepLinkTypes.PERMALINK) {
|
||||
onPermalinkPress(match.postId, match.teamName);
|
||||
}
|
||||
} else {
|
||||
Linking.openURL(url).catch(() => {
|
||||
const onError = () => {
|
||||
const {formatMessage} = this.context.intl;
|
||||
Alert.alert(
|
||||
formatMessage({
|
||||
id: 'mobile.server_link.error.title',
|
||||
defaultMessage: 'Link Error',
|
||||
id: 'mobile.link.error.title',
|
||||
defaultMessage: 'Error',
|
||||
}),
|
||||
formatMessage({
|
||||
id: 'mobile.server_link.error.text',
|
||||
defaultMessage: 'The link could not be found on this server.',
|
||||
id: 'mobile.link.error.text',
|
||||
defaultMessage: 'Unable to open the link.',
|
||||
}),
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
tryOpenURL(url, onError);
|
||||
}
|
||||
});
|
||||
|
||||
errorBadChannel = () => {
|
||||
const {intl} = this.context;
|
||||
const message = {
|
||||
id: t('mobile.server_link.unreachable_channel.error'),
|
||||
defaultMessage: 'This link belongs to a deleted channel or to a channel to which you do not have access.',
|
||||
};
|
||||
|
||||
alertErrorWithFallback(intl, {}, message);
|
||||
};
|
||||
|
||||
parseLinkLiteral = (literal) => {
|
||||
let nextLiteral = literal;
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user