Compare commits

...

28 Commits

Author SHA1 Message Date
Mattermost Build
6efa04cd19 Bump app build number to 338 and version to 1.38.1 (#5058) (#5059)
* Bump app build number to 338

* Bump app version number to 1.38.1

* Update fastlane

(cherry picked from commit 367534df12)

Co-authored-by: Elias Nahum <nahumhbl@gmail.com>
2020-12-18 21:06:13 -03:00
Mattermost Build
725225d77e Set Tablet orientation explicitly to all (#5049) (#5053)
(cherry picked from commit 673f10770d)

Co-authored-by: Elias Nahum <nahumhbl@gmail.com>
2020-12-18 20:41:47 -03:00
Mattermost Build
a31f2acd78 Fix ChannelLoader prop warning (#5055) (#5056)
* Fix ChannelLoader prop warning

* Missing semicolon

(cherry picked from commit f577685264)

Co-authored-by: Miguel Alatzar <migbot@users.noreply.github.com>
2020-12-18 13:57:52 -07:00
Mattermost Build
1c4aeece20 Update Rudder (#5048) (#5051)
(cherry picked from commit be75a688de)

Co-authored-by: Elias Nahum <nahumhbl@gmail.com>
2020-12-18 16:43:57 -03:00
Miguel Alatzar
de0e7ca142 [MM-31376] Do not subtract offset from accessories container (#5042) (#5050)
* Do not subtract offset from accessories container

* Missing space

* Adjust autcomplete offsetY

* Adjust placement of autocomplete

* Space fix

* Unused onLayout
2020-12-18 16:42:06 -03:00
Weblate (bot)
d43f619f40 Translations update from Weblate (#5041)
* Update translation files

Updated by "Cleanup translation files" hook in Weblate.

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Translate-URL: https://translate.mattermost.com/projects/mattermost/mattermost-mobile_release-1-38/
Translation: mattermost-languages-shipped/mattermost-mobile_release-1.38

* Update translation files

Updated by "Cleanup translation files" hook in Weblate.

Translation: mattermost-languages-shipped/mattermost-mobile_release-1.38
Translate-URL: https://translate.mattermost.com/projects/mattermost/mattermost-mobile_release-1-38/

* Translated using Weblate (Turkish)

Currently translated at 100.0% (645 of 645 strings)

Translation: mattermost-languages-shipped/mattermost-mobile_release-1.38
Translate-URL: https://translate.mattermost.com/projects/mattermost/mattermost-mobile_release-1-38/tr/

* Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (645 of 645 strings)

Translation: mattermost-languages-shipped/mattermost-mobile_release-1.38
Translate-URL: https://translate.mattermost.com/projects/mattermost/mattermost-mobile_release-1-38/zh_Hans/

* Translated using Weblate (Dutch)

Currently translated at 100.0% (645 of 645 strings)

Translation: mattermost-languages-shipped/mattermost-mobile_release-1.38
Translate-URL: https://translate.mattermost.com/projects/mattermost/mattermost-mobile_release-1-38/nl/

* Translated using Weblate (Spanish)

Currently translated at 100.0% (645 of 645 strings)

Translation: mattermost-languages-shipped/mattermost-mobile_release-1.38
Translate-URL: https://translate.mattermost.com/projects/mattermost/mattermost-mobile_release-1-38/es/

Co-authored-by: Kaya Zeren <kayazeren@gmail.com>
Co-authored-by: aeomin <lin@aeomin.net>
Co-authored-by: Tom De Moor <tom@controlaltdieliet.be>
Co-authored-by: Elias  Nahum <elias@mattermost.com>
2020-12-14 23:21:29 +01:00
Mattermost Build
d5dfa05cdb Bump app build number to 337 (#5035) (#5036)
(cherry picked from commit 25d5b48db1)

Co-authored-by: Miguel Alatzar <migbot@users.noreply.github.com>
2020-12-11 12:14:33 -07:00
Miguel Alatzar
cc5ff8143b [MM-29381] Show an indicator if channel is still loading after 10 seconds (#5031) (#5034)
* Show still-loading indicator

* Update snapshot test

* Call retryLoadChannels on a 10 second interval

* Select default team on retryLoad if no currentTeamId

* Fix use of jest.useFakeTimers
2020-12-11 15:52:32 -03:00
Mattermost Build
f1b614c386 [MM-30857] request postssince on reconnect (#5000) (#5032)
* [MM-30857] request postssince on reconnect

* [MM-30857] add test

* Address CR, add debounce in case of unstable connection

* remove leftovers

* Address review comments

* remove previous changes

* fix tests

(cherry picked from commit 0ee7b60e84)

Co-authored-by: Guillermo Vayá <guillermo.vaya@mattermost.com>
2020-12-10 20:59:24 -07:00
Mattermost Build
64b0d1602b Improve upload attachment error handling (#5026) (#5030)
(cherry picked from commit 170ef360c1)

Co-authored-by: Miguel Alatzar <migbot@users.noreply.github.com>
2020-12-10 16:34:11 -07:00
Mattermost Build
dc70abaf89 [MM-31231] Fix gap between post list and draft input (#5024) (#5029)
* Update keyboard tracking view patch

* Always subtract bottomSafeArea if shown or will show

* Subtract bottomSafeArea if height is not 0 and is shown or will show

(cherry picked from commit b3796e162c)

Co-authored-by: Miguel Alatzar <migbot@users.noreply.github.com>
2020-12-10 15:51:13 -07:00
Mattermost Build
29a12c152a MM-31202 | MM-30866 fix: Video playback button & hide header footer on playback (#5014) (#5027)
* MM-31202 | MM-30866 fix: Video playback button & hide header footer on playback

* Feedback review

(cherry picked from commit 9abc89129b)

Co-authored-by: Elias Nahum <nahumhbl@gmail.com>
2020-12-10 19:06:55 -03:00
Mattermost Build
20496f5a4f Check for undefined metadata images (#5021) (#5025)
(cherry picked from commit 7088481ac6)

Co-authored-by: Miguel Alatzar <migbot@users.noreply.github.com>
2020-12-10 14:50:08 -07:00
Mattermost Build
0b7d16c7e4 [MM-31216] iOS - Only set badge to 0 if there are no delivered notifications or it is forced (#5015) (#5022)
* Only set badge to 0 if there are no delivered notifications

* Missing semicolon

(cherry picked from commit 77bc6257ac)

Co-authored-by: Miguel Alatzar <migbot@users.noreply.github.com>
2020-12-10 12:44:41 -07:00
Mattermost Build
550498bfc0 MM-30858 fix: follow config to show the option to copy file public link (#5011) (#5016)
(cherry picked from commit 5874e58dd1)

Co-authored-by: Elias Nahum <nahumhbl@gmail.com>
2020-12-10 09:13:35 -03:00
Mattermost Build
37fb33c6a7 MM-31114 fix: regression for in: autocomplete modifier in search screen (#5009) (#5017)
(cherry picked from commit c3b3d0239f)

Co-authored-by: Elias Nahum <nahumhbl@gmail.com>
2020-12-10 09:13:04 -03:00
Mattermost Build
976d2b5fe3 MM-30850 fix(android): Failure to share self-uploaded file (#5010) (#5018)
(cherry picked from commit b27076b06f)

Co-authored-by: Elias Nahum <nahumhbl@gmail.com>
2020-12-10 09:12:30 -03:00
Mattermost Build
84918a666d [MM-31177] Use png over Compass icon for logo (#5008) (#5019)
* Use png over Compass icon for logo

* Update splash screen pngs

* Revert "Update splash screen pngs"

This reverts commit c79c3c1364.

* Update logo

(cherry picked from commit 47deea650e)

Co-authored-by: Miguel Alatzar <migbot@users.noreply.github.com>
2020-12-09 22:28:12 -07:00
Mattermost Build
39fb6af758 [MM-29225] Define LSApplicationQueriesSchemes so Linking.canOpenURL can work (#5007) (#5013)
* Revert "[MM-29225] Linking fix (#4860)"

This reverts commit a5cb92876c.

* Define LSApplicationQueriesSchemes

(cherry picked from commit f3baaa6aa3)

Co-authored-by: Miguel Alatzar <migbot@users.noreply.github.com>
2020-12-09 14:32:08 -07:00
Weblate (bot)
169866e0db Translations update from Weblate (#5005)
* Translated using Weblate (Romanian)

Currently translated at 100.0% (648 of 648 strings)

Co-authored-by: Viorel-Cosmin Miron <cosmin@uhlhost.net>
Translate-URL: https://translate.mattermost.com/projects/mattermost/mattermost-mobile_release-1-38/ro/
Translation: mattermost-languages-shipped/mattermost-mobile_release-1.38

* Translated using Weblate (Romanian)

Currently translated at 100.0% (648 of 648 strings)

Co-authored-by: Elisabeth Kulzer <elisabeth.kulzer@mattermost.com>
Translate-URL: https://translate.mattermost.com/projects/mattermost/mattermost-mobile_release-1-38/ro/
Translation: mattermost-languages-shipped/mattermost-mobile_release-1.38

* Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (648 of 648 strings)

Co-authored-by: aeomin <lin@aeomin.net>
Translate-URL: https://translate.mattermost.com/projects/mattermost/mattermost-mobile_release-1-38/zh_Hans/
Translation: mattermost-languages-shipped/mattermost-mobile_release-1.38

Co-authored-by: Viorel-Cosmin Miron <cosmin@uhlhost.net>
Co-authored-by: Elisabeth Kulzer <elisabeth.kulzer@mattermost.com>
Co-authored-by: aeomin <lin@aeomin.net>
2020-12-07 21:32:44 +01:00
Mattermost Build
fcf33ccc46 Bump app build number to 336 (#4998) (#4999)
(cherry picked from commit 6ebfe6d1c7)

Co-authored-by: Miguel Alatzar <migbot@users.noreply.github.com>
2020-12-02 14:48:03 -07:00
Weblate (bot)
a074289c3f Translated using Weblate (Portuguese (Brazil)) (#4995)
Currently translated at 100.0% (648 of 648 strings)

Co-authored-by: rodrigocorsi <rodrigocorsi@gmail.com>
Translate-URL: https://translate.mattermost.com/projects/mattermost/mattermost-mobile_release-1-38/pt_BR/
Translation: mattermost-languages-shipped/mattermost-mobile_release-1.38

Co-authored-by: rodrigocorsi <rodrigocorsi@gmail.com>
2020-12-01 15:10:16 +01:00
Weblate (bot)
0ab0c9b85b Translations update from Weblate (#4991)
* Update translation files

Updated by "Cleanup translation files" hook in Weblate.

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Translate-URL: https://translate.mattermost.com/projects/mattermost/mattermost-mobile_release-1-38/
Translation: mattermost-languages-shipped/mattermost-mobile_release-1.38

* Update translation files

Updated by "Cleanup translation files" hook in Weblate.

Translation: mattermost-languages-shipped/mattermost-mobile_release-1.38
Translate-URL: https://translate.mattermost.com/projects/mattermost/mattermost-mobile_release-1-38/

* Translated using Weblate (Romanian)

Currently translated at 100.0% (648 of 648 strings)

Translation: mattermost-languages-shipped/mattermost-mobile_release-1.38
Translate-URL: https://translate.mattermost.com/projects/mattermost/mattermost-mobile_release-1-38/ro/

* Translated using Weblate (Turkish)

Currently translated at 100.0% (648 of 648 strings)

Translation: mattermost-languages-shipped/mattermost-mobile_release-1.38
Translate-URL: https://translate.mattermost.com/projects/mattermost/mattermost-mobile_release-1-38/tr/

* Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (648 of 648 strings)

Translation: mattermost-languages-shipped/mattermost-mobile_release-1.38
Translate-URL: https://translate.mattermost.com/projects/mattermost/mattermost-mobile_release-1-38/zh_Hans/

Co-authored-by: Viorel-Cosmin Miron <cosmin@uhlhost.net>
Co-authored-by: Kaya Zeren <kayazeren@gmail.com>
Co-authored-by: aeomin <lin@aeomin.net>
2020-11-27 17:47:51 +01:00
Mattermost Build
8899c586ab MM-30164 fix safe area insets (#4979) (#4984)
* MM-30164 fix safe area insets

* Fix unit test setup mock for react-native-device-info

* Add insets for edit profile screen

* Fix about screen

* Fix theme screen

* Lock phone screen to portrait

* fix unit tests

* Fix autocomplete layout

(cherry picked from commit dcaaaee44c)

Co-authored-by: Elias Nahum <nahumhbl@gmail.com>
2020-11-23 21:24:06 -03:00
Mattermost Build
f5e89c8eab Update NOTICE.txt (#4976) (#4982)
(cherry picked from commit 05beb6c64b)

Co-authored-by: Amy Blais <amy_blais@hotmail.com>
2020-11-23 09:18:10 -03:00
Mattermost Build
6b8008f080 Bump app build number to 335 (#4973) (#4975)
(cherry picked from commit 16bc98bbce)

Co-authored-by: Miguel Alatzar <migbot@users.noreply.github.com>
2020-11-19 10:31:49 -07:00
Mattermost Build
05aa56b2e1 Bump app version number to 1.38.0 (#4972) (#4974)
(cherry picked from commit 91c08143a8)

Co-authored-by: Miguel Alatzar <migbot@users.noreply.github.com>
2020-11-19 10:20:41 -07:00
Mattermost Build
b5b2310f33 update dependencies (#4958) (#4968)
* update dependencies

* revert keychain update

* Update dependencies & Fastlane

* set path agnostic for bash in scrips

* Fix open from push notification race

* patch react-native-localize

(cherry picked from commit b226d451f3)

Co-authored-by: Elias Nahum <nahumhbl@gmail.com>
2020-11-19 13:37:43 -03:00
415 changed files with 7378 additions and 8511 deletions

View File

@@ -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.

View File

@@ -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'

View File

@@ -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"

View File

@@ -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;
}

View File

@@ -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

View File

@@ -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'

View File

@@ -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() || {};

View File

@@ -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};
};
}

View File

@@ -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,
},
});
}

View File

@@ -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};
};
}

View File

@@ -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,
},
});
}

View File

@@ -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));
}
}

View File

@@ -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();

View File

@@ -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",
}
}
/>
`;

View File

@@ -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',

View File

@@ -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}/>,
);

View File

@@ -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),
};
}

View File

@@ -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}
/>
);

View File

@@ -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,
};

View File

@@ -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;

View File

@@ -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;

View File

@@ -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,
};
}

View File

@@ -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}

View File

@@ -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;

View File

@@ -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}
/>
);

View File

@@ -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),
};
}

View File

@@ -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;

View File

@@ -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),
};
}

View File

@@ -17,6 +17,4 @@ function mapStateToProps(state) {
};
}
export const AUTOCOMPLETE_MAX_HEIGHT = 200;
export default connect(mapStateToProps, null, null, {forwardRef: true})(Autocomplete);

View File

@@ -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),
};
}

View File

@@ -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}
/>
)

View File

@@ -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;

View File

@@ -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>
);
}

View File

@@ -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),
};
}

View File

@@ -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>

View File

@@ -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),
};
};

View File

@@ -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 [

View File

@@ -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,
},
};
});

View File

@@ -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);
});
});

View File

@@ -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),
};
}

View File

@@ -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);

View File

@@ -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

View File

@@ -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}>

View File

@@ -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),
};
};
}

View File

@@ -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)]}>

View File

@@ -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>
);

View File

@@ -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),
};
}

View File

@@ -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>

View File

@@ -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

View File

@@ -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),
};
}

View File

@@ -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}>

View File

@@ -29,7 +29,6 @@ describe('UserListRow', () => {
},
theme: Preferences.THEMES.default,
teammateNameDisplay: 'test',
isLandscape: false,
};
test('should match snapshot', () => {

View File

@@ -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>
`;

View File

@@ -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,
},
};
});

View File

@@ -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', () => {

View File

@@ -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,
},
};
});

