Compare commits

..

79 Commits

Author SHA1 Message Date
Elias Nahum
63ce6c7afb Version 1.48.2 build 382 (#5896)
* Bump app version number to  1.48.2

* Bump app build number to  382
2022-01-06 16:08:14 +02:00
Mattermost Build
7bea097c90 Fixes custom status update (#5881) (#5895)
(cherry picked from commit 16d4231ccd)

Co-authored-by: Shaz Amjad <shaz.amjad@mattermost.com>
2022-01-06 15:53:12 +02:00
Mattermost Build
0fe72d1c10 Bump Version 1.48.1 build 381 (#5860) (#5861)
* Bump app version number to  1.48.1

* Bump app build number to  381

(cherry picked from commit f453c77ac6)

Co-authored-by: Elias Nahum <nahumhbl@gmail.com>
2021-12-03 11:46:26 +02:00
Elias Nahum
d3211b66a3 Fix android broken build cause by rudderstack (#5856)
* Fix android broken build cause by rudderstack

* update dependencies
2021-11-30 11:51:09 +02:00
Mattermost Build
39bdb69fdd Added YouTube query to Android Manifest (#5841) (#5855)
(cherry picked from commit 1c0d0bb1a4)

Co-authored-by: Elias Nahum <nahumhbl@gmail.com>
2021-11-30 11:32:42 +02:00
Mattermost Build
8222390c98 MM-36687, MM-38302, MM-37598 Fix push notifications with CRT (#5669) (#5853)
* initalised

* Removed unused packages

* Android: Added groupId for supporting both threadId & channelId

* Fixed ios condition check

* Removed commented code

* Removed unwanted condition

* Removed unused variable

* CRT reduced chunk size to 30, Android global threads showing GlobalThreads & iOS is_crt_enabled field is expected to be a boolean

* Update android/app/src/main/java/com/mattermost/rnbeta/CustomPushNotification.java

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

* Misc fixes

Co-authored-by: Elias Nahum <nahumhbl@gmail.com>
Co-authored-by: Mattermod <mattermod@users.noreply.github.com>
(cherry picked from commit 805b90205a)

Co-authored-by: Anurag Shivarathri <anurag6713@gmail.com>
2021-11-30 11:32:28 +02:00
Mattermost Build
456208d223 [MM-40160] Crt gallery fix (#5833) (#5852)
* Fix Gallery crash

* fix file type icons

* memoize gallery files

(cherry picked from commit 1b285dac49)

Co-authored-by: Elias Nahum <nahumhbl@gmail.com>
2021-11-30 11:32:17 +02:00
Mattermost Build
2e71fcc226 Bump app build number to 380 (#5828) (#5829)
(cherry picked from commit e4dafb4d3b)

Co-authored-by: Elias Nahum <nahumhbl@gmail.com>
2021-11-11 16:53:15 -03:00
Mattermost Build
04d7834024 replace account-group-outline with account-multiple-outline compass icon (#5826) (#5827)
(cherry picked from commit a570fdea24)

Co-authored-by: Elias Nahum <nahumhbl@gmail.com>
2021-11-11 16:25:49 -03:00
Mattermost Build
7888b971e1 Bump app build number to 379 (#5824) (#5825)
(cherry picked from commit 94f683d743)

Co-authored-by: Elias Nahum <nahumhbl@gmail.com>
2021-11-11 08:36:49 -03:00
Jesús Espino
49462bd4a0 Voicechannels (#5753)
* Some extra work on voice channels interface

* Fixing some TODOs

* Improving styling of call in channel

* Improve calls monitoring

* Replacing some of the fontawesome icons with the compass ones

* Improving the layout

* Migrating to webrtc2 for unified plan

* Add screen on and off behavior

* Adding incall manager plugin

* Moving everything into the products/calls folder

* Make products modules routes relatives

* Make products modules routes @mmproducts

* Removing initiator parameter

* Removing trickle parameter

* Simplifying code

* Removing underscore from private variables

* Removing underscore from private things

* More simplifications

* More simplifications

* More simplifications

* Changing sha sum for mmjstool

* Fixing typo

* Migrating simple-peer to typescript

* Migrating simple-peer to typescript

* Improving the size of the screen share

* Adding feature flag to disable the calls feature in mobile

* Fixing some tests

* Removing obsolte tests

* Added call ended support for the post messages

* Fixing some warnings in the tests

* Adding JoinCall tests

* Adding CallMessage tests

* Adding CurrentCall unit tests

* Adding CallAvatar unit tests

* Adding FloatingCallContainer unit tests

* Adding StartCall unit tests

* Adding EnableDisableCalls unit tests

* Adding CallDuration tests

* Improving CallDuration tests

* Adding CallScreen unit tests

* Adding CallOtherActions screen tests

* Fixing some dark theme styles

* Fixing tests

* More robustness around connecting/disconnecting

* Adding FormattedRelativeTime tests

* Adding tests for ChannelItem

* Adding tests for ChannelInfo

* Adding selectors tests

* Adding reducers unit tests

* Adding actions tests

* Removing most of the TODOs

* Removing another TODO

* Updating tests snapshots

* Removing the last TODO

* Fixed a small problem on pressing while a call is ongoing

* Remove all the inlined functions

* Replacing usage of isLandscape selector with useWindowDimensions

* Removed unnecesary makeStyleSheetFromTheme

* Removing unneded  properties from call_duration

* Fixing possible null channels return from getChannel selector

* Moving other inlined functions to its own constant

* Simplifiying enable/disable calls component

* Improving the behavior when you are in the call of the current channel

* Adding missing translation strings

* Simplified a bit the EnableDisableCalls component

* Moving other inlined functions to its own constant

* Updating snapshots

* Improving usage of makeStyleSheetFromTheme

* Moving data reformating from the rest client to the redux action

* Adding calls to the blocklist to the redux-persist

* Fixing tests

* Updating snapshots

* Update file icon name to the last compass icons version

* Fix loading state

* Only show the call connected if the websocket gets connected

* Taking into consideration the indicator bar to position the calls new bars

* Making the MoreMessagesButton component aware of calls components

* Updating snapshots

* Fixing tests

* Updating snapshot

* Fixing different use cases for start call channel menu

* Fixing tests

* Ask for confirmation to start a call when you are already in another call

* Update app/products/calls/components/floating_call_container.tsx

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

* Memoizing userIds in join call

* Applying suggestion around combine the blocklist for calls with the one for typing

* Adding explicit types to the rest client

* Removing unneeded permission

* Making updateIntervalInSeconds prop optional in FormattedRelativeTime

* Making updateIntervalInSeconds prop optional in CallDuration

Co-authored-by: Elias Nahum <nahumhbl@gmail.com>
2021-11-11 11:33:30 +01:00
Mattermost Build
eb8579e84c Fix options modal dismiss for OptionsModal (#5819) (#5822) 2021-11-10 09:27:57 -03:00
Puerco
41caf0d865 Automated cherry pick of #5816 on release-1.48 (#5817) 2021-11-08 19:54:47 +01:00
Mattermost Build
6a91b6544d Version 1.48.0 build 378 (#5811) (#5814)
* Bump app version number to  1.48.0

* Bump app build number to  378

(cherry picked from commit cbc4d5add2)

Co-authored-by: Elias Nahum <nahumhbl@gmail.com>
2021-11-04 09:24:44 -03:00
Mattermost Build
75628cdf2e Disable reachability test (#5812) (#5813)
(cherry picked from commit ec3b44498a)

Co-authored-by: Elias Nahum <nahumhbl@gmail.com>
2021-11-04 09:24:29 -03:00
Puerco
c8ee3bc722 Automated cherry pick of #5803 on release-1.48 (#5810)
* Translated using Weblate (Turkish)

Currently translated at 100.0% (778 of 778 strings)

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

* Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (778 of 778 strings)

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

* Translated using Weblate (Swedish)

Currently translated at 100.0% (778 of 778 strings)

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

* Translated using Weblate (Polish)

Currently translated at 100.0% (778 of 778 strings)

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

* Translated using Weblate (Hungarian)

Currently translated at 100.0% (778 of 778 strings)

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

* Translated using Weblate (Spanish)

Currently translated at 100.0% (778 of 778 strings)

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

* Translated using Weblate (English (Australia))

Currently translated at 100.0% (778 of 778 strings)

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

* Translated using Weblate (Dutch)

Currently translated at 100.0% (778 of 778 strings)

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

Translated using Weblate (Dutch)

Currently translated at 100.0% (778 of 778 strings)

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

* Translated using Weblate (German)

Currently translated at 100.0% (778 of 778 strings)

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

* Translated using Weblate (Korean)

Currently translated at 93.9% (730 of 777 strings)

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

* Translated using Weblate (Japanese)

Currently translated at 100.0% (778 of 778 strings)

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

Translated using Weblate (Japanese)

Currently translated at 100.0% (777 of 777 strings)

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

Co-authored-by: Kaya Zeren <kayazeren@gmail.com>
Co-authored-by: aeomin <lin@aeomin.net>
Co-authored-by: MArtin Johnson <martinjohnson@bahnhof.se>
Co-authored-by: master7 <marcin.karkosz@rajska.info>
Co-authored-by: Tóth Csaba // Online ERP Hungary Kft <csaba.toth@online-erp.hu>
Co-authored-by: Elias  Nahum <elias@mattermost.com>
Co-authored-by: Matthew Williams <Matthew.Williams@outlook.com.au>
Co-authored-by: Tom De Moor <tom@controlaltdieliet.be>
Co-authored-by: jprusch <rs@schaeferbarthold.de>
Co-authored-by: teamzamong <heekang@korea.ac.kr>
Co-authored-by: kaakaa <stooner.hoe@gmail.com>
2021-11-04 09:07:55 +01:00
Mattermost Build
3011992995 Update NOTICE.txt (#5805) (#5809)
(cherry picked from commit 800f3b5648)

Co-authored-by: Amy Blais <29708087+amyblais@users.noreply.github.com>
2021-11-03 11:34:33 -03:00
Mattermost Build
e23960f27d Updated Jump to to match webapp (#5797) (#5808)
Co-authored-by: Mattermod <mattermod@users.noreply.github.com>
(cherry picked from commit fae944c208)

Co-authored-by: Carrie Warner (Mattermost) <74422101+cwarnermm@users.noreply.github.com>
2021-11-03 11:34:19 -03:00
Mattermost Build
84a292003e fix replaceAll with replace as is not available on some JSC (#5801) (#5806) 2021-11-02 04:14:15 -03:00
Elias Nahum
8287e620d8 Upgrade to rn 0.66.1 (#5727)
* Upgrade to rn 0.66.0

* Add keys to re-render post list and channel list

* Finish dep updates and rn to 0.66.1

* upgrade more dependencies

* Fix select_server tests
2021-10-31 13:57:07 -03:00
Kyriakos Z
094a34d05f MM-37934: adds CRT channel notification preferences (#5744)
* MM-37934: adds CRT channel notification preferences

Adds push notification preferences per channel for CRT replies.
These are only enabled when push notifications for root messages
are set to 'mention', in any other case we don't show the switch.

* Adds another test case

Co-authored-by: Mattermod <mattermod@users.noreply.github.com>
2021-10-28 11:06:03 +03:00
Daniel Espino García
76284a78d5 MM-34194 Add multiselect to Apps Forms (#5400)
* Add multiselect to Apps Forms

* Address feedback

* Add selected information and change button name

* Fix styles

* Add missing semicolon and change currentList to currentSelected.

* Address UX feedback

* Fix test

* Address feedback

* Fix snapshots

* Potential fix for flaky test

* Fix iOS back button

* Address feedback

* Fix tests

* Add separator and scroll to bottom on selected items

* Use setTimeout for scroll and also scroll on unselected

* Fix lint

* Fix tsc

* Fix tests
2021-10-27 11:35:11 +02:00
master7
434f4c970e Translated using Weblate (Polish)
Currently translated at 100.0% (777 of 777 strings)

Translation: mattermost-languages-shipped/mattermost-mobile
Translate-URL: https://translate.mattermost.com/projects/mattermost/mattermost-mobile_master/pl/
2021-10-25 11:50:26 -05:00
Tóth Csaba // Online ERP Hungary Kft
b7e9cd296b Translated using Weblate (Hungarian)
Currently translated at 100.0% (777 of 777 strings)

Translation: mattermost-languages-shipped/mattermost-mobile
Translate-URL: https://translate.mattermost.com/projects/mattermost/mattermost-mobile_master/hu/
2021-10-25 11:50:26 -05:00
yeongeun.seo
f98897147d Translated using Weblate (Korean)
Currently translated at 93.1% (724 of 777 strings)

Translation: mattermost-languages-shipped/mattermost-mobile
Translate-URL: https://translate.mattermost.com/projects/mattermost/mattermost-mobile_master/ko/
2021-10-25 11:50:26 -05:00
Anne-Laure Gaillard
0989ad9208 Translated using Weblate (French)
Currently translated at 99.7% (775 of 777 strings)

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

Translated using Weblate (French)

Currently translated at 99.6% (774 of 777 strings)

Translation: mattermost-languages-shipped/mattermost-mobile
Translate-URL: https://translate.mattermost.com/projects/mattermost/mattermost-mobile_master/fr/
2021-10-25 11:50:26 -05:00
Joseph Baylon
3ddc34d737 Detox/E2E: Fix failures due to recent features (#5755) 2021-10-24 19:00:37 -07:00
Elias Nahum
de564513c3 Bump app build number to 377 (#5786) 2021-10-24 16:54:11 -03:00
Elias Nahum
dd866ffeb8 Fix iOS paste screenshot (#5785) 2021-10-24 16:51:24 -03:00
Elias Nahum
acffc7796f Bump to 1.47.2 Build 376 (#5783)
* Bump app version number to  1.47.2

* Bump app build number to  376
2021-10-24 15:01:23 -03:00
Elias Nahum
61c6f5693b Fix TextInput blur crash (#5780) 2021-10-24 14:43:44 -03:00
Anurag Shivarathri
fe12190f4b App crash in android sometimes while opening the notifications (#5776)
* Checks for condition before removing the notification

* Misc
2021-10-24 14:40:28 -03:00
Guillermo Vayá
2a4027f74a Added checklist to PR template (#5766)
Co-authored-by: = <=>
2021-10-20 12:54:04 +02:00
Elias Nahum
c3cb9a5d34 Bump Version 1.47.1 build 375 (#5769)
* Fastlane build and version

* Bump app version number to  1.47.1

* Bump app build number to  375
2021-10-19 16:14:34 -03:00
Matthew Birtch
7df996dcc6 updated ios app icon to flattened version as per marketing team request (#5767) 2021-10-19 16:14:08 -03:00
Elias Nahum
e5142f8524 Fix Post Input and drawers gesture conflict (#5762) 2021-10-18 17:55:25 -03:00
Anurag Shivarathri
789cf9ab54 MM-39348 CRT crash (#5759)
* Cancelling animation frame on unmounting the component

* avoid calling cancel animation twice

* Update app/components/post_draft/post_draft.js

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

Co-authored-by: Elias Nahum <nahumhbl@gmail.com>
2021-10-18 14:26:33 -03:00
Tóth Csaba // Online ERP Hungary Kft
963d04545e Translated using Weblate (Hungarian)
Currently translated at 100.0% (777 of 777 strings)

Translation: mattermost-languages-shipped/mattermost-mobile
Translate-URL: https://translate.mattermost.com/projects/mattermost/mattermost-mobile_master/hu/
2021-10-18 11:22:42 -05:00
prograde
c58c23a10e Translated using Weblate (Swedish)
Currently translated at 100.0% (777 of 777 strings)

Translation: mattermost-languages-shipped/mattermost-mobile
Translate-URL: https://translate.mattermost.com/projects/mattermost/mattermost-mobile_master/sv/
2021-10-18 11:22:42 -05:00
MArtin Johnson
0708a4d81b Translated using Weblate (Swedish)
Currently translated at 100.0% (777 of 777 strings)

Translation: mattermost-languages-shipped/mattermost-mobile
Translate-URL: https://translate.mattermost.com/projects/mattermost/mattermost-mobile_master/sv/
2021-10-18 11:22:42 -05:00
aeomin
d96ecb0fdb Translated using Weblate (Chinese (Simplified))
Currently translated at 100.0% (777 of 777 strings)

Translation: mattermost-languages-shipped/mattermost-mobile
Translate-URL: https://translate.mattermost.com/projects/mattermost/mattermost-mobile_master/zh_Hans/
2021-10-18 11:22:42 -05:00
Tom De Moor
fff455624e Translated using Weblate (Dutch)
Currently translated at 99.8% (776 of 777 strings)

Translation: mattermost-languages-shipped/mattermost-mobile
Translate-URL: https://translate.mattermost.com/projects/mattermost/mattermost-mobile_master/nl/
2021-10-18 11:22:42 -05:00
kaakaa
f18abe11f6 Translated using Weblate (Japanese)
Currently translated at 100.0% (777 of 777 strings)

Translation: mattermost-languages-shipped/mattermost-mobile
Translate-URL: https://translate.mattermost.com/projects/mattermost/mattermost-mobile_master/ja/
2021-10-18 11:22:42 -05:00
Diego Alvarez
a435c96f2e Translated using Weblate (Spanish)
Currently translated at 100.0% (777 of 777 strings)

Translation: mattermost-languages-shipped/mattermost-mobile
Translate-URL: https://translate.mattermost.com/projects/mattermost/mattermost-mobile_master/es/
2021-10-18 11:22:42 -05:00
Elias Nahum
45efbe1c97 Translated using Weblate (Spanish)
Currently translated at 100.0% (777 of 777 strings)

Translation: mattermost-languages-shipped/mattermost-mobile
Translate-URL: https://translate.mattermost.com/projects/mattermost/mattermost-mobile_master/es/
2021-10-18 11:22:42 -05:00
Elias Nahum
f8baaf8505 MM-39337 set allowFontScaling to TextInput components (#5751) 2021-10-14 13:42:09 -03:00
Joseph Baylon
ebb00a205c Detox/E2E: Update deps (#5747)
* Detox/E2E: Update deps

* Add package-lock.json files
2021-10-12 15:09:42 -07:00
Elisabeth Kulzer
c4cb7b0c3e Translations fix (#5746)
* Translated using Weblate (French)

Currently translated at 100.0% (773 of 773 strings)

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

* Translated using Weblate (Japanese)

Currently translated at 100.0% (773 of 773 strings)

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

* Translated using Weblate (Hungarian)

Currently translated at 100.0% (773 of 773 strings)

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

* Translated using Weblate (English (Australia))

Currently translated at 100.0% (773 of 773 strings)

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

* Translated using Weblate (Spanish)

Currently translated at 100.0% (773 of 773 strings)

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

* Translated using Weblate (Swedish)

Currently translated at 100.0% (773 of 773 strings)

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

* Translated using Weblate (Polish)

Currently translated at 100.0% (773 of 773 strings)

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

* Translated using Weblate (Turkish)

Currently translated at 100.0% (773 of 773 strings)

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

* Translated using Weblate (German)

Currently translated at 100.0% (777 of 777 strings)

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

Translated using Weblate (German)

Currently translated at 100.0% (777 of 777 strings)

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

* Translated using Weblate (German)

Currently translated at 100.0% (777 of 777 strings)

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

* Translated using Weblate (German)

Currently translated at 100.0% (777 of 777 strings)

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

* Translated using Weblate (German)

Currently translated at 100.0% (777 of 777 strings)

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

* Translated using Weblate (Polish)

Currently translated at 100.0% (777 of 777 strings)

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

* Translated using Weblate (Turkish)

Currently translated at 100.0% (777 of 777 strings)

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

* Translated using Weblate (Swedish)

Currently translated at 100.0% (777 of 777 strings)

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

* Translated using Weblate (Hungarian)

Currently translated at 100.0% (777 of 777 strings)

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

* Translated using Weblate (English (Australia))

Currently translated at 100.0% (777 of 777 strings)

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

* Translated using Weblate (Spanish)

Currently translated at 99.4% (773 of 777 strings)

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

* Translated using Weblate (French)

Currently translated at 99.4% (773 of 777 strings)

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

* Translated using Weblate (Italian)

Currently translated at 76.1% (592 of 777 strings)

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

* Translated using Weblate (Japanese)

Currently translated at 99.4% (773 of 777 strings)

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

* Translated using Weblate (Korean)

Currently translated at 80.3% (624 of 777 strings)

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

* Translated using Weblate (Dutch)

Currently translated at 99.2% (771 of 777 strings)

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

* Translated using Weblate (Portuguese (Brazil))

Currently translated at 86.6% (673 of 777 strings)

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

* Translated using Weblate (Romanian)

Currently translated at 85.9% (668 of 777 strings)

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

* Translated using Weblate (Russian)

Currently translated at 96.3% (749 of 777 strings)

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

* Translated using Weblate (Ukrainian)

Currently translated at 76.8% (597 of 777 strings)

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

* Translated using Weblate (Chinese (Simplified))

Currently translated at 99.2% (771 of 777 strings)

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

* Translated using Weblate (Chinese (Traditional))

Currently translated at 76.0% (591 of 777 strings)

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

* Translated using Weblate (Bulgarian)

Currently translated at 96.9% (753 of 777 strings)

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

* Update translation files

Updated by "Remove blank strings" hook in Weblate.

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

* Update translation files

Updated by "Remove blank strings" hook in Weblate.

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

* Update translation files

Updated by "Remove blank strings" hook in Weblate.

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

* Update translation files

Updated by "Remove blank strings" hook in Weblate.

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

* Update translation files

Updated by "Remove blank strings" hook in Weblate.

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

* Update translation files

Updated by "Remove blank strings" hook in Weblate.

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

Co-authored-by: Nathanaël <contact@nathanaelhoun.fr>
Co-authored-by: kaakaa <stooner.hoe@gmail.com>
Co-authored-by: Tóth Csaba // Online ERP Hungary Kft <csaba.toth@online-erp.hu>
Co-authored-by: Matthew Williams <Matthew.Williams@outlook.com.au>
Co-authored-by: Elias  Nahum <elias@mattermost.com>
Co-authored-by: MArtin Johnson <martinjohnson@bahnhof.se>
Co-authored-by: master7 <marcin.karkosz@rajska.info>
Co-authored-by: Kaya Zeren <kayazeren@gmail.com>
Co-authored-by: jprusch <rs@schaeferbarthold.de>
Co-authored-by: JtheBAB <srast@bioc.uzh.ch>
Co-authored-by: Erik Pfeiffer <erik@pfeiffers.it>
Co-authored-by: Anonymous <noreply@weblate.org>
Co-authored-by: Hosted Weblate <hosted@weblate.org>
2021-10-12 18:12:53 +02:00
Weblate (bot)
729f098019 Translations update from Weblate (#5743)
* Translated using Weblate (French)

Currently translated at 100.0% (773 of 773 strings)

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

* Translated using Weblate (Japanese)

Currently translated at 100.0% (773 of 773 strings)

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

* Translated using Weblate (Hungarian)

Currently translated at 100.0% (773 of 773 strings)

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

* Translated using Weblate (English (Australia))

Currently translated at 100.0% (773 of 773 strings)

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

* Translated using Weblate (Spanish)

Currently translated at 100.0% (773 of 773 strings)

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

* Translated using Weblate (Swedish)

Currently translated at 100.0% (773 of 773 strings)

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

* Translated using Weblate (Polish)

Currently translated at 100.0% (773 of 773 strings)

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

* Translated using Weblate (Turkish)

Currently translated at 100.0% (773 of 773 strings)

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

* Translated using Weblate (German)

Currently translated at 100.0% (777 of 777 strings)

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

Translated using Weblate (German)

Currently translated at 100.0% (777 of 777 strings)

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

* Translated using Weblate (German)

Currently translated at 100.0% (777 of 777 strings)

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

Co-authored-by: Nathanaël <contact@nathanaelhoun.fr>
Co-authored-by: kaakaa <stooner.hoe@gmail.com>
Co-authored-by: Tóth Csaba // Online ERP Hungary Kft <csaba.toth@online-erp.hu>
Co-authored-by: Matthew Williams <Matthew.Williams@outlook.com.au>
Co-authored-by: Elias  Nahum <elias@mattermost.com>
Co-authored-by: MArtin Johnson <martinjohnson@bahnhof.se>
Co-authored-by: master7 <marcin.karkosz@rajska.info>
Co-authored-by: Kaya Zeren <kayazeren@gmail.com>
Co-authored-by: jprusch <rs@schaeferbarthold.de>
Co-authored-by: JtheBAB <srast@bioc.uzh.ch>
2021-10-12 09:22:14 +02:00
Anurag Shivarathri
856d8bd05f MM-34842 global threads options (#5630)
* fixes MM-37294 MM-37296 MM-37297

* Added conditions to check for post & thread existence

* Update app/mm-redux/selectors/entities/threads.ts

Co-authored-by: Kyriakos Z. <3829551+koox00@users.noreply.github.com>

* type fix

* Never disabling Mark All as unread

* Added follow/unfollow message for not yet thread posts

* Test case fix for mark all as unread enabled all the time

* Removed hardcoded condition

* Fixed MM-37509

* Updated snapshot for sidebar

* Global thread actions init

* Added options

* Update post_options.js

* Test cases fix

* Added border bottom for each thread option

* Update test case

* Reverting snapshot

* Updated snapshot

* Moved options to screens & removed redundants translations

* Reusing post_option for thread_option

* Component name changed to PostOption from ThreadOption

* Snapshot updated

* Removed factory

* Update app/screens/thread_options/index.ts

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

* Update app/screens/thread_options/index.ts

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

Co-authored-by: Kyriakos Z. <3829551+koox00@users.noreply.github.com>
Co-authored-by: Mattermod <mattermod@users.noreply.github.com>
Co-authored-by: Elias Nahum <nahumhbl@gmail.com>
2021-10-11 13:24:38 +05:30
Elias Nahum
5e0c75d772 Bump app build number to 374 (#5741) 2021-10-10 11:52:35 -03:00
Elias Nahum
93dbe4af9e Bump app build number to 373 (#5739) 2021-10-10 11:08:02 -03:00
Elias Nahum
16583c2a3b Fix send message with physical keyboard on iPad 15 (#5737) 2021-10-10 10:49:11 -03:00
Elias Nahum
42071314d3 Bump app build number to 372 (#5734) 2021-10-08 09:49:07 -03:00
Ben Lloyd Pearson
6586d1b283 Update Readme intro and add logo (#5729)
* Update intro and add logo

* Update metadata to new messaging

* Update README.md

Co-authored-by: Amy Blais <29708087+amyblais@users.noreply.github.com>

Co-authored-by: Elias Nahum <nahumhbl@gmail.com>
Co-authored-by: Amy Blais <29708087+amyblais@users.noreply.github.com>
2021-10-08 09:37:53 -03:00
Shaz Amjad
a3c00f59d6 Local only collapse state for mobile (#5721) 2021-10-08 09:32:31 -03:00
Shaz Amjad
000caf09e9 Hides favourites category if empty (#5722) 2021-10-08 09:32:19 -03:00
Shaz Amjad
bd74310f29 Cancel button restored (#5725) 2021-10-08 09:31:56 -03:00
Anurag Shivarathri
a81b56212e MM-38784, MM-38409 New messages line in threads screen & Pull to refresh for global threads screen (#5690)
* New messages line in threads & Pull to refresh for global threads

* Updated snapshot

* Using postlist's RefreshControl component

* Updated threadlist snapshot

* Reverting snapshot

* Updated Snapshots

* Snapshots updated

* Added 'thread last viewed at' to handle new messages line correctly

* Lint fix

* Updated snapshots

* Reverted comparision check

* Remove unused code

* Batching actions

* Do not add new message line for self messages

Co-authored-by: Mattermod <mattermod@users.noreply.github.com>
2021-10-07 15:14:34 +05:30
Anurag Shivarathri
1a35250811 Excluding follow button in the header from being added to the stack (#5723) 2021-10-06 18:07:20 -04:00
Elias Nahum
f1c3538283 MM-39054 patch Android react-native-file-viewer (#5717) 2021-10-06 10:12:10 -03:00
Weblate (bot)
7dd3fbb75d Translated using Weblate (German) (#5719)
Currently translated at 100.0% (773 of 773 strings)

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

Co-authored-by: JtheBAB <srast@bioc.uzh.ch>
2021-10-05 10:06:29 -05:00
Claudio Costa
5260e252a4 Show confirm modal for at here mentions (#5678) 2021-10-05 15:27:25 +02:00
Elias Nahum
e301c9d5d1 Bump app build number to 371 (#5715) 2021-10-04 15:56:51 -03:00
Weblate (bot)
0332b52ee2 Translations update from Weblate (#5713)
* Translated using Weblate (Swedish)

Currently translated at 100.0% (771 of 771 strings)

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

* Translated using Weblate (Spanish)

Currently translated at 100.0% (771 of 771 strings)

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

Co-authored-by: MArtin Johnson <martinjohnson@bahnhof.se>
Co-authored-by: Elias  Nahum <elias@mattermost.com>
2021-10-04 11:36:09 -05:00
Elias Nahum
a28a3826fa update dependencies (#5706) 2021-10-01 11:45:53 -03:00
Weblate (bot)
8447d7feea Translations update from Weblate (#5694)
* Translated using Weblate (German)

Currently translated at 100.0% (771 of 771 strings)

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

* Translated using Weblate (French)

Currently translated at 100.0% (771 of 771 strings)

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

* Translated using Weblate (Dutch)

Currently translated at 100.0% (771 of 771 strings)

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

* Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (771 of 771 strings)

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

* Translated using Weblate (Hungarian)

Currently translated at 100.0% (771 of 771 strings)

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

* Translated using Weblate (English (Australia))

Currently translated at 100.0% (771 of 771 strings)

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

* Translated using Weblate (Japanese)

Currently translated at 100.0% (771 of 771 strings)

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

* Translated using Weblate (Polish)

Currently translated at 100.0% (771 of 771 strings)

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

* Translated using Weblate (Turkish)

Currently translated at 100.0% (771 of 771 strings)

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

Translated using Weblate (Turkish)

Currently translated at 100.0% (771 of 771 strings)

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

Co-authored-by: JtheBAB <srast@bioc.uzh.ch>
Co-authored-by: Nathanaël <contact@nathanaelhoun.fr>
Co-authored-by: Tom De Moor <tom@controlaltdieliet.be>
Co-authored-by: aeomin <lin@aeomin.net>
Co-authored-by: Tóth Csaba // Online ERP Hungary Kft <csaba.toth@online-erp.hu>
Co-authored-by: Matthew Williams <Matthew.Williams@outlook.com.au>
Co-authored-by: kaakaa <stooner.hoe@gmail.com>
Co-authored-by: master7 <marcin.karkosz@rajska.info>
Co-authored-by: Kaya Zeren <kayazeren@gmail.com>
2021-09-30 09:50:44 -03:00
Elias Nahum
523777a207 Use @mattermost/react-native-paste-input to allow pasting of images & files (#5703)
* Use @mattermost/react-native-paste-input to allow pasting of images & files

* upgrade @mattermost/react-native-paste-input
2021-09-30 09:34:57 -03:00
Elias Nahum
d9e8b3e08e Bump app build number to 370 (#5700) 2021-09-28 11:38:35 -03:00
Shaz Amjad
b36dbf9b34 [MM-38683] Android Initialisation Bug + BottomSheet Correction (#5685)
* Fixes various init bugs for android

* Ignores Type Error

* MM-38683: Removes 'cancel' button.

* MM-38683: Removes some 'hideCancel' props.

* Android bottom margin

* Update options modal style

Co-authored-by: Martin Kraft <martin@upspin.org>
Co-authored-by: Elias Nahum <nahumhbl@gmail.com>
2021-09-28 11:28:20 -03:00
Elias Nahum
7e9c574c3f Fix android extension crash (#5696) 2021-09-28 11:27:46 -03:00
Daniel Espino García
3668cb62d3 MM-34811 Add plugin oauth support (#5395)
* Add plugin oauth support

* Fix types

* Switch go to screen by show modal

* Add content width metadata to show correctly the sites

* Remove unneeded metadata

* Fix tsc

* Fix lint

Co-authored-by: Mattermod <mattermod@users.noreply.github.com>
2021-09-27 18:17:56 +02:00
Daniel Espino García
a2ed6ba3bd Sync with App Command Parser with Webapp (#5631)
* Sync with App Command Parser with Webapp

* Add i18n

* Patch flaky test
2021-09-27 14:54:32 +02:00
Daniel Espino García
dd36545079 [MM-36041] Ensure post options and app commands always have the bindings for its channel (#5563)
* Ensure post options always have the bindings for its channel

* Mimic webapp model

* Address feedback

* Add return to validate bindings

* Address feedback

* Use empty bindings constant to avoid rerenderings

Co-authored-by: Michael Kochell <6913320+mickmister@users.noreply.github.com>
2021-09-24 10:32:58 -04:00
Daniel Espino García
edfd743699 [MM-38216] Add multiteam mentions and saved posts (#5677)
* Add multiteam mentions and saved posts

* Minor fix

* Fix long names

* Add tests, improve separation styling, revert changes on the flagged posts client, and omit the team name when the user only belongs to one team

* Fix separator on iOS

* Update snapshot

* Fix separator

* Fix snapshot

* Differentiate styling between iOS and Android

* Change channelTeamName to teamName
2021-09-24 09:35:01 -03:00
Daniel Espino García
6dbb537f22 Open forms in bindings on all locations (#5665) 2021-09-24 09:34:40 -03:00
Daniel Espino García
aa776e4ae6 Fix warnings on channel info row Formatted Text (#5644) 2021-09-23 21:55:34 +02:00
Amy Blais
1287357e7c Update NOTICE.txt (#5688) 2021-09-22 15:35:30 -03:00
Elias Nahum
ada6be9b7a Update dependencies (#5686)
* Update dependencies

* Fix unsigned builds
2021-09-22 13:54:12 -03:00
306 changed files with 28149 additions and 9936 deletions

View File

@@ -57,7 +57,7 @@
"newlines-between": "always",
"pathGroups": [
{
"pattern": "@(@react-native-community|@react-native-cookies|@react-navigation|@rudderstack|@sentry|@testing-library|@storybook)/**",
"pattern": "@(@react-native-async-storage|@react-native-community|@react-native-cookies|@react-navigation|@rudderstack|@sentry|@testing-library|@storybook)/**",
"group": "external",
"position": "before"
},

View File

@@ -25,6 +25,8 @@ emoji=true
exact_by_default=true
format.bracket_spacing=false
module.file_ext=.js
module.file_ext=.json
module.file_ext=.ios.js
@@ -61,4 +63,4 @@ untyped-import
untyped-type-import
[version]
^0.149.0
^0.158.0

View File

@@ -26,6 +26,7 @@ Place an '[x]' (no spaces) in all applicable fields. Please remove unrelated fie
- [ ] Added or updated unit tests (required for all new features)
- [ ] Has UI changes
- [ ] Includes text changes and localization file updates
- [ ] Have tested against the 5 core themes to ensure consistency between them.
#### Device Information
This PR was tested on: <!-- Device name(s), OS version(s) -->

View File

@@ -43,6 +43,41 @@ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
---
## @mattermost/react-native-paste-input
This product contains '@mattermost/react-native-paste-input' by Mattermost.
React Native TextInput component have functionality to capture text input from a user by using the soft and hardware keyboards but lacks the ability to restrict copy & paste options as well as allwing pasting different files formats copied from other apps, like images & videos from the Photos gallery app.
* HOMEPAGE:
* https://github.com/mattermost/react-native-paste-input
* LICENSE: MIT
MIT License
Copyright (c) 2020 Elias Nahum
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/async-storage
This product contains 'async-storage' by Krzysztof Borowy.

View File

@@ -119,6 +119,11 @@ def jscFlavor = 'org.webkit:android-jsc-intl:+'
*/
def enableHermes = project.ext.react.get("enableHermes", false);
/**
* Architectures to build native code for in debug.
*/
def nativeArchitectures = project.getProperties().get("reactNativeDebugArchitectures")
android {
compileSdkVersion rootProject.ext.compileSdkVersion
@@ -127,8 +132,8 @@ android {
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
missingDimensionStrategy "RNNotifications.reactNativeVersion", "reactNative60"
versionCode 377
versionName "1.47.2"
versionCode 382
versionName "1.48.2"
multiDexEnabled = true
testBuildType System.getProperty('testBuildType', 'debug')
testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner'
@@ -160,6 +165,11 @@ android {
debug {
minifyEnabled enableProguardInReleaseBuilds
proguardFiles getDefaultProguardFile("proguard-android.txt"), "proguard-rules.pro"
if (nativeArchitectures) {
ndk {
abiFilters nativeArchitectures.split(',')
}
}
}
unsigned.initWith(buildTypes.release)
unsigned {
@@ -225,7 +235,7 @@ dependencies {
implementation "androidx.swiperefreshlayout:swiperefreshlayout:1.0.0"
debugImplementation("com.facebook.flipper:flipper:${FLIPPER_VERSION}") {
exclude group:'com.facebook.fbjni'
exclude group:'com.facebook.fbjni'
}
debugImplementation("com.facebook.flipper:flipper-network-plugin:${FLIPPER_VERSION}") {
exclude group:'com.facebook.flipper'

View File

@@ -9,6 +9,10 @@
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.BLUETOOTH" />
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<uses-permission android:name="android.permission.WAKE_LOCK" />
<application
android:name=".MainApplication"
@@ -78,5 +82,9 @@
</intent-filter>
</activity>
</application>
<queries>
<intent>
<action android:name="com.google.android.youtube.api.service.START" />
</intent>
</queries>
</manifest>

View File

@@ -15,7 +15,6 @@ import androidx.annotation.Nullable;
import androidx.core.app.NotificationCompat;
import androidx.core.app.NotificationManagerCompat;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
@@ -30,7 +29,6 @@ import static com.wix.reactnativenotifications.Defs.NOTIFICATION_RECEIVED_EVENT_
import com.mattermost.react_native_interface.ResolvePromise;
import org.json.JSONArray;
import org.json.JSONObject;
public class CustomPushNotification extends PushNotification {
@@ -61,7 +59,7 @@ public class CustomPushNotification extends PushNotification {
editor.apply();
}
Map<String, List<Integer>> inputMap = new HashMap<>();
Map<String, Map<String, JSONObject>> inputMap = new HashMap<>();
saveNotificationsMap(context, inputMap);
}
} catch (PackageManager.NameNotFoundException e) {
@@ -69,55 +67,70 @@ public class CustomPushNotification extends PushNotification {
}
}
public static void cancelNotification(Context context, String channelId, Integer notificationId) {
public static void cancelNotification(Context context, String channelId, String rootId, Integer notificationId, Boolean isCRTEnabled) {
if (!android.text.TextUtils.isEmpty(channelId)) {
Map<String, List<Integer>> notificationsInChannel = loadNotificationsMap(context);
List<Integer> notifications = notificationsInChannel.get(channelId);
final String notificationIdStr = notificationId.toString();
final Boolean isThreadNotification = isCRTEnabled && !android.text.TextUtils.isEmpty(rootId);
final String groupId = isThreadNotification ? rootId : channelId;
Map<String, Map<String, JSONObject>> notificationsInChannel = loadNotificationsMap(context);
Map<String, JSONObject> notifications = notificationsInChannel.get(groupId);
if (notifications == null) {
return;
}
final NotificationManager notificationManager = context.getSystemService(NotificationManager.class);
notificationManager.cancel(notificationId);
notifications.remove(notificationId);
notifications.remove(notificationIdStr);
final StatusBarNotification[] statusNotifications = notificationManager.getActiveNotifications();
boolean hasMore = false;
for (final StatusBarNotification status : statusNotifications) {
if (status.getNotification().extras.getString("channel_id").equals(channelId)) {
hasMore = true;
Bundle bundle = status.getNotification().extras;
if (isThreadNotification) {
hasMore = bundle.getString("root_id").equals(rootId);
} else if (isCRTEnabled) {
hasMore = !bundle.getString("root_id").equals(rootId);
} else {
hasMore = bundle.getString("channel_id").equals(channelId);
}
if (hasMore) {
break;
}
}
if (!hasMore) {
notificationsInChannel.remove(channelId);
notificationsInChannel.remove(groupId);
} else {
notificationsInChannel.put(groupId, notifications);
}
saveNotificationsMap(context, notificationsInChannel);
}
}
public static void clearChannelNotifications(Context context, String channelId) {
public static void clearChannelNotifications(Context context, String channelId, String rootId, Boolean isCRTEnabled) {
if (!android.text.TextUtils.isEmpty(channelId)) {
Map<String, List<Integer>> notificationsInChannel = loadNotificationsMap(context);
List<Integer> notifications = notificationsInChannel.get(channelId);
final NotificationManagerCompat notificationManager = NotificationManagerCompat.from(context);
// rootId is available only when CRT is enabled & clearing the thread
final boolean isClearThread = isCRTEnabled && !android.text.TextUtils.isEmpty(rootId);
Map<String, Map<String, JSONObject>> notificationsInChannel = loadNotificationsMap(context);
String groupId = isClearThread ? rootId : channelId;
Map<String, JSONObject> notifications = notificationsInChannel.get(groupId);
if (notifications == null) {
return;
}
notificationsInChannel.remove(channelId);
notificationsInChannel.remove(groupId);
saveNotificationsMap(context, notificationsInChannel);
for (final Integer notificationId : notifications) {
final NotificationManagerCompat notificationManager = NotificationManagerCompat.from(context);
notificationManager.cancel(notificationId);
}
notifications.forEach(
(notificationIdStr, post) -> notificationManager.cancel(Integer.valueOf(notificationIdStr))
);
}
}
public static void clearAllNotifications(Context context) {
if (context != null) {
Map<String, List<Integer>> notificationsInChannel = loadNotificationsMap(context);
Map<String, Map<String, JSONObject>> notificationsInChannel = loadNotificationsMap(context);
notificationsInChannel.clear();
saveNotificationsMap(context, notificationsInChannel);
final NotificationManagerCompat notificationManager = NotificationManagerCompat.from(context);
@@ -132,6 +145,8 @@ public class CustomPushNotification extends PushNotification {
final String ackId = initialData.getString("ack_id");
final String postId = initialData.getString("post_id");
final String channelId = initialData.getString("channel_id");
final String rootId = initialData.getString("root_id");
final boolean isCRTEnabled = initialData.getString("is_crt_enabled") != null && initialData.getString("is_crt_enabled").equals("true");
final boolean isIdLoaded = initialData.getString("id_loaded") != null && initialData.getString("id_loaded").equals("true");
int notificationId = CustomPushNotificationHelper.MESSAGE_NOTIFICATION_ID;
if (postId != null) {
@@ -165,24 +180,41 @@ public class CustomPushNotification extends PushNotification {
if (type.equals(PUSH_TYPE_MESSAGE)) {
if (channelId != null) {
Map<String, List<Integer>> notificationsInChannel = loadNotificationsMap(mContext);
List<Integer> list = notificationsInChannel.get(channelId);
if (list == null) {
list = Collections.synchronizedList(new ArrayList(0));
}
try {
list.add(0, notificationId);
if (list.size() > 1) {
createSummary = false;
}
JSONObject post = new JSONObject();
if (!android.text.TextUtils.isEmpty(rootId)) {
post.put("root_id", rootId);
}
if (!android.text.TextUtils.isEmpty(postId)) {
post.put("post_id", postId);
}
if (createSummary) {
// Add the summary notification id as well
list.add(0, notificationId + 1);
}
final Boolean isThreadNotification = isCRTEnabled && post.has("root_id");
final String groupId = isThreadNotification ? rootId : channelId;
Map<String, Map<String, JSONObject>> notificationsInChannel = loadNotificationsMap(mContext);
Map<String, JSONObject> notifications = notificationsInChannel.get(groupId);
if (notifications == null) {
notifications = Collections.synchronizedMap(new HashMap<String, JSONObject>());
}
notificationsInChannel.put(channelId, list);
saveNotificationsMap(mContext, notificationsInChannel);
if (notifications.size() > 0) {
createSummary = false;
}
notifications.put(String.valueOf(notificationId), post);
if (createSummary) {
// Add the summary notification id as well
notifications.put(String.valueOf(notificationId + 1), new JSONObject());
}
notificationsInChannel.put(groupId, notifications);
saveNotificationsMap(mContext, notificationsInChannel);
} catch(Exception e) {
e.printStackTrace();
}
}
}
@@ -190,7 +222,7 @@ public class CustomPushNotification extends PushNotification {
}
break;
case PUSH_TYPE_CLEAR:
clearChannelNotifications(mContext, channelId);
clearChannelNotifications(mContext, channelId, rootId, isCRTEnabled);
break;
}
@@ -205,22 +237,11 @@ public class CustomPushNotification extends PushNotification {
Bundle data = mNotificationProps.asBundle();
final String channelId = data.getString("channel_id");
final String postId = data.getString("post_id");
Integer notificationId = CustomPushNotificationHelper.MESSAGE_NOTIFICATION_ID;
if (postId != null) {
notificationId = postId.hashCode();
}
final String rootId = data.getString("root_id");
final Boolean isCRTEnabled = data.getBoolean("is_crt_enabled");
if (channelId != null) {
Map<String, List<Integer>> notificationsInChannel = loadNotificationsMap(mContext);
List<Integer> notifications = notificationsInChannel.get(channelId);
if (notifications == null) {
return;
}
notifications.remove(notificationId);
saveNotificationsMap(mContext, notificationsInChannel);
clearChannelNotifications(mContext, channelId);
clearChannelNotifications(mContext, channelId, rootId, isCRTEnabled);
}
}
@@ -253,7 +274,7 @@ public class CustomPushNotification extends PushNotification {
mJsIOHelper.sendEventToJS(NOTIFICATION_RECEIVED_EVENT_NAME, mNotificationProps.asBundle(), mAppLifecycleFacade.getRunningReactContext());
}
private static void saveNotificationsMap(Context context, Map<String, List<Integer>> inputMap) {
private static void saveNotificationsMap(Context context, Map<String, Map<String, JSONObject>> inputMap) {
SharedPreferences pSharedPref = context.getSharedPreferences(PUSH_NOTIFICATIONS, Context.MODE_PRIVATE);
if (pSharedPref != null && context != null) {
JSONObject json = new JSONObject(inputMap);
@@ -265,23 +286,41 @@ public class CustomPushNotification extends PushNotification {
}
}
private static Map<String, List<Integer>> loadNotificationsMap(Context context) {
Map<String, List<Integer>> outputMap = new HashMap<>();
/**
* Map Structure
*
* {
* channel_id1 | thread_id1: {
* notification_id1: {
* post_id: 'p1',
* root_id: 'r1',
* }
* }
* }
*
*/
private static Map<String, Map<String, JSONObject>> loadNotificationsMap(Context context) {
Map<String, Map<String, JSONObject>> outputMap = new HashMap<>();
if (context != null) {
SharedPreferences pSharedPref = context.getSharedPreferences(PUSH_NOTIFICATIONS, Context.MODE_PRIVATE);
try {
if (pSharedPref != null) {
String jsonString = pSharedPref.getString(NOTIFICATIONS_IN_CHANNEL, (new JSONObject()).toString());
JSONObject json = new JSONObject(jsonString);
Iterator<String> keysItr = json.keys();
while (keysItr.hasNext()) {
String key = keysItr.next();
JSONArray array = json.getJSONArray(key);
List<Integer> values = new ArrayList<>();
for (int i = 0; i < array.length(); ++i) {
values.add(array.getInt(i));
// Can be a channel_id or thread_id
Iterator<String> groupIdsItr = json.keys();
while (groupIdsItr.hasNext()) {
String groupId = groupIdsItr.next();
JSONObject notificationsJSONObj = json.getJSONObject(groupId);
Map<String, JSONObject> notifications = new HashMap<>();
Iterator<String> notificationIdKeys = notificationsJSONObj.keys();
while(notificationIdKeys.hasNext()) {
String notificationId = notificationIdKeys.next();
JSONObject post = notificationsJSONObj.getJSONObject(notificationId);
notifications.put(notificationId, post);
}
outputMap.put(key, values);
outputMap.put(groupId, notifications);
}
}
} catch (Exception e) {

View File

@@ -98,6 +98,16 @@ public class CustomPushNotificationHelper {
userInfoBundle = new Bundle();
}
String postId = bundle.getString("post_id");
if (postId != null) {
userInfoBundle.putString("post_id", postId);
}
String rootId = bundle.getString("root_id");
if (rootId != null) {
userInfoBundle.putString("root_id", rootId);
}
String channelId = bundle.getString("channel_id");
if (channelId != null) {
userInfoBundle.putString("channel_id", channelId);
@@ -145,13 +155,17 @@ public class CustomPushNotificationHelper {
String channelId = bundle.getString("channel_id");
String postId = bundle.getString("post_id");
String rootId = bundle.getString("root_id");
int notificationId = postId != null ? postId.hashCode() : MESSAGE_NOTIFICATION_ID;
NotificationPreferences notificationPreferences = NotificationPreferences.getInstance(context);
Boolean is_crt_enabled = bundle.getString("is_crt_enabled") != null && bundle.getString("is_crt_enabled").equals("true");
String groupId = is_crt_enabled && !android.text.TextUtils.isEmpty(rootId) ? rootId : channelId;
addNotificationExtras(notification, bundle);
setNotificationIcons(context, notification, bundle);
setNotificationMessagingStyle(context, notification, bundle);
setNotificationGroup(notification, channelId, createSummary);
setNotificationGroup(notification, groupId, createSummary);
setNotificationBadgeType(notification);
setNotificationSound(notification, notificationPreferences);
setNotificationVibrate(notification, notificationPreferences);

View File

@@ -19,6 +19,9 @@ public class NotificationDismissService extends IntentService {
final Bundle bundle = NotificationIntentAdapter.extractPendingNotificationDataFromIntent(intent);
final String channelId = bundle.getString("channel_id");
final String postId = bundle.getString("post_id");
final String rootId = bundle.getString("root_id");
final Boolean isCRTEnabled = bundle.getString("is_crt_enabled") != null && bundle.getString("is_crt_enabled").equals("true");
int notificationId = CustomPushNotificationHelper.MESSAGE_NOTIFICATION_ID;
if (postId != null) {
notificationId = postId.hashCode();
@@ -26,7 +29,7 @@ public class NotificationDismissService extends IntentService {
notificationId = channelId.hashCode();
}
CustomPushNotification.cancelNotification(context, channelId, notificationId);
CustomPushNotification.cancelNotification(context, channelId, rootId, notificationId, isCRTEnabled);
Log.i("ReactNative", "Dismiss notification");
}
}

View File

@@ -118,6 +118,10 @@ public class NotificationPreferencesModule extends ReactContextBaseJavaModule {
WritableMap map = Arguments.createMap();
Notification n = sbn.getNotification();
Bundle bundle = n.extras;
String postId = bundle.getString("post_id");
map.putString("post_id", postId);
String rootId = bundle.getString("root_id");
map.putString("root_id", rootId);
String channelId = bundle.getString("channel_id");
map.putString("channel_id", channelId);
result.pushMap(map);
@@ -126,8 +130,9 @@ public class NotificationPreferencesModule extends ReactContextBaseJavaModule {
}
@ReactMethod
public void removeDeliveredNotifications(String channelId) {
public void removeDeliveredNotifications(String channelId, String rootId, Boolean isCRTEnabled) {
final Context context = mApplication.getApplicationContext();
CustomPushNotification.clearChannelNotifications(context, channelId);
CustomPushNotification.clearChannelNotifications(context, channelId, rootId, isCRTEnabled);
}
}

View File

@@ -10,7 +10,7 @@ buildscript {
kotlinVersion = "1.5.30"
firebaseVersion = "21.0.0"
RNNKotlinVersion = kotlinVersion
ndkVersion = "20.1.5948944"
ndkVersion = "21.4.7075529"
}
repositories {
@@ -20,7 +20,7 @@ buildscript {
mavenCentral()
}
dependencies {
classpath 'com.android.tools.build:gradle:4.2.1'
classpath 'com.android.tools.build:gradle:4.2.2'
classpath 'com.google.gms:google-services:4.2.0'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlinVersion"
@@ -52,9 +52,6 @@ allprojects {
maven {
url "https://www.jitpack.io"
}
maven {
url ("https://dl.bintray.com/rudderstack/rudderstack")
}
maven {
url "$rootDir/../node_modules/detox/Detox-android"
}

View File

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

View File

@@ -390,7 +390,7 @@ export function markAsViewedAndReadBatch(state, channelId, prevChannelId = '', m
type: ChannelTypes.SET_UNREAD_MSG_COUNT,
data: {
channelId,
count: unreadMessageCount,
count: isCollapsedThreadsEnabled(state) ? unreadMessageCountRoot : unreadMessageCount,
},
}, {
type: ChannelTypes.DECREMENT_UNREAD_MSG_COUNT,

View File

@@ -19,6 +19,11 @@ export function setCustomStatus(customStatus: UserCustomStatus): ActionFunc {
user.props.customStatus = JSON.stringify(customStatus);
dispatch({type: UserTypes.RECEIVED_ME, data: user});
// Server does not like empty 'expires_at' string.
if (!customStatus.expires_at) {
delete customStatus.expires_at;
}
try {
await Client4.updateCustomStatus(customStatus);
} catch (error) {

View File

@@ -2,6 +2,16 @@
// See LICENSE.txt for license information.
import {ViewTypes} from '@constants';
export function updateThreadLastViewedAt(threadId: string, lastViewedAt: number) {
return {
type: ViewTypes.THREAD_LAST_VIEWED_AT,
data: {
threadId,
lastViewedAt,
},
};
}
export const handleViewingGlobalThreadsScreen = () => (
{
type: ViewTypes.VIEWING_GLOBAL_THREADS_SCREEN,

View File

@@ -1,7 +1,8 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {fetchAppBindings} from '@mm-redux/actions/apps';
import {fetchAppBindings, fetchThreadAppBindings} from '@mm-redux/actions/apps';
import {getThreadAppsBindingsChannelId} from '@mm-redux/selectors/entities/apps';
import {getCurrentChannelId} from '@mm-redux/selectors/entities/common';
import {getCurrentUserId} from '@mm-redux/selectors/entities/users';
import {ActionResult, DispatchFunc, GetStateFunc} from '@mm-redux/types/actions';
@@ -10,9 +11,17 @@ import {appsEnabled} from '@utils/apps';
export function handleRefreshAppsBindings() {
return (dispatch: DispatchFunc, getState: GetStateFunc): ActionResult => {
const state = getState();
if (appsEnabled(state)) {
dispatch(fetchAppBindings(getCurrentUserId(state), getCurrentChannelId(state)));
if (!appsEnabled(state)) {
return {data: true};
}
dispatch(fetchAppBindings(getCurrentUserId(state), getCurrentChannelId(state)));
const threadChannelID = getThreadAppsBindingsChannelId(state);
if (threadChannelID) {
dispatch(fetchThreadAppBindings(getCurrentUserId(state), threadChannelID));
}
return {data: true};
};
}

View File

@@ -11,7 +11,7 @@ import {getThreads} from '@mm-redux/actions/threads';
import {getProfilesByIds, getStatusesByIds} from '@mm-redux/actions/users';
import {General} from '@mm-redux/constants';
import {getCurrentChannelId, getCurrentChannelStats} from '@mm-redux/selectors/entities/channels';
import {getConfig} from '@mm-redux/selectors/entities/general';
import {getConfig, getFeatureFlagValue} from '@mm-redux/selectors/entities/general';
import {getPostIdsInChannel} from '@mm-redux/selectors/entities/posts';
import {isCollapsedThreadsEnabled} from '@mm-redux/selectors/entities/preferences';
import {getCurrentTeamId} from '@mm-redux/selectors/entities/teams';
@@ -23,6 +23,20 @@ import {TeamMembership} from '@mm-redux/types/teams';
import {WebSocketMessage} from '@mm-redux/types/websocket';
import EventEmitter from '@mm-redux/utils/event_emitter';
import {removeUserFromList} from '@mm-redux/utils/user_utils';
import {loadCalls} from '@mmproducts/calls/store/actions/calls';
import {
handleCallStarted,
handleCallUserConnected,
handleCallUserDisconnected,
handleCallUserMuted,
handleCallUserUnmuted,
handleCallUserVoiceOn,
handleCallUserVoiceOff,
handleCallChannelEnabled,
handleCallChannelDisabled,
handleCallScreenOn,
handleCallScreenOff,
} from '@mmproducts/calls/store/actions/websockets';
import {getChannelSinceValue} from '@utils/channels';
import websocketClient from '@websocket';
@@ -147,6 +161,10 @@ export function doReconnect(now: number) {
const {data: me}: any = await dispatch(loadMe(null, null, true));
if (!me.error) {
if (getFeatureFlagValue(getState(), 'CallsMobile') === 'true') {
dispatch(loadCalls());
}
const roles = [];
if (me.roles?.length) {
@@ -325,7 +343,7 @@ function handleClose(connectFailCount: number) {
}
function handleEvent(msg: WebSocketMessage) {
return (dispatch: DispatchFunc) => {
return (dispatch: DispatchFunc, getState: GetStateFunc) => {
switch (msg.event) {
case WebsocketEvents.POSTED:
case WebsocketEvents.EPHEMERAL_MESSAGE:
@@ -421,6 +439,33 @@ function handleEvent(msg: WebSocketMessage) {
return dispatch(handleSidebarCategoryOrderUpdated(msg));
}
if (getFeatureFlagValue(getState(), 'CallsMobile') === 'true') {
switch (msg.event) {
case WebsocketEvents.CALLS_CHANNEL_ENABLED:
return dispatch(handleCallChannelEnabled(msg));
case WebsocketEvents.CALLS_CHANNEL_DISABLED:
return dispatch(handleCallChannelDisabled(msg));
case WebsocketEvents.CALLS_USER_CONNECTED:
return dispatch(handleCallUserConnected(msg));
case WebsocketEvents.CALLS_USER_DISCONNECTED:
return dispatch(handleCallUserDisconnected(msg));
case WebsocketEvents.CALLS_USER_MUTED:
return dispatch(handleCallUserMuted(msg));
case WebsocketEvents.CALLS_USER_UNMUTED:
return dispatch(handleCallUserUnmuted(msg));
case WebsocketEvents.CALLS_USER_VOICE_ON:
return dispatch(handleCallUserVoiceOn(msg));
case WebsocketEvents.CALLS_USER_VOICE_OFF:
return dispatch(handleCallUserVoiceOff(msg));
case WebsocketEvents.CALLS_CALL_START:
return dispatch(handleCallStarted(msg));
case WebsocketEvents.CALLS_SCREEN_ON:
return dispatch(handleCallScreenOn(msg));
case WebsocketEvents.CALLS_SCREEN_OFF:
return dispatch(handleCallScreenOff(msg));
}
}
return {data: true};
};
}

View File

@@ -1,10 +1,14 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {batchActions} from 'redux-batched-actions';
import {updateThreadLastViewedAt} from '@actions/views/threads';
import {handleThreadArrived, handleReadChanged, handleAllMarkedRead, handleFollowChanged, getThread as fetchThread} from '@mm-redux/actions/threads';
import {getCurrentUserId} from '@mm-redux/selectors/entities/common';
import {getSelectedPost} from '@mm-redux/selectors/entities/posts';
import {getCurrentTeamId} from '@mm-redux/selectors/entities/teams';
import {getThread} from '@mm-redux/selectors/entities/threads';
import {ActionResult, DispatchFunc, GetStateFunc} from '@mm-redux/types/actions';
import {ActionResult, DispatchFunc, GenericAction, GetStateFunc} from '@mm-redux/types/actions';
import {WebSocketMessage} from '@mm-redux/types/websocket';
export function handleThreadUpdated(msg: WebSocketMessage) {
@@ -27,21 +31,31 @@ export function handleThreadReadChanged(msg: WebSocketMessage) {
const thread = getThread(state, msg.data.thread_id);
// Mark only following threads as read.
if (thread?.is_following) {
dispatch(
handleReadChanged(
msg.data.thread_id,
msg.broadcast.team_id,
msg.data.channel_id,
{
lastViewedAt: msg.data.timestamp,
prevUnreadMentions: thread.unread_mentions,
newUnreadMentions: msg.data.unread_mentions,
prevUnreadReplies: thread.unread_replies,
newUnreadReplies: msg.data.unread_replies,
},
),
);
if (thread) {
const actions: GenericAction[] = [];
const selectedPost = getSelectedPost(state);
if (selectedPost?.id !== thread.id) {
actions.push(updateThreadLastViewedAt(thread.id, msg.data.timestamp));
}
if (thread.is_following) {
actions.push(
handleReadChanged(
msg.data.thread_id,
msg.broadcast.team_id,
msg.data.channel_id,
{
lastViewedAt: msg.data.timestamp,
prevUnreadMentions: thread.unread_mentions,
newUnreadMentions: msg.data.unread_mentions,
prevUnreadReplies: thread.unread_replies,
newUnreadReplies: msg.data.unread_replies,
},
),
);
}
if (actions.length) {
dispatch(batchActions(actions));
}
}
} else {
dispatch(handleAllMarkedRead(msg.broadcast.team_id));

View File

@@ -290,6 +290,10 @@ export default class ClientBase {
return `${this.url}/plugins/com.mattermost.apps`;
}
getCallsRoute() {
return `${this.url}/plugins/com.mattermost.calls`;
}
// Client Helpers
handleRedirectProtocol = (url: string, response: RNFetchBlobFetchRepsonse) => {
const serverUrl = this.getUrl();

View File

@@ -1,6 +1,7 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import ClientCalls, {ClientCallsMix} from '@mmproducts/calls/client/rest';
import mix from '@utils/mix';
import ClientApps, {ClientAppsMix} from './apps';
@@ -34,7 +35,8 @@ interface Client extends ClientBase,
ClientSharedChannelsMix,
ClientTeamsMix,
ClientTosMix,
ClientUsersMix
ClientUsersMix,
ClientCallsMix
{}
class Client extends mix(ClientBase).with(
@@ -52,6 +54,7 @@ class Client extends mix(ClientBase).with(
ClientTeams,
ClientTos,
ClientUsers,
ClientCalls,
) {}
const Client4 = new Client();

View File

@@ -250,8 +250,13 @@ const ClientPosts = (superclass: any) => class extends superclass {
searchPostsWithParams = async (teamId: string, params: any) => {
analytics.trackAPI('api_posts_search', {team_id: teamId});
let route = `${this.getPostsRoute()}/search`;
if (teamId) {
route = `${this.getTeamRoute(teamId)}/posts/search`;
}
return this.doFetch(
`${this.getTeamRoute(teamId)}/posts/search`,
route,
{method: 'post', body: JSON.stringify(params)},
);
};

View File

@@ -0,0 +1,7 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`FormattedRelativeTime should match snapshot 1`] = `
<Text>
a few seconds ago
</Text>
`;

View File

@@ -66,7 +66,7 @@ const GroupMentionItem = (props) => {
>
<View style={style.rowPicture}>
<CompassIcon
name='account-group-outline'
name='account-multiple-outline'
style={style.rowIcon}
/>
</View>

View File

@@ -4685,7 +4685,6 @@ exports[`components/autocomplete/emoji_suggestion should match snapshot 2`] = `
keyExtractor={[Function]}
keyboardShouldPersistTaps="always"
nestedScrollEnabled={false}
numColumns={1}
pageSize={10}
removeClippedSubviews={true}
renderItem={[Function]}

View File

@@ -31,7 +31,6 @@ exports[`components/autocomplete/slash_suggestion should match snapshot 1`] = `
keyExtractor={[Function]}
keyboardShouldPersistTaps="always"
nestedScrollEnabled={false}
numColumns={1}
removeClippedSubviews={true}
renderItem={[Function]}
style={

View File

@@ -33,7 +33,11 @@ describe('AppCommandParser', () => {
...reduxTestState,
entities: {
...reduxTestState.entities,
apps: {bindings},
apps: {
bindings,
threadBindings: bindings,
threadBindingsForms: {},
},
},
} as any;
const testStore = await mockStore(initialState);
@@ -415,6 +419,60 @@ describe('AppCommandParser', () => {
command: '/jira issue create --project == test',
submit: {expectError: 'Multiple `=` signs are not allowed.'},
},
{
title: 'rest field',
command: '/jira issue rest hello world',
autocomplete: {verify: (parsed: ParsedCommand): void => {
expect(parsed.state).toBe(ParseState.Rest);
expect(parsed.binding?.label).toBe('rest');
expect(parsed.incomplete).toBe('hello world');
expect(parsed.values?.summary).toBe(undefined);
}},
submit: {verify: (parsed: ParsedCommand): void => {
expect(parsed.state).toBe(ParseState.Rest);
expect(parsed.binding?.label).toBe('rest');
expect(parsed.values?.summary).toBe('hello world');
}},
},
{
title: 'rest field with other field',
command: '/jira issue rest --verbose true hello world',
autocomplete: {verify: (parsed: ParsedCommand): void => {
expect(parsed.state).toBe(ParseState.Rest);
expect(parsed.binding?.label).toBe('rest');
expect(parsed.incomplete).toBe('hello world');
expect(parsed.values?.summary).toBe(undefined);
expect(parsed.values?.verbose).toBe('true');
}},
submit: {verify: (parsed: ParsedCommand): void => {
expect(parsed.state).toBe(ParseState.Rest);
expect(parsed.binding?.label).toBe('rest');
expect(parsed.values?.summary).toBe('hello world');
expect(parsed.values?.verbose).toBe('true');
}},
},
{
title: 'rest field as flag with other field',
command: '/jira issue rest --summary "hello world" --verbose true',
autocomplete: {verify: (parsed: ParsedCommand): void => {
expect(parsed.state).toBe(ParseState.EndValue);
expect(parsed.binding?.label).toBe('rest');
expect(parsed.incomplete).toBe('true');
expect(parsed.values?.summary).toBe('hello world');
expect(parsed.values?.verbose).toBe(undefined);
}},
submit: {verify: (parsed: ParsedCommand): void => {
expect(parsed.state).toBe(ParseState.EndValue);
expect(parsed.binding?.label).toBe('rest');
expect(parsed.values?.summary).toBe('hello world');
expect(parsed.values?.verbose).toBe('true');
}},
},
{
title: 'error: rest after rest field flag',
command: '/jira issue rest --summary "hello world" --verbose true hello world',
submit: {expectError: 'Unable to identify argument.'},
},
];
table.forEach((tc) => {
@@ -504,6 +562,13 @@ describe('AppCommandParser', () => {
IconData: 'Create icon',
Description: 'Create a new Jira issue',
},
{
Suggestion: 'rest',
Complete: 'jira issue rest',
Hint: 'rest hint',
IconData: 'rest icon',
Description: 'rest description',
},
]);
});
@@ -524,6 +589,14 @@ describe('AppCommandParser', () => {
IconData: 'Create icon',
Description: 'Create a new Jira issue',
},
{
Suggestion: 'rest',
Complete: 'JiRa IsSuE rest',
Hint: 'rest hint',
IconData: 'rest icon',
Description: 'rest description',
},
]);
});
@@ -855,7 +928,7 @@ describe('AppCommandParser', () => {
context: {
app_id: 'jira',
channel_id: 'current_channel_id',
location: '/command',
location: '/command/jira/issue/create',
root_id: 'root_id',
team_id: 'team_id',
},

View File

@@ -4,6 +4,7 @@
/* eslint-disable max-lines */
import {
AppsTypes,
AppCallRequest,
AppBinding,
AppField,
@@ -32,7 +33,6 @@ import {
EXECUTE_CURRENT_COMMAND_ITEM_ID,
COMMAND_SUGGESTION_ERROR,
getExecuteSuggestion,
displayError,
createCallRequest,
selectUserByUsername,
getUserByUsername,
@@ -45,10 +45,13 @@ import {
filterEmptyOptions,
autocompleteUsersInChannel,
autocompleteChannels,
ExtendedAutocompleteSuggestion,
getChannelSuggestions,
getUserSuggestions,
inTextMentionSuggestions,
ExtendedAutocompleteSuggestion,
getAppCommandForm,
getAppRHSCommandForm,
makeRHSAppBindingSelector,
} from './app_command_parser_dependencies';
export interface Store {
@@ -74,6 +77,7 @@ export enum ParseState {
EndQuotedValue = 'EndQuotedValue',
EndTickedValue = 'EndTickedValue',
Error = 'Error',
Rest = 'Rest',
}
interface FormsCache {
@@ -85,6 +89,7 @@ interface Intl {
}
const getCommandBindings = makeAppBindingsSelector(AppBindingLocations.COMMAND);
const getRHSCommandBindings = makeRHSAppBindingSelector(AppBindingLocations.COMMAND);
export class ParsedCommand {
state = ParseState.Start;
@@ -299,12 +304,20 @@ export class ParsedCommand {
// Positional parameter.
this.position++;
// eslint-disable-next-line no-loop-func
const field = fields.find((f: AppField) => f.position === this.position);
let field = fields.find((f: AppField) => f.position === this.position);
if (!field) {
return this.asError(this.intl.formatMessage({
id: 'apps.error.parser.no_argument_pos_x',
defaultMessage: 'Unable to identify argument.',
}));
field = fields.find((f) => f.position === -1 && f.type === AppFieldTypes.TEXT);
if (!field || this.values[field.name]) {
return this.asError(this.intl.formatMessage({
id: 'apps.error.parser.no_argument_pos_x',
defaultMessage: 'Unable to identify argument.',
}));
}
this.incompleteStart = this.i;
this.incomplete = '';
this.field = field;
this.state = ParseState.Rest;
break;
}
this.field = field;
this.state = ParseState.StartValue;
@@ -314,6 +327,28 @@ export class ParsedCommand {
break;
}
case ParseState.Rest: {
if (!this.field) {
return this.asError(this.intl.formatMessage({
id: 'apps.error.parser.missing_field_value',
defaultMessage: 'Field value is missing.',
}));
}
if (autocompleteMode && c === '') {
return this;
}
if (c === '') {
this.values[this.field.name] = this.incomplete;
return this;
}
this.i++;
this.incomplete += c;
break;
}
case ParseState.ParameterSeparator: {
this.incompleteStart = this.i;
switch (c) {
@@ -470,7 +505,7 @@ export class ParsedCommand {
if (this.incompleteStart === this.i - 1) {
return this.asError(this.intl.formatMessage({
id: 'apps.error.parser.empty_value',
defaultMessage: 'empty values are not allowed',
defaultMessage: 'Empty values are not allowed.',
}));
}
this.i++;
@@ -510,7 +545,7 @@ export class ParsedCommand {
if (this.incompleteStart === this.i - 1) {
return this.asError(this.intl.formatMessage({
id: 'apps.error.parser.empty_value',
defaultMessage: 'empty values are not allowed',
defaultMessage: 'Empty values are not allowed.',
}));
}
this.i++;
@@ -544,13 +579,13 @@ export class ParsedCommand {
(!autocompleteMode && this.incomplete !== 'true' && this.incomplete !== 'false'))) {
// reset back where the value started, and treat as a new parameter
this.i = this.incompleteStart;
this.values![this.field.name] = 'true';
this.values[this.field.name] = 'true';
this.state = ParseState.StartParameter;
} else {
if (autocompleteMode && c === '') {
return this;
}
this.values![this.field.name] = this.incomplete;
this.values[this.field.name] = this.incomplete;
this.incomplete = '';
this.incompleteStart = this.i;
if (c === '') {
@@ -572,8 +607,6 @@ export class AppCommandParser {
private rootPostID?: string;
private intl: Intl;
forms: {[location: string]: AppForm} = {};
constructor(store: Store|null, intl: Intl, channelID: string, teamID = '', rootPostID = '') {
this.store = store || getStore() as Store;
this.channelID = channelID;
@@ -619,57 +652,59 @@ export class AppCommandParser {
}
private async addDefaultAndReadOnlyValues(parsed: ParsedCommand) {
await Promise.all(parsed.form?.fields?.map(async (f) => {
if (!parsed.form?.fields) {
return;
}
await Promise.all(parsed.form.fields.map(async (f) => {
if (!f.value) {
return;
}
if (!f.readonly || f.name in parsed.values) {
return;
}
switch (f.type) {
case AppFieldTypes.TEXT:
parsed.values[f.name] = f.value as string;
break;
case AppFieldTypes.BOOL:
parsed.values[f.name] = 'true';
break;
case AppFieldTypes.USER: {
const userID = (f.value as AppSelectOption).value;
let user = selectUser(this.store.getState(), userID);
if (!user) {
const dispatchResult = await this.store.dispatch(getUser(userID));
if ('error' in dispatchResult) {
// Silently fail on default value
break;
if (f.readonly || !(f.name in parsed.values)) {
switch (f.type) {
case AppFieldTypes.TEXT:
parsed.values[f.name] = f.value as string;
break;
case AppFieldTypes.BOOL:
parsed.values[f.name] = 'true';
break;
case AppFieldTypes.USER: {
const userID = (f.value as AppSelectOption).value;
let user = selectUser(this.store.getState(), userID);
if (!user) {
const dispatchResult = await this.store.dispatch(getUser(userID));
if ('error' in dispatchResult) {
// Silently fail on default value
break;
}
user = dispatchResult.data;
}
user = dispatchResult.data;
parsed.values[f.name] = user.username;
break;
}
parsed.values[f.name] = user.username;
break;
}
case AppFieldTypes.CHANNEL: {
const channelID = (f.value as AppSelectOption).label;
let channel = selectChannel(this.store.getState(), channelID);
if (!channel) {
const dispatchResult = await this.store.dispatch(getChannel(channelID));
if ('error' in dispatchResult) {
// Silently fail on default value
break;
case AppFieldTypes.CHANNEL: {
const channelID = (f.value as AppSelectOption).label;
let channel = selectChannel(this.store.getState(), channelID);
if (!channel) {
const dispatchResult = await this.store.dispatch(getChannel(channelID));
if ('error' in dispatchResult) {
// Silently fail on default value
break;
}
channel = dispatchResult.data;
}
channel = dispatchResult.data;
parsed.values[f.name] = channel.name;
break;
}
parsed.values[f.name] = channel.name;
break;
}
case AppFieldTypes.STATIC_SELECT:
case AppFieldTypes.DYNAMIC_SELECT:
parsed.values[f.name] = (f.value as AppSelectOption).value;
break;
case AppFieldTypes.MARKDOWN:
case AppFieldTypes.STATIC_SELECT:
case AppFieldTypes.DYNAMIC_SELECT:
parsed.values[f.name] = (f.value as AppSelectOption).value;
break;
case AppFieldTypes.MARKDOWN:
// Do nothing
// Do nothing
}
}
}) || []);
}
@@ -926,8 +961,11 @@ export class AppCommandParser {
// getCommandBindings returns the commands in the redux store.
// They are grouped by app id since each app has one base command
private getCommandBindings = (): AppBinding[] => {
const bindings = getCommandBindings(this.store.getState());
return bindings;
const state = this.store.getState();
if (this.rootPostID) {
return getRHSCommandBindings(state);
}
return getCommandBindings(state);
}
// getChannel gets the channel in which the user is typing the command
@@ -937,9 +975,6 @@ export class AppCommandParser {
}
public setChannelContext = (channelID: string, teamID = '', rootPostID?: string) => {
if (this.channelID !== channelID || this.rootPostID !== rootPostID || this.teamID !== teamID) {
this.forms = {};
}
this.channelID = channelID;
this.rootPostID = rootPostID;
this.teamID = teamID;
@@ -981,7 +1016,7 @@ export class AppCommandParser {
}
context.channel_id = channel.id;
context.team_id = this.teamID || channel.team_id || getCurrentTeamId(this.store.getState());
context.team_id = channel.team_id || getCurrentTeamId(this.store.getState());
return context;
}
@@ -989,7 +1024,10 @@ export class AppCommandParser {
// fetchForm unconditionaly retrieves the form for the given binding (subcommand)
private fetchForm = async (binding: AppBinding): Promise<{form?: AppForm; error?: string} | undefined> => {
if (!binding.call) {
return undefined;
return {error: this.intl.formatMessage({
id: 'apps.error.parser.missing_call',
defaultMessage: 'Missing binding call.',
})};
}
const payload = createCallRequest(
@@ -1033,28 +1071,25 @@ export class AppCommandParser {
public getForm = async (location: string, binding: AppBinding): Promise<{form?: AppForm; error?: string} | undefined> => {
const rootID = this.rootPostID || '';
const key = `${this.channelID}-${rootID}-${location}`;
const form = this.forms[key];
const form = this.rootPostID ? getAppRHSCommandForm(this.store.getState(), key) : getAppCommandForm(this.store.getState(), key);
if (form) {
return {form};
}
this.forms = {};
const fetched = await this.fetchForm(binding);
if (fetched?.form) {
this.forms[key] = fetched.form;
let actionType: string = AppsTypes.RECEIVED_APP_COMMAND_FORM;
if (this.rootPostID) {
actionType = AppsTypes.RECEIVED_APP_RHS_COMMAND_FORM;
}
this.store.dispatch({
data: {form: fetched.form, location: key},
type: actionType,
});
}
return fetched;
}
// displayError shows an error that was caught by the parser
private displayError = (err: any): void => {
let errStr = err as string;
if (err.message) {
errStr = err.message;
}
displayError(this.intl, errStr);
}
// getSuggestionsForSubCommands returns suggestions for a subcommand's name
private getCommandSuggestions = (parsed: ParsedCommand): AutocompleteSuggestion[] => {
if (!parsed.binding?.bindings?.length) {
@@ -1104,6 +1139,14 @@ export class AppCommandParser {
case ParseState.EndTickedValue:
case ParseState.TickValue:
return this.getValueSuggestions(parsed, '`');
case ParseState.Rest: {
const execute = getExecuteSuggestion(parsed);
const value = await this.getValueSuggestions(parsed);
if (execute) {
return [execute, ...value];
}
return value;
}
}
return [];
}
@@ -1332,11 +1375,11 @@ export class AppCommandParser {
});
return [{
Complete: '',
Suggestion: this.intl.formatMessage({
Suggestion: '',
Hint: this.intl.formatMessage({
id: 'apps.suggestion.dynamic.error',
defaultMessage: 'Dynamic select error',
}),
Hint: '',
IconData: COMMAND_SUGGESTION_ERROR,
Description: errMsg,
}];

View File

@@ -2,7 +2,6 @@
// See LICENSE.txt for license information.
import {intlShape} from 'react-intl';
import {Alert} from 'react-native';
import {getUserByUsername, getUser, autocompleteUsers} from '@mm-redux/actions/users';
import {getCurrentTeamId, getCurrentTeam} from '@mm-redux/selectors/entities/teams';
@@ -37,6 +36,8 @@ export type {
DoAppCallResult,
} from 'types/actions/apps';
export {AppsTypes} from '@mm-redux/action_types';
export type {AutocompleteSuggestion};
export type {
@@ -65,7 +66,7 @@ export {
COMMAND_SUGGESTION_USER,
} from '@mm-redux/constants/apps';
export {makeAppBindingsSelector} from '@mm-redux/selectors/entities/apps';
export {makeAppBindingsSelector, makeRHSAppBindingSelector, getAppCommandForm, getAppRHSCommandForm} from '@mm-redux/selectors/entities/apps';
export {getPost} from '@mm-redux/selectors/entities/posts';
export {getChannel as selectChannel, getCurrentChannel, getChannelByName as selectChannelByName} from '@mm-redux/selectors/entities/channels';
@@ -112,14 +113,6 @@ export const getExecuteSuggestion = (_: ParsedCommand): AutocompleteSuggestion |
return null;
};
export const displayError = (intl: typeof intlShape, body: string) => {
const title = intl.formatMessage({
id: 'mobile.general.error.title',
defaultMessage: 'Error',
});
Alert.alert(title, body);
};
export const errorMessage = (intl: typeof intlShape, error: string, _command: string, _position: number): string => { // eslint-disable-line @typescript-eslint/no-unused-vars
return intl.formatMessage({
id: 'apps.error.parser',

View File

@@ -181,6 +181,37 @@ export const createCommand: AppBinding = {
} as AppForm,
};
export const restCommand: AppBinding = {
app_id: 'jira',
label: 'rest',
location: 'rest',
description: 'rest description',
icon: 'rest icon',
hint: 'rest hint',
form: {
call: {
path: '/create-issue',
},
fields: [
{
name: 'summary',
label: 'summary',
description: 'The Jira issue summary',
type: AppFieldTypes.TEXT,
hint: 'The thing is working great!',
position: -1,
},
{
name: 'verbose',
label: 'verbose',
description: 'display details',
type: AppFieldTypes.BOOL,
hint: 'yes or no!',
},
],
} as AppForm,
};
export const testBindings: AppBinding[] = [
{
app_id: '',
@@ -204,6 +235,7 @@ export const testBindings: AppBinding[] = [
bindings: [
viewCommand,
createCommand,
restCommand,
],
}],
},

View File

@@ -16,7 +16,6 @@ exports[`components/autocomplete/app_slash_suggestion should match snapshot 1`]
keyExtractor={[Function]}
keyboardShouldPersistTaps="always"
nestedScrollEnabled={false}
numColumns={1}
removeClippedSubviews={true}
renderItem={[Function]}
style={

View File

@@ -27,7 +27,12 @@ const makeStore = async (bindings: AppBinding[]) => {
...reduxTestState,
entities: {
...reduxTestState.entities,
apps: {bindings},
apps: {
bindings,
bindingsForms: {},
threadBindings: bindings,
threadBindingsForms: {},
},
},
} as any;
const testStore = await mockStore(initialState);

View File

@@ -155,13 +155,15 @@ const SlashSuggestionItem = (props: Props) => {
</View>
<View style={style.suggestionContainer}>
<Text style={style.suggestionName}>{`${suggestionText}`}</Text>
<Text
ellipsizeMode='tail'
numberOfLines={1}
style={style.suggestionDescription}
>
{description}
</Text>
{Boolean(description) &&
<Text
ellipsizeMode='tail'
numberOfLines={1}
style={style.suggestionDescription}
>
{description}
</Text>
}
</View>
</View>
</TouchableWithFeedback>

View File

@@ -48,7 +48,7 @@ export default class SpecialMentionItem extends PureComponent {
<View style={style.row}>
<View style={style.rowPicture}>
<CompassIcon
name='account-group-outline'
name='account-multiple-outline'
style={style.rowIcon}
/>
</View>

View File

@@ -1,10 +1,9 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import PropTypes from 'prop-types';
import React, {PureComponent} from 'react';
import {intlShape} from 'react-intl';
import {Text, View} from 'react-native';
import {Text, View, Platform} from 'react-native';
import {goToScreen} from '@actions/navigation';
import CompassIcon from '@components/compass_icon';
@@ -12,33 +11,46 @@ import FormattedText from '@components/formatted_text';
import Markdown from '@components/markdown';
import TouchableWithFeedback from '@components/touchable_with_feedback';
import {ViewTypes} from '@constants';
import {ActionResult} from '@mm-redux/types/actions';
import {Channel} from '@mm-redux/types/channels';
import {DialogOption} from '@mm-redux/types/integrations';
import {Theme} from '@mm-redux/types/theme';
import {UserProfile} from '@mm-redux/types/users';
import {displayUsername} from '@mm-redux/utils/user_utils';
import {getMarkdownBlockStyles, getMarkdownTextStyles} from '@utils/markdown';
import {preventDoubleTap} from '@utils/tap';
import {makeStyleSheetFromTheme, changeOpacity} from '@utils/theme';
export default class AutocompleteSelector extends PureComponent {
static propTypes = {
actions: PropTypes.shape({
setAutocompleteSelector: PropTypes.func.isRequired,
}).isRequired,
getDynamicOptions: PropTypes.func,
label: PropTypes.string,
placeholder: PropTypes.string.isRequired,
dataSource: PropTypes.string,
options: PropTypes.arrayOf(PropTypes.object),
selected: PropTypes.object,
optional: PropTypes.bool,
showRequiredAsterisk: PropTypes.bool,
teammateNameDisplay: PropTypes.string,
theme: PropTypes.object.isRequired,
onSelected: PropTypes.func,
helpText: PropTypes.node,
errorText: PropTypes.node,
roundedBorders: PropTypes.bool,
disabled: PropTypes.bool,
};
type Selection = DialogOption | Channel | UserProfile | DialogOption[] | Channel[] | UserProfile[];
type Props = {
actions: {
setAutocompleteSelector: (dataSource: any, onSelect: any, options: any, getDynamicOptions: any) => Promise<ActionResult>;
};
getDynamicOptions?: (term: string) => Promise<ActionResult>;
label?: string;
placeholder?: string;
dataSource?: string;
options?: DialogOption[];
selected?: DialogOption | DialogOption[];
optional?: boolean;
showRequiredAsterisk?: boolean;
teammateNameDisplay?: string;
theme: Theme;
onSelected?: ((item: DialogOption) => void) | ((item: DialogOption[]) => void);
helpText?: string;
errorText?: string;
roundedBorders?: boolean;
disabled?: boolean;
isMultiselect?: boolean;
}
type State = {
selectedText: string;
selected?: DialogOption | DialogOption[];
}
export default class AutocompleteSelector extends PureComponent<Props, State> {
static contextTypes = {
intl: intlShape,
};
@@ -49,26 +61,45 @@ export default class AutocompleteSelector extends PureComponent {
roundedBorders: true,
};
constructor(props) {
constructor(props: Props) {
super(props);
this.state = {
selectedText: null,
selectedText: '',
};
}
static getDerivedStateFromProps(props, state) {
if (props.selected && props.selected !== state.selected) {
static getDerivedStateFromProps(props: Props, state: State) {
if (!props.selected || props.selected === state.selected) {
return null;
}
if (!props.isMultiselect) {
return {
selectedText: props.selected.text,
selectedText: (props.selected as DialogOption).text,
selected: props.selected,
};
}
return null;
const options = props.selected as DialogOption[];
let selectedText = '';
const selected: DialogOption[] = [];
options.forEach((option) => {
if (selectedText !== '') {
selectedText += ', ';
}
selectedText += option.text;
selected.push(option);
});
return {
selectedText,
selected,
};
}
handleSelect = (selected) => {
handleSelect = (selected: Selection) => {
if (!selected) {
return;
}
@@ -78,34 +109,113 @@ export default class AutocompleteSelector extends PureComponent {
teammateNameDisplay,
} = this.props;
let selectedText;
let selectedValue;
if (dataSource === ViewTypes.DATA_SOURCE_USERS) {
selectedText = displayUsername(selected, teammateNameDisplay);
selectedValue = selected.id;
} else if (dataSource === ViewTypes.DATA_SOURCE_CHANNELS) {
selectedText = selected.display_name;
selectedValue = selected.id;
} else {
selectedText = selected.text;
selectedValue = selected.value;
if (!this.props.isMultiselect) {
let selectedText: string;
let selectedValue: string;
switch (dataSource) {
case ViewTypes.DATA_SOURCE_USERS: {
const typedSelected = selected as UserProfile;
selectedText = displayUsername(typedSelected, teammateNameDisplay || '');
selectedValue = typedSelected.id;
break;
}
case ViewTypes.DATA_SOURCE_CHANNELS: {
const typedSelected = selected as Channel;
selectedText = typedSelected.display_name;
selectedValue = typedSelected.id;
break;
}
default: {
const typedSelected = selected as DialogOption;
selectedText = typedSelected.text;
selectedValue = typedSelected.value;
}
}
this.setState({selectedText});
if (this.props.onSelected) {
(this.props.onSelected as (opt: DialogOption) => void)({text: selectedText, value: selectedValue});
}
return;
}
let selectedText = '';
const selectedOptions: DialogOption[] = [];
switch (dataSource) {
case ViewTypes.DATA_SOURCE_USERS: {
const typedSelected = selected as UserProfile[];
typedSelected.forEach((option) => {
if (selectedText !== '') {
selectedText += ', ';
}
const text = displayUsername(option, teammateNameDisplay || '');
selectedText += text;
selectedOptions.push({text, value: option.id});
});
break;
}
case ViewTypes.DATA_SOURCE_CHANNELS: {
const typedSelected = selected as Channel[];
typedSelected.forEach((option) => {
if (selectedText !== '') {
selectedText += ', ';
}
const text = option.display_name;
selectedText += text;
selectedOptions.push({text, value: option.id});
});
break;
}
default: {
const typedSelected = selected as DialogOption[];
typedSelected.forEach((option) => {
if (selectedText !== '') {
selectedText += ', ';
}
selectedText += option.text;
selectedOptions.push(option);
});
break;
}
}
this.setState({selectedText});
if (this.props.onSelected) {
this.props.onSelected({text: selectedText, value: selectedValue});
(this.props.onSelected as (opt: DialogOption[]) => void)(selectedOptions);
}
};
goToSelectorScreen = preventDoubleTap(() => {
goToSelectorScreen = preventDoubleTap(async () => {
const closeButton = await CompassIcon.getImageSource(Platform.select({ios: 'arrow-back-ios', default: 'arrow-left'}), 24, this.props.theme.sidebarHeaderTextColor);
const {formatMessage} = this.context.intl;
const {actions, dataSource, options, placeholder, getDynamicOptions} = this.props;
const {actions, dataSource, options, placeholder, getDynamicOptions, theme} = this.props;
const screen = 'SelectorScreen';
const title = placeholder || formatMessage({id: 'mobile.action_menu.select', defaultMessage: 'Select an option'});
const buttonName = formatMessage({id: 'mobile.forms.select.done', defaultMessage: 'Done'});
actions.setAutocompleteSelector(dataSource, this.handleSelect, options, getDynamicOptions);
goToScreen(screen, title);
let screenOptions = {};
if (this.props.isMultiselect) {
screenOptions = {
topBar: {
leftButtons: [{
id: 'close-dialog',
icon: closeButton,
}],
rightButtons: [{
id: 'submit-form',
showAsAction: 'always',
text: buttonName,
}],
leftButtonColor: theme.sidebarHeaderTextColor,
rightButtonColor: theme.sidebarHeaderTextColor,
},
};
}
goToScreen(screen, title, {isMultiselect: this.props.isMultiselect, selected: this.state.selected}, screenOptions);
});
render() {
@@ -126,6 +236,8 @@ export default class AutocompleteSelector extends PureComponent {
const textStyles = getMarkdownTextStyles(theme);
const blockStyles = getMarkdownBlockStyles(theme);
const chevron = Platform.select({ios: 'chevron-right', default: 'chevron-down'});
let text = placeholder || intl.formatMessage({id: 'mobile.action_menu.select', defaultMessage: 'Select an option'});
let selectedStyle = style.dropdownPlaceholder;
@@ -215,8 +327,8 @@ export default class AutocompleteSelector extends PureComponent {
{text}
</Text>
<CompassIcon
name='chevron-down'
color={changeOpacity(theme.centerChannelColor, 0.5)}
name={chevron}
color={changeOpacity(theme.centerChannelColor, 0.32)}
style={style.icon}
/>
</View>
@@ -228,7 +340,7 @@ export default class AutocompleteSelector extends PureComponent {
}
}
const getStyleSheet = makeStyleSheetFromTheme((theme) => {
const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => {
const input = {
borderWidth: 1,
borderColor: changeOpacity(theme.centerChannelColor, 0.1),
@@ -263,8 +375,9 @@ const getStyleSheet = makeStyleSheetFromTheme((theme) => {
},
icon: {
position: 'absolute',
top: 13,
top: 6,
right: 12,
fontSize: 28,
},
labelContainer: {
flexDirection: 'row',

View File

@@ -2,23 +2,29 @@
// See LICENSE.txt for license information.
import {connect} from 'react-redux';
import {bindActionCreators} from 'redux';
import {ActionCreatorsMapObject, bindActionCreators, Dispatch} from 'redux';
import {setAutocompleteSelector} from '@actions/views/post';
import {getTeammateNameDisplaySetting, getTheme} from '@mm-redux/selectors/entities/preferences';
import AutocompleteSelector from './autocomplete_selector';
function mapStateToProps(state) {
import type {Action, ActionResult, GenericAction} from '@mm-redux/types/actions';
import type {GlobalState} from '@mm-redux/types/store';
function mapStateToProps(state: GlobalState) {
return {
teammateNameDisplay: getTeammateNameDisplaySetting(state),
theme: getTheme(state),
};
}
function mapDispatchToProps(dispatch) {
type Actions = {
setAutocompleteSelector: (dataSource: any, onSelect: any, options: any, getDynamicOptions: any) => Promise<ActionResult>;
}
function mapDispatchToProps(dispatch: Dispatch<GenericAction>) {
return {
actions: bindActionCreators({
actions: bindActionCreators<ActionCreatorsMapObject<Action>, Actions>({
setAutocompleteSelector,
}, dispatch),
};

View File

@@ -86,6 +86,7 @@ export interface AvatarsProps {
breakAt?: number;
style?: StyleProp<ViewStyle>;
theme: Theme;
listTitle?: JSX.Element;
}
export default class Avatars extends PureComponent<AvatarsProps> {
@@ -94,11 +95,12 @@ export default class Avatars extends PureComponent<AvatarsProps> {
};
showParticipantsList = () => {
const {userIds} = this.props;
const {userIds, listTitle} = this.props;
const screen = 'ParticipantsList';
const passProps = {
userIds,
listTitle,
};
showModalOverCurrentContext(screen, passProps);

View File

@@ -25,7 +25,6 @@ exports[`CustomList should match snapshot with FlatList 1`] = `
keyboardDismissMode="on-drag"
keyboardShouldPersistTaps="always"
maxToRenderPerBatch={16}
numColumns={1}
onLayout={[Function]}
onScroll={[Function]}
removeClippedSubviews={true}

View File

@@ -52,33 +52,35 @@ export default class ChannelListRow extends React.PureComponent {
}
return (
<CustomListRow
id={this.props.id}
onPress={this.props.onPress ? this.onPress : null}
enabled={this.props.enabled}
selectable={this.props.selectable}
selected={this.props.selected}
testID={testID}
>
<View
style={style.container}
testID={itemTestID}
<View style={style.outerContainer}>
<CustomListRow
id={this.props.id}
onPress={this.props.onPress ? this.onPress : null}
enabled={this.props.enabled}
selectable={this.props.selectable}
selected={this.props.selected}
testID={testID}
>
<View style={style.titleContainer}>
<CompassIcon
name={icon}
style={style.icon}
/>
<Text
style={style.displayName}
testID={channelDisplayNameTestID}
>
{this.props.channel.display_name}
</Text>
<View
style={style.container}
testID={itemTestID}
>
<View style={style.titleContainer}>
<CompassIcon
name={icon}
style={style.icon}
/>
<Text
style={style.displayName}
testID={channelDisplayNameTestID}
>
{this.props.channel.display_name}
</Text>
</View>
{purpose}
</View>
{purpose}
</View>
</CustomListRow>
</CustomListRow>
</View>
);
}
}
@@ -101,7 +103,12 @@ const getStyleFromTheme = makeStyleSheetFromTheme((theme) => {
container: {
flex: 1,
flexDirection: 'column',
},
outerContainer: {
flex: 1,
flexDirection: 'row',
paddingHorizontal: 15,
overflow: 'hidden',
},
purpose: {
marginTop: 7,

View File

@@ -42,21 +42,23 @@ export default class OptionListRow extends React.PureComponent {
const style = getStyleFromTheme(theme);
return (
<CustomListRow
id={value}
onPress={this.onPress}
enabled={enabled}
selectable={selectable}
selected={selected}
>
<View style={style.textContainer}>
<View>
<Text style={style.optionText}>
{text}
</Text>
<View style={style.container}>
<CustomListRow
id={value}
onPress={this.onPress}
enabled={enabled}
selectable={selectable}
selected={selected}
>
<View style={style.textContainer}>
<View>
<Text style={style.optionText}>
{text}
</Text>
</View>
</View>
</View>
</CustomListRow>
</CustomListRow>
</View>
);
}
}

View File

@@ -6,8 +6,8 @@ exports[`UserListRow should match snapshot 1`] = `
Object {
"flex": 1,
"flexDirection": "row",
"marginHorizontal": 10,
"overflow": "hidden",
"paddingHorizontal": 15,
}
}
>
@@ -142,8 +142,8 @@ exports[`UserListRow should match snapshot for currentUser with (you) populated
Object {
"flex": 1,
"flexDirection": "row",
"marginHorizontal": 10,
"overflow": "hidden",
"paddingHorizontal": 15,
}
}
>
@@ -278,8 +278,8 @@ exports[`UserListRow should match snapshot for deactivated user 1`] = `
Object {
"flex": 1,
"flexDirection": "row",
"marginHorizontal": 10,
"overflow": "hidden",
"paddingHorizontal": 15,
}
}
>
@@ -427,8 +427,8 @@ exports[`UserListRow should match snapshot for guest user 1`] = `
Object {
"flex": 1,
"flexDirection": "row",
"marginHorizontal": 10,
"overflow": "hidden",
"paddingHorizontal": 15,
}
}
>
@@ -563,8 +563,8 @@ exports[`UserListRow should match snapshot for remote user 1`] = `
Object {
"flex": 1,
"flexDirection": "row",
"marginHorizontal": 10,
"overflow": "hidden",
"paddingHorizontal": 15,
}
}
>

View File

@@ -165,7 +165,7 @@ const getStyleFromTheme = makeStyleSheetFromTheme((theme) => {
container: {
flex: 1,
flexDirection: 'row',
marginHorizontal: 10,
paddingHorizontal: 15,
overflow: 'hidden',
},
profileContainer: {

View File

@@ -279,6 +279,7 @@ export default class EditChannelInfo extends PureComponent {
onPress={() => {
this.onTypeSelect(General.OPEN_CHANNEL);
}}
testID='edit_channel_info.type.public.action'
>
<FormattedText
style={style.touchableText}
@@ -305,6 +306,7 @@ export default class EditChannelInfo extends PureComponent {
onPress={() => {
this.onTypeSelect(General.PRIVATE_CHANNEL);
}}
testID='edit_channel_info.type.private.action'
>
<FormattedText
style={style.touchableText}

View File

@@ -0,0 +1,62 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {shallow} from 'enzyme';
import moment from 'moment';
import React from 'react';
import FormattedRelativeTime from './formatted_relative_time';
jest.mock('react', () => ({
...jest.requireActual('react'),
useEffect: (f) => f(),
}));
describe('FormattedRelativeTime', () => {
const baseProps = {
value: moment.now() - 15000,
updateIntervalInSeconds: 10000,
};
test('should match snapshot', () => {
const wrapper = shallow(<FormattedRelativeTime {...baseProps}/>);
expect(wrapper.getElement()).toMatchSnapshot();
});
test('should match string in the past', () => {
const props = {...baseProps, value: moment.now() - ((10 * 60 * 60 * 1000) + (30 * 60 * 1000) + (25 * 1000) + 500)};
const wrapper = shallow(<FormattedRelativeTime {...props}/>);
expect(wrapper.getElement().props.children).toBe('11 hours ago');
});
test('should match string in the future', () => {
const props = {...baseProps, value: moment.now() + 15500};
const wrapper = shallow(<FormattedRelativeTime {...props}/>);
expect(wrapper.getElement().props.children).toBe('in a few seconds');
});
test('should re-render after updateIntervalInSeconds', () => {
jest.useFakeTimers();
const props = {...baseProps, value: moment.now(), updateIntervalInSeconds: 120};
const wrapper = shallow(<FormattedRelativeTime {...props}/>);
expect(wrapper.getElement().props.children).toBe('a few seconds ago');
jest.advanceTimersByTime(60000);
expect(wrapper.getElement().props.children).toBe('a few seconds ago');
jest.advanceTimersByTime(120000);
expect(wrapper.getElement().props.children).toBe('2 minutes ago');
jest.useRealTimers();
});
test('should not re-render if updateIntervalInSeconds is not passed', () => {
jest.useFakeTimers();
const props = {value: baseProps.value};
const wrapper = shallow(<FormattedRelativeTime {...props}/>);
expect(wrapper.getElement().props.children).toBe('a few seconds ago');
jest.advanceTimersByTime(120000000000);
expect(wrapper.getElement().props.children).toBe('a few seconds ago');
jest.useRealTimers();
});
});

View File

@@ -0,0 +1,44 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import moment from 'moment-timezone';
import React, {useEffect, useState} from 'react';
import {Text, TextProps} from 'react-native';
import type {UserTimezone} from '@mm-redux/types/users';
type FormattedRelativeTimeProps = TextProps & {
timezone?: UserTimezone | string;
value: number | string | Date;
updateIntervalInSeconds?: number;
}
const FormattedRelativeTime = ({timezone, value, updateIntervalInSeconds, ...props}: FormattedRelativeTimeProps) => {
const getFormattedRelativeTime = () => {
let zone = timezone;
if (typeof timezone === 'object') {
zone = timezone.useAutomaticTimezone ? timezone.automaticTimezone : timezone.manualTimezone;
}
return timezone ? moment.tz(value, zone as string).fromNow() : moment(value).fromNow();
};
const [formattedTime, setFormattedTime] = useState(getFormattedRelativeTime());
useEffect(() => {
if (updateIntervalInSeconds) {
const interval = setInterval(() => setFormattedTime(getFormattedRelativeTime()), updateIntervalInSeconds * 1000);
return () => {
clearInterval(interval);
};
}
return () => null;
}, [updateIntervalInSeconds]);
return (
<Text {...props}>
{formattedTime}
</Text>
);
};
export default FormattedRelativeTime;

View File

@@ -5,9 +5,14 @@ import React from 'react';
import {injectIntl, intlShape} from 'react-intl';
import {Alert, FlatList} from 'react-native';
import {goToScreen} from '@actions/navigation';
import {THREAD} from '@constants/screen';
import EventEmitter from '@mm-redux/utils/event_emitter';
import ThreadList from './thread_list';
import type {ActionResult} from '@mm-redux/types/actions';
import type {Post} from '@mm-redux/types/posts';
import type {Team} from '@mm-redux/types/teams';
import type {Theme} from '@mm-redux/types/theme';
import type {ThreadsState, UserThread} from '@mm-redux/types/threads';
@@ -16,10 +21,12 @@ import type {$ID} from '@mm-redux/types/utilities';
type Props = {
actions: {
getPostThread: (postId: string) => void;
getThreads: (userId: $ID<UserProfile>, teamId: $ID<Team>, before?: $ID<UserThread>, after?: $ID<UserThread>, perPage?: number, deleted?: boolean, unread?: boolean) => Promise<ActionResult>;
handleViewingGlobalThreadsAll: () => void;
handleViewingGlobalThreadsUnreads: () => void;
markAllThreadsInTeamRead: (userId: $ID<UserProfile>, teamId: $ID<Team>) => void;
selectPost: (postId: string) => void;
};
allThreadIds: $ID<UserThread>[];
intl: typeof intlShape;
@@ -38,6 +45,7 @@ function GlobalThreadsList({actions, allThreadIds, intl, teamId, theme, threadCo
const listRef = React.useRef<FlatList>(null);
const [isLoading, setIsLoading] = React.useState<boolean>(true);
const [isRefreshing, setIsRefreshing] = React.useState<boolean>(false);
const scrollToTop = () => {
listRef.current?.scrollToOffset({offset: 0});
@@ -79,6 +87,16 @@ function GlobalThreadsList({actions, allThreadIds, intl, teamId, theme, threadCo
}
};
const onRefresh = async () => {
if (!isLoading) {
if (!isRefreshing) {
setIsRefreshing(true);
}
await loadThreads('', '', viewingUnreads);
setIsRefreshing(false);
}
};
const markAllAsRead = () => {
Alert.alert(
intl.formatMessage({
@@ -108,13 +126,32 @@ function GlobalThreadsList({actions, allThreadIds, intl, teamId, theme, threadCo
);
};
const goToThread = React.useCallback((post: Post) => {
actions.getPostThread(post.id);
actions.selectPost(post.id);
const passProps = {
channelId: post.channel_id,
rootId: post.id,
};
goToScreen(THREAD, '', passProps);
}, []);
React.useEffect(() => {
EventEmitter.on('goToThread', goToThread);
return () => {
EventEmitter.off('goToThread', goToThread);
};
}, []);
return (
<ThreadList
haveUnreads={haveUnreads}
isLoading={isLoading}
isRefreshing={isRefreshing}
listRef={listRef}
loadMoreThreads={loadMoreThreads}
markAllAsRead={markAllAsRead}
onRefresh={onRefresh}
testID={'global_threads'}
theme={theme}
threadIds={ids}

View File

@@ -4,7 +4,9 @@
import {connect} from 'react-redux';
import {bindActionCreators, Dispatch} from 'redux';
import {getPostThread} from '@actions/views/post';
import {handleViewingGlobalThreadsAll, handleViewingGlobalThreadsUnreads} from '@actions/views/threads';
import {selectPost} from '@mm-redux/actions/posts';
import {getThreads, markAllThreadsInTeamRead} from '@mm-redux/actions/threads';
import {getCurrentUserId} from '@mm-redux/selectors/entities/common';
import {getTheme} from '@mm-redux/selectors/entities/preferences';
@@ -32,10 +34,12 @@ function mapStateToProps(state: GlobalState) {
function mapDispatchToProps(dispatch: Dispatch) {
return {
actions: bindActionCreators({
getPostThread,
getThreads,
handleViewingGlobalThreadsAll,
handleViewingGlobalThreadsUnreads,
markAllThreadsInTeamRead,
selectPost,
}, dispatch),
};
}

View File

@@ -2,6 +2,7 @@
exports[`Global Thread Item Should render thread item with unread messages dot 1`] = `
<TouchableHighlight
onLongPress={[Function]}
onPress={[Function]}
testID="thread_item.post1.item"
underlayColor="rgba(28,88,217,0.08)"
@@ -185,6 +186,7 @@ exports[`Global Thread Item Should render thread item with unread messages dot 1
exports[`Global Thread Item Should show unread mentions count 1`] = `
<TouchableHighlight
onLongPress={[Function]}
onPress={[Function]}
testID="thread_item.post1.item"
underlayColor="rgba(28,88,217,0.08)"

View File

@@ -4,8 +4,7 @@
import {connect} from 'react-redux';
import {bindActionCreators, Dispatch} from 'redux';
import {getPost, getPostThread} from '@actions/views/post';
import {selectPost} from '@mm-redux/actions/posts';
import {getPost} from '@actions/views/post';
import {getChannel} from '@mm-redux/selectors/entities/channels';
import {getPost as getPostSelector} from '@mm-redux/selectors/entities/posts';
import {getThread} from '@mm-redux/selectors/entities/threads';
@@ -30,8 +29,6 @@ function mapDispatchToProps(dispatch: Dispatch) {
return {
actions: bindActionCreators({
getPost,
getPostThread,
selectPost,
}, dispatch),
};
}

View File

@@ -5,13 +5,12 @@ import {shallow} from 'enzyme';
import React from 'react';
import {Text} from 'react-native';
import * as navigationActions from '@actions/navigation';
import {THREAD} from '@constants/screen';
import {Preferences} from '@mm-redux/constants';
import {Channel} from '@mm-redux/types/channels';
import {Post} from '@mm-redux/types/posts';
import {UserThread} from '@mm-redux/types/threads';
import {UserProfile} from '@mm-redux/types/users';
import EventEmitter from '@mm-redux/utils/event_emitter';
import {intl} from '@test/intl-test-helper';
import {ThreadItem} from './thread_item';
@@ -99,7 +98,7 @@ describe('Global Thread Item', () => {
});
test('Should goto threads when pressed on thread item', () => {
const goToScreen = jest.spyOn(navigationActions, 'goToScreen');
EventEmitter.emit = jest.fn();
const wrapper = shallow(
<ThreadItem
{...baseProps}
@@ -108,6 +107,6 @@ describe('Global Thread Item', () => {
const threadItem = wrapper.find({testID: `${testIDPrefix}.item`});
expect(threadItem.exists()).toBeTruthy();
threadItem.simulate('press');
expect(goToScreen).toHaveBeenCalledWith(THREAD, expect.anything(), expect.anything());
expect(EventEmitter.emit).toHaveBeenCalledWith('goToThread', expect.anything());
});
});

View File

@@ -3,29 +3,28 @@
import React from 'react';
import {injectIntl, intlShape} from 'react-intl';
import {View, Text, TouchableHighlight} from 'react-native';
import {Keyboard, Text, TouchableHighlight, View} from 'react-native';
import {goToScreen} from '@actions/navigation';
import {showModalOverCurrentContext} from '@actions/navigation';
import FriendlyDate from '@components/friendly_date';
import RemoveMarkdown from '@components/remove_markdown';
import {GLOBAL_THREADS, THREAD} from '@constants/screen';
import {GLOBAL_THREADS} from '@constants/screen';
import {Posts, Preferences} from '@mm-redux/constants';
import {Channel} from '@mm-redux/types/channels';
import {Post} from '@mm-redux/types/posts';
import {UserThread} from '@mm-redux/types/threads';
import {UserProfile} from '@mm-redux/types/users';
import EventEmitter from '@mm-redux/utils/event_emitter';
import {displayUsername} from '@mm-redux/utils/user_utils';
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
import ThreadFooter from '../thread_footer';
import type {Channel} from '@mm-redux/types/channels';
import type {Post} from '@mm-redux/types/posts';
import type {Theme} from '@mm-redux/types/theme';
import type {UserThread} from '@mm-redux/types/threads';
import type {UserProfile} from '@mm-redux/types/users';
export type DispatchProps = {
actions: {
getPost: (postId: string) => void;
getPostThread: (postId: string) => void;
selectPost: (postId: string) => void;
};
}
@@ -66,13 +65,18 @@ function ThreadItem({actions, channel, intl, post, threadId, testID, theme, thre
const threadStarterName = displayUsername(threadStarter, Preferences.DISPLAY_PREFER_FULL_NAME);
const showThread = () => {
actions.getPostThread(postItem.id);
actions.selectPost(postItem.id);
EventEmitter.emit('goToThread', postItem);
};
const showThreadOptions = () => {
const screen = 'GlobalThreadOptions';
const passProps = {
channelId: postItem.channel_id,
rootId: postItem.id,
rootId: post.id,
};
goToScreen(THREAD, '', passProps);
Keyboard.dismiss();
requestAnimationFrame(() => {
showModalOverCurrentContext(screen, passProps);
});
};
const testIDPrefix = `${testID}.${postItem?.id}`;
@@ -134,6 +138,7 @@ function ThreadItem({actions, channel, intl, post, threadId, testID, theme, thre
return (
<TouchableHighlight
underlayColor={changeOpacity(theme.buttonBg, 0.08)}
onLongPress={showThreadOptions}
onPress={showThread}
testID={`${testIDPrefix}.item`}
>

View File

@@ -86,62 +86,98 @@ exports[`Global Thread List Should render threads with functional tabs & mark al
viewUnreadThreads={[MockFunction]}
viewingUnreads={true}
/>
<FlatList
ListEmptyComponent={
<EmptyState
intl={
Object {
"defaultFormats": Object {},
"defaultLocale": "en",
"formatDate": [Function],
"formatHTMLMessage": [Function],
"formatMessage": [Function],
"formatNumber": [Function],
"formatPlural": [Function],
"formatRelative": [Function],
"formatTime": [Function],
"formats": Object {},
"formatters": Object {
"getDateTimeFormat": [Function],
"getMessageFormat": [Function],
"getNumberFormat": [Function],
"getPluralFormat": [Function],
"getRelativeFormat": [Function],
},
"locale": "en",
"messages": Object {},
"now": [Function],
"onError": [Function],
"textComponent": "span",
"timeZone": null,
<PostListRefreshControl
enabled={true}
isInverted={false}
onRefresh={[MockFunction]}
refreshing={false}
theme={
Object {
"awayIndicator": "#ffbc1f",
"buttonBg": "#1c58d9",
"buttonColor": "#ffffff",
"centerChannelBg": "#ffffff",
"centerChannelColor": "#3f4350",
"codeTheme": "github",
"dndIndicator": "#d24b4e",
"errorTextColor": "#d24b4e",
"linkColor": "#386fe5",
"mentionBg": "#ffffff",
"mentionColor": "#1e325c",
"mentionHighlightBg": "#ffd470",
"mentionHighlightLink": "#1b1d22",
"newMessageSeparator": "#cc8f00",
"onlineIndicator": "#3db887",
"sidebarBg": "#1e325c",
"sidebarHeaderBg": "#192a4d",
"sidebarHeaderTextColor": "#ffffff",
"sidebarTeamBarBg": "#14213e",
"sidebarText": "#ffffff",
"sidebarTextActiveBorder": "#5d89ea",
"sidebarTextActiveColor": "#ffffff",
"sidebarTextHoverBg": "#28427b",
"sidebarUnreadText": "#ffffff",
"type": "Denim",
}
}
>
<FlatList
ListEmptyComponent={
<EmptyState
intl={
Object {
"defaultFormats": Object {},
"defaultLocale": "en",
"formatDate": [Function],
"formatHTMLMessage": [Function],
"formatMessage": [Function],
"formatNumber": [Function],
"formatPlural": [Function],
"formatRelative": [Function],
"formatTime": [Function],
"formats": Object {},
"formatters": Object {
"getDateTimeFormat": [Function],
"getMessageFormat": [Function],
"getNumberFormat": [Function],
"getPluralFormat": [Function],
"getRelativeFormat": [Function],
},
"locale": "en",
"messages": Object {},
"now": [Function],
"onError": [Function],
"textComponent": "span",
"timeZone": null,
}
}
isUnreads={true}
/>
}
ListFooterComponent={null}
contentContainerStyle={
Object {
"flexGrow": 1,
}
isUnreads={true}
/>
}
ListFooterComponent={null}
contentContainerStyle={
Object {
"flexGrow": 1,
}
}
data={
Array [
"thread1",
]
}
initialNumToRender={10}
keyExtractor={[Function]}
numColumns={1}
onEndReached={[Function]}
onEndReachedThreshold={2}
removeClippedSubviews={true}
renderItem={[Function]}
scrollIndicatorInsets={
Object {
"right": 1,
data={
Array [
"thread1",
]
}
}
/>
initialNumToRender={10}
keyExtractor={[Function]}
onEndReached={[Function]}
onEndReachedThreshold={2}
onScroll={[Function]}
removeClippedSubviews={true}
renderItem={[Function]}
scrollIndicatorInsets={
Object {
"right": 1,
}
}
/>
</PostListRefreshControl>
</View>
`;

View File

@@ -25,9 +25,11 @@ describe('Global Thread List', () => {
haveUnreads: true,
intl,
isLoading: false,
isRefreshing: false,
listRef: React.useRef<FlatList>(null),
loadMoreThreads: jest.fn(),
markAllAsRead,
onRefresh: jest.fn(),
testID,
theme: Preferences.THEMES.denim,
threadIds: ['thread1'],

View File

@@ -2,12 +2,13 @@
// See LICENSE.txt for license information.
import React from 'react';
import {injectIntl, intlShape} from 'react-intl';
import {FlatList, Platform, View} from 'react-native';
import {FlatList, NativeSyntheticEvent, NativeScrollEvent, Platform, View} from 'react-native';
import EmptyState from '@components/global_threads/empty_state';
import ThreadItem from '@components/global_threads/thread_item';
import Loading from '@components/loading';
import {INITIAL_BATCH_TO_RENDER} from '@components/post_list/post_list_config';
import CustomRefreshControl from '@components/post_list/post_list_refresh_control';
import {ViewTypes} from '@constants';
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
@@ -21,9 +22,11 @@ export type Props = {
haveUnreads: boolean;
intl: typeof intlShape;
isLoading: boolean;
isRefreshing: boolean;
loadMoreThreads: () => Promise<void>;
listRef: React.RefObject<FlatList>;
markAllAsRead: () => void;
onRefresh: () => void;
testID: string;
theme: Theme;
threadIds: $ID<UserThread>[];
@@ -32,9 +35,11 @@ export type Props = {
viewingUnreads: boolean;
};
function ThreadList({haveUnreads, intl, isLoading, loadMoreThreads, listRef, markAllAsRead, testID, theme, threadIds, viewAllThreads, viewUnreadThreads, viewingUnreads}: Props) {
function ThreadList({haveUnreads, intl, isLoading, isRefreshing, loadMoreThreads, listRef, markAllAsRead, onRefresh, testID, theme, threadIds, viewAllThreads, viewUnreadThreads, viewingUnreads}: Props) {
const style = getStyleSheet(theme);
const [offsetY, setOffsetY] = React.useState(0);
const handleEndReached = React.useCallback(() => {
loadMoreThreads();
}, [loadMoreThreads, viewingUnreads]);
@@ -51,6 +56,17 @@ function ThreadList({haveUnreads, intl, isLoading, loadMoreThreads, listRef, mar
);
}, [theme]);
const onScroll = React.useCallback((event: NativeSyntheticEvent<NativeScrollEvent>) => {
if (Platform.OS === 'android') {
const {y} = event.nativeEvent.contentOffset;
if (y === 0) {
setOffsetY(y);
} else if (offsetY === 0 && y !== 0) {
setOffsetY(y);
}
}
}, [offsetY]);
const renderHeader = () => {
if (!viewingUnreads && !threadIds.length) {
return null;
@@ -96,21 +112,30 @@ function ThreadList({haveUnreads, intl, isLoading, loadMoreThreads, listRef, mar
return (
<View style={style.container}>
{renderHeader()}
<FlatList
contentContainerStyle={style.messagesContainer}
data={threadIds}
keyExtractor={keyExtractor}
ListEmptyComponent={renderEmptyList()}
ListFooterComponent={renderFooter()}
onEndReached={handleEndReached}
onEndReachedThreshold={2}
ref={listRef}
renderItem={renderPost}
initialNumToRender={INITIAL_BATCH_TO_RENDER}
maxToRenderPerBatch={Platform.select({android: 5})}
removeClippedSubviews={true}
scrollIndicatorInsets={style.listScrollIndicator}
/>
<CustomRefreshControl
enabled={offsetY === 0}
isInverted={false}
refreshing={isRefreshing}
onRefresh={onRefresh}
theme={theme}
>
<FlatList
contentContainerStyle={style.messagesContainer}
data={threadIds}
keyExtractor={keyExtractor}
ListEmptyComponent={renderEmptyList()}
ListFooterComponent={renderFooter()}
onEndReached={handleEndReached}
onEndReachedThreshold={2}
onScroll={onScroll}
ref={listRef}
renderItem={renderPost}
initialNumToRender={INITIAL_BATCH_TO_RENDER}
maxToRenderPerBatch={Platform.select({android: 5})}
removeClippedSubviews={true}
scrollIndicatorInsets={style.listScrollIndicator}
/>
</CustomRefreshControl>
</View>
);
}

View File

@@ -1,7 +1,7 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import AsyncStorage from '@react-native-community/async-storage';
import AsyncStorage from '@react-native-async-storage/async-storage';
import {PureComponent} from 'react';
import {Dimensions, EmitterSubscription} from 'react-native';

View File

@@ -9,7 +9,7 @@ import {intlShape} from 'react-intl';
import {Alert, Text} from 'react-native';
import urlParse from 'url-parse';
import {dismissAllModals, popToRoot} from '@actions/navigation';
import {dismissAllModals, popToRoot, showModal} from '@actions/navigation';
import Config from '@assets/config';
import {DeepLinkTypes} from '@constants';
import {getCurrentServerUrl} from '@init/credentials';
@@ -59,13 +59,20 @@ export default class MarkdownLink extends PureComponent {
const match = matchDeepLink(url, serverURL, siteURL);
if (match) {
if (match.type === DeepLinkTypes.CHANNEL) {
switch (match.type) {
case DeepLinkTypes.CHANNEL:
await handleSelectChannelByName(match.channelName, match.teamName, errorBadChannel, intl);
await dismissAllModals();
await popToRoot();
} else if (match.type === DeepLinkTypes.PERMALINK) {
break;
case DeepLinkTypes.PERMALINK: {
const teamName = match.teamName === PERMALINK_GENERIC_TEAM_NAME_REDIRECT ? currentTeamName : match.teamName;
showPermalink(intl, teamName, match.postId);
break;
}
case DeepLinkTypes.PLUGIN:
showModal('PluginInternal', match.id, {link: url});
break;
}
} else {
const onError = () => {

View File

@@ -419,6 +419,11 @@ exports[`PostDraft Should render the DraftInput 1`] = `
testID="post_draft.quick_actions"
>
<View
accessibilityState={
Object {
"disabled": false,
}
}
accessible={true}
collapsable={false}
focusable={true}
@@ -447,6 +452,11 @@ exports[`PostDraft Should render the DraftInput 1`] = `
/>
</View>
<View
accessibilityState={
Object {
"disabled": false,
}
}
accessible={true}
collapsable={false}
focusable={true}
@@ -475,6 +485,11 @@ exports[`PostDraft Should render the DraftInput 1`] = `
/>
</View>
<View
accessibilityState={
Object {
"disabled": false,
}
}
accessible={true}
collapsable={false}
focusable={true}
@@ -498,11 +513,16 @@ exports[`PostDraft Should render the DraftInput 1`] = `
>
<Icon
color="rgba(63,67,80,0.64)"
name="file-document-outline"
name="file-text-outline"
size={24}
/>
</View>
<View
accessibilityState={
Object {
"disabled": false,
}
}
accessible={true}
collapsable={false}
focusable={true}
@@ -531,6 +551,11 @@ exports[`PostDraft Should render the DraftInput 1`] = `
/>
</View>
<View
accessibilityState={
Object {
"disabled": false,
}
}
accessible={true}
collapsable={false}
focusable={true}

View File

@@ -329,12 +329,13 @@ export default class DraftInput extends PureComponent {
const notificationsToChannel = enableConfirmNotificationsToChannel && useChannelMentions;
const notificationsToGroups = enableConfirmNotificationsToChannel && useGroupMentions;
const toAllOrChannel = DraftUtils.textContainsAtAllAtChannel(value);
const groupMentions = (!toAllOrChannel && notificationsToGroups) ? DraftUtils.groupsMentionedInText(groupsWithAllowReference, value) : [];
const toHere = DraftUtils.textContainsAtHere(value);
const groupMentions = (!toAllOrChannel && !toHere && notificationsToGroups) ? DraftUtils.groupsMentionedInText(groupsWithAllowReference, value) : [];
if (value.indexOf('/') === 0) {
this.sendCommand(value);
} else if (notificationsToChannel && membersCount > NOTIFY_ALL_MEMBERS && toAllOrChannel) {
this.showSendToAllOrChannelAlert(membersCount, value);
} else if (notificationsToChannel && membersCount > NOTIFY_ALL_MEMBERS && (toAllOrChannel || toHere)) {
this.showSendToAllOrChannelOrHereAlert(membersCount, value, toHere && !toAllOrChannel);
} else if (groupMentions.length > 0) {
const {groupMentionsSet, memberNotifyCount, channelTimezoneCount} = DraftUtils.mapGroupMentions(channelMemberCountsByGroup, groupMentions);
if (memberNotifyCount > 0) {
@@ -364,11 +365,11 @@ export default class DraftInput extends PureComponent {
}
}
showSendToAllOrChannelAlert = (membersCount, msg) => {
showSendToAllOrChannelOrHereAlert = (membersCount, msg, atHere) => {
const {formatMessage} = this.context.intl;
const {channelTimezoneCount} = this.state;
const {isTimezoneEnabled} = this.props;
const notifyAllMessage = DraftUtils.buildChannelWideMentionMessage(formatMessage, membersCount, isTimezoneEnabled, channelTimezoneCount);
const notifyAllMessage = DraftUtils.buildChannelWideMentionMessage(formatMessage, membersCount, isTimezoneEnabled, channelTimezoneCount, atHere);
const cancel = () => {
this.setInputValue(msg);
this.setState({sendingMessage: false});

View File

@@ -110,6 +110,31 @@ describe('DraftInput', () => {
expect(Alert.alert).toHaveBeenCalledWith('Confirm sending notifications to entire channel', expect.anything(), expect.anything());
});
test('should send an alert when sending a message with a @here mention', () => {
const wrapper = shallowWithIntl(
<DraftInput
{...baseProps}
ref={ref}
/>,
);
const message = '@here';
const instance = wrapper.instance();
expect(instance.input).toEqual({current: null});
instance.input = {
current: {
getValue: () => message,
setValue: jest.fn(),
changeDraft: jest.fn(),
resetTextInput: jest.fn(),
},
};
instance.handleSendMessage();
jest.runOnlyPendingTimers();
expect(Alert.alert).toBeCalled();
expect(Alert.alert).toHaveBeenCalledWith('Confirm sending notifications to entire channel', expect.anything(), expect.anything());
});
test('should send an alert when sending a message with a group mention with group with count more than NOTIFY_ALL', () => {
const wrapper = shallowWithIntl(
<DraftInput

View File

@@ -2,6 +2,11 @@
exports[`CameraButton should match snapshot 1`] = `
<View
accessibilityState={
Object {
"disabled": false,
}
}
accessible={true}
collapsable={false}
focusable={true}

View File

@@ -2,6 +2,11 @@
exports[`FileQuickAction should match snapshot 1`] = `
<View
accessibilityState={
Object {
"disabled": false,
}
}
accessible={true}
collapsable={false}
focusable={true}
@@ -25,7 +30,7 @@ exports[`FileQuickAction should match snapshot 1`] = `
>
<Icon
color="rgba(63,67,80,0.64)"
name="file-document-outline"
name="file-text-outline"
size={24}
/>
</View>

View File

@@ -83,7 +83,7 @@ const FileQuickAction = ({disabled, fileCount = 0, intl, maxFileCount, onUploadF
>
<CompassIcon
color={color}
name='file-document-outline'
name='file-text-outline'
size={ICON_SIZE}
/>
</TouchableWithFeedback>

View File

@@ -2,6 +2,11 @@
exports[`ImageQuickAction should match snapshot 1`] = `
<View
accessibilityState={
Object {
"disabled": false,
}
}
accessible={true}
collapsable={false}
focusable={true}

View File

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

View File

@@ -13,7 +13,7 @@ import {shallowWithIntl} from '@test/intl-test-helper';
import MoreMessagesButton, {
MIN_INPUT,
MAX_INPUT,
INDICATOR_BAR_FACTOR,
BARS_FACTOR,
CANCEL_TIMER_DELAY,
} from './more_messages_button';
@@ -296,17 +296,19 @@ describe('MoreMessagesButton', () => {
expect(Animated.spring).not.toHaveBeenCalledTimes(1);
});
it('should animate to MAX_INPUT - INDICATOR_BAR_FACTOR if visible and indicator bar hides', () => {
it('should animate to MAX_INPUT - BARS_FACTOR if visible and indicator bar hides', () => {
instance.buttonVisible = true;
instance.onIndicatorBarVisible(false);
expect(Animated.spring).toHaveBeenCalledWith(instance.top, {
toValue: MAX_INPUT - INDICATOR_BAR_FACTOR,
toValue: MAX_INPUT - BARS_FACTOR,
useNativeDriver: true,
});
});
it('should animate to MAX_INPUT if visible and indicator becomes visible', () => {
instance.buttonVisible = true;
instance.joinCallBarVisible = true;
instance.currentCallBarVisible = true;
instance.onIndicatorBarVisible(true);
expect(Animated.spring).toHaveBeenCalledWith(instance.top, {
toValue: MAX_INPUT,
@@ -414,13 +416,15 @@ describe('MoreMessagesButton', () => {
instance.show();
expect(instance.buttonVisible).toBe(true);
expect(Animated.spring).toHaveBeenCalledWith(instance.top, {
toValue: MAX_INPUT - INDICATOR_BAR_FACTOR,
toValue: MAX_INPUT - BARS_FACTOR,
useNativeDriver: true,
});
});
it('should account for the indicator bar height when the indicator is visible', () => {
it('should account for the indicator bar heights when the indicator is visible', () => {
instance.indicatorBarVisible = true;
instance.joinCallBarVisible = true;
instance.currentCallBarVisible = true;
instance.buttonVisible = false;
wrapper.setState({moreText: '1 new message'});
wrapper.setProps({deepLinkURL: null, unreadCount: 1});
@@ -457,13 +461,15 @@ describe('MoreMessagesButton', () => {
instance.hide();
expect(instance.buttonVisible).toBe(false);
expect(Animated.spring).toHaveBeenCalledWith(instance.top, {
toValue: MIN_INPUT + INDICATOR_BAR_FACTOR,
toValue: MIN_INPUT + BARS_FACTOR,
useNativeDriver: true,
});
});
it('should account for the indicator bar height when the indicator is visible', () => {
it('should account for the indicator bars heights when the indicator is visible', () => {
instance.indicatorBarVisible = true;
instance.joinCallBarVisible = true;
instance.currentCallBarVisible = true;
instance.buttonVisible = true;
instance.hide();

View File

@@ -7,7 +7,7 @@ import {ActivityIndicator, Animated, AppState, AppStateStatus, NativeEventSubscr
import CompassIcon from '@components/compass_icon';
import TouchableWithFeedback from '@components/touchable_with_feedback';
import ViewTypes, {INDICATOR_BAR_HEIGHT} from '@constants/view';
import ViewTypes, {INDICATOR_BAR_HEIGHT, JOIN_CALL_BAR_HEIGHT, CURRENT_CALL_BAR_HEIGHT} from '@constants/view';
import EventEmitter from '@mm-redux/utils/event_emitter';
import {messageCount} from '@mm-redux/utils/post_list';
import {t} from '@utils/i18n';
@@ -17,22 +17,22 @@ import type {Theme} from '@mm-redux/types/theme';
const HIDDEN_TOP = -400;
const SHOWN_TOP = 0;
export const INDICATOR_BAR_FACTOR = Math.abs(INDICATOR_BAR_HEIGHT / (HIDDEN_TOP - SHOWN_TOP));
export const BARS_FACTOR = Math.abs((INDICATOR_BAR_HEIGHT + JOIN_CALL_BAR_HEIGHT + CURRENT_CALL_BAR_HEIGHT) / (HIDDEN_TOP - SHOWN_TOP));
export const MIN_INPUT = 0;
export const MAX_INPUT = 1;
const TOP_INTERPOL_CONFIG: Animated.InterpolationConfigType = {
inputRange: [
MIN_INPUT,
MIN_INPUT + INDICATOR_BAR_FACTOR,
MAX_INPUT - INDICATOR_BAR_FACTOR,
MIN_INPUT + BARS_FACTOR,
MAX_INPUT - BARS_FACTOR,
MAX_INPUT,
],
outputRange: [
HIDDEN_TOP - INDICATOR_BAR_HEIGHT,
HIDDEN_TOP - (INDICATOR_BAR_HEIGHT + JOIN_CALL_BAR_HEIGHT + CURRENT_CALL_BAR_HEIGHT),
HIDDEN_TOP,
SHOWN_TOP,
SHOWN_TOP + INDICATOR_BAR_HEIGHT,
SHOWN_TOP + INDICATOR_BAR_HEIGHT + JOIN_CALL_BAR_HEIGHT + CURRENT_CALL_BAR_HEIGHT,
],
extrapolate: 'clamp',
};
@@ -71,6 +71,8 @@ export default class MoreMessageButton extends React.PureComponent<MoreMessagesB
disableViewableItems = false;
endIndex: number | null = null;
indicatorBarVisible = false;
joinCallBarVisible = false;
currentCallBarVisible = false;
pressed = false;
removeViewableItemsListener: undefined | (() => void) = undefined;
removeScrollEndIndexListener: undefined | (() => void) = undefined;
@@ -81,6 +83,8 @@ export default class MoreMessageButton extends React.PureComponent<MoreMessagesB
componentDidMount() {
this.appStateListener = AppState.addEventListener('change', this.onAppStateChange);
EventEmitter.on(ViewTypes.INDICATOR_BAR_VISIBLE, this.onIndicatorBarVisible);
EventEmitter.on(ViewTypes.JOIN_CALL_BAR_VISIBLE, this.onJoinCallBarVisible);
EventEmitter.on(ViewTypes.CURRENT_CALL_BAR_VISIBLE, this.onCurrentCallBarVisible);
this.removeViewableItemsListener = this.props.registerViewableItemsListener(this.onViewableItemsChanged);
this.removeScrollEndIndexListener = this.props.registerScrollEndIndexListener(this.onScrollEndIndex);
}
@@ -88,6 +92,8 @@ export default class MoreMessageButton extends React.PureComponent<MoreMessagesB
componentWillUnmount() {
this.appStateListener?.remove();
EventEmitter.off(ViewTypes.INDICATOR_BAR_VISIBLE, this.onIndicatorBarVisible);
EventEmitter.off(ViewTypes.JOIN_CALL_BAR_VISIBLE, this.onJoinCallBarVisible);
EventEmitter.off(ViewTypes.CURRENT_CALL_BAR_VISIBLE, this.onCurrentCallBarVisible);
if (this.removeViewableItemsListener) {
this.removeViewableItemsListener();
}
@@ -144,8 +150,22 @@ export default class MoreMessageButton extends React.PureComponent<MoreMessagesB
onIndicatorBarVisible = (indicatorVisible: boolean) => {
this.indicatorBarVisible = indicatorVisible;
this.animateButton();
}
onCurrentCallBarVisible = (currentCallVisible: boolean) => {
this.currentCallBarVisible = currentCallVisible;
this.animateButton();
}
onJoinCallBarVisible = (joinCallVisible: boolean) => {
this.joinCallBarVisible = joinCallVisible;
this.animateButton();
}
animateButton = () => {
if (this.buttonVisible) {
const toValue = this.indicatorBarVisible ? MAX_INPUT : MAX_INPUT - INDICATOR_BAR_FACTOR;
const toValue = MAX_INPUT - this.getBarsFactor();
Animated.spring(this.top, {
toValue,
useNativeDriver: true,
@@ -169,18 +189,22 @@ export default class MoreMessageButton extends React.PureComponent<MoreMessagesB
show = () => {
if (!this.buttonVisible && this.state.moreText && !this.props.deepLinkURL && !this.canceled && this.props.unreadCount > 0) {
this.buttonVisible = true;
const toValue = this.indicatorBarVisible ? MAX_INPUT : MAX_INPUT - INDICATOR_BAR_FACTOR;
Animated.spring(this.top, {
toValue,
useNativeDriver: true,
}).start();
this.animateButton();
}
}
getBarsFactor = () => {
return Math.abs((
(this.indicatorBarVisible ? 0 : INDICATOR_BAR_HEIGHT) +
(this.joinCallBarVisible ? 0 : JOIN_CALL_BAR_HEIGHT) +
(this.currentCallBarVisible ? 0 : CURRENT_CALL_BAR_HEIGHT)
) / (HIDDEN_TOP - SHOWN_TOP));
}
hide = () => {
if (this.buttonVisible) {
this.buttonVisible = false;
const toValue = this.indicatorBarVisible ? MIN_INPUT : MIN_INPUT + INDICATOR_BAR_FACTOR;
const toValue = MIN_INPUT + this.getBarsFactor();
Animated.spring(this.top, {
toValue,
useNativeDriver: true,

View File

@@ -58,11 +58,15 @@ const ButtonBinding = ({binding, doAppCall, intl, post, postEphemeralCallRespons
const style = getStyleSheet(theme);
const onPress = useCallback(preventDoubleTap(async () => {
if (!binding.call || pressed.current) {
if (pressed.current) {
return;
}
pressed.current = true;
const call = binding.form?.call || binding.call;
if (!call) {
return;
}
const context = createCallContext(
binding.app_id,
@@ -72,13 +76,20 @@ const ButtonBinding = ({binding, doAppCall, intl, post, postEphemeralCallRespons
post.id,
);
const call = createCallRequest(
binding.call,
const callRequest = createCallRequest(
call,
context,
{post: AppExpandLevels.EXPAND_ALL},
);
const res = await doAppCall(call, AppCallTypes.SUBMIT, intl);
if (binding.form) {
showAppForm(binding.form, callRequest, theme);
return;
}
pressed.current = true;
const res = await doAppCall(callRequest, AppCallTypes.SUBMIT, intl);
pressed.current = false;
if (res.error) {

View File

@@ -42,7 +42,9 @@ const MenuBinding = ({binding, doAppCall, intl, post, postEphemeralCallResponseF
return;
}
if (!bind.call) {
const call = bind.form?.call || bind.call;
if (!call) {
return;
}
@@ -54,13 +56,18 @@ const MenuBinding = ({binding, doAppCall, intl, post, postEphemeralCallResponseF
post.id,
);
const call = createCallRequest(
bind.call,
const callRequest = createCallRequest(
call,
context,
{post: AppExpandLevels.EXPAND_ALL},
);
const res = await doAppCall(call, AppCallTypes.SUBMIT, intl);
if (bind.form) {
showAppForm(bind.form, callRequest);
return;
}
const res = await doAppCall(callRequest, AppCallTypes.SUBMIT, intl);
if (res.error) {
const errorResponse = res.error;
const errorMessage = errorResponse.error || intl.formatMessage({

View File

@@ -200,4 +200,6 @@ const DocumentFile = forwardRef<DocumentFileRef, DocumentFileProps>(({background
);
});
DocumentFile.displayName = 'DocumentFile';
export default injectIntl(DocumentFile, {withRef: true});

View File

@@ -25,21 +25,21 @@ const BLUE_ICON = '#338AFF';
const RED_ICON = '#ED522A';
const GREEN_ICON = '#1CA660';
const GRAY_ICON = '#999999';
const FAILED_ICON_NAME_AND_COLOR = ['jumbo-attachment-image-broken', GRAY_ICON];
const FAILED_ICON_NAME_AND_COLOR = ['file-image-broken-outline-large', GRAY_ICON];
const ICON_NAME_AND_COLOR_FROM_FILE_TYPE: Record<string, string[]> = {
audio: ['jumbo-attachment-audio', BLUE_ICON],
code: ['jumbo-attachment-code', BLUE_ICON],
image: ['jumbo-attachment-image', BLUE_ICON],
audio: ['file-audio-outline-large', BLUE_ICON],
code: ['file-code-outline-large', BLUE_ICON],
image: ['file-image-outline-large', BLUE_ICON],
smallImage: ['image-outline', BLUE_ICON],
other: ['jumbo-attachment-generic', BLUE_ICON],
patch: ['jumbo-attachment-patch', BLUE_ICON],
pdf: ['jumbo-attachment-pdf', RED_ICON],
presentation: ['jumbo-attachment-powerpoint', RED_ICON],
spreadsheet: ['jumbo-attachment-excel', GREEN_ICON],
text: ['jumbo-attachment-text', GRAY_ICON],
video: ['jumbo-attachment-video', BLUE_ICON],
word: ['jumbo-attachment-word', BLUE_ICON],
zip: ['jumbo-attachment-zip', BLUE_ICON],
other: ['file-generic-outline-large', BLUE_ICON],
patch: ['file-patch-outline-large', BLUE_ICON],
pdf: ['file-pdf-outline-large', RED_ICON],
presentation: ['file-powerpoint-outline-large', RED_ICON],
spreadsheet: ['file-excel-outline-large', GREEN_ICON],
text: ['file-text-outline-large', GRAY_ICON],
video: ['file-video-outline-large', BLUE_ICON],
word: ['file-word-outline-large', BLUE_ICON],
zip: ['file-zip-outline-large', BLUE_ICON],
};
const styles = StyleSheet.create({

View File

@@ -1,7 +1,7 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React, {useEffect, useRef, useState} from 'react';
import React, {useEffect, useMemo, useState} from 'react';
import {DeviceEventEmitter, StyleProp, StyleSheet, View, ViewStyle} from 'react-native';
import {Client4} from '@client/rest';
@@ -46,10 +46,10 @@ const Files = ({canDownloadFiles, failed, files, isReplyPost, postId, theme}: Fi
const [inViewPort, setInViewPort] = useState(false);
const permanentSidebar = usePermanentSidebar();
const isSplitView = useSplitView();
const imageAttachments = useRef<FileInfo[]>([]).current;
const nonImageAttachments = useRef<FileInfo[]>([]).current;
const {imageAttachments, nonImageAttachments} = useMemo(() => {
const images: FileInfo[] = [];
const nonImages: FileInfo[] = [];
if (!imageAttachments.length && !nonImageAttachments.length) {
files.reduce((info, file) => {
if (isImage(file)) {
let uri;
@@ -58,15 +58,17 @@ const Files = ({canDownloadFiles, failed, files, isReplyPost, postId, theme}: Fi
} else {
uri = isGif(file) ? Client4.getFileUrl(file.id, 0) : Client4.getFilePreviewUrl(file.id, 0);
}
info.imageAttachments.push({...file, uri});
info.images.push({...file, uri});
} else {
info.nonImageAttachments.push(file);
info.nonImages.push(file);
}
return info;
}, {imageAttachments, nonImageAttachments});
}
}, {images, nonImages});
const filesForGallery = useRef<FileInfo[]>(imageAttachments.concat(nonImageAttachments)).current;
return {imageAttachments: images, nonImageAttachments: nonImages};
}, [files]);
const filesForGallery = imageAttachments.concat(nonImageAttachments);
const attachmentIndex = (fileId: string) => {
return filesForGallery.findIndex((file) => file.id === fileId) || 0;
};

View File

@@ -7,7 +7,7 @@ import {showPermalink} from '@actions/views/permalink';
import {THREAD} from '@constants/screen';
import {removePost} from '@mm-redux/actions/posts';
import {getChannel} from '@mm-redux/selectors/entities/channels';
import {getConfig} from '@mm-redux/selectors/entities/general';
import {getConfig, getFeatureFlagValue} from '@mm-redux/selectors/entities/general';
import {getPost, isRootPost} from '@mm-redux/selectors/entities/posts';
import {getMyPreferences, getTeammateNameDisplaySetting, isCollapsedThreadsEnabled} from '@mm-redux/selectors/entities/preferences';
import {getCurrentTeamId} from '@mm-redux/selectors/entities/teams';
@@ -49,6 +49,7 @@ function mapSateToProps(state: GlobalState, ownProps: OwnProps) {
const teammateNameDisplay = getTeammateNameDisplaySetting(state);
const enablePostUsernameOverride = config.EnablePostUsernameOverride === 'true';
const isConsecutivePost = post && previousPost && !author?.is_bot && !isRootPost(state, post.id) && areConsecutivePosts(post, previousPost);
const callsFeatureEnabled = getFeatureFlagValue(state, 'CallsMobile') === 'true';
let isFirstReply = true;
let isLastReply = true;
let canDelete = false;
@@ -97,6 +98,7 @@ function mapSateToProps(state: GlobalState, ownProps: OwnProps) {
teammateNameDisplay,
thread,
threadStarter: getUser(state, post.user_id),
callsFeatureEnabled,
};
}

View File

@@ -16,6 +16,7 @@ import {UserThread} from '@mm-redux/types/threads';
import {UserProfile} from '@mm-redux/types/users';
import EventEmitter from '@mm-redux/utils/event_emitter';
import {fromAutoResponder, isPostEphemeral, isPostPendingOrFailed, isSystemMessage} from '@mm-redux/utils/post_utils';
import CallMessage from '@mmproducts/calls/components/call_message';
import {preventDoubleTap} from '@utils/tap';
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
@@ -54,6 +55,7 @@ type PostProps = {
theme: Theme;
thread: UserThread;
threadStarter: UserProfile;
callsFeatureEnabled: boolean;
};
const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => {
@@ -113,7 +115,7 @@ const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => {
const Post = ({
canDelete, collapsedThreadsEnabled, enablePostUsernameOverride, highlight, highlightPinnedOrFlagged = true, intl, isConsecutivePost, isFirstReply, isFlagged, isLastReply,
location, post, removePost, rootPostAuthor, shouldRenderReplyButton, skipFlaggedHeader, skipPinnedHeader, showAddReaction = true, showPermalink, style,
teammateNameDisplay, testID, theme, thread, threadStarter,
teammateNameDisplay, testID, theme, thread, threadStarter, callsFeatureEnabled,
}: PostProps) => {
const pressDetected = useRef(false);
const styles = getStyleSheet(theme);
@@ -239,6 +241,13 @@ const Post = ({
theme={theme}
/>
);
} else if (post.type === 'custom_calls' && callsFeatureEnabled) {
body = (
<CallMessage
post={post}
theme={theme}
/>
);
} else {
body = (
<Body

View File

@@ -297,10 +297,10 @@ const PostList = ({
if (match) {
if (match.type === DeepLinkTypes.CHANNEL) {
handleSelectChannelByName(match.channelName!, match.teamName, errorBadChannel, intl);
handleSelectChannelByName(match.channelName!, match.teamName!, errorBadChannel, intl);
} else if (match.type === DeepLinkTypes.PERMALINK) {
const teamName = match.teamName === PERMALINK_GENERIC_TEAM_NAME_REDIRECT ? currentTeamName : match.teamName;
onPermalinkPress(match.postId!, teamName);
onPermalinkPress(match.postId!, teamName!);
}
} else {
badDeepLink(intl);

View File

@@ -9,6 +9,7 @@ import type {Theme} from '@mm-redux/types/theme';
type Props = {
children: ReactElement;
enabled: boolean;
isInverted?: boolean;
onRefresh: () => void;
refreshing: boolean;
theme: Theme;
@@ -17,11 +18,13 @@ type Props = {
const style = StyleSheet.create({
container: {
flex: 1,
},
containerInverse: {
scaleY: -1,
},
});
const PostListRefreshControl = ({children, enabled, onRefresh, refreshing, theme}: Props) => {
const PostListRefreshControl = ({children, enabled, isInverted = true, onRefresh, refreshing, theme}: Props) => {
const props = {
colors: [theme.onlineIndicator, theme.awayIndicator, theme.dndIndicator],
onRefresh,
@@ -34,7 +37,7 @@ const PostListRefreshControl = ({children, enabled, onRefresh, refreshing, theme
<RefreshControl
{...props}
enabled={enabled}
style={style.container}
style={[style.container, isInverted ? style.containerInverse : undefined]}
>
{children}
</RefreshControl>
@@ -45,7 +48,7 @@ const PostListRefreshControl = ({children, enabled, onRefresh, refreshing, theme
return React.cloneElement(
children,
{refreshControl, inverted: true},
{refreshControl, inverted: isInverted},
);
};

View File

@@ -808,6 +808,240 @@ exports[`ChannelItem should match snapshot for no displayName 1`] = `null`;
exports[`ChannelItem should match snapshot for showUnreadForMsgs 1`] = `null`;
exports[`ChannelItem should match snapshot when there is a call and but calls are disabled 1`] = `
<TouchableHighlight
onPress={[Function]}
underlayColor="rgba(40,66,123,0.5)"
>
<View
style={
Array [
Object {
"flex": 1,
"flexDirection": "row",
"height": 44,
},
undefined,
]
}
testID="main.sidebar.channels_list.list.channel_item"
>
<View
style={
Array [
Object {
"alignItems": "center",
"flex": 1,
"flexDirection": "row",
"paddingLeft": 16,
},
undefined,
]
}
testID="main.sidebar.channels_list.list.channel_item.channel_id"
>
<ChannelIcon
channelId="channel_id"
hasDraft={false}
isActive={false}
isArchived={false}
isInfo={false}
isUnread={true}
membersCount={1}
size={24}
statusStyle={
Object {
"backgroundColor": "#1e325c",
"borderColor": "transparent",
}
}
testID="main.sidebar.channels_list.list.channel_item.channel_icon"
theme={
Object {
"awayIndicator": "#ffbc1f",
"buttonBg": "#1c58d9",
"buttonColor": "#ffffff",
"centerChannelBg": "#ffffff",
"centerChannelColor": "#3f4350",
"codeTheme": "github",
"dndIndicator": "#d24b4e",
"errorTextColor": "#d24b4e",
"linkColor": "#386fe5",
"mentionBg": "#ffffff",
"mentionColor": "#1e325c",
"mentionHighlightBg": "#ffd470",
"mentionHighlightLink": "#1b1d22",
"newMessageSeparator": "#cc8f00",
"onlineIndicator": "#3db887",
"sidebarBg": "#1e325c",
"sidebarHeaderBg": "#192a4d",
"sidebarHeaderTextColor": "#ffffff",
"sidebarTeamBarBg": "#14213e",
"sidebarText": "#ffffff",
"sidebarTextActiveBorder": "#5d89ea",
"sidebarTextActiveColor": "#ffffff",
"sidebarTextHoverBg": "#28427b",
"sidebarUnreadText": "#ffffff",
"type": "Denim",
}
}
type="O"
/>
<Text
ellipsizeMode="tail"
numberOfLines={1}
style={
Array [
Object {
"alignSelf": "center",
"color": "rgba(255,255,255,0.6)",
"fontFamily": "Open Sans",
"fontSize": 16,
"lineHeight": 24,
"marginLeft": 13,
"maxWidth": "80%",
"paddingRight": 10,
},
Object {
"color": "#ffffff",
"fontWeight": "500",
"maxWidth": "70%",
"opacity": 1,
},
]
}
testID="main.sidebar.channels_list.list.channel_item.display_name"
>
display_name
</Text>
</View>
</View>
</TouchableHighlight>
`;
exports[`ChannelItem should match snapshot when there is a call and calls are enabled 1`] = `
<TouchableHighlight
onPress={[Function]}
underlayColor="rgba(40,66,123,0.5)"
>
<View
style={
Array [
Object {
"flex": 1,
"flexDirection": "row",
"height": 44,
},
undefined,
]
}
testID="main.sidebar.channels_list.list.channel_item"
>
<View
style={
Array [
Object {
"alignItems": "center",
"flex": 1,
"flexDirection": "row",
"paddingLeft": 16,
},
undefined,
]
}
testID="main.sidebar.channels_list.list.channel_item.channel_id"
>
<ChannelIcon
channelId="channel_id"
hasDraft={false}
isActive={false}
isArchived={false}
isInfo={false}
isUnread={true}
membersCount={1}
size={24}
statusStyle={
Object {
"backgroundColor": "#1e325c",
"borderColor": "transparent",
}
}
testID="main.sidebar.channels_list.list.channel_item.channel_icon"
theme={
Object {
"awayIndicator": "#ffbc1f",
"buttonBg": "#1c58d9",
"buttonColor": "#ffffff",
"centerChannelBg": "#ffffff",
"centerChannelColor": "#3f4350",
"codeTheme": "github",
"dndIndicator": "#d24b4e",
"errorTextColor": "#d24b4e",
"linkColor": "#386fe5",
"mentionBg": "#ffffff",
"mentionColor": "#1e325c",
"mentionHighlightBg": "#ffd470",
"mentionHighlightLink": "#1b1d22",
"newMessageSeparator": "#cc8f00",
"onlineIndicator": "#3db887",
"sidebarBg": "#1e325c",
"sidebarHeaderBg": "#192a4d",
"sidebarHeaderTextColor": "#ffffff",
"sidebarTeamBarBg": "#14213e",
"sidebarText": "#ffffff",
"sidebarTextActiveBorder": "#5d89ea",
"sidebarTextActiveColor": "#ffffff",
"sidebarTextHoverBg": "#28427b",
"sidebarUnreadText": "#ffffff",
"type": "Denim",
}
}
type="O"
/>
<Text
ellipsizeMode="tail"
numberOfLines={1}
style={
Array [
Object {
"alignSelf": "center",
"color": "rgba(255,255,255,0.6)",
"fontFamily": "Open Sans",
"fontSize": 16,
"lineHeight": 24,
"marginLeft": 13,
"maxWidth": "80%",
"paddingRight": 10,
},
Object {
"color": "#ffffff",
"fontWeight": "500",
"maxWidth": "70%",
"opacity": 1,
},
]
}
testID="main.sidebar.channels_list.list.channel_item.display_name"
>
display_name
</Text>
<CompassIcon
name="phone-in-talk"
size={16}
style={
Object {
"color": "#ffffff",
"flex": 1,
"marginRight": 20,
"textAlign": "right",
}
}
/>
</View>
</View>
</TouchableHighlight>
`;
exports[`ChannelItem should match snapshot with custom status emoji 1`] = `
<TouchableHighlight
onPress={[Function]}

View File

@@ -13,6 +13,7 @@ import {
import Badge from '@components/badge';
import ChannelIcon from '@components/channel_icon';
import CompassIcon from '@components/compass_icon';
import CustomStatusEmoji from '@components/custom_status/custom_status_emoji';
import {General} from '@mm-redux/constants';
import {preventDoubleTap} from '@utils/tap';
@@ -41,6 +42,8 @@ export default class ChannelItem extends PureComponent {
isSearchResult: PropTypes.bool,
viewingGlobalThreads: PropTypes.bool,
customStatusEnabled: PropTypes.bool.isRequired,
channelHasCall: PropTypes.bool.isRequired,
callsFeatureEnabled: PropTypes.bool.isRequired,
};
static defaultProps = {
@@ -214,6 +217,13 @@ export default class ChannelItem extends PureComponent {
</Text>
{customStatus}
{badge}
{this.props.callsFeatureEnabled && this.props.channelHasCall &&
<CompassIcon
name='phone-in-talk'
size={16}
style={style.hasCall}
/>
}
</View>
</View>
</TouchableHighlight>
@@ -288,5 +298,11 @@ export const getStyleSheet = makeStyleSheetFromTheme((theme) => {
muted: {
opacity: 0.5,
},
hasCall: {
color: theme.sidebarText,
flex: 1,
textAlign: 'right',
marginRight: 20,
},
};
});

View File

@@ -40,6 +40,8 @@ describe('ChannelItem', () => {
isSearchResult: false,
isBot: false,
customStatusEnabled: true,
channelHasCall: false,
callsFeatureEnabled: false,
};
test('should match snapshot', () => {
@@ -50,6 +52,34 @@ describe('ChannelItem', () => {
expect(wrapper.getElement()).toMatchSnapshot();
});
test('should match snapshot when there is a call and calls are enabled', () => {
const newProps = {
...baseProps,
callsFeatureEnabled: true,
channelHasCall: true,
};
const wrapper = shallowWithIntl(
<ChannelItem {...newProps}/>,
);
expect(wrapper.getElement()).toMatchSnapshot();
});
test('should match snapshot when there is a call and but calls are disabled', () => {
const newProps = {
...baseProps,
callsFeatureEnabled: false,
channelHasCall: true,
};
const wrapper = shallowWithIntl(
<ChannelItem {...newProps}/>,
);
expect(wrapper.getElement()).toMatchSnapshot();
});
test('should match snapshot with mentions and muted', () => {
const newProps = {
...baseProps,

View File

@@ -11,10 +11,12 @@ import {
makeGetChannel,
shouldHideDefaultChannel,
} from '@mm-redux/selectors/entities/channels';
import {getFeatureFlagValue} from '@mm-redux/selectors/entities/general';
import {getTheme, getTeammateNameDisplaySetting, isCollapsedThreadsEnabled} from '@mm-redux/selectors/entities/preferences';
import {getCurrentUserId, getUser} from '@mm-redux/selectors/entities/users';
import {getMsgCountInChannel, getUserIdFromChannelName, isChannelMuted} from '@mm-redux/utils/channel_utils';
import {displayUsername} from '@mm-redux/utils/user_utils';
import {getCalls} from '@mmproducts/calls/store/selectors/calls';
import {isCustomStatusEnabled} from '@selectors/custom_status';
import {getViewingGlobalThreads} from '@selectors/threads';
import {getDraftForChannel} from '@selectors/views';
@@ -31,6 +33,7 @@ function makeMapStateToProps() {
const currentUserId = getCurrentUserId(state);
const channelDraft = getDraftForChannel(state, channel.id);
const collapsedThreadsEnabled = isCollapsedThreadsEnabled(state);
const channelHasCall = Boolean(getCalls(state)[ownProps.channelId]);
let displayName = channel.display_name;
let isGuest = false;
@@ -72,6 +75,7 @@ function makeMapStateToProps() {
if (member && member.notify_props) {
showUnreadForMsgs = member.notify_props.mark_unread !== General.MENTION;
}
const callsFeatureEnabled = getFeatureFlagValue(state, 'CallsMobile') === 'true';
const viewingGlobalThreads = getViewingGlobalThreads(state);
return {
@@ -92,6 +96,8 @@ function makeMapStateToProps() {
unreadMsgs,
viewingGlobalThreads,
customStatusEnabled: isCustomStatusEnabled(state),
channelHasCall,
callsFeatureEnabled,
};
};
}

View File

@@ -146,7 +146,7 @@ export default class ChannelsList extends PureComponent {
<SearchBar
testID={searchBarTestID}
ref={this.setSearchBarRef}
placeholder={intl.formatMessage({id: 'mobile.channel_drawer.search', defaultMessage: 'Jump to...'})}
placeholder={intl.formatMessage({id: 'mobile.channel_drawer.search', defaultMessage: 'Find channel'})}
cancelTitle={intl.formatMessage({id: 'mobile.post.cancel', defaultMessage: 'Cancel'})}
backgroundColor='transparent'
inputHeight={36}

View File

@@ -83,6 +83,7 @@ function mapStateToProps(state) {
showLegacySidebar,
unreadsOnTop,
currentChannelId,
currentTeamId,
};
}

View File

@@ -56,6 +56,7 @@ export default class List extends PureComponent {
showLegacySidebar: PropTypes.bool.isRequired,
unreadsOnTop: PropTypes.bool.isRequired,
currentChannelId: PropTypes.string,
currentTeamId: PropTypes.string,
};
static contextTypes = {
@@ -419,6 +420,21 @@ export default class List extends PureComponent {
}
};
const categoryId = () => {
switch (type) {
case CategoryTypes.UNREADS:
return CategoryTypes.UNREADS.toLowerCase();
case CategoryTypes.FAVORITES:
return CategoryTypes.FAVORITES.toLowerCase();
case CategoryTypes.CHANNELS:
return CategoryTypes.CHANNELS.toLowerCase();
case CategoryTypes.DIRECT_MESSAGES:
return CategoryTypes.DIRECT_MESSAGES.toLowerCase();
default:
return name.replace(/ /g, '_').toLowerCase();
}
};
const header = (
<View style={styles.titleContainer}>
{(type !== CategoryTypes.UNREADS && data.length > 0) &&
@@ -434,7 +450,7 @@ export default class List extends PureComponent {
<View style={styles.separatorContainer}>
<Text> </Text>
</View>
{action && this.renderSectionAction(styles, action, anchor, id)}
{action && this.renderSectionAction(styles, action, anchor, categoryId())}
</View>
);
@@ -532,7 +548,7 @@ export default class List extends PureComponent {
};
render() {
const {testID, styles, theme, showLegacySidebar, collapsedThreadsEnabled} = this.props;
const {testID, styles, theme, showLegacySidebar, collapsedThreadsEnabled, currentTeamId} = this.props;
const {sections, categorySections, showIndicator} = this.state;
const paddingBottom = this.listContentPadding();
@@ -545,6 +561,7 @@ export default class List extends PureComponent {
<View
style={styles.container}
onLayout={this.onLayout}
key={currentTeamId}
>
{collapsedThreadsEnabled && (
<ThreadsSidebarEntry/>

View File

@@ -1,7 +1,7 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import AsyncStorage from '@react-native-community/async-storage';
import AsyncStorage from '@react-native-async-storage/async-storage';
import React from 'react';
import {intlShape} from 'react-intl';

View File

@@ -7,4 +7,5 @@ export default {
GROUPCHANNEL: 'groupchannel',
PERMALINK: 'permalink',
OTHER: 'other',
PLUGIN: 'plugin',
};

View File

@@ -27,6 +27,8 @@ export const NotificationLevels = {
export const NOTIFY_ALL_MEMBERS = 5;
export const INDICATOR_BAR_HEIGHT = 38;
export const JOIN_CALL_BAR_HEIGHT = 38;
export const CURRENT_CALL_BAR_HEIGHT = 74;
export const CHANNEL_ITEM_LARGE_BADGE_MAX_WIDTH = 38;
export const CHANNEL_ITEM_SMALL_BADGE_MAX_WIDTH = 32;
@@ -105,6 +107,11 @@ const ViewTypes = keyMirror({
VIEWING_GLOBAL_THREADS_UNREADS: null,
VIEWING_GLOBAL_THREADS_ALL: null,
THREAD_LAST_VIEWED_AT: null,
JOIN_CALL_BAR_VISIBLE: null,
CURRENT_CALL_BAR_VISIBLE: null,
});
const RequiredServer = {
@@ -118,7 +125,7 @@ export default {
...ViewTypes,
RequiredServer,
POST_VISIBILITY_CHUNK_SIZE: 60,
CRT_CHUNK_SIZE: 60,
CRT_CHUNK_SIZE: 30,
FEATURE_TOGGLE_PREFIX: 'feature_enabled_',
EMBED_PREVIEW: 'embed_preview',
LINK_PREVIEW_DISPLAY: 'link_previews',

View File

@@ -51,5 +51,16 @@ const WebsocketEvents = {
SIDEBAR_CATEGORY_UPDATED: 'sidebar_category_updated',
SIDEBAR_CATEGORY_DELETED: 'sidebar_category_deleted',
SIDEBAR_CATEGORY_ORDER_UPDATED: 'sidebar_category_order_updated',
CALLS_CHANNEL_ENABLED: 'custom_com.mattermost.calls_channel_enable_voice',
CALLS_CHANNEL_DISABLED: 'custom_com.mattermost.calls_channel_disable_voice',
CALLS_USER_CONNECTED: 'custom_com.mattermost.calls_user_connected',
CALLS_USER_DISCONNECTED: 'custom_com.mattermost.calls_user_disconnected',
CALLS_USER_MUTED: 'custom_com.mattermost.calls_user_muted',
CALLS_USER_UNMUTED: 'custom_com.mattermost.calls_user_unmuted',
CALLS_USER_VOICE_ON: 'custom_com.mattermost.calls_user_voice_on',
CALLS_USER_VOICE_OFF: 'custom_com.mattermost.calls_user_voice_off',
CALLS_CALL_START: 'custom_com.mattermost.calls_call_start',
CALLS_SCREEN_ON: 'custom_com.mattermost.calls_user_screen_on',
CALLS_SCREEN_OFF: 'custom_com.mattermost.calls_user_screen_off',
};
export default WebsocketEvents;

View File

@@ -1,7 +1,7 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import AsyncStorage from '@react-native-community/async-storage';
import AsyncStorage from '@react-native-async-storage/async-storage';
import {useEffect, useState} from 'react';
import {useWindowDimensions} from 'react-native';

View File

@@ -1,7 +1,7 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import AsyncStorage from '@react-native-community/async-storage';
import AsyncStorage from '@react-native-async-storage/async-storage';
import * as KeyChain from 'react-native-keychain';

View File

@@ -1,7 +1,7 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import AsyncStorage from '@react-native-community/async-storage';
import AsyncStorage from '@react-native-async-storage/async-storage';
import {DeviceTypes} from '@constants';

View File

@@ -1,7 +1,7 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import AsyncStorage from '@react-native-community/async-storage';
import AsyncStorage from '@react-native-async-storage/async-storage';
import CookieManager from '@react-native-cookies/cookies';
import {AppState, Dimensions, Keyboard, Linking, Platform} from 'react-native';

View File

@@ -5,6 +5,7 @@
import {Notifications} from 'react-native-notifications';
import * as Preferences from '@mm-redux/selectors/entities/preferences';
import * as ViewSelectors from '@selectors/views';
import Store from '@store/store';
@@ -49,13 +50,118 @@ describe('PushNotification', () => {
// Clear channel1 notifications
await PushNotification.clearChannelNotifications(channel1ID);
await Notifications.ios.getDeliveredNotifications(async (deliveredNotifs) => {
expect(deliveredNotifs.length).toBe(2);
const channel1DeliveredNotifications = deliveredNotifs.filter((n) => n.channel_id === channel1ID);
const channel2DeliveredNotifications = deliveredNotifs.filter((n) => n.channel_id === channel2ID);
expect(channel1DeliveredNotifications.length).toBe(0);
expect(channel2DeliveredNotifications.length).toBe(2);
});
const deliveredNotifs = await Notifications.ios.getDeliveredNotifications();
expect(deliveredNotifs.length).toBe(2);
const channel1DeliveredNotifications = deliveredNotifs.filter((n) => n.channel_id === channel1ID);
const channel2DeliveredNotifications = deliveredNotifs.filter((n) => n.channel_id === channel2ID);
expect(channel1DeliveredNotifications.length).toBe(0);
expect(channel2DeliveredNotifications.length).toBe(2);
});
it('should clear root posts only from the channel notifications when CRT is enabled', async () => {
Store.redux = {
getState: jest.fn(),
};
Preferences.isCollapsedThreadsEnabled = jest.fn().mockImplementation(() => true);
ViewSelectors.getBadgeCount = jest.fn().mockReturnValue(5);
const deliveredNotifications = [
// Three channel1 delivered notifications
{
identifier: 'channel1-1',
channel_id: channel1ID,
root_id: 'root-id-1',
},
{
identifier: 'channel1-2',
channel_id: channel1ID,
},
{
identifier: 'channel1-3',
channel_id: channel1ID,
},
// Two channel2 delivered notifications
{
identifier: 'channel2-1',
channel_id: channel2ID,
root_id: 'root-id-2',
},
{
identifier: 'channel2-2',
channel_id: channel2ID,
},
];
Notifications.setDeliveredNotifications(deliveredNotifications);
const notificationCount = deliveredNotifications.length;
expect(notificationCount).toBe(5);
// Clear channel1 notifications
await PushNotification.clearChannelNotifications(channel1ID);
const deliveredNotifs = await Notifications.ios.getDeliveredNotifications();
expect(deliveredNotifs.length).toBe(3);
const channel1DeliveredNotifications = deliveredNotifs.filter((n) => n.channel_id === channel1ID);
const channel2DeliveredNotifications = deliveredNotifs.filter((n) => n.channel_id === channel2ID);
expect(channel1DeliveredNotifications.length).toBe(1);
expect(channel2DeliveredNotifications.length).toBe(2);
});
it('should clear all thread notifications', async () => {
Store.redux = null;
ViewSelectors.getBadgeCount = jest.fn().mockReturnValue(5);
const root1ID = 'root-1-id';
const root2ID = 'root-2-id';
const root3ID = 'root-3-id';
const deliveredNotifications = [
// Three channel1 delivered notifications
{
identifier: 'channel1-1',
channel_id: channel1ID,
root_id: root1ID,
},
{
identifier: 'channel1-2',
channel_id: channel1ID,
root_id: root1ID,
},
{
identifier: 'channel1-3',
channel_id: channel1ID,
root_id: root2ID,
},
// Two channel2 delivered notifications
{
identifier: 'channel2-2',
channel_id: channel2ID,
},
{
identifier: 'channel2-2',
channel_id: channel2ID,
root_id: root3ID,
},
];
Notifications.setDeliveredNotifications(deliveredNotifications);
const notificationCount = deliveredNotifications.length;
expect(notificationCount).toBe(5);
// Clear channel1 notifications
await PushNotification.clearChannelNotifications(channel1ID, root1ID);
const deliveredNotifs = await Notifications.ios.getDeliveredNotifications();
expect(deliveredNotifs.length).toBe(3);
const channel1DeliveredNotifications = deliveredNotifs.filter((n) => n.channel_id === channel1ID);
const channel2DeliveredNotifications = deliveredNotifs.filter((n) => n.channel_id === channel2ID);
expect(channel1DeliveredNotifications.length).toBe(1);
expect(channel2DeliveredNotifications.length).toBe(2);
});
it('should clear all notifications', async () => {
@@ -63,7 +169,7 @@ describe('PushNotification', () => {
const cancelAllLocalNotifications = jest.spyOn(PushNotification, 'cancelAllLocalNotifications');
PushNotification.clearNotifications();
await expect(setApplicationIconBadgeNumber).toHaveBeenCalledWith(0);
expect(setApplicationIconBadgeNumber).toHaveBeenCalledWith(0);
expect(Notifications.ios.setBadgeCount).toHaveBeenCalledWith(0);
expect(cancelAllLocalNotifications).toHaveBeenCalled();
expect(Notifications.cancelAllLocalNotifications).toHaveBeenCalled();

View File

@@ -46,6 +46,8 @@ const NOTIFICATION_TYPE = {
interface NotificationWithChannel extends Notification {
identifier: string;
channel_id: string;
post_id: string;
root_id: string;
}
class PushNotifications {
@@ -61,6 +63,13 @@ class PushNotifications {
this.getInitialNotification();
}
getNotifications = async (): Promise<NotificationWithChannel[]> => {
if (Platform.OS === 'android') {
return AndroidNotificationPreferences.getDeliveredNotifications();
}
return Notifications.ios.getDeliveredNotifications() as Promise<NotificationWithChannel[]>;
}
cancelAllLocalNotifications() {
Notifications.cancelAllLocalNotifications();
}
@@ -75,34 +84,54 @@ class PushNotifications {
}
};
clearChannelNotifications = async (channelId: string) => {
if (Platform.OS === 'android') {
const notifications = await AndroidNotificationPreferences.getDeliveredNotifications();
const notificationForChannel = notifications.find((n: NotificationWithChannel) => n.channel_id === channelId);
if (notificationForChannel) {
AndroidNotificationPreferences.removeDeliveredNotifications(channelId);
}
} else {
const ids: string[] = [];
const notifications = await Notifications.ios.getDeliveredNotifications();
clearChannelNotifications = async (channelId: string, rootId?: string) => {
const notifications = await this.getNotifications();
//set the badge count to the total amount of notifications present in the not-center
let badgeCount = notifications.length;
let collapsedThreadsEnabled = false;
if (Store.redux) {
collapsedThreadsEnabled = isCollapsedThreadsEnabled(Store.redux.getState());
}
for (let i = 0; i < notifications.length; i++) {
const notification = notifications[i] as NotificationWithChannel;
if (notification.channel_id === channelId) {
ids.push(notification.identifier);
badgeCount--;
const clearThreads = Boolean(rootId);
const notificationIds: string[] = [];
for (let i = 0; i < notifications.length; i++) {
const notification = notifications[i];
if (notification.channel_id === channelId) {
let doesNotificationMatch = true;
if (clearThreads) {
doesNotificationMatch = notification.root_id === rootId;
} else if (collapsedThreadsEnabled) {
// Do not match when CRT is enabled BUT post is not a root post
doesNotificationMatch = !notification.root_id;
}
if (doesNotificationMatch) {
notificationIds.push(notification.identifier || notification.post_id);
// For Android, We just need one matching notification to clear the notifications
if (Platform.OS === 'android') {
break;
}
}
}
}
if (ids.length) {
Notifications.ios.removeDeliveredNotifications(ids);
}
if (Platform.OS === 'ios') {
//set the badge count to the total amount of notifications present in the not-center
const badgeCount = notifications.length - notificationIds.length;
this.setBadgeCountByMentions(badgeCount);
}
if (!notificationIds.length) {
return;
}
if (Platform.OS === 'android') {
AndroidNotificationPreferences.removeDeliveredNotifications(channelId, rootId, collapsedThreadsEnabled);
} else {
Notifications.ios.removeDeliveredNotifications(notificationIds);
}
}
setBadgeCountByMentions = (initialBadge = 0) => {

View File

@@ -105,7 +105,9 @@ Navigation.events().registerAppLaunchedListener(() => {
});
export function componentDidAppearListener({componentId}) {
EphemeralStore.addNavigationComponentId(componentId);
if (componentId.indexOf('!screen') !== 0) {
EphemeralStore.addNavigationComponentId(componentId);
}
switch (componentId) {
case 'MainSidebar':

View File

@@ -52,6 +52,12 @@ describe('componentDidAppearListener', () => {
expect(EventEmitter.emit).toHaveBeenCalledTimes(1);
expect(EventEmitter.emit).toHaveBeenCalledWith(NavigationTypes.BLUR_POST_DRAFT);
});
it('should not add componentIds starting with "!screen" to the store as they are not screens', () => {
const componentId = '!screen';
componentDidAppearListener({componentId});
expect(EphemeralStore.addNavigationComponentId).not.toHaveBeenCalledWith(componentId);
});
});
describe('componentDidDisappearListener', () => {

View File

@@ -4,4 +4,9 @@ import keyMirror from '@mm-redux/utils/key_mirror';
export default keyMirror({
RECEIVED_APP_BINDINGS: null,
RECEIVED_THREAD_APP_BINDINGS: null,
CLEAR_APP_BINDINGS: null,
CLEAR_THREAD_APP_BINDINGS: null,
RECEIVED_APP_COMMAND_FORM: null,
RECEIVED_APP_RHS_COMMAND_FORM: null,
});

View File

@@ -1,6 +1,8 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import CallsTypes from '@mmproducts/calls/store/action_types/calls';
import AppsTypes from './apps';
import BotTypes from './bots';
import ChannelCategoryTypes from './channel_categories';
@@ -43,4 +45,5 @@ export {
ThreadTypes,
RemoteClusterTypes,
AppsTypes,
CallsTypes,
};

View File

@@ -16,6 +16,30 @@ export function fetchAppBindings(userID: string, channelID: string): ActionFunc
return dispatch(bindClientFunc({
clientFunc: () => Client4.getAppsBindings(userID, channelID, teamID),
onSuccess: AppsTypes.RECEIVED_APP_BINDINGS,
onFailure: AppsTypes.CLEAR_APP_BINDINGS,
}));
};
}
export function fetchThreadAppBindings(userID: string, channelID: string): ActionFunc {
return async (dispatch: DispatchFunc, getState: GetStateFunc) => {
const channel = getChannel(getState(), channelID);
const teamID = channel?.team_id || '';
return dispatch(bindClientFunc({
clientFunc: async () => {
const bindings = await Client4.getAppsBindings(userID, channelID, teamID);
return {bindings, channelID};
},
onSuccess: AppsTypes.RECEIVED_THREAD_APP_BINDINGS,
onRequest: AppsTypes.CLEAR_THREAD_APP_BINDINGS,
}));
};
}
export function clearThreadAppBindings() {
return {
type: AppsTypes.CLEAR_THREAD_APP_BINDINGS,
data: true,
};
}

View File

@@ -1,6 +1,8 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import * as calls from '@mmproducts/calls/store/actions/calls';
import * as bots from './bots';
import * as channels from './channels';
import * as emojis from './emojis';
@@ -37,5 +39,6 @@ export {
timezone,
users,
remoteCluster,
calls,
};

View File

@@ -4,6 +4,7 @@
import {intlShape} from 'react-intl';
import {Alert} from 'react-native';
import {showModal} from '@actions/navigation';
import {handleSelectChannel, handleSelectChannelByName, loadChannelsByTeamName} from '@actions/views/channel';
import {makeDirectChannel} from '@actions/views/more_dms';
import {showPermalink} from '@actions/views/permalink';
@@ -146,6 +147,9 @@ export function handleGotoLocation(href: string, intl: typeof intlShape): Action
dispatch(makeGroupMessageVisibleIfNecessary(match.id));
dispatch(handleSelectChannel(match.id));
break;
case DeepLinkTypes.PLUGIN:
showModal('PluginInternal', match.id, {link: url});
break;
}
} else {
const {formatMessage} = intl;

View File

@@ -2,9 +2,10 @@
// See LICENSE.txt for license information.
/* eslint-disable max-lines */
import {updateThreadLastViewedAt} from '@actions/views/threads';
import {Client4} from '@client/rest';
import {WebsocketEvents} from '@constants';
import {THREAD} from '@constants/screen';
import {GLOBAL_THREADS, THREAD} from '@constants/screen';
import {analytics} from '@init/analytics';
import {PostTypes, ChannelTypes, FileTypes, IntegrationTypes} from '@mm-redux/action_types';
import {handleFollowChanged, updateThreadRead} from '@mm-redux/actions/threads';
@@ -443,11 +444,14 @@ export function setUnreadPost(userId: string, postId: string, location: string)
return {};
}
const collapsedThreadsEnabled = isCollapsedThreadsEnabled(state);
const isUnreadFromThreadScreen = collapsedThreadsEnabled && location === THREAD;
if (isUnreadFromThreadScreen) {
const isUnreadFromThread = collapsedThreadsEnabled && (location === THREAD || location === GLOBAL_THREADS);
if (isUnreadFromThread) {
const currentTeamId = getThreadTeamId(state, postId);
const threadId = post.root_id || post.id;
dispatch(handleFollowChanged(threadId, currentTeamId, true));
const actions: GenericAction[] = [];
actions.push(handleFollowChanged(threadId, currentTeamId, true));
actions.push(updateThreadLastViewedAt(threadId, post.create_at));
dispatch(batchActions(actions));
await dispatch(updateThreadRead(userId, threadId, post.create_at));
return {data: true};
}

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