Compare commits

...

53 Commits

Author SHA1 Message Date
Mattermost Build
15e081f572 Bump app build number to 341 (#5112) (#5114)
(cherry picked from commit 3338511355)

Co-authored-by: Miguel Alatzar <migbot@users.noreply.github.com>
2021-01-11 16:09:06 -07:00
Mattermost Build
1be535cc22 [MM-30976] fix viewing/joining archived channels using channel links (#5106) (#5113)
(cherry picked from commit aaba9fa472)

Co-authored-by: Ashish Bhate <bhate.ashish@gmail.com>
2021-01-11 15:59:14 -07:00
Mattermost Build
6a2f02be62 MM-31705 allow file local path to use multiple dots (#5109) (#5110)
* MM-31705 allow file local path to use multiple dots

* Add unit test

(cherry picked from commit 2e790212f9)

Co-authored-by: Elias Nahum <nahumhbl@gmail.com>
2021-01-11 17:04:49 -03:00
Mattermost Build
4f28e61632 MM-31873 fix race condition that prevented the device id to be registered on Android (#5104) (#5108)
(cherry picked from commit 0d590c742f)

Co-authored-by: Elias Nahum <nahumhbl@gmail.com>
2021-01-11 15:39:51 -03:00
Mattermost Build
c7cf32ebb8 [MM-31778]: fix false error message on channel join (#5098) (#5099)
(cherry picked from commit c3f86d1797)

Co-authored-by: Ashish Bhate <bhate.ashish@gmail.com>
2021-01-08 11:06:42 -07:00
Mattermost Build
142a04fac5 Bump app build number to 340 (#5096) (#5097)
(cherry picked from commit 407333b446)

Co-authored-by: Miguel Alatzar <migbot@users.noreply.github.com>
2021-01-07 15:37:36 -07:00
Mattermost Build
60b82ea5a8 Default to an empty channel object (#5094) (#5095)
(cherry picked from commit ac85110ce7)

Co-authored-by: Miguel Alatzar <migbot@users.noreply.github.com>
2021-01-07 15:18:29 -07:00
Mattermost Build
64989a728c Revert "[MM-29225] Define LSApplicationQueriesSchemes so Linking.canO… (#5067) (#5091)
* Revert "[MM-29225] Define LSApplicationQueriesSchemes so Linking.canOpenURL can work (#5007)"

This reverts commit f3baaa6aa3.

* Create and use tryOpenURL

* Add missing onError functions

* Update app/components/markdown/markdown_link/markdown_link.js

Co-authored-by: Elias Nahum <nahumhbl@gmail.com>

Co-authored-by: Elias Nahum <nahumhbl@gmail.com>
(cherry picked from commit 7702c050bf)

Co-authored-by: Miguel Alatzar <migbot@users.noreply.github.com>
2021-01-06 09:21:17 -07:00
Weblate (bot)
7317ffeb21 Translations update from Weblate (#5088)
* Translated using Weblate (Korean)

Currently translated at 100.0% (645 of 645 strings)

Translation: mattermost-languages-shipped/mattermost-mobile_master
Translate-URL: https://translate.mattermost.com/projects/mattermost/mattermost-mobile_master/ko/

* Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (645 of 645 strings)

Translation: mattermost-languages-shipped/mattermost-mobile_master
Translate-URL: https://translate.mattermost.com/projects/mattermost/mattermost-mobile_master/pt_BR/

* Translated using Weblate (Romanian)

Currently translated at 100.0% (645 of 645 strings)

Translation: mattermost-languages-shipped/mattermost-mobile_master
Translate-URL: https://translate.mattermost.com/projects/mattermost/mattermost-mobile_master/ro/

* Translated using Weblate (Ukrainian)

Currently translated at 97.9% (632 of 645 strings)

Translation: mattermost-languages-shipped/mattermost-mobile_master
Translate-URL: https://translate.mattermost.com/projects/mattermost/mattermost-mobile_master/uk/

* Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (645 of 645 strings)

Translation: mattermost-languages-shipped/mattermost-mobile_master
Translate-URL: https://translate.mattermost.com/projects/mattermost/mattermost-mobile_master/zh_Hans/

* Translated using Weblate (Japanese)

Currently translated at 100.0% (645 of 645 strings)

Translation: mattermost-languages-shipped/mattermost-mobile_master
Translate-URL: https://translate.mattermost.com/projects/mattermost/mattermost-mobile_master/ja/

Co-authored-by: Lee Dae-yeop <leedaeyeop@gmail.com>
Co-authored-by: rodrigocorsi <rodrigocorsi@gmail.com>
Co-authored-by: Viorel-Cosmin Miron <cosmin@uhlhost.net>
Co-authored-by: Ivan Novikov <monah1744@gmail.com>
Co-authored-by: aeomin <lin@aeomin.net>
Co-authored-by: kaakaa <stooner.hoe@gmail.com>
2021-01-05 14:20:42 +01:00
Mattermost Build
49031c26d4 Bump app build number to 339 (#5086) (#5087)
(cherry picked from commit 53885d2bf9)

Co-authored-by: Miguel Alatzar <migbot@users.noreply.github.com>
2021-01-04 15:53:57 -07:00
Mattermost Build
c133dab50f Bump app version number to 1.39.0 (#5084) (#5085)
(cherry picked from commit ff1901eb61)

Co-authored-by: Miguel Alatzar <migbot@users.noreply.github.com>
2021-01-04 15:44:51 -07:00
Mattermost Build
47c0ff2655 Handle go to location from CommandResponse (#4620) (#5082)
* First draft to handle go to location on mobile

* Fix lint

* Fix test

* Remove unnecessary change

* Add not handled cases

* Add i18n missing string

* Fix typo

* Extract handleGotoLocation into an action

* Fix minor issues and extract showPermalinkView to an action

* Fix minor issues and extract showPermalinkView to an action

* Add missing change

* Fix this reference

* Remove unneeded event handlers, sort imports, early handle errors, make group channel visible, remove duplications and move functions to the right place

* Fix tests

* Handle error when opening permalink

(cherry picked from commit 7bb777f4b3)

Co-authored-by: Daniel Espino García <larkox@gmail.com>
2021-01-04 15:17:51 -07:00
Mattermost Build
55fc50d7c2 Bump app build number to 338 and version to 1.38.1 (#5058) (#5060)
* 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:33 -03:00
Mattermost Build
8b0c831814 Set Tablet orientation explicitly to all (#5049) (#5054)
(cherry picked from commit 673f10770d)

Co-authored-by: Elias Nahum <nahumhbl@gmail.com>
2020-12-18 20:42:16 -03:00
Mattermost Build
971d5990e8 Fix ChannelLoader prop warning (#5055) (#5057)
* 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:55 -07:00
Mattermost Build
e91af36780 Update Rudder (#5048) (#5052)
(cherry picked from commit be75a688de)

Co-authored-by: Elias Nahum <nahumhbl@gmail.com>
2020-12-18 16:47:49 -03:00
Mattermost Build
70e5cace11 [MM-31376] Do not subtract offset from accessories container (#5042) (#5045)
* Do not subtract offset from accessories container

* Missing space

* Adjust autcomplete offsetY

* Adjust placement of autocomplete

* Space fix

* Unused onLayout

(cherry picked from commit 8fb6510a32)

Co-authored-by: Miguel Alatzar <migbot@users.noreply.github.com>
2020-12-17 19:25:06 -07:00
Joseph Baylon
7aebfd0b13 MM-30286 Detox/E2E: Add e2e test for MM-T3249 and added useful helpers (#4990)
* MM-30286 Detox/E2E: Add e2e test for MM-T3249 and added useful helper functions

* Update app/components/sidebars/main/channels_list/channels_list.js

Co-authored-by: Elias Nahum <nahumhbl@gmail.com>

* Add team icon content helper query

* Fixed reset for teams

* MM-30286 Detox/E2E: Add e2e test for MM-T3235 (#5003)

* MM-30286 Detox/E2E: Add e2e test for MM-T3255 (#5006)

* Fix merge issues

Co-authored-by: Elias Nahum <nahumhbl@gmail.com>
Co-authored-by: Mattermod <mattermod@users.noreply.github.com>
2020-12-15 17:24:45 -08:00
Weblate (bot)
8d9ed361f8 Translations update from Weblate (#5040)
* Update translation files

Updated by "Cleanup translation files" hook in Weblate.

Translation: mattermost-languages-shipped/mattermost-mobile_master
Translate-URL: https://translate.mattermost.com/projects/mattermost/mattermost-mobile_master/

Update translation files

Updated by "Cleanup translation files" hook in Weblate.

Translation: mattermost-languages-shipped/mattermost-mobile_master
Translate-URL: https://translate.mattermost.com/projects/mattermost/mattermost-mobile_master/

* Translated using Weblate (Turkish)

Currently translated at 100.0% (645 of 645 strings)

Translation: mattermost-languages-shipped/mattermost-mobile_master
Translate-URL: https://translate.mattermost.com/projects/mattermost/mattermost-mobile_master/tr/

* Translated using Weblate (Dutch)

Currently translated at 100.0% (645 of 645 strings)

Translation: mattermost-languages-shipped/mattermost-mobile_master
Translate-URL: https://translate.mattermost.com/projects/mattermost/mattermost-mobile_master/nl/

* Translated using Weblate (Spanish)

Currently translated at 100.0% (645 of 645 strings)

Translation: mattermost-languages-shipped/mattermost-mobile_master
Translate-URL: https://translate.mattermost.com/projects/mattermost/mattermost-mobile_master/es/

* Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (645 of 645 strings)

Translation: mattermost-languages-shipped/mattermost-mobile_master
Translate-URL: https://translate.mattermost.com/projects/mattermost/mattermost-mobile_master/zh_Hans/

Co-authored-by: Kaya Zeren <kayazeren@gmail.com>
Co-authored-by: Tom De Moor <tom@controlaltdieliet.be>
Co-authored-by: Elias  Nahum <elias@mattermost.com>
Co-authored-by: aeomin <lin@aeomin.net>
2020-12-14 21:49:42 +01:00
Miguel Alatzar
25d5b48db1 Bump app build number to 337 (#5035) 2020-12-11 12:05:55 -07:00
Miguel Alatzar
68d76af4dd [MM-29381] Show an indicator if channel is still loading after 10 seconds (#5031)
* 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 11:32:56 -07:00
Guillermo Vayá
0ee7b60e84 [MM-30857] request postssince on reconnect (#5000)
* [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
2020-12-10 20:49:44 -07:00
dependabot[bot]
ad6d3f42c3 Bump ini from 1.3.5 to 1.3.7 in /detox (#5023)
Bumps [ini](https://github.com/isaacs/ini) from 1.3.5 to 1.3.7.
- [Release notes](https://github.com/isaacs/ini/releases)
- [Commits](https://github.com/isaacs/ini/compare/v1.3.5...v1.3.7)

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2020-12-11 09:33:15 +08:00
A C SREEDHAR REDDY
f0598dde54 MM-18998 Add emojis in the post to recent emojis (#4986)
* add emojis available in the post to recent emojis

* made add recent reactions more performant

* lint fix

* added support for namedicons, update only if send post is success

* updated emojiUnicode function

* limit the number of recent emojis

* added e2e tests for MM-T3495

* filter out aliases

* Typo fix

* return data:true when success false when fails

Co-authored-by: Mattermod <mattermod@users.noreply.github.com>
2020-12-10 20:20:40 -03:00
Miguel Alatzar
170ef360c1 Improve upload attachment error handling (#5026) 2020-12-10 16:08:13 -07:00
Miguel Alatzar
b3796e162c [MM-31231] Fix gap between post list and draft input (#5024)
* 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
2020-12-10 19:39:15 -03:00
Elias Nahum
9abc89129b MM-31202 | MM-30866 fix: Video playback button & hide header footer on playback (#5014)
* MM-31202 | MM-30866 fix: Video playback button & hide header footer on playback

* Feedback review
2020-12-10 18:47:54 -03:00
Miguel Alatzar
7088481ac6 Check for undefined metadata images (#5021) 2020-12-10 13:55:49 -07:00
Ed Trist
c106e9f973 [MM-23391] Sort sidebar teams order by user preference (#4911)
* [MM-23391] Sort sidebar teams order by user preference

* Update imports

Co-authored-by: Elias Nahum <nahumhbl@gmail.com>

* Small changes to selector from review

* Select first ordered team in user preference on login

* Check teams length instead

Co-authored-by: Elias Nahum <nahumhbl@gmail.com>

* Return early if teamsOrder is empty

* Return sorted copy of teams instead of mutating original array

Co-authored-by: Elias Nahum <nahumhbl@gmail.com>
Co-authored-by: Mattermod <mattermod@users.noreply.github.com>
2020-12-10 17:35:05 -03:00
A C SREEDHAR REDDY
47b62daccb filtered_list : removed deprecated lifecycles and memoized dataSource (#4989)
* filtered_list: memoized dataSource instead of storing in a state

* removed deprecated lifecycles

* removed if
2020-12-10 16:37:55 -03:00
Miguel Alatzar
77bc6257ac [MM-31216] iOS - Only set badge to 0 if there are no delivered notifications or it is forced (#5015)
* Only set badge to 0 if there are no delivered notifications

* Missing semicolon
2020-12-10 12:29:47 -07:00
A C SREEDHAR REDDY
6e2936e2e9 MM-18764 Remove deprecated lifecycles (#4981)
* create_channel: remove componentWillReceiveProps

* lint fix

* list: removed componentwillreceiveprops

* removed deprecated lifecycle methods

* removed deprecated lifecycle methods
2020-12-10 13:04:32 -03:00
amwsis
20f210cb03 Typo error update in README.md (#4950) 2020-12-10 09:16:33 -03:00
Miguel Alatzar
47deea650e [MM-31177] Use png over Compass icon for logo (#5008)
* Use png over Compass icon for logo

* Update splash screen pngs

* Revert "Update splash screen pngs"

This reverts commit c79c3c1364.

* Update logo
2020-12-09 22:16:29 -07:00
Elias Nahum
b27076b06f MM-30850 fix(android): Failure to share self-uploaded file (#5010) 2020-12-09 22:14:13 -07:00
Elias Nahum
c3b3d0239f MM-31114 fix: regression for in: autocomplete modifier in search screen (#5009) 2020-12-09 22:13:23 -07:00
Elias Nahum
5874e58dd1 MM-30858 fix: follow config to show the option to copy file public link (#5011) 2020-12-09 22:11:13 -07:00
Miguel Alatzar
f3baaa6aa3 [MM-29225] Define LSApplicationQueriesSchemes so Linking.canOpenURL can work (#5007)
* Revert "[MM-29225] Linking fix (#4860)"

This reverts commit a5cb92876c.

* Define LSApplicationQueriesSchemes
2020-12-09 13:42:21 -07:00
Weblate (bot)
fc71c686b2 Translations update from Weblate (#5004)
* Translated using Weblate (Romanian)

Currently translated at 100.0% (648 of 648 strings)

Translation: mattermost-languages-shipped/mattermost-mobile_master
Translate-URL: https://translate.mattermost.com/projects/mattermost/mattermost-mobile_master/ro/

* Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (648 of 648 strings)

Translation: mattermost-languages-shipped/mattermost-mobile_master
Translate-URL: https://translate.mattermost.com/projects/mattermost/mattermost-mobile_master/zh_Hans/

Co-authored-by: Viorel-Cosmin Miron <cosmin@uhlhost.net>
Co-authored-by: aeomin <lin@aeomin.net>
2020-12-07 21:32:00 +01:00
Ashish Bhate
9a08f57155 don't hide installation errors (#4993) 2020-12-06 11:05:34 -03:00
Miguel Alatzar
6ebfe6d1c7 Bump app build number to 336 (#4998) 2020-12-02 14:34:32 -07:00
A C SREEDHAR REDDY
757a673416 MM-18769 Removed deprecated lifecycles (#4983)
* select_team : updated snapshot and removed deprecated lifecycles

* removed deprecated lifecycle methods

* Added e2e for MM-T3619

* Fix lint issue

Co-authored-by: Joseph Baylon <joseph.baylon@mattermost.com>
2020-12-02 12:37:04 -08:00
A C SREEDHAR REDDY
0360ceeb6e MM-18771 Removed componentwillreceiveprops (#4980)
* at_mention : removed componentwillreceiveprops

* channel_mention : remove componentwillreceiveprops

* slash_suggestion : remove componentWillReceiveProps
2020-12-01 14:36:56 -03:00
Joseph Baylon
04bb204191 Fix eol-last lint issues (#4996) 2020-12-01 14:18:32 -03:00
Ashish Bhate
77d63dfd96 [MM-29037]: Unit and e2e tests for MM-28100 (#4951)
* [MM-29037]: Unit and e2e tests for MM-28100

* e2e test

* review fixes

* move test to correct folder

* update to latest master

* remove console log

* skip query when not required

* re-add console.log
2020-12-01 14:01:34 -03:00
Joseph Baylon
67398d83cb Add eslint eol-last to require newline at end of files (#4985) 2020-11-30 21:46:05 -07:00
Weblate (bot)
e719f43485 Translations update from Weblate (#4992)
* Update translation files

Updated by "Cleanup translation files" hook in Weblate.

Translation: mattermost-languages-shipped/mattermost-mobile_master
Translate-URL: https://translate.mattermost.com/projects/mattermost/mattermost-mobile_master/

Update translation files

Updated by "Cleanup translation files" hook in Weblate.

Translation: mattermost-languages-shipped/mattermost-mobile_master
Translate-URL: https://translate.mattermost.com/projects/mattermost/mattermost-mobile_master/

Update translation files

Updated by "Cleanup translation files" hook in Weblate.

Translation: mattermost-languages-shipped/mattermost-mobile_master
Translate-URL: https://translate.mattermost.com/projects/mattermost/mattermost-mobile_master/

* Translated using Weblate (Korean)

Currently translated at 100.0% (649 of 649 strings)

Translation: mattermost-languages-shipped/mattermost-mobile_master
Translate-URL: https://translate.mattermost.com/projects/mattermost/mattermost-mobile_master/ko/

* Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (649 of 649 strings)

Translation: mattermost-languages-shipped/mattermost-mobile_master
Translate-URL: https://translate.mattermost.com/projects/mattermost/mattermost-mobile_master/pt_BR/

* Translated using Weblate (Russian)

Currently translated at 100.0% (649 of 649 strings)

Translation: mattermost-languages-shipped/mattermost-mobile_master
Translate-URL: https://translate.mattermost.com/projects/mattermost/mattermost-mobile_master/ru/

* Translated using Weblate (Turkish)

Currently translated at 100.0% (649 of 649 strings)

Translation: mattermost-languages-shipped/mattermost-mobile_master
Translate-URL: https://translate.mattermost.com/projects/mattermost/mattermost-mobile_master/tr/

* Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (649 of 649 strings)

Translation: mattermost-languages-shipped/mattermost-mobile_master
Translate-URL: https://translate.mattermost.com/projects/mattermost/mattermost-mobile_master/zh_Hans/

* Translated using Weblate (Romanian)

Currently translated at 100.0% (649 of 649 strings)

Translation: mattermost-languages-shipped/mattermost-mobile_master
Translate-URL: https://translate.mattermost.com/projects/mattermost/mattermost-mobile_master/ro/

Co-authored-by: Ji-Hyeon Gim <potatogim@potatogim.net>
Co-authored-by: rodrigocorsi <rodrigocorsi@gmail.com>
Co-authored-by: Alexey Napalkov <flynbit@gmail.com>
Co-authored-by: Kaya Zeren <kayazeren@gmail.com>
Co-authored-by: aeomin <lin@aeomin.net>
Co-authored-by: Viorel-Cosmin Miron <cosmin@uhlhost.net>
2020-11-27 17:57:22 +01:00
Joseph Baylon
5f6fd6df7a MM-30286 Detox/E2E: Add e2e test for MM-T3236 and added basic unit tests (#4969)
* MM-30286 Detox/E2E: Add e2e test for MM-T3236

* Fix more testID hierarchies

* Fix typo

* Fix failing test

* Remove extra lines

* Updated to use string interpolation

* Update channels list element query

* Updated channel item unit test to be more consistent

* Fix error text testID hierarchies; fix edit channel info testIDs

* Fix snap file

* Fix line return
2020-11-24 16:58:09 +08:00
Elias Nahum
dcaaaee44c MM-30164 fix safe area insets (#4979)
* 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
2020-11-23 20:10:09 -03:00
Amy Blais
05beb6c64b Update NOTICE.txt (#4976) 2020-11-23 08:22:14 -03:00
Miguel Alatzar
16bc98bbce Bump app build number to 335 (#4973) 2020-11-19 10:20:35 -07:00
Miguel Alatzar
91c08143a8 Bump app version number to 1.38.0 (#4972) 2020-11-19 09:59:46 -07:00
Elias Nahum
b226d451f3 update dependencies (#4958)
* 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
2020-11-18 19:45:07 -07:00
621 changed files with 12263 additions and 9267 deletions

View File

@@ -22,6 +22,7 @@
"__DEV__": true
},
"rules": {
"eol-last": ["error", "always"],
"global-require": 0,
"no-undefined": 0,
"react/display-name": [2, { "ignoreTranspilerName": false }],

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

@@ -63,7 +63,7 @@ We plan to add support for tablets in the future, but the timeline depends on ho
### I keep getting a message "Cannot connect to the server. Please check your server URL and internet connection."
This sometimes appears when there is an issue with the SSL certitificate configuration.
This sometimes appears when there is an issue with the SSL certificate configuration.
To check that your SSL certificate is set up correctly, test the SSL certificate by visiting a site such as https://www.ssllabs.com/ssltest/index.html. If theres an error about the missing chain or certificate path, there is likely an intermediate certificate missing that needs to be included.

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 341
versionName "1.39.0"
multiDexEnabled = true
testBuildType System.getProperty('testBuildType', 'debug')
testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner'
@@ -250,7 +250,7 @@ dependencies {
implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
implementation project(':reactnativenotifications')
implementation 'com.google.firebase:firebase-messaging:17.3.4'
implementation "com.google.firebase:firebase-messaging:$firebaseVersion"
// For animated GIF support
implementation 'com.facebook.fresco:fresco:2.0.0'

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

@@ -419,4 +419,4 @@ async function getProfilesFromPromises(promises: Array<Promise<ActionResult>>):
} catch {
return null;
}
}
}

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

@@ -213,6 +213,8 @@ export function handleSelectChannel(channelId) {
console.log('channel switch to', channel?.display_name, channelId, (Date.now() - dt), 'ms'); //eslint-disable-line
}
return {data: true};
};
}
@@ -222,7 +224,7 @@ export function handleSelectChannelByName(channelName, teamName, errorHandler) {
const {teams: currentTeams, currentTeamId} = state.entities.teams;
const currentTeam = currentTeams[currentTeamId];
const currentTeamName = currentTeam?.name;
const response = await dispatch(getChannelByNameAndTeamName(teamName || currentTeamName, channelName));
const response = await dispatch(getChannelByNameAndTeamName(teamName || currentTeamName, channelName, true));
const {error, data: channel} = response;
const currentChannelId = getCurrentChannelId(state);
@@ -249,16 +251,16 @@ export function handleSelectChannelByName(channelName, teamName, errorHandler) {
if (!myMemberships[channel.id]) {
const currentUserId = getCurrentUserId(state);
console.log('joining channel', channel?.display_name, channel.id); //eslint-disable-line
const result = await dispatch(joinChannel(currentUserId, teamName, channel.id));
const result = await dispatch(joinChannel(currentUserId, '', channel.id));
if (result.error || !result.data || !result.data.channel) {
return {error};
return result;
}
}
}
dispatch(handleSelectChannel(channel.id));
}
return null;
return {data: true};
};
}
@@ -293,6 +295,8 @@ export function markChannelViewedAndRead(channelId, previousChannelId, markOnSer
const actions = markAsViewedAndReadBatch(state, channelId, previousChannelId, markOnServer);
dispatch(batchActions(actions, 'BATCH_MARK_CHANNEL_VIEWED_AND_READ'));
return {data: true};
};
}

View File

@@ -11,6 +11,7 @@ import {ViewTypes} from '@constants';
import {ChannelTypes} from '@mm-redux/action_types';
import postReducer from '@mm-redux/reducers/entities/posts';
import initialState from '@store/initial_state';
import {General} from '@mm-redux/constants';
const {
handleSelectChannel,
@@ -67,6 +68,10 @@ describe('Actions.Views.Channel', () => {
type: MOCK_SELECT_CHANNEL_TYPE,
data: 'selected-channel-id',
});
actions.joinChannel = jest.fn((userId, teamId, channelId) => ({
type: 'MOCK_JOIN_CHANNEL',
data: {channel: {id: channelId}},
}));
const postActions = require('./post');
postActions.getPostsSince = jest.fn(() => {
return {
@@ -145,6 +150,7 @@ describe('Actions.Views.Channel', () => {
channelSelectors.getMyChannelMember = jest.fn(() => ({data: {member: {}}}));
const appChannelSelectors = require('app/selectors/channel');
const getChannelReachableOriginal = appChannelSelectors.getChannelReachable;
appChannelSelectors.getChannelReachable = jest.fn(() => true);
test('handleSelectChannelByName success', async () => {
@@ -205,6 +211,82 @@ describe('Actions.Views.Channel', () => {
expect(receivedChannel).toBe(false);
});
test('handleSelectChannelByName select channel that user is not a member of', async () => {
actions.getChannelByNameAndTeamName = jest.fn(() => {
return {
type: MOCK_RECEIVE_CHANNEL_TYPE,
data: {id: 'channel-id-3', name: 'channel-id-3', display_name: 'Test Channel', type: General.OPEN_CHANNEL},
};
});
store = mockStore(storeObj);
await store.dispatch(handleSelectChannelByName('channel-id-3', currentTeamName));
const storeActions = store.getActions();
const receivedChannel = storeActions.some((action) => action.type === MOCK_RECEIVE_CHANNEL_TYPE);
expect(receivedChannel).toBe(true);
expect(actions.joinChannel).toBeCalled();
const joinedChannel = storeActions.some((action) => action.type === 'MOCK_JOIN_CHANNEL' && action.data.channel.id === 'channel-id-3');
expect(joinedChannel).toBe(true);
});
test('handleSelectChannelByName select archived channel with ExperimentalViewArchivedChannels enabled', async () => {
const archivedChannelStoreObj = {...storeObj};
archivedChannelStoreObj.entities.general.config.ExperimentalViewArchivedChannels = 'true';
store = mockStore(archivedChannelStoreObj);
appChannelSelectors.getChannelReachable = getChannelReachableOriginal;
actions.getChannelByNameAndTeamName = jest.fn(() => {
return {
type: MOCK_RECEIVE_CHANNEL_TYPE,
data: {id: 'channel-id-3', name: 'channel-id-3', display_name: 'Test Channel', type: General.OPEN_CHANNEL, delete_at: 100},
};
});
channelSelectors.getChannelByName = jest.fn(() => {
return {
data: {id: 'channel-id-3', name: 'channel-id-3', display_name: 'Test Channel', type: General.OPEN_CHANNEL, delete_at: 100},
};
});
const errorHandler = jest.fn();
await store.dispatch(handleSelectChannelByName('channel-id-3', currentTeamName, errorHandler));
const storeActions = store.getActions();
const receivedChannel = storeActions.some((action) => action.type === MOCK_RECEIVE_CHANNEL_TYPE);
expect(receivedChannel).toBe(true);
expect(errorHandler).not.toBeCalled();
});
test('handleSelectChannelByName select archived channel with ExperimentalViewArchivedChannels disabled', async () => {
const noArchivedChannelStoreObj = {...storeObj};
noArchivedChannelStoreObj.entities.general.config.ExperimentalViewArchivedChannels = 'false';
store = mockStore(noArchivedChannelStoreObj);
appChannelSelectors.getChannelReachable = getChannelReachableOriginal;
actions.getChannelByNameAndTeamName = jest.fn(() => {
return {
type: MOCK_RECEIVE_CHANNEL_TYPE,
data: {id: 'channel-id-3', name: 'channel-id-3', display_name: 'Test Channel', type: General.OPEN_CHANNEL, delete_at: 100},
};
});
channelSelectors.getChannelByName = jest.fn(() => {
return {
data: {id: 'channel-id-3', name: 'channel-id-3', display_name: 'Test Channel', type: General.OPEN_CHANNEL, delete_at: 100},
};
});
const errorHandler = jest.fn();
await store.dispatch(handleSelectChannelByName('channel-id-3', currentTeamName, errorHandler));
const storeActions = store.getActions();
const receivedChannel = storeActions.some((action) => action.type === MOCK_RECEIVE_CHANNEL_TYPE);
expect(receivedChannel).toBe(true);
expect(errorHandler).toBeCalled();
});
test('loadPostsIfNecessaryWithRetry for the first time', async () => {
store = mockStore(storeObj);

View File

@@ -9,6 +9,8 @@ import {Client4} from '@mm-redux/client';
import {getPostIdsInCurrentChannel, makeGetPostIdsForThread} from '@mm-redux/selectors/entities/posts';
import {ViewTypes} from 'app/constants';
import {EmojiIndicesByAlias, EmojiIndicesByUnicode, Emojis} from '@utils/emojis';
import emojiRegex from 'emoji-regex';
const getPostIdsForThread = makeGetPostIdsForThread();
@@ -97,4 +99,42 @@ async function getCustomEmojiByName(name) {
}
return null;
}
}
export function addRecentUsedEmojisInMessage(message) {
return (dispatch) => {
const RE_UNICODE_EMOJI = emojiRegex();
const RE_NAMED_EMOJI = /(:([a-zA-Z0-9_-]+):)/g;
const emojis = message.match(RE_UNICODE_EMOJI);
const namedEmojis = message.match(RE_NAMED_EMOJI);
function emojiUnicode(input) {
const emoji = [];
for (const i of input) {
emoji.push(i.codePointAt(0).toString(16));
}
return emoji.join('-');
}
const emojisAvailableWithMattermost = [];
if (emojis) {
for (const emoji of emojis) {
const unicode = emojiUnicode(emoji);
const index = EmojiIndicesByUnicode.get(unicode || '');
if (index) {
emojisAvailableWithMattermost.push(Emojis[index].aliases[0]);
}
}
}
if (namedEmojis) {
for (const emoji of namedEmojis) {
const index = EmojiIndicesByAlias.get(emoji.slice(1, -1));
if (index) {
emojisAvailableWithMattermost.push(Emojis[index].aliases[0]);
}
}
}
dispatch({
type: ViewTypes.ADD_RECENT_EMOJI_ARRAY,
emojis: emojisAvailableWithMattermost,
});
};
}

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

@@ -40,6 +40,7 @@ export function showPermalink(intl: typeof intlShape, teamName: string, postId:
showModalOverCurrentContext(screen, passProps, options);
}
}
return {};
};
}
@@ -48,4 +49,4 @@ export function closePermalink() {
showingPermalink = false;
return dispatch(selectFocusedPostId(''));
};
}
}

View File

@@ -185,4 +185,4 @@ describe('Actions.Views.Post', () => {
const receivedStatuses = actionTypes.filter((type) => type === UserTypes.RECEIVED_STATUSES);
expect(receivedStatuses.length).toBe(1);
});
});
});

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

@@ -7,8 +7,10 @@ import {lastChannelIdForTeam} from '@actions/helpers/channels';
import {NavigationTypes} from '@constants';
import {ChannelTypes, TeamTypes} from '@mm-redux/action_types';
import {getMyTeams} from '@mm-redux/actions/teams';
import {RequestStatus} from '@mm-redux/constants';
import {Preferences, RequestStatus} from '@mm-redux/constants';
import {getConfig} from '@mm-redux/selectors/entities/general';
import {get as getPreference} from '@mm-redux/selectors/entities/preferences';
import {getCurrentLocale} from 'app/selectors/i18n';
import EventEmitter from '@mm-redux/utils/event_emitter';
import {selectFirstAvailableTeam} from '@utils/teams';
@@ -50,6 +52,8 @@ export function selectDefaultTeam() {
const state = getState();
const {ExperimentalPrimaryTeam} = getConfig(state);
const locale = getCurrentLocale(state);
const userTeamOrderPreference = getPreference(state, Preferences.TEAMS_ORDER, '', '');
const {teams, myMembers} = state.entities.teams;
const myTeams = Object.keys(teams).reduce((result, id) => {
if (myMembers[id]) {
@@ -59,7 +63,7 @@ export function selectDefaultTeam() {
return result;
}, []);
let defaultTeam = selectFirstAvailableTeam(myTeams, ExperimentalPrimaryTeam);
let defaultTeam = selectFirstAvailableTeam(myTeams, locale, userTeamOrderPreference, ExperimentalPrimaryTeam);
if (defaultTeam) {
dispatch(handleTeamChange(defaultTeam.id));
@@ -75,7 +79,7 @@ export function selectDefaultTeam() {
}
if (data) {
defaultTeam = selectFirstAvailableTeam(data, ExperimentalPrimaryTeam);
defaultTeam = selectFirstAvailableTeam(data, locale, userTeamOrderPreference, ExperimentalPrimaryTeam);
}
if (defaultTeam) {

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

@@ -242,4 +242,4 @@ export function setCurrentUserStatusOffline() {
}
/* eslint-disable no-import-assign */
HelperActions.forceLogoutIfNecessary = forceLogoutIfNecessary;
HelperActions.forceLogoutIfNecessary = forceLogoutIfNecessary;

View File

@@ -176,4 +176,4 @@ describe('Websocket Chanel Events', () => {
const {channels} = store.getState().entities.channels;
assert.ok(Object.keys(channels).length);
});
});
});

View File

@@ -57,4 +57,4 @@ describe('Websocket General Events', () => {
assert.ok(config.EnableCustomEmoji === 'true');
assert.ok(config.EnableLinkPreviews === 'false');
});
});
});

View File

@@ -24,4 +24,4 @@ export function handleLicenseChangedEvent(msg: WebSocketMessage): GenericAction
type: GeneralTypes.CLIENT_LICENSE_RECEIVED,
data,
};
}
}

View File

@@ -20,4 +20,4 @@ export function handleGroupUpdatedEvent(msg: WebSocketMessage) {
]));
return {data: 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

@@ -44,4 +44,4 @@ describe('Websocket Integration Events', () => {
assert.ok(dialog.trigger_id === 'sometriggerid');
assert.ok(dialog.dialog);
});
});
});

View File

@@ -11,4 +11,4 @@ export function handleOpenDialogEvent(msg: WebSocketMessage) {
dispatch({type: IntegrationTypes.RECEIVED_DIALOG, data: JSON.parse(data)});
return {data: true};
};
}
}

View File

@@ -206,4 +206,4 @@ export function handlePostUnread(msg: WebSocketMessage) {
return {data: null};
};
}
}

View File

@@ -57,4 +57,4 @@ export function handlePreferencesDeletedEvent(msg: WebSocketMessage): GenericAct
const preferences = JSON.parse(msg.data.preferences);
return {type: PreferenceTypes.DELETED_PREFERENCES, data: preferences};
}
}

View File

@@ -57,4 +57,4 @@ describe('Websocket Reaction Events', () => {
assert.ok(emojis);
assert.ok(emojis[created.id]);
});
});
});

View File

@@ -38,4 +38,4 @@ export function handleReactionRemovedEvent(msg: WebSocketMessage): GenericAction
type: PostTypes.REACTION_DELETED,
data: reaction,
};
}
}

View File

@@ -30,4 +30,4 @@ export function handleRoleUpdatedEvent(msg: WebSocketMessage): GenericAction {
type: RoleTypes.RECEIVED_ROLE,
data: role,
};
}
}

View File

@@ -103,4 +103,4 @@ describe('Websocket Team Events', () => {
const {myMembers} = store.getState().entities.teams;
assert.ifError(myMembers[team.id]);
});
});
});

View File

@@ -103,4 +103,4 @@ export function handleTeamAddedEvent(msg: WebSocketMessage) {
return {data: true};
};
}
}

View File

@@ -91,4 +91,4 @@ describe('Websocket User Events', () => {
assert.strictEqual(profiles[user.id].first_name, 'tester4');
});
});
});
});

View File

@@ -202,4 +202,4 @@ export function handleUserUpdatedEvent(msg: WebSocketMessage) {
}
return {data: true};
};
}
}

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

@@ -15,6 +15,7 @@ exports[`Badge should match snapshot 1`] = `
},
]
}
testID="badge"
>
<View
style={
@@ -70,6 +71,7 @@ exports[`Badge should match snapshot 1`] = `
},
]
}
testID="badge.unread_count"
>
99+
</Text>

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,4 +35,4 @@ const AppVersion = () => {
);
};
export default AppVersion;
export default AppVersion;

View File

@@ -41,4 +41,4 @@ describe('AtMention', () => {
wrapper.setState({user: {username: 'Victor.Welch'}});
expect(wrapper.getElement()).toMatchSnapshot();
});
});
});

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,
@@ -55,51 +54,38 @@ export default class AtMention extends PureComponent {
sections: [],
};
}
componentWillReceiveProps(nextProps) {
const {groups, inChannel, outChannel, teamMembers, isSearch, matchTerm, requestStatus} = nextProps;
// Not invoked, render nothing.
if (matchTerm === null) {
this.setState({
sections: [],
});
return;
}
if (matchTerm !== this.props.matchTerm) {
const sections = this.buildSections(nextProps);
this.setState({
sections,
});
this.props.onResultCountChange(sections.reduce((total, section) => total + section.data.length, 0));
// Update user autocomplete list with results of server request
const {currentTeamId, currentChannelId} = this.props;
const channelId = isSearch ? '' : currentChannelId;
this.props.actions.autocompleteUsers(matchTerm, currentTeamId, channelId);
return;
}
// Server request is complete
if (
groups !== this.props.groups ||
(
requestStatus !== RequestStatus.STARTED &&
(inChannel !== this.props.inChannel || outChannel !== this.props.outChannel || teamMembers !== this.props.teamMembers)
)
) {
const sections = this.buildSections(nextProps);
this.setState({
sections,
});
this.props.onResultCountChange(sections.reduce((total, section) => total + section.data.length, 0));
}
updateSections(sections) {
this.setState({sections});
}
componentDidUpdate(prevProps, prevState) {
if (this.props.matchTerm !== prevProps.matchTerm) {
if (this.props.matchTerm === null) {
this.updateSections([]);
} else {
const sections = this.buildSections(this.props);
this.updateSections(sections);
this.props.onResultCountChange(sections.reduce((total, section) => total + section.data.length, 0));
// Update user autocomplete list with results of server request
const {currentTeamId, currentChannelId} = this.props;
const channelId = this.props.isSearch ? '' : currentChannelId;
this.props.actions.autocompleteUsers(this.props.matchTerm, currentTeamId, channelId);
}
}
if (this.props.matchTerm !== null && this.props.matchTerm === prevProps.matchTerm) {
if (
this.props.groups !== prevProps.groups ||
(
this.props.requestStatus !== RequestStatus.STARTED &&
(this.props.inChannel !== prevProps.inChannel || this.props.outChannel !== prevProps.outChannel || this.props.teamMembers !== prevProps.teamMembers)
)
) {
const sections = this.buildSections(this.props);
this.updateSections(sections);
this.props.onResultCountChange(sections.reduce((total, section) => total + section.data.length, 0));
}
}
if (prevState.sections.length !== this.state.sections.length && this.state.sections.length === 0) {
this.props.onResultCountChange(0);
}
@@ -213,7 +199,6 @@ export default class AtMention extends PureComponent {
id={section.id}
defaultMessage={section.defaultMessage}
theme={this.props.theme}
isLandscape={this.props.isLandscape}
isFirstSection={isFirstSection}
/>
);

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,
};
@@ -62,15 +61,23 @@ export default class ChannelMention extends PureComponent {
this.props.actions.autocompleteChannelsForSearch(currentTeamId, matchTerm);
}, 200);
componentWillReceiveProps(nextProps) {
const {isSearch, matchTerm, myChannels, otherChannels, privateChannels, publicChannels, directAndGroupMessages, requestStatus, myMembers} = nextProps;
resetState() {
this.setState({
mentionComplete: false,
sections: [],
});
}
if ((matchTerm !== this.props.matchTerm && matchTerm === null) || this.state.mentionComplete) {
updateSections(sections) {
this.setState({sections});
}
componentDidUpdate(prevProps, prevState) {
const {isSearch, matchTerm, myChannels, otherChannels, privateChannels, publicChannels, directAndGroupMessages, requestStatus, myMembers} = this.props;
if ((matchTerm !== prevProps.matchTerm && matchTerm === null) || (this.state.mentionComplete !== prevState.mentionComplete && this.state.mentionComplete)) {
// if the term changes but is null or the mention has been completed we render this component as null
this.setState({
mentionComplete: false,
sections: [],
});
this.resetState();
this.props.onResultCountChange(0);
@@ -80,15 +87,15 @@ export default class ChannelMention extends PureComponent {
return;
}
if (matchTerm !== this.props.matchTerm) {
if (matchTerm !== prevProps.matchTerm) {
const {currentTeamId} = this.props;
this.runSearch(currentTeamId, matchTerm);
}
if (matchTerm === '' || (myChannels !== this.props.myChannels || otherChannels !== this.props.otherChannels ||
privateChannels !== this.props.privateChannels || publicChannels !== this.props.publicChannels ||
directAndGroupMessages !== this.props.directAndGroupMessages ||
myMembers !== this.props.myMembers)) {
if ((matchTerm !== prevProps.matchTerm && matchTerm === '') || (myChannels !== prevProps.myChannels || otherChannels !== prevProps.otherChannels ||
privateChannels !== prevProps.privateChannels || publicChannels !== prevProps.publicChannels ||
directAndGroupMessages !== prevProps.directAndGroupMessages ||
myMembers !== prevProps.myMembers)) {
const sections = [];
if (isSearch) {
if (publicChannels.length) {
@@ -140,9 +147,7 @@ export default class ChannelMention extends PureComponent {
}
}
this.setState({
sections,
});
this.updateSections(sections);
this.props.onResultCountChange(sections.reduce((total, section) => total + section.data.length, 0));
}
}
@@ -191,7 +196,6 @@ export default class ChannelMention extends PureComponent {
defaultMessage={section.defaultMessage}
loading={!section.hideLoadingIndicator && this.props.requestStatus === RequestStatus.STARTED}
theme={this.props.theme}
isLandscape={this.props.isLandscape}
isFirstSection={isFirstSection}
/>
);

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

@@ -72,4 +72,4 @@ describe('components/autocomplete/emoji_suggestion', () => {
expect(wrapper.state('dataSource')).toEqual(output3);
}, 100);
});
});
});

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,
@@ -49,51 +48,53 @@ export default class SlashSuggestion extends PureComponent {
lastCommandRequest: 0,
};
componentWillReceiveProps(nextProps) {
if ((nextProps.value === this.props.value && nextProps.suggestions === this.props.suggestions && nextProps.commands === this.props.commands) ||
nextProps.isSearch || nextProps.value.startsWith('//') || !nextProps.channelId) {
setActive(active) {
this.setState({active});
}
setLastCommandRequest(lastCommandRequest) {
this.setState({lastCommandRequest});
}
componentDidUpdate(prevProps) {
if ((this.props.value === prevProps.value && this.props.suggestions === prevProps.suggestions && this.props.commands === prevProps.commands) ||
this.props.isSearch || this.props.value.startsWith('//') || !this.props.channelId) {
return;
}
const {currentTeamId} = this.props;
const {currentTeamId} = prevProps;
const {
commands: nextCommands,
currentTeamId: nextTeamId,
value: nextValue,
suggestions: nextSuggestions,
} = nextProps;
} = this.props;
if (nextValue[0] !== '/') {
this.setState({
active: false,
});
this.setActive(false);
this.props.onResultCountChange(0);
return;
}
if (nextValue.indexOf(' ') === -1) { // return suggestions for a top level cached commands
if (currentTeamId !== nextTeamId) {
this.setState({
lastCommandRequest: 0,
});
this.setLastCommandRequest(0);
}
const dataIsStale = Date.now() - this.state.lastCommandRequest > TIME_BEFORE_NEXT_COMMAND_REQUEST;
if ((!nextCommands.length || dataIsStale)) {
this.props.actions.getAutocompleteCommands(nextProps.currentTeamId);
this.setState({
lastCommandRequest: Date.now(),
});
this.props.actions.getAutocompleteCommands(this.props.currentTeamId);
this.setLastCommandRequest(Date.now());
}
const matches = this.filterSlashSuggestions(nextValue.substring(1), nextCommands);
this.updateSuggestions(matches);
} else if (isMinimumServerVersion(Client4.getServerVersion(), 5, 24)) {
if (nextSuggestions === this.props.suggestions) {
if (nextSuggestions === prevProps.suggestions) {
const args = {
channel_id: this.props.channelId,
...(this.props.rootId && {root_id: this.props.rootId, parent_id: this.props.rootId}),
channel_id: prevProps.channelId,
...(prevProps.rootId && {root_id: prevProps.rootId, parent_id: prevProps.rootId}),
};
this.props.actions.getCommandAutocompleteSuggestions(nextValue, nextTeamId, args);
} else {
@@ -111,9 +112,7 @@ export default class SlashSuggestion extends PureComponent {
this.updateSuggestions(matches);
}
} else {
this.setState({
active: false,
});
this.setActive(false);
}
}
@@ -187,7 +186,6 @@ export default class SlashSuggestion extends PureComponent {
theme={this.props.theme}
suggestion={item.Suggestion}
complete={item.Complete}
isLandscape={this.props.isLandscape}
/>
)

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

@@ -21,6 +21,7 @@ export default class Badge extends PureComponent {
};
static propTypes = {
testID: PropTypes.string,
containerStyle: ViewPropTypes.style,
count: PropTypes.number.isRequired,
extraPaddingHorizontal: PropTypes.number,
@@ -108,12 +109,15 @@ export default class Badge extends PureComponent {
};
renderText = () => {
const {containerStyle, count, style} = this.props;
const {testID, containerStyle, count, style} = this.props;
const unreadCountTestID = `${testID}.unread_count`;
const unreadIndicatorID = `${testID}.unread_indicator`;
let unreadCount = null;
let unreadIndicator = null;
if (count < 0) {
unreadIndicator = (
<View
testID={unreadIndicatorID}
style={[styles.text, this.props.countStyle]}
onLayout={this.onLayout}
/>
@@ -127,6 +131,7 @@ export default class Badge extends PureComponent {
unreadCount = (
<View style={styles.verticalAlign}>
<Text
testID={unreadCountTestID}
style={[styles.text, this.props.countStyle]}
onLayout={this.onLayout}
>
@@ -137,7 +142,10 @@ export default class Badge extends PureComponent {
}
return (
<View style={[styles.badgeContainer, containerStyle]}>
<View
testID={testID}
style={[styles.badgeContainer, containerStyle]}
>
<View
ref={this.setBadgeRef}
style={[styles.badge, style, {opacity: 0}]}

View File

@@ -9,6 +9,7 @@ import Badge from './badge';
describe('Badge', () => {
const baseProps = {
testID: 'badge',
count: 100,
countStyle: {color: '#145dbf', fontSize: 10},
style: {backgroundColor: '#ffffff'},

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

@@ -6,7 +6,6 @@ import PropTypes from 'prop-types';
import {
Alert,
Animated,
Linking,
TouchableOpacity,
View,
} from 'react-native';
@@ -17,6 +16,7 @@ import FormattedText from '@components/formatted_text';
import {DeviceTypes} from '@constants';
import {checkUpgradeType, isUpgradeAvailable} from '@utils/client_upgrade';
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
import {tryOpenURL} from '@utils/url';
import {showModal, dismissModal} from '@actions/navigation';
const {View: AnimatedView} = Animated;
@@ -62,17 +62,21 @@ export default class ClientUpgradeListener extends PureComponent {
}
}
componentWillReceiveProps(nextProps) {
const {forceUpgrade, latestVersion, minVersion} = this.props;
const {latestVersion: nextLatestVersion, minVersion: nextMinVersion, lastUpgradeCheck} = nextProps;
setTop(top) {
this.setState({top});
}
componentDidUpdate(prevProps) {
const {forceUpgrade, latestVersion, minVersion} = prevProps;
const {latestVersion: nextLatestVersion, minVersion: nextMinVersion, lastUpgradeCheck} = this.props;
const versionMismatch = latestVersion !== nextLatestVersion || minVersion !== nextMinVersion;
if (versionMismatch && (forceUpgrade || Date.now() - lastUpgradeCheck > UPDATE_TIMEOUT)) {
this.checkUpgrade(minVersion, latestVersion, nextProps.isLandscape);
} else if (this.props.isLandscape !== nextProps.isLandscape &&
this.checkUpgrade(minVersion, latestVersion, this.props.isLandscape);
} else if (prevProps.isLandscape !== this.props.isLandscape &&
isUpgradeAvailable(this.state.upgradeType) && DeviceTypes.IS_IPHONE_WITH_INSETS) {
const newTop = nextProps.isLandscape ? 45 : 100;
this.setState({top: new Animated.Value(newTop)});
const newTop = this.props.isLandscape ? 45 : 100;
this.setTop(new Animated.Value(newTop));
}
}
@@ -117,7 +121,7 @@ export default class ClientUpgradeListener extends PureComponent {
const {downloadLink} = this.props;
const {intl} = this.context;
Linking.openURL(downloadLink).catch(() => {
const onError = () => {
Alert.alert(
intl.formatMessage({
id: 'mobile.client_upgrade.download_error.title',
@@ -128,7 +132,8 @@ export default class ClientUpgradeListener extends PureComponent {
defaultMessage: 'An error occurred while trying to open the download link.',
}),
);
});
};
tryOpenURL(downloadLink, onError);
this.toggleUpgradeMessage(false);
};

View File

@@ -4,4 +4,4 @@
import {createIconSetFromFontello} from 'react-native-vector-icons';
import fontelloConfig from '@assets/compass-icons.json';
export default createIconSetFromFontello(fontelloConfig, 'compass-icons', 'compass-icons.ttf');
export default createIconSetFromFontello(fontelloConfig, 'compass-icons', 'compass-icons.ttf');

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

@@ -79,4 +79,4 @@ const getStyleSheet = makeStyleSheetFromTheme((theme) => {
};
});
export default DeletedPost;
export default DeletedPost;

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}
@@ -17,22 +30,18 @@ exports[`EditChannelInfo should match snapshot 1`] = `
"flex": 1,
}
}
testID="edit_channel_info"
>
<TouchableWithoutFeedback
onPress={[Function]}
>
<View
style={
Array [
Object {
"backgroundColor": "rgba(61,60,64,0.06)",
"flex": 1,
"paddingTop": 30,
},
Object {
"height": 600,
},
]
Object {
"backgroundColor": "rgba(61,60,64,0.06)",
"flex": 1,
"paddingTop": 30,
}
}
>
<View>
@@ -41,30 +50,20 @@ exports[`EditChannelInfo should match snapshot 1`] = `
defaultMessage="Name"
id="channel_modal.name"
style={
Array [
Object {
"color": "#3d3c40",
"fontSize": 14,
"marginLeft": 15,
},
Object {
"paddingHorizontal": 44,
},
]
Object {
"color": "#3d3c40",
"fontSize": 14,
"marginLeft": 15,
}
}
/>
</View>
<View
style={
Array [
Object {
"backgroundColor": "#ffffff",
"marginTop": 10,
},
Object {
"paddingHorizontal": 44,
},
]
Object {
"backgroundColor": "#ffffff",
"marginTop": 10,
}
}
>
<TextInputWithLocalizedPlaceholder
@@ -89,7 +88,7 @@ exports[`EditChannelInfo should match snapshot 1`] = `
"paddingHorizontal": 15,
}
}
testID="edit_channel.name.input"
testID="edit_channel_info.name.input"
underlineColorAndroid="transparent"
value="display_name"
/>
@@ -98,15 +97,10 @@ exports[`EditChannelInfo should match snapshot 1`] = `
<View>
<View
style={
Array [
Object {
"flexDirection": "row",
"marginTop": 30,
},
Object {
"paddingHorizontal": 44,
},
]
Object {
"flexDirection": "row",
"marginTop": 30,
}
}
>
<FormattedText
@@ -134,15 +128,10 @@ exports[`EditChannelInfo should match snapshot 1`] = `
</View>
<View
style={
Array [
Object {
"backgroundColor": "#ffffff",
"marginTop": 10,
},
Object {
"paddingHorizontal": 44,
},
]
Object {
"backgroundColor": "#ffffff",
"marginTop": 10,
}
}
>
<TextInputWithLocalizedPlaceholder
@@ -173,7 +162,7 @@ exports[`EditChannelInfo should match snapshot 1`] = `
},
]
}
testID="edit_channel.purpose.input"
testID="edit_channel_info.purpose.input"
textAlignVertical="top"
underlineColorAndroid="transparent"
value="purpose"
@@ -184,17 +173,12 @@ exports[`EditChannelInfo should match snapshot 1`] = `
defaultMessage="Describe how this channel should be used."
id="channel_modal.descriptionHelp"
style={
Array [
Object {
"color": "rgba(61,60,64,0.5)",
"fontSize": 14,
"marginHorizontal": 15,
"marginTop": 10,
},
Object {
"paddingHorizontal": 44,
},
]
Object {
"color": "rgba(61,60,64,0.5)",
"fontSize": 14,
"marginHorizontal": 15,
"marginTop": 10,
}
}
/>
</View>
@@ -202,15 +186,10 @@ exports[`EditChannelInfo should match snapshot 1`] = `
<View
onLayout={[Function]}
style={
Array [
Object {
"flexDirection": "row",
"marginTop": 15,
},
Object {
"paddingHorizontal": 44,
},
]
Object {
"flexDirection": "row",
"marginTop": 15,
}
}
>
<FormattedText
@@ -238,15 +217,10 @@ exports[`EditChannelInfo should match snapshot 1`] = `
</View>
<View
style={
Array [
Object {
"backgroundColor": "#ffffff",
"marginTop": 10,
},
Object {
"paddingHorizontal": 44,
},
]
Object {
"backgroundColor": "#ffffff",
"marginTop": 10,
}
}
>
<TextInputWithLocalizedPlaceholder
@@ -278,7 +252,7 @@ exports[`EditChannelInfo should match snapshot 1`] = `
},
]
}
testID="edit_channel.header.input"
testID="edit_channel_info.header.input"
textAlignVertical="top"
underlineColorAndroid="transparent"
value="header"
@@ -295,17 +269,12 @@ exports[`EditChannelInfo should match snapshot 1`] = `
defaultMessage="Set text that will appear in the header of the channel beside the channel name. For example, include frequently used links by typing [Link Title](http://example.com)."
id="channel_modal.headerHelp"
style={
Array [
Object {
"color": "rgba(61,60,64,0.5)",
"fontSize": 14,
"marginHorizontal": 15,
"marginTop": 10,
},
Object {
"paddingHorizontal": 44,
},
]
Object {
"color": "rgba(61,60,64,0.5)",
"fontSize": 14,
"marginHorizontal": 15,
"marginTop": 10,
}
}
/>
</View>
@@ -342,5 +311,5 @@ exports[`EditChannelInfo should match snapshot 1`] = `
value="header"
/>
</View>
</React.Fragment>
</RNCSafeAreaView>
`;

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,10 +7,11 @@ import {shallow} from 'enzyme';
import Preferences from '@mm-redux/constants/preferences';
import Autocomplete from 'app/components/autocomplete';
import EditChannelInfo from './edit_channel_info';
import EditChannelInfo from './index';
describe('EditChannelInfo', () => {
const baseProps = {
testID: 'edit_channel_info',
theme: Preferences.THEMES.default,
deviceWidth: 400,
deviceHeight: 600,
@@ -32,7 +33,6 @@ describe('EditChannelInfo', () => {
oldChannelURL: '/team_a/channels/channel_old',
oldHeader: 'old_header',
oldPurpose: 'old_purpose',
isLandscape: true,
};
test('should match snapshot', () => {
@@ -91,4 +91,4 @@ describe('EditChannelInfo', () => {
instance.onKeyboardDidShow();
expect(instance.scrollHeaderToTop).toHaveBeenCalledTimes(2);
});
});
});

View File

@@ -1,15 +1,456 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {connect} from 'react-redux';
import React, {PureComponent} from 'react';
import PropTypes from 'prop-types';
import {
Platform,
TouchableWithoutFeedback,
View,
} from 'react-native';
import {KeyboardAwareScrollView} from 'react-native-keyboard-aware-scrollview';
import {SafeAreaView} from 'react-native-safe-area-context';
import {isLandscape} from 'app/selectors/device';
import EditChannelInfo from './edit_channel_info';
import {General} from '@mm-redux/constants';
function mapStateToProps(state) {
return {
isLandscape: isLandscape(state),
import Autocomplete from 'app/components/autocomplete';
import ErrorText from 'app/components/error_text';
import FormattedText from 'app/components/formatted_text';
import Loading from 'app/components/loading';
import StatusBar from 'app/components/status_bar';
import TextInputWithLocalizedPlaceholder from 'app/components/text_input_with_localized_placeholder';
import DEVICE from '@constants/device';
import {
changeOpacity,
makeStyleSheetFromTheme,
getKeyboardAppearanceFromTheme,
} from 'app/utils/theme';
import {t} from 'app/utils/i18n';
import {popTopScreen, dismissModal} from 'app/actions/navigation';
export default class EditChannelInfo extends PureComponent {
static propTypes = {
theme: PropTypes.object.isRequired,
channelType: PropTypes.string,
enableRightButton: PropTypes.func,
saving: PropTypes.bool.isRequired,
editing: PropTypes.bool,
error: PropTypes.oneOfType([PropTypes.string, PropTypes.object]),
displayName: PropTypes.string,
channelURL: PropTypes.string,
purpose: PropTypes.string,
header: PropTypes.string,
onDisplayNameChange: PropTypes.func,
onPurposeChange: PropTypes.func,
onHeaderChange: PropTypes.func,
oldDisplayName: PropTypes.string,
oldChannelURL: PropTypes.string,
oldHeader: PropTypes.string,
oldPurpose: PropTypes.string,
testID: PropTypes.string,
};
static defaultProps = {
editing: false,
};
constructor(props) {
super(props);
this.nameInput = React.createRef();
this.urlInput = React.createRef();
this.purposeInput = React.createRef();
this.headerInput = React.createRef();
this.scroll = React.createRef();
this.state = {
keyboardVisible: false,
keyboardPosition: 0,
};
}
blur = () => {
if (this.nameInput?.current) {
this.nameInput.current.blur();
}
// TODO: uncomment below once the channel URL field is added
// if (this.urlInput?.current) {
// this.urlInput.current.blur();
// }
if (this.purposeInput?.current) {
this.purposeInput.current.blur();
}
if (this.headerInput?.current) {
this.headerInput.current.blur();
}
if (this.scroll?.current) {
this.scroll.current.scrollTo({x: 0, y: 0, animated: true});
}
};
close = (goBack = false) => {
if (goBack) {
popTopScreen();
} else {
dismissModal();
}
};
canUpdate = (displayName, channelURL, purpose, header) => {
const {
oldDisplayName,
oldChannelURL,
oldPurpose,
oldHeader,
} = this.props;
return displayName !== oldDisplayName || channelURL !== oldChannelURL ||
purpose !== oldPurpose || header !== oldHeader;
};
enableRightButton = (enable = false) => {
this.props.enableRightButton(enable);
};
onDisplayNameChangeText = (displayName) => {
const {editing, onDisplayNameChange} = this.props;
onDisplayNameChange(displayName);
if (editing) {
const {channelURL, purpose, header} = this.props;
const canUpdate = this.canUpdate(displayName, channelURL, purpose, header);
this.enableRightButton(canUpdate);
return;
}
const displayNameExists = displayName && displayName.length >= 2;
this.props.enableRightButton(displayNameExists);
};
onPurposeChangeText = (purpose) => {
const {editing, onPurposeChange} = this.props;
onPurposeChange(purpose);
if (editing) {
const {displayName, channelURL, header} = this.props;
const canUpdate = this.canUpdate(displayName, channelURL, purpose, header);
this.enableRightButton(canUpdate);
}
};
onHeaderChangeText = (header) => {
const {editing, onHeaderChange} = this.props;
onHeaderChange(header);
if (editing) {
const {displayName, channelURL, purpose} = this.props;
const canUpdate = this.canUpdate(displayName, channelURL, purpose, header);
this.enableRightButton(canUpdate);
}
};
onHeaderLayout = ({nativeEvent}) => {
this.setState({headerPosition: nativeEvent.layout.y});
}
onKeyboardDidShow = () => {
this.setState({keyboardVisible: true});
if (this.state.headerHasFocus) {
this.setState({headerHasFocus: false});
this.scrollHeaderToTop();
}
}
onKeyboardDidHide = () => {
this.setState({keyboardVisible: false});
}
onKeyboardOffsetChanged = (keyboardPosition) => {
this.setState({keyboardPosition});
}
onHeaderFocus = () => {
if (this.state.keyboardVisible) {
this.scrollHeaderToTop();
} else {
this.setState({headerHasFocus: true});
}
};
scrollHeaderToTop = () => {
if (this.scroll.current) {
this.scroll.current.scrollTo({x: 0, y: this.state.headerPosition});
}
}
render() {
const {
theme,
channelType,
displayName,
header,
purpose,
error,
saving,
testID,
} = this.props;
const {keyboardVisible, keyboardPosition} = this.state;
const bottomStyle = {
bottom: Platform.select({
ios: keyboardPosition,
android: 0,
}),
};
const style = getStyleSheet(theme);
const displayHeaderOnly = channelType === General.DM_CHANNEL ||
channelType === General.GM_CHANNEL;
if (saving) {
return (
<View style={style.container}>
<StatusBar/>
<Loading color={theme.centerChannelColor}/>
</View>
);
}
let displayError;
if (error) {
displayError = (
<SafeAreaView
edges={['bottom', 'left', 'right']}
style={style.errorContainer}
>
<View style={style.errorWrapper}>
<ErrorText
testID='edit_channel_info.error.text'
error={error}
/>
</View>
</SafeAreaView>
);
}
return (
<SafeAreaView
edges={['bottom', 'left', 'right']}
style={style.container}
>
<StatusBar/>
<KeyboardAwareScrollView
testID={testID}
ref={this.scroll}
style={style.container}
keyboardShouldPersistTaps={'always'}
onKeyboardDidShow={this.onKeyboardDidShow}
onKeyboardDidHide={this.onKeyboardDidHide}
enableAutomaticScroll={!keyboardVisible}
>
{displayError}
<TouchableWithoutFeedback onPress={this.blur}>
<View style={style.scrollView}>
{!displayHeaderOnly && (
<View>
<View>
<FormattedText
style={style.title}
id='channel_modal.name'
defaultMessage='Name'
/>
</View>
<View style={style.inputContainer}>
<TextInputWithLocalizedPlaceholder
testID='edit_channel_info.name.input'
ref={this.nameInput}
value={displayName}
onChangeText={this.onDisplayNameChangeText}
style={style.input}
autoCapitalize='none'
autoCorrect={false}
placeholder={{id: t('channel_modal.nameEx'), defaultMessage: 'E.g.: "Bugs", "Marketing", "客户支持"'}}
placeholderTextColor={changeOpacity(theme.centerChannelColor, 0.5)}
underlineColorAndroid='transparent'
disableFullscreenUI={true}
maxLength={64}
keyboardAppearance={getKeyboardAppearanceFromTheme(theme)}
/>
</View>
</View>
)}
{!displayHeaderOnly && (
<View>
<View style={style.titleContainer30}>
<FormattedText
style={style.title}
id='channel_modal.purpose'
defaultMessage='Purpose'
/>
<FormattedText
style={style.optional}
id='channel_modal.optional'
defaultMessage='(optional)'
/>
</View>
<View style={style.inputContainer}>
<TextInputWithLocalizedPlaceholder
testID='edit_channel_info.purpose.input'
ref={this.purposeInput}
value={purpose}
onChangeText={this.onPurposeChangeText}
style={[style.input, {height: 110}]}
autoCapitalize='none'
autoCorrect={false}
placeholder={{id: t('channel_modal.purposeEx'), defaultMessage: 'E.g.: "A channel to file bugs and improvements"'}}
placeholderTextColor={changeOpacity(theme.centerChannelColor, 0.5)}
multiline={true}
blurOnSubmit={false}
textAlignVertical='top'
underlineColorAndroid='transparent'
disableFullscreenUI={true}
keyboardAppearance={getKeyboardAppearanceFromTheme(theme)}
/>
</View>
<View>
<FormattedText
style={style.helpText}
id='channel_modal.descriptionHelp'
defaultMessage='Describe how this channel should be used.'
/>
</View>
</View>
)}
<View
onLayout={this.onHeaderLayout}
style={style.titleContainer15}
>
<FormattedText
style={style.title}
id='channel_modal.header'
defaultMessage='Header'
/>
<FormattedText
style={style.optional}
id='channel_modal.optional'
defaultMessage='(optional)'
/>
</View>
<View style={style.inputContainer}>
<TextInputWithLocalizedPlaceholder
testID='edit_channel_info.header.input'
ref={this.headerInput}
value={header}
onChangeText={this.onHeaderChangeText}
style={[style.input, {height: 110}]}
autoCapitalize='none'
autoCorrect={false}
placeholder={{id: t('channel_modal.headerEx'), defaultMessage: 'E.g.: "[Link Title](http://example.com)"'}}
placeholderTextColor={changeOpacity(theme.centerChannelColor, 0.5)}
multiline={true}
blurOnSubmit={false}
onFocus={this.onHeaderFocus}
textAlignVertical='top'
underlineColorAndroid='transparent'
disableFullscreenUI={true}
keyboardAppearance={getKeyboardAppearanceFromTheme(theme)}
/>
</View>
<View style={style.headerHelpText}>
<FormattedText
style={style.helpText}
id='channel_modal.headerHelp'
defaultMessage={'Set text that will appear in the header of the channel beside the channel name. For example, include frequently used links by typing [Link Title](http://example.com).'}
/>
</View>
</View>
</TouchableWithoutFeedback>
</KeyboardAwareScrollView>
<View style={[style.autocompleteContainer, bottomStyle]}>
<Autocomplete
cursorPosition={header.length}
maxHeight={DEVICE.AUTOCOMPLETE_MAX_HEIGHT}
onChangeText={this.onHeaderChangeText}
value={header}
nestedScrollEnabled={true}
onKeyboardOffsetChanged={this.onKeyboardOffsetChanged}
offsetY={8}
style={style.autocomplete}
/>
</View>
</SafeAreaView>
);
}
}
export default connect(mapStateToProps)(EditChannelInfo);
const getStyleSheet = makeStyleSheetFromTheme((theme) => {
return {
autocomplete: {
position: undefined,
},
autocompleteContainer: {
position: 'absolute',
width: '100%',
flex: 1,
justifyContent: 'flex-end',
},
container: {
flex: 1,
},
scrollView: {
flex: 1,
backgroundColor: changeOpacity(theme.centerChannelColor, 0.06),
paddingTop: 30,
},
errorContainer: {
backgroundColor: changeOpacity(theme.centerChannelColor, 0.06),
width: '100%',
},
errorWrapper: {
justifyContent: 'center',
alignItems: 'center',
},
inputContainer: {
marginTop: 10,
backgroundColor: theme.centerChannelBg,
},
input: {
color: theme.centerChannelColor,
fontSize: 14,
height: 40,
paddingHorizontal: 15,
},
titleContainer30: {
flexDirection: 'row',
marginTop: 30,
},
titleContainer15: {
flexDirection: 'row',
marginTop: 15,
},
title: {
fontSize: 14,
color: theme.centerChannelColor,
marginLeft: 15,
},
optional: {
color: changeOpacity(theme.centerChannelColor, 0.5),
fontSize: 14,
marginLeft: 5,
},
helpText: {
fontSize: 14,
color: changeOpacity(theme.centerChannelColor, 0.5),
marginTop: 10,
marginHorizontal: 15,
},
headerHelpText: {
zIndex: -1,
},
};
});

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"
@@ -24,48 +33,46 @@ exports[`components/emoji_picker/emoji_picker.ios should match snapshot 1`] = `
"paddingVertical": 5,
}
}
testID="emoji_picker"
>
<View
style={null}
>
<Search
autoCapitalize="none"
backArrowSize={24}
backgroundColor="transparent"
blurOnSubmit={false}
cancelTitle="Cancel"
containerHeight={40}
deleteIconSize={20}
editable={true}
inputHeight={33}
inputStyle={
Object {
"backgroundColor": "#ffffff",
"color": "#3d3c40",
"fontSize": 13,
}
<Search
autoCapitalize="none"
backArrowSize={24}
backgroundColor="transparent"
blurOnSubmit={false}
cancelTitle="Cancel"
containerHeight={40}
deleteIconSize={20}
editable={true}
inputHeight={33}
inputStyle={
Object {
"backgroundColor": "#ffffff",
"color": "#3d3c40",
"fontSize": 13,
}
keyboardAppearance="light"
keyboardShouldPersist={false}
keyboardType="default"
onAnimationComplete={[Function]}
onBlur={[Function]}
onCancelButtonPress={[Function]}
onChangeText={[Function]}
onSelectionChange={[Function]}
placeholder="Search"
placeholderTextColor="rgba(61,60,64,0.5)"
returnKeyType="search"
searchBarRightMargin={0}
searchIconSize={24}
showArrow={false}
showCancel={true}
tintColorDelete="rgba(61,60,64,0.5)"
tintColorSearch="rgba(61,60,64,0.8)"
titleCancelColor="#3d3c40"
value=""
/>
</View>
}
keyboardAppearance="light"
keyboardShouldPersist={false}
keyboardType="default"
onAnimationComplete={[Function]}
onBlur={[Function]}
onCancelButtonPress={[Function]}
onChangeText={[Function]}
onSelectionChange={[Function]}
placeholder="Search"
placeholderTextColor="rgba(61,60,64,0.5)"
returnKeyType="search"
searchBarRightMargin={0}
searchIconSize={24}
showArrow={false}
showCancel={true}
testID="emoji_picker.search_bar"
tintColorDelete="rgba(61,60,64,0.5)"
tintColorSearch="rgba(61,60,64,0.8)"
titleCancelColor="#3d3c40"
value=""
/>
</View>
<View
style={
@@ -10220,5 +10227,5 @@ exports[`components/emoji_picker/emoji_picker.ios should match snapshot 1`] = `
</KeyboardTrackingView>
</View>
</KeyboardAvoidingView>
</Connect(SafeAreaIos)>
</RNCSafeAreaView>
`;

View File

@@ -12,8 +12,9 @@ import EmojiPickerBase, {getStyleSheetFromTheme} from './emoji_picker_base';
export default class EmojiPicker extends EmojiPickerBase {
render() {
const {formatMessage} = this.context.intl;
const {theme} = this.props;
const {testID, theme} = this.props;
const {searchTerm} = this.state;
const searchBarTestID = `${testID}.search_bar`;
const styles = getStyleSheetFromTheme(theme);
const searchBarInput = {
@@ -24,8 +25,12 @@ export default class EmojiPicker extends EmojiPickerBase {
return (
<React.Fragment>
<View style={styles.searchBar}>
<View
testID={testID}
style={styles.searchBar}
>
<SearchBar
testID={searchBarTestID}
ref={this.setSearchBarRef}
placeholder={formatMessage({id: 'search_bar.search', defaultMessage: 'Search'})}
cancelTitle={formatMessage({id: 'mobile.post.cancel', defaultMessage: 'Cancel'})}

View File

@@ -7,20 +7,20 @@ import {
View,
} from 'react-native';
import {KeyboardTrackingView} from 'react-native-keyboard-tracking-view';
import {SafeAreaView} from 'react-native-safe-area-context';
import SafeAreaView from 'app/components/safe_area_view';
import {paddingHorizontal as padding} from 'app/components/safe_area_view/iphone_x_spacing';
import SearchBar from 'app/components/search_bar';
import {DeviceTypes} from 'app/constants';
import {changeOpacity, getKeyboardAppearanceFromTheme} from 'app/utils/theme';
import SearchBar from '@components/search_bar';
import {DeviceTypes} from '@constants';
import {changeOpacity, getKeyboardAppearanceFromTheme} from '@utils/theme';
import EmojiPickerBase, {getStyleSheetFromTheme, SCROLLVIEW_NATIVE_ID} from './emoji_picker_base';
export default class EmojiPicker extends EmojiPickerBase {
render() {
const {formatMessage} = this.context.intl;
const {isLandscape, theme} = this.props;
const {testID, isLandscape, theme} = this.props;
const {searchTerm} = this.state;
const searchBarTestID = `${testID}.search_bar`;
const styles = getStyleSheetFromTheme(theme);
const shorten = DeviceTypes.IS_IPHONE_WITH_INSETS && isLandscape ? 6 : 2;
@@ -38,8 +38,8 @@ export default class EmojiPicker extends EmojiPickerBase {
return (
<SafeAreaView
excludeHeader={true}
excludeFooter={true}
style={{flex: 1}}
edges={['left', 'right']}
>
<KeyboardAvoidingView
behavior='padding'
@@ -47,27 +47,29 @@ export default class EmojiPicker extends EmojiPickerBase {
keyboardVerticalOffset={keyboardOffset}
style={styles.flex}
>
<View style={styles.searchBar}>
<View style={padding(isLandscape)}>
<SearchBar
ref={this.setSearchBarRef}
placeholder={formatMessage({id: 'search_bar.search', defaultMessage: 'Search'})}
cancelTitle={formatMessage({id: 'mobile.post.cancel', defaultMessage: 'Cancel'})}
backgroundColor='transparent'
inputHeight={33}
inputStyle={searchBarInput}
placeholderTextColor={changeOpacity(theme.centerChannelColor, 0.5)}
tintColorSearch={changeOpacity(theme.centerChannelColor, 0.8)}
tintColorDelete={changeOpacity(theme.centerChannelColor, 0.5)}
titleCancelColor={theme.centerChannelColor}
onChangeText={this.changeSearchTerm}
onCancelButtonPress={this.cancelSearch}
autoCapitalize='none'
value={searchTerm}
keyboardAppearance={getKeyboardAppearanceFromTheme(theme)}
onAnimationComplete={this.setRebuiltEmojis}
/>
</View>
<View
testID={testID}
style={styles.searchBar}
>
<SearchBar
testID={searchBarTestID}
ref={this.setSearchBarRef}
placeholder={formatMessage({id: 'search_bar.search', defaultMessage: 'Search'})}
cancelTitle={formatMessage({id: 'mobile.post.cancel', defaultMessage: 'Cancel'})}
backgroundColor='transparent'
inputHeight={33}
inputStyle={searchBarInput}
placeholderTextColor={changeOpacity(theme.centerChannelColor, 0.5)}
tintColorSearch={changeOpacity(theme.centerChannelColor, 0.8)}
tintColorDelete={changeOpacity(theme.centerChannelColor, 0.5)}
titleCancelColor={theme.centerChannelColor}
onChangeText={this.changeSearchTerm}
onCancelButtonPress={this.cancelSearch}
autoCapitalize='none'
value={searchTerm}
keyboardAppearance={getKeyboardAppearanceFromTheme(theme)}
onAnimationComplete={this.setRebuiltEmojis}
/>
</View>
<View style={[styles.container]}>
{this.renderListComponent(shorten)}

View File

@@ -34,6 +34,7 @@ describe('components/emoji_picker/emoji_picker.ios', () => {
const fuse = new Fuse(emojis, options);
const baseProps = {
testID: 'emoji_picker',
actions: {
getCustomEmojis: jest.fn(),
incrementEmojiPickerPage: jest.fn(),
@@ -80,18 +81,6 @@ describe('components/emoji_picker/emoji_picker.ios', () => {
expect(result).toEqual(output);
});
test('should set rebuildEmojis to true when deviceWidth changes', async () => {
const wrapper = shallowWithIntl(<EmojiPicker {...baseProps}/>);
const instance = wrapper.instance();
expect(instance.rebuildEmojis).toBe(undefined);
const newDeviceWidth = baseProps.deviceWidth * 2;
wrapper.setProps({deviceWidth: newDeviceWidth});
expect(instance.rebuildEmojis).toBe(true);
});
test('should rebuild emojis emojis when emojis change', async () => {
const wrapper = shallowWithIntl(<EmojiPicker {...baseProps}/>);
const instance = wrapper.instance();

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 {
@@ -42,6 +41,7 @@ export function filterEmojiSearchInput(searchText) {
export default class EmojiPicker extends PureComponent {
static propTypes = {
testID: PropTypes.string,
customEmojisEnabled: PropTypes.bool.isRequired,
customEmojiPage: PropTypes.number.isRequired,
deviceWidth: PropTypes.number.isRequired,
@@ -101,8 +101,9 @@ export default class EmojiPicker extends PureComponent {
if (this.props.emojis !== prevProps.emojis) {
this.rebuildEmojis = true;
this.setRebuiltEmojis();
}
this.setRebuiltEmojis();
}
setSearchBarRef = (ref) => {
@@ -231,15 +232,17 @@ export default class EmojiPicker extends PureComponent {
return Math.floor(Number(((deviceWidth - (SECTION_MARGIN * shorten)) / ((EMOJI_SIZE + 7) + (EMOJI_GUTTER * shorten)))));
};
renderItem = ({item}) => {
renderItem = ({item, section}) => {
return (
<EmojiPickerRow
key={item.key}
emojiGutter={EMOJI_GUTTER}
emojiSize={EMOJI_SIZE}
items={item.items}
onEmojiPress={this.props.onEmojiPress}
/>
<View testID={section.defaultMessage}>
<EmojiPickerRow
key={item.key}
emojiGutter={EMOJI_GUTTER}
emojiSize={EMOJI_SIZE}
items={item.items}
onEmojiPress={this.props.onEmojiPress}
/>
</View>
);
};
@@ -305,7 +308,7 @@ export default class EmojiPicker extends PureComponent {
onPress={() => this.props.onEmojiPress(item)}
style={style.flatListRow}
>
<View style={[style.flatListEmoji, padding(this.props.isLandscape)]}>
<View style={style.flatListEmoji}>
<Emoji
emojiName={item}
textStyle={style.emojiText}

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

@@ -12,13 +12,14 @@ import {makeStyleSheetFromTheme} from 'app/utils/theme';
export default class ErrorText extends PureComponent {
static propTypes = {
testID: PropTypes.string,
error: PropTypes.oneOfType([PropTypes.string, PropTypes.object]),
textStyle: CustomPropTypes.Style,
theme: PropTypes.object.isRequired,
};
render() {
const {error, textStyle, theme} = this.props;
const {testID, error, textStyle, theme} = this.props;
if (!error) {
return null;
}
@@ -29,7 +30,7 @@ export default class ErrorText extends PureComponent {
if (intl) {
return (
<FormattedText
testID='error.text'
testID={testID}
id={intl.id}
defaultMessage={intl.defaultMessage}
values={intl.values}
@@ -40,7 +41,7 @@ export default class ErrorText extends PureComponent {
return (
<Text
testID='error.text'
testID={testID}
style={[GlobalStyles.errorLabel, style.errorLabel, textStyle]}
>
{error.message || error}

View File

@@ -9,6 +9,7 @@ import ErrorText from './error_text.js';
describe('ErrorText', () => {
const baseProps = {
testID: 'error.text',
textStyle: {
fontSize: 14,
marginHorizontal: 15,

View File

@@ -52,4 +52,4 @@ export default class ImageViewPort extends PureComponent {
render() {
return null;
}
}
}

View File

@@ -160,13 +160,24 @@ export default class Markdown extends PureComponent {
renderText = ({context, literal}) => {
if (context.indexOf('image') !== -1) {
// If this text is displayed, it will be styled by the image component
return <Text>{literal}</Text>;
return (
<Text testID='markdown_text'>
{literal}
</Text>
);
}
// Construct the text style based off of the parents of this node since RN's inheritance is limited
const style = this.computeTextStyle(this.props.baseTextStyle, context);
return <Text style={style}>{literal}</Text>;
return (
<Text
testID='markdown_text'
style={style}
>
{literal}
</Text>
);
};
renderCodeSpan = ({context, literal}) => {

View File

@@ -6,7 +6,6 @@ import React from 'react';
import {intlShape} from 'react-intl';
import {
Alert,
Linking,
Platform,
StyleSheet,
Text,
@@ -25,7 +24,7 @@ import EphemeralStore from '@store/ephemeral_store';
import BottomSheet from '@utils/bottom_sheet';
import {generateId} from '@utils/file';
import {calculateDimensions, getViewPortWidth, isGifTooLarge, openGalleryAtIndex} from '@utils/images';
import {normalizeProtocol} from '@utils/url';
import {normalizeProtocol, tryOpenURL} from '@utils/url';
import mattermostManaged from 'app/mattermost_managed';
@@ -126,7 +125,7 @@ export default class MarkdownImage extends ImageViewPort {
const url = normalizeProtocol(this.props.linkDestination);
const {intl} = this.context;
Linking.openURL(url).catch(() => {
const onError = () => {
Alert.alert(
intl.formatMessage({
id: 'mobile.link.error.title',
@@ -137,7 +136,9 @@ export default class MarkdownImage extends ImageViewPort {
defaultMessage: 'Unable to open the link.',
}),
);
});
};
tryOpenURL(url, onError);
};
handleLinkLongPress = async () => {

View File

@@ -3,20 +3,19 @@
import React, {Children, PureComponent} from 'react';
import PropTypes from 'prop-types';
import {Alert, Linking, Text} from 'react-native';
import {Alert, Text} from 'react-native';
import Clipboard from '@react-native-community/clipboard';
import urlParse from 'url-parse';
import {intlShape} from 'react-intl';
import urlParse from 'url-parse';
import Config from '@assets/config';
import {DeepLinkTypes} from '@constants';
import CustomPropTypes from '@constants/custom_prop_types';
import {getCurrentServerUrl} from '@init/credentials';
import BottomSheet from '@utils/bottom_sheet';
import {alertErrorWithFallback} from '@utils/general';
import {t} from '@utils/i18n';
import {errorBadChannel} from '@utils/draft';
import {preventDoubleTap} from '@utils/tap';
import {matchDeepLink, normalizeProtocol} from '@utils/url';
import {matchDeepLink, normalizeProtocol, tryOpenURL} from '@utils/url';
import mattermostManaged from 'app/mattermost_managed';
@@ -59,37 +58,30 @@ export default class MarkdownLink extends PureComponent {
if (match) {
if (match.type === DeepLinkTypes.CHANNEL) {
this.props.actions.handleSelectChannelByName(match.channelName, match.teamName, this.errorBadChannel);
const {intl} = this.context;
this.props.actions.handleSelectChannelByName(match.channelName, match.teamName, errorBadChannel.bind(null, intl));
} else if (match.type === DeepLinkTypes.PERMALINK) {
onPermalinkPress(match.postId, match.teamName);
}
} else {
Linking.openURL(url).catch(() => {
const onError = () => {
const {formatMessage} = this.context.intl;
Alert.alert(
formatMessage({
id: 'mobile.server_link.error.title',
defaultMessage: 'Link Error',
id: 'mobile.link.error.title',
defaultMessage: 'Error',
}),
formatMessage({
id: 'mobile.server_link.error.text',
defaultMessage: 'The link could not be found on this server.',
id: 'mobile.link.error.text',
defaultMessage: 'Unable to open the link.',
}),
);
});
};
tryOpenURL(url, onError);
}
});
errorBadChannel = () => {
const {intl} = this.context;
const message = {
id: t('mobile.server_link.unreachable_channel.error'),
defaultMessage: 'This link belongs to a deleted channel or to a channel to which you do not have access.',
};
alertErrorWithFallback(intl, {}, message);
};
parseLinkLiteral = (literal) => {
let nextLiteral = literal;

Some files were not shown because too many files have changed in this diff Show More