Compare commits

...

66 Commits

Author SHA1 Message Date
Mattermost Build
06c6139ed6 Bump app build number to 326 (#4825) (#4826)
(cherry picked from commit 8abee342ff)

Co-authored-by: Miguel Alatzar <migbot@users.noreply.github.com>
2020-09-21 12:54:17 -07:00
Mattermost Build
4f4ed42c32 Bump app version number to 1.35.1 (#4823) (#4824)
(cherry picked from commit 337287d69e)

Co-authored-by: Miguel Alatzar <migbot@users.noreply.github.com>
2020-09-21 12:32:56 -07:00
Mattermost Build
20e3ee14ec Fix getPostThread import (#4819) (#4820)
(cherry picked from commit de1f38b839)

Co-authored-by: Miguel Alatzar <migbot@users.noreply.github.com>
2020-09-21 14:47:00 -03:00
Mattermost Build
69a20e3299 Bump app build number to 325 (#4802) (#4804)
(cherry picked from commit 1b38cafee6)

Co-authored-by: Elias Nahum <nahumhbl@gmail.com>
2020-09-10 17:04:43 -03:00
Mattermost Build
209f650b7c Fix iOS AppSetting timeout (#4801) (#4803)
(cherry picked from commit 102445db66)

Co-authored-by: Elias Nahum <nahumhbl@gmail.com>
2020-09-10 17:01:43 -03:00
Mattermost Build
558897c75a Bump app build number to 324 (#4799) (#4800)
(cherry picked from commit d44d3bcbe6)

Co-authored-by: Elias Nahum <nahumhbl@gmail.com>
2020-09-10 15:48:55 -03:00
Mattermost Build
e6401da017 AppConfig setting to control the default request timeout (#4797) (#4798)
* AppConfig setting to control the default request timeout

* Set default timeout to 10s

(cherry picked from commit bab1cc0601)

Co-authored-by: Elias Nahum <nahumhbl@gmail.com>
2020-09-10 15:20:41 -03:00
Mattermost Build
ff29788749 Bump app build number to 323 (#4791) (#4795)
(cherry picked from commit cab0b4fafa)

Co-authored-by: Miguel Alatzar <migbot@users.noreply.github.com>
2020-09-09 13:48:37 -03:00
Mattermost Build
7729cf2d10 MM-28295 Do not remove the Channel screen from the stack tracking (#4793) (#4794)
* Do not remove the Channel screen from the stack tracking

* Reset stack for select server & when no teams are present

(cherry picked from commit 96bf1db243)

Co-authored-by: Elias Nahum <nahumhbl@gmail.com>
2020-09-09 13:33:27 -03:00
Mattermost Build
4c621890b0 Remove promoGraphic.png (#4789) (#4790)
(cherry picked from commit 7bd757ad15)

Co-authored-by: Miguel Alatzar <migbot@users.noreply.github.com>
2020-09-09 00:54:19 -07:00
Mattermost Build
c418703dba Bump app build number to 322 (#4787) (#4788)
(cherry picked from commit 40fd9552d5)

Co-authored-by: Miguel Alatzar <migbot@users.noreply.github.com>
2020-09-08 21:58:45 -07:00
Mattermost Build
22dfaf1e0f MM-27132 Fix groups not actually being highlighted for self on mobile (#4776) (#4786)
(cherry picked from commit b8a002683f)

Co-authored-by: Farhan Munshi <3207297+fmunshi@users.noreply.github.com>
2020-09-08 21:29:41 -07:00
Mattermost Build
937209c6d7 Check team count if currentTeamId is set (#4781) (#4784)
(cherry picked from commit ef583d4fb6)

Co-authored-by: Miguel Alatzar <migbot@users.noreply.github.com>
2020-09-08 15:38:59 -07:00
Mattermost Build
ff1f0a741d Update react-native-elements (#4779) (#4783)
(cherry picked from commit 524609eb36)

Co-authored-by: Miguel Alatzar <migbot@users.noreply.github.com>
2020-09-08 15:38:51 -07:00
Mattermost Build
9ecc755c65 MM-28450 Fix crash when posting at-mention that does not belong to the channel (#4778) (#4782)
(cherry picked from commit 05f264554e)

Co-authored-by: Elias Nahum <nahumhbl@gmail.com>
2020-09-08 18:02:25 -03:00
Mattermost Build
c11b92b64e MM-28295 Android fix opening sidebar after closing setting screen (#4777) (#4780)
(cherry picked from commit e40d0e4c8a)

Co-authored-by: Elias Nahum <nahumhbl@gmail.com>
2020-09-08 16:49:36 -03:00
Mattermost Build
b51f7a0bb2 Always load the thread & modify thread selector (#4771) (#4775)
* Always load the thread & modify thread selector

* review feedback

(cherry picked from commit bd7afae6f0)

Co-authored-by: Elias Nahum <nahumhbl@gmail.com>
2020-09-08 13:21:04 -03:00
Mattermost Build
9db54fb462 MM-28296 Fix Android @mention render (#4764) (#4774)
* MM-28296 Fix Android @mention render

* Simplify mention style

* Update @mention snapshot

* @mention fix lint

* Address QA feedback

* QA Review

(cherry picked from commit c97b2be927)

Co-authored-by: Elias Nahum <nahumhbl@gmail.com>
2020-09-08 13:20:55 -03:00
Mattermost Build
bcd5397300 MM-28307 Fix display name in convert channel to private alert (#4767) (#4773)
(cherry picked from commit 4ecccbbb16)

Co-authored-by: Elias Nahum <nahumhbl@gmail.com>
2020-09-08 13:11:54 -03:00
Mattermost Build
053798814e Improve Ram-Bundles for Android and Enable on iOS (#4765) (#4772)
(cherry picked from commit b97a88f243)

Co-authored-by: Elias Nahum <nahumhbl@gmail.com>
2020-09-08 12:20:05 -03:00
Weblate (bot)
04e158fa81 Translations update from Weblate (#4769)
* Added translation using Weblate (Bulgarian)

* Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (635 of 635 strings)

Translation: mattermost-languages-shipped/mattermost-mobile_release-1.35
Translate-URL: https://translate.mattermost.com/projects/mattermost/mattermost-mobile_release-1-35/pt_BR/

* Translated using Weblate (Japanese)

Currently translated at 100.0% (635 of 635 strings)

Translation: mattermost-languages-shipped/mattermost-mobile_release-1.35
Translate-URL: https://translate.mattermost.com/projects/mattermost/mattermost-mobile_release-1-35/ja/

* Translated using Weblate (Japanese)

Currently translated at 100.0% (635 of 635 strings)

Translation: mattermost-languages-shipped/mattermost-mobile_release-1.35
Translate-URL: https://translate.mattermost.com/projects/mattermost/mattermost-mobile_release-1-35/ja/

* Translated using Weblate (Korean)

Currently translated at 100.0% (635 of 635 strings)

Translation: mattermost-languages-shipped/mattermost-mobile_release-1.35
Translate-URL: https://translate.mattermost.com/projects/mattermost/mattermost-mobile_release-1-35/ko/

* Deleted translation using Weblate (Bulgarian)

Co-authored-by: rodrigocorsi <rodrigocorsi@gmail.com>
Co-authored-by: Tomoaki Abe <abe@enzou.tokyo>
Co-authored-by: kaakaa <stooner.hoe@gmail.com>
Co-authored-by: retheviper <youngbina@hotmail.com>
Co-authored-by: Elisabeth Kulzer <elisabeth.kulzer@mattermost.com>
2020-09-08 09:36:18 +02:00
Mattermost Build
11f24a76a1 Add word boundaries for (all | channel | here) (#4754) (#4762)
* Add word boundaries for (all | channel | here)

* do not include underscore in channel wide mention highlight

* Regex suggested in code review

Co-authored-by: Harrison Healey <harrisonmhealey@gmail.com>

* Restyle at_mention suffix

Co-authored-by: Harrison Healey <harrisonmhealey@gmail.com>
(cherry picked from commit dce5675f05)

Co-authored-by: Elias Nahum <nahumhbl@gmail.com>
2020-09-01 14:58:46 -04:00
Weblate (bot)
8c5b493405 Translations update from Weblate (#4755)
* Translated using Weblate (Spanish)

Currently translated at 100.0% (635 of 635 strings)

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

* Translated using Weblate (Turkish)

Currently translated at 100.0% (635 of 635 strings)

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

* Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (635 of 635 strings)

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

* Translated using Weblate (German)

Currently translated at 98.5% (626 of 635 strings)

Translation: mattermost-languages-shipped/mattermost-mobile_release-1.35
Translate-URL: https://translate.mattermost.com/projects/mattermost/mattermost-mobile_release-1-35/de/

Co-authored-by: Elias  Nahum <elias@mattermost.com>
Co-authored-by: Kaya Zeren <kayazeren@gmail.com>
Co-authored-by: aeomin <lin@aeomin.net>
Co-authored-by: Elisabeth Kulzer <elisabeth.kulzer@mattermost.com>
2020-09-01 08:36:59 -04:00
Mattermost Build
fa605ae6e5 Bump app build number to 321 (#4749) (#4750)
(cherry picked from commit 619153ebe0)

Co-authored-by: Miguel Alatzar <migbot@users.noreply.github.com>
2020-08-28 11:35:36 -07:00
Miguel Alatzar
2666c7c541 Store value before resetting text input (#4748) 2020-08-28 11:15:08 -07:00
Mattermost Build
537acefb2d MM-25684 Highlight at-mention followed by a period (#4733) (#4747)
* MM-25684 Highlight at-mention followed by a period

* Refactor at_mention to not highlight the period

* Do not add a link for invalid mentions

* Suggested review

Co-authored-by: Miguel Alatzar <migbot@users.noreply.github.com>

Co-authored-by: Miguel Alatzar <migbot@users.noreply.github.com>
(cherry picked from commit 1d0fc0510c)

Co-authored-by: Elias Nahum <nahumhbl@gmail.com>
2020-08-28 13:16:24 -04:00
Mattermost Build
dccdf1a238 Show arrows in calendar (#4745) (#4746)
* Show arrows in calendar

* Set calendar arrow size platform specific

(cherry picked from commit 514ba57a8e)

Co-authored-by: Elias Nahum <nahumhbl@gmail.com>
2020-08-28 12:58:42 -04:00
Mattermost Build
5f8910004b Update Dependencies (#4735) (#4743)
* Update rudderstack dependency

* Upgrade sentry dependency

* Upgrade react-native-elements dependency

* Upgrade react-native-elements dependency

* Upgrade react-native-navigation dependency

* Upgrade react-native-permissions dependency

* Upgrade dev dependencies

(cherry picked from commit e624fa7ecf)

Co-authored-by: Elias Nahum <nahumhbl@gmail.com>
2020-08-27 18:47:32 -04:00
Mattermost Build
c56b223fa6 Bump app build number to 320 (#4740) (#4741)
(cherry picked from commit 9aa68bb346)

Co-authored-by: Elias Nahum <nahumhbl@gmail.com>
2020-08-27 13:02:13 -04:00
Mattermost Build
d45268d877 SSO: Rebuild the server url without query string and/or hash (#4731) (#4734)
(cherry picked from commit 7a0b5f982e)

Co-authored-by: Elias Nahum <nahumhbl@gmail.com>
2020-08-27 08:51:46 -04:00
Farhan Munshi
d50923e841 [MM-27132] Handle group mention keys for group synced channels and teams (#4726) (#4732)
* Handle group synced channel highlighting better

* Add tests

* Dont check if teamRoleFound if no teamId given for haveIChannelPermission

* Pass team id for channel mentions and group mentions checks

* Actually make a selector

* Check for falsey teamId instead of undefined

* Remove true &&

* Iterate over object values instead of for in
2020-08-26 13:40:42 -04:00
Mattermost Build
449d7272d2 Apply flex style only to empty results (#4728) (#4729)
(cherry picked from commit ffb5213b3a)

Co-authored-by: Miguel Alatzar <migbot@users.noreply.github.com>
2020-08-25 16:12:24 -07:00
Mattermost Build
a47b2a3831 Upgrade depdendencies (#4725) (#4727)
* @react-navigation/*
* react-native-navigation
* react-native-calendars
* react-native-gesture-handler
* react-native-reanimated
* react-native-safe-area-context
* react-native-screens
* react-native-video

(cherry picked from commit 183eec0fad)

Co-authored-by: Elias Nahum <nahumhbl@gmail.com>
2020-08-25 17:41:54 -04:00
Mattermost Build
a0b272016b Update NOTICE.txt (#4717) (#4724)
(cherry picked from commit 34021f2dad)

Co-authored-by: Amy Blais <amy_blais@hotmail.com>
2020-08-25 08:37:25 -04:00
Mattermost Build
a100ed4ad2 Update dev dependencies & some others (#4720) (#4721)
* Dev dependencies

* Upgrade async-storage / netinfo / device-info / elements / mmkv dependencies

(cherry picked from commit 9628dc9547)

Co-authored-by: Elias Nahum <nahumhbl@gmail.com>
2020-08-25 08:18:49 -04:00
Elisabeth Kulzer
f306cef236 Add new translations, remove empty translation strings (#4722)
* Translated using Weblate (Italian)

Currently translated at 100.0% (635 of 635 strings)

Translation: mattermost-languages-shipped/mattermost-mobile_release-1.35
Translate-URL: https://translate.mattermost.com/projects/mattermost/mattermost-mobile_release-1-35/it/

Translated using Weblate (Chinese (Traditional))

Currently translated at 99.5% (632 of 635 strings)

Translation: mattermost-languages-shipped/mattermost-mobile_release-1.35
Translate-URL: https://translate.mattermost.com/projects/mattermost/mattermost-mobile_release-1-35/zh_Hant/

Translated using Weblate (Korean)

Currently translated at 99.5% (632 of 635 strings)

Translation: mattermost-languages-shipped/mattermost-mobile_release-1.35
Translate-URL: https://translate.mattermost.com/projects/mattermost/mattermost-mobile_release-1-35/ko/

Translated using Weblate (Portuguese (Brazil))

Currently translated at 99.8% (634 of 635 strings)

Translation: mattermost-languages-shipped/mattermost-mobile_release-1.35
Translate-URL: https://translate.mattermost.com/projects/mattermost/mattermost-mobile_release-1-35/pt_BR/

Translated using Weblate (Japanese)

Currently translated at 99.8% (634 of 635 strings)

Translation: mattermost-languages-shipped/mattermost-mobile_release-1.35
Translate-URL: https://translate.mattermost.com/projects/mattermost/mattermost-mobile_release-1-35/ja/

Translated using Weblate (Chinese (Simplified))

Currently translated at 99.5% (632 of 635 strings)

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

Translated using Weblate (Polish)

Currently translated at 97.9% (622 of 635 strings)

Translation: mattermost-languages-shipped/mattermost-mobile_release-1.35
Translate-URL: https://translate.mattermost.com/projects/mattermost/mattermost-mobile_release-1-35/pl/

Translated using Weblate (Russian)

Currently translated at 100.0% (635 of 635 strings)

Translation: mattermost-languages-shipped/mattermost-mobile_release-1.35
Translate-URL: https://translate.mattermost.com/projects/mattermost/mattermost-mobile_release-1-35/ru/

Translated using Weblate (Turkish)

Currently translated at 99.8% (634 of 635 strings)

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

Translated using Weblate (French)

Currently translated at 97.9% (622 of 635 strings)

Translation: mattermost-languages-shipped/mattermost-mobile_release-1.35
Translate-URL: https://translate.mattermost.com/projects/mattermost/mattermost-mobile_release-1-35/fr/

Translated using Weblate (Romanian)

Currently translated at 100.0% (635 of 635 strings)

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

Translated using Weblate (Ukrainian)

Currently translated at 98.1% (623 of 635 strings)

Translation: mattermost-languages-shipped/mattermost-mobile_release-1.35
Translate-URL: https://translate.mattermost.com/projects/mattermost/mattermost-mobile_release-1-35/uk/

Translated using Weblate (Spanish)

Currently translated at 99.5% (632 of 635 strings)

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

Translated using Weblate (German)

Currently translated at 98.4% (625 of 635 strings)

Translation: mattermost-languages-shipped/mattermost-mobile_release-1.35
Translate-URL: https://translate.mattermost.com/projects/mattermost/mattermost-mobile_release-1-35/de/

Translated using Weblate (Dutch)

Currently translated at 100.0% (635 of 635 strings)

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

* Translated using Weblate (Italian)

Currently translated at 100.0% (635 of 635 strings)

Translation: mattermost-languages-shipped/mattermost-mobile_release-1.35
Translate-URL: https://translate.mattermost.com/projects/mattermost/mattermost-mobile_release-1-35/it/

* Translated using Weblate (French)

Currently translated at 97.4% (619 of 635 strings)

Translation: mattermost-languages-shipped/mattermost-mobile_release-1.35
Translate-URL: https://translate.mattermost.com/projects/mattermost/mattermost-mobile_release-1-35/fr/

* Translated using Weblate (French)

Currently translated at 97.4% (619 of 635 strings)

Translation: mattermost-languages-shipped/mattermost-mobile_release-1.35
Translate-URL: https://translate.mattermost.com/projects/mattermost/mattermost-mobile_release-1-35/fr/

* Translated using Weblate (French)

Currently translated at 97.4% (619 of 635 strings)

Translation: mattermost-languages-shipped/mattermost-mobile_release-1.35
Translate-URL: https://translate.mattermost.com/projects/mattermost/mattermost-mobile_release-1-35/fr/

* Translated using Weblate (German)

Currently translated at 97.9% (622 of 635 strings)

Translation: mattermost-languages-shipped/mattermost-mobile_release-1.35
Translate-URL: https://translate.mattermost.com/projects/mattermost/mattermost-mobile_release-1-35/de/

* Translated using Weblate (Polish)

Currently translated at 95.9% (609 of 635 strings)

Translation: mattermost-languages-shipped/mattermost-mobile_release-1.35
Translate-URL: https://translate.mattermost.com/projects/mattermost/mattermost-mobile_release-1-35/pl/

* Translated using Weblate (Polish)

Currently translated at 95.9% (609 of 635 strings)

Translation: mattermost-languages-shipped/mattermost-mobile_release-1.35
Translate-URL: https://translate.mattermost.com/projects/mattermost/mattermost-mobile_release-1-35/pl/

* Translated using Weblate (Polish)

Currently translated at 95.9% (609 of 635 strings)

Translation: mattermost-languages-shipped/mattermost-mobile_release-1.35
Translate-URL: https://translate.mattermost.com/projects/mattermost/mattermost-mobile_release-1-35/pl/

* Translated using Weblate (Polish)

Currently translated at 95.9% (609 of 635 strings)

Translation: mattermost-languages-shipped/mattermost-mobile_release-1.35
Translate-URL: https://translate.mattermost.com/projects/mattermost/mattermost-mobile_release-1-35/pl/

* Translated using Weblate (Ukrainian)

Currently translated at 89.4% (568 of 635 strings)

Translation: mattermost-languages-shipped/mattermost-mobile_release-1.35
Translate-URL: https://translate.mattermost.com/projects/mattermost/mattermost-mobile_release-1-35/uk/

* Translated using Weblate (Ukrainian)

Currently translated at 89.4% (568 of 635 strings)

Translation: mattermost-languages-shipped/mattermost-mobile_release-1.35
Translate-URL: https://translate.mattermost.com/projects/mattermost/mattermost-mobile_release-1-35/uk/

* Translated using Weblate (Ukrainian)

Currently translated at 89.4% (568 of 635 strings)

Translation: mattermost-languages-shipped/mattermost-mobile_release-1.35
Translate-URL: https://translate.mattermost.com/projects/mattermost/mattermost-mobile_release-1-35/uk/

* Translated using Weblate (Ukrainian)

Currently translated at 89.4% (568 of 635 strings)

Translation: mattermost-languages-shipped/mattermost-mobile_release-1.35
Translate-URL: https://translate.mattermost.com/projects/mattermost/mattermost-mobile_release-1-35/uk/

* Translated using Weblate (Ukrainian)

Currently translated at 89.4% (568 of 635 strings)

Translation: mattermost-languages-shipped/mattermost-mobile_release-1.35
Translate-URL: https://translate.mattermost.com/projects/mattermost/mattermost-mobile_release-1-35/uk/

* Translated using Weblate (Ukrainian)

Currently translated at 89.4% (568 of 635 strings)

Translation: mattermost-languages-shipped/mattermost-mobile_release-1.35
Translate-URL: https://translate.mattermost.com/projects/mattermost/mattermost-mobile_release-1-35/uk/

* Translated using Weblate (Ukrainian)

Currently translated at 89.4% (568 of 635 strings)

Translation: mattermost-languages-shipped/mattermost-mobile_release-1.35
Translate-URL: https://translate.mattermost.com/projects/mattermost/mattermost-mobile_release-1-35/uk/

* Translated using Weblate (Ukrainian)

Currently translated at 89.4% (568 of 635 strings)

Translation: mattermost-languages-shipped/mattermost-mobile_release-1.35
Translate-URL: https://translate.mattermost.com/projects/mattermost/mattermost-mobile_release-1-35/uk/

* Translated using Weblate (Ukrainian)

Currently translated at 89.4% (568 of 635 strings)

Translation: mattermost-languages-shipped/mattermost-mobile_release-1.35
Translate-URL: https://translate.mattermost.com/projects/mattermost/mattermost-mobile_release-1-35/uk/

* Translated using Weblate (Ukrainian)

Currently translated at 89.4% (568 of 635 strings)

Translation: mattermost-languages-shipped/mattermost-mobile_release-1.35
Translate-URL: https://translate.mattermost.com/projects/mattermost/mattermost-mobile_release-1-35/uk/

* Translated using Weblate (Ukrainian)

Currently translated at 89.4% (568 of 635 strings)

Translation: mattermost-languages-shipped/mattermost-mobile_release-1.35
Translate-URL: https://translate.mattermost.com/projects/mattermost/mattermost-mobile_release-1-35/uk/

* Translated using Weblate (Ukrainian)

Currently translated at 89.4% (568 of 635 strings)

Translation: mattermost-languages-shipped/mattermost-mobile_release-1.35
Translate-URL: https://translate.mattermost.com/projects/mattermost/mattermost-mobile_release-1-35/uk/

* Translated using Weblate (Ukrainian)

Currently translated at 89.4% (568 of 635 strings)

Translation: mattermost-languages-shipped/mattermost-mobile_release-1.35
Translate-URL: https://translate.mattermost.com/projects/mattermost/mattermost-mobile_release-1-35/uk/

* Translated using Weblate (German)

Currently translated at 98.1% (623 of 635 strings)

Translation: mattermost-languages-shipped/mattermost-mobile_release-1.35
Translate-URL: https://translate.mattermost.com/projects/mattermost/mattermost-mobile_release-1-35/de/

* Translated using Weblate (French)

Currently translated at 97.9% (622 of 635 strings)

Translation: mattermost-languages-shipped/mattermost-mobile_release-1.35
Translate-URL: https://translate.mattermost.com/projects/mattermost/mattermost-mobile_release-1-35/fr/

* Translated using Weblate (Polish)

Currently translated at 96.5% (613 of 635 strings)

Translation: mattermost-languages-shipped/mattermost-mobile_release-1.35
Translate-URL: https://translate.mattermost.com/projects/mattermost/mattermost-mobile_release-1-35/pl/

* Translated using Weblate (Ukrainian)

Currently translated at 91.4% (581 of 635 strings)

Translation: mattermost-languages-shipped/mattermost-mobile_release-1.35
Translate-URL: https://translate.mattermost.com/projects/mattermost/mattermost-mobile_release-1-35/uk/

* Add new translations, remove empty strings.

Co-authored-by: Mattermost Weblate Notify Bot <dev-ops@mattermost.com>
Co-authored-by: Zack-83 <giacomo.lanza@ptb.de>
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Elisabeth Kulzer <elisabeth.kulzer@mattermost.com>
Co-authored-by: Mattermod <mattermod@users.noreply.github.com>
2020-08-25 14:01:25 +02:00
Mattermost Build
089d158b87 oauth should not be able to edit full name (#4713) (#4714)
(cherry picked from commit 2800de0922)

Co-authored-by: Hossein Ahmadian-Yazdi <hahmadia@users.noreply.github.com>
2020-08-21 16:51:56 -04:00
Mattermost Build
355f7a6b6e Use substring over replaceFirst (#4707) (#4709)
(cherry picked from commit 68457c5e7e)

Co-authored-by: Miguel Alatzar <migbot@users.noreply.github.com>
2020-08-20 20:08:16 -04:00
Mattermost Build
075b99d48d Fix soft crash for edge emoji formatting (#4705) (#4708)
(cherry picked from commit ea4e21de93)

Co-authored-by: Elias Nahum <nahumhbl@gmail.com>
2020-08-20 20:01:01 -04:00
Mattermost Build
41a1d55713 Upgrade react-native-fast-image, react-native-localize, react-native-vector-icons, react-native-video & dev dependencies (#4698) (#4706)
(cherry picked from commit 3fce45f308)

Co-authored-by: Elias Nahum <nahumhbl@gmail.com>
2020-08-20 14:55:28 -04:00
Mattermost Build
504072d01b MM-25832 Prevent permalink view from loading posts multiple times (#4696) (#4703)
* Prevent permalink view from loading posts multiple times

* Android unhandled exception and initial loader indicator

* Fix permalink tests

(cherry picked from commit c095d45330)

Co-authored-by: Elias Nahum <nahumhbl@gmail.com>
2020-08-19 20:00:41 -04:00
Mattermost Build
55511e1fdb MM-26329 Fix Channel info guests info in landscape (#4692) (#4702)
* MM-26329 Fix Channel info guests info in landscape

* Channnel info refactor

* Rename canEditChannel to canEdit in connected component

(cherry picked from commit 55cfce9b89)

Co-authored-by: Elias Nahum <nahumhbl@gmail.com>
2020-08-19 10:18:46 -04:00
Mattermost Build
c4e23af279 Bump Version to 1.35.0 and Build to 319 (#4699) (#4700)
* Bump app build number to 319

* Bump app version number to 1.35.0

(cherry picked from commit 3e8e78c5fc)

Co-authored-by: Elias Nahum <nahumhbl@gmail.com>
2020-08-18 20:19:39 -04:00
Mattermost Build
031bac9863 Allow swipe to close the sidebar after search is cancelled (#4688) (#4697)
(cherry picked from commit 033cdfc459)

Co-authored-by: Elias Nahum <nahumhbl@gmail.com>
2020-08-18 18:23:11 -04:00
Mattermost Build
1082f08757 Upgrade react-native-permissions dependency (#4693) (#4695)
(cherry picked from commit b84901e0bf)

Co-authored-by: Elias Nahum <nahumhbl@gmail.com>
2020-08-18 12:52:30 -04:00
Mattermost Build
49bd5a13ac MM-26436: Allows others' posts to be delete with just 'delete_others_posts' permission. (#4660) (#4694)
(cherry picked from commit 7f7e7e7554)

Co-authored-by: Martin Kraft <martinkraft@gmail.com>
2020-08-18 09:43:09 -04:00
Mattermost Build
122676ef5d Upgrade react-native-mmkv-storage dependency (#4671) (#4691)
(cherry picked from commit a909583025)

Co-authored-by: Elias Nahum <nahumhbl@gmail.com>
2020-08-17 20:50:25 -04:00
Weblate (bot)
7e9c20e191 Translations update from Weblate (#4690)
* Translated using Weblate (Italian)

Currently translated at 100.0% (635 of 635 strings)

Translation: mattermost-languages-shipped/mattermost-mobile_release-1.35
Translate-URL: https://translate.mattermost.com/projects/mattermost/mattermost-mobile_release-1-35/it/

Translated using Weblate (Chinese (Traditional))

Currently translated at 99.5% (632 of 635 strings)

Translation: mattermost-languages-shipped/mattermost-mobile_release-1.35
Translate-URL: https://translate.mattermost.com/projects/mattermost/mattermost-mobile_release-1-35/zh_Hant/

Translated using Weblate (Korean)

Currently translated at 99.5% (632 of 635 strings)

Translation: mattermost-languages-shipped/mattermost-mobile_release-1.35
Translate-URL: https://translate.mattermost.com/projects/mattermost/mattermost-mobile_release-1-35/ko/

Translated using Weblate (Portuguese (Brazil))

Currently translated at 99.8% (634 of 635 strings)

Translation: mattermost-languages-shipped/mattermost-mobile_release-1.35
Translate-URL: https://translate.mattermost.com/projects/mattermost/mattermost-mobile_release-1-35/pt_BR/

Translated using Weblate (Japanese)

Currently translated at 99.8% (634 of 635 strings)

Translation: mattermost-languages-shipped/mattermost-mobile_release-1.35
Translate-URL: https://translate.mattermost.com/projects/mattermost/mattermost-mobile_release-1-35/ja/

Translated using Weblate (Chinese (Simplified))

Currently translated at 99.5% (632 of 635 strings)

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

Translated using Weblate (Polish)

Currently translated at 97.9% (622 of 635 strings)

Translation: mattermost-languages-shipped/mattermost-mobile_release-1.35
Translate-URL: https://translate.mattermost.com/projects/mattermost/mattermost-mobile_release-1-35/pl/

Translated using Weblate (Russian)

Currently translated at 100.0% (635 of 635 strings)

Translation: mattermost-languages-shipped/mattermost-mobile_release-1.35
Translate-URL: https://translate.mattermost.com/projects/mattermost/mattermost-mobile_release-1-35/ru/

Translated using Weblate (Turkish)

Currently translated at 99.8% (634 of 635 strings)

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

Translated using Weblate (French)

Currently translated at 97.9% (622 of 635 strings)

Translation: mattermost-languages-shipped/mattermost-mobile_release-1.35
Translate-URL: https://translate.mattermost.com/projects/mattermost/mattermost-mobile_release-1-35/fr/

Translated using Weblate (Romanian)

Currently translated at 100.0% (635 of 635 strings)

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

Translated using Weblate (Ukrainian)

Currently translated at 98.1% (623 of 635 strings)

Translation: mattermost-languages-shipped/mattermost-mobile_release-1.35
Translate-URL: https://translate.mattermost.com/projects/mattermost/mattermost-mobile_release-1-35/uk/

Translated using Weblate (Spanish)

Currently translated at 99.5% (632 of 635 strings)

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

Translated using Weblate (German)

Currently translated at 98.4% (625 of 635 strings)

Translation: mattermost-languages-shipped/mattermost-mobile_release-1.35
Translate-URL: https://translate.mattermost.com/projects/mattermost/mattermost-mobile_release-1-35/de/

Translated using Weblate (Dutch)

Currently translated at 100.0% (635 of 635 strings)

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

* Translated using Weblate (Italian)

Currently translated at 100.0% (635 of 635 strings)

Translation: mattermost-languages-shipped/mattermost-mobile_release-1.35
Translate-URL: https://translate.mattermost.com/projects/mattermost/mattermost-mobile_release-1-35/it/

Co-authored-by: Mattermost Weblate Notify Bot <dev-ops@mattermost.com>
Co-authored-by: Zack-83 <giacomo.lanza@ptb.de>
2020-08-17 20:40:19 -04:00
Mattermost Build
4d027f4e6e Send event to JS only if React initialized (#4668) (#4674)
(cherry picked from commit 66a8f8a55f)

Co-authored-by: Miguel Alatzar <migbot@users.noreply.github.com>
2020-08-15 11:52:34 -04:00
Mattermost Build
0112b96366 Upgrade react-native-image-picker dependency (#4670) (#4687)
(cherry picked from commit 40a264a4de)

Co-authored-by: Elias Nahum <nahumhbl@gmail.com>
2020-08-15 11:51:57 -04:00
Mattermost Build
c387d2661c Upgrade react-native-document-picker dependency (#4669) (#4686)
(cherry picked from commit 53fa5a1db7)

Co-authored-by: Elias Nahum <nahumhbl@gmail.com>
2020-08-15 11:36:27 -04:00
Mattermost Build
034a9400c5 MM-27711 Play YouTube video using expanded link (#4666) (#4685)
(cherry picked from commit d1d7b7f9d6)

Co-authored-by: Elias Nahum <nahumhbl@gmail.com>
2020-08-15 11:21:43 -04:00
Mattermost Build
1f47ba71cf Upgrade react-native-device-info dependency (#4664) (#4684)
(cherry picked from commit c48ffd0f65)

Co-authored-by: Elias Nahum <nahumhbl@gmail.com>
2020-08-15 11:09:04 -04:00
Mattermost Build
dc87e20bcb Update react-native-elements (#4658) (#4683)
(cherry picked from commit 27849beed4)

Co-authored-by: Elias Nahum <nahumhbl@gmail.com>
2020-08-15 10:58:07 -04:00
Mattermost Build
abb93e9b3d [MM-27139] Use file preview URL for images in post (#4656) (#4682)
* Use file preview URL for images in post

* Use getFilePreviewUrl for gallery as well

* But use getFileUrl for GIFs in gallery

(cherry picked from commit 304e3674c8)

Co-authored-by: Miguel Alatzar <migbot@users.noreply.github.com>
2020-08-15 10:46:19 -04:00
Mattermost Build
57a11783c3 Update moment-timezone & react-native-calendars dependency (#4646) (#4681)
(cherry picked from commit 24f6d2df92)

Co-authored-by: Elias Nahum <nahumhbl@gmail.com>
2020-08-15 10:34:39 -04:00
Mattermost Build
ea0b24149c MM-23739 Empty search state for emoji picker (#4641) (#4680)
* Returning a "no results for" default message

* Added styles & i18n text

* Updating icon to Octicons

* Replacing conditional render with ListEmptyComponent

* Running mmjstool sort

* Changing fontWeight, center layout, icon style

(cherry picked from commit c57c2f4245)

Co-authored-by: Andre Vasconcelos <andre.onogoro@gmail.com>
2020-08-15 10:23:08 -04:00
Mattermost Build
dbf96ac8eb Upgrade fuse.js dependency (#4640) (#4679)
(cherry picked from commit 9773e321c1)

Co-authored-by: Elias Nahum <nahumhbl@gmail.com>
2020-08-15 10:15:40 -04:00
Mattermost Build
34389c70c9 Upgrade sentry dependency (#4636) (#4678)
(cherry picked from commit 1abdd1be54)

Co-authored-by: Elias Nahum <nahumhbl@gmail.com>
2020-08-15 10:02:37 -04:00
Mattermost Build
b807140fe1 [MM-12526] removed from channel alert (#4633) (#4677)
* GH-13222: Shows an alert when you are removed from the channel that you are currently viewing

* GH-13222: Removed channel alert improved text

Co-authored-by: Mattermod <mattermod@users.noreply.github.com>
(cherry picked from commit 8b9ef0e849)

Co-authored-by: Kyle Watson <mail@watsonk.me>
2020-08-14 19:18:18 -04:00
Elias Nahum
9ed9c763e2 Update netinfo dependency and rn to 0.63.2 (#4614) 2020-08-14 18:11:10 -04:00
Mattermost Build
6a9300afb7 Update async-storage dependency (#4599) (#4676)
(cherry picked from commit 60418d57c7)

Co-authored-by: Elias Nahum <nahumhbl@gmail.com>
2020-08-14 18:02:16 -04:00
Mattermost Build
d1c81ef2d4 Update camera-roll dependency (#4595) (#4675)
(cherry picked from commit e7e0ca0d5c)

Co-authored-by: Elias Nahum <nahumhbl@gmail.com>
2020-08-14 17:31:28 -04:00
Elias Nahum
1805874740 Merge branch 'release-1.35' of github.com:mattermost/mattermost-mobile into release-1.35 2020-08-14 14:06:07 -04:00
Elias Nahum
10572d17ad MM-26817 Upgrade to RN 0.63 (#4566)
* Upgrade to RN 0.63

* Bump to RN 0.63.1

* Fix RN patch

* Use JSC Intl version

* Update android/app/build.gradle

Co-authored-by: Miguel Alatzar <migbot@users.noreply.github.com>

* Fix Android external storage permission

* Fix emoji imageUrl when no server url is present

* Patch react-native-image-picker

* Allow to post attachment only messages

Co-authored-by: Miguel Alatzar <migbot@users.noreply.github.com>
2020-08-14 14:05:23 -04:00
Mattermost Build
4ded73b927 [MM-26489] No need to decode file URI on Android (#4610) (#4673)
* Encode lone % chars

* No need to decode on Android

(cherry picked from commit 7c827fe319)

Co-authored-by: Miguel Alatzar <migbot@users.noreply.github.com>
2020-08-14 11:02:12 -07:00
222 changed files with 15609 additions and 12203 deletions

View File

@@ -26,12 +26,13 @@
"no-undefined": 0,
"react/display-name": [2, { "ignoreTranspilerName": false }],
"react/jsx-filename-extension": 0,
"@typescript-eslint/camelcase": [
2,
"camelcase": [
0,
{
"properties": "never"
}
],
"@typescript-eslint/ban-types": 0,
"@typescript-eslint/no-non-null-assertion": 0,
"@typescript-eslint/no-unused-vars": [
2,

View File

@@ -71,4 +71,4 @@ untyped-import
untyped-type-import
[version]
^0.113.0
^0.122.0

View File

@@ -113,6 +113,41 @@ SOFTWARE.
---
## @react-native-community/clipboard
This product contains '@react-native-community/clipboard' by React Native Community.
React Native Clipboard API for both iOS and Android
* HOMEPAGE:
* https://github.com/react-native-community/clipboard
* LICENSE: MIT
MIT License
Copyright (c) 2015-present, Facebook, Inc.
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-community/masked-view
This product contains '@react-native-community/masked-view' by React Native Community.
@@ -1920,41 +1955,6 @@ SOFTWARE.
---
## react-native-keyboard-aware-scroll-view
This product contains a modified version of 'react-native-keyboard-aware-scroll-view' by APSL.
A ScrollView component that handles keyboard appearance and automatically scrolls to focused TextInput.
* HOMEPAGE:
* https://github.com/APSL/react-native-keyboard-aware-scroll-view
* LICENSE: MIT
The MIT License (MIT)
Copyright (c) 2015 APSL
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-keyboard-tracking-view
This product contains a modified version of 'react-native-keyboard-tracking-view' by Artal Druk.

View File

@@ -108,8 +108,7 @@ def enableSeparateBuildPerCPUArchitecture = false
*/
def enableProguardInReleaseBuilds = false
// Add v8-android - prebuilt libv8android.so into APK
def jscFlavor = 'org.chromium:v8-android:+'
def jscFlavor = 'org.webkit:android-jsc-intl:+'
/**
* Whether to enable the Hermes VM.
@@ -133,8 +132,8 @@ android {
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
missingDimensionStrategy "RNNotifications.reactNativeVersion", "reactNative60"
versionCode 318
versionName "1.34.0"
versionCode 326
versionName "1.35.1"
multiDexEnabled = true
ndk {
abiFilters 'armeabi-v7a','arm64-v8a','x86','x86_64'
@@ -194,14 +193,6 @@ android {
targetCompatibility 1.8
}
packagingOptions {
// Make sure libjsc.so does not packed in APK
exclude "**/libjsc.so"
pickFirst "lib/armeabi-v7a/libc++_shared.so"
pickFirst "lib/arm64-v8a/libc++_shared.so"
pickFirst "lib/x86/libc++_shared.so"
pickFirst "lib/x86_64/libc++_shared.so"
}
}
repositories {

View File

@@ -19,6 +19,7 @@
android:theme="@style/AppTheme"
android:installLocation="auto"
android:networkSecurityConfig="@xml/network_security_config"
android:requestLegacyExternalStorage="true"
>
<meta-data android:name="firebase_analytics_collection_deactivated" android:value="true" />
<meta-data android:name="android.content.APP_RESTRICTIONS"

View File

@@ -172,7 +172,9 @@ public class CustomPushNotification extends PushNotification {
break;
}
notifyReceivedToJS();
if (mAppLifecycleFacade.isReactInitialized()) {
notifyReceivedToJS();
}
}
@Override
@@ -532,7 +534,12 @@ public class CustomPushNotification extends PushNotification {
}
private String removeSenderNameFromMessage(String message, String senderName) {
return message.replaceFirst(senderName, "").replaceFirst(": ", "").trim();
Integer index = message.indexOf(senderName);
if (index == 0) {
message = message.substring(senderName.length());
}
return message.replaceFirst(": ", "").trim();
}
private void notificationReceiptDelivery(String ackId, String postId, String type, boolean isIdLoaded, ResolvePromise promise) {

View File

@@ -14,6 +14,8 @@
<string name="allowOtherServers_description">Allow the user to change the above server URL.</string>
<string name="username_title">Default Username</string>
<string name="username_description">Set the username or email address to use to authenticate against the Mattermost Server.</string>
<string name="timeout_title">Default Request Timeout</string>
<string name="timeout_description">How long in milliseconds the mobile app should wait for the server to respond.</string>
<string name="vendor_title">EMM Vendor or Company Name</string>
<string name="vendor_description">Name of the EMM vendor or company deploying the app. Used in help text when prompting for passcodes so users are aware why the app is being protected.</string>
</resources>

View File

@@ -43,6 +43,12 @@
android:description="@string/username_description"
android:restrictionType="string"
android:defaultValue="" />
<restriction
android:key="timeout"
android:title="@string/timeout_title"
android:description="@string/timeout_description"
android:restrictionType="string"
android:defaultValue="10000" />
<restriction
android:key="vendor"
android:title="@string/vendor_title"

View File

@@ -2,10 +2,10 @@
buildscript {
ext {
buildToolsVersion = "28.0.3"
buildToolsVersion = "29.0.2"
minSdkVersion = 24
compileSdkVersion = 28
targetSdkVersion = 28
compileSdkVersion = 29
targetSdkVersion = 29
supportLibVersion = "28.0.0"
kotlinVersion = "1.3.61"
RNNKotlinVersion = kotlinVersion
@@ -18,7 +18,7 @@ buildscript {
mavenCentral()
}
dependencies {
classpath 'com.android.tools.build:gradle:3.5.2'
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"
@@ -48,17 +48,17 @@ allprojects {
jcenter()
maven {
// All of React Native (JS, Obj-C sources, Android binaries) is installed from npm
// url "$rootDir/../node_modules/react-native/android"
url("$rootDir/../node_modules/react-native/android")
// Replace AAR from original RN with AAR from react-native-v8
url("$rootDir/../node_modules/react-native-v8/dist")
// url("$rootDir/../node_modules/react-native-v8/dist")
}
maven {
// Local Maven repo containing AARs with JSC library built for Android
// url "$rootDir/../node_modules/jsc-android/dist"
url("$rootDir/../node_modules/jsc-android/dist")
// prebuilt libv8android.so
url("$rootDir/../node_modules/v8-android/dist")
// url("$rootDir/../node_modules/v8-android/dist")
}
maven {
url "https://www.jitpack.io"

View File

@@ -30,4 +30,4 @@ android.useAndroidX=true
android.enableJetifier=true
# Version of flipper SDK to use with React Native
FLIPPER_VERSION=0.33.1
FLIPPER_VERSION=0.37.0

Binary file not shown.

View File

@@ -2,4 +2,4 @@ distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-6.0.1-all.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-6.2-all.zip

29
android/gradlew vendored
View File

@@ -154,19 +154,19 @@ if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
else
eval `echo args$i`="\"$arg\""
fi
i=$((i+1))
i=`expr $i + 1`
done
case $i in
(0) set -- ;;
(1) set -- "$args0" ;;
(2) set -- "$args0" "$args1" ;;
(3) set -- "$args0" "$args1" "$args2" ;;
(4) set -- "$args0" "$args1" "$args2" "$args3" ;;
(5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
(6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
(7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
(8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
(9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
0) set -- ;;
1) set -- "$args0" ;;
2) set -- "$args0" "$args1" ;;
3) set -- "$args0" "$args1" "$args2" ;;
4) set -- "$args0" "$args1" "$args2" "$args3" ;;
5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
esac
fi
@@ -175,14 +175,9 @@ save () {
for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
echo " "
}
APP_ARGS=$(save "$@")
APP_ARGS=`save "$@"`
# Collect all arguments for the java command, following the shell quoting and substitution rules
eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong
if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then
cd "$(dirname "$0")"
fi
exec "$JAVACMD" "$@"

27
android/gradlew.bat vendored
View File

@@ -13,64 +13,91 @@
@rem See the License for the specific language governing permissions and
@rem limitations under the License.
@rem
@if "%DEBUG%" == "" @echo off
@rem ##########################################################################
@rem
@rem Gradle startup script for Windows
@rem
@rem ##########################################################################
@rem Set local scope for the variables with windows NT shell
if "%OS%"=="Windows_NT" setlocal
set DIRNAME=%~dp0
if "%DIRNAME%" == "" set DIRNAME=.
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
@rem Find java.exe
if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if "%ERRORLEVEL%" == "0" goto init
echo.
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:findJavaFromJavaHome
set JAVA_HOME=%JAVA_HOME:"=%
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto init
echo.
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:init
@rem Get command-line arguments, handling Windows variants
if not "%OS%" == "Windows_NT" goto win9xME_args
:win9xME_args
@rem Slurp the command line arguments.
set CMD_LINE_ARGS=
set _SKIP=2
:win9xME_args_slurp
if "x%~1" == "x" goto execute
set CMD_LINE_ARGS=%*
:execute
@rem Setup the command line
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
@rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
:end
@rem End local scope for the variables with windows NT shell
if "%ERRORLEVEL%"=="0" goto mainEnd
:fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code!
if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
exit /b 1
:mainEnd
if "%OS%"=="Windows_NT" endlocal
:omega

View File

@@ -13,8 +13,6 @@ import EphemeralStore from '@store/ephemeral_store';
import Store from '@store/store';
import {NavigationTypes} from '@constants';
const CHANNEL_SCREEN = 'Channel';
function getThemeFromState() {
const state = Store.redux?.getState() || {};
@@ -29,8 +27,8 @@ export function resetToChannel(passProps = {}) {
const stack = {
children: [{
component: {
id: CHANNEL_SCREEN,
name: CHANNEL_SCREEN,
id: NavigationTypes.CHANNEL_SCREEN,
name: NavigationTypes.CHANNEL_SCREEN,
passProps,
options: {
layout: {
@@ -88,6 +86,8 @@ export function resetToChannel(passProps = {}) {
export function resetToSelectServer(allowOtherServers) {
const theme = Preferences.THEMES.default;
EphemeralStore.clearNavigationComponents();
Navigation.setRoot({
root: {
stack: {
@@ -150,6 +150,8 @@ export function resetToTeams(name, title, passProps = {}, options = {}) {
},
};
EphemeralStore.clearNavigationComponents();
Navigation.setRoot({
root: {
stack: {
@@ -427,7 +429,7 @@ export function closeMainSideMenu() {
}
Keyboard.dismiss();
Navigation.mergeOptions(CHANNEL_SCREEN, {
Navigation.mergeOptions(NavigationTypes.CHANNEL_SCREEN, {
sideMenu: {
left: {visible: false},
},
@@ -439,7 +441,7 @@ export function enableMainSideMenu(enabled, visible = true) {
return;
}
Navigation.mergeOptions(CHANNEL_SCREEN, {
Navigation.mergeOptions(NavigationTypes.CHANNEL_SCREEN, {
sideMenu: {
left: {enabled, visible},
},
@@ -452,7 +454,7 @@ export function openSettingsSideMenu() {
}
Keyboard.dismiss();
Navigation.mergeOptions(CHANNEL_SCREEN, {
Navigation.mergeOptions(NavigationTypes.CHANNEL_SCREEN, {
sideMenu: {
right: {visible: true},
},
@@ -465,7 +467,7 @@ export function closeSettingsSideMenu() {
}
Keyboard.dismiss();
Navigation.mergeOptions(CHANNEL_SCREEN, {
Navigation.mergeOptions(NavigationTypes.CHANNEL_SCREEN, {
sideMenu: {
right: {visible: false},
},

View File

@@ -31,7 +31,7 @@ import {getChannelByName as selectChannelByName, getChannelsIdForTeam} from '@mm
import EventEmitter from '@mm-redux/utils/event_emitter';
import {lastChannelIdForTeam, loadSidebarDirectMessagesProfiles} from '@actions/helpers/channels';
import {getPosts, getPostsBefore, getPostsSince, getPostThread, loadUnreadChannelPosts} from '@actions/views/post';
import {getPosts, getPostsBefore, getPostsSince, loadUnreadChannelPosts} from '@actions/views/post';
import {INSERT_TO_COMMENT, INSERT_TO_DRAFT} from '@constants/post_draft';
import {getChannelReachable} from '@selectors/channel';
import telemetry from '@telemetry';
@@ -111,18 +111,6 @@ export function fetchPostActionWithRetry(action, maxTries = MAX_RETRIES) {
};
}
export function loadThreadIfNecessary(rootId) {
return (dispatch, getState) => {
const state = getState();
const {posts, postsInThread} = state.entities.posts;
const threadPosts = postsInThread[rootId];
if (!posts[rootId] || !threadPosts) {
dispatch(getPostThread(rootId));
}
};
}
export function selectInitialChannel(teamId) {
return (dispatch, getState) => {
const state = getState();

View File

@@ -152,6 +152,7 @@ export function handleUserRemovedEvent(msg: WebSocketMessage) {
dispatch(batchActions(actions, 'BATCH_WS_USER_REMOVED'));
if (redirectToDefaultChannel) {
EventEmitter.emit(General.REMOVED_FROM_CHANNEL, channel.display_name);
EventEmitter.emit(General.SWITCH_TO_DEFAULT_CHANNEL, currentTeamId);
}
} catch {

View File

@@ -1,26 +1,3 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`FormattedTime should render correctly 1`] = `
<View
pointerEvents="box-none"
style={
Object {
"flex": 1,
}
}
>
<View
collapsable={true}
pointerEvents="box-none"
style={
Object {
"flex": 1,
}
}
>
<Text>
7:02 PM
</Text>
</View>
</View>
`;
exports[`FormattedTime should render correctly 1`] = `undefined`;

View File

@@ -18,7 +18,7 @@ exports[`AnnouncementBanner should match snapshot 1`] = `
]
}
>
<Component
<ForwardRef
onPress={[Function]}
style={
Array [
@@ -57,7 +57,7 @@ exports[`AnnouncementBanner should match snapshot 1`] = `
name="info"
size={16}
/>
</Component>
</ForwardRef>
</ForwardRef(AnimatedComponentWrapper)>
`;

View File

@@ -1,8 +1,20 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`AtMention should match snapshot, no highlight 1`] = `
<Text>
@John.Smith
<Text
style={Object {}}
>
<Text
style={
Array [
Object {
"backgroundColor": "yellow",
},
]
}
>
@John.Smith
</Text>
</Text>
`;
@@ -10,13 +22,22 @@ exports[`AtMention should match snapshot, with highlight 1`] = `
<Text
onLongPress={[Function]}
onPress={[Function]}
style={Object {}}
>
<Text
style={null}
style={
Array [
Object {
"color": "#ff0000",
},
Object {
"backgroundColor": "yellow",
},
]
}
>
@John.Smith
</Text>
</Text>
`;
@@ -24,16 +45,18 @@ exports[`AtMention should match snapshot, without highlight 1`] = `
<Text
onLongPress={[Function]}
onPress={[Function]}
style={Object {}}
>
<Text
style={
Object {
"color": "#ff0000",
}
Array [
Object {
"color": "#ff0000",
},
]
}
>
@Victor.Welch
</Text>
</Text>
`;

View File

@@ -3,16 +3,16 @@
import React from 'react';
import PropTypes from 'prop-types';
import {Clipboard, Text} from 'react-native';
import {StyleSheet, Text} from 'react-native';
import Clipboard from '@react-native-community/clipboard';
import {intlShape} from 'react-intl';
import MaterialIcon from 'react-native-vector-icons/MaterialIcons';
import {showModal} from '@actions/navigation';
import {displayUsername} from '@mm-redux/utils/user_utils';
import CustomPropTypes from 'app/constants/custom_prop_types';
import CustomPropTypes from '@constants/custom_prop_types';
import mattermostManaged from 'app/mattermost_managed';
import BottomSheet from 'app/utils/bottom_sheet';
import {showModal} from 'app/actions/navigation';
import BottomSheet from '@utils/bottom_sheet';
export default class AtMention extends React.PureComponent {
static propTypes = {
@@ -141,39 +141,85 @@ export default class AtMention extends React.PureComponent {
render() {
const {isSearchResult, mentionName, mentionStyle, onPostPress, teammateNameDisplay, textStyle, mentionKeys} = this.props;
const {user} = this.state;
const mentionTextStyle = [];
let backgroundColor;
let canPress = false;
let highlighted;
let isMention = false;
let mention;
let onLongPress;
let onPress;
let suffix;
let suffixElement;
let styleText;
if (!user.username) {
const group = this.getGroupFromMentionName();
if (group.allow_reference) {
highlighted = mentionKeys.some((item) => item.key === group.name);
return (
<Text
style={textStyle}
>
<Text style={highlighted ? null : mentionStyle}>
{`@${group.name}`}
</Text>
</Text>
);
}
return <Text style={textStyle}>{'@' + mentionName}</Text>;
if (textStyle) {
const {backgroundColor: bg, ...otherStyles} = StyleSheet.flatten(textStyle);
backgroundColor = bg;
styleText = otherStyles;
}
const suffix = this.props.mentionName.substring(user.username.length);
highlighted = mentionKeys.some((item) => item.key === user.username);
if (user?.username) {
suffix = this.props.mentionName.substring(user.username.length);
highlighted = mentionKeys.some((item) => item.key.includes(user.username));
mention = displayUsername(user, teammateNameDisplay);
isMention = true;
canPress = true;
} else {
const group = this.getGroupFromMentionName();
if (group.allow_reference) {
highlighted = mentionKeys.some((item) => item.key === `@${group.name}`);
isMention = true;
mention = group.name;
suffix = this.props.mentionName.substring(group.name.length);
} else {
const pattern = new RegExp(/\b(all|channel|here)(?:\.\B|_\b|\b)/, 'i');
const mentionMatch = pattern.exec(mentionName);
highlighted = true;
if (mentionMatch) {
mention = mentionMatch.length > 1 ? mentionMatch[1] : mentionMatch[0];
suffix = mentionName.replace(mention, '');
isMention = true;
} else {
mention = mentionName;
}
}
}
if (canPress) {
onLongPress = this.handleLongPress;
onPress = isSearchResult ? onPostPress : this.goToUserProfile;
}
if (suffix) {
const suffixStyle = {...styleText, color: this.props.theme.centerChannelColor};
suffixElement = (
<Text style={suffixStyle}>
{suffix}
</Text>
);
}
if (isMention) {
mentionTextStyle.push(mentionStyle);
}
if (highlighted) {
mentionTextStyle.push({backgroundColor});
}
return (
<Text
style={textStyle}
onPress={isSearchResult ? onPostPress : this.goToUserProfile}
onLongPress={this.handleLongPress}
style={styleText}
onPress={onPress}
onLongPress={onLongPress}
>
<Text style={highlighted ? null : mentionStyle}>
{'@' + displayUsername(user, teammateNameDisplay)}
<Text style={mentionTextStyle}>
{'@' + mention}
</Text>
{suffix}
{suffixElement}
</Text>
);
}

View File

@@ -12,6 +12,7 @@ describe('AtMention', () => {
teammateNameDisplay: '',
mentionName: 'John.Smith',
mentionStyle: {color: '#ff0000'},
textStyle: {backgroundColor: 'yellow'},
theme: {},
};

View File

@@ -9,7 +9,7 @@ import {getAllUserMentionKeys} from '@mm-redux/selectors/entities/search';
import {getTeammateNameDisplaySetting, getTheme} from '@mm-redux/selectors/entities/preferences';
import {getGroupsByName} from '@mm-redux/selectors/entities/groups';
import {getAllGroupsForReferenceByName} from '@mm-redux/selectors/entities/groups';
import AtMention from './at_mention';
@@ -19,7 +19,7 @@ function mapStateToProps(state, ownProps) {
usersByUsername: getUsersByUsername(state),
mentionKeys: ownProps.mentionKeys || getAllUserMentionKeys(state),
teammateNameDisplay: getTeammateNameDisplaySetting(state),
groupsByName: getGroupsByName(state),
groupsByName: getAllGroupsForReferenceByName(state),
};
}

View File

@@ -37,6 +37,7 @@ function mapStateToProps(state, ownProps) {
state,
{
channel: currentChannelId,
team: currentTeamId,
permission: Permissions.USE_CHANNEL_MENTIONS,
default: true,
},
@@ -57,7 +58,7 @@ function mapStateToProps(state, ownProps) {
outChannel = filterMembersNotInChannel(state, matchTerm);
}
if (hasLicense && isMinimumServerVersion(state.entities.general.serverVersion, 5, 24)) {
if (haveIChannelPermission(state, {channel: currentChannelId, team: currentTeamId, permission: Permissions.USE_GROUP_MENTIONS, default: true}) && hasLicense && isMinimumServerVersion(state.entities.general.serverVersion, 5, 24)) {
if (matchTerm) {
groups = searchAssociatedGroupsForReferenceLocal(state, matchTerm, currentTeamId, currentChannelId);
} else {

View File

@@ -165,6 +165,8 @@ const getDateFontSize = () => {
};
const calendarTheme = memoizeResult((theme) => ({
arrowHeight: Platform.select({ios: 13, android: 26}),
arrowWidth: Platform.select({ios: 8, android: 22}),
calendarBackground: theme.centerChannelBg,
monthTextColor: changeOpacity(theme.centerChannelColor, 0.8),
dayTextColor: theme.centerChannelColor,

View File

@@ -12,10 +12,6 @@ describe('ChannelLoader', () => {
const baseProps = {
channelIsLoading: true,
theme: Preferences.THEMES.default,
actions: {
handleSelectChannel: jest.fn(),
setChannelLoading: jest.fn(),
},
isLandscape: false,
};

View File

@@ -5,20 +5,18 @@ exports[`EditChannelInfo should match snapshot 1`] = `
<Connect(StatusBar) />
<KeyboardAwareScrollView
enableAutomaticScroll={true}
enableOnAndroid={false}
enableResetScrollToCoords={true}
extraHeight={75}
extraScrollHeight={0}
keyboardOpeningTime={250}
getTextInputRefs={[Function]}
keyboardShouldPersistTaps="always"
onKeyboardDidHide={[Function]}
onKeyboardDidShow={[Function]}
scrollToBottomOnKBShow={false}
scrollToInputAdditionalOffset={75}
startScrolledToBottom={false}
style={
Object {
"flex": 1,
}
}
viewIsInsideTabBar={false}
>
<TouchableWithoutFeedback
onPress={[Function]}

View File

@@ -8,7 +8,7 @@ import {
TouchableWithoutFeedback,
View,
} from 'react-native';
import {KeyboardAwareScrollView} from 'react-native-keyboard-aware-scroll-view';
import {KeyboardAwareScrollView} from 'react-native-keyboard-aware-scrollview';
import {KeyboardTrackingView} from 'react-native-keyboard-tracking-view';
import {General} from '@mm-redux/constants';
@@ -92,7 +92,7 @@ export default class EditChannelInfo extends PureComponent {
}
if (this.scroll?.current) {
this.scroll.current.scrollToPosition(0, 0, true);
this.scroll.current.scrollTo({x: 0, y: 0, animated: true});
}
};
@@ -184,7 +184,7 @@ export default class EditChannelInfo extends PureComponent {
scrollHeaderToTop = () => {
if (this.scroll.current) {
this.scroll.current.scrollToPosition(0, this.state.headerPosition);
this.scroll.current.scrollTo({x: 0, y: this.state.headerPosition});
}
}

View File

@@ -17,6 +17,7 @@ function mapStateToProps(state, ownProps) {
const config = getConfig(state);
const emojiName = ownProps.emojiName;
const customEmojis = getCustomEmojisByName(state);
const serverUrl = Client4.getUrl();
let imageUrl = '';
let unicode;
@@ -26,9 +27,13 @@ function mapStateToProps(state, ownProps) {
const emoji = Emojis[EmojiIndicesByAlias.get(emojiName)];
unicode = emoji.filename;
if (BuiltInEmojis.includes(emojiName)) {
imageUrl = Client4.getSystemEmojiImageUrl(emoji.filename);
if (serverUrl) {
imageUrl = Client4.getSystemEmojiImageUrl(emoji.filename);
} else {
displayTextOnly = true;
}
}
} else if (customEmojis.has(emojiName)) {
} else if (customEmojis.has(emojiName) && serverUrl) {
const emoji = customEmojis.get(emojiName);
imageUrl = Client4.getCustomEmojiImageUrl(emoji.id);
isCustomEmoji = true;

View File

@@ -8,7 +8,7 @@ exports[`components/emoji_picker/emoji_picker.ios should match snapshot 1`] = `
<KeyboardAvoidingView
behavior="padding"
enabled={false}
keyboardVerticalOffset={50}
keyboardVerticalOffset={80}
style={
Object {
"flex": 1,
@@ -9997,7 +9997,7 @@ exports[`components/emoji_picker/emoji_picker.ios should match snapshot 1`] = `
}
}
>
<Component
<ForwardRef
onPress={[Function]}
style={
Object {
@@ -10023,8 +10023,8 @@ exports[`components/emoji_picker/emoji_picker.ios should match snapshot 1`] = `
]
}
/>
</Component>
<Component
</ForwardRef>
<ForwardRef
onPress={[Function]}
style={
Object {
@@ -10048,8 +10048,8 @@ exports[`components/emoji_picker/emoji_picker.ios should match snapshot 1`] = `
]
}
/>
</Component>
<Component
</ForwardRef>
<ForwardRef
onPress={[Function]}
style={
Object {
@@ -10073,8 +10073,8 @@ exports[`components/emoji_picker/emoji_picker.ios should match snapshot 1`] = `
]
}
/>
</Component>
<Component
</ForwardRef>
<ForwardRef
onPress={[Function]}
style={
Object {
@@ -10098,8 +10098,8 @@ exports[`components/emoji_picker/emoji_picker.ios should match snapshot 1`] = `
]
}
/>
</Component>
<Component
</ForwardRef>
<ForwardRef
onPress={[Function]}
style={
Object {
@@ -10123,8 +10123,8 @@ exports[`components/emoji_picker/emoji_picker.ios should match snapshot 1`] = `
]
}
/>
</Component>
<Component
</ForwardRef>
<ForwardRef
onPress={[Function]}
style={
Object {
@@ -10148,8 +10148,8 @@ exports[`components/emoji_picker/emoji_picker.ios should match snapshot 1`] = `
]
}
/>
</Component>
<Component
</ForwardRef>
<ForwardRef
onPress={[Function]}
style={
Object {
@@ -10173,8 +10173,8 @@ exports[`components/emoji_picker/emoji_picker.ios should match snapshot 1`] = `
]
}
/>
</Component>
<Component
</ForwardRef>
<ForwardRef
onPress={[Function]}
style={
Object {
@@ -10198,8 +10198,8 @@ exports[`components/emoji_picker/emoji_picker.ios should match snapshot 1`] = `
]
}
/>
</Component>
<Component
</ForwardRef>
<ForwardRef
onPress={[Function]}
style={
Object {
@@ -10223,7 +10223,7 @@ exports[`components/emoji_picker/emoji_picker.ios should match snapshot 1`] = `
]
}
/>
</Component>
</ForwardRef>
</View>
</View>
</KeyboardTrackingView>

View File

@@ -25,7 +25,7 @@ export default class EmojiPicker extends EmojiPickerBase {
const shorten = DeviceTypes.IS_IPHONE_WITH_INSETS && isLandscape ? 6 : 2;
let keyboardOffset = DeviceTypes.IS_IPHONE_WITH_INSETS ? 50 : 30;
let keyboardOffset = DeviceTypes.IS_IPHONE_WITH_INSETS ? 80 : 60;
if (isLandscape) {
keyboardOffset = DeviceTypes.IS_IPHONE_WITH_INSETS ? 0 : 10;
}

View File

@@ -14,6 +14,8 @@ import {
View,
} from 'react-native';
import FontAwesomeIcon from 'react-native-vector-icons/FontAwesome';
import Octicons from 'react-native-vector-icons/Octicons';
import sectionListGetItemLayout from 'react-native-section-list-get-item-layout';
import Emoji from 'app/components/emoji';
@@ -249,6 +251,8 @@ export default class EmojiPicker extends PureComponent {
let listComponent;
if (searchTerm) {
const contentContainerStyle = filteredEmojis.length ? null : styles.flex;
listComponent = (
<FlatList
data={filteredEmojis}
@@ -258,6 +262,8 @@ export default class EmojiPicker extends PureComponent {
nativeID={SCROLLVIEW_NATIVE_ID}
pageSize={10}
renderItem={this.flatListRenderItem}
ListEmptyComponent={this.renderEmptyList}
contentContainerStyle={contentContainerStyle}
style={styles.flatList}
/>
);
@@ -449,6 +455,42 @@ export default class EmojiPicker extends PureComponent {
</View>
);
};
renderEmptyList = () => {
const {theme} = this.props;
const {formatMessage} = this.context.intl;
const {searchTerm} = this.state;
const styles = getStyleSheetFromTheme(theme);
const title = formatMessage({
id: 'mobile.emoji_picker.search.not_found_title',
defaultMessage: 'No results found for "{searchTerm}"',
}, {
searchTerm,
});
const description = formatMessage({
id: 'mobile.emoji_picker.search.not_found_description',
defaultMessage: 'Check the spelling or try another search.',
});
return (
<View style={[styles.flex, styles.flexCenter]}>
<View style={styles.flexCenter}>
<View style={styles.notFoundIcon}>
<Octicons
name='search'
size={60}
color={theme.buttonBg}
/>
</View>
<Text style={[styles.notFoundText, styles.notFoundText20]}>
{title}
</Text>
<Text style={[styles.notFoundText, styles.notFoundText15]}>
{description}
</Text>
</View>
</View>
);
}
}
export const getStyleSheetFromTheme = makeStyleSheetFromTheme((theme) => {
@@ -515,6 +557,31 @@ export const getStyleSheetFromTheme = makeStyleSheetFromTheme((theme) => {
borderRightColor: changeOpacity(theme.centerChannelColor, 0.2),
overflow: 'hidden',
},
flexCenter: {
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
},
notFoundIcon: {
backgroundColor: changeOpacity(theme.centerChannelColor, 0.04),
width: 120,
height: 120,
borderRadius: 60,
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
},
notFoundText: {
color: theme.centerChannelColor,
marginTop: 16,
},
notFoundText20: {
fontSize: 20,
fontWeight: '600',
},
notFoundText15: {
fontSize: 15,
},
searchBar: {
backgroundColor: changeOpacity(theme.centerChannelColor, 0.2),
paddingVertical: 5,

View File

@@ -0,0 +1,38 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`FileAttachmentImage should match snapshot 1`] = `
<View
style={
Object {
"borderRadius": 5,
"overflow": "hidden",
}
}
>
<View
style={
Object {
"paddingBottom": "100%",
}
}
/>
<Connect(ProgressiveImage)
onError={[Function]}
resizeMethod="resize"
resizeMode="cover"
style={
Array [
Object {
"bottom": 0,
"left": 0,
"position": "absolute",
"right": 0,
"top": 0,
},
undefined,
]
}
tintDefaultSource={true}
/>
</View>
`;

View File

@@ -11,7 +11,6 @@ import {
import brokenImageIcon from '@assets/images/icons/brokenimage.png';
import ProgressiveImage from '@components/progressive_image';
import {Client4} from '@mm-redux/client';
import {isGif} from '@utils/file';
import {changeOpacity} from '@utils/theme';
const SMALL_IMAGE_MAX_HEIGHT = 48;
@@ -81,7 +80,7 @@ export default class FileAttachmentImage extends PureComponent {
imageProps.defaultSource = {uri: file.localPath};
} else if (file.id) {
imageProps.thumbnailUri = Client4.getFileThumbnailUrl(file.id);
imageProps.imageUri = isGif(file) ? Client4.getFilePreviewUrl(file.id) : Client4.getFileUrl(file.id);
imageProps.imageUri = Client4.getFilePreviewUrl(file.id);
}
return imageProps;
};

View File

@@ -0,0 +1,62 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React from 'react';
import {shallow} from 'enzyme';
import {Client4} from '@mm-redux/client';
import brokenImageIcon from '@assets/images/icons/brokenimage.png';
import FileAttachmentImage from './file_attachment_image.js';
describe('FileAttachmentImage', () => {
const baseProps = {
file: {},
};
test('should match snapshot', () => {
const wrapper = shallow(
<FileAttachmentImage {...baseProps}/>,
);
expect(wrapper.getElement()).toMatchSnapshot();
});
describe('imageProps', () => {
const wrapper = shallow(
<FileAttachmentImage {...baseProps}/>,
);
const instance = wrapper.instance();
it('should have brokenImageIcon as defaultSource if state.failed is true', () => {
wrapper.setState({failed: true});
const file = {};
const imageProps = instance.imageProps(file);
expect(imageProps.defaultSource).toStrictEqual(brokenImageIcon);
expect(imageProps.thumbnailUri).toBeUndefined();
expect(imageProps.imageUri).toBeUndefined();
});
it('should have file.localPath as defaultSource if localPath is set', () => {
wrapper.setState({failed: false});
const file = {localPath: '/localPath.png'};
const imageProps = instance.imageProps(file);
expect(imageProps.defaultSource).toStrictEqual({uri: file.localPath});
expect(imageProps.thumbnailUri).toBeUndefined();
expect(imageProps.imageUri).toBeUndefined();
});
it('should have thumbnailUri and imageUri if the file has an ID', () => {
const getFileThumbnailUrl = jest.spyOn(Client4, 'getFileThumbnailUrl');
const getFilePreviewUrl = jest.spyOn(Client4, 'getFilePreviewUrl');
wrapper.setState({failed: false});
const file = {id: 'id'};
const imageProps = instance.imageProps(file);
expect(getFileThumbnailUrl).toHaveBeenCalled();
expect(getFilePreviewUrl).toHaveBeenCalled();
expect(imageProps.defaultSource).toBeUndefined();
expect(imageProps.thumbnailUri).toBeDefined();
expect(imageProps.imageUri).toBeDefined();
});
});
});

View File

@@ -86,7 +86,7 @@ export default class FileAttachmentList extends ImageViewPort {
if (file.localPath) {
uri = file.localPath;
} else {
uri = Client4.getFileUrl(file.id);
uri = isGif(file) ? Client4.getFileUrl(file.id) : Client4.getFilePreviewUrl(file.id);
}
results.push({

View File

@@ -2,7 +2,7 @@
// See LICENSE.txt for license information.
import React from 'react';
import {render} from '@testing-library/react-native';
import TestRenderer from 'react-test-renderer';
import {IntlProvider} from 'react-intl';
import moment from 'moment-timezone';
@@ -21,9 +21,10 @@ describe('FormattedTime', () => {
let wrapper = renderWithIntl(
<FormattedTime {...baseProps}/>,
);
let element = wrapper.root.find((el) => el.type === 'Text' && el.children && el.children[0] === '7:02 PM');
expect(wrapper.baseElement).toMatchSnapshot();
expect(wrapper.getByText('7:02 PM')).toBeTruthy();
expect(element).toBeTruthy();
wrapper = renderWithIntl(
<FormattedTime
@@ -32,7 +33,8 @@ describe('FormattedTime', () => {
/>,
);
expect(wrapper.getByText('19:02')).toBeTruthy();
element = wrapper.root.find((el) => el.type === 'Text' && el.children && el.children[0] === '19:02');
expect(element).toBeTruthy();
});
it('should support localization', () => {
@@ -42,7 +44,8 @@ describe('FormattedTime', () => {
'es',
);
expect(wrapper.getByText('7:02 PM')).toBeTruthy();
let element = wrapper.root.find((el) => el.type === 'Text' && el.children && el.children[0] === '7:02 PM');
expect(element).toBeTruthy();
moment.locale('ko');
wrapper = renderWithIntl(
@@ -50,7 +53,8 @@ describe('FormattedTime', () => {
'ko',
);
expect(wrapper.getByText('오후 7:02')).toBeTruthy();
element = wrapper.root.find((el) => el.type === 'Text' && el.children && el.children[0] === '오후 7:02');
expect(element).toBeTruthy();
wrapper = renderWithIntl(
<FormattedTime
@@ -60,7 +64,8 @@ describe('FormattedTime', () => {
'ko',
);
expect(wrapper.getByText('19:02')).toBeTruthy();
element = wrapper.root.find((el) => el.type === 'Text' && el.children && el.children[0] === '19:02');
expect(element).toBeTruthy();
});
it('should fallback to default short format for unsupported locale of react-intl ', () => {
@@ -73,7 +78,8 @@ describe('FormattedTime', () => {
'es',
);
expect(wrapper.getByText('8:47 AM')).toBeTruthy();
let element = wrapper.root.find((el) => el.type === 'Text' && el.children && el.children[0] === '8:47 AM');
expect(element).toBeTruthy();
wrapper = renderWithIntl(
<FormattedTime
@@ -84,10 +90,11 @@ describe('FormattedTime', () => {
'es',
);
expect(wrapper.getByText('8:47')).toBeTruthy();
element = wrapper.root.find((el) => el.type === 'Text' && el.children && el.children[0] === '8:47');
expect(element).toBeTruthy();
});
});
function renderWithIntl(component, locale = 'en') {
return render(<IntlProvider locale={locale}>{component}</IntlProvider>);
return TestRenderer.create(<IntlProvider locale={locale}>{component}</IntlProvider>);
}

View File

@@ -5,12 +5,12 @@ import {PropTypes} from 'prop-types';
import React from 'react';
import {intlShape} from 'react-intl';
import {
Clipboard,
Keyboard,
StyleSheet,
Text,
View,
} from 'react-native';
import Clipboard from '@react-native-community/clipboard';
import CustomPropTypes from 'app/constants/custom_prop_types';
import FormattedText from 'app/components/formatted_text';

View File

@@ -38,6 +38,7 @@ export default class MarkdownEmoji extends PureComponent {
editedIndicator: this.renderEditedIndicator,
emoji: this.renderEmoji,
paragraph: this.renderParagraph,
document: this.renderParagraph,
text: this.renderText,
},
});
@@ -66,7 +67,7 @@ export default class MarkdownEmoji extends PureComponent {
renderParagraph = ({children}) => {
const style = getStyleSheet(this.props.theme);
return (
<View style={style.block}>{children}</View>
<View style={style.block}><Text>{children}</Text></View>
);
};

View File

@@ -5,7 +5,6 @@ import PropTypes from 'prop-types';
import React from 'react';
import {intlShape} from 'react-intl';
import {
Clipboard,
Linking,
Platform,
StyleSheet,
@@ -13,6 +12,7 @@ import {
View,
} from 'react-native';
import FastImage from 'react-native-fast-image';
import Clipboard from '@react-native-community/clipboard';
import brokenImageIcon from '@assets/images/icons/brokenimage.png';
import ImageViewPort from '@components/image_viewport';

View File

@@ -3,7 +3,8 @@
import React, {Children, PureComponent} from 'react';
import PropTypes from 'prop-types';
import {Alert, Clipboard, Linking, Text} from 'react-native';
import {Alert, Linking, Text} from 'react-native';
import Clipboard from '@react-native-community/clipboard';
import urlParse from 'url-parse';
import {intlShape} from 'react-intl';

View File

@@ -281,12 +281,10 @@ export function highlightMentions(ast, mentionKeys) {
} else if (node.type === 'at_mention') {
const matches = mentionKeys.some((mention) => {
const mentionName = '@' + node.mentionName;
const flags = mention.caseSensitive ? '' : 'i';
const pattern = new RegExp(`${escapeRegex(mention.key)}\\.?`, flags);
if (mention.caseSensitive) {
return mention.key === mentionName;
}
return mention.key.toLowerCase() === mentionName.toLowerCase();
return pattern.test(mentionName);
});
if (!matches) {

View File

@@ -2751,6 +2751,30 @@ describe('Components.Markdown.transform', () => {
}],
}],
},
}, {
name: 'Mention followed by a period',
input: 'This is a mention for @channel.',
mentionKeys: [{key: 'channel'}],
expected: {
type: 'document',
children: [{
type: 'paragraph',
children: [{
type: 'text',
literal: 'This is a mention for ',
}, {
type: 'mention_highlight',
children: [{
type: 'at_mention',
_mentionName: 'channel.',
children: [{
type: 'text',
literal: '@channel.',
}],
}],
}],
}],
},
}];
for (const test of tests) {
@@ -3055,7 +3079,7 @@ function nodeToString(node) {
}
const ignoredKeys = {_sourcepos: true, _lastLineBlank: true, _open: true, _string_content: true, _info: true, _isFenced: true, _fenceChar: true, _fenceLength: true, _fenceOffset: true, _onEnter: true, _onExit: true};
function astToJson(node, visited = [], indent = '') { // eslint-disable-line @typescript-eslint/no-unused-vars
function astToJson(node, visited = [], indent = '') {
let out = '{';
const myVisited = [...visited];
@@ -3081,7 +3105,7 @@ function astToJson(node, visited = [], indent = '') { // eslint-disable-line @ty
} else if (typeof value === 'boolean') {
out += String(value);
} else if (typeof value === 'object') {
out += astToJson(value, myVisited, indent + ' ');
out += astToJson(value, myVisited, indent + ' '); // eslint-disable-line @typescript-eslint/no-unused-vars
}
if (i !== keys.length - 1) {

View File

@@ -11,12 +11,14 @@ import {ViewTypes} from '@constants';
import NetworkIndicator from './network_indicator';
jest.useFakeTimers();
describe('AttachmentFooter', () => {
Animated.sequence = jest.fn(() => ({
start: jest.fn((cb) => cb()),
}));
Animated.timing = jest.fn(() => ({
start: jest.fn(),
start: jest.fn((cb) => cb()),
}));
const baseProps = {
@@ -43,7 +45,7 @@ describe('AttachmentFooter', () => {
const wrapper = shallow(<NetworkIndicator {...baseProps}/>);
const instance = wrapper.instance();
it('emits INDICATOR_BAR_VISIBLE with true only if not already visible', () => {
it('emits INDICATOR_BAR_VISIBLE with true only if not already visible', async () => {
instance.visible = true;
instance.show();
expect(EventEmitter.emit).not.toHaveBeenCalled();
@@ -52,6 +54,7 @@ describe('AttachmentFooter', () => {
instance.show();
expect(EventEmitter.emit).toHaveBeenCalledWith(ViewTypes.INDICATOR_BAR_VISIBLE, true);
expect(instance.visible).toBe(true);
expect(wrapper.state('opacity')).toBe(1);
});
});
@@ -60,7 +63,7 @@ describe('AttachmentFooter', () => {
const wrapper = shallow(<NetworkIndicator {...baseProps}/>);
const instance = wrapper.instance();
it('emits INDICATOR_BAR_VISIBLE with false only if visible', () => {
it('emits INDICATOR_BAR_VISIBLE with false only if visible', async () => {
instance.visible = false;
instance.connected();
expect(EventEmitter.emit).not.toHaveBeenCalled();
@@ -69,6 +72,7 @@ describe('AttachmentFooter', () => {
instance.connected();
expect(EventEmitter.emit).toHaveBeenCalledWith(ViewTypes.INDICATOR_BAR_VISIBLE, false);
expect(instance.visible).toBe(false);
expect(wrapper.state('opacity')).toBe(0);
});
});
});

View File

@@ -1,12 +1,12 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`PasteableTextInput should render pasteable text input 1`] = `
<TextInput
<Component
allowFontScaling={true}
onPaste={[Function]}
rejectResponderTermination={true}
underlineColorAndroid="transparent"
>
My Text
</TextInput>
</Component>
`;

View File

@@ -33,6 +33,7 @@ const POST_TIMEOUT = 20000;
export function makeMapStateToProps() {
const memoizeHasEmojisOnly = memoizeResult((message, customEmojis) => hasEmojisOnly(message, customEmojis));
const getReactionsForPost = makeGetReactionsForPost();
const getMentionKeysForPost = makeGetMentionKeysForPost();
return (state, ownProps) => {
const post = ownProps.post;
@@ -104,7 +105,7 @@ export function makeMapStateToProps() {
isEmojiOnly,
shouldRenderJumboEmoji,
theme: getTheme(state),
mentionKeys: makeGetMentionKeysForPost(state, postProps?.disable_group_highlight, postProps?.mentionHighlightDisabled),
mentionKeys: getMentionKeysForPost(state, channel, postProps?.disable_group_highlight, postProps?.mentionHighlightDisabled),
canDelete,
...getDimensions(state),
};

View File

@@ -238,9 +238,10 @@ export default class PostBodyAdditionalContent extends ImageViewPort {
};
playYouTubeVideo = () => {
const {link} = this.props;
const videoId = getYouTubeVideoId(link);
const startTime = this.getYouTubeTime(link);
const {expandedLink, link} = this.props;
const videoLink = expandedLink || link;
const videoId = getYouTubeVideoId(videoLink);
const startTime = this.getYouTubeTime(videoLink);
if (Platform.OS === 'ios') {
YouTubeStandaloneIOS.
@@ -258,7 +259,7 @@ export default class PostBodyAdditionalContent extends ImageViewPort {
startTime,
}).catch(this.playYouTubeVideoError);
} else {
Linking.openURL(link);
Linking.openURL(videoLink);
}
}
};

View File

@@ -69,6 +69,7 @@ export function mapStateToProps(state, ownProps) {
state,
{
channel: currentChannel.id,
team: currentChannel.team_id,
permission: Permissions.USE_CHANNEL_MENTIONS,
default: true,
},
@@ -82,6 +83,7 @@ export function mapStateToProps(state, ownProps) {
channel: currentChannel.id,
team: currentChannel.team_id,
permission: Permissions.USE_GROUP_MENTIONS,
default: true,
},
);

View File

@@ -238,9 +238,12 @@ export default class PostDraft extends PureComponent {
);
};
doSubmitMessage = () => {
doSubmitMessage = (message = null) => {
const {createPost, currentUserId, channelId, files, handleClearFiles, rootId} = this.props;
const value = this.input.current?.getValue() || '';
let value = message;
if (!value) {
value = this.input.current?.getValue() || '';
}
const postFiles = files.filter((f) => !f.failed);
const post = {
user_id: currentUserId,
@@ -329,10 +332,10 @@ export default class PostDraft extends PureComponent {
return;
}
const value = this.input.current.getValue();
this.input.current.resetTextInput();
requestAnimationFrame(() => {
const value = this.input.current.getValue();
if (!this.isSendButtonEnabled()) {
this.input.current.setValue(value);
return;
@@ -373,12 +376,12 @@ export default class PostDraft extends PureComponent {
onPress: () => {
// Remove only failed files
handleClearFailedFiles(channelId, rootId);
this.sendMessage();
this.sendMessage(value);
},
}],
);
} else {
this.sendMessage();
this.sendMessage(value);
}
});
};
@@ -487,8 +490,7 @@ export default class PostDraft extends PureComponent {
return {groupMentionsSet, memberNotifyCount, channelTimezoneCount};
}
sendMessage = () => {
const value = this.input.current?.getValue() || '';
sendMessage = (value = '') => {
const {enableConfirmNotificationsToChannel, membersCount, useGroupMentions, useChannelMentions} = this.props;
const notificationsToChannel = enableConfirmNotificationsToChannel && useChannelMentions;
const notificationsToGroups = enableConfirmNotificationsToChannel && useGroupMentions;
@@ -504,10 +506,10 @@ export default class PostDraft extends PureComponent {
if (memberNotifyCount > 0) {
this.showSendToGroupsAlert(Array.from(groupMentionsSet), memberNotifyCount, channelTimezoneCount, value);
} else {
this.doSubmitMessage();
this.doSubmitMessage(value);
}
} else {
this.doSubmitMessage();
this.doSubmitMessage(value);
}
};

View File

@@ -13,6 +13,8 @@ jest.mock('react-native-image-picker', () => ({
launchCamera: jest.fn(),
}));
jest.useFakeTimers();
describe('PostDraft', () => {
const baseProps = {
addReactionToLatestPost: jest.fn(),
@@ -95,10 +97,12 @@ describe('PostDraft', () => {
getValue: () => message,
setValue: jest.fn(),
changeDraft: jest.fn(),
resetTextInput: jest.fn(),
},
};
instance.sendMessage();
instance.handleSendMessage();
jest.runOnlyPendingTimers();
expect(Alert.alert).toBeCalled();
expect(Alert.alert).toHaveBeenCalledWith('Confirm sending notifications to entire channel', expect.anything(), expect.anything());
});
@@ -118,9 +122,11 @@ describe('PostDraft', () => {
getValue: () => message,
setValue: jest.fn(),
changeDraft: jest.fn(),
resetTextInput: jest.fn(),
},
};
instance.sendMessage();
instance.handleSendMessage();
jest.runOnlyPendingTimers();
expect(Alert.alert).toBeCalled();
});
@@ -139,10 +145,12 @@ describe('PostDraft', () => {
getValue: () => message,
setValue: jest.fn(),
changeDraft: jest.fn(),
resetTextInput: jest.fn(),
},
};
instance.sendMessage();
instance.handleSendMessage();
jest.runOnlyPendingTimers();
expect(Alert.alert).not.toBeCalled();
});
@@ -162,10 +170,12 @@ describe('PostDraft', () => {
getValue: () => message,
setValue: jest.fn(),
changeDraft: jest.fn(),
resetTextInput: jest.fn(),
},
};
instance.sendMessage();
instance.handleSendMessage();
jest.runOnlyPendingTimers();
expect(Alert.alert).not.toHaveBeenCalled();
});
@@ -185,10 +195,12 @@ describe('PostDraft', () => {
getValue: () => message,
setValue: jest.fn(),
changeDraft: jest.fn(),
resetTextInput: jest.fn(),
},
};
instance.sendMessage();
instance.handleSendMessage();
jest.runOnlyPendingTimers();
expect(Alert.alert).not.toHaveBeenCalled();
});

View File

@@ -68,8 +68,10 @@ export default class FileQuickAction extends PureComponent {
}
}
// Decode file uri to get the actual path
res.uri = decodeURIComponent(res.uri);
if (Platform.OS === 'ios') {
// Decode file uri to get the actual path
res.uri = decodeURIComponent(res.uri);
}
this.props.onUploadFiles([res]);
} catch (error) {

View File

@@ -46,7 +46,7 @@ exports[`PostList setting channel deep link 1`] = `
onScrollToIndexFailed={[Function]}
onViewableItemsChanged={null}
refreshControl={
<RefreshControl
<RefreshControlMock
colors={
Array [
"#3d3c40",
@@ -123,7 +123,7 @@ exports[`PostList setting permalink deep link 1`] = `
onScrollToIndexFailed={[Function]}
onViewableItemsChanged={null}
refreshControl={
<RefreshControl
<RefreshControlMock
colors={
Array [
"#3d3c40",
@@ -200,7 +200,7 @@ exports[`PostList should match snapshot 1`] = `
onScrollToIndexFailed={[Function]}
onViewableItemsChanged={null}
refreshControl={
<RefreshControl
<RefreshControlMock
colors={
Array [
"#3d3c40",

View File

@@ -30,6 +30,7 @@ export default class ProgressiveImage extends PureComponent {
static defaultProps = {
style: {},
defaultSource: undefined,
resizeMode: 'contain',
};

View File

@@ -20,6 +20,7 @@ describe('ProgressiveImage', () => {
resizeMode: 'contain',
theme: Preferences.THEMES.default,
tintDefaultSource: false,
defaultSource: null,
};
const wrapper = shallow(<ProgressiveImage {...baseProps}/>);
@@ -50,6 +51,7 @@ describe('ProgressiveImage', () => {
resizeMode: 'contain',
theme: Preferences.THEMES.default,
tintDefaultSource: false,
defaultSource: null,
};
const wrapper = shallow(<ProgressiveImage {...baseProps}/>);

View File

@@ -10,10 +10,12 @@ import Preferences from '@mm-redux/constants/preferences';
describe('Reactions', () => {
const baseProps = {
theme: Preferences.THEMES.default,
recentEmojis: [],
addReaction: jest.fn(),
deviceWidth: undefined,
isLandscape: false,
openReactionScreen: jest.fn(),
recentEmojis: [],
theme: Preferences.THEMES.default,
};
test('Should match snapshot with default emojis', () => {

View File

@@ -125,7 +125,7 @@ describe('SafeAreaIos', () => {
expect(wrapper.state().safeAreaInsets).not.toEqual(TEST_INSETS_1.safeAreaInsets);
});
test('should set safe area insets on change if mounted and DeviceTypes.IS_IPHONE_WITH_INSETS is true', () => {
test('should set safe area insets on change if mounted and DeviceTypes.IS_IPHONE_WITH_INSETS is true', async () => {
DeviceTypes.IS_IPHONE_WITH_INSETS = true;
mattermostManaged.hasSafeAreaInsets = false;
@@ -140,7 +140,7 @@ describe('SafeAreaIos', () => {
expect(wrapper.state().safeAreaInsets).toEqual(TEST_INSETS_2.safeAreaInsets);
});
test('should set safe area insets on change if mounted and mattermostManaged.hasSafeAreaInsets is true', () => {
test('should set safe area insets on change if mounted and mattermostManaged.hasSafeAreaInsets is true', async () => {
DeviceTypes.IS_IPHONE_WITH_INSETS = false;
mattermostManaged.hasSafeAreaInsets = true;
@@ -155,7 +155,7 @@ describe('SafeAreaIos', () => {
expect(wrapper.state().safeAreaInsets).toEqual(TEST_INSETS_2.safeAreaInsets);
});
test('should not set safe area insets on change if mounted and neither DeviceTypes.IS_IPHONE_WITH_INSETS nor mattermostManaged.hasSafeAreaInsets is true', () => {
test('should not set safe area insets on change if mounted and neither DeviceTypes.IS_IPHONE_WITH_INSETS nor mattermostManaged.hasSafeAreaInsets is true', async () => {
DeviceTypes.IS_IPHONE_WITH_INSETS = false;
mattermostManaged.hasSafeAreaInsets = false;
@@ -170,7 +170,7 @@ describe('SafeAreaIos', () => {
expect(wrapper.state().safeAreaInsets).not.toEqual(TEST_INSETS_2.safeAreaInsets);
});
test('should set safe area insets on change not mounted', () => {
test('should set safe area insets on change not mounted', async () => {
DeviceTypes.IS_IPHONE_WITH_INSETS = true;
mattermostManaged.hasSafeAreaInsets = true;
@@ -186,7 +186,7 @@ describe('SafeAreaIos', () => {
expect(wrapper.state().safeAreaInsets).not.toEqual(TEST_INSETS_2.safeAreaInsets);
});
test('should set portrait safe area insets', () => {
test('should set portrait safe area insets', async () => {
const wrapper = shallow(
<SafeAreaIos {...baseProps}/>,
);
@@ -203,7 +203,7 @@ describe('SafeAreaIos', () => {
expect(EphemeralStore.safeAreaInsets[LANDSCAPE]).toEqual(null);
});
test('should set portrait safe area insets from EphemeralStore', () => {
test('should set portrait safe area insets from EphemeralStore', async () => {
const wrapper = shallow(
<SafeAreaIos {...baseProps}/>,
);
@@ -236,7 +236,7 @@ describe('SafeAreaIos', () => {
expect(EphemeralStore.safeAreaInsets[PORTRAIT]).toEqual(null);
});
test('should set landscape safe area insets from EphemeralStore', () => {
test('should set landscape safe area insets from EphemeralStore', async () => {
const wrapper = shallow(
<SafeAreaIos {...baseProps}/>,
);
@@ -252,7 +252,7 @@ describe('SafeAreaIos', () => {
expect(EphemeralStore.safeAreaInsets[PORTRAIT]).toEqual(null);
});
test('should add safeAreaInsetsForRootViewDidChange listener when EphemeralStore values are not set', () => {
test('should add safeAreaInsetsForRootViewDidChange listener when EphemeralStore values are not set', async () => {
const addEventListener = jest.spyOn(SafeArea, 'addEventListener');
expect(EphemeralStore.safeAreaInsets[PORTRAIT]).toEqual(null);
@@ -281,7 +281,7 @@ describe('SafeAreaIos', () => {
expect(addEventListener).not.toHaveBeenCalled();
});
test('should remove safeAreaInsetsForRootViewDidChange listener when EphemeralStore values are set', () => {
test('should remove safeAreaInsetsForRootViewDidChange listener when EphemeralStore values are set', async () => {
const removeEventListener = jest.spyOn(SafeArea, 'removeEventListener');
const wrapper = shallow(

View File

@@ -1,7 +1,7 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`ChannelItem should match snapshot 1`] = `
<Component
<ForwardRef
onPress={[Function]}
underlayColor="rgba(69,120,191,0.5)"
>
@@ -99,11 +99,11 @@ exports[`ChannelItem should match snapshot 1`] = `
</Text>
</View>
</View>
</Component>
</ForwardRef>
`;
exports[`ChannelItem should match snapshot for current user i.e currentUser (you) 1`] = `
<Component
<ForwardRef
onPress={[Function]}
underlayColor="rgba(69,120,191,0.5)"
>
@@ -211,11 +211,11 @@ exports[`ChannelItem should match snapshot for current user i.e currentUser (you
</Text>
</View>
</View>
</Component>
</ForwardRef>
`;
exports[`ChannelItem should match snapshot for current user i.e currentUser (you) when isSearchResult 1`] = `
<Component
<ForwardRef
onPress={[Function]}
underlayColor="rgba(69,120,191,0.5)"
>
@@ -323,11 +323,11 @@ exports[`ChannelItem should match snapshot for current user i.e currentUser (you
</Text>
</View>
</View>
</Component>
</ForwardRef>
`;
exports[`ChannelItem should match snapshot for deactivated user and is currentChannel 1`] = `
<Component
<ForwardRef
onPress={[Function]}
underlayColor="rgba(69,120,191,0.5)"
>
@@ -435,11 +435,11 @@ exports[`ChannelItem should match snapshot for deactivated user and is currentCh
</Text>
</View>
</View>
</Component>
</ForwardRef>
`;
exports[`ChannelItem should match snapshot for deactivated user and is searchResult 1`] = `
<Component
<ForwardRef
onPress={[Function]}
underlayColor="rgba(69,120,191,0.5)"
>
@@ -537,11 +537,11 @@ exports[`ChannelItem should match snapshot for deactivated user and is searchRes
</Text>
</View>
</View>
</Component>
</ForwardRef>
`;
exports[`ChannelItem should match snapshot for deactivated user and not searchResults or currentChannel 1`] = `
<Component
<ForwardRef
onPress={[Function]}
underlayColor="rgba(69,120,191,0.5)"
>
@@ -639,11 +639,11 @@ exports[`ChannelItem should match snapshot for deactivated user and not searchRe
</Text>
</View>
</View>
</Component>
</ForwardRef>
`;
exports[`ChannelItem should match snapshot for isManualUnread 1`] = `
<Component
<ForwardRef
onPress={[Function]}
underlayColor="rgba(69,120,191,0.5)"
>
@@ -741,7 +741,7 @@ exports[`ChannelItem should match snapshot for isManualUnread 1`] = `
</Text>
</View>
</View>
</Component>
</ForwardRef>
`;
exports[`ChannelItem should match snapshot for no displayName 1`] = `null`;
@@ -749,7 +749,7 @@ exports[`ChannelItem should match snapshot for no displayName 1`] = `null`;
exports[`ChannelItem should match snapshot for showUnreadForMsgs 1`] = `null`;
exports[`ChannelItem should match snapshot with draft 1`] = `
<Component
<ForwardRef
onPress={[Function]}
underlayColor="rgba(69,120,191,0.5)"
>
@@ -847,11 +847,11 @@ exports[`ChannelItem should match snapshot with draft 1`] = `
</Text>
</View>
</View>
</Component>
</ForwardRef>
`;
exports[`ChannelItem should match snapshot with mentions and muted 1`] = `
<Component
<ForwardRef
onPress={[Function]}
underlayColor="rgba(69,120,191,0.5)"
>
@@ -982,5 +982,5 @@ exports[`ChannelItem should match snapshot with mentions and muted 1`] = `
/>
</View>
</View>
</Component>
</ForwardRef>
`;

View File

@@ -50,7 +50,7 @@ export default class MainSidebarIOS extends MainSidebarBase {
if (this.state.drawerOpened && this.drawerRef?.current) {
this.drawerRef.current.closeDrawer();
} else if (this.drawerSwiper && DeviceTypes.IS_TABLET) {
this.resetDrawer(true);
this.resetDrawer();
}
};

View File

@@ -156,6 +156,9 @@ export default class MainSidebarBase extends Component {
onSearchEnds = () => {
this.setState({searching: false});
if (this.drawerRef?.current) {
this.drawerRef.current.canClose = true;
}
};
onSearchStart = () => {

View File

@@ -285,9 +285,9 @@ export default class SlideUpPanel extends PureComponent {
>
<Animated.View>
<SlideUpPanelIndicator/>
{headerComponent}
</Animated.View>
</PanGestureHandler>
{headerComponent}
<PanGestureHandler
ref={this.panRef}
simultaneousHandlers={[this.scrollRef, this.masterRef]}

View File

@@ -47,7 +47,7 @@ exports[`components/widgets/settings/RadioSetting should match snapshot when err
]
}
>
<Component
<ForwardRef
onPress={[Function]}
>
<View
@@ -87,8 +87,8 @@ exports[`components/widgets/settings/RadioSetting should match snapshot when err
}
}
/>
</Component>
<Component
</ForwardRef>
<ForwardRef
onPress={[Function]}
>
<View
@@ -128,8 +128,8 @@ exports[`components/widgets/settings/RadioSetting should match snapshot when err
}
}
/>
</Component>
<Component
</ForwardRef>
<ForwardRef
onPress={[Function]}
>
<View
@@ -164,7 +164,7 @@ exports[`components/widgets/settings/RadioSetting should match snapshot when err
width={12}
/>
</View>
</Component>
</ForwardRef>
</View>
<View
style={null}

View File

@@ -18,4 +18,6 @@ const NavigationTypes = keyMirror({
BLUR_POST_DRAFT: null,
});
NavigationTypes.CHANNEL_SCREEN = 'Channel';
export default NavigationTypes;

View File

@@ -20,10 +20,15 @@ import mattermostManaged from 'app/mattermost_managed';
export const HEADER_X_CLUSTER_ID = 'X-Cluster-Id';
export const HEADER_TOKEN = 'Token';
const DEFAULT_TIMEOUT = 10000;
let managedConfig;
mattermostManaged.addEventListener('managedConfigDidChange', (config) => {
if (config?.timeout !== managedConfig?.timeout) {
initFetchConfig();
return;
}
managedConfig = config;
});
@@ -150,11 +155,16 @@ Client4.doFetchWithResponse = async (url, options) => {
const initFetchConfig = async () => {
const fetchConfig = {
auto: true,
timeout: 5000, // Set the base timeout for every request to 5s
timeout: DEFAULT_TIMEOUT, // Set the base timeout for every request to 5s
};
try {
managedConfig = await mattermostManaged.getConfig();
if (managedConfig?.timeout) {
const timeout = parseInt(managedConfig.timeout, 10);
fetchConfig.timeout = timeout || DEFAULT_TIMEOUT;
}
} catch {
// no managed config
}

View File

@@ -124,7 +124,9 @@ export function componentDidAppearListener({componentId}) {
}
export function componentDidDisappearListener({componentId}) {
EphemeralStore.removeNavigationComponentId(componentId);
if (componentId !== NavigationTypes.CHANNEL_SCREEN) {
EphemeralStore.removeNavigationComponentId(componentId);
}
if (componentId === 'MainSidebar') {
EventEmitter.emit(NavigationTypes.MAIN_SIDEBAR_DID_CLOSE);

View File

@@ -3126,6 +3126,7 @@ export class ClientError extends Error {
intl: { defaultMessage: string; id: string } | { defaultMessage: string; id: string } | { id: string; defaultMessage: string; values: any } | { id: string; defaultMessage: string };
server_error_id: any;
status_code: any;
details: Error;
constructor(baseUrl: string, data: any) {
super(data.message + ': ' + cleanUrlForLogging(baseUrl, data.url));
@@ -3134,6 +3135,7 @@ export class ClientError extends Error {
this.intl = data.intl;
this.server_error_id = data.server_error_id;
this.status_code = data.status_code;
this.details = data.details;
// Ensure message is treated as a property of this class when object spreading. Without this,
// copying the object by using `{...error}` would not include the message.

View File

@@ -44,6 +44,7 @@ export default {
RESTRICT_DIRECT_MESSAGE_ANY: 'any',
RESTRICT_DIRECT_MESSAGE_TEAM: 'team',
SWITCH_TO_DEFAULT_CHANNEL: 'switch_to_default_channel',
REMOVED_FROM_CHANNEL: 'removed_from_channel',
DEFAULT_CHANNEL: 'town-square',
DM_CHANNEL: 'D',
OPEN_CHANNEL: 'O',

View File

@@ -2,15 +2,13 @@
// See LICENSE.txt for license information.
import * as reselect from 'reselect';
import {GlobalState} from '@mm-redux/types/store';
import {Dictionary} from '@mm-redux/types/utilities';
import {Dictionary, NameMappedObjects} from '@mm-redux/types/utilities';
import {Group} from '@mm-redux/types/groups';
import {filterGroupsMatchingTerm} from '@mm-redux/utils/group_utils';
import {getCurrentUserLocale} from '@mm-redux/selectors/entities/i18n';
import {getChannel} from '@mm-redux/selectors/entities/channels';
import {UserMentionKey} from '@mm-redux/selectors/entities/users';
import {haveIChannelPermission} from '@mm-redux/selectors/entities/roles';
import {getTeam} from '@mm-redux/selectors/entities/teams';
import {Permissions} from '@mm-redux/constants';
const emptyList: any[] = [];
const emptySyncables = {
@@ -73,15 +71,6 @@ export function getAssociatedGroupsForReference(state: GlobalState, teamId: stri
const channel = getChannel(state, channelId);
const locale = getCurrentUserLocale(state);
if (!haveIChannelPermission(state, {
permission: Permissions.USE_GROUP_MENTIONS,
channel: channelId,
team: teamId,
default: true,
})) {
return emptyList;
}
let groupsForReference = [];
if (team && team.group_constrained && channel && channel.group_constrained) {
const groupsFromChannel = getGroupsAssociatedToChannelForReference(state, channelId);
@@ -173,24 +162,8 @@ export const getAllAssociatedGroupsForReference = reselect.createSelector(
},
);
export const getMyAllowReferencedGroups = reselect.createSelector(
getMyGroups,
(myGroups) => {
return Object.values(myGroups).filter((group) => group.allow_reference && group.delete_at === 0);
},
);
export const getCurrentUserGroupMentionKeys = reselect.createSelector(
getMyAllowReferencedGroups,
(groups: Array<Group>) => {
const keys: UserMentionKey[] = [];
groups.forEach((group) => keys.push({key: `@${group.name}`}));
return keys;
},
);
export const getGroupsByName = reselect.createSelector(
getAllGroups,
export const getAssociatedGroupsByName: (state: GlobalState, teamID: string, channelId: string) => NameMappedObjects<Group> = reselect.createSelector(
getAssociatedGroupsForReference,
(groups) => {
const groupsByName: Dictionary<Group> = {};
@@ -201,3 +174,49 @@ export const getGroupsByName = reselect.createSelector(
return groupsByName;
},
);
export const getAllGroupsForReferenceByName: (state: GlobalState) => NameMappedObjects<Group> = reselect.createSelector(
getAllAssociatedGroupsForReference,
(groups) => {
const groupsByName: Dictionary<Group> = {};
Object.values(groups).forEach((group) => {
groupsByName[group.name] = group;
});
return groupsByName;
},
);
export const getMyAllowReferencedGroups = reselect.createSelector(
getMyGroups,
(myGroups) => {
return Object.values(myGroups).filter((group) => group.allow_reference && group.delete_at === 0);
},
);
export const getMyGroupMentionKeys: (state: GlobalState) => UserMentionKey[] = reselect.createSelector(
getMyAllowReferencedGroups,
(groups: Array<Group>) => {
const keys: UserMentionKey[] = [];
groups.forEach((group) => keys.push({key: `@${group.name}`}));
return keys;
},
);
export const getMyGroupsAssociatedToChannelForReference: (state: GlobalState, teamId: string, channelId: string) => Group[] = reselect.createSelector(
getMyGroups,
getAssociatedGroupsByName,
(myGroups, groups) => {
return Object.values(myGroups).filter((group) => group.allow_reference && group.delete_at === 0 && groups[group.name]);
},
);
export const getMyGroupMentionKeysForChannel: (state: GlobalState, teamId: string, channelId: string) => UserMentionKey[] = reselect.createSelector(
getMyGroupsAssociatedToChannelForReference,
(groups: Array<Group>) => {
const keys: UserMentionKey[] = [];
groups.forEach((group) => keys.push({key: `@${group.name}`}));
return keys;
},
);

View File

@@ -69,23 +69,20 @@ export const getPostsInCurrentChannel: (a: GlobalState) => Array<PostWithFormatD
export function makeGetPostIdsForThread(): (b: GlobalState, a: $ID<Post>) => Array<$ID<Post>> {
return createIdsSelector(
getAllPosts,
(state: GlobalState, rootId: string) => state.entities.posts.postsInThread[rootId] || [],
(state: GlobalState, rootId) => state.entities.posts.posts[rootId],
(posts, postsForThread, rootPost) => {
(state: GlobalState, rootId: string) => state.entities.posts.posts[rootId],
(posts, rootPost) => {
const thread: Post[] = [];
if (rootPost) {
thread.push(rootPost);
}
postsForThread.forEach((id) => {
const post = posts[id];
if (post) {
thread.push(post);
const postsArray = Object.values(posts).filter((p) => p.root_id === rootPost.id);
if (postsArray.length) {
thread.push(...postsArray);
}
});
thread.sort(comparePosts);
thread.sort(comparePosts);
}
return thread.map((post) => post.id);
},

View File

@@ -104,7 +104,7 @@ export const getMyCurrentChannelPermissions = reselect.createSelector(
for (const permission of roles[roleName].permissions) {
permissions.add(permission);
}
roleFound = true && teamRoleFound;
roleFound = teamRoleFound;
}
}
}
@@ -144,8 +144,10 @@ export const getMyChannelPermissions = reselect.createSelector(
getMyChannelRoles,
getRoles,
getMyTeamPermissions,
(state, options: PermissionsOptions) => options.channel,
(myChannelRoles, roles, {permissions: teamPermissions, roleFound: teamRoleFound}, channelId) => {
(state, options: PermissionsOptions) => options,
(myChannelRoles, roles, {permissions: teamPermissions, roleFound: teamRoleFound}, options: PermissionsOptions) => {
const channelId = options.channel;
const teamId = options.team;
const permissions = new Set<string>();
let roleFound = false;
if (myChannelRoles[channelId!]) {
@@ -154,7 +156,7 @@ export const getMyChannelPermissions = reselect.createSelector(
for (const permission of roles[roleName].permissions) {
permissions.add(permission);
}
roleFound = true && teamRoleFound;
roleFound = teamRoleFound || !teamId;
}
}
}

View File

@@ -65,38 +65,104 @@ describe('Selectors.Search', () => {
assert.deepEqual(Selectors.getAllUserMentionKeys(state), [{key: 'First', caseSensitive: true}, {key: '@user'}, {key: '@I-AM-THE-BEST!'}, {key: '@Do-you-love-me?'}]);
});
describe('makeGetMentionKeysForPost', () => {
it('should return all mentionKeys', () => {
const postProps = {
disable_group_highlight: false,
mentionHighlightDisabled: false,
};
const state = {
entities: {
users: {
currentUserId: 'a123',
profiles: {
a123: {
username: 'a123',
notify_props: {
channel: 'true',
},
},
},
},
groups: {
myGroups: {
developers: {
id: 123,
name: 'developers',
allow_reference: true,
delete_at: 0,
describe('getMentionKeysForPost', () => {
const group = {
id: 123,
name: 'developers',
allow_reference: true,
delete_at: 0,
};
const group2 = {
id: 1234,
name: 'developers2',
allow_reference: true,
delete_at: 0,
};
const channel1 = {...TestHelper.fakeChannelWithId(team1.id), group_constrained: true};
const channel2 = {...TestHelper.fakeChannelWithId(team1.id), group_constrained: false};
const state = {
entities: {
users: {
currentUserId: 'a123',
profiles: {
a123: {
username: 'a123',
notify_props: {
channel: 'true',
},
},
},
},
groups: {
syncables: {},
members: {},
groups: {
[group.name]: group,
[group2.name]: group2,
},
myGroups: {
[group.name]: group,
[group2.name]: group2,
},
},
teams: {
teams: {
[team1.id]: team1,
},
groupsAssociatedToTeam: {
[team1.id]: {ids: []},
},
},
channels: {
channels: {
[channel1.id]: channel1,
[channel2.id]: channel2,
},
groupsAssociatedToChannel: {
[channel1.id]: {ids: [group.id]},
[channel2.id]: {ids: []},
},
},
general: {
config: {},
},
preferences: {
myPreferences: {},
},
},
};
const getMentionKeysForPost = Selectors.makeGetMentionKeysForPost();
it('should return all mentionKeys for post if null channel given', () => {
const postProps = {
disable_group_highlight: false,
mentionHighlightDisabled: false,
};
const results = Selectors.makeGetMentionKeysForPost(state, postProps.disable_group_highlight, postProps.mentionHighlightDisabled);
const results = getMentionKeysForPost(state, null, postProps.disable_group_highlight, postProps.mentionHighlightDisabled);
const expected = [{key: '@channel'}, {key: '@all'}, {key: '@here'}, {key: '@a123'}, {key: '@developers'}, {key: '@developers2'}];
assert.deepEqual(results, expected);
});
it('should return all mentionKeys for post made in non group constrained channel', () => {
const postProps = {
disable_group_highlight: false,
mentionHighlightDisabled: false,
};
const results = getMentionKeysForPost(state, channel2, postProps.disable_group_highlight, postProps.mentionHighlightDisabled);
const expected = [{key: '@channel'}, {key: '@all'}, {key: '@here'}, {key: '@a123'}, {key: '@developers'}, {key: '@developers2'}];
assert.deepEqual(results, expected);
});
it('should return mentionKeys for post made in group constrained channel', () => {
const postProps = {
disable_group_highlight: false,
mentionHighlightDisabled: false,
};
const results = getMentionKeysForPost(state, channel1, postProps.disable_group_highlight, postProps.mentionHighlightDisabled);
const expected = [{key: '@channel'}, {key: '@all'}, {key: '@here'}, {key: '@a123'}, {key: '@developers'}];
assert.deepEqual(results, expected);
});
@@ -106,32 +172,7 @@ describe('Selectors.Search', () => {
disable_group_highlight: true,
mentionHighlightDisabled: false,
};
const state = {
entities: {
users: {
currentUserId: 'a123',
profiles: {
a123: {
username: 'a123',
notify_props: {
channel: 'true',
},
},
},
},
groups: {
myGroups: {
developers: {
id: 123,
name: 'developers',
allow_reference: true,
delete_at: 0,
},
},
},
},
};
const results = Selectors.makeGetMentionKeysForPost(state, postProps.disable_group_highlight, postProps.mentionHighlightDisabled);
const results = getMentionKeysForPost(state, channel1, postProps.disable_group_highlight, postProps.mentionHighlightDisabled);
const expected = [{key: '@channel'}, {key: '@all'}, {key: '@here'}, {key: '@a123'}];
assert.deepEqual(results, expected);
});
@@ -141,32 +182,7 @@ describe('Selectors.Search', () => {
disable_group_highlight: false,
mentionHighlightDisabled: true,
};
const state = {
entities: {
users: {
currentUserId: 'a123',
profiles: {
a123: {
username: 'a123',
notify_props: {
channel: 'true',
},
},
},
},
groups: {
myGroups: {
developers: {
id: 123,
name: 'developers',
allow_reference: true,
delete_at: 0,
},
},
},
},
};
const results = Selectors.makeGetMentionKeysForPost(state, postProps.disable_group_highlight, postProps.mentionHighlightDisabled);
const results = getMentionKeysForPost(state, channel1, postProps.disable_group_highlight, postProps.mentionHighlightDisabled);
const expected = [{key: '@a123'}, {key: '@developers'}];
assert.deepEqual(results, expected);
});
@@ -176,32 +192,7 @@ describe('Selectors.Search', () => {
disable_group_highlight: true,
mentionHighlightDisabled: true,
};
const state = {
entities: {
users: {
currentUserId: 'a123',
profiles: {
a123: {
username: 'a123',
notify_props: {
channel: 'true',
},
},
},
},
groups: {
myGroups: {
developers: {
id: 123,
name: 'developers',
allow_reference: true,
delete_at: 0,
},
},
},
},
};
const results = Selectors.makeGetMentionKeysForPost(state, postProps.disable_group_highlight, postProps.mentionHighlightDisabled);
const results = getMentionKeysForPost(state, channel1, postProps.disable_group_highlight, postProps.mentionHighlightDisabled);
const expected = [{key: '@a123'}];
assert.deepEqual(results, expected);
});

View File

@@ -3,9 +3,10 @@
import * as reselect from 'reselect';
import {GlobalState} from '@mm-redux/types/store';
import {Channel} from '@mm-redux/types/channels';
import {UserMentionKey} from './users';
import {getCurrentUserMentionKeys} from '@mm-redux/selectors/entities/users';
import {getCurrentUserGroupMentionKeys} from '@mm-redux/selectors/entities/groups';
import {getMyGroupMentionKeys, getMyGroupMentionKeysForChannel} from '@mm-redux/selectors/entities/groups';
import {getCurrentTeamId} from '@mm-redux/selectors/entities/teams';
@@ -19,29 +20,30 @@ export const getCurrentSearchForCurrentTeam = reselect.createSelector(
export const getAllUserMentionKeys: (state: GlobalState) => UserMentionKey[] = reselect.createSelector(
getCurrentUserMentionKeys,
(state: GlobalState) => getCurrentUserGroupMentionKeys(state),
(state: GlobalState) => getMyGroupMentionKeys(state),
(userMentionKeys, groupMentionKeys) => {
return userMentionKeys.concat(groupMentionKeys);
},
);
export const makeGetMentionKeysForPost: (state: GlobalState, disableGroupHighlight: boolean, mentionHighlightDisabled: boolean) => UserMentionKey[] = reselect.createSelector(
getAllUserMentionKeys,
getCurrentUserMentionKeys,
(state: GlobalState, disableGroupHighlight: boolean) => disableGroupHighlight,
(state: GlobalState, disableGroupHighlight: boolean, mentionHighlightDisabled: boolean) => mentionHighlightDisabled,
(allMentionKeys, mentionKeysWithoutGroups, disableGroupHighlight = false, mentionHighlightDisabled = false) => {
let mentionKeys = allMentionKeys;
if (disableGroupHighlight) {
mentionKeys = mentionKeysWithoutGroups;
}
export function makeGetMentionKeysForPost(): (state: GlobalState, channel: Channel, disableGroupHighlight: boolean, mentionHighlightDisabled: boolean) => UserMentionKey[] {
return reselect.createSelector(
getCurrentUserMentionKeys,
(state: GlobalState, channel: Channel) => (channel?.id ? getMyGroupMentionKeysForChannel(state, channel?.team_id, channel?.id) : getMyGroupMentionKeys(state)),
(state: GlobalState, channel: Channel, disableGroupHighlight: boolean) => disableGroupHighlight,
(state: GlobalState, channel: Channel, disableGroupHighlight: boolean, mentionHighlightDisabled: boolean) => mentionHighlightDisabled,
(mentionKeysWithoutGroups, groupMentionKeys, disableGroupHighlight = false, mentionHighlightDisabled = false) => {
let mentionKeys = mentionKeysWithoutGroups;
if (!disableGroupHighlight) {
mentionKeys = mentionKeys.concat(groupMentionKeys);
}
if (mentionHighlightDisabled) {
const CHANNEL_MENTIONS = ['@all', '@channel', '@here'];
mentionKeys = mentionKeys.filter((value) => !CHANNEL_MENTIONS.includes(value.key));
}
return mentionKeys;
},
);
if (mentionHighlightDisabled) {
const CHANNEL_MENTIONS = ['@all', '@channel', '@here'];
mentionKeys = mentionKeys.filter((value) => !CHANNEL_MENTIONS.includes(value.key));
}
return mentionKeys;
},
);
}

View File

@@ -29,10 +29,9 @@ type BatchAction = {
export type Action = GenericAction | Thunk | BatchAction | ActionFunc;
export type ActionResult = {
data: any;
} | {
error: any;
};
data?: any;
error?: any;
}
export type DispatchFunc = (action: Action, getState?: GetStateFunc | null) => Promise<ActionResult>;
export type ActionFunc = (dispatch: DispatchFunc, getState: GetStateFunc) => Promise<ActionResult|ActionResult[]> | ActionResult;

View File

@@ -13,3 +13,8 @@ export type GeneralState = {
serverVersion: string;
timezones: Array<string>;
};
export type FormattedMsg = {
id: string;
defaultMessage: string;
};

View File

@@ -2,4 +2,5 @@
// See LICENSE.txt for license information.
declare module 'gfycat-sdk';
declare module 'remote-redux-devtools';
declare module 'redux-action-buffer';
declare module 'redux-action-buffer';
declare module 'react-intl';

View File

@@ -62,11 +62,18 @@ export function canDeletePost(state: GlobalState, config: any, license: any, tea
const isOwner = isPostOwner(userId, post);
if (hasNewPermissions(state)) {
const canDelete = haveIChannelPermission(state, {team: teamId, channel: channelId, permission: Permissions.DELETE_POST});
if (!isOwner) {
return canDelete && haveIChannelPermission(state, {team: teamId, channel: channelId, permission: Permissions.DELETE_OTHERS_POSTS});
let permissions = [];
if (isOwner) {
permissions = [Permissions.DELETE_POST];
} else {
const {serverVersion} = state.entities.general;
// prior to v5.27, the server used to require delete_own_posts and
// delete_others_posts permissions to be able to delete a post by a
// different author.
permissions = isMinimumServerVersion(serverVersion, 5, 27) ? [Permissions.DELETE_OTHERS_POSTS] : [Permissions.DELETE_POST, Permissions.DELETE_OTHERS_POSTS];
}
return canDelete;
return permissions.every((permission) => haveIChannelPermission(state, {team: teamId, channel: channelId, permission, default: false}));
}
// Backwards compatibility with pre-advanced permissions config settings.
@@ -87,11 +94,12 @@ export function canEditPost(state: GlobalState, config: any, license: any, teamI
let canEdit = true;
if (hasNewPermissions(state)) {
const {serverVersion} = state.entities.general;
let permissions = [];
if (isOwner) {
permissions = [Permissions.EDIT_POST];
} else {
const {serverVersion} = state.entities.general;
// prior to v5.26, the server used to require edit_own_posts and
// edit_others_posts permissions to be able to edit a post by a
// different author.

View File

@@ -4,8 +4,9 @@
import React, {PureComponent} from 'react';
import PropTypes from 'prop-types';
import {intlShape} from 'react-intl';
import {Animated, Keyboard, StyleSheet} from 'react-native';
import {Alert, Animated, Keyboard, StyleSheet} from 'react-native';
import MaterialIcon from 'react-native-vector-icons/MaterialIcons';
import {General} from '@mm-redux/constants';
import {showModal, showModalOverCurrentContext} from '@actions/navigation';
import LocalConfig from '@assets/config';
@@ -83,6 +84,7 @@ export default class ChannelBase extends PureComponent {
EventEmitter.on(NavigationTypes.BLUR_POST_DRAFT, this.blurPostDraft);
EventEmitter.on('leave_team', this.handleLeaveTeam);
EventEmitter.on(TYPING_VISIBLE, this.runTypingAnimations);
EventEmitter.on(General.REMOVED_FROM_CHANNEL, this.handleRemovedFromChannel);
if (currentTeamId) {
this.loadChannels(currentTeamId);
@@ -154,6 +156,7 @@ export default class ChannelBase extends PureComponent {
EventEmitter.off(NavigationTypes.BLUR_POST_DRAFT, this.blurPostDraft);
EventEmitter.off('leave_team', this.handleLeaveTeam);
EventEmitter.off(TYPING_VISIBLE, this.runTypingAnimations);
EventEmitter.off(General.REMOVED_FROM_CHANNEL, this.handleRemovedFromChannel);
}
registerTypingAnimation = (animation) => {
@@ -203,6 +206,21 @@ export default class ChannelBase extends PureComponent {
this.props.actions.selectDefaultTeam();
};
handleRemovedFromChannel = (channelName) => {
const {formatMessage} = this.context.intl;
Alert.alert(
formatMessage({
id: 'mobile.user_removed.title',
defaultMessage: 'Removed from {channelName}',
}, {channelName}),
formatMessage({
id: 'mobile.user_removed.message',
defaultMessage: 'You were removed from the channel.',
}),
);
};
loadChannels = (teamId) => {
const {loadChannelsForTeam, selectInitialChannel} = this.props.actions;
if (EphemeralStore.getStartFromNotification()) {

View File

@@ -2,7 +2,10 @@
// See LICENSE.txt for license information.
import React from 'react';
import {Alert} from 'react-native';
import {shallow} from 'enzyme';
import EventEmitter from '@mm-redux/utils/event_emitter';
import {General} from '@mm-redux/constants';
import Preferences from '@mm-redux/constants/preferences';
@@ -95,4 +98,15 @@ describe('ChannelBase', () => {
removeAnimation();
expect(instance.typingAnimations).toStrictEqual([]);
});
test('should display an alert when the user is removed from the current channel', () => {
const alert = jest.spyOn(Alert, 'alert');
shallow(
<ChannelBase {...baseProps}/>,
{context: {intl: {formatMessage: jest.fn()}}},
);
EventEmitter.emit(General.REMOVED_FROM_CHANNEL);
expect(alert).toHaveBeenCalled();
});
});

View File

@@ -1,7 +1,7 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`ChannelDrawerButton should match, full snapshot 1`] = `
<Component
<ForwardRef
accessibilityHint="Opens the channels and teams drawer"
accessibilityLabel="Channels and teams"
accessibilityRole="button"
@@ -36,11 +36,11 @@ exports[`ChannelDrawerButton should match, full snapshot 1`] = `
/>
</View>
</View>
</Component>
</ForwardRef>
`;
exports[`ChannelDrawerButton should match, full snapshot 2`] = `
<Component
<ForwardRef
accessibilityHint="Opens the channels and teams drawer"
accessibilityLabel="Channels and teams"
accessibilityRole="button"
@@ -104,5 +104,5 @@ exports[`ChannelDrawerButton should match, full snapshot 2`] = `
/>
</View>
</View>
</Component>
</ForwardRef>
`;

View File

@@ -62,11 +62,14 @@ export default class ChannelNavBar extends PureComponent {
}
};
handlePermanentSidebar = () => {
handlePermanentSidebar = async () => {
if (DeviceTypes.IS_TABLET && this.mounted) {
AsyncStorage.getItem(DeviceTypes.PERMANENT_SIDEBAR_SETTINGS).then((enabled) => {
try {
const enabled = await AsyncStorage.getItem(DeviceTypes.PERMANENT_SIDEBAR_SETTINGS);
this.setState({permanentSidebar: enabled === 'true'});
});
} catch {
// do nothing
}
}
};

View File

@@ -33,12 +33,12 @@ describe('ChannelNavBar', () => {
expect(wrapper.getElement()).toMatchSnapshot();
});
test('should not set the permanentSidebar state if not Tablet', () => {
test('should not set the permanentSidebar state if not Tablet', async () => {
const wrapper = shallow(
<ChannelNavBar {...baseProps}/>,
);
wrapper.instance().handlePermanentSidebar();
await wrapper.instance().handlePermanentSidebar();
expect(wrapper.state('permanentSidebar')).toBeUndefined();
});
@@ -50,11 +50,10 @@ describe('ChannelNavBar', () => {
DeviceTypes.IS_TABLET = true;
await wrapper.instance().handlePermanentSidebar();
expect(wrapper.state('permanentSidebar')).toBeDefined();
});
test('drawerButtonVisible appears for android tablets', () => {
test('drawerButtonVisible appears for android tablets', async () => {
const wrapper = shallow(
<ChannelNavBar {...baseProps}/>,
);
@@ -65,7 +64,7 @@ describe('ChannelNavBar', () => {
expect(wrapper.instance().drawerButtonVisible()).toBe(true);
});
test('drawerButtonVisible appears for android phones', () => {
test('drawerButtonVisible appears for android phones', async () => {
const wrapper = shallow(
<ChannelNavBar {...baseProps}/>,
);
@@ -76,7 +75,7 @@ describe('ChannelNavBar', () => {
expect(wrapper.instance().drawerButtonVisible()).toBe(true);
});
test('drawerButtonVisible appears for iOS phones', () => {
test('drawerButtonVisible appears for iOS phones', async () => {
const wrapper = shallow(
<ChannelNavBar {...baseProps}/>,
);
@@ -87,7 +86,7 @@ describe('ChannelNavBar', () => {
expect(wrapper.instance().drawerButtonVisible()).toBe(true);
});
test('drawerButtonVisible appears for iOS tablets with PermanentSidebar at default false, and not in splitview', () => {
test('drawerButtonVisible appears for iOS tablets with PermanentSidebar at default false, and not in splitview', async () => {
const wrapper = shallow(
<ChannelNavBar {...baseProps}/>,
);
@@ -100,7 +99,7 @@ describe('ChannelNavBar', () => {
expect(wrapper.instance().drawerButtonVisible()).toBe(true);
});
test('drawerButtonVisible does not appear for iOS tablets with permanentSidebar enabled', () => {
test('drawerButtonVisible does not appear for iOS tablets with permanentSidebar enabled', async () => {
const wrapper = shallow(
<ChannelNavBar {...baseProps}/>,
);
@@ -113,7 +112,7 @@ describe('ChannelNavBar', () => {
expect(wrapper.instance().drawerButtonVisible()).toBe(false);
});
test('drawerButtonVisible appears for iOS tablets with splitview enabled', () => {
test('drawerButtonVisible appears for iOS tablets with splitview enabled', async () => {
const wrapper = shallow(
<ChannelNavBar {...baseProps}/>,
);

View File

@@ -1,7 +1,7 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`ChannelTitle should match snapshot 1`] = `
<Component
<ForwardRef
style={
Object {
"flex": 1,
@@ -47,11 +47,11 @@ exports[`ChannelTitle should match snapshot 1`] = `
}
/>
</View>
</Component>
</ForwardRef>
`;
exports[`ChannelTitle should match snapshot when is DM and has guests and the teammate is the guest (when can show subtitles) 1`] = `
<Component
<ForwardRef
style={
Object {
"flex": 1,
@@ -122,11 +122,11 @@ exports[`ChannelTitle should match snapshot when is DM and has guests and the te
}
/>
</View>
</Component>
</ForwardRef>
`;
exports[`ChannelTitle should match snapshot when is DM and has guests but the teammate is not the guest (when can show subtitles) 1`] = `
<Component
<ForwardRef
style={
Object {
"flex": 1,
@@ -172,11 +172,11 @@ exports[`ChannelTitle should match snapshot when is DM and has guests but the te
}
/>
</View>
</Component>
</ForwardRef>
`;
exports[`ChannelTitle should match snapshot when isSelfDMChannel is true 1`] = `
<Component
<ForwardRef
style={
Object {
"flex": 1,
@@ -230,5 +230,5 @@ exports[`ChannelTitle should match snapshot when isSelfDMChannel is true 1`] = `
}
/>
</View>
</Component>
</ForwardRef>
`;

View File

@@ -35,7 +35,7 @@ class SettingDrawerButton extends PureComponent {
const icon = (
<Icon
name='md-more'
name='ellipsis-vertical'
size={25}
color={theme.sidebarHeaderTextColor}
/>

View File

@@ -30,7 +30,7 @@ export default class ChannelPostList extends PureComponent {
static propTypes = {
actions: PropTypes.shape({
loadPostsIfNecessaryWithRetry: PropTypes.func.isRequired,
loadThreadIfNecessary: PropTypes.func.isRequired,
getPostThread: PropTypes.func.isRequired,
increasePostVisibility: PropTypes.func.isRequired,
selectPost: PropTypes.func.isRequired,
recordLoadTime: PropTypes.func.isRequired,
@@ -106,7 +106,7 @@ export default class ChannelPostList extends PureComponent {
const rootId = (post.root_id || post.id);
Keyboard.dismiss();
actions.loadThreadIfNecessary(rootId);
actions.getPostThread(rootId);
actions.selectPost(rootId);
const screen = 'Thread';

View File

@@ -12,7 +12,7 @@ describe('ChannelPostList', () => {
const baseProps = {
actions: {
loadPostsIfNecessaryWithRetry: jest.fn(),
loadThreadIfNecessary: jest.fn(),
getPostThread: jest.fn(),
increasePostVisibility: jest.fn(),
selectPost: jest.fn(),
recordLoadTime: jest.fn(),

View File

@@ -4,21 +4,20 @@
import {bindActionCreators} from 'redux';
import {connect} from 'react-redux';
import {
loadPostsIfNecessaryWithRetry,
increasePostVisibility,
refreshChannelWithRetry,
} from '@actions/views/channel';
import {getPostThread} from '@actions/views/post';
import {recordLoadTime} from 'app/actions/views/root';
import {Types} from '@constants';
import {selectPost} from '@mm-redux/actions/posts';
import {getPostIdsInCurrentChannel} from '@mm-redux/selectors/entities/posts';
import {getCurrentChannelId} from '@mm-redux/selectors/entities/channels';
import {getCurrentUserId} from '@mm-redux/selectors/entities/users';
import {getTheme} from '@mm-redux/selectors/entities/preferences';
import {
loadPostsIfNecessaryWithRetry,
loadThreadIfNecessary,
increasePostVisibility,
refreshChannelWithRetry,
} from 'app/actions/views/channel';
import {recordLoadTime} from 'app/actions/views/root';
import {Types} from 'app/constants';
import {isLandscape} from 'app/selectors/device';
import {isLandscape} from '@selectors/device';
import ChannelPostList from './channel_post_list';
@@ -44,7 +43,7 @@ function mapDispatchToProps(dispatch) {
return {
actions: bindActionCreators({
loadPostsIfNecessaryWithRetry,
loadThreadIfNecessary,
getPostThread,
increasePostVisibility,
selectPost,
recordLoadTime,

View File

@@ -1,6 +1,6 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`channel_info should match snapshot 1`] = `
exports[`channelInfo should match snapshot 1`] = `
<View
style={
Object {
@@ -75,15 +75,9 @@ exports[`channel_info should match snapshot 1`] = `
}
>
<React.Fragment>
<channelInfoRow
action={[Function]}
defaultMessage="Favorite"
detail={false}
icon="star-o"
<Connect(Favorite)
channelId="1234"
isLandscape={false}
rightArrow={true}
shouldRender={true}
textId="mobile.routes.channelInfo.favorite"
theme={
Object {
"awayIndicator": "#ffbc42",
@@ -113,26 +107,8 @@ exports[`channel_info should match snapshot 1`] = `
"type": "Mattermost",
}
}
togglable={true}
/>
<View
style={
Object {
"backgroundColor": undefined,
"height": 1,
"marginHorizontal": 15,
}
}
/>
<channelInfoRow
action={[Function]}
defaultMessage="Mute channel"
detail={false}
icon="bell-slash-o"
isLandscape={false}
rightArrow={true}
shouldRender={true}
textId="channel_notifications.muteChannel.settings"
<Separator
theme={
Object {
"awayIndicator": "#ffbc42",
@@ -162,26 +138,10 @@ exports[`channel_info should match snapshot 1`] = `
"type": "Mattermost",
}
}
togglable={true}
/>
<View
style={
Object {
"backgroundColor": undefined,
"height": 1,
"marginHorizontal": 15,
}
}
/>
<channelInfoRow
action={[Function]}
defaultMessage="Ignore @channel, @here, @all"
detail={false}
icon="at"
<Connect(Mute)
channelId="1234"
isLandscape={false}
rightArrow={true}
shouldRender={true}
textId="channel_notifications.ignoreChannelMentions.settings"
theme={
Object {
"awayIndicator": "#ffbc42",
@@ -211,25 +171,9 @@ exports[`channel_info should match snapshot 1`] = `
"type": "Mattermost",
}
}
togglable={true}
userId="1234"
/>
<View
style={
Object {
"backgroundColor": undefined,
"height": 1,
"marginHorizontal": 15,
}
}
/>
<channelInfoRow
action={[Function]}
defaultMessage="Pinned Posts"
image={1}
isLandscape={false}
rightArrow={true}
shouldRender={true}
textId="channel_header.pinnedPosts"
<Separator
theme={
Object {
"awayIndicator": "#ffbc42",
@@ -259,228 +203,202 @@ exports[`channel_info should match snapshot 1`] = `
"type": "Mattermost",
}
}
togglable={false}
/>
<React.Fragment>
<View
style={
Object {
"backgroundColor": undefined,
"height": 1,
"marginHorizontal": 15,
}
}
/>
<channelInfoRow
action={[Function]}
defaultMessage="Manage Members"
detail={2}
icon="users"
isLandscape={false}
rightArrow={true}
shouldRender={true}
textId="channel_header.manageMembers"
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",
}
}
togglable={false}
/>
</React.Fragment>
<React.Fragment>
<View
style={
Object {
"backgroundColor": undefined,
"height": 1,
"marginHorizontal": 15,
}
}
/>
<channelInfoRow
action={[Function]}
defaultMessage="Add Members"
icon="user-plus"
isLandscape={false}
rightArrow={true}
shouldRender={true}
textId="channel_header.addMembers"
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",
}
}
togglable={false}
/>
</React.Fragment>
<React.Fragment>
<View
style={
Object {
"backgroundColor": undefined,
"height": 1,
"marginHorizontal": 15,
}
}
/>
<channelInfoRow
action={[Function]}
defaultMessage="Convert to Private Channel"
icon="lock"
isLandscape={false}
rightArrow={true}
shouldRender={true}
textId="mobile.channel_info.convert"
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",
}
}
togglable={false}
/>
</React.Fragment>
<React.Fragment>
<View
style={
Object {
"backgroundColor": undefined,
"height": 1,
"marginHorizontal": 15,
}
}
/>
<channelInfoRow
action={[Function]}
defaultMessage="Edit Channel"
icon="edit"
isLandscape={false}
rightArrow={true}
shouldRender={true}
textId="mobile.channel_info.edit"
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",
}
}
togglable={false}
/>
</React.Fragment>
</React.Fragment>
<React.Fragment>
<View
style={
Object {
"backgroundColor": undefined,
"height": 1,
"marginHorizontal": 15,
}
}
/>
<channelInfoRow
action={[Function]}
defaultMessage="Leave Channel"
icon="sign-out"
<Connect(IgnoreMentions)
channelId="1234"
isLandscape={false}
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",
}
}
/>
<Separator
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",
}
}
/>
<Connect(Pinned)
channelId="1234"
isLandscape={false}
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",
}
}
/>
<Connect(ManageMembers)
isLandscape={false}
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",
}
}
/>
<Connect(AddMembers)
isLandscape={false}
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",
}
}
/>
<Connect(ConvertPrivate)
isLandscape={false}
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",
}
}
/>
<Connect(EditChannel)
isLandscape={false}
rightArrow={true}
shouldRender={true}
textId="navbar.leave"
theme={
Object {
"awayIndicator": "#ffbc42",
@@ -510,40 +428,52 @@ exports[`channel_info should match snapshot 1`] = `
"type": "Mattermost",
}
}
togglable={false}
/>
</React.Fragment>
</View>
<View
style={
Array [
Object {
"backgroundColor": "#ffffff",
"borderBottomColor": undefined,
"borderBottomWidth": 1,
"borderTopColor": undefined,
"borderTopWidth": 1,
},
Object {
"borderBottomColor": undefined,
"borderBottomWidth": 1,
"borderTopColor": undefined,
"borderTopWidth": 1,
"marginTop": 40,
},
]
Object {
"marginTop": 40,
}
}
>
<channelInfoRow
action={[Function]}
defaultMessage="Archive Channel"
icon="archive"
iconColor="#CA3B27"
<Connect(Leave)
close={[Function]}
isLandscape={false}
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",
}
}
/>
<Connect(Archive)
close={[Function]}
isLandscape={false}
rightArrow={true}
shouldRender={true}
textColor="#CA3B27"
textId="mobile.routes.channelInfo.delete_channel"
theme={
Object {
"awayIndicator": "#ffbc42",
@@ -573,7 +503,6 @@ exports[`channel_info should match snapshot 1`] = `
"type": "Mattermost",
}
}
togglable={false}
/>
</View>
</ScrollView>

View File

@@ -92,7 +92,7 @@ exports[`channel_info_header should match snapshot 1`] = `
]
}
>
<Component
<ForwardRef
onLongPress={[Function]}
>
<View
@@ -126,7 +126,7 @@ exports[`channel_info_header should match snapshot 1`] = `
Purpose string
</Text>
</View>
</Component>
</ForwardRef>
</View>
<View
style={
@@ -138,7 +138,7 @@ exports[`channel_info_header should match snapshot 1`] = `
]
}
>
<Component
<ForwardRef
onLongPress={[Function]}
>
<View
@@ -275,7 +275,7 @@ exports[`channel_info_header should match snapshot 1`] = `
value="Header string"
/>
</View>
</Component>
</ForwardRef>
</View>
<Text
style={
@@ -395,9 +395,12 @@ exports[`channel_info_header should match snapshot when DM and hasGuests and is
</View>
<View
style={
Object {
"marginTop": 15,
}
Array [
Object {
"marginTop": 15,
},
null,
]
}
>
<View
@@ -431,7 +434,7 @@ exports[`channel_info_header should match snapshot when DM and hasGuests and is
]
}
>
<Component
<ForwardRef
onLongPress={[Function]}
>
<View
@@ -465,7 +468,7 @@ exports[`channel_info_header should match snapshot when DM and hasGuests and is
Purpose string
</Text>
</View>
</Component>
</ForwardRef>
</View>
<View
style={
@@ -477,7 +480,7 @@ exports[`channel_info_header should match snapshot when DM and hasGuests and is
]
}
>
<Component
<ForwardRef
onLongPress={[Function]}
>
<View
@@ -614,7 +617,7 @@ exports[`channel_info_header should match snapshot when DM and hasGuests and is
value="Header string"
/>
</View>
</Component>
</ForwardRef>
</View>
<Text
style={
@@ -742,7 +745,7 @@ exports[`channel_info_header should match snapshot when DM and hasGuests but its
]
}
>
<Component
<ForwardRef
onLongPress={[Function]}
>
<View
@@ -776,7 +779,7 @@ exports[`channel_info_header should match snapshot when DM and hasGuests but its
Purpose string
</Text>
</View>
</Component>
</ForwardRef>
</View>
<View
style={
@@ -788,7 +791,7 @@ exports[`channel_info_header should match snapshot when DM and hasGuests but its
]
}
>
<Component
<ForwardRef
onLongPress={[Function]}
>
<View
@@ -925,7 +928,7 @@ exports[`channel_info_header should match snapshot when DM and hasGuests but its
value="Header string"
/>
</View>
</Component>
</ForwardRef>
</View>
<Text
style={
@@ -1045,9 +1048,12 @@ exports[`channel_info_header should match snapshot when GM and hasGuests 1`] = `
</View>
<View
style={
Object {
"marginTop": 15,
}
Array [
Object {
"marginTop": 15,
},
null,
]
}
>
<View
@@ -1081,7 +1087,7 @@ exports[`channel_info_header should match snapshot when GM and hasGuests 1`] = `
]
}
>
<Component
<ForwardRef
onLongPress={[Function]}
>
<View
@@ -1115,7 +1121,7 @@ exports[`channel_info_header should match snapshot when GM and hasGuests 1`] = `
Purpose string
</Text>
</View>
</Component>
</ForwardRef>
</View>
<View
style={
@@ -1127,7 +1133,7 @@ exports[`channel_info_header should match snapshot when GM and hasGuests 1`] = `
]
}
>
<Component
<ForwardRef
onLongPress={[Function]}
>
<View
@@ -1264,7 +1270,7 @@ exports[`channel_info_header should match snapshot when GM and hasGuests 1`] = `
value="Header string"
/>
</View>
</Component>
</ForwardRef>
</View>
<Text
style={
@@ -1392,7 +1398,7 @@ exports[`channel_info_header should match snapshot when is group constrained 1`]
]
}
>
<Component
<ForwardRef
onLongPress={[Function]}
>
<View
@@ -1426,7 +1432,7 @@ exports[`channel_info_header should match snapshot when is group constrained 1`]
Purpose string
</Text>
</View>
</Component>
</ForwardRef>
</View>
<View
style={
@@ -1438,7 +1444,7 @@ exports[`channel_info_header should match snapshot when is group constrained 1`]
]
}
>
<Component
<ForwardRef
onLongPress={[Function]}
>
<View
@@ -1575,7 +1581,7 @@ exports[`channel_info_header should match snapshot when is group constrained 1`]
value="Header string"
/>
</View>
</Component>
</ForwardRef>
</View>
<Text
style={
@@ -1717,9 +1723,12 @@ exports[`channel_info_header should match snapshot when public channel and hasGu
</View>
<View
style={
Object {
"marginTop": 15,
}
Array [
Object {
"marginTop": 15,
},
null,
]
}
>
<View
@@ -1753,7 +1762,7 @@ exports[`channel_info_header should match snapshot when public channel and hasGu
]
}
>
<Component
<ForwardRef
onLongPress={[Function]}
>
<View
@@ -1787,7 +1796,7 @@ exports[`channel_info_header should match snapshot when public channel and hasGu
Purpose string
</Text>
</View>
</Component>
</ForwardRef>
</View>
<View
style={
@@ -1799,7 +1808,7 @@ exports[`channel_info_header should match snapshot when public channel and hasGu
]
}
>
<Component
<ForwardRef
onLongPress={[Function]}
>
<View
@@ -1936,7 +1945,7 @@ exports[`channel_info_header should match snapshot when public channel and hasGu
value="Header string"
/>
</View>
</Component>
</ForwardRef>
</View>
<Text
style={

View File

@@ -0,0 +1,76 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`ChannelInfo -> Add Members should match snapshot 1`] = `
<React.Fragment>
<Separator
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",
}
}
/>
<channelInfoRow
action={[Function]}
defaultMessage="Add Members"
icon="user-plus"
isLandscape={false}
rightArrow={true}
shouldRender={true}
textId="channel_header.addMembers"
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",
}
}
togglable={false}
/>
</React.Fragment>
`;

View File

@@ -0,0 +1,55 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React from 'react';
import {shallowWithIntl} from 'test/intl-test-helper';
import Preferences from '@mm-redux/constants/preferences';
import AddMembers from './add_members';
jest.mock('@utils/theme', () => {
const original = jest.requireActual('../../../utils/theme');
return {
...original,
changeOpacity: jest.fn(),
};
});
describe('ChannelInfo -> Add Members', () => {
const baseProps = {
canManageUsers: true,
groupConstrained: false,
isLandscape: false,
theme: Preferences.THEMES.default,
};
test('should match snapshot', () => {
const wrapper = shallowWithIntl(
<AddMembers
{...baseProps}
/>,
);
expect(wrapper.getElement()).toMatchSnapshot();
});
test('should render null if cannot manage members', () => {
const wrapper = shallowWithIntl(
<AddMembers
{...baseProps}
canManageUsers={false}
/>,
);
expect(wrapper.getElement()).toBeNull();
});
test('should render null if channel is constrained to groups', () => {
const wrapper = shallowWithIntl(
<AddMembers
{...baseProps}
groupConstrained={true}
/>,
);
expect(wrapper.getElement()).toBeNull();
});
});

View File

@@ -0,0 +1,55 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React, {PureComponent} from 'react';
import {intlShape} from 'react-intl';
import {goToScreen} from '@actions/navigation';
import {Theme} from '@mm-redux/types/preferences';
import ChannelInfoRow from '@screens/channel_info/channel_info_row';
import Separator from '@screens/channel_info/separator';
import {t} from '@utils/i18n';
import {preventDoubleTap} from '@utils/tap';
interface AddMembersProps {
canManageUsers: boolean;
groupConstrained: boolean;
isLandscape: boolean;
theme: Theme;
}
export default class AddMembers extends PureComponent<AddMembersProps> {
static contextTypes = {
intl: intlShape.isRequired,
};
goToChannelAddMembers = preventDoubleTap(() => {
const {intl} = this.context;
const screen = 'ChannelAddMembers';
const title = intl.formatMessage({id: 'channel_header.addMembers', defaultMessage: 'Add Members'});
goToScreen(screen, title);
});
render() {
const {canManageUsers, groupConstrained, isLandscape, theme} = this.props;
if (canManageUsers && !groupConstrained) {
return (
<>
<Separator theme={theme}/>
<ChannelInfoRow
action={this.goToChannelAddMembers}
defaultMessage='Add Members'
icon='user-plus'
textId={t('channel_header.addMembers')}
theme={theme}
isLandscape={isLandscape}
/>
</>
);
}
return null;
}
}

View File

@@ -0,0 +1,22 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {connect} from 'react-redux';
import {canManageChannelMembers, getCurrentChannel} from '@mm-redux/selectors/entities/channels';
import AddMembers from './add_members';
function mapStateToProps(state) {
const currentChannel = getCurrentChannel(state);
let canManageUsers = currentChannel?.id ? canManageChannelMembers(state) : false;
if (currentChannel.group_constrained) {
canManageUsers = false;
}
return {
canManageUsers,
groupConstrained: currentChannel.group_constrained,
};
}
export default connect(mapStateToProps)(AddMembers);

View File

@@ -0,0 +1,155 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`ChannelInfo -> Archive should match snapshot for Archive Channel 1`] = `
<React.Fragment>
<Separator
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",
}
}
/>
<channelInfoRow
action={[Function]}
defaultMessage="Archive Channel"
icon="archive"
iconColor="#CA3B27"
isLandscape={false}
rightArrow={false}
shouldRender={true}
textColor="#CA3B27"
textId="mobile.routes.channelInfo.delete_channel"
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",
}
}
togglable={false}
/>
</React.Fragment>
`;
exports[`ChannelInfo -> Archive should match snapshot for Unarchive Channel 1`] = `
<React.Fragment>
<Separator
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",
}
}
/>
<channelInfoRow
action={[Function]}
defaultMessage="Unarchive Channel"
icon="archive"
iconColor="#CA3B27"
isLandscape={false}
rightArrow={false}
shouldRender={true}
textColor="#CA3B27"
textId="mobile.routes.channelInfo.unarchive_channel"
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",
}
}
togglable={false}
/>
</React.Fragment>
`;

View File

@@ -0,0 +1,67 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React from 'react';
import {shallowWithIntl} from 'test/intl-test-helper';
import Preferences from '@mm-redux/constants/preferences';
import Archive from './archive';
jest.mock('@utils/theme', () => {
const original = jest.requireActual('../../../utils/theme');
return {
...original,
changeOpacity: jest.fn(),
};
});
describe('ChannelInfo -> Archive', () => {
const baseProps = {
canArchive: true,
canUnarchive: false,
channelId: '123',
close: jest.fn(),
deleteChannel: jest.fn(),
displayName: 'Test Channel',
getChannel: jest.fn(),
handleSelectChannel: jest.fn(),
isLandscape: false,
isPublic: true,
unarchiveChannel: jest.fn(),
selectPenultimateChannel: jest.fn(),
teamId: 'team-123',
theme: Preferences.THEMES.default,
viewArchivedChannels: true,
};
test('should match snapshot for Archive Channel', () => {
const wrapper = shallowWithIntl(
<Archive
{...baseProps}
/>,
);
expect(wrapper.getElement()).toMatchSnapshot();
});
test('should match snapshot for Unarchive Channel', () => {
const wrapper = shallowWithIntl(
<Archive
{...baseProps}
canUnarchive={true}
/>,
);
expect(wrapper.getElement()).toMatchSnapshot();
});
test('should match snapshot Not render Archive', () => {
const wrapper = shallowWithIntl(
<Archive
{...baseProps}
canArchive={false}
canUnarchive={false}
/>,
);
expect(wrapper.getElement()).toBeNull();
});
});

View File

@@ -0,0 +1,177 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React, {PureComponent} from 'react';
import {Alert} from 'react-native';
import {intlShape} from 'react-intl';
import {ActionResult} from '@mm-redux/types/actions';
import {FormattedMsg} from '@mm-redux/types/general';
import {Theme} from '@mm-redux/types/preferences';
import ChannelInfoRow from '@screens/channel_info/channel_info_row';
import Separator from '@screens/channel_info/separator';
import {alertErrorWithFallback} from '@utils/general';
import {t} from '@utils/i18n';
import {preventDoubleTap} from '@utils/tap';
interface ArchiveProps {
canArchive: boolean;
canUnarchive: boolean;
channelId: string;
close: (redirect: boolean) => void;
deleteChannel: (channelId: string) => Promise<ActionResult>;
displayName: string;
getChannel: (channelId: string) => Promise<ActionResult>;
handleSelectChannel: (channelId: string) => Promise<ActionResult>;
isLandscape: boolean;
isPublic: boolean;
unarchiveChannel: (channelId: string) => Promise<ActionResult>;
selectPenultimateChannel: (channelId: string) => Promise<ActionResult>;
teamId: string;
theme: Theme;
viewArchivedChannels: boolean;
}
export default class Archive extends PureComponent<ArchiveProps> {
static contextTypes = {
intl: intlShape.isRequired,
};
alertAndHandleYesAction = (title: FormattedMsg, message: FormattedMsg, onPressAction: () => void) => {
const {formatMessage} = this.context.intl;
const {displayName, isPublic} = this.props;
// eslint-disable-next-line multiline-ternary
const term = isPublic ? formatMessage({id: 'mobile.channel_info.publicChannel', defaultMessage: 'Public Channel'}) :
formatMessage({id: 'mobile.channel_info.privateChannel', defaultMessage: 'Private Channel'});
Alert.alert(
formatMessage(title, {term}),
formatMessage(
message,
{
term: term.toLowerCase(),
name: displayName,
},
),
[{
text: formatMessage({id: 'mobile.channel_info.alertNo', defaultMessage: 'No'}),
}, {
text: formatMessage({id: 'mobile.channel_info.alertYes', defaultMessage: 'Yes'}),
onPress: onPressAction,
}],
);
}
handleDelete = preventDoubleTap(() => {
const {channelId, deleteChannel, displayName, teamId} = this.props;
const title = {id: t('mobile.channel_info.alertTitleDeleteChannel'), defaultMessage: 'Archive {term}'};
const message = {
id: t('mobile.channel_info.alertMessageDeleteChannel'),
defaultMessage: 'Are you sure you want to archive the {term} {name}?',
};
const onPressAction = async () => {
const result = await deleteChannel(channelId);
if (result.error) {
alertErrorWithFallback(
this.context.intl,
result.error,
{
id: t('mobile.channel_info.delete_failed'),
defaultMessage: "We couldn't archive the channel {displayName}. Please check your connection and try again.",
},
{
displayName,
},
);
if (result.error.server_error_id === 'api.channel.delete_channel.deleted.app_error') {
this.props.getChannel(channelId);
}
} else if (this.props.viewArchivedChannels) {
this.props.handleSelectChannel(channelId);
this.props.close(false);
} else {
this.props.selectPenultimateChannel(teamId);
this.props.close(false);
}
};
this.alertAndHandleYesAction(title, message, onPressAction);
});
handleUnarchive = preventDoubleTap(() => {
const {channelId, displayName} = this.props;
const title = {id: t('mobile.channel_info.alertTitleUnarchiveChannel'), defaultMessage: 'Unarchive {term}'};
const message = {
id: t('mobile.channel_info.alertMessageUnarchiveChannel'),
defaultMessage: 'Are you sure you want to unarchive the {term} {name}?',
};
const onPressAction = async () => {
const result = await this.props.unarchiveChannel(channelId);
if (result.error) {
alertErrorWithFallback(
this.context.intl,
result.error,
{
id: t('mobile.channel_info.unarchive_failed'),
defaultMessage: "We couldn't unarchive the channel {displayName}. Please check your connection and try again.",
},
{
displayName,
},
);
if (result.error.server_error_id === 'api.channel.unarchive_channel.unarchive.app_error') {
this.props.getChannel(channelId);
}
} else {
this.props.close(false);
}
};
this.alertAndHandleYesAction(title, message, onPressAction);
});
render() {
const {canArchive, canUnarchive, isLandscape, theme} = this.props;
if (!canArchive && !canUnarchive) {
return null;
}
let element;
if (canUnarchive) {
element = (
<ChannelInfoRow
action={this.handleUnarchive}
defaultMessage='Unarchive Channel'
icon='archive'
iconColor='#CA3B27'
textColor='#CA3B27'
textId={t('mobile.routes.channelInfo.unarchive_channel')}
theme={theme}
isLandscape={isLandscape}
rightArrow={false}
/>
);
} else {
element = (
<ChannelInfoRow
action={this.handleDelete}
defaultMessage='Archive Channel'
iconColor='#CA3B27'
icon='archive'
textId={t('mobile.routes.channelInfo.delete_channel')}
textColor='#CA3B27'
theme={theme}
isLandscape={isLandscape}
rightArrow={false}
/>
);
}
return (
<>
<Separator theme={theme}/>
{element}
</>
);
}
}

View File

@@ -0,0 +1,76 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {connect} from 'react-redux';
import {
handleSelectChannel,
selectPenultimateChannel,
} from '@actions/views/channel';
import {deleteChannel, getChannel, unarchiveChannel} from '@mm-redux/actions/channels';
import {General} from '@mm-redux/constants';
import Permissions from '@mm-redux/constants/permissions';
import {getCurrentChannel, isCurrentChannelReadOnly} from '@mm-redux/selectors/entities/channels';
import {getConfig, getLicense, hasNewPermissions} from '@mm-redux/selectors/entities/general';
import {haveITeamPermission} from '@mm-redux/selectors/entities/roles';
import {getCurrentTeamId} from '@mm-redux/selectors/entities/teams';
import {getCurrentUser, getCurrentUserRoles} from '@mm-redux/selectors/entities/users';
import {showDeleteOption} from '@mm-redux/utils/channel_utils';
import {isMinimumServerVersion} from '@mm-redux/utils/helpers';
import {isAdmin as checkIsAdmin, isChannelAdmin as checkIsChannelAdmin, isSystemAdmin as checkIsSystemAdmin} from '@mm-redux/utils/user_utils';
import {isGuest as isUserGuest} from '@utils/users';
import Archive from './archive';
function mapStateToProps(state) {
const config = getConfig(state);
const license = getLicense(state);
const currentChannel = getCurrentChannel(state);
const currentUser = getCurrentUser(state);
const roles = getCurrentUserRoles(state) || '';
const isGuest = isUserGuest(currentUser);
const isDefaultChannel = currentChannel.name === General.DEFAULT_CHANNEL;
const isDirectMessage = currentChannel.type === General.DM_CHANNEL;
const isGroupMessage = currentChannel.type === General.GM_CHANNEL;
const isAdmin = checkIsAdmin(roles);
const isChannelAdmin = checkIsChannelAdmin(roles);
const isSystemAdmin = checkIsSystemAdmin(roles);
const canLeave = (!isDefaultChannel && !isDirectMessage && !isGroupMessage) || (isDefaultChannel && isGuest);
const canDelete = showDeleteOption(state, config, license, currentChannel, isAdmin, isSystemAdmin, isChannelAdmin);
const canUnarchive = (currentChannel?.delete_at > 0 && !isDirectMessage && !isGroupMessage);
const viewArchivedChannels = config.ExperimentalViewArchivedChannels === 'true';
const {serverVersion} = state.entities.general;
let isReadOnly = false;
if (currentUser?.id && currentChannel?.id) {
isReadOnly = isCurrentChannelReadOnly(state) || false;
}
let canUnarchiveChannel = false;
if (hasNewPermissions(state) && isMinimumServerVersion(serverVersion, 5, 20)) {
canUnarchiveChannel = haveITeamPermission(state, {
team: getCurrentTeamId(state),
permission: Permissions.MANAGE_TEAM,
});
}
return {
canArchive: (canLeave && canDelete && !isReadOnly),
canUnarchive: canUnarchive && canUnarchiveChannel,
channelId: currentChannel?.id || '',
displayName: (currentChannel?.display_name || '').trim(),
isPublic: currentChannel?.type === General.OPEN_CHANNEL,
teamId: currentChannel?.team_id || '',
viewArchivedChannels,
};
}
const mapDispatchToProps = {
deleteChannel,
getChannel,
handleSelectChannel,
unarchiveChannel,
selectPenultimateChannel,
};
export default connect(mapStateToProps, mapDispatchToProps)(Archive);

View File

@@ -5,69 +5,47 @@ import React, {PureComponent} from 'react';
import PropTypes from 'prop-types';
import {intlShape} from 'react-intl';
import {
Alert,
ScrollView,
View,
} from 'react-native';
import {Navigation} from 'react-native-navigation';
import {dismissModal, goToScreen, showModalOverCurrentContext} from '@actions/navigation';
import pinIcon from '@assets/images/channel_info/pin.png';
import {dismissModal, showModalOverCurrentContext} from '@actions/navigation';
import StatusBar from '@components/status_bar';
import {General, Users} from '@mm-redux/constants';
import {alertErrorWithFallback} from '@utils/general';
import {t} from '@utils/i18n';
import {preventDoubleTap} from '@utils/tap';
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
import AddMembers from './add_members';
import Archive from './archive';
import ChannelInfoHeader from './channel_info_header';
import ChannelInfoRow from './channel_info_row';
import ConvertPrivate from './convert_private';
import EditChannel from './edit_channel';
import Favorite from './favorite';
import IgnoreMentions from './ignore_mentions';
import Leave from './leave';
import ManageMembers from './manage_members';
import Mute from './mute';
import Pinned from './pinned';
import Separator from './separator';
export default class ChannelInfo extends PureComponent {
static propTypes = {
actions: PropTypes.shape({
clearPinnedPosts: PropTypes.func.isRequired,
closeDMChannel: PropTypes.func.isRequired,
closeGMChannel: PropTypes.func.isRequired,
convertChannelToPrivate: PropTypes.func.isRequired,
deleteChannel: PropTypes.func.isRequired,
unarchiveChannel: PropTypes.func.isRequired,
getChannelStats: PropTypes.func.isRequired,
getChannel: PropTypes.func.isRequired,
leaveChannel: PropTypes.func.isRequired,
loadChannelsByTeamName: PropTypes.func.isRequired,
favoriteChannel: PropTypes.func.isRequired,
unfavoriteChannel: PropTypes.func.isRequired,
getCustomEmojisInText: PropTypes.func.isRequired,
selectFocusedPostId: PropTypes.func.isRequired,
updateChannelNotifyProps: PropTypes.func.isRequired,
selectPenultimateChannel: PropTypes.func.isRequired,
handleSelectChannel: PropTypes.func.isRequired,
setChannelDisplayName: PropTypes.func.isRequired,
}),
componentId: PropTypes.string,
viewArchivedChannels: PropTypes.bool.isRequired,
canDeleteChannel: PropTypes.bool.isRequired,
canUnarchiveChannel: PropTypes.bool.isRequired,
currentChannel: PropTypes.object.isRequired,
currentChannelCreatorName: PropTypes.string,
currentChannelMemberCount: PropTypes.number,
currentChannelGuestCount: PropTypes.number,
currentChannelPinnedPostCount: PropTypes.number,
currentChannelMemberCount: PropTypes.number,
currentUserId: PropTypes.string,
currentUserIsGuest: PropTypes.bool,
isBot: PropTypes.bool.isRequired,
isLandscape: PropTypes.bool.isRequired,
isTeammateGuest: PropTypes.bool.isRequired,
status: PropTypes.string,
theme: PropTypes.object.isRequired,
isChannelMuted: PropTypes.bool.isRequired,
isCurrent: PropTypes.bool.isRequired,
isFavorite: PropTypes.bool.isRequired,
canConvertChannel: PropTypes.bool.isRequired,
canManageUsers: PropTypes.bool.isRequired,
canEditChannel: PropTypes.bool.isRequired,
ignoreChannelMentions: PropTypes.bool.isRequired,
isBot: PropTypes.bool.isRequired,
isTeammateGuest: PropTypes.bool.isRequired,
isLandscape: PropTypes.bool.isRequired,
};
static defaultProps = {
@@ -78,30 +56,6 @@ export default class ChannelInfo extends PureComponent {
intl: intlShape.isRequired,
};
constructor(props) {
super(props);
this.state = {
isFavorite: props.isFavorite,
isMuted: props.isChannelMuted,
ignoreChannelMentions: props.ignoreChannelMentions,
};
}
static getDerivedStateFromProps(nextProps, state) {
if (state.isFavorite !== nextProps.isFavorite ||
state.isMuted !== nextProps.isChannelMuted ||
state.ignoreChannelMentions !== nextProps.ignoreChannelMentions) {
return {
isFavorite: nextProps.isFavorite,
isMuted: nextProps.isChannelMuted,
ignoreChannelMentions: nextProps.ignoreChannelMentions,
};
}
return null;
}
componentDidMount() {
this.navigationEventListener = Navigation.events().bindComponent(this);
this.props.actions.getChannelStats(this.props.currentChannel.id);
@@ -124,234 +78,6 @@ export default class ChannelInfo extends PureComponent {
dismissModal();
};
goToChannelAddMembers = preventDoubleTap(() => {
const {intl} = this.context;
const screen = 'ChannelAddMembers';
const title = intl.formatMessage({id: 'channel_header.addMembers', defaultMessage: 'Add Members'});
goToScreen(screen, title);
});
goToChannelMembers = preventDoubleTap(() => {
const {canManageUsers} = this.props;
const {intl} = this.context;
const id = canManageUsers ? t('channel_header.manageMembers') : t('channel_header.viewMembers');
const defaultMessage = canManageUsers ? 'Manage Members' : 'View Members';
const screen = 'ChannelMembers';
const title = intl.formatMessage({id, defaultMessage});
goToScreen(screen, title);
});
goToPinnedPosts = preventDoubleTap(() => {
const {currentChannel} = this.props;
const {formatMessage} = this.context.intl;
const id = t('channel_header.pinnedPosts');
const defaultMessage = 'Pinned Posts';
const screen = 'PinnedPosts';
const title = formatMessage({id, defaultMessage});
const passProps = {
currentChannelId: currentChannel.id,
};
goToScreen(screen, title, passProps);
});
handleChannelEdit = preventDoubleTap(() => {
const {intl} = this.context;
const id = t('mobile.channel_info.edit');
const defaultMessage = 'Edit Channel';
const screen = 'EditChannel';
const title = intl.formatMessage({id, defaultMessage});
goToScreen(screen, title);
});
handleConfirmConvertToPrivate = preventDoubleTap(async () => {
const {actions, currentChannel} = this.props;
const result = await actions.convertChannelToPrivate(currentChannel.id);
const displayName = {displayName: currentChannel.display_name.trim()};
const {formatMessage} = this.context.intl;
if (result.error) {
alertErrorWithFallback(
this.context.intl,
result.error,
{
id: t('mobile.channel_info.convert_failed'),
defaultMessage: 'We were unable to convert {displayName} to a private channel.',
},
{
displayName,
},
[{
text: formatMessage({id: 'mobile.share_extension.error_close', defaultMessage: 'Close'}),
}, {
text: formatMessage({id: 'mobile.terms_of_service.alert_retry', defaultMessage: 'Try Again'}),
onPress: this.handleConfirmConvertToPrivate,
}],
);
} else {
Alert.alert(
'',
formatMessage({id: t('mobile.channel_info.convert_success'), defaultMessage: '{displayName} is now a private channel.'}, displayName),
);
}
})
handleConvertToPrivate = preventDoubleTap(() => {
const {currentChannel} = this.props;
const {formatMessage} = this.context.intl;
const displayName = {displayName: currentChannel.display_name.trim()};
const title = {id: t('mobile.channel_info.alertTitleConvertChannel'), defaultMessage: 'Convert {displayName} to a private channel?'};
const message = {
id: t('mobile.channel_info.alertMessageConvertChannel'),
defaultMessage: 'When you convert {displayName} to a private channel, history and membership are preserved. Publicly shared files remain accessible to anyone with the link. Membership in a private channel is by invitation only.\n\nThe change is permanent and cannot be undone.\n\nAre you sure you want to convert {displayName} to a private channel?',
};
Alert.alert(
formatMessage(title, displayName),
formatMessage(message, displayName),
[{
text: formatMessage({id: 'mobile.channel_info.alertNo', defaultMessage: 'No'}),
}, {
text: formatMessage({id: 'mobile.channel_info.alertYes', defaultMessage: 'Yes'}),
onPress: this.handleConfirmConvertToPrivate,
}],
);
});
handleLeave = preventDoubleTap(() => {
const title = {id: t('mobile.channel_info.alertTitleLeaveChannel'), defaultMessage: 'Leave {term}'};
const message = {
id: t('mobile.channel_info.alertMessageLeaveChannel'),
defaultMessage: 'Are you sure you want to leave the {term} {name}?',
};
const onPressAction = () => {
this.props.actions.leaveChannel(this.props.currentChannel, true).then(() => {
this.close();
});
};
this.alertAndHandleYesAction(title, message, onPressAction);
});
handleDelete = preventDoubleTap(() => {
const channel = this.props.currentChannel;
const title = {id: t('mobile.channel_info.alertTitleDeleteChannel'), defaultMessage: 'Archive {term}'};
const message = {
id: t('mobile.channel_info.alertMessageDeleteChannel'),
defaultMessage: 'Are you sure you want to archive the {term} {name}?',
};
const onPressAction = async () => {
const result = await this.props.actions.deleteChannel(channel.id);
if (result.error) {
alertErrorWithFallback(
this.context.intl,
result.error,
{
id: t('mobile.channel_info.delete_failed'),
defaultMessage: "We couldn't archive the channel {displayName}. Please check your connection and try again.",
},
{
displayName: channel.display_name.trim(),
},
);
if (result.error.server_error_id === 'api.channel.delete_channel.deleted.app_error') {
this.props.actions.getChannel(channel.id);
}
} else if (this.props.viewArchivedChannels) {
this.props.actions.handleSelectChannel(channel.id);
this.close(false);
} else {
this.props.actions.selectPenultimateChannel(channel.team_id);
this.close(false);
}
};
this.alertAndHandleYesAction(title, message, onPressAction);
});
handleUnarchive = preventDoubleTap(() => {
const channel = this.props.currentChannel;
const title = {id: t('mobile.channel_info.alertTitleUnarchiveChannel'), defaultMessage: 'Unarchive {term}'};
const message = {
id: t('mobile.channel_info.alertMessageUnarchiveChannel'),
defaultMessage: 'Are you sure you want to unarchive the {term} {name}?',
};
const onPressAction = async () => {
const result = await this.props.actions.unarchiveChannel(channel.id);
if (result.error) {
alertErrorWithFallback(
this.context.intl,
result.error,
{
id: t('mobile.channel_info.unarchive_failed'),
defaultMessage: "We couldn't unarchive the channel {displayName}. Please check your connection and try again.",
},
{
displayName: channel.display_name.trim(),
},
);
if (result.error.server_error_id === 'api.channel.unarchive_channel.unarchive.app_error') {
this.props.actions.getChannel(channel.id);
}
} else {
this.close(false);
}
};
this.alertAndHandleYesAction(title, message, onPressAction);
});
alertAndHandleYesAction = (title, message, onPressAction) => {
const {formatMessage} = this.context.intl;
const channel = this.props.currentChannel;
const term = channel.type === General.OPEN_CHANNEL ?
formatMessage({id: 'mobile.channel_info.publicChannel', defaultMessage: 'Public Channel'}) :
formatMessage({id: 'mobile.channel_info.privateChannel', defaultMessage: 'Private Channel'});
Alert.alert(
formatMessage(title, {term}),
formatMessage(
message,
{
term: term.toLowerCase(),
name: channel.display_name.trim(),
},
),
[{
text: formatMessage({id: 'mobile.channel_info.alertNo', defaultMessage: 'No'}),
}, {
text: formatMessage({id: 'mobile.channel_info.alertYes', defaultMessage: 'Yes'}),
onPress: onPressAction,
}],
);
}
handleClose = preventDoubleTap(() => {
const {currentChannel, isCurrent, isFavorite} = this.props;
const channel = Object.assign({}, currentChannel, {isCurrent}, {isFavorite});
const {closeDMChannel, closeGMChannel} = this.props.actions;
switch (channel.type) {
case General.DM_CHANNEL:
closeDMChannel(channel).then(() => {
this.close();
});
break;
case General.GM_CHANNEL:
closeGMChannel(channel).then(() => {
this.close();
});
break;
}
});
handleFavorite = preventDoubleTap(() => {
const {isFavorite, actions, currentChannel} = this.props;
const {favoriteChannel, unfavoriteChannel} = actions;
const toggleFavorite = isFavorite ? unfavoriteChannel : favoriteChannel;
this.setState({isFavorite: !isFavorite});
toggleFavorite(currentChannel.id);
});
handleClosePermalink = () => {
const {actions} = this.props;
actions.selectFocusedPostId('');
@@ -363,29 +89,6 @@ export default class ChannelInfo extends PureComponent {
this.showPermalinkView(postId);
};
handleMuteChannel = preventDoubleTap(() => {
const {actions, currentChannel, currentUserId, isChannelMuted} = this.props;
const {updateChannelNotifyProps} = actions;
const opts = {
mark_unread: isChannelMuted ? 'all' : 'mention',
};
this.setState({isMuted: !isChannelMuted});
updateChannelNotifyProps(currentUserId, currentChannel.id, opts);
});
handleIgnoreChannelMentions = preventDoubleTap(() => {
const {actions, currentChannel, currentUserId, ignoreChannelMentions} = this.props;
const {updateChannelNotifyProps} = actions;
const opts = {
ignore_channel_mentions: ignoreChannelMentions ? Users.IGNORE_CHANNEL_MENTIONS_OFF : Users.IGNORE_CHANNEL_MENTIONS_ON,
};
this.setState({ignoreChannelMentions: !ignoreChannelMentions});
updateChannelNotifyProps(currentUserId, currentChannel.id, opts);
});
showPermalinkView = (postId) => {
const {actions} = this.props;
const screen = 'Permalink';
@@ -405,194 +108,66 @@ export default class ChannelInfo extends PureComponent {
showModalOverCurrentContext(screen, passProps, options);
};
renderViewOrManageMembersRow = () => {
const channel = this.props.currentChannel;
const isDirectMessage = channel.type === General.DM_CHANNEL;
return !isDirectMessage;
};
renderLeaveOrDeleteChannelRow = () => {
const channel = this.props.currentChannel;
const isGuest = this.props.currentUserIsGuest;
const isDefaultChannel = channel.name === General.DEFAULT_CHANNEL;
const isDirectMessage = channel.type === General.DM_CHANNEL;
const isGroupMessage = channel.type === General.GM_CHANNEL;
return (!isDefaultChannel && !isDirectMessage && !isGroupMessage) || (isDefaultChannel && isGuest);
};
renderCloseDirect = () => {
const channel = this.props.currentChannel;
const isDirectMessage = channel.type === General.DM_CHANNEL;
const isGroupMessage = channel.type === General.GM_CHANNEL;
return isDirectMessage || isGroupMessage;
};
renderUnarchiveChannel = () => {
const {canUnarchiveChannel} = this.props;
if (!canUnarchiveChannel) {
return false;
}
const channel = this.props.currentChannel;
const channelIsArchived = channel.delete_at !== 0;
const isDirectMessage = channel.type === General.DM_CHANNEL;
const isGroupMessage = channel.type === General.GM_CHANNEL;
return channelIsArchived && (!isDirectMessage && !isGroupMessage);
};
renderConvertToPrivateRow = () => {
const {currentChannel, canConvertChannel} = this.props;
const isDefaultChannel = currentChannel.name === General.DEFAULT_CHANNEL;
const isPublicChannel = currentChannel.type === General.OPEN_CHANNEL;
return !isDefaultChannel && isPublicChannel && canConvertChannel;
}
actionsRows = (style, channelIsArchived) => {
const {
currentChannelMemberCount,
currentChannelPinnedPostCount,
canManageUsers,
canEditChannel,
theme,
currentChannel,
isLandscape,
} = this.props;
const {currentChannel, currentUserId, isLandscape, theme} = this.props;
if (channelIsArchived) {
return (this.renderViewOrManageMembersRow() &&
<View>
<ChannelInfoRow
action={this.goToChannelMembers}
defaultMessage={canManageUsers ? 'Manage Members' : 'View Members'}
detail={currentChannelMemberCount}
icon='users'
textId={canManageUsers ? t('channel_header.manageMembers') : t('channel_header.viewMembers')}
theme={theme}
isLandscape={isLandscape}
/>
</View>);
return (
<ManageMembers
isLandscape={isLandscape}
theme={theme}
separator={false}
/>);
}
return (
<React.Fragment>
<ChannelInfoRow
action={this.handleFavorite}
defaultMessage='Favorite'
detail={this.state.isFavorite}
icon='star-o'
textId={t('mobile.routes.channelInfo.favorite')}
togglable={true}
theme={theme}
<>
<Favorite
channelId={currentChannel.id}
isLandscape={isLandscape}
/>
<View style={style.separator}/>
<ChannelInfoRow
action={this.handleMuteChannel}
defaultMessage='Mute channel'
detail={this.state.isMuted}
icon='bell-slash-o'
textId={t('channel_notifications.muteChannel.settings')}
togglable={true}
theme={theme}
isLandscape={isLandscape}
/>
<View style={style.separator}/>
<ChannelInfoRow
action={this.handleIgnoreChannelMentions}
defaultMessage='Ignore @channel, @here, @all'
detail={this.state.ignoreChannelMentions}
icon='at'
textId={t('channel_notifications.ignoreChannelMentions.settings')}
togglable={true}
<Separator theme={theme}/>
<Mute
channelId={currentChannel.id}
isLandscape={isLandscape}
userId={currentUserId}
theme={theme}
isLandscape={isLandscape}
/>
<View style={style.separator}/>
<ChannelInfoRow
action={this.goToPinnedPosts}
defaultMessage='Pinned Posts'
detail={currentChannelPinnedPostCount}
image={pinIcon}
textId={t('channel_header.pinnedPosts')}
<Separator theme={theme}/>
<IgnoreMentions
channelId={currentChannel.id}
isLandscape={isLandscape}
theme={theme}
isLandscape={isLandscape}
/>
{
/**
<ChannelInfoRow
action={() => true}
defaultMessage='Notification Preferences'
icon='bell-o'
textId='channel_header.notificationPreferences'
theme={theme}
/>
<View style={style.separator}/>
**/
}
{this.renderViewOrManageMembersRow() &&
<React.Fragment>
<View style={style.separator}/>
<ChannelInfoRow
action={this.goToChannelMembers}
defaultMessage={canManageUsers ? 'Manage Members' : 'View Members'}
detail={currentChannelMemberCount}
icon='users'
textId={canManageUsers ? t('channel_header.manageMembers') : t('channel_header.viewMembers')}
theme={theme}
isLandscape={isLandscape}
/>
</React.Fragment>
}
{canManageUsers && !currentChannel.group_constrained &&
<React.Fragment>
<View style={style.separator}/>
<ChannelInfoRow
action={this.goToChannelAddMembers}
defaultMessage='Add Members'
icon='user-plus'
textId={t('channel_header.addMembers')}
theme={theme}
isLandscape={isLandscape}
/>
</React.Fragment>
}
{this.renderConvertToPrivateRow() && (
<React.Fragment>
<View style={style.separator}/>
<ChannelInfoRow
action={this.handleConvertToPrivate}
defaultMessage='Convert to Private Channel'
icon='lock'
textId={t('mobile.channel_info.convert')}
theme={theme}
isLandscape={isLandscape}
/>
</React.Fragment>
)}
{canEditChannel && (
<React.Fragment>
<View style={style.separator}/>
<ChannelInfoRow
action={this.handleChannelEdit}
defaultMessage='Edit Channel'
icon='edit'
textId={t('mobile.channel_info.edit')}
theme={theme}
isLandscape={isLandscape}
/>
</React.Fragment>
)}
</React.Fragment>
<Separator theme={theme}/>
<Pinned
channelId={currentChannel.id}
isLandscape={isLandscape}
theme={theme}
/>
<ManageMembers
isLandscape={isLandscape}
theme={theme}
/>
<AddMembers
isLandscape={isLandscape}
theme={theme}
/>
<ConvertPrivate
isLandscape={isLandscape}
theme={theme}
/>
<EditChannel
isLandscape={isLandscape}
theme={theme}
/>
</>
);
};
render() {
const {
canDeleteChannel,
currentChannel,
currentChannelCreatorName,
currentChannelMemberCount,
@@ -607,26 +182,13 @@ export default class ChannelInfo extends PureComponent {
const style = getStyleSheet(theme);
const channelIsArchived = currentChannel.delete_at !== 0;
let i18nId;
let defaultMessage;
switch (currentChannel.type) {
case General.DM_CHANNEL:
i18nId = t('mobile.channel_list.closeDM');
defaultMessage = 'Close Direct Message';
break;
case General.GM_CHANNEL:
i18nId = t('mobile.channel_list.closeGM');
defaultMessage = 'Close Group Message';
break;
}
return (
<View style={style.container}>
<StatusBar/>
<ScrollView
style={style.scrollView}
>
{currentChannel.hasOwnProperty('id') &&
{Boolean(currentChannel?.id) &&
<ChannelInfoHeader
createAt={currentChannel.create_at}
creator={currentChannelCreatorName}
@@ -648,62 +210,19 @@ export default class ChannelInfo extends PureComponent {
}
<View style={style.rowsContainer}>
{this.actionsRows(style, channelIsArchived)}
{this.renderLeaveOrDeleteChannelRow() &&
<React.Fragment>
<View style={style.separator}/>
<ChannelInfoRow
action={this.handleLeave}
defaultMessage='Leave Channel'
icon='sign-out'
textId={t('navbar.leave')}
theme={theme}
isLandscape={isLandscape}
/>
{this.renderUnarchiveChannel() &&
<React.Fragment>
<View style={style.separator}/>
<ChannelInfoRow
action={this.handleUnarchive}
defaultMessage='Unarchive Channel'
icon='archive' // might need to change the icon...
textId={t('mobile.routes.channelInfo.unarchive_channel')}
theme={theme}
isLandscape={isLandscape}
/>
</React.Fragment>
}
</React.Fragment>
}
</View>
{this.renderLeaveOrDeleteChannelRow() && canDeleteChannel && !channelIsArchived &&
<View style={[style.rowsContainer, style.footer]}>
<ChannelInfoRow
action={this.handleDelete}
defaultMessage='Archive Channel'
iconColor='#CA3B27'
icon='archive'
textId={t('mobile.routes.channelInfo.delete_channel')}
textColor='#CA3B27'
theme={theme}
<View style={style.footer}>
<Leave
close={this.close}
isLandscape={isLandscape}
theme={theme}
/>
<Archive
close={this.close}
isLandscape={isLandscape}
theme={theme}
/>
</View>
}
{this.renderCloseDirect() &&
<View style={[style.rowsContainer, style.footer]}>
<ChannelInfoRow
action={this.handleClose}
defaultMessage={defaultMessage}
icon='times'
iconColor='#CA3B27'
rightArrow={false}
textId={i18nId}
textColor='#CA3B27'
theme={theme}
isLandscape={isLandscape}
/>
</View>
}
</ScrollView>
</View>
);
@@ -721,10 +240,6 @@ const getStyleSheet = makeStyleSheetFromTheme((theme) => {
},
footer: {
marginTop: 40,
borderTopWidth: 1,
borderBottomWidth: 1,
borderTopColor: changeOpacity(theme.centerChannelColor, 0.1),
borderBottomColor: changeOpacity(theme.centerChannelColor, 0.1),
},
rowsContainer: {
borderTopWidth: 1,
@@ -733,10 +248,5 @@ const getStyleSheet = makeStyleSheetFromTheme((theme) => {
borderBottomColor: changeOpacity(theme.centerChannelColor, 0.1),
backgroundColor: theme.centerChannelBg,
},
separator: {
marginHorizontal: 15,
height: 1,
backgroundColor: changeOpacity(theme.centerChannelColor, 0.1),
},
};
});

View File

@@ -9,11 +9,6 @@ import {General} from '@mm-redux/constants';
import ChannelInfo from './channel_info';
// ChannelInfoRow expects to receive the pinIcon as a number
jest.mock('@assets/images/channel_info/pin.png', () => {
return 1;
});
jest.mock('@utils/theme', () => {
const original = jest.requireActual('../../utils/theme');
return {
@@ -22,7 +17,7 @@ jest.mock('@utils/theme', () => {
};
});
describe('channel_info', () => {
describe('channelInfo', () => {
const intlMock = {
formatMessage: jest.fn(),
formatDate: jest.fn(),
@@ -34,12 +29,6 @@ describe('channel_info', () => {
now: jest.fn(),
};
const baseProps = {
canDeleteChannel: true,
canUnarchiveChannel: false,
canConvertChannel: true,
canManageUsers: true,
viewArchivedChannels: true,
canEditChannel: true,
currentChannel: {
id: '1234',
display_name: 'Channel Name',
@@ -54,35 +43,17 @@ describe('channel_info', () => {
currentChannelMemberCount: 2,
currentChannelGuestCount: 0,
currentUserId: '1234',
currentUserIsGuest: false,
isChannelMuted: false,
ignoreChannelMentions: false,
isCurrent: true,
isFavorite: false,
status: 'status',
theme: Preferences.THEMES.default,
isBot: false,
isTeammateGuest: false,
isLandscape: false,
actions: {
clearPinnedPosts: jest.fn(),
closeDMChannel: jest.fn(),
closeGMChannel: jest.fn(),
convertChannelToPrivate: jest.fn(),
deleteChannel: jest.fn(),
unarchiveChannel: jest.fn(),
getChannelStats: jest.fn(),
getChannel: jest.fn(),
leaveChannel: jest.fn(),
loadChannelsByTeamName: jest.fn(),
favoriteChannel: jest.fn(),
unfavoriteChannel: jest.fn(),
getCustomEmojisInText: jest.fn(),
selectFocusedPostId: jest.fn(),
updateChannelNotifyProps: jest.fn(),
selectPenultimateChannel: jest.fn(),
setChannelDisplayName: jest.fn(),
handleSelectChannel: jest.fn(),
},
};
@@ -96,63 +67,6 @@ describe('channel_info', () => {
expect(wrapper.getElement()).toMatchSnapshot();
});
test('should render convert to private button when user has team admin permissions', async () => {
const wrapper = shallow(
<ChannelInfo
{...baseProps}
/>,
{context: {intl: intlMock}},
);
const instance = wrapper.instance();
const render = instance.renderConvertToPrivateRow();
expect(render).toBeTruthy();
});
test('should not render convert to private button when user is not team admin or above', async () => {
const wrapper = shallow(
<ChannelInfo
{...baseProps}
canConvertChannel={false}
/>,
{context: {intl: intlMock}},
);
const instance = wrapper.instance();
const render = instance.renderConvertToPrivateRow();
expect(render).toBeFalsy();
});
test('should not render convert to private button currentChannel is already private', async () => {
const props = Object.assign({}, baseProps);
props.currentChannel.type = General.PRIVATE_CHANNEL;
const wrapper = shallow(
<ChannelInfo
{...props}
/>,
{context: {intl: intlMock}},
);
const instance = wrapper.instance();
const render = instance.renderConvertToPrivateRow();
expect(render).toBeFalsy();
});
test('should not render convert to private button when currentChannel is a default channel', async () => {
const props = Object.assign({}, baseProps);
props.currentChannel.name = General.DEFAULT_CHANNEL;
const wrapper = shallow(
<ChannelInfo
{...props}
/>,
{context: {intl: intlMock}},
);
const instance = wrapper.instance();
const render = instance.renderConvertToPrivateRow();
expect(render).toBeFalsy();
});
test('should dismiss modal on close', () => {
const dismissModal = jest.spyOn(NavigationActions, 'dismissModal');
const wrapper = shallow(
@@ -167,38 +81,4 @@ describe('channel_info', () => {
instance.close();
expect(dismissModal).toHaveBeenCalled();
});
test('should render unarchive channel button when currentChannel is an archived channel', async () => {
const props = Object.assign({}, baseProps);
props.canUnarchiveChannel = true;
props.currentChannel.delete_at = 1234566;
const wrapper = shallow(
<ChannelInfo
{...props}
/>,
{context: {intl: intlMock}},
);
const instance = wrapper.instance();
const render = instance.renderUnarchiveChannel();
expect(render).toBeTruthy();
});
test('should not render unarchive channel button when currentChannel is an active channel', async () => {
const props = Object.assign({}, baseProps);
props.canUnarchiveChannel = false;
props.currentChannel.delete_at = 0;
const wrapper = shallow(
<ChannelInfo
{...props}
/>,
{context: {intl: intlMock}},
);
const instance = wrapper.instance();
const render = instance.renderUnarchiveChannel();
expect(render).toBeFalsy();
});
});

View File

@@ -4,13 +4,13 @@
import React from 'react';
import PropTypes from 'prop-types';
import {
Clipboard,
Platform,
Text,
TouchableHighlight,
View,
} from 'react-native';
import {intlShape} from 'react-intl';
import Clipboard from '@react-native-community/clipboard';
import {General} from '@mm-redux/constants';
@@ -52,7 +52,7 @@ export default class ChannelInfoHeader extends React.PureComponent {
};
renderHasGuestText = (style) => {
const {type, hasGuests, isTeammateGuest} = this.props;
const {type, hasGuests, isLandscape, isTeammateGuest} = this.props;
if (!hasGuests) {
return null;
}
@@ -74,7 +74,7 @@ export default class ChannelInfoHeader extends React.PureComponent {
defaultMessage = 'This channel has guests';
}
return (
<View style={style.section}>
<View style={[style.section, padding(isLandscape, -15)]}>
<View style={style.row}>
<FormattedText
style={style.header}

View File

@@ -102,6 +102,7 @@ channelInfoRow.propTypes = {
iconColor: PropTypes.string,
image: PropTypes.number,
imageTintColor: PropTypes.string,
isLandscape: PropTypes.bool,
rightArrow: PropTypes.bool,
textId: PropTypes.string.isRequired,
togglable: PropTypes.bool,

View File

@@ -0,0 +1,76 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`ChannelInfo -> ConvertPrivate should match snapshot for Convert to Private Channel 1`] = `
<React.Fragment>
<Separator
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",
}
}
/>
<channelInfoRow
action={[Function]}
defaultMessage="Convert to Private Channel"
icon="lock"
isLandscape={false}
rightArrow={false}
shouldRender={true}
textId="mobile.channel_info.convert"
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",
}
}
togglable={false}
/>
</React.Fragment>
`;

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