forked from Ivasoft/mattermost-mobile
Compare commits
28 Commits
changeme
...
release-1.
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6efa04cd19 | ||
|
|
725225d77e | ||
|
|
a31f2acd78 | ||
|
|
1c4aeece20 | ||
|
|
de0e7ca142 | ||
|
|
d43f619f40 | ||
|
|
d5dfa05cdb | ||
|
|
cc5ff8143b | ||
|
|
f1b614c386 | ||
|
|
64b0d1602b | ||
|
|
dc70abaf89 | ||
|
|
29a12c152a | ||
|
|
20496f5a4f | ||
|
|
0b7d16c7e4 | ||
|
|
550498bfc0 | ||
|
|
37fb33c6a7 | ||
|
|
976d2b5fe3 | ||
|
|
84918a666d | ||
|
|
39fb6af758 | ||
|
|
169866e0db | ||
|
|
fcf33ccc46 | ||
|
|
a074289c3f | ||
|
|
0ab0c9b85b | ||
|
|
8899c586ab | ||
|
|
f5e89c8eab | ||
|
|
6b8008f080 | ||
|
|
05aa56b2e1 | ||
|
|
b5b2310f33 |
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.
|
||||
|
||||
@@ -132,8 +132,8 @@ android {
|
||||
minSdkVersion rootProject.ext.minSdkVersion
|
||||
targetSdkVersion rootProject.ext.targetSdkVersion
|
||||
missingDimensionStrategy "RNNotifications.reactNativeVersion", "reactNative60"
|
||||
versionCode 334
|
||||
versionName "1.37.0"
|
||||
versionCode 338
|
||||
versionName "1.38.1"
|
||||
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'
|
||||
|
||||
@@ -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() || {};
|
||||
|
||||
@@ -293,6 +293,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};
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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};
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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,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,
|
||||
@@ -213,7 +212,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,
|
||||
};
|
||||
|
||||
@@ -191,7 +190,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),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
@@ -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),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -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),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -117,7 +117,11 @@ export default class ClientUpgradeListener extends PureComponent {
|
||||
const {downloadLink} = this.props;
|
||||
const {intl} = this.context;
|
||||
|
||||
Linking.openURL(downloadLink).catch(() => {
|
||||
Linking.canOpenURL(downloadLink).then((supported) => {
|
||||
if (supported) {
|
||||
return Linking.openURL(downloadLink);
|
||||
}
|
||||
|
||||
Alert.alert(
|
||||
intl.formatMessage({
|
||||
id: 'mobile.client_upgrade.download_error.title',
|
||||
@@ -128,6 +132,8 @@ export default class ClientUpgradeListener extends PureComponent {
|
||||
defaultMessage: 'An error occurred while trying to open the download link.',
|
||||
}),
|
||||
);
|
||||
|
||||
return false;
|
||||
});
|
||||
|
||||
this.toggleUpgradeMessage(false);
|
||||
|
||||
@@ -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', () => {
|
||||
|
||||
@@ -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}
|
||||
@@ -23,16 +36,11 @@ exports[`EditChannelInfo should match snapshot 1`] = `
|
||||
>
|
||||
<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 +49,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
|
||||
@@ -98,15 +96,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 +127,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
|
||||
@@ -184,17 +172,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 +185,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 +216,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
|
||||
@@ -295,17 +268,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 +310,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,7 +7,7 @@ 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 = {
|
||||
@@ -32,7 +32,6 @@ describe('EditChannelInfo', () => {
|
||||
oldChannelURL: '/team_a/channels/channel_old',
|
||||
oldHeader: 'old_header',
|
||||
oldPurpose: 'old_purpose',
|
||||
isLandscape: true,
|
||||
};
|
||||
|
||||
test('should match snapshot', () => {
|
||||
|
||||
@@ -1,15 +1,453 @@
|
||||
// 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 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.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.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.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"
|
||||
@@ -25,47 +34,43 @@ exports[`components/emoji_picker/emoji_picker.ios should match snapshot 1`] = `
|
||||
}
|
||||
}
|
||||
>
|
||||
<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}
|
||||
tintColorDelete="rgba(61,60,64,0.5)"
|
||||
tintColorSearch="rgba(61,60,64,0.8)"
|
||||
titleCancelColor="#3d3c40"
|
||||
value=""
|
||||
/>
|
||||
</View>
|
||||
<View
|
||||
style={
|
||||
@@ -10220,5 +10225,5 @@ exports[`components/emoji_picker/emoji_picker.ios should match snapshot 1`] = `
|
||||
</KeyboardTrackingView>
|
||||
</View>
|
||||
</KeyboardAvoidingView>
|
||||
</Connect(SafeAreaIos)>
|
||||
</RNCSafeAreaView>
|
||||
`;
|
||||
|
||||
@@ -7,12 +7,11 @@ 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';
|
||||
|
||||
@@ -38,8 +37,8 @@ export default class EmojiPicker extends EmojiPickerBase {
|
||||
|
||||
return (
|
||||
<SafeAreaView
|
||||
excludeHeader={true}
|
||||
excludeFooter={true}
|
||||
style={{flex: 1}}
|
||||
edges={['left', 'right']}
|
||||
>
|
||||
<KeyboardAvoidingView
|
||||
behavior='padding'
|
||||
@@ -48,26 +47,24 @@ export default class EmojiPicker extends EmojiPickerBase {
|
||||
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>
|
||||
<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 style={[styles.container]}>
|
||||
{this.renderListComponent(shorten)}
|
||||
|
||||
@@ -80,18 +80,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 {
|
||||
@@ -101,8 +100,9 @@ export default class EmojiPicker extends PureComponent {
|
||||
|
||||
if (this.props.emojis !== prevProps.emojis) {
|
||||
this.rebuildEmojis = true;
|
||||
this.setRebuiltEmojis();
|
||||
}
|
||||
|
||||
this.setRebuiltEmojis();
|
||||
}
|
||||
|
||||
setSearchBarRef = (ref) => {
|
||||
@@ -305,7 +305,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',
|
||||
|
||||
@@ -5,7 +5,6 @@ import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import {intlShape} from 'react-intl';
|
||||
import {
|
||||
Alert,
|
||||
Linking,
|
||||
Platform,
|
||||
StyleSheet,
|
||||
@@ -124,19 +123,11 @@ export default class MarkdownImage extends ImageViewPort {
|
||||
|
||||
handleLinkPress = () => {
|
||||
const url = normalizeProtocol(this.props.linkDestination);
|
||||
const {intl} = this.context;
|
||||
|
||||
Linking.openURL(url).catch(() => {
|
||||
Alert.alert(
|
||||
intl.formatMessage({
|
||||
id: 'mobile.link.error.title',
|
||||
defaultMessage: 'Error',
|
||||
}),
|
||||
intl.formatMessage({
|
||||
id: 'mobile.link.error.text',
|
||||
defaultMessage: 'Unable to open the link.',
|
||||
}),
|
||||
);
|
||||
Linking.canOpenURL(url).then((supported) => {
|
||||
if (supported) {
|
||||
Linking.openURL(url);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -64,18 +64,22 @@ export default class MarkdownLink extends PureComponent {
|
||||
onPermalinkPress(match.postId, match.teamName);
|
||||
}
|
||||
} else {
|
||||
Linking.openURL(url).catch(() => {
|
||||
const {formatMessage} = this.context.intl;
|
||||
Alert.alert(
|
||||
formatMessage({
|
||||
id: 'mobile.server_link.error.title',
|
||||
defaultMessage: 'Link Error',
|
||||
}),
|
||||
formatMessage({
|
||||
id: 'mobile.server_link.error.text',
|
||||
defaultMessage: 'The link could not be found on this server.',
|
||||
}),
|
||||
);
|
||||
Linking.canOpenURL(url).then((supported) => {
|
||||
if (supported) {
|
||||
Linking.openURL(url);
|
||||
} else {
|
||||
const {formatMessage} = this.context.intl;
|
||||
Alert.alert(
|
||||
formatMessage({
|
||||
id: 'mobile.server_link.error.title',
|
||||
defaultMessage: 'Link Error',
|
||||
}),
|
||||
formatMessage({
|
||||
id: 'mobile.server_link.error.text',
|
||||
defaultMessage: 'The link could not be found on this server.',
|
||||
}),
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
@@ -2,10 +2,9 @@
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React, {PureComponent} from 'react';
|
||||
import {Alert, Linking, Text, View} from 'react-native';
|
||||
import {Linking, Text, View} from 'react-native';
|
||||
import FastImage from 'react-native-fast-image';
|
||||
import PropTypes from 'prop-types';
|
||||
import {intlShape} from 'react-intl';
|
||||
|
||||
import {changeOpacity, makeStyleSheetFromTheme} from 'app/utils/theme';
|
||||
|
||||
@@ -17,27 +16,10 @@ export default class AttachmentAuthor extends PureComponent {
|
||||
theme: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
static contextTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
};
|
||||
|
||||
openLink = () => {
|
||||
const {link} = this.props;
|
||||
const {intl} = this.context;
|
||||
|
||||
if (link) {
|
||||
Linking.openURL(link).catch(() => {
|
||||
Alert.alert(
|
||||
intl.formatMessage({
|
||||
id: 'mobile.link.error.title',
|
||||
defaultMessage: 'Error',
|
||||
}),
|
||||
intl.formatMessage({
|
||||
id: 'mobile.link.error.text',
|
||||
defaultMessage: 'Unable to open the link.',
|
||||
}),
|
||||
);
|
||||
});
|
||||
if (link && Linking.canOpenURL(link)) {
|
||||
Linking.openURL(link);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -2,9 +2,8 @@
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React, {PureComponent} from 'react';
|
||||
import {Alert, Linking, Text, View} from 'react-native';
|
||||
import {Linking, Text, View} from 'react-native';
|
||||
import PropTypes from 'prop-types';
|
||||
import {intlShape} from 'react-intl';
|
||||
|
||||
import {makeStyleSheetFromTheme} from 'app/utils/theme';
|
||||
import Markdown from 'app/components/markdown';
|
||||
@@ -16,27 +15,10 @@ export default class AttachmentTitle extends PureComponent {
|
||||
value: PropTypes.string,
|
||||
};
|
||||
|
||||
static contextTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
};
|
||||
|
||||
openLink = () => {
|
||||
const {link} = this.props;
|
||||
const {intl} = this.context;
|
||||
|
||||
if (link) {
|
||||
Linking.openURL(link).catch(() => {
|
||||
Alert.alert(
|
||||
intl.formatMessage({
|
||||
id: 'mobile.link.error.title',
|
||||
defaultMessage: 'Error',
|
||||
}),
|
||||
intl.formatMessage({
|
||||
id: 'mobile.link.error.text',
|
||||
defaultMessage: 'Unable to open the link.',
|
||||
}),
|
||||
);
|
||||
});
|
||||
if (link && Linking.canOpenURL(link)) {
|
||||
Linking.openURL(link);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -20,6 +20,12 @@ exports[`AttachmentFooter matches snapshot 1`] = `
|
||||
}
|
||||
>
|
||||
<ForwardRef(AnimatedComponentWrapper)
|
||||
edges={
|
||||
Array [
|
||||
"left",
|
||||
"right",
|
||||
]
|
||||
}
|
||||
style={
|
||||
Object {
|
||||
"alignItems": "center",
|
||||
|
||||
@@ -12,19 +12,20 @@ import {
|
||||
View,
|
||||
} from 'react-native';
|
||||
import NetInfo from '@react-native-community/netinfo';
|
||||
import {SafeAreaView} from 'react-native-safe-area-context';
|
||||
|
||||
import {RequestStatus} from '@mm-redux/constants';
|
||||
import EventEmitter from '@mm-redux/utils/event_emitter';
|
||||
|
||||
import CompassIcon from '@components/compass_icon';
|
||||
import FormattedText from '@components/formatted_text';
|
||||
import {DeviceTypes, ViewTypes} from '@constants';
|
||||
import {ViewTypes} from '@constants';
|
||||
import {INDICATOR_BAR_HEIGHT} from '@constants/view';
|
||||
import networkConnectionListener, {checkConnection} from '@utils/network';
|
||||
import {t} from '@utils/i18n';
|
||||
|
||||
import mattermostBucket from 'app/mattermost_bucket';
|
||||
import PushNotifications from 'app/push_notifications';
|
||||
import PushNotifications from '@init/push_notifications';
|
||||
|
||||
const MAX_WEBSOCKET_RETRIES = 3;
|
||||
const CONNECTION_RETRY_SECONDS = 5;
|
||||
@@ -33,10 +34,11 @@ const {
|
||||
ANDROID_TOP_LANDSCAPE,
|
||||
ANDROID_TOP_PORTRAIT,
|
||||
IOS_TOP_LANDSCAPE,
|
||||
IOS_TOP_PORTRAIT,
|
||||
IOS_INSETS_TOP_PORTRAIT,
|
||||
} = ViewTypes;
|
||||
|
||||
const AnimatedSafeAreaView = Animated.createAnimatedComponent(SafeAreaView);
|
||||
|
||||
export default class NetworkIndicator extends PureComponent {
|
||||
static propTypes = {
|
||||
actions: PropTypes.shape({
|
||||
@@ -64,12 +66,17 @@ export default class NetworkIndicator extends PureComponent {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
const navBarHeight = Platform.select({
|
||||
android: props.isLandscape ? ANDROID_TOP_LANDSCAPE : ANDROID_TOP_PORTRAIT,
|
||||
ios: props.isLandscape ? IOS_TOP_LANDSCAPE : IOS_INSETS_TOP_PORTRAIT,
|
||||
});
|
||||
|
||||
this.state = {
|
||||
opacity: 0,
|
||||
navBarHeight,
|
||||
};
|
||||
|
||||
const navBar = this.getNavBarHeight(props.isLandscape);
|
||||
this.top = new Animated.Value(navBar - INDICATOR_BAR_HEIGHT);
|
||||
this.top = new Animated.Value(navBarHeight - INDICATOR_BAR_HEIGHT);
|
||||
this.clearNotificationTimeout = null;
|
||||
|
||||
this.backgroundColor = new Animated.Value(0);
|
||||
@@ -83,29 +90,28 @@ export default class NetworkIndicator extends PureComponent {
|
||||
this.mounted = true;
|
||||
|
||||
AppState.addEventListener('change', this.handleAppStateChange);
|
||||
EventEmitter.on(ViewTypes.CHANNEL_NAV_BAR_CHANGED, this.getNavBarHeight);
|
||||
|
||||
// Attempt to connect when this component mounts
|
||||
// if the websocket is already connected it does not try and connect again
|
||||
this.connect(true);
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
componentDidUpdate(prevProps, prevState) {
|
||||
const {
|
||||
currentChannelId: prevChannelId,
|
||||
isLandscape: prevIsLandscape,
|
||||
websocketStatus: previousWebsocketStatus,
|
||||
} = prevProps;
|
||||
const {currentChannelId, isLandscape, websocketErrorCount, websocketStatus} = this.props;
|
||||
const {currentChannelId, websocketErrorCount, websocketStatus} = this.props;
|
||||
|
||||
if (currentChannelId !== prevChannelId && this.clearNotificationTimeout) {
|
||||
clearTimeout(this.clearNotificationTimeout);
|
||||
this.clearNotificationTimeout = null;
|
||||
}
|
||||
|
||||
if (isLandscape !== prevIsLandscape) {
|
||||
const navBar = this.getNavBarHeight(isLandscape);
|
||||
if (prevState.navBarHeight !== this.state.navBarHeight) {
|
||||
const initialTop = websocketErrorCount || previousWebsocketStatus === RequestStatus.FAILURE || previousWebsocketStatus === RequestStatus.NOT_STARTED ? 0 : INDICATOR_BAR_HEIGHT;
|
||||
this.top.setValue(navBar - initialTop);
|
||||
this.top.setValue(this.state.navBarHeight - initialTop);
|
||||
}
|
||||
|
||||
if (this.props.isOnline) {
|
||||
@@ -132,6 +138,7 @@ export default class NetworkIndicator extends PureComponent {
|
||||
stopPeriodicStatusUpdates();
|
||||
this.networkListener.removeEventListener();
|
||||
AppState.removeEventListener('change', this.handleAppStateChange);
|
||||
EventEmitter.off(ViewTypes.CHANNEL_NAV_BAR_CHANGED, this.getNavBarHeight);
|
||||
|
||||
clearTimeout(this.connectionRetryTimeout);
|
||||
this.connectionRetryTimeout = null;
|
||||
@@ -180,7 +187,7 @@ export default class NetworkIndicator extends PureComponent {
|
||||
),
|
||||
Animated.timing(
|
||||
this.top, {
|
||||
toValue: (this.getNavBarHeight() - INDICATOR_BAR_HEIGHT),
|
||||
toValue: (this.state.navBarHeight - INDICATOR_BAR_HEIGHT),
|
||||
duration: 300,
|
||||
delay: 500,
|
||||
useNativeDriver: false,
|
||||
@@ -196,26 +203,8 @@ export default class NetworkIndicator extends PureComponent {
|
||||
}
|
||||
};
|
||||
|
||||
getNavBarHeight = (isLandscape = this.props.isLandscape) => {
|
||||
if (Platform.OS === 'android') {
|
||||
if (isLandscape) {
|
||||
return ANDROID_TOP_LANDSCAPE;
|
||||
}
|
||||
|
||||
return ANDROID_TOP_PORTRAIT;
|
||||
}
|
||||
|
||||
const iPhoneWithInsets = DeviceTypes.IS_IPHONE_WITH_INSETS;
|
||||
|
||||
if (iPhoneWithInsets && isLandscape) {
|
||||
return IOS_TOP_LANDSCAPE;
|
||||
} else if (iPhoneWithInsets) {
|
||||
return IOS_INSETS_TOP_PORTRAIT;
|
||||
} else if (isLandscape && !DeviceTypes.IS_TABLET) {
|
||||
return IOS_TOP_LANDSCAPE;
|
||||
}
|
||||
|
||||
return IOS_TOP_PORTRAIT;
|
||||
getNavBarHeight = (navBarHeight) => {
|
||||
this.setState({navBarHeight});
|
||||
};
|
||||
|
||||
handleWebSocket = (open) => {
|
||||
@@ -329,7 +318,7 @@ export default class NetworkIndicator extends PureComponent {
|
||||
|
||||
Animated.timing(
|
||||
this.top, {
|
||||
toValue: this.getNavBarHeight(),
|
||||
toValue: this.state.navBarHeight,
|
||||
duration: 300,
|
||||
useNativeDriver: false,
|
||||
},
|
||||
@@ -391,14 +380,17 @@ export default class NetworkIndicator extends PureComponent {
|
||||
pointerEvents='none'
|
||||
style={[styles.container, {top: this.top, backgroundColor: background, opacity: this.state.opacity}]}
|
||||
>
|
||||
<Animated.View style={styles.wrapper}>
|
||||
<AnimatedSafeAreaView
|
||||
edges={['left', 'right']}
|
||||
style={styles.wrapper}
|
||||
>
|
||||
<FormattedText
|
||||
defaultMessage={defaultMessage}
|
||||
id={i18nId}
|
||||
style={styles.message}
|
||||
/>
|
||||
{action}
|
||||
</Animated.View>
|
||||
</AnimatedSafeAreaView>
|
||||
</Animated.View>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -14,7 +14,6 @@ import {isDateLine, isStartOfNewMessages} from '@mm-redux/utils/post_list';
|
||||
import {isPostFlagged, isSystemMessage} from '@mm-redux/utils/post_utils';
|
||||
|
||||
import {insertToDraft, setPostTooltipVisible} from 'app/actions/views/channel';
|
||||
import {isLandscape} from 'app/selectors/device';
|
||||
|
||||
import Post from './post';
|
||||
|
||||
@@ -86,7 +85,6 @@ function makeMapStateToProps() {
|
||||
theme: getTheme(state),
|
||||
isFlagged: isPostFlagged(post.id, myPreferences),
|
||||
isCommentMention,
|
||||
isLandscape: isLandscape(state),
|
||||
previousPostExists: Boolean(previousPost),
|
||||
beforePrevPostUserId: (beforePrevPost ? beforePrevPost.user_id : null),
|
||||
};
|
||||
|
||||
@@ -23,7 +23,6 @@ import PostHeader from '@components/post_header';
|
||||
import PostProfilePicture from '@components/post_profile_picture';
|
||||
import PostPreHeader from '@components/post_header/post_pre_header';
|
||||
import TouchableWithFeedback from '@components/touchable_with_feedback';
|
||||
import {paddingHorizontal as padding} from '@components/safe_area_view/iphone_x_spacing';
|
||||
|
||||
import {NavigationTypes} from '@constants';
|
||||
|
||||
@@ -70,7 +69,6 @@ export default class Post extends PureComponent {
|
||||
isCommentMention: PropTypes.bool,
|
||||
location: PropTypes.string,
|
||||
isBot: PropTypes.bool,
|
||||
isLandscape: PropTypes.bool.isRequired,
|
||||
previousPostExists: PropTypes.bool,
|
||||
beforePrevPostUserId: PropTypes.string,
|
||||
};
|
||||
@@ -266,7 +264,6 @@ export default class Post extends PureComponent {
|
||||
skipFlaggedHeader,
|
||||
skipPinnedHeader,
|
||||
location,
|
||||
isLandscape,
|
||||
previousPostExists,
|
||||
beforePrevPostUserId,
|
||||
} = this.props;
|
||||
@@ -327,7 +324,7 @@ export default class Post extends PureComponent {
|
||||
return (
|
||||
<View
|
||||
testID={testID}
|
||||
style={[style.postStyle, highlighted, padding(isLandscape)]}
|
||||
style={[style.postStyle, highlighted]}
|
||||
>
|
||||
<TouchableWithFeedback
|
||||
onPress={this.handlePress}
|
||||
|
||||
@@ -166,7 +166,8 @@ export default class PostBodyAdditionalContent extends ImageViewPort {
|
||||
imageUrl = link;
|
||||
} else if (isYoutubeLink(link)) {
|
||||
const videoId = getYouTubeVideoId(link);
|
||||
imageUrl = Object.keys(this.props.metadata.images)[0] || `https://i.ytimg.com/vi/${videoId}/hqdefault.jpg`;
|
||||
const images = Object.keys(this.props.metadata?.images || {});
|
||||
imageUrl = images[0] || `https://i.ytimg.com/vi/${videoId}/hqdefault.jpg`;
|
||||
}
|
||||
|
||||
return imageUrl;
|
||||
|
||||
@@ -7,6 +7,7 @@ import {Preferences} from '@mm-redux/constants';
|
||||
|
||||
import {shallowWithIntl} from 'test/intl-test-helper';
|
||||
|
||||
import * as Utils from '@utils/url';
|
||||
import PostBodyAdditionalContent from './post_body_additional_content.js';
|
||||
|
||||
describe('PostBodyAdditionalContent', () => {
|
||||
@@ -49,4 +50,56 @@ describe('PostBodyAdditionalContent', () => {
|
||||
instance.load();
|
||||
expect(baseProps.actions.getRedirectLocation).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
test('getImageUrl should return passed URL if content is an image', () => {
|
||||
const wrapper = shallowWithIntl(<PostBodyAdditionalContent {...baseProps}/>);
|
||||
const instance = wrapper.instance();
|
||||
instance.isImage = jest.fn().mockReturnValueOnce(true);
|
||||
|
||||
const url = 'https://test.url';
|
||||
const imageUrl = instance.getImageUrl(url);
|
||||
expect(imageUrl).toEqual(url);
|
||||
});
|
||||
|
||||
test('getImageUrl should return first metadata image URL if content is a YouTube link', () => {
|
||||
const url1 = 'https://test.url1';
|
||||
const url2 = 'https://test.url2';
|
||||
const props = {
|
||||
...baseProps,
|
||||
metadata: {
|
||||
images: {
|
||||
[url1]: 'URL 1',
|
||||
[url2]: 'URL 2',
|
||||
},
|
||||
},
|
||||
};
|
||||
const wrapper = shallowWithIntl(<PostBodyAdditionalContent {...props}/>);
|
||||
const instance = wrapper.instance();
|
||||
instance.isImage = jest.fn().mockReturnValueOnce(false);
|
||||
Utils.isYoutubeLink = jest.fn().mockReturnValueOnce(true); // eslint-disable-line no-import-assign
|
||||
|
||||
const imageUrl = instance.getImageUrl();
|
||||
expect(imageUrl).toEqual(url1);
|
||||
});
|
||||
|
||||
test('getImageUrl should return default URL if content is a YouTube link and there is no image metadata', () => {
|
||||
const wrapper = shallowWithIntl(<PostBodyAdditionalContent {...baseProps}/>);
|
||||
const instance = wrapper.instance();
|
||||
instance.isImage = jest.fn().mockReturnValueOnce(false);
|
||||
Utils.isYoutubeLink = jest.fn().mockReturnValueOnce(true); // eslint-disable-line no-import-assign
|
||||
Utils.getYouTubeVideoId = jest.fn().mockReturnValueOnce('videoId'); // eslint-disable-line no-import-assign
|
||||
|
||||
const imageUrl = instance.getImageUrl();
|
||||
expect(imageUrl).toEqual('https://i.ytimg.com/vi/videoId/hqdefault.jpg');
|
||||
});
|
||||
|
||||
test('getImageUrl should return undefined if content is not an image nor a YouTube link', () => {
|
||||
const wrapper = shallowWithIntl(<PostBodyAdditionalContent {...baseProps}/>);
|
||||
const instance = wrapper.instance();
|
||||
instance.isImage = jest.fn().mockReturnValueOnce(false);
|
||||
Utils.isYoutubeLink = jest.fn().mockReturnValueOnce(false); // eslint-disable-line no-import-assign
|
||||
|
||||
const imageUrl = instance.getImageUrl();
|
||||
expect(imageUrl).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -217,56 +217,40 @@ exports[`PostDraft Should render the DraftInput 1`] = `
|
||||
}
|
||||
}
|
||||
>
|
||||
<View
|
||||
<Text
|
||||
ellipsizeMode="tail"
|
||||
numberOfLines={1}
|
||||
style={
|
||||
Object {
|
||||
"backgroundColor": "#ffffff",
|
||||
"flex": 1,
|
||||
"marginLeft": 0,
|
||||
"marginRight": 0,
|
||||
"backgroundColor": "transparent",
|
||||
"color": "#3d3c40",
|
||||
"fontSize": 11,
|
||||
"marginBottom": 2,
|
||||
"paddingLeft": 10,
|
||||
"paddingTop": 3,
|
||||
"position": "absolute",
|
||||
}
|
||||
}
|
||||
>
|
||||
<Text
|
||||
ellipsizeMode="tail"
|
||||
numberOfLines={1}
|
||||
style={
|
||||
Object {
|
||||
"backgroundColor": "transparent",
|
||||
"color": "#3d3c40",
|
||||
"fontSize": 11,
|
||||
"marginBottom": 2,
|
||||
"paddingLeft": 10,
|
||||
"paddingTop": 3,
|
||||
"position": "absolute",
|
||||
}
|
||||
}
|
||||
/>
|
||||
<View
|
||||
style={
|
||||
Object {
|
||||
"backgroundColor": "#ffffff",
|
||||
"height": 0,
|
||||
}
|
||||
}
|
||||
/>
|
||||
</View>
|
||||
/>
|
||||
</View>
|
||||
<View
|
||||
<RNCSafeAreaView
|
||||
edges={
|
||||
Array [
|
||||
"left",
|
||||
"right",
|
||||
]
|
||||
}
|
||||
onLayout={[Function]}
|
||||
style={
|
||||
Array [
|
||||
Object {
|
||||
"alignItems": "flex-end",
|
||||
"backgroundColor": "#ffffff",
|
||||
"borderTopColor": "rgba(61,60,64,0.2)",
|
||||
"borderTopWidth": 1,
|
||||
"flexDirection": "row",
|
||||
"justifyContent": "center",
|
||||
"paddingBottom": 2,
|
||||
},
|
||||
null,
|
||||
]
|
||||
Object {
|
||||
"alignItems": "flex-end",
|
||||
"backgroundColor": "#ffffff",
|
||||
"borderTopColor": "rgba(61,60,64,0.2)",
|
||||
"borderTopWidth": 1,
|
||||
"flexDirection": "row",
|
||||
"justifyContent": "center",
|
||||
"paddingBottom": 2,
|
||||
}
|
||||
}
|
||||
testID="post_draft"
|
||||
>
|
||||
@@ -566,13 +550,17 @@ exports[`PostDraft Should render the DraftInput 1`] = `
|
||||
</View>
|
||||
</View>
|
||||
</RCTScrollView>
|
||||
</View>
|
||||
</RNCSafeAreaView>
|
||||
</KeyboardTrackingView>
|
||||
`;
|
||||
|
||||
exports[`PostDraft Should render the ReadOnly for canPost 1`] = `
|
||||
<RCTSafeAreaView
|
||||
emulateUnlessSupported={true}
|
||||
<RNCSafeAreaView
|
||||
edges={
|
||||
Array [
|
||||
"bottom",
|
||||
]
|
||||
}
|
||||
style={
|
||||
Object {
|
||||
"backgroundColor": "rgba(61,60,64,0.04)",
|
||||
@@ -617,12 +605,16 @@ exports[`PostDraft Should render the ReadOnly for canPost 1`] = `
|
||||
This channel is read-only.
|
||||
</Text>
|
||||
</View>
|
||||
</RCTSafeAreaView>
|
||||
</RNCSafeAreaView>
|
||||
`;
|
||||
|
||||
exports[`PostDraft Should render the ReadOnly for channelIsReadOnly 1`] = `
|
||||
<RCTSafeAreaView
|
||||
emulateUnlessSupported={true}
|
||||
<RNCSafeAreaView
|
||||
edges={
|
||||
Array [
|
||||
"bottom",
|
||||
]
|
||||
}
|
||||
style={
|
||||
Object {
|
||||
"backgroundColor": "rgba(61,60,64,0.04)",
|
||||
@@ -667,5 +659,5 @@ exports[`PostDraft Should render the ReadOnly for channelIsReadOnly 1`] = `
|
||||
This channel is read-only.
|
||||
</Text>
|
||||
</View>
|
||||
</RCTSafeAreaView>
|
||||
</RNCSafeAreaView>
|
||||
`;
|
||||
|
||||
@@ -6,6 +6,7 @@ import PropTypes from 'prop-types';
|
||||
import {Platform, ScrollView, View} from 'react-native';
|
||||
import {intlShape} from 'react-intl';
|
||||
import HWKeyboardEvent from 'react-native-hw-keyboard-event';
|
||||
import {SafeAreaView} from 'react-native-safe-area-context';
|
||||
|
||||
import Autocomplete from '@components/autocomplete';
|
||||
import PostInput from '@components/post_draft/post_input';
|
||||
@@ -13,7 +14,7 @@ import QuickActions from '@components/post_draft/quick_actions';
|
||||
import SendAction from '@components/post_draft/send_action';
|
||||
import Typing from '@components/post_draft/typing';
|
||||
import Uploads from '@components/post_draft/uploads';
|
||||
import {paddingHorizontal as padding} from '@components/safe_area_view/iphone_x_spacing';
|
||||
import DEVICE from '@constants/device';
|
||||
import {CHANNEL_POST_TEXTBOX_CURSOR_CHANGE, CHANNEL_POST_TEXTBOX_VALUE_CHANGE, IS_REACTION_REGEX} from '@constants/post_draft';
|
||||
import {NOTIFY_ALL_MEMBERS} from '@constants/view';
|
||||
import EventEmitter from '@mm-redux/utils/event_emitter';
|
||||
@@ -23,7 +24,6 @@ import {confirmOutOfOfficeDisabled} from '@utils/status';
|
||||
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
|
||||
|
||||
const AUTOCOMPLETE_MARGIN = 20;
|
||||
const AUTOCOMPLETE_MAX_HEIGHT = 200;
|
||||
const HW_SHIFT_ENTER_TEXT = Platform.OS === 'ios' ? '\n' : '';
|
||||
const HW_EVENT_IN_SCREEN = ['Channel', 'Thread'];
|
||||
|
||||
@@ -400,8 +400,8 @@ export default class DraftInput extends PureComponent {
|
||||
channelDisplayName,
|
||||
channelId,
|
||||
cursorPositionEvent,
|
||||
isLandscape,
|
||||
files,
|
||||
isLandscape,
|
||||
maxMessageLength,
|
||||
screenId,
|
||||
valueEvent,
|
||||
@@ -417,10 +417,22 @@ export default class DraftInput extends PureComponent {
|
||||
theme={theme}
|
||||
registerTypingAnimation={registerTypingAnimation}
|
||||
/>
|
||||
<View
|
||||
testID={testID}
|
||||
style={[style.inputWrapper, padding(isLandscape)]}
|
||||
{Platform.OS === 'android' &&
|
||||
<Autocomplete
|
||||
cursorPositionEvent={cursorPositionEvent}
|
||||
maxHeight={Math.min(this.state.top - AUTOCOMPLETE_MARGIN, DEVICE.AUTOCOMPLETE_MAX_HEIGHT)}
|
||||
onChangeText={this.handleInputQuickAction}
|
||||
valueEvent={valueEvent}
|
||||
rootId={rootId}
|
||||
channelId={channelId}
|
||||
offsetY={0}
|
||||
/>
|
||||
}
|
||||
<SafeAreaView
|
||||
edges={['left', 'right']}
|
||||
onLayout={this.handleLayout}
|
||||
style={style.inputWrapper}
|
||||
testID={testID}
|
||||
>
|
||||
<ScrollView
|
||||
style={style.inputContainer}
|
||||
@@ -467,17 +479,7 @@ export default class DraftInput extends PureComponent {
|
||||
/>
|
||||
</View>
|
||||
</ScrollView>
|
||||
</View>
|
||||
{Platform.OS === 'android' &&
|
||||
<Autocomplete
|
||||
cursorPositionEvent={cursorPositionEvent}
|
||||
maxHeight={Math.min(this.state.top - AUTOCOMPLETE_MARGIN, AUTOCOMPLETE_MAX_HEIGHT)}
|
||||
onChangeText={this.handleInputQuickAction}
|
||||
valueEvent={valueEvent}
|
||||
rootId={rootId}
|
||||
channelId={channelId}
|
||||
/>
|
||||
}
|
||||
</SafeAreaView>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ import {intlShape} from 'react-intl';
|
||||
|
||||
import PasteableTextInput from '@components/pasteable_text_input';
|
||||
import {NavigationTypes} from '@constants';
|
||||
import DEVICE from '@constants/device';
|
||||
import {INSERT_TO_COMMENT, INSERT_TO_DRAFT} from '@constants/post_draft';
|
||||
import EventEmitter from '@mm-redux/utils/event_emitter';
|
||||
import {t} from '@utils/i18n';
|
||||
@@ -270,7 +271,7 @@ export default class PostInput extends PureComponent {
|
||||
const {channelDisplayName, isLandscape, theme} = this.props;
|
||||
const style = getStyleSheet(theme);
|
||||
const placeholder = this.getPlaceHolder();
|
||||
let maxHeight = 150;
|
||||
let maxHeight = DEVICE.POST_INPUT_MAX_HEIGHT;
|
||||
|
||||
if (isLandscape) {
|
||||
maxHeight = 88;
|
||||
|
||||
@@ -17,7 +17,6 @@ describe('PostInput', () => {
|
||||
handleCommentDraftChanged: jest.fn(),
|
||||
handlePostDraftChanged: jest.fn(),
|
||||
inputEventType: '',
|
||||
isLandscape: false,
|
||||
maxMessageLength: 4000,
|
||||
onPasteFiles: jest.fn(),
|
||||
onSend: jest.fn(),
|
||||
|
||||
@@ -1,7 +1,12 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`PostDraft ReadOnly Should match snapshot 1`] = `
|
||||
<ForwardRef(SafeAreaView)
|
||||
<RNCSafeAreaView
|
||||
edges={
|
||||
Array [
|
||||
"bottom",
|
||||
]
|
||||
}
|
||||
style={
|
||||
Object {
|
||||
"backgroundColor": "rgba(61,60,64,0.04)",
|
||||
@@ -46,5 +51,5 @@ exports[`PostDraft ReadOnly Should match snapshot 1`] = `
|
||||
}
|
||||
/>
|
||||
</View>
|
||||
</ForwardRef(SafeAreaView)>
|
||||
</RNCSafeAreaView>
|
||||
`;
|
||||
|
||||
@@ -2,7 +2,8 @@
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React, {ReactNode} from 'react';
|
||||
import {SafeAreaView, View} from 'react-native';
|
||||
import {View} from 'react-native';
|
||||
import {SafeAreaView} from 'react-native-safe-area-context';
|
||||
|
||||
import CompassIcon from '@components/compass_icon';
|
||||
import FormattedText from '@components/formatted_text';
|
||||
@@ -16,10 +17,13 @@ interface ReadOnlyProps {
|
||||
const ReadOnlyChannnel = ({theme}: ReadOnlyProps): ReactNode => {
|
||||
const style = getStyle(theme);
|
||||
return (
|
||||
<SafeAreaView style={style.background}>
|
||||
<SafeAreaView
|
||||
edges={['bottom']}
|
||||
style={style.background}
|
||||
>
|
||||
<View
|
||||
testID='post_draft.read_only'
|
||||
style={style.container}
|
||||
testID='post_draft.read_only'
|
||||
>
|
||||
<CompassIcon
|
||||
name='glasses'
|
||||
|
||||
@@ -11,7 +11,6 @@ import {
|
||||
import EventEmitter from '@mm-redux/utils/event_emitter';
|
||||
|
||||
import FormattedText from '@components/formatted_text';
|
||||
import SafeAreaView from '@components/safe_area_view';
|
||||
import {makeStyleSheetFromTheme} from '@utils/theme';
|
||||
import {TYPING_VISIBLE, TYPING_HEIGHT} from '@constants/post_draft';
|
||||
|
||||
@@ -95,19 +94,13 @@ export default class Typing extends PureComponent {
|
||||
|
||||
return (
|
||||
<Animated.View style={{bottom: this.typingBottom}}>
|
||||
<SafeAreaView
|
||||
excludeHeader={true}
|
||||
excludeFooter={true}
|
||||
useLandscapeMargin={true}
|
||||
<Text
|
||||
style={style.typing}
|
||||
ellipsizeMode='tail'
|
||||
numberOfLines={1}
|
||||
>
|
||||
<Text
|
||||
style={style.typing}
|
||||
ellipsizeMode='tail'
|
||||
numberOfLines={1}
|
||||
>
|
||||
{this.renderTyping()}
|
||||
</Text>
|
||||
</SafeAreaView>
|
||||
{this.renderTyping()}
|
||||
</Text>
|
||||
</Animated.View>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -27,6 +27,7 @@ import UploadItem from './upload_item';
|
||||
|
||||
const showFiles = {opacity: 1, height: 68};
|
||||
const hideFiles = {opacity: 0, height: 0};
|
||||
const showError = {height: 20};
|
||||
const hideError = {height: 0};
|
||||
|
||||
export default class Uploads extends PureComponent {
|
||||
@@ -53,12 +54,14 @@ export default class Uploads extends PureComponent {
|
||||
};
|
||||
|
||||
state = {
|
||||
errorVisible: false,
|
||||
fileSizeWarning: null,
|
||||
showFileMaxWarning: false,
|
||||
};
|
||||
|
||||
errorContainerRef = React.createRef();
|
||||
containerRef = React.createRef();
|
||||
hideErrorTimer = null;
|
||||
|
||||
componentDidMount() {
|
||||
EventEmitter.on(MAX_FILE_COUNT_WARNING, this.handleFileMaxWarning);
|
||||
@@ -85,8 +88,14 @@ export default class Uploads extends PureComponent {
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
if (this.containerRef.current && this.props.files.length !== prevProps.files.length) {
|
||||
this.showOrHideContainer();
|
||||
if (this.props.files.length !== prevProps.files.length) {
|
||||
if (this.containerRef.current) {
|
||||
this.showOrHideContainer();
|
||||
}
|
||||
|
||||
if (prevProps.files.length === MAX_FILE_COUNT && this.state.showFileMaxWarning) {
|
||||
this.hideError();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -111,13 +120,12 @@ export default class Uploads extends PureComponent {
|
||||
openGalleryAtIndex(index, files.filter((f) => !f.failed && !f.loading));
|
||||
}
|
||||
|
||||
clearErrorsFromState = (delay) => {
|
||||
setTimeout(() => {
|
||||
this.setState({
|
||||
showFileMaxWarning: false,
|
||||
fileSizeWarning: null,
|
||||
});
|
||||
}, delay || 0);
|
||||
clearErrorsFromState = () => {
|
||||
this.setState({
|
||||
errorVisible: false,
|
||||
showFileMaxWarning: false,
|
||||
fileSizeWarning: null,
|
||||
});
|
||||
}
|
||||
|
||||
handleAndroidBack = () => {
|
||||
@@ -132,10 +140,7 @@ export default class Uploads extends PureComponent {
|
||||
handleFileMaxWarning = () => {
|
||||
this.setState({showFileMaxWarning: true});
|
||||
if (this.errorContainerRef.current) {
|
||||
this.makeErrorVisible(true, 20);
|
||||
setTimeout(() => {
|
||||
this.makeErrorVisible(false, 20);
|
||||
}, 5000);
|
||||
this.showError();
|
||||
}
|
||||
};
|
||||
|
||||
@@ -150,10 +155,7 @@ export default class Uploads extends PureComponent {
|
||||
});
|
||||
|
||||
this.setState({fileSizeWarning: message});
|
||||
this.makeErrorVisible(true, 20);
|
||||
setTimeout(() => {
|
||||
this.makeErrorVisible(false, 20);
|
||||
}, 5000);
|
||||
this.showError();
|
||||
}
|
||||
};
|
||||
|
||||
@@ -200,10 +202,7 @@ export default class Uploads extends PureComponent {
|
||||
});
|
||||
|
||||
this.setState({fileSizeWarning: message});
|
||||
this.makeErrorVisible(true, 20);
|
||||
setTimeout(() => {
|
||||
this.makeErrorVisible(false, 20);
|
||||
}, 5000);
|
||||
this.showError();
|
||||
}
|
||||
};
|
||||
|
||||
@@ -238,15 +237,30 @@ export default class Uploads extends PureComponent {
|
||||
this.handleFileSizeWarning();
|
||||
} else {
|
||||
this.props.initUploadFiles(files, this.props.rootId);
|
||||
this.hideError();
|
||||
}
|
||||
};
|
||||
|
||||
makeErrorVisible = (visible, height) => {
|
||||
showError = () => {
|
||||
if (this.hideErrorTimer) {
|
||||
clearTimeout(this.hideErrorTimer);
|
||||
}
|
||||
|
||||
this.makeErrorVisible(true);
|
||||
this.hideErrorTimer = setTimeout(this.hideError, 5000);
|
||||
}
|
||||
|
||||
hideError = () => this.makeErrorVisible(false);
|
||||
|
||||
makeErrorVisible = (visible) => {
|
||||
if (this.errorContainerRef.current) {
|
||||
if (visible) {
|
||||
this.errorContainerRef.current.transition(hideError, {height}, 200, 'ease-out');
|
||||
} else {
|
||||
this.errorContainerRef.current.transition({height}, hideError, 200, 'ease-in');
|
||||
if (visible && !this.state.errorVisible) {
|
||||
this.setState({errorVisible: true});
|
||||
this.errorContainerRef.current.transition(hideError, showError, 200, 'ease-out');
|
||||
} else if (!visible && this.state.errorVisible) {
|
||||
this.setState({errorVisible: false});
|
||||
this.errorContainerRef.current.transition(showError, hideError, 200, 'ease-in');
|
||||
this.clearErrorsFromState();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,7 +18,7 @@ exports[`MoreMessagesButton should match snapshot 1`] = `
|
||||
Object {
|
||||
"transform": Array [
|
||||
Object {
|
||||
"translateY": -138,
|
||||
"translateY": -438,
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
@@ -15,7 +15,7 @@ import ViewTypes, {INDICATOR_BAR_HEIGHT} from '@constants/view';
|
||||
import {makeStyleSheetFromTheme, hexToHue} from '@utils/theme';
|
||||
import {t} from '@utils/i18n';
|
||||
|
||||
const HIDDEN_TOP = -100;
|
||||
const HIDDEN_TOP = -400;
|
||||
const SHOWN_TOP = 0;
|
||||
export const INDICATOR_BAR_FACTOR = Math.abs(INDICATOR_BAR_HEIGHT / (HIDDEN_TOP - SHOWN_TOP));
|
||||
export const MIN_INPUT = 0;
|
||||
|
||||
@@ -3,19 +3,14 @@
|
||||
exports[`Reactions Should match snapshot with default emojis 1`] = `
|
||||
<View
|
||||
style={
|
||||
Array [
|
||||
Object {
|
||||
"alignItems": "center",
|
||||
"flex": 1,
|
||||
"flexDirection": "row",
|
||||
"height": 72,
|
||||
"justifyContent": "space-between",
|
||||
},
|
||||
Object {
|
||||
"paddingLeft": 12,
|
||||
"paddingRight": 12,
|
||||
},
|
||||
]
|
||||
Object {
|
||||
"alignItems": "center",
|
||||
"flex": 1,
|
||||
"flexDirection": "row",
|
||||
"height": 72,
|
||||
"justifyContent": "space-between",
|
||||
"paddingHorizontal": 12,
|
||||
}
|
||||
}
|
||||
>
|
||||
<ReactionButton
|
||||
@@ -265,19 +260,14 @@ exports[`Reactions Should match snapshot with default emojis 1`] = `
|
||||
exports[`Reactions Should match snapshot with default emojis 2`] = `
|
||||
<View
|
||||
style={
|
||||
Array [
|
||||
Object {
|
||||
"alignItems": "center",
|
||||
"flex": 1,
|
||||
"flexDirection": "row",
|
||||
"height": 72,
|
||||
"justifyContent": "space-between",
|
||||
},
|
||||
Object {
|
||||
"paddingLeft": 12,
|
||||
"paddingRight": 12,
|
||||
},
|
||||
]
|
||||
Object {
|
||||
"alignItems": "center",
|
||||
"flex": 1,
|
||||
"flexDirection": "row",
|
||||
"height": 72,
|
||||
"justifyContent": "space-between",
|
||||
"paddingHorizontal": 12,
|
||||
}
|
||||
}
|
||||
>
|
||||
<ReactionButton
|
||||
|
||||
@@ -3,16 +3,12 @@
|
||||
|
||||
import {connect} from 'react-redux';
|
||||
import {getTheme} from '@mm-redux/selectors/entities/preferences';
|
||||
import {getDimensions, isLandscape} from 'app/selectors/device';
|
||||
import ReactionPicker from './reaction_picker';
|
||||
|
||||
function mapStateToProps(state) {
|
||||
const {deviceWidth} = getDimensions(state);
|
||||
return {
|
||||
theme: getTheme(state),
|
||||
recentEmojis: state.views.recentEmojis,
|
||||
deviceWidth,
|
||||
isLandscape: isLandscape(state),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -1,15 +1,11 @@
|
||||
// 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 {
|
||||
View,
|
||||
TouchableWithoutFeedback,
|
||||
} from 'react-native';
|
||||
import {View, TouchableWithoutFeedback, useWindowDimensions} from 'react-native';
|
||||
|
||||
import CompassIcon from '@components/compass_icon';
|
||||
import {paddingHorizontal as padding} from '@components/safe_area_view/iphone_x_spacing';
|
||||
import {
|
||||
REACTION_PICKER_HEIGHT,
|
||||
DEFAULT_EMOJIS,
|
||||
@@ -23,88 +19,73 @@ import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
|
||||
|
||||
import ReactionButton from './reaction_button';
|
||||
|
||||
export default class ReactionPicker extends PureComponent {
|
||||
static propTypes = {
|
||||
addReaction: PropTypes.func.isRequired,
|
||||
openReactionScreen: PropTypes.func.isRequired,
|
||||
theme: PropTypes.object.isRequired,
|
||||
recentEmojis: PropTypes.array,
|
||||
deviceWidth: PropTypes.number,
|
||||
isLandscape: PropTypes.bool.isRequired,
|
||||
const ReactionPicker = (props) => {
|
||||
const {theme} = props;
|
||||
const style = getStyleSheet(theme);
|
||||
const {width} = useWindowDimensions();
|
||||
const isSmallDevice = width < SMALL_ICON_BREAKPOINT;
|
||||
|
||||
const handlePress = (emoji) => {
|
||||
props.addReaction(emoji);
|
||||
};
|
||||
|
||||
let containerSize = LARGE_CONTAINER_SIZE;
|
||||
let iconSize = LARGE_ICON_SIZE;
|
||||
|
||||
if (isSmallDevice) {
|
||||
containerSize = SMALL_CONTAINER_SIZE;
|
||||
iconSize = SMALL_ICON_SIZE;
|
||||
}
|
||||
|
||||
handlePress = (emoji) => {
|
||||
this.props.addReaction(emoji);
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
theme,
|
||||
deviceWidth,
|
||||
isLandscape,
|
||||
} = this.props;
|
||||
const style = getStyleSheet(theme);
|
||||
const isSmallDevice = deviceWidth < SMALL_ICON_BREAKPOINT;
|
||||
|
||||
let containerSize = LARGE_CONTAINER_SIZE;
|
||||
let iconSize = LARGE_ICON_SIZE;
|
||||
|
||||
if (isSmallDevice) {
|
||||
containerSize = SMALL_CONTAINER_SIZE;
|
||||
iconSize = SMALL_ICON_SIZE;
|
||||
}
|
||||
|
||||
// Mixing recent emojis with default list and removing duplicates
|
||||
const emojis = Array.from(new Set(this.props.recentEmojis.concat(DEFAULT_EMOJIS))).splice(0, 6);
|
||||
|
||||
const list = emojis.map((emoji) => {
|
||||
return (
|
||||
<ReactionButton
|
||||
key={emoji}
|
||||
theme={theme}
|
||||
addReaction={this.handlePress}
|
||||
emoji={emoji}
|
||||
iconSize={iconSize}
|
||||
containerSize={containerSize}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
let paddingRes = padding(isLandscape, 12);
|
||||
if (!paddingRes) {
|
||||
paddingRes = {
|
||||
paddingLeft: 12,
|
||||
paddingRight: 12,
|
||||
};
|
||||
}
|
||||
|
||||
const emojis = Array.from(new Set(props.recentEmojis.concat(DEFAULT_EMOJIS))).splice(0, 6);
|
||||
const list = emojis.map((emoji) => {
|
||||
return (
|
||||
<View style={[style.reactionListContainer, paddingRes]}>
|
||||
{list}
|
||||
<TouchableWithoutFeedback
|
||||
onPress={this.props.openReactionScreen}
|
||||
testID='open.add_reaction.button'
|
||||
>
|
||||
<View
|
||||
style={[
|
||||
style.reactionContainer,
|
||||
{
|
||||
width: containerSize,
|
||||
height: containerSize,
|
||||
},
|
||||
]}
|
||||
>
|
||||
<CompassIcon
|
||||
name='emoticon-plus-outline'
|
||||
size={31.2}
|
||||
style={style.icon}
|
||||
/>
|
||||
</View>
|
||||
</TouchableWithoutFeedback>
|
||||
</View>
|
||||
<ReactionButton
|
||||
key={emoji}
|
||||
theme={theme}
|
||||
addReaction={handlePress}
|
||||
emoji={emoji}
|
||||
iconSize={iconSize}
|
||||
containerSize={containerSize}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return (
|
||||
<View style={style.reactionListContainer}>
|
||||
{list}
|
||||
<TouchableWithoutFeedback
|
||||
onPress={props.openReactionScreen}
|
||||
testID='open.add_reaction.button'
|
||||
>
|
||||
<View
|
||||
style={[
|
||||
style.reactionContainer,
|
||||
{
|
||||
width: containerSize,
|
||||
height: containerSize,
|
||||
},
|
||||
]}
|
||||
>
|
||||
<CompassIcon
|
||||
name='emoticon-plus-outline'
|
||||
size={31.2}
|
||||
style={style.icon}
|
||||
/>
|
||||
</View>
|
||||
</TouchableWithoutFeedback>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
ReactionPicker.propTypes = {
|
||||
addReaction: PropTypes.func.isRequired,
|
||||
openReactionScreen: PropTypes.func.isRequired,
|
||||
theme: PropTypes.object.isRequired,
|
||||
recentEmojis: PropTypes.array,
|
||||
};
|
||||
|
||||
export default ReactionPicker;
|
||||
|
||||
const getStyleSheet = makeStyleSheetFromTheme((theme) => {
|
||||
return {
|
||||
@@ -117,6 +98,7 @@ const getStyleSheet = makeStyleSheetFromTheme((theme) => {
|
||||
alignItems: 'center',
|
||||
height: REACTION_PICKER_HEIGHT,
|
||||
justifyContent: 'space-between',
|
||||
paddingHorizontal: 12,
|
||||
},
|
||||
reactionContainer: {
|
||||
backgroundColor: changeOpacity(theme.centerChannelColor, 0.04),
|
||||
|
||||
@@ -11,8 +11,6 @@ import Preferences from '@mm-redux/constants/preferences';
|
||||
describe('Reactions', () => {
|
||||
const baseProps = {
|
||||
addReaction: jest.fn(),
|
||||
deviceWidth: undefined,
|
||||
isLandscape: false,
|
||||
openReactionScreen: jest.fn(),
|
||||
recentEmojis: [],
|
||||
theme: Preferences.THEMES.default,
|
||||
|
||||
@@ -1,32 +0,0 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`SafeAreaIos should match snapshot 1`] = `
|
||||
<View
|
||||
style={
|
||||
Object {
|
||||
"backgroundColor": "#ffffff",
|
||||
"flex": 1,
|
||||
"marginLeft": 0,
|
||||
"marginRight": 0,
|
||||
}
|
||||
}
|
||||
>
|
||||
<ForwardRef(AnimatedComponentWrapper)
|
||||
style={
|
||||
Object {
|
||||
"backgroundColor": "#1153ab",
|
||||
"paddingTop": 44,
|
||||
"zIndex": 10,
|
||||
}
|
||||
}
|
||||
/>
|
||||
<View
|
||||
style={
|
||||
Object {
|
||||
"backgroundColor": "#ffffff",
|
||||
"height": 0,
|
||||
}
|
||||
}
|
||||
/>
|
||||
</View>
|
||||
`;
|
||||
@@ -5,7 +5,7 @@ import {connect} from 'react-redux';
|
||||
|
||||
import {getTheme} from '@mm-redux/selectors/entities/preferences';
|
||||
|
||||
import SafeAreaView from './safe_area_view';
|
||||
import SafeAreaView from './safe_area';
|
||||
|
||||
function mapStateToProps(state) {
|
||||
return {
|
||||
|
||||
@@ -1,20 +0,0 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {DeviceTypes, ViewTypes} from 'app/constants';
|
||||
|
||||
export const paddingHorizontal = (isLandscape, modifier = 0) => {
|
||||
return DeviceTypes.IS_IPHONE_WITH_INSETS && isLandscape ? {paddingHorizontal: ViewTypes.IOS_HORIZONTAL_LANDSCAPE + modifier} : null;
|
||||
};
|
||||
|
||||
export const paddingLeft = (isLandscape, modifier = 0) => {
|
||||
return DeviceTypes.IS_IPHONE_WITH_INSETS && isLandscape ? {paddingLeft: ViewTypes.IOS_HORIZONTAL_LANDSCAPE + modifier} : null;
|
||||
};
|
||||
|
||||
export const paddingRight = (isLandscape, modifier = 0) => {
|
||||
return DeviceTypes.IS_IPHONE_WITH_INSETS && isLandscape ? {paddingRight: ViewTypes.IOS_HORIZONTAL_LANDSCAPE + modifier} : null;
|
||||
};
|
||||
|
||||
export const marginHorizontal = (isLandscape, modifier = 0) => {
|
||||
return DeviceTypes.IS_IPHONE_WITH_INSETS && isLandscape ? {marginHorizontal: ViewTypes.IOS_HORIZONTAL_LANDSCAPE + modifier} : null;
|
||||
};
|
||||
84
app/components/safe_area_view/safe_area.tsx
Normal file
84
app/components/safe_area_view/safe_area.tsx
Normal file
@@ -0,0 +1,84 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React, {useCallback} from 'react';
|
||||
import {View} from 'react-native';
|
||||
import {useSafeAreaInsets} from 'react-native-safe-area-context';
|
||||
|
||||
import type {Theme} from '@mm-redux/types/preferences';
|
||||
|
||||
type SafeAreaProps = {
|
||||
backgroundColor?: string;
|
||||
children: Array<React.ReactChild> | React.ReactChild;
|
||||
excludeHeader?: boolean;
|
||||
excludeFooter?: boolean;
|
||||
excludeLeft?: boolean;
|
||||
excludeRight?: boolean;
|
||||
headerComponent?: React.ReactElement;
|
||||
footerColor?: string;
|
||||
footerComponent?: React.ReactElement;
|
||||
navBarBackgroundColor?: string;
|
||||
theme: Theme;
|
||||
}
|
||||
|
||||
const SafeArea = (props: SafeAreaProps) => {
|
||||
const {
|
||||
backgroundColor,
|
||||
excludeFooter,
|
||||
excludeHeader,
|
||||
excludeLeft,
|
||||
excludeRight,
|
||||
footerColor,
|
||||
footerComponent,
|
||||
headerComponent,
|
||||
navBarBackgroundColor,
|
||||
theme,
|
||||
} = props;
|
||||
const insets = useSafeAreaInsets();
|
||||
|
||||
const renderTop = useCallback(() => {
|
||||
if (excludeHeader) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let topColor = theme.sidebarHeaderBg;
|
||||
if (navBarBackgroundColor) {
|
||||
topColor = navBarBackgroundColor;
|
||||
}
|
||||
|
||||
return (
|
||||
<View style={{backgroundColor: topColor, zIndex: 10, paddingTop: insets.top, paddingLeft: insets.left, paddingRight: insets.right}}>
|
||||
{headerComponent}
|
||||
</View>
|
||||
);
|
||||
}, [insets, props.theme]);
|
||||
|
||||
let bgColor = theme.centerChannelBg;
|
||||
if (backgroundColor) {
|
||||
bgColor = backgroundColor;
|
||||
}
|
||||
|
||||
let bottomColor = theme.centerChannelBg;
|
||||
if (footerColor) {
|
||||
bottomColor = footerColor;
|
||||
}
|
||||
|
||||
let bottomInset = insets.bottom;
|
||||
if (excludeFooter) {
|
||||
bottomInset = 0;
|
||||
}
|
||||
|
||||
return (
|
||||
<View style={{flex: 1, backgroundColor: bgColor}}>
|
||||
{renderTop()}
|
||||
<View style={{flex: 1, marginLeft: excludeLeft ? 0 : insets.left, marginRight: excludeRight ? 0 : insets.right}}>
|
||||
{props.children}
|
||||
</View>
|
||||
<View style={{marginBottom: bottomInset, backgroundColor: bottomColor}}>
|
||||
{footerComponent}
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
export default SafeArea;
|
||||
@@ -1,8 +0,0 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
const SafeAreaAndroid = (props) => {
|
||||
return props.children;
|
||||
};
|
||||
|
||||
export default SafeAreaAndroid;
|
||||
@@ -1,243 +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 {Animated, Dimensions, Keyboard, NativeModules, View} from 'react-native';
|
||||
import SafeArea from 'react-native-safe-area';
|
||||
|
||||
import EventEmitter from '@mm-redux/utils/event_emitter';
|
||||
|
||||
import {DeviceTypes, ViewTypes} from 'app/constants';
|
||||
import mattermostManaged from 'app/mattermost_managed';
|
||||
import EphemeralStore from 'app/store/ephemeral_store';
|
||||
|
||||
const {StatusBarManager} = NativeModules;
|
||||
const {PORTRAIT, LANDSCAPE} = ViewTypes;
|
||||
|
||||
export default class SafeAreaIos extends PureComponent {
|
||||
static propTypes = {
|
||||
backgroundColor: PropTypes.string,
|
||||
children: PropTypes.node.isRequired,
|
||||
excludeHeader: PropTypes.bool,
|
||||
excludeFooter: PropTypes.bool,
|
||||
footerColor: PropTypes.string,
|
||||
footerComponent: PropTypes.node,
|
||||
keyboardOffset: PropTypes.number.isRequired,
|
||||
navBarBackgroundColor: PropTypes.string,
|
||||
headerComponent: PropTypes.node,
|
||||
theme: PropTypes.object.isRequired,
|
||||
useLandscapeMargin: PropTypes.bool.isRequired,
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
keyboardOffset: 0,
|
||||
useLandscapeMargin: false,
|
||||
};
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
const insetTop = DeviceTypes.IS_IPHONE_WITH_INSETS ? 44 : 20;
|
||||
let insetBottom = 0;
|
||||
if ((DeviceTypes.IS_IPHONE_WITH_INSETS || mattermostManaged.hasSafeAreaInsets) && props.excludeFooter) {
|
||||
insetBottom = 20;
|
||||
}
|
||||
|
||||
this.state = {
|
||||
keyboard: false,
|
||||
safeAreaInsets: {
|
||||
top: insetTop,
|
||||
left: 0,
|
||||
bottom: insetBottom,
|
||||
right: 0,
|
||||
},
|
||||
};
|
||||
|
||||
this.topBarHeight = new Animated.Value(insetTop);
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.mounted = true;
|
||||
|
||||
Dimensions.addEventListener('change', this.getSafeAreaInsets);
|
||||
EventEmitter.on('update_safe_area_view', this.getSafeAreaInsets);
|
||||
if (EphemeralStore.safeAreaInsets[PORTRAIT] === null || EphemeralStore.safeAreaInsets[LANDSCAPE] === null) {
|
||||
SafeArea.addEventListener('safeAreaInsetsForRootViewDidChange', this.onSafeAreaInsetsForRootViewChange);
|
||||
}
|
||||
|
||||
this.keyboardDidShowListener = Keyboard.addListener('keyboardWillShow', this.keyboardWillShow);
|
||||
this.keyboardDidHideListener = Keyboard.addListener('keyboardWillHide', this.keyboardWillHide);
|
||||
|
||||
this.getSafeAreaInsets();
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
Dimensions.removeEventListener('change', this.getSafeAreaInsets);
|
||||
EventEmitter.off('update_safe_area_view', this.getSafeAreaInsets);
|
||||
SafeArea.removeEventListener('safeAreaInsetsForRootViewDidChange', this.onSafeAreaInsetsForRootViewChange);
|
||||
this.keyboardDidShowListener.remove();
|
||||
this.keyboardDidHideListener.remove();
|
||||
|
||||
this.mounted = false;
|
||||
}
|
||||
|
||||
getSafeAreaInsets = async (dimensions) => {
|
||||
this.getStatusBarHeight();
|
||||
|
||||
if (DeviceTypes.IS_IPHONE_WITH_INSETS || mattermostManaged.hasSafeAreaInsets) {
|
||||
const window = dimensions?.window || Dimensions.get('window');
|
||||
const orientation = window.width > window.height ? LANDSCAPE : PORTRAIT;
|
||||
const {safeAreaInsets} = await SafeArea.getSafeAreaInsetsForRootView();
|
||||
this.setSafeAreaInsets(safeAreaInsets, orientation);
|
||||
}
|
||||
}
|
||||
|
||||
setSafeAreaInsets = (safeAreaInsets, orientation) => {
|
||||
if (EphemeralStore.safeAreaInsets[orientation] === null) {
|
||||
EphemeralStore.safeAreaInsets[orientation] = safeAreaInsets;
|
||||
}
|
||||
|
||||
if (this.mounted) {
|
||||
this.setState({
|
||||
safeAreaInsets: EphemeralStore.safeAreaInsets[orientation],
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
getStatusBarHeight = () => {
|
||||
try {
|
||||
StatusBarManager.getHeight(
|
||||
(statusBarFrameData) => {
|
||||
if (this.mounted) {
|
||||
if (statusBarFrameData.height === 0) {
|
||||
this.hideTopBar();
|
||||
} else {
|
||||
this.showTopBar();
|
||||
}
|
||||
}
|
||||
},
|
||||
);
|
||||
} catch (e) {
|
||||
// not needed
|
||||
}
|
||||
};
|
||||
|
||||
onSafeAreaInsetsForRootViewChange = ({safeAreaInsets}) => {
|
||||
if (EphemeralStore.safeAreaInsets[PORTRAIT] !== null && EphemeralStore.safeAreaInsets[LANDSCAPE] !== null) {
|
||||
SafeArea.removeEventListener('safeAreaInsetsForRootViewDidChange', this.onSafeAreaInsetsForRootViewChange);
|
||||
return;
|
||||
}
|
||||
|
||||
if (DeviceTypes.IS_IPHONE_WITH_INSETS || mattermostManaged.hasSafeAreaInsets) {
|
||||
this.getStatusBarHeight();
|
||||
|
||||
const {width, height} = Dimensions.get('window');
|
||||
const orientation = width > height ? LANDSCAPE : PORTRAIT;
|
||||
this.setSafeAreaInsets(safeAreaInsets, orientation);
|
||||
}
|
||||
}
|
||||
|
||||
keyboardWillHide = () => {
|
||||
this.setState({keyboard: false});
|
||||
};
|
||||
|
||||
keyboardWillShow = () => {
|
||||
this.setState({keyboard: true});
|
||||
};
|
||||
|
||||
hideTopBar = () => {
|
||||
Animated.timing(this.topBarHeight, {
|
||||
toValue: 10,
|
||||
duration: 350,
|
||||
useNativeDriver: false,
|
||||
}).start();
|
||||
}
|
||||
|
||||
showTopBar = () => {
|
||||
Animated.timing(this.topBarHeight, {
|
||||
toValue: this.state.safeAreaInsets.top,
|
||||
duration: 350,
|
||||
useNativeDriver: false,
|
||||
}).start();
|
||||
}
|
||||
|
||||
renderTopBar = () => {
|
||||
const {headerComponent, excludeHeader, navBarBackgroundColor, theme} = this.props;
|
||||
|
||||
if (excludeHeader) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let topColor = theme.sidebarHeaderBg;
|
||||
if (navBarBackgroundColor) {
|
||||
topColor = navBarBackgroundColor;
|
||||
}
|
||||
|
||||
if (headerComponent) {
|
||||
return (
|
||||
<Animated.View
|
||||
style={{
|
||||
backgroundColor: topColor,
|
||||
height: this.topBarHeight,
|
||||
zIndex: 10,
|
||||
}}
|
||||
>
|
||||
{headerComponent}
|
||||
</Animated.View>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Animated.View
|
||||
style={{
|
||||
backgroundColor: topColor,
|
||||
paddingTop: this.topBarHeight,
|
||||
zIndex: 10,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
render() {
|
||||
const {backgroundColor, children, excludeFooter, footerColor, footerComponent, keyboardOffset, theme, useLandscapeMargin} = this.props;
|
||||
const {keyboard, safeAreaInsets} = this.state;
|
||||
|
||||
let bgColor = theme.centerChannelBg;
|
||||
if (backgroundColor) {
|
||||
bgColor = backgroundColor;
|
||||
}
|
||||
|
||||
let bottomColor = theme.centerChannelBg;
|
||||
if (footerColor) {
|
||||
bottomColor = footerColor;
|
||||
}
|
||||
|
||||
let offset = 0;
|
||||
if (keyboardOffset && mattermostManaged.hasSafeAreaInsets) {
|
||||
offset = keyboardOffset;
|
||||
}
|
||||
|
||||
let bottomInset = safeAreaInsets.bottom;
|
||||
if (excludeFooter) {
|
||||
bottomInset = 0;
|
||||
}
|
||||
|
||||
return (
|
||||
<View
|
||||
style={{
|
||||
flex: 1,
|
||||
backgroundColor: bgColor,
|
||||
marginLeft: useLandscapeMargin ? safeAreaInsets.left : 0,
|
||||
marginRight: useLandscapeMargin ? safeAreaInsets.right : 0,
|
||||
}}
|
||||
>
|
||||
{this.renderTopBar()}
|
||||
{children}
|
||||
<View style={{height: keyboard ? offset : bottomInset, backgroundColor: bottomColor}}>
|
||||
{footerComponent}
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,305 +0,0 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
import React from 'react';
|
||||
import SafeArea from 'react-native-safe-area';
|
||||
import {shallow} from 'enzyme';
|
||||
|
||||
import Preferences from '@mm-redux/constants/preferences';
|
||||
|
||||
import {DeviceTypes, ViewTypes} from 'app/constants';
|
||||
import mattermostManaged from 'app/mattermost_managed';
|
||||
import EphemeralStore from 'app/store/ephemeral_store';
|
||||
|
||||
import SafeAreaIos from './safe_area_view.ios';
|
||||
|
||||
const {PORTRAIT, LANDSCAPE} = ViewTypes;
|
||||
|
||||
describe('SafeAreaIos', () => {
|
||||
const baseProps = {
|
||||
children: [],
|
||||
keyboardOffset: 100,
|
||||
useLandscapeMargin: false,
|
||||
theme: Preferences.THEMES.default,
|
||||
};
|
||||
|
||||
const TEST_INSETS_1 = {
|
||||
safeAreaInsets: {
|
||||
top: 123,
|
||||
left: 123,
|
||||
bottom: 123,
|
||||
right: 123,
|
||||
},
|
||||
};
|
||||
|
||||
const TEST_INSETS_2 = {
|
||||
safeAreaInsets: {
|
||||
top: 456,
|
||||
left: 456,
|
||||
bottom: 456,
|
||||
right: 456,
|
||||
},
|
||||
};
|
||||
|
||||
const PORTRAIT_INSETS = {
|
||||
safeAreaInsets: {
|
||||
top: 111,
|
||||
left: 111,
|
||||
bottom: 111,
|
||||
right: 111,
|
||||
},
|
||||
};
|
||||
|
||||
const LANDSCAPE_INSETS = {
|
||||
safeAreaInsets: {
|
||||
top: 222,
|
||||
left: 222,
|
||||
bottom: 222,
|
||||
right: 222,
|
||||
},
|
||||
};
|
||||
|
||||
const IGNORED_INSETS = {
|
||||
safeAreaInsets: {
|
||||
top: 333,
|
||||
left: 333,
|
||||
bottom: 333,
|
||||
right: 333,
|
||||
},
|
||||
};
|
||||
|
||||
SafeArea.getSafeAreaInsetsForRootView = jest.fn().mockImplementation(() => {
|
||||
return Promise.resolve(TEST_INSETS_1);
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
EphemeralStore.safeAreaInsets = {
|
||||
[PORTRAIT]: null,
|
||||
[LANDSCAPE]: null,
|
||||
};
|
||||
});
|
||||
|
||||
test('should match snapshot', () => {
|
||||
const wrapper = shallow(
|
||||
<SafeAreaIos {...baseProps}/>,
|
||||
);
|
||||
|
||||
expect(wrapper.getElement()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('should get safe area insets on mount if DeviceTypes.IS_IPHONE_WITH_INSETS is true', async () => {
|
||||
DeviceTypes.IS_IPHONE_WITH_INSETS = true;
|
||||
mattermostManaged.hasSafeAreaInsets = false;
|
||||
|
||||
const wrapper = shallow(
|
||||
<SafeAreaIos {...baseProps}/>,
|
||||
);
|
||||
|
||||
expect(SafeArea.getSafeAreaInsetsForRootView).toHaveBeenCalled();
|
||||
await SafeArea.getSafeAreaInsetsForRootView();
|
||||
expect(wrapper.state().safeAreaInsets).toEqual(TEST_INSETS_1.safeAreaInsets);
|
||||
});
|
||||
|
||||
test('should get safe area insets on mount if mattermostManaged.hasSafeAreaInsets is true', async () => {
|
||||
DeviceTypes.IS_IPHONE_WITH_INSETS = false;
|
||||
mattermostManaged.hasSafeAreaInsets = true;
|
||||
|
||||
const wrapper = shallow(
|
||||
<SafeAreaIos {...baseProps}/>,
|
||||
);
|
||||
|
||||
expect(SafeArea.getSafeAreaInsetsForRootView).toHaveBeenCalled();
|
||||
await SafeArea.getSafeAreaInsetsForRootView();
|
||||
expect(wrapper.state().safeAreaInsets).toEqual(TEST_INSETS_1.safeAreaInsets);
|
||||
});
|
||||
|
||||
test('should not get safe area insets on mount if neither DeviceTypes.IS_IPHONE_WITH_INSET nor mattermostManaged.hasSafeAreaInsets is true', async () => {
|
||||
DeviceTypes.IS_IPHONE_WITH_INSETS = false;
|
||||
mattermostManaged.hasSafeAreaInsets = false;
|
||||
|
||||
const wrapper = shallow(
|
||||
<SafeAreaIos {...baseProps}/>,
|
||||
);
|
||||
|
||||
expect(SafeArea.getSafeAreaInsetsForRootView).not.toHaveBeenCalled();
|
||||
await SafeArea.getSafeAreaInsetsForRootView();
|
||||
expect(wrapper.state().safeAreaInsets).not.toEqual(TEST_INSETS_1.safeAreaInsets);
|
||||
});
|
||||
|
||||
test('should set safe area insets on change if mounted and DeviceTypes.IS_IPHONE_WITH_INSETS is true', async () => {
|
||||
DeviceTypes.IS_IPHONE_WITH_INSETS = true;
|
||||
mattermostManaged.hasSafeAreaInsets = false;
|
||||
|
||||
const wrapper = shallow(
|
||||
<SafeAreaIos {...baseProps}/>,
|
||||
);
|
||||
|
||||
expect(wrapper.state().safeAreaInsets).not.toEqual(TEST_INSETS_2.safeAreaInsets);
|
||||
|
||||
const instance = wrapper.instance();
|
||||
instance.onSafeAreaInsetsForRootViewChange(TEST_INSETS_2);
|
||||
expect(wrapper.state().safeAreaInsets).toEqual(TEST_INSETS_2.safeAreaInsets);
|
||||
});
|
||||
|
||||
test('should set safe area insets on change if mounted and mattermostManaged.hasSafeAreaInsets is true', async () => {
|
||||
DeviceTypes.IS_IPHONE_WITH_INSETS = false;
|
||||
mattermostManaged.hasSafeAreaInsets = true;
|
||||
|
||||
const wrapper = shallow(
|
||||
<SafeAreaIos {...baseProps}/>,
|
||||
);
|
||||
|
||||
expect(wrapper.state().safeAreaInsets).not.toEqual(TEST_INSETS_2.safeAreaInsets);
|
||||
|
||||
const instance = wrapper.instance();
|
||||
instance.onSafeAreaInsetsForRootViewChange(TEST_INSETS_2);
|
||||
expect(wrapper.state().safeAreaInsets).toEqual(TEST_INSETS_2.safeAreaInsets);
|
||||
});
|
||||
|
||||
test('should not set safe area insets on change if mounted and neither DeviceTypes.IS_IPHONE_WITH_INSETS nor mattermostManaged.hasSafeAreaInsets is true', async () => {
|
||||
DeviceTypes.IS_IPHONE_WITH_INSETS = false;
|
||||
mattermostManaged.hasSafeAreaInsets = false;
|
||||
|
||||
const wrapper = shallow(
|
||||
<SafeAreaIos {...baseProps}/>,
|
||||
);
|
||||
|
||||
expect(wrapper.state().safeAreaInsets).not.toEqual(TEST_INSETS_2.safeAreaInsets);
|
||||
|
||||
const instance = wrapper.instance();
|
||||
instance.onSafeAreaInsetsForRootViewChange(TEST_INSETS_2);
|
||||
expect(wrapper.state().safeAreaInsets).not.toEqual(TEST_INSETS_2.safeAreaInsets);
|
||||
});
|
||||
|
||||
test('should set safe area insets on change not mounted', async () => {
|
||||
DeviceTypes.IS_IPHONE_WITH_INSETS = true;
|
||||
mattermostManaged.hasSafeAreaInsets = true;
|
||||
|
||||
const wrapper = shallow(
|
||||
<SafeAreaIos {...baseProps}/>,
|
||||
);
|
||||
|
||||
expect(wrapper.state().safeAreaInsets).not.toEqual(TEST_INSETS_2.safeAreaInsets);
|
||||
|
||||
const instance = wrapper.instance();
|
||||
instance.mounted = false;
|
||||
instance.onSafeAreaInsetsForRootViewChange(TEST_INSETS_2);
|
||||
expect(wrapper.state().safeAreaInsets).not.toEqual(TEST_INSETS_2.safeAreaInsets);
|
||||
});
|
||||
|
||||
test('should set portrait safe area insets', async () => {
|
||||
const wrapper = shallow(
|
||||
<SafeAreaIos {...baseProps}/>,
|
||||
);
|
||||
|
||||
expect(wrapper.state().safeAreaInsets).not.toEqual(PORTRAIT_INSETS.safeAreaInsets);
|
||||
expect(EphemeralStore.safeAreaInsets[PORTRAIT]).toEqual(null);
|
||||
expect(EphemeralStore.safeAreaInsets[LANDSCAPE]).toEqual(null);
|
||||
|
||||
const orientation = PORTRAIT;
|
||||
const instance = wrapper.instance();
|
||||
instance.setSafeAreaInsets(PORTRAIT_INSETS.safeAreaInsets, orientation);
|
||||
expect(wrapper.state().safeAreaInsets).toEqual(PORTRAIT_INSETS.safeAreaInsets);
|
||||
expect(EphemeralStore.safeAreaInsets[PORTRAIT]).toEqual(PORTRAIT_INSETS.safeAreaInsets);
|
||||
expect(EphemeralStore.safeAreaInsets[LANDSCAPE]).toEqual(null);
|
||||
});
|
||||
|
||||
test('should set portrait safe area insets from EphemeralStore', async () => {
|
||||
const wrapper = shallow(
|
||||
<SafeAreaIos {...baseProps}/>,
|
||||
);
|
||||
|
||||
EphemeralStore.safeAreaInsets[PORTRAIT] = PORTRAIT_INSETS.safeAreaInsets;
|
||||
expect(wrapper.state().safeAreaInsets).not.toEqual(PORTRAIT_INSETS.safeAreaInsets);
|
||||
|
||||
const orientation = PORTRAIT;
|
||||
const instance = wrapper.instance();
|
||||
instance.setSafeAreaInsets(IGNORED_INSETS.safeAreaInsets, orientation);
|
||||
expect(wrapper.state().safeAreaInsets).toEqual(PORTRAIT_INSETS.safeAreaInsets);
|
||||
expect(EphemeralStore.safeAreaInsets[PORTRAIT]).toEqual(PORTRAIT_INSETS.safeAreaInsets);
|
||||
expect(EphemeralStore.safeAreaInsets[LANDSCAPE]).toEqual(null);
|
||||
});
|
||||
|
||||
test('should set landscape safe area insets', () => {
|
||||
const wrapper = shallow(
|
||||
<SafeAreaIos {...baseProps}/>,
|
||||
);
|
||||
|
||||
expect(wrapper.state().safeAreaInsets).not.toEqual(LANDSCAPE_INSETS.safeAreaInsets);
|
||||
expect(EphemeralStore.safeAreaInsets[PORTRAIT]).toEqual(null);
|
||||
expect(EphemeralStore.safeAreaInsets[LANDSCAPE]).toEqual(null);
|
||||
|
||||
const orientation = LANDSCAPE;
|
||||
const instance = wrapper.instance();
|
||||
instance.setSafeAreaInsets(LANDSCAPE_INSETS.safeAreaInsets, orientation);
|
||||
expect(wrapper.state().safeAreaInsets).toEqual(LANDSCAPE_INSETS.safeAreaInsets);
|
||||
expect(EphemeralStore.safeAreaInsets[LANDSCAPE]).toEqual(LANDSCAPE_INSETS.safeAreaInsets);
|
||||
expect(EphemeralStore.safeAreaInsets[PORTRAIT]).toEqual(null);
|
||||
});
|
||||
|
||||
test('should set landscape safe area insets from EphemeralStore', async () => {
|
||||
const wrapper = shallow(
|
||||
<SafeAreaIos {...baseProps}/>,
|
||||
);
|
||||
|
||||
EphemeralStore.safeAreaInsets[LANDSCAPE] = LANDSCAPE_INSETS.safeAreaInsets;
|
||||
expect(wrapper.state().safeAreaInsets).not.toEqual(LANDSCAPE_INSETS.safeAreaInsets);
|
||||
|
||||
const orientation = LANDSCAPE;
|
||||
const instance = wrapper.instance();
|
||||
instance.setSafeAreaInsets(IGNORED_INSETS.safeAreaInsets, orientation);
|
||||
expect(wrapper.state().safeAreaInsets).toEqual(LANDSCAPE_INSETS.safeAreaInsets);
|
||||
expect(EphemeralStore.safeAreaInsets[LANDSCAPE]).toEqual(LANDSCAPE_INSETS.safeAreaInsets);
|
||||
expect(EphemeralStore.safeAreaInsets[PORTRAIT]).toEqual(null);
|
||||
});
|
||||
|
||||
test('should add safeAreaInsetsForRootViewDidChange listener when EphemeralStore values are not set', async () => {
|
||||
const addEventListener = jest.spyOn(SafeArea, 'addEventListener');
|
||||
|
||||
expect(EphemeralStore.safeAreaInsets[PORTRAIT]).toEqual(null);
|
||||
expect(EphemeralStore.safeAreaInsets[LANDSCAPE]).toEqual(null);
|
||||
let wrapper = shallow(
|
||||
<SafeAreaIos {...baseProps}/>,
|
||||
);
|
||||
let instance = wrapper.instance();
|
||||
expect(addEventListener).toHaveBeenCalledWith('safeAreaInsetsForRootViewDidChange', instance.onSafeAreaInsetsForRootViewChange);
|
||||
addEventListener.mockClear();
|
||||
|
||||
EphemeralStore.safeAreaInsets[PORTRAIT] = TEST_INSETS_1.safeAreaInsets;
|
||||
wrapper = shallow(
|
||||
<SafeAreaIos {...baseProps}/>,
|
||||
);
|
||||
instance = wrapper.instance();
|
||||
expect(addEventListener).toHaveBeenCalledWith('safeAreaInsetsForRootViewDidChange', instance.onSafeAreaInsetsForRootViewChange);
|
||||
addEventListener.mockClear();
|
||||
|
||||
EphemeralStore.safeAreaInsets[PORTRAIT] = TEST_INSETS_1.safeAreaInsets;
|
||||
EphemeralStore.safeAreaInsets[LANDSCAPE] = TEST_INSETS_1.safeAreaInsets;
|
||||
wrapper = shallow(
|
||||
<SafeAreaIos {...baseProps}/>,
|
||||
);
|
||||
instance = wrapper.instance();
|
||||
expect(addEventListener).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('should remove safeAreaInsetsForRootViewDidChange listener when EphemeralStore values are set', async () => {
|
||||
const removeEventListener = jest.spyOn(SafeArea, 'removeEventListener');
|
||||
|
||||
const wrapper = shallow(
|
||||
<SafeAreaIos {...baseProps}/>,
|
||||
);
|
||||
const instance = wrapper.instance();
|
||||
expect(EphemeralStore.safeAreaInsets[PORTRAIT]).toEqual(null);
|
||||
expect(EphemeralStore.safeAreaInsets[LANDSCAPE]).toEqual(null);
|
||||
|
||||
instance.onSafeAreaInsetsForRootViewChange(TEST_INSETS_1);
|
||||
expect(removeEventListener).not.toHaveBeenCalled();
|
||||
|
||||
EphemeralStore.safeAreaInsets[PORTRAIT] = TEST_INSETS_1.safeAreaInsets;
|
||||
instance.onSafeAreaInsetsForRootViewChange(TEST_INSETS_1);
|
||||
expect(removeEventListener).not.toHaveBeenCalled();
|
||||
|
||||
EphemeralStore.safeAreaInsets[LANDSCAPE] = TEST_INSETS_1.safeAreaInsets;
|
||||
instance.onSafeAreaInsetsForRootViewChange(TEST_INSETS_1);
|
||||
expect(removeEventListener).toHaveBeenCalledWith('safeAreaInsetsForRootViewDidChange', instance.onSafeAreaInsetsForRootViewChange);
|
||||
});
|
||||
});
|
||||
@@ -28,10 +28,6 @@ const DRAGGING = 'Dragging';
|
||||
const SETTLING = 'Settling';
|
||||
const emptyObject = {};
|
||||
|
||||
export const DRAWER_INITIAL_OFFSET = 40;
|
||||
export const TABLET_WIDTH = 250;
|
||||
|
||||
|
||||
export default class DrawerLayout extends Component {
|
||||
static propTypes = {
|
||||
children: PropTypes.any,
|
||||
57
app/components/sidebars/drawer_layout/index.tsx
Normal file
57
app/components/sidebars/drawer_layout/index.tsx
Normal file
@@ -0,0 +1,57 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
import {useSafeAreaInsets} from 'react-native-safe-area-context';
|
||||
|
||||
import DrawerLayout from './drawer_layout';
|
||||
|
||||
export const DRAWER_INITIAL_OFFSET = 40;
|
||||
export const TABLET_WIDTH = 250;
|
||||
|
||||
interface DrawerLayoutAdapterProps {
|
||||
children: any;
|
||||
drawerBackgroundColor?: string;
|
||||
drawerLockMode?: 'unlocked' | 'locked-closed' | 'locked-open';
|
||||
drawerPosition?: 'left' | 'right';
|
||||
drawerWidth: number;
|
||||
keyboardDismissMode?: 'none' | 'on-drag';
|
||||
forwardRef: any;
|
||||
isTablet: boolean;
|
||||
onDrawerClose?: () => void;
|
||||
onDrawerOpen?: () => void;
|
||||
onDrawerSlide?: (event: {nativeEvent: {offset: number}}) => void;
|
||||
onDrawerStateChanged?: () => 'Idle' | 'Dragging' | 'Settling';
|
||||
renderNavigationView: (drawerWidth: number) => any;
|
||||
statusBarBackgroundColor?: string;
|
||||
testID?: string;
|
||||
}
|
||||
|
||||
const DrawerLayoutAdapter = (props: DrawerLayoutAdapterProps) => {
|
||||
const insets = useSafeAreaInsets();
|
||||
const horizontal = insets.left + insets.right;
|
||||
|
||||
return (
|
||||
<DrawerLayout
|
||||
drawerBackgroundColor={props.drawerBackgroundColor}
|
||||
drawerLockMode={props.drawerLockMode}
|
||||
drawerPosition={props.drawerPosition}
|
||||
drawerWidth={props.drawerWidth - horizontal}
|
||||
isTablet={props.isTablet}
|
||||
keyboardDismissMode={props.keyboardDismissMode}
|
||||
onDrawerClose={props.onDrawerClose}
|
||||
onDrawerOpen={props.onDrawerOpen}
|
||||
onDrawerSlide={props.onDrawerSlide}
|
||||
onDrawerStateChanged={props.onDrawerStateChanged}
|
||||
ref={props.forwardRef}
|
||||
renderNavigationView={props.renderNavigationView}
|
||||
statusBarBackgroundColor={props.statusBarBackgroundColor}
|
||||
testID={props.testID}
|
||||
useNativeAnimations={true}
|
||||
>
|
||||
{props.children}
|
||||
</DrawerLayout>
|
||||
);
|
||||
};
|
||||
|
||||
export default DrawerLayoutAdapter;
|
||||
@@ -1,9 +1,13 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`MainSidebar should match, full snapshot 1`] = `
|
||||
<DrawerLayout
|
||||
drawerPosition="left"
|
||||
<DrawerLayoutAdapter
|
||||
drawerWidth={710}
|
||||
forwardRef={
|
||||
Object {
|
||||
"current": null,
|
||||
}
|
||||
}
|
||||
isTablet={false}
|
||||
onDrawerClose={[Function]}
|
||||
onDrawerOpen={[Function]}
|
||||
|
||||
@@ -14,7 +14,6 @@ exports[`ChannelItem should match snapshot 1`] = `
|
||||
"height": 44,
|
||||
},
|
||||
undefined,
|
||||
null,
|
||||
]
|
||||
}
|
||||
>
|
||||
@@ -117,7 +116,6 @@ exports[`ChannelItem should match snapshot for current user i.e currentUser (you
|
||||
"height": 44,
|
||||
},
|
||||
undefined,
|
||||
null,
|
||||
]
|
||||
}
|
||||
>
|
||||
@@ -230,7 +228,6 @@ exports[`ChannelItem should match snapshot for current user i.e currentUser (you
|
||||
"height": 44,
|
||||
},
|
||||
undefined,
|
||||
null,
|
||||
]
|
||||
}
|
||||
>
|
||||
@@ -343,7 +340,6 @@ exports[`ChannelItem should match snapshot for deactivated user and is currentCh
|
||||
"height": 44,
|
||||
},
|
||||
undefined,
|
||||
null,
|
||||
]
|
||||
}
|
||||
>
|
||||
@@ -456,7 +452,6 @@ exports[`ChannelItem should match snapshot for deactivated user and is searchRes
|
||||
"height": 44,
|
||||
},
|
||||
undefined,
|
||||
null,
|
||||
]
|
||||
}
|
||||
>
|
||||
@@ -559,7 +554,6 @@ exports[`ChannelItem should match snapshot for deactivated user and not searchRe
|
||||
"height": 44,
|
||||
},
|
||||
undefined,
|
||||
null,
|
||||
]
|
||||
}
|
||||
>
|
||||
@@ -662,7 +656,6 @@ exports[`ChannelItem should match snapshot for isManualUnread 1`] = `
|
||||
"height": 44,
|
||||
},
|
||||
undefined,
|
||||
null,
|
||||
]
|
||||
}
|
||||
>
|
||||
@@ -769,7 +762,6 @@ exports[`ChannelItem should match snapshot with draft 1`] = `
|
||||
"height": 44,
|
||||
},
|
||||
undefined,
|
||||
null,
|
||||
]
|
||||
}
|
||||
>
|
||||
@@ -874,7 +866,6 @@ exports[`ChannelItem should match snapshot with mentions and muted 1`] = `
|
||||
Object {
|
||||
"opacity": 0.5,
|
||||
},
|
||||
null,
|
||||
]
|
||||
}
|
||||
>
|
||||
|
||||
@@ -11,7 +11,6 @@ import {
|
||||
import {intlShape} from 'react-intl';
|
||||
|
||||
import {General} from '@mm-redux/constants';
|
||||
import {paddingLeft as padding} from 'app/components/safe_area_view/iphone_x_spacing';
|
||||
import Badge from 'app/components/badge';
|
||||
import ChannelIcon from 'app/components/channel_icon';
|
||||
import {preventDoubleTap} from 'app/utils/tap';
|
||||
@@ -37,7 +36,6 @@ export default class ChannelItem extends PureComponent {
|
||||
unreadMsgs: PropTypes.number.isRequired,
|
||||
isSearchResult: PropTypes.bool,
|
||||
isBot: PropTypes.bool.isRequired,
|
||||
isLandscape: PropTypes.bool.isRequired,
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
@@ -78,7 +76,6 @@ export default class ChannelItem extends PureComponent {
|
||||
isSearchResult,
|
||||
channel,
|
||||
isBot,
|
||||
isLandscape,
|
||||
} = this.props;
|
||||
|
||||
// Only ever show an archived channel if it's the currently viewed channel.
|
||||
@@ -173,7 +170,7 @@ export default class ChannelItem extends PureComponent {
|
||||
underlayColor={changeOpacity(theme.sidebarTextHoverBg, 0.5)}
|
||||
onPress={this.onPress}
|
||||
>
|
||||
<View style={[style.container, mutedStyle, padding(isLandscape)]}>
|
||||
<View style={[style.container, mutedStyle]}>
|
||||
{extraBorder}
|
||||
<View style={[style.item, extraItemStyle]}>
|
||||
{icon}
|
||||
|
||||
@@ -39,7 +39,6 @@ describe('ChannelItem', () => {
|
||||
unreadMsgs: 1,
|
||||
isSearchResult: false,
|
||||
isBot: false,
|
||||
isLandscape: false,
|
||||
};
|
||||
|
||||
test('should match snapshot', () => {
|
||||
|
||||
@@ -15,9 +15,8 @@ import {getTheme, getTeammateNameDisplaySetting} from '@mm-redux/selectors/entit
|
||||
import {getCurrentUserId, getUser} from '@mm-redux/selectors/entities/users';
|
||||
import {getUserIdFromChannelName, isChannelMuted} from '@mm-redux/utils/channel_utils';
|
||||
import {displayUsername} from '@mm-redux/utils/user_utils';
|
||||
import {isLandscape} from 'app/selectors/device';
|
||||
import {getDraftForChannel} from 'app/selectors/views';
|
||||
import {isGuest as isGuestUser} from 'app/utils/users';
|
||||
import {getDraftForChannel} from '@selectors/views';
|
||||
import {isGuest as isGuestUser} from '@utils/users';
|
||||
|
||||
import ChannelItem from './channel_item';
|
||||
|
||||
@@ -82,7 +81,6 @@ function makeMapStateToProps() {
|
||||
isBot,
|
||||
isChannelMuted: isChannelMuted(member),
|
||||
isGuest,
|
||||
isLandscape: isLandscape(state),
|
||||
isManualUnread: isManuallyUnread(state, ownProps.channelId),
|
||||
mentions: member ? member.mention_count : 0,
|
||||
shouldHideChannel,
|
||||
|
||||
@@ -8,10 +8,10 @@ import {
|
||||
View,
|
||||
} from 'react-native';
|
||||
import {intlShape} from 'react-intl';
|
||||
import {SafeAreaView} from 'react-native-safe-area-context';
|
||||
|
||||
import CompassIcon from '@components/compass_icon';
|
||||
import SearchBar from '@components/search_bar';
|
||||
import {paddingLeft as padding} from '@components/safe_area_view/iphone_x_spacing';
|
||||
import {ViewTypes} from '@constants';
|
||||
import {
|
||||
changeOpacity,
|
||||
@@ -32,7 +32,6 @@ export default class ChannelsList extends PureComponent {
|
||||
onSearchStart: PropTypes.func.isRequired,
|
||||
onSelectChannel: PropTypes.func.isRequired,
|
||||
theme: PropTypes.object.isRequired,
|
||||
isLandscape: PropTypes.bool.isRequired,
|
||||
onShowTeams: PropTypes.func,
|
||||
};
|
||||
|
||||
@@ -93,11 +92,7 @@ export default class ChannelsList extends PureComponent {
|
||||
|
||||
render() {
|
||||
const {intl} = this.context;
|
||||
const {
|
||||
onShowTeams,
|
||||
theme,
|
||||
isLandscape,
|
||||
} = this.props;
|
||||
const {onShowTeams, theme} = this.props;
|
||||
|
||||
const {searching, term} = this.state;
|
||||
const styles = getStyleSheet(theme);
|
||||
@@ -130,7 +125,7 @@ export default class ChannelsList extends PureComponent {
|
||||
|
||||
const title = (
|
||||
<View
|
||||
style={[styles.searchContainer, padding(isLandscape)]}
|
||||
style={styles.searchContainer}
|
||||
>
|
||||
<SearchBar
|
||||
ref={this.setSearchBarRef}
|
||||
@@ -158,7 +153,8 @@ export default class ChannelsList extends PureComponent {
|
||||
);
|
||||
|
||||
return (
|
||||
<View
|
||||
<SafeAreaView
|
||||
edges={['left']}
|
||||
style={styles.container}
|
||||
testID='channels.list'
|
||||
>
|
||||
@@ -166,7 +162,7 @@ export default class ChannelsList extends PureComponent {
|
||||
{title}
|
||||
</View>
|
||||
{list}
|
||||
</View>
|
||||
</SafeAreaView>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user