View File

@@ -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>
`;

View File

@@ -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)}

View File

@@ -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();

View File

@@ -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}

View File

@@ -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',

View File

@@ -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);
}
});
};

View File

@@ -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.',
}),
);
}
});
}
});

View File

@@ -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);
}
};

View File

@@ -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);
}
};

View File

@@ -20,6 +20,12 @@ exports[`AttachmentFooter matches snapshot 1`] = `
}
>
<ForwardRef(AnimatedComponentWrapper)
edges={
Array [
"left",
"right",
]
}
style={
Object {
"alignItems": "center",

View File

@@ -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>
);
}

View File

@@ -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),
};

View File

@@ -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}

View File

@@ -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;

View File

@@ -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();
});
});

View File

@@ -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>
`;

View File

@@ -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>
</>
);
}

View File

@@ -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;

View File

@@ -17,7 +17,6 @@ describe('PostInput', () => {
handleCommentDraftChanged: jest.fn(),
handlePostDraftChanged: jest.fn(),
inputEventType: '',
isLandscape: false,
maxMessageLength: 4000,
onPasteFiles: jest.fn(),
onSend: jest.fn(),

View File

@@ -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>
`;

View File

@@ -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'

View File

@@ -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>
);
}

View File

@@ -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();
}
}
}

View File

@@ -18,7 +18,7 @@ exports[`MoreMessagesButton should match snapshot 1`] = `
Object {
"transform": Array [
Object {
"translateY": -138,
"translateY": -438,
},
],
},

View File

@@ -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;

View File

@@ -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

View File

@@ -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),
};
}

View File

@@ -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),

View File

@@ -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,

View File

@@ -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>
`;

View File

@@ -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 {

View File

@@ -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;
};

View 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;

View File

@@ -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;

View File

@@ -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>
);
}
}

View File

@@ -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);
});
});

View File

@@ -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,

View 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;

View File

@@ -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]}

View File

@@ -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,
]
}
>

View File

@@ -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}

View File

@@ -39,7 +39,6 @@ describe('ChannelItem', () => {
unreadMsgs: 1,
isSearchResult: false,
isBot: false,
isLandscape: false,
};
test('should match snapshot', () => {

View File

@@ -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,

View File

@@ -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