Merge 4.20.0 into master (#3412)

* [FIX] App not prompting join code for password protected channels (#2514)

* Adding joinCode parameter

Co-authored-by: Vitor Leal <vitor_leal2201@hotmail.com>
Co-authored-by: Fernando Aguilar <fernando.aguilar@hotmail.com.br>

* Insert join code input

Signed-off-by: Vitor.Leal <vitor_leal2201@hotmail.com>

* Add joinCode field on db

Signed-off-by: Vitor.Leal <vitor_leal2201@hotmail.com>

* Add label i18 pt-br and en-us

Signed-off-by: Vitor.Leal <vitor_leal2201@hotmail.com>

* Add insert join code text

Signed-off-by: Vitor.Leal <vitor_leal2201@hotmail.com>

* Fix atribute name

Signed-off-by: Vitor.Leal <vitor_leal2201@hotmail.com>

* Add join text

Signed-off-by: Vitor.Leal <vitor_leal2201@hotmail.com>

Co-authored-by: Daniel Maike <danmke@hotmail.com>
Co-authored-by: Fernando Aguilar <fernando.aguilar@hotmail.com.br>

* Fix attributes joinCode, joinCodeRequired and pass attribute param in navigation

Signed-off-by: Daniel Maike <danmke@hotmail.com>

Co-authored-by: Vitor Leal <vitor_leal2201@hotmail.com>

* Fixing attribute joinCodeRequired pass to goRoom

Signed-off-by: Daniel Maike <danmke@hotmail.com>

* Changed textinput style

Signed-off-by: Daniel Maike <danmke@hotmail.com>

Co-authored-by: Vitor Leal <vitor_leal2201@hotmail.com>

* Delete not necessary attribute

Signed-off-by: Daniel Maike <danmke@hotmail.com>

* Fixing input style

Co-authored-by: Vitor Leal <vitor_leal2201@hotmail.com>

* Undo unncessary changes

* use a join code modal

* tests: e2e tests to join protected channel

* fix: undo unnecessary change

* tests: cancel join code

* Remove some tests

* Minor fixes

Co-authored-by: Vitor Leal <vitor_leal2201@hotmail.com>
Co-authored-by: Fernando Aguilar <fernando.aguilar@hotmail.com.br>
Co-authored-by: Djorkaeff Alexandre <djorkaeff.unb@gmail.com>
Co-authored-by: youssef-md <emaildeyoussefmuhamad@gmail.com>
Co-authored-by: Diego Mello <diegolmello@gmail.com>

* [I18n] Add Arabic (#2537)

* Arabic language setup

* Added arabic translation

* Arabic translation Proofreading

Co-authored-by: Diego Mello <diegolmello@gmail.com>

* [I18N] Add missing zh_TW and zh_CN strings (#2680)

* feat(i18n): add some missing strings and improve some translation

* fix: add missing server version

Co-authored-by: Diego Mello <diegolmello@gmail.com>

* [IMPROVEMENT] Add username on status messages (#2553)

* 1689 - missing user name for status messages

* 1689 - missing user name for status messages. Fixed broken e2e test "should pin message".

* Minor tweak

* Remove center style

* Small refactor on User

* Remove toLowerCase

* Update tests

Co-authored-by: Diego Mello <diegolmello@gmail.com>

* [FIX] Filenames are incorrect in non-latin alphabets on upload (#2671)

* fix: filename on react-native-image-crop-picker

* fix: use rn-fetch-blob to upload files

* fix: FileUpload as a service

* fix: cancel upload on iOS

* fix: file upload from share extension

Co-authored-by: Diego Mello <diegolmello@gmail.com>

* [IMPROVEMENT] Ease white labelling for Android (#2685)

* improve white labelling for Android

* Move application ID to gradle properties

* Fix CI

* Point foss sufix to main app

* Use npx on android-whitelabel script

Co-authored-by: Diego Mello <diegolmello@gmail.com>

* [FIX] Chats order (#2688)

* Persist highest value on subscription.roomUpdatedAt

* Update tests

Co-authored-by: Djorkaeff Alexandre <djorkaeff.unb@gmail.com>

* [REGRESSION] Re-enable Jitsi Chat (#2687)

* Fix main jitsi

* Fix iOS

* Clear build.gradle cache

* Don't restore gradle

* cache is back

* Use master

* Point to react-native-jitsi-meet#master

* [CHORE] Build official apps on CI (#2701)

* Duplicated target and changed Bridging Header

* Display name

* Unnecessary dumb swift file removed

* Buildable name

* Reorder Info.plist

* Rename Official target's bundle id

* Ignore .mobileprovision

* Fix provisioning of official app

* Starting signing

* stash fastfile

* starting official ci iOS

* Uncomment Fastfile keychain

* Fix CI config

* allowProvisioningUpdates

* Changing AppIcon and Splash Screen

* Remove unnecessary folder inside of Images.xcassets

* Reorder notificationservice and shareextension plists

* Fix signing

* Manual signing style for official

* Split official signing

* Update project provisioning

* Use ENV as profile

* Output match

* Keys

* TestFlight refactor

* Setting up android

* android-official-play-build job

* Start removing unnecessary fastlane tasks on Android

* Trying to refactor Android jobs

* android-env

* Remove foss build for now

* Fork

* Fix if conditions

* Fix push

* ios-build command

* Rename Android builds

* Upload dSYMs

* Refactoring workflow

* Reorder upload-to-testflight

* upload-to-google-play-beta command

* Fix ci

* Fix android fork build

* Fix keystore

* Fix options on fastlane android

* Fix keystore

* Check isOfficial on iOS

* Check isOfficial on db

* Remove unused imports

* Database names on Android

* Tag fix

* Minor fixes

* Set IS_OFFICIAL on CI

* Fix detox

* follow review suggestions

Co-authored-by: Djorkaeff Alexandre <djorkaeff.unb@gmail.com>

* [i18n] Update fr (#2697)

Co-authored-by: Diego Mello <diegolmello@gmail.com>

* [i18n] Update fr (#2705)

Typo

Co-authored-by: Diego Mello <diegolmello@gmail.com>

* [FIX] Empty space on Messagebox (#2704)

Co-authored-by: Diego Mello <diegolmello@gmail.com>

* [FIX] Yarn android scripts (#2716)

* [CHORE] Rename Experimental iOS lane (#2717)

* Move build_fork to the end

* Rename release to build_experimental

* [IMPROVEMENT] Use class variable instead of state for List's animated (#2718)

* [FIX] Bottom sheet being hidden sometimes (#2722)

* [IMPROVEMENT] Match background and text mention colors (#2723)

* [FIX] App freezing if Markdown preview contains sequential empty spaces (#2726)

* Remove sequential empty spaces from Markdown preview

* Use Markdown preview on RepliedThread

* [FIX] Official app without sharedUserId (#2734)

* [CHORE] Update React Native to 0.63.4 (#2737)

* Bump version to 4.13.1 (#2739)

* [REGRESSION] Multiple uploads not working on iOS (#2738)

* Update React Native to 0.63.4

* Fix multiple uploads not working on iOS

* [FIX] Unable to save attachment on iOS (#2743)

* Fix rn-fetch-blob's document dir without forward slash

* Update camera roll

* [FIX] Generate Jitsi access token when making a call (#2694)

fixes: #2693

 # Please enter the commit message for your changes. Lines starting

Co-authored-by: Diego Mello <diegolmello@gmail.com>

* [FIX] Jitsi notification delay (#2668)

Co-authored-by: Diego Mello <diegolmello@gmail.com>

* [FIX] Channels list not following the same sorting logic from web client (#2763)

* [FIX] Pods lost on Official target (#2764)

* [FIX] RoomItem using deprecated animated event signature (#2771)

* [FIX] Server autocomplete text breaking line (#2774)

* [FIX] ServerDropdown flashing bigger server icon (#2775)

* [FIX] ServerDropdown flashing bigger server icon

* Remove unused logo and update image path where needed

* Minor tweak

Co-authored-by: Diego Mello <diegolmello@gmail.com>

* [FIX] Rooms list not being updated on some cases (#2765)

* Request subscriptions on RoomsListView.constructor

* Removes opened rooms from last message persisting

* Change server reducer

* Prevent undefined ids causing query error

* [FIX] Share Extension hitting memory limit on iOS (#2788)

* [FIX] Disallow swipe to dismiss on share extension

* Limit query to 20 and clean up props

* Remove rn-extension-share branch pointer

* Test new branch

* Remove branch

* [IMPROVEMENT] Threads layout tweaks (#2686)

* improvement: Thread Details

* fix: re-render Thread Messages Item

* fix: update snapshots

* improve: thread details component

* fix: cast replies length

* improvement: format date of threads

* improvement: thread details styles

* fix: wrap text

* tests: update snapshot

* improvement: use same date format for all dates

* Icon size 24

* Remove date

* Remove prop drill

* Badge position

* Badge container tweak

* Fix inline style

* Move ThreadDetails to containers

* Update stories

* Fix lint

* Remove wrong prop

Co-authored-by: Diego Mello <diegolmello@gmail.com>

* [CHORE] Remove some migrations (#2792)

* Remove force rooms refresh

* Remove MMKV migration

* Bump version to 4.14.0 (#2797)

* [FIX] Messagebox tracking lost on pop gesture navigation (#2799)

* Use setTimeout instead of InteractionManager

* Update tracking lib

* [FIX] Back button closing activity when on root stack screen (#2804)

* Make hardware back button to behave as home button on root screens

* Remove unnecessary code

* Remove handleBackPress from OnboardingView

* Fix lint

* [i18n] Add missing German strings (#2715)

Co-authored-by: Diego Mello <diegolmello@gmail.com>

* [NEW] Encrypted Discussions  (#2813)

* I18n key fix

* Add encrypted switch

* Remove unused i18n keys

* Add enabled to encryption reducer

* Show encrypted option on CreateDiscussionView only when e2e encryption is properly set

* Add localSearch and use it on search

* Use encrypted from parent channel

* Fix method calls as rest api with 2fa enabled

* Fix logout after reset keys

* Use encryption reducer instead of lib directly to check render

* Check for room type logic to display encryption option on create discussion

* Check toggle-room-e2e-encryption permission on RoomActionsView

* Check for encryption status instead of setting on server

* Fix

* Disable switch instead of hide it

* Fix spotlight for DMs

* Fix server test

* [FIX] Messagebox missing style for text color (#2786)

* Changing auxilaryTintColor

* Changed Placeholder color to BodyText color

* added color prop

* eslint changes

* used array for styles

Co-authored-by: Diego Mello <diegolmello@gmail.com>

* [I18N] Update arabic (#2696)

* Update ar.js

* Update ar.js

Co-authored-by: Diego Mello <diegolmello@gmail.com>

* [FIX] Workspace input without i18n (#2689)

* [FIX] Translation of strings in Login page

* Strings are added for translation.

fixes: #2620

* Add pt-BR

Co-authored-by: Diego Mello <diegolmello@gmail.com>

* [FIX] Spotlight returning duplicated entries (#2805)

* Update rocketchat.js

* Updated search function

* Minor improvements

* Remove atIndex

* Add remove logic to remove duplicate data from response

Co-authored-by: Diego Mello <diegolmello@gmail.com>

* [CHORE] Refactor ServerItem (#2778)

* Updated ServerDropdown and ServerItem

* Added ServerItem stories

* Update ServerDropdown.js

* Updated ServerItem stories

* Updated ServerItem stories and ServerItem component

* Updated SelectServerView, ServerItem and ServerItem stories

* Updated ServerItem stories

* Updated ServerItem stories

* Update tests

Co-authored-by: Diego Mello <diegolmello@gmail.com>

* [DOCS] Updated Quick Start docs link in e2e/readme (#2802)

Co-authored-by: Diego Mello <diegolmello@gmail.com>

* [I18N] Add Turkish (#2793)

* Turkish language support added

* Update tr.js

Co-authored-by: Diego Mello <diegolmello@gmail.com>

* [FIX] Lint on #2793 (#2818)

* [I18N] Add missing german strings (#2689) (#2820)

* [I18N] Add missing italian strings (#2817)

* [FIX] Server version becoming null on server change (#2821)

* [FIX] Wrong styling on E2E encryption banner (#2767)

* [FIX] Wrong styling on E2E encryption banner

* [FIX] Wrong styling on E2E encryption banner

* [FIX] Wrong styling on E2E encryption banner

* [FIX] Wrong styling on E2E encryption banner (#2767)

* Updated SortDropdown, ListHeader, ListItem and added stories for List.Item

* Updated SortDropdown

* Removed unused component

* Updated List.Item and stories

* Reverted unnecessary changes and updated ListItem stories

* Fix minor indentation

* Stop breaking Touch's default underlay color

* Fix indentation

* Remove falsy comparison from render

* Fix left icon

* Use List.Item on OmnichannelStatus

* Add missing separator

* Lint

* Fix sort dropdown

* Remove unnecessary styles

* Fix detox

Co-authored-by: Diego Mello <diegolmello@gmail.com>

* [FIX] App Store using Experimental's app id (#2826)

* [FIX] Wrong username on push notifications (#2825)

* [FIX] Share extension memory issues on iOS (#2845)

* Remove unnecessary class prop

* Stop rendering servers when there's only one

* Map and alloc only necessary columns from query

* Fetch servers count instead of all servers records

* Fetch only needed servers

* Separators

* Remove renderContent

* Minor fix

* Refactor query

* Smaller avatars in memory

* Fix getItemLayout

* Add topic

* Load less pods

* tests

* Import only used functions from lodash

* Fix pods

* Import only used functions from semver

* Fix media sharing

* Update pods

* Disables preview and thumb on iOS

* Update expo-video-thumbnail

* Unnecessary change

* [FIX] Logout from other locations not prompting confirmation option (#2854)

* Fixed logout toast bug for the iOS

* Removing callToAction and replacing with confirmationText

Co-authored-by: Diego Mello <diegolmello@gmail.com>

* Bump version to 4.14.1 (#2859)

* [IMPROVEMENT] Check for focused rooms on in-app notifications (#2857)

* Update InAppNotification and room reducer

* Update InAppNotification

This reverts commit 60330a1e04cfe8d2e5aa311f367083d831682c49.

* Stop subscribing to threads

* Remove ref

* Fix prop-types

Co-authored-by: Diego Mello <diegolmello@gmail.com>

* [FIX] Real name being ignored in SearchMessagesView (#2838)

Co-authored-by: Gerzon Z <gerzonc@icloud.com>
Co-authored-by: Diego Mello <diegolmello@gmail.com>

* [CHORE] Remove unnecessary share reducer calls (#2861)

* Remove unnecesary share reducer calls

* Update Avatar

Co-authored-by: Diego Mello <diegolmello@gmail.com>

* [FIX] Breadcrumbs exceeding characters limit (#2862)

* [FIX] breadcrumbs exceeding

* fix.breadcrumbs-exceeding-change-events

Co-authored-by: Diego Mello <diegolmello@gmail.com>

* [FIX] App compressing videos on iOS (#2915)

* Update index.js

* Update index.js

* [FIX] Real name setting ignored on reply preview (#2908)

Co-authored-by: Diego Mello <diegolmello@gmail.com>

* [FIX] Reply component sending unused prop to Description (#2900)

Co-authored-by: Diego Mello <diegolmello@gmail.com>

* [CHORE] BackdropOpacity based on themes (#2863)

* Added backdropOpacity based on theme

* Updated ActionSheet, ReactionsModal, ReactionPicker and Sidebar

* Updated MultiSelect

Co-authored-by: Diego Mello <diegolmello@gmail.com>

* [FIX] Webview not falling back to default auth challenge when no cert is provided (#2918)

* [FIX] Android - fallback to default auth challenge handling when no cert is provided

* If a certificate auth challenge is requested on Android the webview will hang if no certificate is loaded.
  To prevent this, fallback to default Android behavior and cancel the challenge with request.cancel()

* No client certificate case defaults to super implementation

* Update react-native-webview

* Downgrade to previous dependency version

Co-authored-by: Diego Mello <diegolmello@gmail.com>
Co-authored-by: Gerzon Z <gerzonc@icloud.com>
Co-authored-by: Jan Garaj <jan.garaj@gmail.com>

* [FIX] Support Jitsi_URL_Room_Hash (#2905)

* [FIX] Temp attachment files not being flushed after saved to gallery (#2871)

* Update AttachmentView.js

* Update AttachmentView.js

* Update AttachmentView.js

* Update AttachmentView.js

Co-authored-by: Diego Mello <diegolmello@gmail.com>

* [CHORE] Update iOS profiles for Experimental app (#2933)

* [IMPROVE] Deleted thread reply redirects to thread (#2840)

Co-authored-by: Diego Mello <diegolmello@gmail.com>

* [FIX] Thread showing typing indicator from main room (#2869)

* [FIX] Remove typing indicator from thread's header

* remove unnecessary props and change usersTyping condition

Co-authored-by: Diego Mello <diegolmello@gmail.com>

* [FIX] DM rooms show typing status from last group room (#2878)

* [FIX] DM rooms show typing status from last group room

* Undo changes

* Check if current typing is from focused room before dispatching to redux

Co-authored-by: Diego Mello <diegolmello@gmail.com>

* [FIX] Can't copy or edit media's description (#2885)

* [FIX] Image descriptions issues

* shorten the condition string

* fix selectedMessage state

Co-authored-by: Diego Mello <diegolmello@gmail.com>

* [FIX] RightButtonsContainer re-render check not returning default value (#2899)

Co-authored-by: Diego Mello <diegolmello@gmail.com>

* [CHORE] Remove InteractionManager blocks (#2906)

* [FIX] Remove InteractionManager blocks

* Minor fix

Co-authored-by: Diego Mello <diegolmello@gmail.com>

* [FIX] App not sending second argument for EventEmitter.removeListener on some places (#2909)

Co-authored-by: Diego Mello <diegolmello@gmail.com>

* [FIX] Temp message ignoring real name (#2919)

Co-authored-by: Diego Mello <diegolmello@gmail.com>

* [FIX] System message of e2e encryption is missing (#2888)

* [FIX] System message of e2e encryption missing

* add new encryption string

* add to stories

* Add pt-BR

* Move stories

Co-authored-by: Diego Mello <diegolmello@gmail.com>

* [CHORE] Add permissions to Redux (#2914)

* [FIX] Add permissions to Redux store

* add only permissions being used in the app

* add clear permissions reducer

* call RocketChat.hasPermission from reducer

* add server version comparison on getPermissions

* refactor hasPermission function

* refactor hasPermission function

* remove uncomment code

* use Q.experimentalSortBy()

* add coerce function

* Change Rocketchat.hasPermission

* Apply on isReadOnly

* Apply to RoomInfoEditView

* Apply to RoomInfoView and RoomInfoEditView

* canAutoTranslate

* Unnecessary clear permissions

* Revert getUpdatedSince

* Naming fix

Co-authored-by: Diego Mello <diegolmello@gmail.com>

* [CHORE] Add hold step for ios and android build experimental (#2943)

* [CHORE] Add hold step for ios-build-experimental and android-build-experimental

* Android hold step

* add ios hold step

Co-authored-by: Diego Mello <diegolmello@gmail.com>

* [IMPROVEMENT] Remove lodash.isEqual (#2893)

* Added dequal and react-fast-compare as substitutes to lodash.isEqual

* Update ReplyPreview.js

* Remove react-fast-compare

* Removed deep-equal and upgrade babel-eslint dev dependency

* Fix avatar

* Fix Messagebox

* Fix CreateDiscussionView

* ModalBlockView

* NewMessageView

* ProfileView

* RoomInfoEditView

* ServerDropdown

* Return local search as object instead of observable

* SelectedUsersView

Co-authored-by: Diego Mello <diegolmello@gmail.com>

* [I18N] Add missing Russian strings (#2946)

* [i18n] Add missing Russian strings

* Couple fixes

* Fix Direct_message

Translate Direct_message as already has been translated

Co-authored-by: Diego Mello <diegolmello@gmail.com>

* [CHORE] Use shortcut syntax for get collections (#2932)

Co-authored-by: Diego Mello <diegolmello@gmail.com>

* [FIX] Use List.Separator in all places (#2931)

* [FIX] Use List.Separator in all places

* add List.Separator

* change List.Separator

Co-authored-by: Diego Mello <diegolmello@gmail.com>

* [FIX] Limit new message list query size to 50 (#2947)

* Limit query to 50

* Remove observable

* [FIX] Support chats order for older versions of the server (#2934)

* Update mergeSubscriptionsRooms.js

* Update mergeSubscriptionsRooms.js

* Update mergeSubscriptionsRooms.js

* Minor refactor

Co-authored-by: Diego Mello <diegolmello@gmail.com>

* [FIX] Reactions modal's backdrop color too light (#2949)

Co-authored-by: Diego Mello <diegolmello@gmail.com>

* Bump version to 4.15.0 (#2950)

* [FIX] Share extension not working correctly on Official app (#2963)

* [FIX] Cannot read property 'some' of undefined on hasPermission (#2966)

Co-authored-by: Diego Mello <diegolmello@gmail.com>

* [FIX] Deep linking and other connectivity issues (#2894)

* Navigate from push notification only if necessary

* Use JS SDK branch

* Stop reconnecting if it's already connected

* Fix RoomsListView forever loading after tapping push notification of another server

* Execute fewer operations on app/index

* Remove roomsRequest call from onForeground

* Apply check and reopen

* Stop opening in-app notification when the app is on backgorund

* Connecting tweaks

* Fix deep linking not working if the app is on background

* Force reset yarn cache

* Upgrade JS SDK

* Remove listener on unmount

* Fix resume on Android after back button is pressed

* Fix local authentication resume

* Fix back button android

* Change JS SDK branch

* [FIX] Messagebox's placeholder color is too bright (#2968)

* [IMPROVEMENT] Message attachment colors (#2860)

* Added convertStrToHex function and updated Reply component

* Removed convertStrtToHex function and added attachmentBackground

* Added color2k, removed transparent view and applied transparentize to backgroundColor

* Added stories

* Update Reply stories

* Update Reply stories

* Fix lint

* Update Reply stories

* Fix props

* Move tests to Message stories

Co-authored-by: Diego Mello <diegolmello@gmail.com>

* [FIX] App forgetting workspace when server is not finished added (#2798)

* [FIX] App forgetting workspace

* Added e2e tests

* Update login.js

* Update logout.js

* Reverted changes on login and share, updated init

* Update 08-persistantworkspace.spec.js

* Revert unnecessary changes

* Revert line change

* Update share.js

* Tweak tests

* Use wm shorthand

* Remove irrelevant calls to RocketChat.TOKEN_KEY

Co-authored-by: Diego Mello <diegolmello@gmail.com>

* [TESTS] Add E2E tests to draft message (#2960)

* [E2E TEST] Draft message

* Fix tests

Co-authored-by: Diego Mello <diegolmello@gmail.com>

* [TESTS] Add E2E tests to group DM (#2961)

Co-authored-by: Diego Mello <diegolmello@gmail.com>

* [TESTS] Add E2E tests to directory (#2964)

* [E2E TEST] Directory

* Fix tests

Co-authored-by: Diego Mello <diegolmello@gmail.com>

* [CHORE] Simplify server version comparison (#2922)

* Simplify server version where needed

* Added lte and gte functions and updated imports

* Updated functions names

* Update util functions

* Update util function and added methods

* Remove lt and coerce from getPermissions and mergeSubscriptionsRooms

* Fix comparison

* Update getPermissions.js

* Remove unused import

* Fix lint

* Fix lint

Co-authored-by: Diego Mello <diegolmello@gmail.com>

* [TESTS] Add E2E tests to discussions (#2970)

* [E2E TEST] Discussions

* fix error Cannot find UI elemen

* Fix tests

Co-authored-by: Diego Mello <diegolmello@gmail.com>

* [FIX] Attachment not rendering markdown (#2924)

* [FIX] Render markdown in Fields content

* Added stories

Co-authored-by: Diego Mello <diegolmello@gmail.com>

* [TESTS] Add e2e tests for mark message as unread (#2953)

* [E2E TEST] Add e2e tests for mark message as unread

* fixed test for draft message

* change test name

* move test to other file

* Remove unnecessary tests

* Rename file

* Update jest tests

Co-authored-by: Diego Mello <diegolmello@gmail.com>

* [TESTS] Add E2E tests to delete server (#2954)

* [E2E TEST] Delete server

* fixed test for delete server

* fix tests

* minor changes

* Rename file

Co-authored-by: Diego Mello <diegolmello@gmail.com>

* [CHORE] Refactor RoomActionsView permissions (#2872)

Co-authored-by: Diego Mello <diegolmello@gmail.com>

* [CHORE] Add status and teams icons (#2989)

Co-authored-by: Gerzon Z <gerzonc@icloud.com>

* [FIX] SSO not working with 2FA (TOTP) (#2978)

* Update AuthenticationWebView.js

* Updated loginTOTP

* Added validation

* Update rocketchat.js

* Update rocketchat.js

* Update rocketchat.js

* Update rocketchat.js

* Fix resolve

* Remove incognito

* Fix totp being requested on webview

Co-authored-by: Diego Mello <diegolmello@gmail.com>

* [IMPROVEMENT] User status icons (#2991)

* Add status and teams

* Update icons, icon size and getUsersPresence

* Minor changes

* Refactor RoomTypeIcon

* Minor tweaks

* Update unit tests

* Minor fixes

* Fix styles

* Small refactor

* Update jest

Co-authored-by: Diego Mello <diegolmello@gmail.com>

* [REGRESSION] Auth via deep linking not working (#3015)

* Update rocketchat and add e2e test for deep linking

* Update rocketchat and add e2e test for deep linking

* Update deeplinking e2e

* fix deep linking auth

* Test deep linking auth

* Fix deeplink to rid and add tests

* Small refactor

* Add non existing server test

Co-authored-by: Diego Mello <diegolmello@gmail.com>

* [FIX] Create discussion request being sent with null value on encryption param (#3033)

* [CHORE] Use JSON files for i18n (#3011)

* [IMPROVEMENT] Load only i18n files needed (#3014)

* Use json

* Load only i18n files needed

* [REGRESSION] Clear local server cache not loading rooms (#3007)

* Fix clear cache

* Write e2e tests

* Fix lint

* Fix isRTL

* [FIX] Custom OAuth and iframe login attempts being called multiple times (#3020)

* [FIX] App crashing when attachment color is an invalid HEX (#3021)

* [IMPROVEMENT] Add "Message" option to Room Info (#3029)

* [CHORE] Go to room from hashtag

* Layout tweaks

Co-authored-by: Diego Mello <diegolmello@gmail.com>

* [FIX] Can't change status (#3018)

Co-authored-by: Diego Mello <diegolmello@gmail.com>

* [FIX] Search input not using the whole header space (#3012)

* [FIX] Search input not using the whole space

* Fix on getHeaderTitlePosition

Co-authored-by: Diego Mello <diegolmello@gmail.com>

* [FIX] E2EE password hiding automatically (#2972)

* [FIX] E2EE password hiding automatically

* add e2e test

* fixed hiding banner

* move e2e tests to 01-e2eencryption

* remove console.log

* Fix tests

Co-authored-by: Diego Mello <diegolmello@gmail.com>

* [TESTS] Move threads tests to its own file (#2965)

* [E2E TEST] Move threads test to another file

* changed descirbe title

* Rearrange files

Co-authored-by: Diego Mello <diegolmello@gmail.com>

* [FIX] Regex typo on markdown (#2928)

* [FIX] Fix Regex Typo

* Add story for testing

Co-authored-by: Diego Mello <diegolmello@gmail.com>

* [FIX] Make attachment validation compatible with web client (#2927)

* [FIX] Make attachment validation compatible with web client

* Added stories

Co-authored-by: Diego Mello <diegolmello@gmail.com>

* [FIX] Non-reply attachments displaying time (#2902)

* Remove time if no message_link

* Fix message stories for replies

* Final stories fix

Co-authored-by: Diego Mello <diegolmello@gmail.com>

* [FIX] i18n not being applied on login/register labels (#2930)

* Use I18n translate in login text input label

* Add to register and add missing strings

Co-authored-by: Diego Mello <diegolmello@gmail.com>

* Revert "[FIX] Make attachment validation compatible with web client (#2927)" (#3036)

This reverts commit d6200745c0.

* Bump version to 4.16.0 (#3037)

* [NEW] Basic support to Teams (#3016)

* Database migration

* RoomItem icon

* Team icons

* Teams group

* Small tweak on RoomTypeIcon

* RoomView Header

* Add team's channels to RoomView header

* Starting TeamChannelsView

* Icon size

* o data found

* Update TeamChannelsView, add teams subscriptions and send params to TeamChannelsView

* Use teams.ListRooms endpoint, render rooms list, remove unused functions

* Show team main on TeamChannelsView

* Disable swipe

* Pagination working

* Fix blinking no data found

* Search working

* Refactor to use BackgroundContainer while loading

* Go to room

* Cleanup

* Go to actions

* Events

* Lint

* Add debounce to go room

* Fix for tablet

* i18n

* Small fix

* Minor refactor

* Use local data when it exists

* Show last message

* Force teams migration

* Add stories to BackgroundContainer

* Remove unused component

* Move RoomViewHeader into containers folder

* Refactoring

* Testing RoomHeader

* i18n

* Fix server endpoint version

* Fix events

Co-authored-by: Gerzon Z <gerzonzcanario@gmail.com>

* [CHORE] Refactor mention tracking logic (#2997)

* [Improvement] Improve mentions

This PR focuses on improving command, emoji, channel and user mentions.

* [Tests] Added e2e tests for mention improvement

* [Improvement] Modify slash command mention logic.
Added slash command with argument preview
Slash command should show only if the message bigins with /

* Return data on search for empty text

* Minor fixes

* Update e2e tests

* Minor fix

* [FIX] allow command mentioning in between text

Co-authored-by: Diego Mello <diegolmello@gmail.com>

* [FIX] Status text not being updated on sidebar (#3041)

* Update StatusView.js

* Minor tweak

* Minor tweaks

Co-authored-by: Diego Mello <diegolmello@gmail.com>

* [FIX] Unable to search non-latin alphabet names on members list (#3039)

* Add search by name in members list

* Update RoomMembersView search

Co-authored-by: Diego Mello <diegolmello@gmail.com>

* Search stops working after some time (#3044)

* Bump version to 4.17.0 (#3058)

* [CHORE] Add job to upload Experimental to Google Play production (#3050)

* [REGRESSION] SAML stopped working after #2978 (#3060)

* [REGRESSION] Room actions not loading on tablet (#3061)

* Bump version to 4.16.1 (#3063)

* [REGRESSION] Fallback language stopped working (#3072)

* [CHORE] Update Detox to 18.10.0 (#3052)

* Updated detox and 5 tests

* Update e2e cases for Detox v18, update setUserStatus and added SET_STATUS_FAIL

* Downgrade mocha

* Exclude arm64 from building and update tests cases

* Update more tests cases, add registeringUser4

* Update more test files and add room-actions-scrollview testID

* Update package.json

* Remove unused username from test file and update 08-roominfo test file

* Fixing

* Mark as unread

* Fixing flaky tests

* Minor fixes

Co-authored-by: Diego Mello <diegolmello@gmail.com>

* [FIX] Message author touchable taking whole space available (#3048)

Co-authored-by: Gerzon Z <gerzonc@icloud.com>

* [CHORE] Improve stories (#3028)

* [CHORE] Improve stories

* Refactor Avatar and UIKitModal

* fixed undefined 'name'

* Remove commented stories

* Remove Markdown from stories/index, update Markdown test file and remove markdown stories from Message stories

* Remove StoriesSeparator

* Refactor Markdown

* Remove commented lines of code

* Small refactor

* Re-add stories

Co-authored-by: Gerzon Z <gerzonzcanario@gmail.com>
Co-authored-by: Gerzon Z <gerzonc@icloud.com>
Co-authored-by: Diego Mello <diegolmello@gmail.com>

* Bump version to 4.17.0 (#3083)

* [REGRESSION] Fallback not working when device's language is available (#3091)

* Always add 'en' i18n

* Add tests

* Bump version to 4.16.2 (#3092)

* [FIX] Connecting stream listener not being cleared (#3008)

Co-authored-by: Diego Mello <diegolmello@gmail.com>

* [FIX] App making calls to DDP after socket was killed by OS (#3062)

Co-authored-by: Gerzon Z <gerzonc@icloud.com>

* [NEW] Create Team (#3082)

Co-authored-by: Diego Mello <diegolmello@gmail.com>

* Language update from LingoHub 🤖 (#3139)

Project Name: Rocket.Chat.ReactNative
Project Link: https://translate.lingohub.com/rocketchat/dashboard/rocket-dot-chat-dot-reactnative
User: Robot LingoHub

Easy language translations with LingoHub 🚀

Co-authored-by: Robot LingoHub <robot@lingohub.com>

* [NEW] Add/Create/Remove channel on a team (#3090)

* Added Create Team

* Added actionTypes, actions, ENG strings for Teams and updated NewMessageView

* Added createTeam sagas, createTeam reducer, new Team string and update CreateChannelView

* Remove unnecessary actionTypes, reducers and sagas, e2e tests and navigation to team view

* Minor tweaks

* Show TeamChannelsView only if joined the team

* Minor tweak

* Added AddChannelTeamView

* Added permissions, translations strings for teams,  deleteTeamRoom and addTeamRooms, AddExistingChannelView, updated CreateChannelView, TeamChannelsView

* Refactor touch component and update removeRoom and deleteRoom methods

* Minor tweaks

* Minor tweaks for removing channels and addExistingChannelView

* Added missing events and fixed channels list

* Minor tweaks for refactored touch component

* Minor tweaks

* Remove unnecesary changes, update TeamChannelsView, AddExistingChannelView, AddChannelTeamView, createChannel, goRoom and Touchable

* Add screens to ModalStack, events, autoJoin, update createChannel, addRoomsToTeam and Touchable

* Minor tweak

* Update loadMessagesForRoom.js

* Updated schema, tag component, touch, AddChannelTeamView, AddExistingChannelView, ActionSheet Item

* Fix unnecessary changes

* Add i18n, update createChannel, AddExistingChannelTeamView, AddChannelTeamView, RightButton and TeamChannelsView

* Updated styles, added tag story

* Minor tweak

* Minor tweaks

* Auto-join tweak

* Minor tweaks

* Minor tweak on search

* One way to refactor :P

* Next level refactor :)

* Fix create group dm

* Refactor renderItem

* Minor bug fixes

* Fix stories

Co-authored-by: Diego Mello <diegolmello@gmail.com>

* [FIX] E2E Tests not working because of ES6 import (#3147)

* Update ITeam.js

* Minor tweak

* [NEW] Leave Teams (#3116)

* Added Create Team

* Added actionTypes, actions, ENG strings for Teams and updated NewMessageView

* Added createTeam sagas, createTeam reducer, new Team string and update CreateChannelView

* Remove unnecessary actionTypes, reducers and sagas, e2e tests and navigation to team view

* Minor tweaks

* Show TeamChannelsView only if joined the team

* Minor tweak

* Added AddChannelTeamView

* Added permissions, translations strings for teams,  deleteTeamRoom and addTeamRooms, AddExistingChannelView, updated CreateChannelView, TeamChannelsView

* Refactor touch component and update removeRoom and deleteRoom methods

* Minor tweaks

* Minor tweaks for removing channels and addExistingChannelView

* Added missing events and fixed channels list

* Minor tweaks for refactored touch component

* Added SelectListView and logic for leaving team

* Minor tweak

* Minor tweak

* Minor tweaks

* Remove unnecesary changes, update TeamChannelsView, AddExistingChannelView, AddChannelTeamView, createChannel, goRoom and Touchable

* Remove unnecesary prop

* Add screens to ModalStack, events, autoJoin, update createChannel, addRoomsToTeam and Touchable

* Minor tweak

* Update loadMessagesForRoom.js

* Updated schema, tag component, touch, AddChannelTeamView, AddExistingChannelView, ActionSheet Item

* Fix unnecessary changes

* Add i18n, update createChannel, AddExistingChannelTeamView, AddChannelTeamView, RightButton and TeamChannelsView

* Updated styles, added tag story

* Minor tweak

* Minor tweaks

* Auto-join tweak

* Minor tweaks

* Minor tweak on search

* Minor refactor to ListItem, add SelectListView to ModalStack, update handleLeaveTeam

* Minor tweaks

* Update SelectListView

* Update handleLeaveTeam, remove unnecessary method, add story

* Minor tweak

* Minor visual tweaks

* Updated SelectListView, RoomActionsView, leaveTeam method and string translations

* Update SelectListVIew

* Minor tweak

* Update SelectListView

* Minor tweak

* Fix for List.Item subtitles being pushed down by title's flex

* Minor tweaks

* Update RoomActionsView

* Use showConfirmationAlert and showErrorAlert

* Lint

Co-authored-by: Diego Mello <diegolmello@gmail.com>

* [NEW] Jump to message (#3099)

* Scrolling

* Add loadMore button at the end of loadMessagesForRoom

* Delete dummy item on tap

* Only insert loadMore dummy if there's more data

* load surrounding messages

* fixes and load next

* First dummy and dummy-next

* Save load next messages

* Check if message exists before fetching surroundings

* Refactoring List

* Jumping to message :)

* Showing blocking loader while scrolling/fetching message

* Check if message exists on local db before inserting dummy

* Delete dummies automatically when the message sent to updateMessages again

* Minor cleanup

* Fix scroll

* Highlight message

* Jump to bottom

* Load more on scroll

* Adding stories to LoadMore

* Refactoring

* Add loading indicator to LoadMore

* Small refactor

* Add LoadMore to threads

* getMoreMessages

* chat.getThreadMessages -> getThreadMessages

* Start jumping to threads

* Add jumpToMessageId on RoomView

* Nav to correct channel

* Fix PK issue on thread_messages

* Disable jump to thread from another room

* Fix nav to thread params

* Add navToRoom

* Refactor styles

* Test notch

* Fix Android border

* Fix thread message on title

* Fix NavBottomFAB on threads

* Minor cleanup

* Workaround for readThreads being called too often

* Lint

* Update tests

* Jump from search

* Go to threads from search

* Remove getItemLayout and rely on viewable items

* Fix load older

* stash working

* Fix infinite loading

* Lower itemVisiblePercentThreshhold to 10, so very long messages behave as viewable

* Add generateLoadMoreId util

* Minor cleanup

* Jump to message from notification/deep linking

* Add getMessageInfo

* Nav to threads from other rooms

* getThreadName

* Unnecessary logic

* getRoomInfo

* Colocate getMessageInfo closer to RoomView

* Minor cleanup

* Remove search from RoomActionsView

* Minor fix for search on not joined public channels

* Jump to any link

* Fix tablets

* Jump to message from MessagesView and other bug fixes

* Fix issue on Urls

* Adds race condition to cancel jump to message if it's stuck or after 5 seconds

* Jump from message search quote

* lint

* Stop onPress

* Small refactor on load methods

* Minor fixes for loadThreadMessages

* Minor typo

* LoadMore i18n

* Minor cleanup

* [FIX] Method calls not sending date params as EJSON (#3159)

* [FIX] Read receipt not displaying full date (#3133)

Co-authored-by: Diego Mello <diegolmello@gmail.com>

* [NEW] Remove member from team (#3117)

* Added Create Team

* Added actionTypes, actions, ENG strings for Teams and updated NewMessageView

* Added createTeam sagas, createTeam reducer, new Team string and update CreateChannelView

* Remove unnecessary actionTypes, reducers and sagas, e2e tests and navigation to team view

* Minor tweaks

* Show TeamChannelsView only if joined the team

* Minor tweak

* Added AddChannelTeamView

* Added permissions, translations strings for teams,  deleteTeamRoom and addTeamRooms, AddExistingChannelView, updated CreateChannelView, TeamChannelsView

* Refactor touch component and update removeRoom and deleteRoom methods

* Minor tweaks

* Minor tweaks for removing channels and addExistingChannelView

* Added missing events and fixed channels list

* Minor tweaks for refactored touch component

* Added SelectListView and logic for leaving team

* Added addTeamMember and removeTeamMember

* Minor tweak

* Minor tweak

* Minor tweaks

* Remove unnecesary changes, update TeamChannelsView, AddExistingChannelView, AddChannelTeamView, createChannel, goRoom and Touchable

* Remove unnecesary prop

* Add screens to ModalStack, events, autoJoin, update createChannel, addRoomsToTeam and Touchable

* Minor tweak

* Update loadMessagesForRoom.js

* Updated schema, tag component, touch, AddChannelTeamView, AddExistingChannelView, ActionSheet Item

* Fix unnecessary changes

* Add i18n, update createChannel, AddExistingChannelTeamView, AddChannelTeamView, RightButton and TeamChannelsView

* Updated styles, added tag story

* Minor tweak

* Minor tweaks

* Auto-join tweak

* Minor tweaks

* Minor tweak on search

* Minor refactor to ListItem, add SelectListView to ModalStack, update handleLeaveTeam

* Minor tweaks

* Update SelectListView

* Update handleLeaveTeam, remove unnecessary method, add story

* Minor tweak

* Minor visual tweaks

* Update SelectListView.js

* Update RoomMembersView

* Updated SelectListView, RoomActionsView, leaveTeam method and string translations

* Update SelectListVIew

* Minor tweak

* Update SelectListView

* Minor tweak

* Minor tweaks

* Fix for List.Item subtitles being pushed down by title's flex

* Minor tweaks

* Update RoomActionsView

* Use showConfirmationAlert and showErrorAlert

* Remove addTeamMember, update removeTeamMember

* Update Alert

* Minor tweaks

* Minor tweaks

* Minor tweak

* Update showActionSheet on RoomMembersView

* Remove team main from query and move code around

* Fetch roles

* Update RoomMembersView and SelectListView

* Updated leaveTeam and handleRemoveFromTeam

* Fix validation

* Remove unnecessary function

* Added confirmationAlert for missing permissions case

Co-authored-by: Diego Mello <diegolmello@gmail.com>

* [FIX] Add Existing Channel screen showing discussions and channels without permission (#3151)

* [Fix] the filter to show the existing channel

* [Refactor] the function that filter to isolate it

* Refactor how to wsearch properly

Co-authored-by: Diego Mello <diegolmello@gmail.com>

* [FIX] Member search not trimming search text (#3129)

* Fixed logout toast bug for the iOS

* Removing callToAction and replacing with confirmationText

* Handling member search with spaces to the left and right of name/username

* Changing location of string trimmer

Co-authored-by: Diego Mello <diegolmello@gmail.com>

* [FIX] Discussions not subscribing properly to messages when opened from inside the room (#3149)

* [FIX] Promise at subscription Room

* E2E - Update previous roomView count after send msg in discussion

* Not needed rn

Co-authored-by: Diego Mello <diegolmello@gmail.com>

* [FIX] Team creation not raising error if something unexpected happens (#3152)

* [IMPROVEMENT] Add error to AddExistingChannel

* Fix the alert error when create a channel

* Fix the error alert box when create channel and teams

Co-authored-by: Diego Mello <diegolmello@gmail.com>

* [FIX] Check permissions on team channels action sheet (#3155)

* [IMPROVEMENT] Show only the option that user can manage in TeamChannelsView

* Refactor the showActionSheet function

* Added remove team channel permission

* Cleanup

Co-authored-by: Diego Mello <diegolmello@gmail.com>

* [FIX] Add channels to team's flow using different navigators (#3157)

* [FIX] the navigation to AddChannelTeamView and next screens

* Fix the order inside the NewMessageStackNavigator

* Delete spaces after arrow function in onPress

* Adjusted InsideStackNavigator to a conditional animation

* Fixed route for iPad

* Small change

Co-authored-by: Diego Mello <diegolmello@gmail.com>

* [IMPROVEMENT] Allow discussions to be edited (#3137)

* Refactored the filter to work the edit for channel and discussion

* Removed the filter which type of room can be edit

Co-authored-by: Diego Mello <diegolmello@gmail.com>

* Fix tests

Co-authored-by: Diego Mello <diegolmello@gmail.com>

* [NEW] Delete Teams (#3123)

* Added Create Team

* Added actionTypes, actions, ENG strings for Teams and updated NewMessageView

* Added createTeam sagas, createTeam reducer, new Team string and update CreateChannelView

* Remove unnecessary actionTypes, reducers and sagas, e2e tests and navigation to team view

* Minor tweaks

* Show TeamChannelsView only if joined the team

* Minor tweak

* Added AddChannelTeamView

* Added permissions, translations strings for teams,  deleteTeamRoom and addTeamRooms, AddExistingChannelView, updated CreateChannelView, TeamChannelsView

* Refactor touch component and update removeRoom and deleteRoom methods

* Minor tweaks

* Minor tweaks for removing channels and addExistingChannelView

* Added missing events and fixed channels list

* Minor tweaks for refactored touch component

* Added SelectListView and logic for leaving team

* Added addTeamMember and removeTeamMember

* Minor tweak

* Added deleteTeam function

* Minor tweak

* Minor tweaks

* Remove unnecesary changes, update TeamChannelsView, AddExistingChannelView, AddChannelTeamView, createChannel, goRoom and Touchable

* Remove unnecesary prop

* Add screens to ModalStack, events, autoJoin, update createChannel, addRoomsToTeam and Touchable

* Minor tweak

* Update loadMessagesForRoom.js

* Updated schema, tag component, touch, AddChannelTeamView, AddExistingChannelView, ActionSheet Item

* Fix unnecessary changes

* Add i18n, update createChannel, AddExistingChannelTeamView, AddChannelTeamView, RightButton and TeamChannelsView

* Updated styles, added tag story

* Minor tweak

* Minor tweaks

* Auto-join tweak

* Minor tweaks

* Minor tweak on search

* Minor refactor to ListItem, add SelectListView to ModalStack, update handleLeaveTeam

* Minor tweaks

* Update SelectListView

* Update handleLeaveTeam, remove unnecessary method, add story

* Minor tweak

* Minor visual tweaks

* Update SelectListView.js

* Update index.js

* Update RoomMembersView

* Updated SelectListView, RoomActionsView, leaveTeam method and string translations

* Update SelectListVIew

* Minor tweak

* Update SelectListView

* Minor tweak

* Minor tweaks

* Fix for List.Item subtitles being pushed down by title's flex

* Minor tweaks

* Update RoomActionsView

* Use showConfirmationAlert and showErrorAlert

* Remove addTeamMember, update removeTeamMember

* Update Alert

* Minor tweaks

* Minor tweaks

* Minor tweak

* Update showActionSheet on RoomMembersView

* Remove team main from query and move code around

* Fetch roles

* Update RoomMembersView and SelectListView

* Update rocketchat.js

* Updated leaveTeam and handleRemoveFromTeam

* Fix validation

* Remove unnecessary function

* Update RoomActionsView

* Update en.json

* updated deleteTeam function and permissions

* Added showConfirmationAlert

* Added string translations for teams

* Fix permission

* Minor tweaks

* Typo

Co-authored-by: Diego Mello <diegolmello@gmail.com>

* [FIX] Android navigation bar color when Loading modal appears (#3165)

* [FIX] Modal appearance

* Undo and only add android:navigationBarColor

Co-authored-by: Diego Mello <diegolmello@gmail.com>

* [FIX] Check for old servers for Teams (#3171)

Co-authored-by: Diego Mello <diegolmello@gmail.com>

* [NEW] Convert/Move Channel to Team (#3164)

* Added Create Team

* Added actionTypes, actions, ENG strings for Teams and updated NewMessageView

* Added createTeam sagas, createTeam reducer, new Team string and update CreateChannelView

* Remove unnecessary actionTypes, reducers and sagas, e2e tests and navigation to team view

* Minor tweaks

* Show TeamChannelsView only if joined the team

* Minor tweak

* Added AddChannelTeamView

* Added permissions, translations strings for teams,  deleteTeamRoom and addTeamRooms, AddExistingChannelView, updated CreateChannelView, TeamChannelsView

* Refactor touch component and update removeRoom and deleteRoom methods

* Minor tweaks

* Minor tweaks for removing channels and addExistingChannelView

* Added missing events and fixed channels list

* Minor tweaks for refactored touch component

* Added SelectListView and logic for leaving team

* Added addTeamMember and removeTeamMember

* Minor tweak

* Added deleteTeam function

* Minor tweak

* Minor tweaks

* Remove unnecesary changes, update TeamChannelsView, AddExistingChannelView, AddChannelTeamView, createChannel, goRoom and Touchable

* Remove unnecesary prop

* Add screens to ModalStack, events, autoJoin, update createChannel, addRoomsToTeam and Touchable

* Minor tweak

* Update loadMessagesForRoom.js

* Updated schema, tag component, touch, AddChannelTeamView, AddExistingChannelView, ActionSheet Item

* Fix unnecessary changes

* Add i18n, update createChannel, AddExistingChannelTeamView, AddChannelTeamView, RightButton and TeamChannelsView

* Updated styles, added tag story

* Minor tweak

* Minor tweaks

* Auto-join tweak

* Minor tweaks

* Minor tweak on search

* Minor refactor to ListItem, add SelectListView to ModalStack, update handleLeaveTeam

* Minor tweaks

* Update SelectListView

* Update handleLeaveTeam, remove unnecessary method, add story

* Minor tweak

* Minor visual tweaks

* Update SelectListView.js

* Update index.js

* Update RoomMembersView

* Updated SelectListView, RoomActionsView, leaveTeam method and string translations

* Update SelectListVIew

* Minor tweak

* Update SelectListView

* Minor tweak

* Minor tweaks

* Fix for List.Item subtitles being pushed down by title's flex

* Minor tweaks

* Update RoomActionsView

* Use showConfirmationAlert and showErrorAlert

* Remove addTeamMember, update removeTeamMember

* Update Alert

* Minor tweaks

* Minor tweaks

* Minor tweak

* Update showActionSheet on RoomMembersView

* Remove team main from query and move code around

* Fetch roles

* Update RoomMembersView and SelectListView

* Update rocketchat.js

* Updated leaveTeam and handleRemoveFromTeam

* Fix validation

* Remove unnecessary function

* Update RoomActionsView

* Update en.json

* updated deleteTeam function and permissions

* Added showConfirmationAlert

* Added string translations for teams

* Fix permission

* Added moveChannelToTeam and convertToTeam functionality

* Fix SelectListView RadioButton

* Fix moveToTeam

* Added searchBar to SelectListVIew

* Update RoomView , SelectListVIew and string translation for error

Co-authored-by: Diego Mello <diegolmello@gmail.com>

* [TEST] E2E Tests for Teams (#3178)

* Added Create Team

* Added actionTypes, actions, ENG strings for Teams and updated NewMessageView

* Added createTeam sagas, createTeam reducer, new Team string and update CreateChannelView

* Remove unnecessary actionTypes, reducers and sagas, e2e tests and navigation to team view

* Minor tweaks

* Show TeamChannelsView only if joined the team

* Minor tweak

* Added AddChannelTeamView

* Added permissions, translations strings for teams,  deleteTeamRoom and addTeamRooms, AddExistingChannelView, updated CreateChannelView, TeamChannelsView

* Refactor touch component and update removeRoom and deleteRoom methods

* Minor tweaks

* Minor tweaks for removing channels and addExistingChannelView

* Added missing events and fixed channels list

* Minor tweaks for refactored touch component

* Added SelectListView and logic for leaving team

* Added addTeamMember and removeTeamMember

* Minor tweak

* Added deleteTeam function

* Minor tweak

* Minor tweaks

* Remove unnecesary changes, update TeamChannelsView, AddExistingChannelView, AddChannelTeamView, createChannel, goRoom and Touchable

* Remove unnecesary prop

* Add screens to ModalStack, events, autoJoin, update createChannel, addRoomsToTeam and Touchable

* Minor tweak

* Update loadMessagesForRoom.js

* Updated schema, tag component, touch, AddChannelTeamView, AddExistingChannelView, ActionSheet Item

* Fix unnecessary changes

* Add i18n, update createChannel, AddExistingChannelTeamView, AddChannelTeamView, RightButton and TeamChannelsView

* Updated styles, added tag story

* Minor tweak

* Minor tweaks

* Auto-join tweak

* Minor tweaks

* Minor tweak on search

* Minor refactor to ListItem, add SelectListView to ModalStack, update handleLeaveTeam

* Minor tweaks

* Update SelectListView

* Update handleLeaveTeam, remove unnecessary method, add story

* Minor tweak

* Minor visual tweaks

* Update SelectListView.js

* Update index.js

* Update RoomMembersView

* Updated SelectListView, RoomActionsView, leaveTeam method and string translations

* Update SelectListVIew

* Minor tweak

* Update SelectListView

* Minor tweak

* Minor tweaks

* Fix for List.Item subtitles being pushed down by title's flex

* Minor tweaks

* Update RoomActionsView

* Use showConfirmationAlert and showErrorAlert

* Remove addTeamMember, update removeTeamMember

* Update Alert

* Minor tweaks

* Minor tweaks

* Minor tweak

* Update showActionSheet on RoomMembersView

* Remove team main from query and move code around

* Fetch roles

* Update RoomMembersView and SelectListView

* Update rocketchat.js

* Updated leaveTeam and handleRemoveFromTeam

* Fix validation

* Remove unnecessary function

* Update RoomActionsView

* Update en.json

* updated deleteTeam function and permissions

* Added showConfirmationAlert

* Added string translations for teams

* Fix permission

* Added moveChannelToTeam and convertToTeam functionality

* Fix SelectListView RadioButton

* Fix moveToTeam

* Added searchBar to SelectListVIew

* Update RoomView , SelectListVIew and string translation for error

* E2E for Teams

* Fix tests and cleanup

* Minor refactor

* Wrong label

* Move/convert

* Fix convert

Co-authored-by: Diego Mello <diegolmello@gmail.com>

* [NEW] Add Teams to Directory (#3181)

* Added Teams to DirectoryView

* Fix icon

* Minor tweaks

* add tests

Co-authored-by: Diego Mello <diegolmello@gmail.com>

* [CHORE] Add logEvents for Teams (#3182)

* added events for team channels view and add existing channel view

* add logevents for room actions view and room info edit view

Co-authored-by: Diego Mello <diegolmello@gmail.com>

* [FIX] Disable jitsi call for teams (#3183)

Co-authored-by: Diego Mello <diegolmello@gmail.com>

* [FIX] Show alert `Not allowed` when click on a private channel that you don't be invited before (#3177)

* [FIX] Showing only channel you joined

* [FIX] How to get the params to mnavigation to other room from TeamChannelList

* Show alert Not allowed when trying access private channel that you don't joined

Co-authored-by: Diego Mello <diegolmello@gmail.com>

* [IMPROVEMENT] Load team's rooms from local database on team leave (#3185)

* [IMPROVEMENT] Search team list rooms of user in watermelon db

* Minor nitpick

Co-authored-by: Diego Mello <diegolmello@gmail.com>

* [FIX] Option to prevent users from using Invisible status (#3186)

* [FIX] Option to prevent users from using Invisible status

* Added error to pt-BR

Co-authored-by: Diego Mello <diegolmello@gmail.com>

* [FIX] Item not animating on tap on team's channels view (#3187)

* [FIX] Directory sending incorrect room type (#3188)

Co-authored-by: Diego Mello <diegolmello@gmail.com>

* [FIX] App not showing proper alert on team leave (#3161)

* [IMPROVEMENT] refactoring how to leave team

* Fix the data passed to leaveTeam

* Fixed the lint error in i18n, the path of i18n, merged two ifs in one

* Fixed the Saga's flow when try to leave a room

* Fixed params passed to leaveRoom

* Fix the function name of leaveTeam

Co-authored-by: Diego Mello <diegolmello@gmail.com>

* Language update from LingoHub 🤖 (#3192)

Project Name: Rocket.Chat.ReactNative
Project Link: https://translate.lingohub.com/rocketchat/dashboard/rocket-dot-chat-dot-reactnative
User: Robot LingoHub

Easy language translations with LingoHub 🚀

Co-authored-by: Robot LingoHub <robot@lingohub.com>
Co-authored-by: Diego Mello <diegolmello@gmail.com>

* [NEW] Support Google OAuth from external browser (#3134)

* Deep linking to the app

* Handle deep linking

* Bump version to 4.17.0 (#3093)

* Revert "[IMPROVEMENT] Load team's rooms from local database on team leave (#3185)" (#3194)

This reverts commit fa00ef92ef.

* [FIX] Teams tests (#3196)

* Make team_main not optional and fix tests

* Undo isOptional and fix query

* Comment

* [FIX] Wrong system messages being passed as parameters to room save (#3197)

* [FIX] RoomItem's long press crashing the app if prop is missing (#3199)

* Check onLongPress prop

* Add Touch stories

* [FIX] Crashing on link press (#3204)

* [FIX] Don't show Block Button inside Group DM Actions (#3195)

* [FIX] Don't show Block Button inside Group DM Actions

* Use RocketChat.isGroupChat instead of simple if condition

* Add return

Co-authored-by: Diego Mello <diegolmello@gmail.com>

* [TEST] Fixed E2E tests (#3201)

* [FIX] Test E2E i18n

* 01-createroom and 02-room fixed

* 03-roomactions and 04-discussions

* 05-threads and 07-markasunread from room

* Test 07-markasunread

* Set notifications 'YES' and delete true in 03-forgotpassword and 04-createuser

* Fixed the data that 02-team uses and changed the message in 07-markasunread

* Added group.alternate to data.docker and commented the test for the fallback language

Co-authored-by: Diego Mello <diegolmello@gmail.com>

* [TEST] E2E for Jump to Message (#3202)

* E2E tests for jump to message

* Fix thread tests

* Remove unnecessary function and uncomment tests

* Minor tweak

* Fix tests and minor tweaks

* Minor tweaks

* Update docker data

* Fix docker

* Fix duplicated testid

* Minor refactor

* Fix jump to old message test

* Fix load on scroll

* Add fab test

* Minor addition

* stash

* almost there

* Final changes

Co-authored-by: Diego Mello <diegolmello@gmail.com>

* [IMPROVE] Subscribe to permissions (#2993)

* [CHORE] Subscribe to permissions

* add redux action for update

* Minor tweaks

Co-authored-by: Gerzon Z <gerzonc@icloud.com>
Co-authored-by: Gerzon Z <gerzonzcanario@gmail.com>
Co-authored-by: Diego Mello <diegolmello@gmail.com>

* [IMPROVE] Subscribe to roles (#2992)

* [CHORE] Subscribe to Roles

* subscribe to roles-change

* add subscribe for stream-roles

* fixed subscribe roles

* Add componentDidUpdate to RoomMembersView and propType

* Update componentDidUpdate in RoomMembersView, roles reducer,  getRoles method and actionType

* Minor tweaks

* Remove componentDidUpdate

* Fix add role

* Fix initialState and remove role

* Minor try/catch fix

* Fix lint

* Fix offline first

Co-authored-by: Diego Mello <diegolmello@gmail.com>
Co-authored-by: Gerzon Z <gerzonc@icloud.com>
Co-authored-by: Gerzon Z <gerzonzcanario@gmail.com>

* [IMPROVE] Subscribe to settings  (#3222)

* Add action and reducer

* Add streamNotifyAll listener

* Minor tweak

* Minor tweak

* Fix update not taking in consideration other type columnns

Co-authored-by: Diego Mello <diegolmello@gmail.com>

* Chore: Add Lint to E2E tests (#3217)

* Added eslint plugin dependencie and fixed the eslint.js

* E2E Tests folder Assorted

* Linted all e2e, just e2e/docker that don't changed

* Update 09-jumptomessage.spec.js

* Removed async from describe function

* Remove outdated detox linter lib

* Add overrides to eslintrc

Co-authored-by: Gerzon Z <gerzonc@icloud.com>
Co-authored-by: Gerzon Z <gerzonzcanario@gmail.com>
Co-authored-by: Diego Mello <diegolmello@gmail.com>

* [FIX] App not showing proper alert on team delete (#3219)

* [FIX] Rule to delete team's channel

* Fixed Saga and flow to delete team and team's channel

* Adjusted the warning alert as the Figma

Co-authored-by: Gerzon Z <gerzonc@icloud.com>
Co-authored-by: Diego Mello <diegolmello@gmail.com>

* [IMPROVE] Add Jitsi button to Teams (#3223)

* [IMPROVE] Add Jitsi button to teams

* Added setting to check is Jitsi is Enable for Channel too

* Fix typo

Co-authored-by: Gerzon Z <gerzonc@icloud.com>
Co-authored-by: Gerzon Z <gerzonzcanario@gmail.com>
Co-authored-by: Diego Mello <diegolmello@gmail.com>

* [FIX] Jump to message from in-app notification (#3225)

* [FIX] Jump to message by in-app notification

* Bug fix to scroll proper the last message

* Minor tweak

Co-authored-by: Gerzon Z <gerzonc@icloud.com>
Co-authored-by: Gerzon Z <gerzonzcanario@gmail.com>
Co-authored-by: Diego Mello <diegolmello@gmail.com>

* [FIX] Google OAuth triggering cookies logic (#3244)

* Remove checkCookiesAndLogout

* Add loginEmailPassword to loginOAuthOrSso

* Add isFromWebView field

* Fix migrations

* Minor tweak

* Fix OAuth for other services

* Fix migrations

* Stop persisting loginEmailPassword

Co-authored-by: Diego Mello <diegolmello@gmail.com>

* Language update from LingoHub 🤖 (#3251)

Project Name: Rocket.Chat.ReactNative
Project Link: https://translate.lingohub.com/rocketchat/dashboard/rocket-dot-chat-dot-reactnative
User: Robot LingoHub

Easy language translations with LingoHub 🚀

Co-authored-by: Robot LingoHub <robot@lingohub.com>
Co-authored-by: Diego Mello <diegolmello@gmail.com>

* [IMPROVE] Message body readability on dark themes (#2981)

* [CHORE] Apply auxiliaryText on message body

* change bodyText to uxiliaryText

* Update tests

* Update bodyText color and rollback PR changes

* Update Storyshots.test.js.snap

* Minor tweak

Co-authored-by: Diego Mello <diegolmello@gmail.com>
Co-authored-by: Gerzon Z <gerzonc@icloud.com>
Co-authored-by: Gerzon Z <gerzonzcanario@gmail.com>

* [FIX] Subscribe to settings making app to hang on login (#3254)

* [FIX] Poor performance in messages list on Android 11 (#3260)

* Bump version to 4.18.0 (#3252)

* [FIX] Create team crashing the app (#3248)

Co-authored-by: Gerzon Z <gerzonc@icloud.com>
Co-authored-by: Gerzon Z <gerzonzcanario@gmail.com>
Co-authored-by: Diego Mello <diegolmello@gmail.com>

* [IMPROVE] Convert Team to Channel (#3249)

* [IMPROVE] Add convert team to a channel

* Action to SelectListView and new words to i18n

* Implemented the post and it's working with selected channels or not

* Fixed the Convert Team Warning at english i18n and changed the function name

* E2E test completed in sequence the convert/move teams

* [IMPROVE] Add convert team to a channel

* Action to SelectListView and new words to i18n

* Implemented the post and it's working with selected channels or not

* Fixed the Convert Team Warning at english i18n and changed the function name

* rebase develop into this branch

* [IMPROVE] Add convert team to a channel

* Action to SelectListView and new words to i18n

* Implemented the post and it's working with selected channels or not

* Fixed the Convert Team Warning at english i18n and changed the function name

* rebase develop into this branch

Co-authored-by: Diego Mello <diegolmello@gmail.com>

* [IMPROVE] Set black as default dark theme (#3270)

* Update default darkLevel

* Minor tweak

* [IMPROVE] Make `system default` the default browser (#3265)

* [FIX] use systemdefault: as the default browser, not inApp

* Fix

Co-authored-by: Diego Mello <diegolmello@gmail.com>

* Language update from LingoHub 🤖 (#3269)

Project Name: Rocket.Chat.ReactNative
Project Link: https://translate.lingohub.com/rocketchat/dashboard/rocket-dot-chat-dot-reactnative
User: Robot LingoHub

Easy language translations with LingoHub 🚀

Co-authored-by: Robot LingoHub <robot@lingohub.com>
Co-authored-by: Diego Mello <diegolmello@gmail.com>

* [IMPROVE] Remove difference between public/private on "Group by type" (#3271)

* Merge channels and private groups

* Remove i18n

* Regression: Settings pagination not working (#3277)

* Regression: Markdown handlePress not working properly (#3278)

Co-authored-by: Diego Mello <diegolmello@gmail.com>

* Chore: Improve QA workflow (#3285)

* Chore: Update dependencies (#3206)

* Update non-dev patches

* Update dev patches

* Update minors

* Update dev minors

* Update few non semver

* Cookies

* datepicker, netinfo, base64 and bootsplash

* Patch cookies

* Update navigation

* Device info

* mocha

* localize

* react-native-picker-select

* vector icons, xregexp, popover

* try save husky

* document picker

* Remove emotion dev

* Downgrade some libs and make sure jest is passing

* Update storybook to stable

* mocha, axios, bootsplash

* Update lint job to node 15

* Chore: Update React Native to 0.64.2 (#3245)

* Update non-dev patches

* Update dev patches

* Update minors

* Update dev minors

* Update few non semver

* Cookies

* datepicker, netinfo, base64 and bootsplash

* Patch cookies

* Update navigation

* Device info

* mocha

* localize

* react-native-picker-select

* vector icons, xregexp, popover

* try save husky

* document picker

* Remove emotion dev

* Downgrade some libs and make sure jest is passing

* Update storybook to stable

* mocha, axios, bootsplash

* Update lint job to node 15

* Update android image to api 29 and xcode to 12.4

* building

* Fix lint

* Get rid of Storybooks errors

* Patch react-native-simple-crypto

* Remove pods from git

* Stash simple crypto

* Stash Flipper

* Remove single crypto patch

* Add manage-pods command

* Update Xcode to 12.5.0

* Fix E2E tests

* Cleanup podfile

* Fix Storybook

* Remove RN patch

* Fix iOS build release

* Fix cocoapods cache on CI

* Try to fix pods using bundle

* Update gems

* Add app_store_connect_api_key env to CI

* APP_STORE_CONNECT_API_KEY -> APP_STORE_CONNECT_API_BASE64

* Rollback to older usage of app_store_connect_api_key

* tmp

* Run manage-pods on TestFlight

* Use Podfile instead of Podfile.lock for cache

* Increase no_output_timeout from 20 minutes to 40

* Restore node modules on upload-to-testflight

* Add pod install to docs

* Chore: Run lint and tests on staged files only (#3291)

* Bump version to 4.19.0 (#3307)

* Chore: Update Bugsnag (#3300)

* Remove bugsnag-react-native

* Really remove bugsnag from android

* Install @bugsnag/react-native

* Logging error on Android correctly

* Cleanup

* Fix bugsnag mock

* iOS builds

* Fix CI mistake

* Upload dSYMs to Bugsnag

* Upload source maps automatically on iOS

* Cleanup

* Enable Bugsnag on share extension

* Add test error

* Use large macos

* Bump to 4.19.0 temporarily to test on TestFlight official

* Use temp keys

* Fix upload source maps for Official iOS build

* Remove tests

* Set version back to 4.18.0

* Language update from LingoHub 🤖 (#3297)

Project Name: Rocket.Chat.ReactNative
Project Link: https://translate.lingohub.com/rocketchat/dashboard/rocket-dot-chat-dot-reactnative
User: Robot LingoHub

Easy language translations with LingoHub 🚀

Co-authored-by: Robot LingoHub <robot@lingohub.com>
Co-authored-by: Diego Mello <diegolmello@gmail.com>

* [FIX] Unarchive permission not honored (#3237)

* [FIX] Show alert when unarchive error

* Title in alert

* Disable button when the user donesn't have the role permission

* Use ARCHIVE/UNARCHIVE instead of their lowercase in alert and removed capitalize lodash

* Check if the error eis translated before parse through i18n

* Remove unnecessary code

Co-authored-by: Diego Mello <diegolmello@gmail.com>
Co-authored-by: Levy Costa <levycosta471@gmail.com>

* [FIX] Hardcoded backdrop opacity on loading component (#3255)

* Added withTheme and themes to Loading

* Added animation to backdrop opacity

* Minor tweak

* Fix internal image impacted by opacity

Co-authored-by: Diego Mello <diegolmello@gmail.com>
Co-authored-by: Levy Costa <levycosta471@gmail.com>

* [FIX] Share extension not working on iOS (#3310)

* Temp add all pods to share extension

* Cleanup

* [FIX] Permissions to edit livechat when the user is a livechat-agent (#3294)

* [FIX] Permissions to edit livechat

* Added the permission to edit livechat room custom fields

Co-authored-by: Levy Costa <levycosta471@gmail.com>
Co-authored-by: Diego Mello <diegolmello@gmail.com>

* [FIX] Reactive footer when agents take chats (#3288)

Co-authored-by: Diego Mello <diegolmello@gmail.com>

* [FIX] Omnichannel custom fields are not rendered properly (#3295)

* [FIX] Permissions to edit livechat

* [FIX] Custom fields labels and values

* refactor field

* Added the permission to edit livechat room custom fields

* Fix the inputs.focus()

Co-authored-by: Levy Costa <levycosta471@gmail.com>
Co-authored-by: Diego Mello <diegolmello@gmail.com>

* [FIX] Wrong message when room is closed by the Guest (#3289)

Co-authored-by: Gerzon Z <gerzonc@icloud.com>
Co-authored-by: Diego Mello <diegolmello@gmail.com>

* [FIX] Dealing well with pre-configured tags in Omnichannel (#3298)

* [FIX] Permissions to edit livechat

* [FIX] Tags with multiselect and tagParamsSelected

* Removed console.log and the new set to filter

* Added the permission to edit livechat room custom fields

* Change Title Livechat_edit to Edit

* Added marginBottom to multiSelect

* Added marginBottom to multiSelect

Co-authored-by: Gerzon Z <gerzonc@icloud.com>

* [FIX] Bugsnag and Analytics opt-out (#3335)

* Deleted redux actions for bugsnag and analytics, in addition fixed to eon/off reports for both

* Removed console.log

* minor tweak

* Enable and disable crashlytics and remove breadcrumb from bugsnag

* minor tweaks with the names of the variables

* minor tweak

Co-authored-by: Diego Mello <diegolmello@gmail.com>

* [FIX] Show thumbnails in message view (#2975)

* [FIX] Show thumbnails in message view

fixes: #2853

* Add stories for thumbnails and update test

* [Test] Update tests

* added stories

Co-authored-by: Reinaldo Neto <reinaldonetof@hotmail.com>
Co-authored-by: Levy Costa <levycosta471@gmail.com>
Co-authored-by: Diego Mello <diegolmello@gmail.com>

* [FIX] Show button attachment on messages (#2980)

* [FIX] Show button attachment in message list

fixes: #2684

* Changed the Button and theme, text theme and how to call the function

* Fix the props passed in Message

* Function to context

* Added button attachment to stories

* New snapshot

Co-authored-by: Reinaldo Neto <47038980+reinaldonetof@users.noreply.github.com>
Co-authored-by: Reinaldo Neto <reinaldonetof@hotmail.com>
Co-authored-by: Levy Costa <levycosta471@gmail.com>
Co-authored-by: Diego Mello <diegolmello@gmail.com>

* [FIX] The unread section is not removed after receiving a new message and swipe to read (#3281)

* Fix unread section from direct messages and thread messages

* Minor tweak

* removed the thread unread, but the thread unread is on branch fix.unread-thread-from-listview

Co-authored-by: Diego Mello <diegolmello@gmail.com>

* [FIX] Evaluate values in handle failure (#3235)

* [FIX] HEvaluating proper the error for channel, team and undefined

* Added some team errors in i18n

* Added unauthorized to i18n

* Test if there is channel name too, to prevent to show {missing roomName}

* Refactor the treatment error to check if exists before translate with i18n

* Remove some check conditional points

* Minor tweak

* Added array with error inside the createChannel

* Moved error array to inside the handleFailure

* added creating_discussion

Co-authored-by: Gerzon Z <gerzonzcanario@gmail.com>
Co-authored-by: Gerzon Z <gerzonc@icloud.com>
Co-authored-by: Diego Mello <diegolmello@gmail.com>

* [FIX] E2E Encryption button doesn't appear (#3343)

Co-authored-by: Diego Mello <diegolmello@gmail.com>

* Regression: Orientation lock on Android not working (#3345)

* Update MainApplication.java

* Update MainApplication.java

* Downgrade react-native-orientation-locker

* Pods

Co-authored-by: Diego Mello <diegolmello@gmail.com>

* [FIX] TextInput breaking line (#2873)

* Update TextInput's padding

* Chante textAlign to auto and to ellipses longer text than the width

* Added story with changes in text input

* Changed in TextInput stories

Co-authored-by: Reinaldo Neto <47038980+reinaldonetof@users.noreply.github.com>
Co-authored-by: Reinaldo Neto <reinaldonetof@hotmail.com>
Co-authored-by: Diego Mello <diegolmello@gmail.com>

* Chore: Update Jitsi to 3.6.0 (#3292)

* Fix RN deps

* Update react-native-jitsi-meet

* Working on iOS from Jitsi source

* Dependencies installed

* Temp android

* Kinda working android

* Working on iOS with our SDK

* Use our maven repo

* Fix temp maven url

* Cleanup

* Fix maven url

* Bring chat back

* Add activity indicator

* Update react-native-jitsi-meet

* Fix loading on iOS

* Clear gradle cache

* Try 3.6.0

* Dummy change to update gradle cache

* Point to merged forks

* update pod commit

* Bump version to 4.20.0 (#3366)

* Chore: Start Typescript migration (#3279)

* [IMPROVE] Show full image when available (#3370)

Co-authored-by: Gerzon Z <gerzonc@icloud.com>

* [FIX] Black screen on share extension if lock screen is enabled (#3320)

* Resolve issue causing black screen when sharing

* Add logEvent to error in local authenticate

* minor tweak

* Revert changes

Co-authored-by: Reinaldo Neto <reinaldonetof@hotmail.com>
Co-authored-by: Reinaldo Neto <47038980+reinaldonetof@users.noreply.github.com>
Co-authored-by: Diego Mello <diegolmello@gmail.com>

* Fix: lint-staged not working properly(#3382)

* Chore: Remove CocoaPods folder (#3381)

* Chore: Migrate AdminPanelView to Typescript (#3377)

Co-authored-by: Diego Mello <diegolmello@gmail.com>

* Chore: Migrate AutoTranslateView to Typescript (#3380)

* [improve] - migrate the view: AutoTranslateView to typescript

* TODO -> TODO:

Co-authored-by: Diego Mello <diegolmello@gmail.com>

* Fix: @rocketchat/sdk not fetching correct commit (#3384)

Co-authored-by: AlexAlexandre <alexalexandrejr@gmail.com>

* Chore: Migrate CreateDiscussionView to Typescript (#3378)

* [improve] - migrate the view: CreateDiscussionView to typescript

* minor changes

Co-authored-by: Diego Mello <diegolmello@gmail.com>

* Chore: Migrate DirectoryView to Typescript (#3379)

* [improve] - migrate the view: DirectoryView to typescript

* [improve] - migrate the view: removing unnecessary variables

* minor changes

Co-authored-by: Diego Mello <diegolmello@gmail.com>

* [IMPROVE] Fetch members from API endpoint (#3351)

Co-authored-by: Diego Mello <diegolmello@gmail.com>

* Language update from LingoHub 🤖 (#3374)

Project Name: Rocket.Chat.ReactNative
Project Link: https://translate.lingohub.com/rocketchat/dashboard/rocket-dot-chat-dot-reactnative
User: Robot LingoHub

Easy language translations with LingoHub 🚀

Co-authored-by: Robot LingoHub <robot@lingohub.com>
Co-authored-by: Diego Mello <diegolmello@gmail.com>

* [IMPROVE] Voice messages improvements (#3385)

Co-authored-by: Diego Mello <diegolmello@gmail.com>
Co-authored-by: Marco Jacotec <mj@jacotec.de>

* [NEW] Canned responses (#3355)

Co-authored-by: Diego Mello <diegolmello@gmail.com>

* [FIX] Preserve voice message if recording is interrupted (#3397)

* 7c25909671

* Minor changes

Co-authored-by: Marco Jakobs <mj@jacotec.de>

* [IMPROVE] Onboarding changes (#3387)

- Change the first screen of the app
- Minor changes on NewServerView and make it the first screen of the app
- Add "Create workspace" to ServerDropdown

Co-authored-by: Diego Mello <diegolmello@gmail.com>

* Chore: Point to new white label URL (#3402)

* [FIX] Canned Responses minor fixes (#3400)

* fix onChangeText usedCanned on tablet

* removed refreshControl

Co-authored-by: Diego Mello <diegolmello@gmail.com>

* [FIX] Room Actions buttons not showing after taking a channel from Omnichannel Queue (#3399)

Co-authored-by: Diego Mello <diegolmello@gmail.com>

* [FIX] Fetch members on RoomMembersView (#3403)

* [FIX] Fetch members on RoomMembersView

* needed to add a conditional to the response

* result back properly from rocketchat lib

Co-authored-by: Diego Mello <diegolmello@gmail.com>

Co-authored-by: Daniel Maike <danmke@hotmail.com>
Co-authored-by: Vitor Leal <vitor_leal2201@hotmail.com>
Co-authored-by: Fernando Aguilar <fernando.aguilar@hotmail.com.br>
Co-authored-by: Djorkaeff Alexandre <djorkaeff.unb@gmail.com>
Co-authored-by: youssef-md <emaildeyoussefmuhamad@gmail.com>
Co-authored-by: Abdullah Alhamoud <10301923+abalhamoud@users.noreply.github.com>
Co-authored-by: David-Tsui <st880221@gmail.com>
Co-authored-by: Dave Koo <dkoo761@gmail.com>
Co-authored-by: Graham Smith <graham@wiseman-designs.com>
Co-authored-by: Fazil Boudjelal <fazildiablou@hotmail.fr>
Co-authored-by: Lucas Dousse <Cormoran96@users.noreply.github.com>
Co-authored-by: Sumukha Hegde <SUMUKHA214@GMAIL.COM>
Co-authored-by: Gerzon Z <gerzonzcanario@gmail.com>
Co-authored-by: Gerzon Z <gerzonc@icloud.com>
Co-authored-by: phriedrich <info@phriedrich.de>
Co-authored-by: yash-rajpal <58601732+yash-rajpal@users.noreply.github.com>
Co-authored-by: Hakan YILMAZ <mukerrem.yilmaz@hotmail.com>
Co-authored-by: Vincenzo Esposito <aenon.esposito@gmail.com>
Co-authored-by: Arkadyuti Bandyopadhyay <bandyopadhyayarkadyuti@gmail.com>
Co-authored-by: Anant Bhasin <38764067+aKn1ghtOut@users.noreply.github.com>
Co-authored-by: Gung Wah <41157464+kresnaputra@users.noreply.github.com>
Co-authored-by: Billy Newman <newmanw10@gmail.com>
Co-authored-by: Jan Garaj <jan.garaj@gmail.com>
Co-authored-by: ankar84 <ankar84@gmail.com>
Co-authored-by: sadegh <sadeghmohamadnia@yahoo.com>
Co-authored-by: Noach Magedman <nmagedman@gmail.com>
Co-authored-by: lingohub[bot] <69908207+lingohub[bot]@users.noreply.github.com>
Co-authored-by: Robot LingoHub <robot@lingohub.com>
Co-authored-by: Reinaldo Neto <47038980+reinaldonetof@users.noreply.github.com>
Co-authored-by: Levy Costa <levycosta471@gmail.com>
Co-authored-by: Reinaldo Neto <reinaldonetof@hotmail.com>
Co-authored-by: Alex Junior <alexalexandrejr@gmail.com>
Co-authored-by: Diego Sampaio <chinello@gmail.com>
Co-authored-by: Chris Price <56982873+cprice-kgi@users.noreply.github.com>
Co-authored-by: Marco Jacotec <mj@jacotec.de>
Co-authored-by: Debdut Chakraborty <debdut.chakraborty@rocket.chat>
This commit is contained in:
Diego Mello 2021-10-01 15:12:09 -03:00 committed by GitHub
parent 00b4c9af33
commit 4db5db7fa0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
619 changed files with 39539 additions and 27554 deletions

View File

@ -340,7 +340,7 @@ jobs:
<<: *defaults <<: *defaults
docker: docker:
- image: circleci/node:15 - image: circleci/node:15
resource_class: large
environment: environment:
CODECOV_TOKEN: caa771ab-3d45-4756-8e2a-e1f25996fef6 CODECOV_TOKEN: caa771ab-3d45-4756-8e2a-e1f25996fef6
@ -376,6 +376,7 @@ jobs:
environment: environment:
<<: *android-env <<: *android-env
<<: *bash-env <<: *bash-env
resource_class: large
steps: steps:
- android-build - android-build
@ -386,6 +387,7 @@ jobs:
environment: environment:
<<: *android-env <<: *android-env
<<: *bash-env <<: *bash-env
resource_class: large
steps: steps:
- android-build - android-build

View File

@ -1,161 +1,156 @@
module.exports = { module.exports = {
"settings": { settings: {
"import/resolver": { 'import/resolver': {
"node": { node: {
"extensions": [".js", ".ios.js", ".android.js", ".native.js", ".tsx"] extensions: ['.js', '.ios.js', '.android.js', '.native.js', '.ts', '.tsx']
} }
}
},
"parser": "@babel/eslint-parser",
"extends": "airbnb",
"parserOptions": {
"sourceType": "module",
"ecmaVersion": 2017,
"ecmaFeatures": {
"experimentalObjectRestSpread" : true,
"jsx": true,
"legacyDecorators": true
} }
}, },
"plugins": [ parser: '@babel/eslint-parser',
"react", extends: ['@rocket.chat/eslint-config', 'prettier'],
"jsx-a11y", parserOptions: {
"import", sourceType: 'module',
"react-native", ecmaVersion: 2017,
"@babel" ecmaFeatures: {
], experimentalObjectRestSpread: true,
"env": { jsx: true,
"browser": true, legacyDecorators: true
"commonjs": true, }
"es6": true,
"node": true,
"jquery": true,
"mocha": true
}, },
"rules": { plugins: ['react', 'jsx-a11y', 'import', 'react-native', '@babel'],
"react/jsx-filename-extension": [1, { env: {
"extensions": [".js", ".jsx"] browser: true,
}], commonjs: true,
"react/require-default-props": [0], es6: true,
"react/no-unused-prop-types": [2, { node: true,
"skipShapeProps": true jquery: true,
}], mocha: true
"react/no-did-mount-set-state": 0,
"react/no-multi-comp": [0],
"react/jsx-indent": [2, "tab"],
"react/jsx-indent-props": [2, "tab"],
"react/forbid-prop-types": 0,
"jsx-quotes": [2, "prefer-single"],
"jsx-a11y/href-no-hash": 0,
"jsx-a11y/aria-role": 0,
"import/prefer-default-export": 0,
"import/no-cycle": 0,
"camelcase": 0,
"no-underscore-dangle": 0,
"no-return-assign": 0,
"no-param-reassign": 0,
"no-tabs": 0,
"no-multi-spaces": 2,
"no-eval": 2,
"no-extend-native": 2,
"no-multi-str": 2,
"no-use-before-define": 2,
"no-const-assign": 2,
"no-cond-assign": 2,
"no-constant-condition": 2,
"no-control-regex": 2,
"no-debugger": 2,
"no-delete-var": 2,
"no-dupe-keys": 2,
"no-dupe-args": 2,
"no-dupe-class-members": 2,
"no-duplicate-case": 2,
"no-else-return": [0, {allowElseIf: true}],
"no-empty": 2,
"no-empty-character-class": 2,
"no-ex-assign": 2,
"no-extra-boolean-cast": 2,
"no-extra-semi": 2,
"no-fallthrough": 2,
"no-func-assign": 2,
"no-inner-declarations": [2, "functions"],
"no-invalid-regexp": 2,
"no-irregular-whitespace": 2,
"no-mixed-spaces-and-tabs": 2,
"no-sparse-arrays": 2,
"no-negated-in-lhs": 2,
"no-obj-calls": 2,
"no-octal": 2,
"no-redeclare": 2,
"no-regex-spaces": 2,
"no-undef": 2,
"no-unreachable": 2,
"no-unused-expressions": 0,
"no-unused-vars": [2, {
"vars": "all",
"args": "after-used"
}],
"max-len": 0,
"react/jsx-uses-vars": 2,
"no-void": 2,
"no-var": 2,
"one-var": [2, "never"],
"no-lonely-if": 2,
"no-trailing-spaces": 2,
"complexity": [1, 31],
"space-in-parens": [2, "never"],
"space-before-function-paren": [2, "never"],
"space-before-blocks": [2, "always"],
"indent": [2, "tab", {"SwitchCase": 1}],
"eol-last": [2, "always"],
"comma-dangle": [2, "never"],
"keyword-spacing": 2,
"block-spacing": 2,
"brace-style": [2, "1tbs", { "allowSingleLine": true }],
"computed-property-spacing": 2,
"comma-spacing": 2,
"comma-style": 2,
"guard-for-in": 2,
"wrap-iife": 2,
"block-scoped-var": 2,
"curly": [2, "all"],
"eqeqeq": [2, "allow-null"],
"new-cap": [2],
"use-isnan": 2,
"valid-typeof": 2,
"linebreak-style": 0,
"prefer-template": 2,
"template-curly-spacing": [2, "always"],
"quotes": [2, "single"],
"semi": [2, "always"],
"prefer-const": 2,
"object-shorthand": 2,
"consistent-return": 0,
"global-require": "off",
"react-native/no-unused-styles": 2,
"react/jsx-one-expression-per-line": 0,
"require-await": 2,
"func-names": 0,
"react/sort-comp": ["error", {
"order": [
"static-variables",
"static-methods",
"lifecycle",
"everything-else",
"render"
]
}],
"react/static-property-placement": [0],
"arrow-parens": ["error", "as-needed", { requireForBlockBody: true }],
"react/jsx-props-no-spreading": [1],
"react/jsx-curly-newline": [0],
"react/state-in-constructor": [0],
"no-async-promise-executor": [0],
"max-classes-per-file": [0],
"no-multiple-empty-lines": [0]
}, },
"globals": { rules: {
"__DEV__": true 'import/extensions': [
'error',
'ignorePackages',
{
js: 'warning',
jsx: 'warning',
ts: 'warning',
tsx: 'warning'
}
],
'react/jsx-filename-extension': [
1,
{
extensions: ['.js', '.jsx', '.ts', '.tsx']
}
],
'react/require-default-props': [0],
'ordered-imports': [0],
'react/no-did-mount-set-state': 0,
'react/no-multi-comp': [0],
'react/jsx-indent-props': [2, 'tab'],
'jsx-quotes': [2, 'prefer-single'],
'jsx-a11y/href-no-hash': 0,
'jsx-a11y/aria-role': 0,
'import/prefer-default-export': 0,
'import/no-cycle': 0,
'import/order': [
'error',
{
'newlines-between': 'ignore'
}
],
camelcase: 0,
'no-underscore-dangle': 0,
'no-return-assign': 0,
'no-param-reassign': 0,
'no-tabs': 0,
'no-multi-spaces': 2,
'no-eval': 2,
'no-extend-native': 2,
'no-multi-str': 2,
'no-use-before-define': 2,
'no-const-assign': 2,
'no-cond-assign': 2,
'no-constant-condition': 2,
'no-control-regex': 2,
'no-debugger': 2,
'no-delete-var': 2,
'no-dupe-keys': 2,
'no-dupe-args': 2,
'no-dupe-class-members': 2,
'no-duplicate-case': 2,
'no-else-return': [0, { allowElseIf: true }],
'no-empty': 2,
'no-empty-character-class': 2,
'no-ex-assign': 2,
'no-extra-boolean-cast': 2,
'no-extra-semi': 2,
'no-fallthrough': 2,
'no-func-assign': 2,
'no-inner-declarations': [2, 'functions'],
'no-invalid-regexp': 2,
'no-irregular-whitespace': 2,
'no-mixed-spaces-and-tabs': 1,
'no-sparse-arrays': 2,
'no-negated-in-lhs': 2,
'no-obj-calls': 2,
'no-octal': 2,
'no-redeclare': 2,
'no-regex-spaces': 2,
'no-undef': 2,
'no-unreachable': 2,
'no-unused-expressions': 0,
'no-unused-vars': 'off',
'max-len': 0,
'react/jsx-uses-vars': 2,
'no-void': 2,
'no-var': 2,
'one-var': [2, 'never'],
'no-lonely-if': 2,
'no-trailing-spaces': 2,
complexity: [1, 31],
'space-in-parens': [2, 'never'],
'space-before-blocks': [2, 'always'],
indent: 'off',
'eol-last': [2, 'always'],
'comma-dangle': [2, 'never'],
'keyword-spacing': 2,
'block-spacing': 2,
'brace-style': [2, '1tbs', { allowSingleLine: true }],
'computed-property-spacing': 2,
'comma-spacing': 2,
'comma-style': 2,
'guard-for-in': 2,
'wrap-iife': 2,
'block-scoped-var': 2,
curly: [2, 'all'],
eqeqeq: [2, 'allow-null'],
'new-cap': 'off',
'use-isnan': 2,
'valid-typeof': 2,
'linebreak-style': 0,
'prefer-template': 2,
quotes: [1, 'single'],
semi: [2, 'always'],
'prefer-const': 2,
'object-shorthand': 2,
'consistent-return': 0,
'global-require': 'off',
'react-native/no-unused-styles': 2,
'react/jsx-one-expression-per-line': 0,
'require-await': 2,
'func-names': 0,
'react/static-property-placement': [0],
'arrow-parens': ['warn', 'as-needed', { requireForBlockBody: true }],
'react/jsx-curly-newline': [0],
'react/state-in-constructor': [0],
'no-async-promise-executor': [0],
'max-classes-per-file': [0],
'no-multiple-empty-lines': [0],
'no-sequences': 'off'
},
globals: {
__DEV__: true
}, },
overrides: [ overrides: [
{ {
@ -173,6 +168,86 @@ module.exports = {
'no-await-in-loop': 0, 'no-await-in-loop': 0,
'no-restricted-syntax': 0 'no-restricted-syntax': 0
} }
},
{
files: ['**/*.ts', '**/*.tsx'],
extends: [
'plugin:@typescript-eslint/recommended',
'plugin:@typescript-eslint/eslint-recommended',
'@rocket.chat/eslint-config',
'prettier'
],
parser: '@typescript-eslint/parser',
parserOptions: {
sourceType: 'module',
ecmaVersion: 2018,
warnOnUnsupportedTypeScriptVersion: false,
ecmaFeatures: {
experimentalObjectRestSpread: true,
legacyDecorators: true
}
},
plugins: ['react', '@typescript-eslint'],
rules: {
'@typescript-eslint/no-var-requires': 'off',
'@typescript-eslint/no-empty-function': [0],
'@typescript-eslint/ban-types': [0],
'func-call-spacing': 'off',
'jsx-quotes': ['error', 'prefer-single'],
indent: 'off',
'comma-dangle': [2, 'never'],
'no-return-assign': 0,
'no-dupe-class-members': 'off',
'no-extra-parens': 'off',
'no-spaced-func': 'off',
'no-unused-vars': 'off',
'no-useless-constructor': 'off',
'no-use-before-define': 'off',
'react/jsx-uses-react': 'error',
'react/jsx-uses-vars': 'error',
'react/jsx-no-undef': 'error',
'react/jsx-fragments': ['error', 'syntax'],
'@typescript-eslint/ban-ts-comment': 'off',
'@typescript-eslint/indent': [
'warn',
'tab',
{
SwitchCase: 1
}
],
'@typescript-eslint/no-extra-parens': [
'warn',
'all',
{
conditionalAssign: true,
nestedBinaryExpressions: false,
returnAssign: true,
ignoreJSX: 'all',
enforceForArrowConditionals: false
}
],
'@typescript-eslint/no-dupe-class-members': 'error',
'@typescript-eslint/no-explicit-any': 'off',
'@typescript-eslint/no-unused-vars': [
'error',
{
argsIgnorePattern: '^_',
ignoreRestSiblings: true
}
],
'new-cap': 'off',
'lines-between-class-members': 'off'
},
globals: {
JSX: true
},
settings: {
'import/resolver': {
node: {
extensions: ['.js', '.ts', '.tsx']
}
}
}
} }
] ]
}; };

25
.prettierignore Normal file
View File

@ -0,0 +1,25 @@
.circleci/
.github/
.husky
build/
node_modules/
coverage/
e2e/docker/
artifacts/
android/
ios/
patches/
scripts/
.bettercodehub.yml
.buckconfig
.gitattributes
.gitignore
.snyk
.watchmanconfig
CONTRIBUTING.md
README.md
SECURITY.md
npm-debug.log
yarn-error.log

10
.prettierrc.js Normal file
View File

@ -0,0 +1,10 @@
module.exports = {
bracketSpacing: true,
jsxBracketSameLine: true,
singleQuote: true,
jsxSingleQuote: true,
trailingComma: 'none',
printWidth: 130,
useTabs: true,
arrowParens: 'avoid'
};

View File

@ -54,6 +54,18 @@ To check for lint issues on your code, run this on your terminal:
yarn lint yarn lint
``` ```
## Code formatting
We use [Prettier](https://prettier.io) to format the code style in our project. We have a pre-commit hook enforcing commits to follow our style guides.
To fix your code formatting issues, run this on your terminal:
```sh
yarn prettier
```
[Check this link](https://prettier.io/docs/en/editors.html) to see how to integrate Prettier with your preferred code editor, and run Prettier when save your file for example.
## Tests ## Tests
It's always important to ensure everything is working properly and that's why tests are great. We have unit and e2e tests on this project. It's always important to ensure everything is working properly and that's why tests are great. We have unit and e2e tests on this project.

View File

@ -31,7 +31,7 @@ Also check the [#react-native](https://open.rocket.chat/channel/react-native) co
Are you a dev and would like to help? Found a bug that you would like to report or a missing feature that you would like to work on? Great! We have written down a [Contribution guide](https://github.com/RocketChat/Rocket.Chat.ReactNative/blob/develop/CONTRIBUTING.md) so you can start easily. Are you a dev and would like to help? Found a bug that you would like to report or a missing feature that you would like to work on? Great! We have written down a [Contribution guide](https://github.com/RocketChat/Rocket.Chat.ReactNative/blob/develop/CONTRIBUTING.md) so you can start easily.
## Whitelabel ## Whitelabel
Do you want to make the app run on your own server only? [Follow our whitelabel documentation.](https://docs.rocket.chat/guides/developer/mobile-apps/whitelabeling-mobile-apps) Do you want to make the app run on your own server only? [Follow our whitelabel documentation.](https://developer.rocket.chat/mobile-app/mobile-app-white-labelling)
## Engage with us ## Engage with us
### Share your story ### Share your story

View File

@ -899,27 +899,22 @@ exports[`Storyshots BackgroundContainer black theme - loading 1`] = `
</View> </View>
<ActivityIndicator <ActivityIndicator
animating={true} animating={true}
color="#999999" color="#f9f9f9"
hidesWhenStopped={true} hidesWhenStopped={true}
size="small" size="small"
style={ style={
Array [ Object {
Object { "backgroundColor": "transparent",
"backgroundColor": "transparent", "fontFamily": "System",
"fontFamily": "System", "fontSize": 16,
"fontSize": 16, "fontWeight": "400",
"fontWeight": "400", "left": 0,
"left": 0, "paddingHorizontal": 24,
"paddingHorizontal": 24, "position": "absolute",
"position": "absolute", "right": 0,
"right": 0, "textAlign": "center",
"textAlign": "center", "top": 60,
"top": 60, }
},
Object {
"color": "#f9f9f9",
},
]
} }
/> />
</View> </View>
@ -1037,27 +1032,22 @@ exports[`Storyshots BackgroundContainer dark theme - loading 1`] = `
</View> </View>
<ActivityIndicator <ActivityIndicator
animating={true} animating={true}
color="#999999" color="#f9f9f9"
hidesWhenStopped={true} hidesWhenStopped={true}
size="small" size="small"
style={ style={
Array [ Object {
Object { "backgroundColor": "transparent",
"backgroundColor": "transparent", "fontFamily": "System",
"fontFamily": "System", "fontSize": 16,
"fontSize": 16, "fontWeight": "400",
"fontWeight": "400", "left": 0,
"left": 0, "paddingHorizontal": 24,
"paddingHorizontal": 24, "position": "absolute",
"position": "absolute", "right": 0,
"right": 0, "textAlign": "center",
"textAlign": "center", "top": 60,
"top": 60, }
},
Object {
"color": "#f9f9f9",
},
]
} }
/> />
</View> </View>
@ -1175,27 +1165,22 @@ exports[`Storyshots BackgroundContainer loading 1`] = `
</View> </View>
<ActivityIndicator <ActivityIndicator
animating={true} animating={true}
color="#999999" color="#6C727A"
hidesWhenStopped={true} hidesWhenStopped={true}
size="small" size="small"
style={ style={
Array [ Object {
Object { "backgroundColor": "transparent",
"backgroundColor": "transparent", "fontFamily": "System",
"fontFamily": "System", "fontSize": 16,
"fontSize": 16, "fontWeight": "400",
"fontWeight": "400", "left": 0,
"left": 0, "paddingHorizontal": 24,
"paddingHorizontal": 24, "position": "absolute",
"position": "absolute", "right": 0,
"right": 0, "textAlign": "center",
"textAlign": "center", "top": 60,
"top": 60, }
},
Object {
"color": "#6C727A",
},
]
} }
/> />
</View> </View>
@ -1337,6 +1322,595 @@ exports[`Storyshots BackgroundContainer text 1`] = `
</View> </View>
`; `;
exports[`Storyshots CannedResponseItem Itens 1`] = `
Array [
<View
accessible={true}
focusable={true}
onClick={[Function]}
onResponderGrant={[Function]}
onResponderMove={[Function]}
onResponderRelease={[Function]}
onResponderTerminate={[Function]}
onResponderTerminationRequest={[Function]}
onStartShouldSetResponder={[Function]}
style={
Object {
"backgroundColor": "#ffffff",
"maxHeight": 141,
"minHeight": 117,
"opacity": 1,
"padding": 16,
}
}
>
<View
style={
Object {
"flexDirection": "row",
"height": 36,
}
}
>
<View
style={
Object {
"flex": 1,
}
}
>
<Text
style={
Array [
Object {
"backgroundColor": "transparent",
"flex": 1,
"fontFamily": "System",
"fontSize": 14,
"fontWeight": "500",
"paddingBottom": 0,
"paddingTop": 0,
"textAlign": "left",
},
Object {
"color": "#0d0e12",
},
]
}
>
!
!FAQ4
</Text>
<Text
style={
Array [
Object {
"backgroundColor": "transparent",
"flex": 1,
"fontFamily": "System",
"fontSize": 12,
"fontWeight": "400",
"paddingBottom": 0,
"paddingTop": 0,
"textAlign": "left",
},
Object {
"color": "#6C727A",
},
]
}
>
Private
</Text>
</View>
<View
accessible={true}
focusable={true}
onClick={[Function]}
onResponderGrant={[Function]}
onResponderMove={[Function]}
onResponderRelease={[Function]}
onResponderTerminate={[Function]}
onResponderTerminationRequest={[Function]}
onStartShouldSetResponder={[Function]}
style={
Object {
"backgroundColor": "#f3f4f5",
"borderRadius": 2,
"height": 28,
"justifyContent": "center",
"marginBottom": 12,
"marginLeft": 8,
"opacity": 1,
"paddingHorizontal": 14,
"width": 56,
}
}
>
<Text
accessibilityLabel="Use"
style={
Array [
Object {
"backgroundColor": "transparent",
"fontFamily": "System",
"fontSize": 16,
"fontWeight": "500",
"textAlign": "center",
},
Object {
"color": "#0d0e12",
},
Object {
"fontSize": 12,
},
undefined,
]
}
>
Use
</Text>
</View>
</View>
<Text
ellipsizeMode="tail"
numberOfLines={2}
style={
Array [
Object {
"backgroundColor": "transparent",
"fontFamily": "System",
"fontSize": 14,
"fontWeight": "400",
"marginTop": 8,
"paddingBottom": 0,
"paddingTop": 0,
"textAlign": "left",
},
Object {
"color": "#6C727A",
},
]
}
>
ZCVXZVXCZVZXVZXCVZXCVXZCVZX
</Text>
<View
style={
Object {
"flexDirection": "row",
"overflow": "hidden",
}
}
/>
</View>,
<View
accessible={true}
focusable={true}
onClick={[Function]}
onResponderGrant={[Function]}
onResponderMove={[Function]}
onResponderRelease={[Function]}
onResponderTerminate={[Function]}
onResponderTerminationRequest={[Function]}
onStartShouldSetResponder={[Function]}
style={
Object {
"backgroundColor": "#ffffff",
"maxHeight": 141,
"minHeight": 117,
"opacity": 1,
"padding": 16,
}
}
>
<View
style={
Object {
"flexDirection": "row",
"height": 36,
}
}
>
<View
style={
Object {
"flex": 1,
}
}
>
<Text
style={
Array [
Object {
"backgroundColor": "transparent",
"flex": 1,
"fontFamily": "System",
"fontSize": 14,
"fontWeight": "500",
"paddingBottom": 0,
"paddingTop": 0,
"textAlign": "left",
},
Object {
"color": "#0d0e12",
},
]
}
>
!
test4mobilePrivate
</Text>
<Text
style={
Array [
Object {
"backgroundColor": "transparent",
"flex": 1,
"fontFamily": "System",
"fontSize": 12,
"fontWeight": "400",
"paddingBottom": 0,
"paddingTop": 0,
"textAlign": "left",
},
Object {
"color": "#6C727A",
},
]
}
>
Private
</Text>
</View>
<View
accessible={true}
focusable={true}
onClick={[Function]}
onResponderGrant={[Function]}
onResponderMove={[Function]}
onResponderRelease={[Function]}
onResponderTerminate={[Function]}
onResponderTerminationRequest={[Function]}
onStartShouldSetResponder={[Function]}
style={
Object {
"backgroundColor": "#f3f4f5",
"borderRadius": 2,
"height": 28,
"justifyContent": "center",
"marginBottom": 12,
"marginLeft": 8,
"opacity": 1,
"paddingHorizontal": 14,
"width": 56,
}
}
>
<Text
accessibilityLabel="Use"
style={
Array [
Object {
"backgroundColor": "transparent",
"fontFamily": "System",
"fontSize": 16,
"fontWeight": "500",
"textAlign": "center",
},
Object {
"color": "#0d0e12",
},
Object {
"fontSize": 12,
},
undefined,
]
}
>
Use
</Text>
</View>
</View>
<Text
ellipsizeMode="tail"
numberOfLines={2}
style={
Array [
Object {
"backgroundColor": "transparent",
"fontFamily": "System",
"fontSize": 14,
"fontWeight": "400",
"marginTop": 8,
"paddingBottom": 0,
"paddingTop": 0,
"textAlign": "left",
},
Object {
"color": "#6C727A",
},
]
}
>
test for mobile private
</Text>
<View
style={
Object {
"flexDirection": "row",
"overflow": "hidden",
}
}
>
<View
style={
Array [
Object {
"borderRadius": 4,
"height": 16,
"marginRight": 4,
"marginTop": 8,
},
Object {
"backgroundColor": "#E6E6E7",
},
]
}
>
<Text
style={
Array [
Object {
"backgroundColor": "transparent",
"fontFamily": "System",
"fontSize": 12,
"fontWeight": "400",
"paddingBottom": 0,
"paddingHorizontal": 4,
"paddingTop": 0,
"textAlign": "left",
},
Object {
"color": "#6C727A",
},
]
}
>
HQ
</Text>
</View>
<View
style={
Array [
Object {
"borderRadius": 4,
"height": 16,
"marginRight": 4,
"marginTop": 8,
},
Object {
"backgroundColor": "#E6E6E7",
},
]
}
>
<Text
style={
Array [
Object {
"backgroundColor": "transparent",
"fontFamily": "System",
"fontSize": 12,
"fontWeight": "400",
"paddingBottom": 0,
"paddingHorizontal": 4,
"paddingTop": 0,
"textAlign": "left",
},
Object {
"color": "#6C727A",
},
]
}
>
Closed
</Text>
</View>
<View
style={
Array [
Object {
"borderRadius": 4,
"height": 16,
"marginRight": 4,
"marginTop": 8,
},
Object {
"backgroundColor": "#E6E6E7",
},
]
}
>
<Text
style={
Array [
Object {
"backgroundColor": "transparent",
"fontFamily": "System",
"fontSize": 12,
"fontWeight": "400",
"paddingBottom": 0,
"paddingHorizontal": 4,
"paddingTop": 0,
"textAlign": "left",
},
Object {
"color": "#6C727A",
},
]
}
>
HQ
</Text>
</View>
<View
style={
Array [
Object {
"borderRadius": 4,
"height": 16,
"marginRight": 4,
"marginTop": 8,
},
Object {
"backgroundColor": "#E6E6E7",
},
]
}
>
<Text
style={
Array [
Object {
"backgroundColor": "transparent",
"fontFamily": "System",
"fontSize": 12,
"fontWeight": "400",
"paddingBottom": 0,
"paddingHorizontal": 4,
"paddingTop": 0,
"textAlign": "left",
},
Object {
"color": "#6C727A",
},
]
}
>
Problem in Product Y
</Text>
</View>
<View
style={
Array [
Object {
"borderRadius": 4,
"height": 16,
"marginRight": 4,
"marginTop": 8,
},
Object {
"backgroundColor": "#E6E6E7",
},
]
}
>
<Text
style={
Array [
Object {
"backgroundColor": "transparent",
"fontFamily": "System",
"fontSize": 12,
"fontWeight": "400",
"paddingBottom": 0,
"paddingHorizontal": 4,
"paddingTop": 0,
"textAlign": "left",
},
Object {
"color": "#6C727A",
},
]
}
>
HQ
</Text>
</View>
<View
style={
Array [
Object {
"borderRadius": 4,
"height": 16,
"marginRight": 4,
"marginTop": 8,
},
Object {
"backgroundColor": "#E6E6E7",
},
]
}
>
<Text
style={
Array [
Object {
"backgroundColor": "transparent",
"fontFamily": "System",
"fontSize": 12,
"fontWeight": "400",
"paddingBottom": 0,
"paddingHorizontal": 4,
"paddingTop": 0,
"textAlign": "left",
},
Object {
"color": "#6C727A",
},
]
}
>
Closed
</Text>
</View>
<View
style={
Array [
Object {
"borderRadius": 4,
"height": 16,
"marginRight": 4,
"marginTop": 8,
},
Object {
"backgroundColor": "#E6E6E7",
},
]
}
>
<Text
style={
Array [
Object {
"backgroundColor": "transparent",
"fontFamily": "System",
"fontSize": 12,
"fontWeight": "400",
"paddingBottom": 0,
"paddingHorizontal": 4,
"paddingTop": 0,
"textAlign": "left",
},
Object {
"color": "#6C727A",
},
]
}
>
Problem in Product Y
</Text>
</View>
</View>
</View>,
]
`;
exports[`Storyshots Header Buttons badge 1`] = ` exports[`Storyshots Header Buttons badge 1`] = `
<RNCSafeAreaView <RNCSafeAreaView
edges={ edges={
@ -4693,7 +5267,7 @@ exports[`Storyshots List pressable 1`] = `
"justifyContent": "center", "justifyContent": "center",
"paddingHorizontal": 12, "paddingHorizontal": 12,
}, },
false, undefined,
Object { Object {
"height": 92, "height": 92,
}, },
@ -6211,7 +6785,7 @@ exports[`Storyshots List with bigger font 1`] = `
"justifyContent": "center", "justifyContent": "center",
"paddingHorizontal": 12, "paddingHorizontal": 12,
}, },
false, undefined,
Object { Object {
"height": 69, "height": 69,
}, },
@ -6625,7 +7199,7 @@ exports[`Storyshots List with bigger font 1`] = `
"justifyContent": "center", "justifyContent": "center",
"paddingHorizontal": 12, "paddingHorizontal": 12,
}, },
false, undefined,
Object { Object {
"height": 69, "height": 69,
}, },
@ -7080,7 +7654,7 @@ exports[`Storyshots List with black theme 1`] = `
"justifyContent": "center", "justifyContent": "center",
"paddingHorizontal": 12, "paddingHorizontal": 12,
}, },
false, undefined,
Object { Object {
"height": 92, "height": 92,
}, },
@ -7494,7 +8068,7 @@ exports[`Storyshots List with black theme 1`] = `
"justifyContent": "center", "justifyContent": "center",
"paddingHorizontal": 12, "paddingHorizontal": 12,
}, },
false, undefined,
Object { Object {
"height": 92, "height": 92,
}, },
@ -7972,7 +8546,7 @@ exports[`Storyshots List with custom colors 1`] = `
"justifyContent": "center", "justifyContent": "center",
"paddingHorizontal": 12, "paddingHorizontal": 12,
}, },
false, undefined,
Object { Object {
"height": 92, "height": 92,
}, },
@ -8129,7 +8703,7 @@ exports[`Storyshots List with dark theme 1`] = `
"justifyContent": "center", "justifyContent": "center",
"paddingHorizontal": 12, "paddingHorizontal": 12,
}, },
false, undefined,
Object { Object {
"height": 92, "height": 92,
}, },
@ -8543,7 +9117,7 @@ exports[`Storyshots List with dark theme 1`] = `
"justifyContent": "center", "justifyContent": "center",
"paddingHorizontal": 12, "paddingHorizontal": 12,
}, },
false, undefined,
Object { Object {
"height": 92, "height": 92,
}, },
@ -10410,7 +10984,7 @@ exports[`Storyshots List with small font 1`] = `
"justifyContent": "center", "justifyContent": "center",
"paddingHorizontal": 12, "paddingHorizontal": 12,
}, },
false, undefined,
Object { Object {
"height": 36.800000000000004, "height": 36.800000000000004,
}, },
@ -10824,7 +11398,7 @@ exports[`Storyshots List with small font 1`] = `
"justifyContent": "center", "justifyContent": "center",
"paddingHorizontal": 12, "paddingHorizontal": 12,
}, },
false, undefined,
Object { Object {
"height": 36.800000000000004, "height": 36.800000000000004,
}, },
@ -40581,6 +41155,7 @@ exports[`Storyshots Message Show a button as attachment 1`] = `
"color": "#ffffff", "color": "#ffffff",
}, },
undefined, undefined,
undefined,
] ]
} }
> >

View File

@ -25,7 +25,7 @@ import com.android.build.OutputFile
* bundleAssetName: "index.android.bundle", * bundleAssetName: "index.android.bundle",
* *
* // the entry file for bundle generation. If none specified and * // the entry file for bundle generation. If none specified and
* // "index.android.js" exists, it will be used. Otherwise "index.js" is * // "index.android.js" exists, it will be used. Otherwise "index.tsx" is
* // default. Can be overridden with ENTRY_FILE environment variable. * // default. Can be overridden with ENTRY_FILE environment variable.
* entryFile: "index.android.js", * entryFile: "index.android.js",
* *
@ -144,7 +144,7 @@ android {
minSdkVersion rootProject.ext.minSdkVersion minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion
versionCode VERSIONCODE as Integer versionCode VERSIONCODE as Integer
versionName "4.19.0" versionName "4.20.0"
vectorDrawables.useSupportLibrary = true vectorDrawables.useSupportLibrary = true
if (!isFoss) { if (!isFoss) {
manifestPlaceholders = [BugsnagAPIKey: BugsnagAPIKey as String] manifestPlaceholders = [BugsnagAPIKey: BugsnagAPIKey as String]

View File

@ -1,5 +1,5 @@
{ {
"name": "RocketChatRN", "name": "RocketChatRN",
"share": "ShareRocketChatRN", "share": "ShareRocketChatRN",
"displayName": "RocketChatRN" "displayName": "RocketChatRN"
} }

View File

@ -1,21 +1,15 @@
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types';
import { NavigationContainer } from '@react-navigation/native'; import { NavigationContainer } from '@react-navigation/native';
import { createStackNavigator } from '@react-navigation/stack'; import { createStackNavigator } from '@react-navigation/stack';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import Navigation from './lib/Navigation'; import Navigation from './lib/Navigation';
import { defaultHeader, getActiveRouteName, navigationTheme } from './utils/navigation'; import { defaultHeader, getActiveRouteName, navigationTheme } from './utils/navigation';
import { import { ROOT_INSIDE, ROOT_LOADING, ROOT_OUTSIDE, ROOT_SET_USERNAME } from './actions/app';
ROOT_LOADING, ROOT_OUTSIDE, ROOT_NEW_SERVER, ROOT_INSIDE, ROOT_SET_USERNAME
} from './actions/app';
// Stacks // Stacks
import AuthLoadingView from './views/AuthLoadingView'; import AuthLoadingView from './views/AuthLoadingView';
// SetUsername Stack // SetUsername Stack
import SetUsernameView from './views/SetUsernameView'; import SetUsernameView from './views/SetUsernameView';
import OutsideStack from './stacks/OutsideStack'; import OutsideStack from './stacks/OutsideStack';
import InsideStack from './stacks/InsideStack'; import InsideStack from './stacks/InsideStack';
import MasterDetailStack from './stacks/MasterDetailStack'; import MasterDetailStack from './stacks/MasterDetailStack';
@ -26,16 +20,13 @@ import { setCurrentScreen } from './utils/log';
const SetUsername = createStackNavigator(); const SetUsername = createStackNavigator();
const SetUsernameStack = () => ( const SetUsernameStack = () => (
<SetUsername.Navigator screenOptions={defaultHeader}> <SetUsername.Navigator screenOptions={defaultHeader}>
<SetUsername.Screen <SetUsername.Screen name='SetUsernameView' component={SetUsernameView} />
name='SetUsernameView'
component={SetUsernameView}
/>
</SetUsername.Navigator> </SetUsername.Navigator>
); );
// App // App
const Stack = createStackNavigator(); const Stack = createStackNavigator();
const App = React.memo(({ root, isMasterDetail }) => { const App = React.memo(({ root, isMasterDetail }: { root: string; isMasterDetail: boolean }) => {
if (!root) { if (!root) {
return null; return null;
} }
@ -54,61 +45,32 @@ const App = React.memo(({ root, isMasterDetail }) => {
<NavigationContainer <NavigationContainer
theme={navTheme} theme={navTheme}
ref={Navigation.navigationRef} ref={Navigation.navigationRef}
onStateChange={(state) => { onStateChange={state => {
const previousRouteName = Navigation.routeNameRef.current; const previousRouteName = Navigation.routeNameRef.current;
const currentRouteName = getActiveRouteName(state); const currentRouteName = getActiveRouteName(state);
if (previousRouteName !== currentRouteName) { if (previousRouteName !== currentRouteName) {
setCurrentScreen(currentRouteName); setCurrentScreen(currentRouteName);
} }
Navigation.routeNameRef.current = currentRouteName; Navigation.routeNameRef.current = currentRouteName;
}} }}>
>
<Stack.Navigator screenOptions={{ headerShown: false, animationEnabled: false }}> <Stack.Navigator screenOptions={{ headerShown: false, animationEnabled: false }}>
<> <>
{root === ROOT_LOADING ? ( {root === ROOT_LOADING ? <Stack.Screen name='AuthLoading' component={AuthLoadingView} /> : null}
<Stack.Screen {root === ROOT_OUTSIDE ? <Stack.Screen name='OutsideStack' component={OutsideStack} /> : null}
name='AuthLoading'
component={AuthLoadingView}
/>
) : null}
{root === ROOT_OUTSIDE || root === ROOT_NEW_SERVER ? (
<Stack.Screen
name='OutsideStack'
component={OutsideStack}
/>
) : null}
{root === ROOT_INSIDE && isMasterDetail ? ( {root === ROOT_INSIDE && isMasterDetail ? (
<Stack.Screen <Stack.Screen name='MasterDetailStack' component={MasterDetailStack} />
name='MasterDetailStack'
component={MasterDetailStack}
/>
) : null}
{root === ROOT_INSIDE && !isMasterDetail ? (
<Stack.Screen
name='InsideStack'
component={InsideStack}
/>
) : null}
{root === ROOT_SET_USERNAME ? (
<Stack.Screen
name='SetUsernameStack'
component={SetUsernameStack}
/>
) : null} ) : null}
{root === ROOT_INSIDE && !isMasterDetail ? <Stack.Screen name='InsideStack' component={InsideStack} /> : null}
{root === ROOT_SET_USERNAME ? <Stack.Screen name='SetUsernameStack' component={SetUsernameStack} /> : null}
</> </>
</Stack.Navigator> </Stack.Navigator>
</NavigationContainer> </NavigationContainer>
); );
}); });
const mapStateToProps = state => ({ const mapStateToProps = (state: any) => ({
root: state.app.root, root: state.app.root,
isMasterDetail: state.app.isMasterDetail isMasterDetail: state.app.isMasterDetail
}); });
App.propTypes = {
root: PropTypes.string,
isMasterDetail: PropTypes.bool
};
const AppContainer = connect(mapStateToProps)(App); const AppContainer = connect(mapStateToProps)(App);
export default AppContainer; export default AppContainer;

View File

@ -2,21 +2,16 @@
import { NativeModules } from 'react-native'; import { NativeModules } from 'react-native';
import Reactotron from 'reactotron-react-native'; import Reactotron from 'reactotron-react-native';
import { reactotronRedux } from 'reactotron-redux'; import { reactotronRedux } from 'reactotron-redux';
import sagaPlugin from 'reactotron-redux-saga' import sagaPlugin from 'reactotron-redux-saga';
if (__DEV__) { if (__DEV__) {
const scriptURL = NativeModules.SourceCode.scriptURL; const scriptURL = NativeModules.SourceCode.scriptURL;
const scriptHostname = scriptURL.split('://')[1].split(':')[0]; const scriptHostname = scriptURL.split('://')[1].split(':')[0];
Reactotron Reactotron.configure({ host: scriptHostname }).useReactNative().use(reactotronRedux()).use(sagaPlugin()).connect();
.configure({ host: scriptHostname }) // Running on android device
.useReactNative() // $ adb reverse tcp:9090 tcp:9090
.use(reactotronRedux()) Reactotron.clear();
.use(sagaPlugin()) console.warn = Reactotron.log;
.connect(); console.log = Reactotron.log;
// Running on android device console.disableYellowBox = true;
// $ adb reverse tcp:9090 tcp:9090
Reactotron.clear();
console.warn = Reactotron.log;
console.log = Reactotron.log;
console.disableYellowBox = true;
} }

View File

@ -4,23 +4,13 @@ const FAILURE = 'FAILURE';
const defaultTypes = [REQUEST, SUCCESS, FAILURE]; const defaultTypes = [REQUEST, SUCCESS, FAILURE];
function createRequestTypes(base, types = defaultTypes) { function createRequestTypes(base, types = defaultTypes) {
const res = {}; const res = {};
types.forEach(type => (res[type] = `${ base }_${ type }`)); types.forEach(type => (res[type] = `${base}_${type}`));
return res; return res;
} }
// Login events // Login events
export const LOGIN = createRequestTypes('LOGIN', [ export const LOGIN = createRequestTypes('LOGIN', [...defaultTypes, 'SET_SERVICES', 'SET_PREFERENCE', 'SET_LOCAL_AUTHENTICATED']);
...defaultTypes, export const SHARE = createRequestTypes('SHARE', ['SELECT_SERVER', 'SET_USER', 'SET_SETTINGS', 'SET_SERVER_INFO']);
'SET_SERVICES',
'SET_PREFERENCE',
'SET_LOCAL_AUTHENTICATED'
]);
export const SHARE = createRequestTypes('SHARE', [
'SELECT_SERVER',
'SET_USER',
'SET_SETTINGS',
'SET_SERVER_INFO'
]);
export const USER = createRequestTypes('USER', ['SET']); export const USER = createRequestTypes('USER', ['SET']);
export const ROOMS = createRequestTypes('ROOMS', [ export const ROOMS = createRequestTypes('ROOMS', [
...defaultTypes, ...defaultTypes,
@ -33,8 +23,24 @@ export const ROOMS = createRequestTypes('ROOMS', [
'OPEN_SEARCH_HEADER', 'OPEN_SEARCH_HEADER',
'CLOSE_SEARCH_HEADER' 'CLOSE_SEARCH_HEADER'
]); ]);
export const ROOM = createRequestTypes('ROOM', ['SUBSCRIBE', 'UNSUBSCRIBE', 'LEAVE', 'DELETE', 'REMOVED', 'CLOSE', 'FORWARD', 'USER_TYPING']); export const ROOM = createRequestTypes('ROOM', [
export const INQUIRY = createRequestTypes('INQUIRY', [...defaultTypes, 'SET_ENABLED', 'RESET', 'QUEUE_ADD', 'QUEUE_UPDATE', 'QUEUE_REMOVE']); 'SUBSCRIBE',
'UNSUBSCRIBE',
'LEAVE',
'DELETE',
'REMOVED',
'CLOSE',
'FORWARD',
'USER_TYPING'
]);
export const INQUIRY = createRequestTypes('INQUIRY', [
...defaultTypes,
'SET_ENABLED',
'RESET',
'QUEUE_ADD',
'QUEUE_UPDATE',
'QUEUE_REMOVE'
]);
export const APP = createRequestTypes('APP', ['START', 'READY', 'INIT', 'INIT_LOCAL_SETTINGS', 'SET_MASTER_DETAIL']); export const APP = createRequestTypes('APP', ['START', 'READY', 'INIT', 'INIT_LOCAL_SETTINGS', 'SET_MASTER_DETAIL']);
export const MESSAGES = createRequestTypes('MESSAGES', ['REPLY_BROADCAST']); export const MESSAGES = createRequestTypes('MESSAGES', ['REPLY_BROADCAST']);
export const CREATE_CHANNEL = createRequestTypes('CREATE_CHANNEL', [...defaultTypes]); export const CREATE_CHANNEL = createRequestTypes('CREATE_CHANNEL', [...defaultTypes]);

View File

@ -3,7 +3,6 @@ import { APP } from './actionsTypes';
export const ROOT_OUTSIDE = 'outside'; export const ROOT_OUTSIDE = 'outside';
export const ROOT_INSIDE = 'inside'; export const ROOT_INSIDE = 'inside';
export const ROOT_LOADING = 'loading'; export const ROOT_LOADING = 'loading';
export const ROOT_NEW_SERVER = 'newServer';
export const ROOT_SET_USERNAME = 'setUsername'; export const ROOT_SET_USERNAME = 'setUsername';
export function appStart({ root, ...args }) { export function appStart({ root, ...args }) {

View File

@ -32,7 +32,6 @@ export function inviteLinksClear() {
}; };
} }
export function inviteLinksCreate(rid) { export function inviteLinksCreate(rid) {
return { return {
type: types.INVITE_LINKS.CREATE, type: types.INVITE_LINKS.CREATE,

View File

@ -1,6 +1,5 @@
import * as types from './actionsTypes'; import * as types from './actionsTypes';
export function roomsRequest(params = { allData: false }) { export function roomsRequest(params = { allData: false }) {
return { return {
type: types.ROOMS.REQUEST, type: types.ROOMS.REQUEST,

View File

@ -125,15 +125,15 @@ const keyCommands = [
discoverabilityTitle: I18n.t('Add_server') discoverabilityTitle: I18n.t('Add_server')
}, },
// Refers to select rooms on list // Refers to select rooms on list
...([1, 2, 3, 4, 5, 6, 7, 8, 9].map(value => ({ ...[1, 2, 3, 4, 5, 6, 7, 8, 9].map(value => ({
input: `${ value }`, input: `${value}`,
modifierFlags: constants.keyModifierCommand modifierFlags: constants.keyModifierCommand
}))), })),
// Refers to select servers on list // Refers to select servers on list
...([1, 2, 3, 4, 5, 6, 7, 8, 9].map(value => ({ ...[1, 2, 3, 4, 5, 6, 7, 8, 9].map(value => ({
input: `${ value }`, input: `${value}`,
modifierFlags: constants.keyModifierCommand | constants.keyModifierAlternate modifierFlags: constants.keyModifierCommand | constants.keyModifierAlternate
}))) }))
]; ];
export const setKeyCommands = () => KeyCommands.setKeyCommands(keyCommands); export const setKeyCommands = () => KeyCommands.setKeyCommands(keyCommands);
@ -161,7 +161,8 @@ export const handleCommandSubmit = event => commandHandle(event, KEY_SEND_MESSAG
export const handleCommandShowUpload = event => commandHandle(event, KEY_UPLOAD, ['command']); export const handleCommandShowUpload = event => commandHandle(event, KEY_UPLOAD, ['command']);
export const handleCommandScroll = event => commandHandle(event, [constants.keyInputUpArrow, constants.keyInputDownArrow], ['alternate']); export const handleCommandScroll = event =>
commandHandle(event, [constants.keyInputUpArrow, constants.keyInputDownArrow], ['alternate']);
export const handleCommandRoomActions = event => commandHandle(event, KEY_ROOM_ACTIONS, ['command']); export const handleCommandRoomActions = event => commandHandle(event, KEY_ROOM_ACTIONS, ['command']);

View File

@ -1,4 +1,4 @@
export const STATUS_COLORS = { export const STATUS_COLORS: any = {
online: '#2de0a5', online: '#2de0a5',
busy: '#f5455c', busy: '#f5455c',
away: '#ffd21f', away: '#ffd21f',
@ -19,7 +19,7 @@ const mentions = {
mentionOtherColor: '#F3BE08' mentionOtherColor: '#F3BE08'
}; };
export const themes = { export const themes: any = {
light: { light: {
backgroundColor: '#ffffff', backgroundColor: '#ffffff',
focusedBackground: '#ffffff', focusedBackground: '#ffffff',

View File

@ -2,8 +2,10 @@ import { getBundleId, isIOS } from '../utils/deviceInfo';
const APP_STORE_ID = '1148741252'; const APP_STORE_ID = '1148741252';
export const PLAY_MARKET_LINK = `https://play.google.com/store/apps/details?id=${ getBundleId }`; export const PLAY_MARKET_LINK = `https://play.google.com/store/apps/details?id=${getBundleId}`;
export const FDROID_MARKET_LINK = 'https://f-droid.org/en/packages/chat.rocket.android'; export const FDROID_MARKET_LINK = 'https://f-droid.org/en/packages/chat.rocket.android';
export const APP_STORE_LINK = `https://itunes.apple.com/app/id${ APP_STORE_ID }`; export const APP_STORE_LINK = `https://itunes.apple.com/app/id${APP_STORE_ID}`;
export const LICENSE_LINK = 'https://github.com/RocketChat/Rocket.Chat.ReactNative/blob/develop/LICENSE'; export const LICENSE_LINK = 'https://github.com/RocketChat/Rocket.Chat.ReactNative/blob/develop/LICENSE';
export const STORE_REVIEW_LINK = isIOS ? `itms-apps://itunes.apple.com/app/id${ APP_STORE_ID }?action=write-review` : `market://details?id=${ getBundleId }`; export const STORE_REVIEW_LINK = isIOS
? `itms-apps://itunes.apple.com/app/id${APP_STORE_ID}?action=write-review`
: `market://details?id=${getBundleId}`;

View File

@ -202,5 +202,8 @@ export default {
}, },
Jitsi_Enable_Channels: { Jitsi_Enable_Channels: {
type: 'valuesAsBoolean' type: 'valuesAsBoolean'
},
Canned_Responses_Enable: {
type: 'valueAsBoolean'
} }
}; };

View File

@ -1,208 +0,0 @@
import React, {
useRef,
useState,
useEffect,
forwardRef,
useImperativeHandle,
useCallback,
isValidElement
} from 'react';
import PropTypes from 'prop-types';
import { Keyboard, Text } from 'react-native';
import { useSafeAreaInsets } from 'react-native-safe-area-context';
import { TapGestureHandler, State } from 'react-native-gesture-handler';
import ScrollBottomSheet from 'react-native-scroll-bottom-sheet';
import Animated, {
Extrapolate,
interpolate,
Value,
Easing
} from 'react-native-reanimated';
import * as Haptics from 'expo-haptics';
import { useBackHandler } from '@react-native-community/hooks';
import { Item } from './Item';
import { Handle } from './Handle';
import { Button } from './Button';
import { themes } from '../../constants/colors';
import styles, { ITEM_HEIGHT } from './styles';
import { isTablet, isIOS } from '../../utils/deviceInfo';
import * as List from '../List';
import I18n from '../../i18n';
import { useOrientation, useDimensions } from '../../dimensions';
const getItemLayout = (data, index) => ({ length: ITEM_HEIGHT, offset: ITEM_HEIGHT * index, index });
const HANDLE_HEIGHT = isIOS ? 40 : 56;
const MAX_SNAP_HEIGHT = 16;
const CANCEL_HEIGHT = 64;
const ANIMATION_DURATION = 250;
const ANIMATION_CONFIG = {
duration: ANIMATION_DURATION,
// https://easings.net/#easeInOutCubic
easing: Easing.bezier(0.645, 0.045, 0.355, 1.0)
};
const ActionSheet = React.memo(forwardRef(({ children, theme }, ref) => {
const bottomSheetRef = useRef();
const [data, setData] = useState({});
const [isVisible, setVisible] = useState(false);
const { height } = useDimensions();
const { isLandscape } = useOrientation();
const insets = useSafeAreaInsets();
const maxSnap = Math.max(
(
height
// Items height
- (ITEM_HEIGHT * (data?.options?.length || 0))
// Handle height
- HANDLE_HEIGHT
// Custom header height
- (data?.headerHeight || 0)
// Insets bottom height (Notch devices)
- insets.bottom
// Cancel button height
- (data?.hasCancel ? CANCEL_HEIGHT : 0)
),
MAX_SNAP_HEIGHT
);
/*
* if the action sheet cover more
* than 60% of the whole screen
* and it's not at the landscape mode
* we'll provide more one snap
* that point 50% of the whole screen
*/
const snaps = (height - maxSnap > height * 0.6) && !isLandscape ? [maxSnap, height * 0.5, height] : [maxSnap, height];
const openedSnapIndex = snaps.length > 2 ? 1 : 0;
const closedSnapIndex = snaps.length - 1;
const toggleVisible = () => setVisible(!isVisible);
const hide = () => {
bottomSheetRef.current?.snapTo(closedSnapIndex);
};
const show = (options) => {
setData(options);
toggleVisible();
};
const onBackdropPressed = ({ nativeEvent }) => {
if (nativeEvent.oldState === State.ACTIVE) {
hide();
}
};
useBackHandler(() => {
if (isVisible) {
hide();
}
return isVisible;
});
useEffect(() => {
if (isVisible) {
Keyboard.dismiss();
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
bottomSheetRef.current?.snapTo(openedSnapIndex);
}
}, [isVisible]);
// Hides action sheet when orientation changes
useEffect(() => {
setVisible(false);
}, [isLandscape]);
useImperativeHandle(ref, () => ({
showActionSheet: show,
hideActionSheet: hide
}));
const renderHandle = useCallback(() => (
<>
<Handle theme={theme} />
{isValidElement(data?.customHeader) ? data.customHeader : null}
</>
));
const renderFooter = useCallback(() => (data?.hasCancel ? (
<Button
onPress={hide}
style={[styles.button, { backgroundColor: themes[theme].auxiliaryBackground }]}
theme={theme}
>
<Text style={[styles.text, { color: themes[theme].bodyText }]}>
{I18n.t('Cancel')}
</Text>
</Button>
) : null));
const renderItem = useCallback(({ item }) => <Item item={item} hide={hide} theme={theme} />);
const animatedPosition = React.useRef(new Value(0));
const opacity = interpolate(animatedPosition.current, {
inputRange: [0, 1],
outputRange: [0, themes[theme].backdropOpacity],
extrapolate: Extrapolate.CLAMP
});
return (
<>
{children}
{isVisible && (
<>
<TapGestureHandler onHandlerStateChange={onBackdropPressed}>
<Animated.View
testID='action-sheet-backdrop'
style={[
styles.backdrop,
{
backgroundColor: themes[theme].backdropColor,
opacity
}
]}
/>
</TapGestureHandler>
<ScrollBottomSheet
testID='action-sheet'
ref={bottomSheetRef}
componentType='FlatList'
snapPoints={snaps}
initialSnapIndex={closedSnapIndex}
renderHandle={renderHandle}
onSettle={index => (index === closedSnapIndex) && toggleVisible()}
animatedPosition={animatedPosition.current}
containerStyle={[
styles.container,
{ backgroundColor: themes[theme].focusedBackground },
(isLandscape || isTablet) && styles.bottomSheet
]}
animationConfig={ANIMATION_CONFIG}
// FlatList props
data={data?.options}
renderItem={renderItem}
keyExtractor={item => item.title}
style={{ backgroundColor: themes[theme].focusedBackground }}
contentContainerStyle={styles.content}
ItemSeparatorComponent={List.Separator}
ListHeaderComponent={List.Separator}
ListFooterComponent={renderFooter}
getItemLayout={getItemLayout}
removeClippedSubviews={isIOS}
/>
</>
)}
</>
);
}));
ActionSheet.propTypes = {
children: PropTypes.node,
theme: PropTypes.string
};
export default ActionSheet;

View File

@ -0,0 +1,194 @@
import React, { forwardRef, isValidElement, useEffect, useImperativeHandle, useRef, useState } from 'react';
import { Keyboard, Text } from 'react-native';
import { useSafeAreaInsets } from 'react-native-safe-area-context';
import { State, TapGestureHandler } from 'react-native-gesture-handler';
import ScrollBottomSheet from 'react-native-scroll-bottom-sheet';
import Animated, { Easing, Extrapolate, Value, interpolate } from 'react-native-reanimated';
import * as Haptics from 'expo-haptics';
import { useBackHandler } from '@react-native-community/hooks';
import { Item } from './Item';
import { Handle } from './Handle';
import { Button } from './Button';
import { themes } from '../../constants/colors';
import styles, { ITEM_HEIGHT } from './styles';
import { isIOS, isTablet } from '../../utils/deviceInfo';
import * as List from '../List';
import I18n from '../../i18n';
import { IDimensionsContextProps, useDimensions, useOrientation } from '../../dimensions';
interface IActionSheetData {
options: any;
headerHeight?: number;
hasCancel?: boolean;
customHeader: any;
}
const getItemLayout = (data: any, index: number) => ({ length: ITEM_HEIGHT, offset: ITEM_HEIGHT * index, index });
const HANDLE_HEIGHT = isIOS ? 40 : 56;
const MAX_SNAP_HEIGHT = 16;
const CANCEL_HEIGHT = 64;
const ANIMATION_DURATION = 250;
const ANIMATION_CONFIG = {
duration: ANIMATION_DURATION,
// https://easings.net/#easeInOutCubic
easing: Easing.bezier(0.645, 0.045, 0.355, 1.0)
};
const ActionSheet = React.memo(
forwardRef(({ children, theme }: { children: JSX.Element; theme: string }, ref) => {
const bottomSheetRef: any = useRef();
const [data, setData] = useState<IActionSheetData>({} as IActionSheetData);
const [isVisible, setVisible] = useState(false);
const { height }: Partial<IDimensionsContextProps> = useDimensions();
const { isLandscape } = useOrientation();
const insets = useSafeAreaInsets();
const maxSnap = Math.max(
height! -
// Items height
ITEM_HEIGHT * (data?.options?.length || 0) -
// Handle height
HANDLE_HEIGHT -
// Custom header height
(data?.headerHeight || 0) -
// Insets bottom height (Notch devices)
insets.bottom -
// Cancel button height
(data?.hasCancel ? CANCEL_HEIGHT : 0),
MAX_SNAP_HEIGHT
);
/*
* if the action sheet cover more
* than 60% of the whole screen
* and it's not at the landscape mode
* we'll provide more one snap
* that point 50% of the whole screen
*/
const snaps: any = height! - maxSnap > height! * 0.6 && !isLandscape ? [maxSnap, height! * 0.5, height] : [maxSnap, height];
const openedSnapIndex = snaps.length > 2 ? 1 : 0;
const closedSnapIndex = snaps.length - 1;
const toggleVisible = () => setVisible(!isVisible);
const hide = () => {
bottomSheetRef.current?.snapTo(closedSnapIndex);
};
const show = (options: any) => {
setData(options);
toggleVisible();
};
const onBackdropPressed = ({ nativeEvent }: any) => {
if (nativeEvent.oldState === State.ACTIVE) {
hide();
}
};
useBackHandler(() => {
if (isVisible) {
hide();
}
return isVisible;
});
useEffect(() => {
if (isVisible) {
Keyboard.dismiss();
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
bottomSheetRef.current?.snapTo(openedSnapIndex);
}
}, [isVisible]);
// Hides action sheet when orientation changes
useEffect(() => {
setVisible(false);
}, [isLandscape]);
useImperativeHandle(ref, () => ({
showActionSheet: show,
hideActionSheet: hide
}));
const renderHandle = () => (
<>
<Handle theme={theme} />
{isValidElement(data?.customHeader) ? data.customHeader : null}
</>
);
const renderFooter = () =>
data?.hasCancel ? (
<Button onPress={hide} style={[styles.button, { backgroundColor: themes[theme].auxiliaryBackground }]} theme={theme}>
<Text style={[styles.text, { color: themes[theme].bodyText }]}>{I18n.t('Cancel')}</Text>
</Button>
) : null;
const renderItem = ({ item }: any) => <Item item={item} hide={hide} theme={theme} />;
const animatedPosition = React.useRef(new Value(0));
const opacity = interpolate(animatedPosition.current, {
inputRange: [0, 1],
outputRange: [0, themes[theme].backdropOpacity],
extrapolate: Extrapolate.CLAMP
});
return (
<>
{children}
{isVisible && (
<>
<TapGestureHandler onHandlerStateChange={onBackdropPressed}>
<Animated.View
testID='action-sheet-backdrop'
style={[
styles.backdrop,
{
backgroundColor: themes[theme].backdropColor,
opacity
}
]}
/>
</TapGestureHandler>
<ScrollBottomSheet
testID='action-sheet'
ref={bottomSheetRef}
componentType='FlatList'
snapPoints={snaps}
initialSnapIndex={closedSnapIndex}
renderHandle={renderHandle}
onSettle={index => index === closedSnapIndex && toggleVisible()}
animatedPosition={animatedPosition.current}
containerStyle={
[
styles.container,
{ backgroundColor: themes[theme].focusedBackground },
(isLandscape || isTablet) && styles.bottomSheet
] as any
}
animationConfig={ANIMATION_CONFIG}
// FlatList props
data={data?.options}
renderItem={renderItem}
keyExtractor={(item: any) => item.title}
style={{ backgroundColor: themes[theme].focusedBackground }}
contentContainerStyle={styles.content}
ItemSeparatorComponent={List.Separator}
ListHeaderComponent={List.Separator}
ListFooterComponent={renderFooter}
getItemLayout={getItemLayout}
removeClippedSubviews={isIOS}
/>
</>
)}
</>
);
})
);
export default ActionSheet;

View File

@ -1,15 +1,11 @@
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types';
import { View } from 'react-native'; import { View } from 'react-native';
import styles from './styles'; import styles from './styles';
import { themes } from '../../constants/colors'; import { themes } from '../../constants/colors';
export const Handle = React.memo(({ theme }) => ( export const Handle = React.memo(({ theme }: { theme: string }) => (
<View style={[styles.handle, { backgroundColor: themes[theme].focusedBackground }]} testID='action-sheet-handle'> <View style={[styles.handle, { backgroundColor: themes[theme].focusedBackground }]} testID='action-sheet-handle'>
<View style={[styles.handleIndicator, { backgroundColor: themes[theme].auxiliaryText }]} /> <View style={[styles.handleIndicator, { backgroundColor: themes[theme].auxiliaryText }]} />
</View> </View>
)); ));
Handle.propTypes = {
theme: PropTypes.string
};

View File

@ -1,13 +1,25 @@
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types';
import { Text, View } from 'react-native'; import { Text, View } from 'react-native';
import { themes } from '../../constants/colors'; import { themes } from '../../constants/colors';
import { CustomIcon } from '../../lib/Icons'; import { CustomIcon } from '../../lib/Icons';
import styles from './styles';
import { Button } from './Button'; import { Button } from './Button';
import styles from './styles';
export const Item = React.memo(({ item, hide, theme }) => { interface IActionSheetItem {
item: {
title: string;
icon: string;
danger: boolean;
testID: string;
onPress(): void;
right: Function;
};
theme: string;
hide(): void;
}
export const Item = React.memo(({ item, hide, theme }: IActionSheetItem) => {
const onPress = () => { const onPress = () => {
hide(); hide();
item?.onPress(); item?.onPress();
@ -18,34 +30,16 @@ export const Item = React.memo(({ item, hide, theme }) => {
onPress={onPress} onPress={onPress}
style={[styles.item, { backgroundColor: themes[theme].focusedBackground }]} style={[styles.item, { backgroundColor: themes[theme].focusedBackground }]}
theme={theme} theme={theme}
testID={item.testID} testID={item.testID}>
>
<CustomIcon name={item.icon} size={20} color={item.danger ? themes[theme].dangerColor : themes[theme].bodyText} /> <CustomIcon name={item.icon} size={20} color={item.danger ? themes[theme].dangerColor : themes[theme].bodyText} />
<View style={styles.titleContainer}> <View style={styles.titleContainer}>
<Text <Text
numberOfLines={1} numberOfLines={1}
style={[styles.title, { color: item.danger ? themes[theme].dangerColor : themes[theme].bodyText }]} style={[styles.title, { color: item.danger ? themes[theme].dangerColor : themes[theme].bodyText }]}>
>
{item.title} {item.title}
</Text> </Text>
</View> </View>
{ item.right ? ( {item.right ? <View style={styles.rightContainer}>{item.right ? item.right() : null}</View> : null}
<View style={styles.rightContainer}>
{item.right ? item.right() : null}
</View>
) : null }
</Button> </Button>
); );
}); });
Item.propTypes = {
item: PropTypes.shape({
title: PropTypes.string,
icon: PropTypes.string,
danger: PropTypes.bool,
onPress: PropTypes.func,
right: PropTypes.func,
testID: PropTypes.string
}),
hide: PropTypes.func,
theme: PropTypes.string
};

View File

@ -1,45 +0,0 @@
import React, { useRef, useContext, forwardRef } from 'react';
import PropTypes from 'prop-types';
import ActionSheet from './ActionSheet';
import { useTheme } from '../../theme';
const context = React.createContext({
showActionSheet: () => {},
hideActionSheet: () => {}
});
export const useActionSheet = () => useContext(context);
const { Provider, Consumer } = context;
export const withActionSheet = Component => forwardRef((props, ref) => (
<Consumer>
{contexts => <Component {...props} {...contexts} ref={ref} />}
</Consumer>
));
export const ActionSheetProvider = React.memo(({ children }) => {
const ref = useRef();
const { theme } = useTheme();
const getContext = () => ({
showActionSheet: (options) => {
ref.current?.showActionSheet(options);
},
hideActionSheet: () => {
ref.current?.hideActionSheet();
}
});
return (
<Provider value={getContext()}>
<ActionSheet ref={ref} theme={theme}>
{children}
</ActionSheet>
</Provider>
);
});
ActionSheetProvider.propTypes = {
children: PropTypes.node
};

View File

@ -0,0 +1,45 @@
import React, { ForwardedRef, forwardRef, useContext, useRef } from 'react';
import ActionSheet from './ActionSheet';
import { useTheme } from '../../theme';
interface IActionSheetProvider {
Provider: any;
Consumer: any;
}
const context: IActionSheetProvider = React.createContext({
showActionSheet: () => {},
hideActionSheet: () => {}
});
export const useActionSheet = () => useContext(context);
const { Provider, Consumer } = context;
export const withActionSheet = (Component: React.FC) =>
forwardRef((props: any, ref: ForwardedRef<any>) => (
<Consumer>{(contexts: any) => <Component {...props} {...contexts} ref={ref} />}</Consumer>
));
export const ActionSheetProvider = React.memo(({ children }: { children: JSX.Element | JSX.Element[] }) => {
const ref: ForwardedRef<any> = useRef();
const { theme }: any = useTheme();
const getContext = () => ({
showActionSheet: (options: any) => {
ref.current?.showActionSheet(options);
},
hideActionSheet: () => {
ref.current?.hideActionSheet();
}
});
return (
<Provider value={getContext()}>
<ActionSheet ref={ref} theme={theme}>
<>{children}</>
</ActionSheet>
</Provider>
);
});

View File

@ -1,40 +0,0 @@
import React from 'react';
import { ActivityIndicator, StyleSheet } from 'react-native';
import { PropTypes } from 'prop-types';
import { themes } from '../constants/colors';
const styles = StyleSheet.create({
indicator: {
padding: 16,
flex: 1
},
absolute: {
position: 'absolute',
left: 0,
right: 0,
top: 0,
bottom: 0,
alignItems: 'center',
justifyContent: 'center'
}
});
const RCActivityIndicator = ({ theme, absolute, ...props }) => (
<ActivityIndicator
style={[styles.indicator, absolute && styles.absolute]}
color={themes[theme].auxiliaryText}
{...props}
/>
);
RCActivityIndicator.propTypes = {
theme: PropTypes.string,
absolute: PropTypes.bool,
props: PropTypes.object
};
RCActivityIndicator.defaultProps = {
theme: 'light'
};
export default RCActivityIndicator;

View File

@ -0,0 +1,34 @@
import React from 'react';
import { ActivityIndicator, ActivityIndicatorProps, StyleSheet } from 'react-native';
import { themes } from '../constants/colors';
type TTheme = 'light' | 'dark' | 'black' | string;
interface IActivityIndicator extends ActivityIndicatorProps {
theme?: TTheme;
absolute?: boolean;
props?: object;
}
const styles = StyleSheet.create({
indicator: {
padding: 16,
flex: 1
},
absolute: {
position: 'absolute',
left: 0,
right: 0,
top: 0,
bottom: 0,
alignItems: 'center',
justifyContent: 'center'
}
});
const RCActivityIndicator = ({ theme = 'light', absolute, ...props }: IActivityIndicator) => (
<ActivityIndicator style={[styles.indicator, absolute && styles.absolute]} color={themes[theme].auxiliaryText} {...props} />
);
export default RCActivityIndicator;

View File

@ -1,6 +1,5 @@
import React from 'react'; import React from 'react';
import { StyleSheet, View, Text } from 'react-native'; import { StyleSheet, Text, View } from 'react-native';
import PropTypes from 'prop-types';
import { themes } from '../constants/colors'; import { themes } from '../constants/colors';
import sharedStyles from '../views/Styles'; import sharedStyles from '../views/Styles';
@ -21,14 +20,13 @@ const styles = StyleSheet.create({
} }
}); });
const AppVersion = React.memo(({ theme }) => ( const AppVersion = React.memo(({ theme }: { theme: string }) => (
<View style={styles.container}> <View style={styles.container}>
<Text style={[styles.text, { color: themes[theme].auxiliaryText }]}>{I18n.t('Version_no', { version: '' })}<Text style={styles.bold}>{getReadableVersion}</Text></Text> <Text style={[styles.text, { color: themes[theme].auxiliaryText }]}>
{I18n.t('Version_no', { version: '' })}
<Text style={styles.bold}>{getReadableVersion}</Text>
</Text>
</View> </View>
)); ));
AppVersion.propTypes = {
theme: PropTypes.string
};
export default AppVersion; export default AppVersion;

View File

@ -1,130 +0,0 @@
import React from 'react';
import PropTypes from 'prop-types';
import { View } from 'react-native';
import FastImage from '@rocket.chat/react-native-fast-image';
import Touchable from 'react-native-platform-touchable';
import { settings as RocketChatSettings } from '@rocket.chat/sdk';
import { avatarURL } from '../../utils/avatar';
import Emoji from '../markdown/Emoji';
const Avatar = React.memo(({
text,
size,
server,
borderRadius,
style,
avatar,
type,
children,
user,
onPress,
emoji,
theme,
getCustomEmoji,
avatarETag,
isStatic,
rid,
blockUnauthenticatedAccess,
serverVersion
}) => {
if ((!text && !avatar && !emoji && !rid) || !server) {
return null;
}
const avatarStyle = {
width: size,
height: size,
borderRadius
};
let image;
if (emoji) {
image = (
<Emoji
theme={theme}
baseUrl={server}
getCustomEmoji={getCustomEmoji}
isMessageContainsOnlyEmoji
literal={emoji}
style={avatarStyle}
/>
);
} else {
let uri = avatar;
if (!isStatic) {
uri = avatarURL({
type,
text,
size,
user,
avatar,
server,
avatarETag,
serverVersion,
rid,
blockUnauthenticatedAccess
});
}
image = (
<FastImage
style={avatarStyle}
source={{
uri,
headers: RocketChatSettings.customHeaders,
priority: FastImage.priority.high
}}
/>
);
}
if (onPress) {
image = (
<Touchable onPress={onPress}>
{image}
</Touchable>
);
}
return (
<View style={[avatarStyle, style]}>
{image}
{children}
</View>
);
});
Avatar.propTypes = {
server: PropTypes.string,
style: PropTypes.any,
text: PropTypes.string,
avatar: PropTypes.string,
emoji: PropTypes.string,
size: PropTypes.number,
borderRadius: PropTypes.number,
type: PropTypes.string,
children: PropTypes.object,
user: PropTypes.shape({
id: PropTypes.string,
token: PropTypes.string
}),
theme: PropTypes.string,
onPress: PropTypes.func,
getCustomEmoji: PropTypes.func,
avatarETag: PropTypes.string,
isStatic: PropTypes.bool,
rid: PropTypes.string,
blockUnauthenticatedAccess: PropTypes.bool,
serverVersion: PropTypes.string
};
Avatar.defaultProps = {
text: '',
size: 25,
type: 'd',
borderRadius: 4
};
export default Avatar;

View File

@ -0,0 +1,96 @@
import React from 'react';
import { View } from 'react-native';
import FastImage from '@rocket.chat/react-native-fast-image';
import Touchable from 'react-native-platform-touchable';
import { settings as RocketChatSettings } from '@rocket.chat/sdk';
import { avatarURL } from '../../utils/avatar';
import Emoji from '../markdown/Emoji';
import { IAvatar } from './interfaces';
const Avatar = React.memo(
({
server,
style,
avatar,
children,
user,
onPress,
emoji,
theme,
getCustomEmoji,
avatarETag,
isStatic,
rid,
blockUnauthenticatedAccess,
serverVersion,
text,
size = 25,
borderRadius = 4,
type = 'd'
}: Partial<IAvatar>) => {
if ((!text && !avatar && !emoji && !rid) || !server) {
return null;
}
const avatarStyle = {
width: size,
height: size,
borderRadius
};
let image;
if (emoji) {
image = (
<Emoji
theme={theme}
baseUrl={server}
getCustomEmoji={getCustomEmoji}
isMessageContainsOnlyEmoji
literal={emoji}
style={avatarStyle}
/>
);
} else {
let uri = avatar;
if (!isStatic) {
uri = avatarURL({
type,
text,
size,
user,
avatar,
server,
avatarETag,
serverVersion,
rid,
blockUnauthenticatedAccess
});
}
image = (
<FastImage
style={avatarStyle}
source={{
uri,
headers: RocketChatSettings.customHeaders,
priority: FastImage.priority.high
}}
/>
);
}
if (onPress) {
image = <Touchable onPress={onPress}>{image}</Touchable>;
}
return (
<View style={[avatarStyle, style]}>
{image}
{children}
</View>
);
}
);
export default Avatar;

View File

@ -1,27 +1,23 @@
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { Q } from '@nozbe/watermelondb'; import { Q } from '@nozbe/watermelondb';
import database from '../../lib/database'; import database from '../../lib/database';
import { getUserSelector } from '../../selectors/login'; import { getUserSelector } from '../../selectors/login';
import Avatar from './Avatar'; import Avatar from './Avatar';
import { IAvatar } from './interfaces';
class AvatarContainer extends React.Component { class AvatarContainer extends React.Component<Partial<IAvatar>, any> {
static propTypes = { private mounted: boolean;
rid: PropTypes.string,
text: PropTypes.string, private subscription!: any;
type: PropTypes.string,
blockUnauthenticatedAccess: PropTypes.bool,
serverVersion: PropTypes.string
};
static defaultProps = { static defaultProps = {
text: '', text: '',
type: 'd' type: 'd'
}; };
constructor(props) { constructor(props: Partial<IAvatar>) {
super(props); super(props);
this.mounted = false; this.mounted = false;
this.state = { avatarETag: '' }; this.state = { avatarETag: '' };
@ -32,7 +28,7 @@ class AvatarContainer extends React.Component {
this.mounted = true; this.mounted = true;
} }
componentDidUpdate(prevProps) { componentDidUpdate(prevProps: any) {
const { text, type } = this.props; const { text, type } = this.props;
if (prevProps.text !== text || prevProps.type !== type) { if (prevProps.text !== text || prevProps.type !== type) {
this.init(); this.init();
@ -50,7 +46,7 @@ class AvatarContainer extends React.Component {
return type === 'd'; return type === 'd';
} }
init = async() => { init = async () => {
const db = database.active; const db = database.active;
const usersCollection = db.get('users'); const usersCollection = db.get('users');
const subsCollection = db.get('subscriptions'); const subsCollection = db.get('subscriptions');
@ -59,7 +55,7 @@ class AvatarContainer extends React.Component {
try { try {
if (this.isDirect) { if (this.isDirect) {
const { text } = this.props; const { text } = this.props;
const [user] = await usersCollection.query(Q.where('username', text)).fetch(); const [user] = await usersCollection.query(Q.where('username', text!)).fetch();
record = user; record = user;
} else { } else {
const { rid } = this.props; const { rid } = this.props;
@ -71,37 +67,32 @@ class AvatarContainer extends React.Component {
if (record) { if (record) {
const observable = record.observe(); const observable = record.observe();
this.subscription = observable.subscribe((r) => { this.subscription = observable.subscribe((r: any) => {
const { avatarETag } = r; const { avatarETag } = r;
if (this.mounted) { if (this.mounted) {
this.setState({ avatarETag }); this.setState({ avatarETag });
} else { } else {
// @ts-ignore
this.state.avatarETag = avatarETag; this.state.avatarETag = avatarETag;
} }
}); });
} }
} };
render() { render() {
const { avatarETag } = this.state; const { avatarETag } = this.state;
const { serverVersion } = this.props; const { serverVersion } = this.props;
return ( return <Avatar avatarETag={avatarETag} serverVersion={serverVersion} {...this.props} />;
<Avatar
avatarETag={avatarETag}
serverVersion={serverVersion}
{...this.props}
/>
);
} }
} }
const mapStateToProps = state => ({ const mapStateToProps = (state: any) => ({
user: getUserSelector(state), user: getUserSelector(state),
server: state.share.server.server || state.server.server, server: state.share.server.server || state.server.server,
serverVersion: state.share.server.version || state.server.version, serverVersion: state.share.server.version || state.server.version,
blockUnauthenticatedAccess: blockUnauthenticatedAccess:
state.share.settings?.Accounts_AvatarBlockUnauthenticatedAccess state.share.settings?.Accounts_AvatarBlockUnauthenticatedAccess ??
?? state.settings.Accounts_AvatarBlockUnauthenticatedAccess state.settings.Accounts_AvatarBlockUnauthenticatedAccess ??
?? true true
}); });
export default connect(mapStateToProps)(AvatarContainer); export default connect(mapStateToProps)(AvatarContainer);

View File

@ -0,0 +1,23 @@
export interface IAvatar {
server: string;
style: any;
text: string;
avatar: string;
emoji: string;
size: number;
borderRadius: number;
type: string;
children: JSX.Element;
user: {
id: string;
token: string;
};
theme: string;
onPress(): void;
getCustomEmoji(): any;
avatarETag: string;
isStatic: boolean;
rid: string;
blockUnauthenticatedAccess: boolean;
serverVersion: string;
}

View File

@ -2,48 +2,30 @@
import React from 'react'; import React from 'react';
import { storiesOf } from '@storybook/react-native'; import { storiesOf } from '@storybook/react-native';
import BackgroundContainer from '.';
import { ThemeContext } from '../../theme'; import { ThemeContext } from '../../theme';
import { longText } from '../../../storybook/utils'; import { longText } from '../../../storybook/utils';
import BackgroundContainer from '.';
const stories = storiesOf('BackgroundContainer', module); const stories = storiesOf('BackgroundContainer', module);
stories.add('basic', () => ( stories.add('basic', () => <BackgroundContainer />);
<BackgroundContainer />
));
stories.add('loading', () => ( stories.add('loading', () => <BackgroundContainer loading />);
<BackgroundContainer loading />
));
stories.add('text', () => ( stories.add('text', () => <BackgroundContainer text='Text here' />);
<BackgroundContainer text='Text here' />
));
stories.add('long text', () => ( stories.add('long text', () => <BackgroundContainer text={longText} />);
<BackgroundContainer text={longText} />
));
const ThemeStory = ({ theme, ...props }) => ( const ThemeStory = ({ theme, ...props }) => (
<ThemeContext.Provider <ThemeContext.Provider value={{ theme }}>
value={{ theme }}
>
<BackgroundContainer {...props} /> <BackgroundContainer {...props} />
</ThemeContext.Provider> </ThemeContext.Provider>
); );
stories.add('dark theme - loading', () => ( stories.add('dark theme - loading', () => <ThemeStory theme='dark' loading />);
<ThemeStory theme='dark' loading />
));
stories.add('dark theme - text', () => ( stories.add('dark theme - text', () => <ThemeStory theme='dark' text={longText} />);
<ThemeStory theme='dark' text={longText} />
));
stories.add('black theme - loading', () => ( stories.add('black theme - loading', () => <ThemeStory theme='black' loading />);
<ThemeStory theme='black' loading />
));
stories.add('black theme - text', () => ( stories.add('black theme - text', () => <ThemeStory theme='black' text={longText} />);
<ThemeStory theme='black' text={longText} />
));

View File

@ -1,13 +1,16 @@
import React from 'react'; import React from 'react';
import { import { ActivityIndicator, ImageBackground, StyleSheet, Text, View } from 'react-native';
ImageBackground, StyleSheet, Text, View, ActivityIndicator
} from 'react-native';
import PropTypes from 'prop-types';
import { withTheme } from '../../theme'; import { withTheme } from '../../theme';
import sharedStyles from '../../views/Styles'; import sharedStyles from '../../views/Styles';
import { themes } from '../../constants/colors'; import { themes } from '../../constants/colors';
interface IBackgroundContainer {
text: string;
theme: string;
loading: boolean;
}
const styles = StyleSheet.create({ const styles = StyleSheet.create({
container: { container: {
flex: 1 flex: 1
@ -29,17 +32,12 @@ const styles = StyleSheet.create({
} }
}); });
const BackgroundContainer = ({ theme, text, loading }) => ( const BackgroundContainer = ({ theme, text, loading }: IBackgroundContainer) => (
<View style={styles.container}> <View style={styles.container}>
<ImageBackground source={{ uri: `message_empty_${ theme }` }} style={styles.image} /> <ImageBackground source={{ uri: `message_empty_${theme}` }} style={styles.image} />
{text ? <Text style={[styles.text, { color: themes[theme].auxiliaryTintColor }]}>{text}</Text> : null} {text ? <Text style={[styles.text, { color: themes[theme].auxiliaryTintColor }]}>{text}</Text> : null}
{loading ? <ActivityIndicator style={[styles.text, { color: themes[theme].auxiliaryTintColor }]} /> : null} {loading ? <ActivityIndicator style={styles.text} color={themes[theme].auxiliaryTintColor} /> : null}
</View> </View>
); );
BackgroundContainer.propTypes = {
text: PropTypes.string,
theme: PropTypes.string,
loading: PropTypes.bool
};
export default withTheme(BackgroundContainer); export default withTheme(BackgroundContainer);

View File

@ -1,94 +0,0 @@
import React from 'react';
import PropTypes from 'prop-types';
import { StyleSheet, Text } from 'react-native';
import Touchable from 'react-native-platform-touchable';
import { themes } from '../../constants/colors';
import sharedStyles from '../../views/Styles';
import ActivityIndicator from '../ActivityIndicator';
const styles = StyleSheet.create({
container: {
paddingHorizontal: 14,
justifyContent: 'center',
height: 48,
borderRadius: 2,
marginBottom: 12
},
text: {
fontSize: 16,
...sharedStyles.textMedium,
...sharedStyles.textAlignCenter
},
disabled: {
opacity: 0.3
}
});
export default class Button extends React.PureComponent {
static propTypes = {
title: PropTypes.oneOfType([PropTypes.string, PropTypes.element]),
type: PropTypes.string,
onPress: PropTypes.func,
disabled: PropTypes.bool,
backgroundColor: PropTypes.string,
loading: PropTypes.bool,
theme: PropTypes.string,
color: PropTypes.string,
fontSize: PropTypes.string,
style: PropTypes.any
}
static defaultProps = {
title: 'Press me!',
type: 'primary',
onPress: () => alert('It works!'),
disabled: false,
loading: false
}
render() {
const {
title, type, onPress, disabled, backgroundColor, color, loading, style, theme, fontSize, ...otherProps
} = this.props;
const isPrimary = type === 'primary';
let textColor = isPrimary ? themes[theme].buttonText : themes[theme].bodyText;
if (color) {
textColor = color;
}
return (
<Touchable
onPress={onPress}
disabled={disabled || loading}
style={[
styles.container,
backgroundColor
? { backgroundColor }
: { backgroundColor: isPrimary ? themes[theme].actionTintColor : themes[theme].backgroundColor },
disabled && styles.disabled,
style
]}
{...otherProps}
>
{
loading
? <ActivityIndicator color={textColor} />
: (
<Text
style={[
styles.text,
{ color: textColor },
fontSize && { fontSize }
]}
accessibilityLabel={title}
>
{title}
</Text>
)
}
</Touchable>
);
}
}

View File

@ -0,0 +1,84 @@
import React from 'react';
import { StyleSheet, Text } from 'react-native';
import Touchable from 'react-native-platform-touchable';
import { themes } from '../../constants/colors';
import sharedStyles from '../../views/Styles';
import ActivityIndicator from '../ActivityIndicator';
interface IButtonProps {
title: string;
type: string;
onPress(): void;
disabled: boolean;
backgroundColor: string;
loading: boolean;
theme: string;
color: string;
fontSize: any;
style: any;
styleText?: any;
testID: string;
}
const styles = StyleSheet.create({
container: {
paddingHorizontal: 14,
justifyContent: 'center',
height: 48,
borderRadius: 2,
marginBottom: 12
},
text: {
fontSize: 16,
...sharedStyles.textMedium,
...sharedStyles.textAlignCenter
},
disabled: {
opacity: 0.3
}
});
export default class Button extends React.PureComponent<Partial<IButtonProps>, any> {
static defaultProps = {
title: 'Press me!',
type: 'primary',
onPress: () => alert('It works!'),
disabled: false,
loading: false
};
render() {
const { title, type, onPress, disabled, backgroundColor, color, loading, style, theme, fontSize, styleText, ...otherProps } =
this.props;
const isPrimary = type === 'primary';
let textColor = isPrimary ? themes[theme!].buttonText : themes[theme!].bodyText;
if (color) {
textColor = color;
}
return (
<Touchable
onPress={onPress}
disabled={disabled || loading}
style={[
styles.container,
backgroundColor
? { backgroundColor }
: { backgroundColor: isPrimary ? themes[theme!].actionTintColor : themes[theme!].backgroundColor },
disabled && styles.disabled,
style
]}
{...otherProps}>
{loading ? (
<ActivityIndicator color={textColor} />
) : (
<Text style={[styles.text, { color: textColor }, fontSize && { fontSize }, styleText]} accessibilityLabel={title}>
{title}
</Text>
)}
</Touchable>
);
}
}

View File

@ -1,10 +1,13 @@
import React from 'react'; import React from 'react';
import { StyleSheet } from 'react-native'; import { StyleSheet } from 'react-native';
import PropTypes from 'prop-types';
import { CustomIcon } from '../lib/Icons'; import { CustomIcon } from '../lib/Icons';
import { themes } from '../constants/colors'; import { themes } from '../constants/colors';
interface ICheck {
style?: object;
theme: string;
}
const styles = StyleSheet.create({ const styles = StyleSheet.create({
icon: { icon: {
width: 22, width: 22,
@ -13,11 +16,8 @@ const styles = StyleSheet.create({
} }
}); });
const Check = React.memo(({ theme, style }) => <CustomIcon style={[styles.icon, style]} color={themes[theme].tintColor} size={22} name='check' />); const Check = React.memo(({ theme, style }: ICheck) => (
<CustomIcon style={[styles.icon, style]} color={themes[theme].tintColor} size={22} name='check' />
Check.propTypes = { ));
style: PropTypes.object,
theme: PropTypes.string
};
export default Check; export default Check;

View File

@ -1,26 +0,0 @@
import React from 'react';
import FastImage from '@rocket.chat/react-native-fast-image';
import PropTypes from 'prop-types';
const CustomEmoji = React.memo(({ baseUrl, emoji, style }) => (
<FastImage
style={style}
source={{
uri: `${ baseUrl }/emoji-custom/${ encodeURIComponent(emoji.content || emoji.name) }.${ emoji.extension }`,
priority: FastImage.priority.high
}}
resizeMode={FastImage.resizeMode.contain}
/>
), (prevProps, nextProps) => {
const prevEmoji = prevProps.emoji.content || prevProps.emoji.name;
const nextEmoji = nextProps.emoji.content || nextProps.emoji.name;
return prevEmoji === nextEmoji;
});
CustomEmoji.propTypes = {
baseUrl: PropTypes.string.isRequired,
emoji: PropTypes.object.isRequired,
style: PropTypes.any
};
export default CustomEmoji;

View File

@ -0,0 +1,24 @@
import React from 'react';
import FastImage from '@rocket.chat/react-native-fast-image';
import { ICustomEmoji } from './interfaces';
const CustomEmoji = React.memo(
({ baseUrl, emoji, style }: ICustomEmoji) => (
<FastImage
style={style}
source={{
uri: `${baseUrl}/emoji-custom/${encodeURIComponent(emoji.content || emoji.name)}.${emoji.extension}`,
priority: FastImage.priority.high
}}
resizeMode={FastImage.resizeMode.contain}
/>
),
(prevProps, nextProps) => {
const prevEmoji = prevProps.emoji.content || prevProps.emoji.name;
const nextEmoji = nextProps.emoji.content || nextProps.emoji.name;
return prevEmoji === nextEmoji;
}
);
export default CustomEmoji;

View File

@ -1,44 +1,41 @@
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types'; import { FlatList, Text, TouchableOpacity } from 'react-native';
import { Text, TouchableOpacity, FlatList } from 'react-native';
import shortnameToUnicode from '../../utils/shortnameToUnicode'; import shortnameToUnicode from '../../utils/shortnameToUnicode';
import styles from './styles'; import styles from './styles';
import CustomEmoji from './CustomEmoji'; import CustomEmoji from './CustomEmoji';
import scrollPersistTaps from '../../utils/scrollPersistTaps'; import scrollPersistTaps from '../../utils/scrollPersistTaps';
import { IEmoji, IEmojiCategory } from './interfaces';
const EMOJI_SIZE = 50; const EMOJI_SIZE = 50;
const renderEmoji = (emoji, size, baseUrl) => { const renderEmoji = (emoji: IEmoji, size: number, baseUrl: string) => {
if (emoji && emoji.isCustom) { if (emoji && emoji.isCustom) {
return <CustomEmoji style={[styles.customCategoryEmoji, { height: size - 16, width: size - 16 }]} emoji={emoji} baseUrl={baseUrl} />; return (
<CustomEmoji
style={[styles.customCategoryEmoji, { height: size - 16, width: size - 16 }]}
emoji={emoji}
baseUrl={baseUrl}
/>
);
} }
return ( return (
<Text style={[styles.categoryEmoji, { height: size, width: size, fontSize: size - 14 }]}> <Text style={[styles.categoryEmoji, { height: size, width: size, fontSize: size - 14 }]}>
{shortnameToUnicode(`:${ emoji }:`)} {shortnameToUnicode(`:${emoji}:`)}
</Text> </Text>
); );
}; };
class EmojiCategory extends React.Component { class EmojiCategory extends React.Component<Partial<IEmojiCategory>> {
static propTypes = { renderItem(emoji: any) {
baseUrl: PropTypes.string.isRequired,
emojis: PropTypes.any,
onEmojiSelected: PropTypes.func,
emojisPerRow: PropTypes.number,
width: PropTypes.number
}
renderItem(emoji) {
const { baseUrl, onEmojiSelected } = this.props; const { baseUrl, onEmojiSelected } = this.props;
return ( return (
<TouchableOpacity <TouchableOpacity
activeOpacity={0.7} activeOpacity={0.7}
key={emoji && emoji.isCustom ? emoji.content : emoji} key={emoji && emoji.isCustom ? emoji.content : emoji}
onPress={() => onEmojiSelected(emoji)} onPress={() => onEmojiSelected!(emoji)}
testID={`reaction-picker-${ emoji && emoji.isCustom ? emoji.content : emoji }`} testID={`reaction-picker-${emoji && emoji.isCustom ? emoji.content : emoji}`}>
> {renderEmoji(emoji, EMOJI_SIZE, baseUrl!)}
{renderEmoji(emoji, EMOJI_SIZE, baseUrl)}
</TouchableOpacity> </TouchableOpacity>
); );
} }
@ -51,13 +48,14 @@ class EmojiCategory extends React.Component {
} }
const numColumns = Math.trunc(width / EMOJI_SIZE); const numColumns = Math.trunc(width / EMOJI_SIZE);
const marginHorizontal = (width - (numColumns * EMOJI_SIZE)) / 2; const marginHorizontal = (width - numColumns * EMOJI_SIZE) / 2;
return ( return (
// @ts-ignore
<FlatList <FlatList
contentContainerStyle={{ marginHorizontal }} contentContainerStyle={{ marginHorizontal }}
// rerender FlatList in case of width changes // rerender FlatList in case of width changes
key={`emoji-category-${ width }`} key={`emoji-category-${width}`}
keyExtractor={item => (item && item.isCustom && item.content) || item} keyExtractor={item => (item && item.isCustom && item.content) || item}
data={emojis} data={emojis}
extraData={this.props} extraData={this.props}

View File

@ -1,49 +0,0 @@
import React from 'react';
import PropTypes from 'prop-types';
import { View, TouchableOpacity, Text } from 'react-native';
import styles from './styles';
import { themes } from '../../constants/colors';
export default class TabBar extends React.Component {
static propTypes = {
goToPage: PropTypes.func,
activeTab: PropTypes.number,
tabs: PropTypes.array,
tabEmojiStyle: PropTypes.object,
theme: PropTypes.string
}
shouldComponentUpdate(nextProps) {
const { activeTab, theme } = this.props;
if (nextProps.activeTab !== activeTab) {
return true;
}
if (nextProps.theme !== theme) {
return true;
}
return false;
}
render() {
const {
tabs, goToPage, tabEmojiStyle, activeTab, theme
} = this.props;
return (
<View style={styles.tabsContainer}>
{tabs.map((tab, i) => (
<TouchableOpacity
activeOpacity={0.7}
key={tab}
onPress={() => goToPage(i)}
style={styles.tab}
testID={`reaction-picker-${ tab }`}
>
<Text style={[styles.tabEmoji, tabEmojiStyle]}>{tab}</Text>
{activeTab === i ? <View style={[styles.activeTabLine, { backgroundColor: themes[theme].tintColor }]} /> : <View style={styles.tabLine} />}
</TouchableOpacity>
))}
</View>
);
}
}

View File

@ -0,0 +1,50 @@
import React from 'react';
import { Text, TouchableOpacity, View } from 'react-native';
import styles from './styles';
import { themes } from '../../constants/colors';
interface ITabBarProps {
goToPage: Function;
activeTab: number;
tabs: [];
tabEmojiStyle: object;
theme: string;
}
export default class TabBar extends React.Component<Partial<ITabBarProps>> {
shouldComponentUpdate(nextProps: any) {
const { activeTab, theme } = this.props;
if (nextProps.activeTab !== activeTab) {
return true;
}
if (nextProps.theme !== theme) {
return true;
}
return false;
}
render() {
const { tabs, goToPage, tabEmojiStyle, activeTab, theme } = this.props;
return (
<View style={styles.tabsContainer}>
{tabs!.map((tab, i) => (
<TouchableOpacity
activeOpacity={0.7}
key={tab}
onPress={() => goToPage!(i)}
style={styles.tab}
testID={`reaction-picker-${tab}`}>
<Text style={[styles.tabEmoji, tabEmojiStyle]}>{tab}</Text>
{activeTab === i ? (
<View style={[styles.activeTabLine, { backgroundColor: themes[theme!].tintColor }]} />
) : (
<View style={styles.tabLine} />
)}
</TouchableOpacity>
))}
</View>
);
}
}

View File

@ -1,6 +1,5 @@
import React, { Component } from 'react'; import React, { Component } from 'react';
import { View } from 'react-native'; import { View } from 'react-native';
import PropTypes from 'prop-types';
import ScrollableTabView from 'react-native-scrollable-tab-view'; import ScrollableTabView from 'react-native-scrollable-tab-view';
import { dequal } from 'dequal'; import { dequal } from 'dequal';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
@ -18,22 +17,33 @@ import shortnameToUnicode from '../../utils/shortnameToUnicode';
import log from '../../utils/log'; import log from '../../utils/log';
import { themes } from '../../constants/colors'; import { themes } from '../../constants/colors';
import { withTheme } from '../../theme'; import { withTheme } from '../../theme';
import { IEmoji } from './interfaces';
const scrollProps = { const scrollProps = {
keyboardShouldPersistTaps: 'always', keyboardShouldPersistTaps: 'always',
keyboardDismissMode: 'none' keyboardDismissMode: 'none'
}; };
class EmojiPicker extends Component { interface IEmojiPickerProps {
static propTypes = { isMessageContainsOnlyEmoji: boolean;
baseUrl: PropTypes.string.isRequired, getCustomEmoji?: Function;
customEmojis: PropTypes.object, baseUrl: string;
onEmojiSelected: PropTypes.func, customEmojis?: any;
tabEmojiStyle: PropTypes.object, style: object;
theme: PropTypes.string theme?: string;
}; onEmojiSelected?: Function;
tabEmojiStyle?: object;
}
constructor(props) { interface IEmojiPickerState {
frequentlyUsed: [];
customEmojis: any;
show: boolean;
width: number | null;
}
class EmojiPicker extends Component<IEmojiPickerProps, IEmojiPickerState> {
constructor(props: IEmojiPickerProps) {
super(props); super(props);
const customEmojis = Object.keys(props.customEmojis) const customEmojis = Object.keys(props.customEmojis)
.filter(item => item === props.customEmojis[item].name) .filter(item => item === props.customEmojis[item].name)
@ -55,7 +65,7 @@ class EmojiPicker extends Component {
this.setState({ show: true }); this.setState({ show: true });
} }
shouldComponentUpdate(nextProps, nextState) { shouldComponentUpdate(nextProps: any, nextState: any) {
const { frequentlyUsed, show, width } = this.state; const { frequentlyUsed, show, width } = this.state;
const { theme } = this.props; const { theme } = this.props;
if (nextProps.theme !== theme) { if (nextProps.theme !== theme) {
@ -73,67 +83,72 @@ class EmojiPicker extends Component {
return false; return false;
} }
onEmojiSelected = (emoji) => { onEmojiSelected = (emoji: IEmoji) => {
try { try {
const { onEmojiSelected } = this.props; const { onEmojiSelected } = this.props;
if (emoji.isCustom) { if (emoji.isCustom) {
this._addFrequentlyUsed({ this._addFrequentlyUsed({
content: emoji.content, extension: emoji.extension, isCustom: true content: emoji.content,
extension: emoji.extension,
isCustom: true
}); });
onEmojiSelected(`:${ emoji.content }:`); onEmojiSelected!(`:${emoji.content}:`);
} else { } else {
const content = emoji; const content = emoji;
this._addFrequentlyUsed({ content, isCustom: false }); this._addFrequentlyUsed({ content, isCustom: false });
const shortname = `:${ emoji }:`; const shortname = `:${emoji}:`;
onEmojiSelected(shortnameToUnicode(shortname), shortname); onEmojiSelected!(shortnameToUnicode(shortname), shortname);
} }
} catch (e) { } catch (e) {
log(e); log(e);
} }
} };
// eslint-disable-next-line react/sort-comp _addFrequentlyUsed = protectedFunction(async (emoji: IEmoji) => {
_addFrequentlyUsed = protectedFunction(async(emoji) => {
const db = database.active; const db = database.active;
const freqEmojiCollection = db.get('frequently_used_emojis'); const freqEmojiCollection = db.get('frequently_used_emojis');
let freqEmojiRecord; let freqEmojiRecord: any;
try { try {
freqEmojiRecord = await freqEmojiCollection.find(emoji.content); freqEmojiRecord = await freqEmojiCollection.find(emoji.content);
} catch (error) { } catch (error) {
// Do nothing // Do nothing
} }
await db.action(async() => { await db.action(async () => {
if (freqEmojiRecord) { if (freqEmojiRecord) {
await freqEmojiRecord.update((f) => { await freqEmojiRecord.update((f: any) => {
f.count += 1; f.count += 1;
}); });
} else { } else {
await freqEmojiCollection.create((f) => { await freqEmojiCollection.create((f: any) => {
f._raw = sanitizedRaw({ id: emoji.content }, freqEmojiCollection.schema); f._raw = sanitizedRaw({ id: emoji.content }, freqEmojiCollection.schema);
Object.assign(f, emoji); Object.assign(f, emoji);
f.count = 1; f.count = 1;
}); });
} }
}); });
}) });
updateFrequentlyUsed = async() => { updateFrequentlyUsed = async () => {
const db = database.active; const db = database.active;
const frequentlyUsedRecords = await db.get('frequently_used_emojis').query().fetch(); const frequentlyUsedRecords = await db.get('frequently_used_emojis').query().fetch();
let frequentlyUsed = orderBy(frequentlyUsedRecords, ['count'], ['desc']); let frequentlyUsed: any = orderBy(frequentlyUsedRecords, ['count'], ['desc']);
frequentlyUsed = frequentlyUsed.map((item) => { frequentlyUsed = frequentlyUsed.map((item: IEmoji) => {
if (item.isCustom) { if (item.isCustom) {
return { content: item.content, extension: item.extension, isCustom: item.isCustom }; return { content: item.content, extension: item.extension, isCustom: item.isCustom };
} }
return shortnameToUnicode(`${ item.content }`); return shortnameToUnicode(`${item.content}`);
}); });
this.setState({ frequentlyUsed }); this.setState({ frequentlyUsed });
} };
onLayout = ({ nativeEvent: { layout: { width } } }) => this.setState({ width }); onLayout = ({
nativeEvent: {
layout: { width }
}
}: any) => this.setState({ width });
renderCategory(category, i, label) { renderCategory(category: any, i: number, label: string) {
const { frequentlyUsed, customEmojis, width } = this.state; const { frequentlyUsed, customEmojis, width } = this.state;
const { baseUrl } = this.props; const { baseUrl } = this.props;
@ -148,9 +163,9 @@ class EmojiPicker extends Component {
return ( return (
<EmojiCategory <EmojiCategory
emojis={emojis} emojis={emojis}
onEmojiSelected={emoji => this.onEmojiSelected(emoji)} onEmojiSelected={(emoji: IEmoji) => this.onEmojiSelected(emoji)}
style={styles.categoryContainer} style={styles.categoryContainer}
width={width} width={width!}
baseUrl={baseUrl} baseUrl={baseUrl}
tabLabel={label} tabLabel={label}
/> />
@ -168,23 +183,21 @@ class EmojiPicker extends Component {
<View onLayout={this.onLayout} style={{ flex: 1 }}> <View onLayout={this.onLayout} style={{ flex: 1 }}>
<ScrollableTabView <ScrollableTabView
renderTabBar={() => <TabBar tabEmojiStyle={tabEmojiStyle} theme={theme} />} renderTabBar={() => <TabBar tabEmojiStyle={tabEmojiStyle} theme={theme} />}
/* @ts-ignore*/
contentProps={scrollProps} contentProps={scrollProps}
style={{ backgroundColor: themes[theme].focusedBackground }} style={{ backgroundColor: themes[theme!].focusedBackground }}>
> {categories.tabs.map((tab, i) =>
{ i === 0 && frequentlyUsed.length === 0
categories.tabs.map((tab, i) => ( ? null // when no frequentlyUsed don't show the tab
(i === 0 && frequentlyUsed.length === 0) ? null // when no frequentlyUsed don't show the tab : this.renderCategory(tab.category, i, tab.tabLabel)
: ( )}
this.renderCategory(tab.category, i, tab.tabLabel)
)))
}
</ScrollableTabView> </ScrollableTabView>
</View> </View>
); );
} }
} }
const mapStateToProps = state => ({ const mapStateToProps = (state: any) => ({
customEmojis: state.customEmojis customEmojis: state.customEmojis
}); });

View File

@ -0,0 +1,22 @@
export interface IEmoji {
content: any;
name: string;
extension: any;
isCustom: boolean;
}
export interface ICustomEmoji {
baseUrl: string;
emoji: IEmoji;
style: any;
}
export interface IEmojiCategory {
baseUrl: string;
emojis: IEmoji[];
onEmojiSelected: Function;
emojisPerRow: number;
width: number;
style: any;
tabLabel: string;
}

View File

@ -1,6 +1,5 @@
import React from 'react'; import React from 'react';
import { ScrollView, StyleSheet, View } from 'react-native'; import { ScrollView, StyleSheet, View } from 'react-native';
import PropTypes from 'prop-types';
import { themes } from '../constants/colors'; import { themes } from '../constants/colors';
import sharedStyles from '../views/Styles'; import sharedStyles from '../views/Styles';
@ -11,33 +10,35 @@ import AppVersion from './AppVersion';
import { isTablet } from '../utils/deviceInfo'; import { isTablet } from '../utils/deviceInfo';
import SafeAreaView from './SafeAreaView'; import SafeAreaView from './SafeAreaView';
interface IFormContainer {
theme: string;
testID: string;
children: JSX.Element;
}
const styles = StyleSheet.create({ const styles = StyleSheet.create({
scrollView: { scrollView: {
minHeight: '100%' minHeight: '100%'
} }
}); });
export const FormContainerInner = ({ children }) => ( export const FormContainerInner = ({ children }: { children: JSX.Element }) => (
<View style={[sharedStyles.container, isTablet && sharedStyles.tabletScreenContent]}> <View style={[sharedStyles.container, isTablet && sharedStyles.tabletScreenContent]}>{children}</View>
{children}
</View>
); );
const FormContainer = ({ const FormContainer = ({ children, theme, testID, ...props }: IFormContainer) => (
children, theme, testID, ...props // @ts-ignore
}) => (
<KeyboardView <KeyboardView
style={{ backgroundColor: themes[theme].backgroundColor }} style={{ backgroundColor: themes[theme].backgroundColor }}
contentContainerStyle={sharedStyles.container} contentContainerStyle={sharedStyles.container}
keyboardVerticalOffset={128} keyboardVerticalOffset={128}>
>
<StatusBar /> <StatusBar />
{/* @ts-ignore*/}
<ScrollView <ScrollView
style={sharedStyles.container} style={sharedStyles.container}
contentContainerStyle={[sharedStyles.containerScrollView, styles.scrollView]} contentContainerStyle={[sharedStyles.containerScrollView, styles.scrollView]}
{...scrollPersistTaps} {...scrollPersistTaps}
{...props} {...props}>
>
<SafeAreaView testID={testID} style={{ backgroundColor: themes[theme].backgroundColor }}> <SafeAreaView testID={testID} style={{ backgroundColor: themes[theme].backgroundColor }}>
{children} {children}
<AppVersion theme={theme} /> <AppVersion theme={theme} />
@ -46,14 +47,4 @@ const FormContainer = ({
</KeyboardView> </KeyboardView>
); );
FormContainer.propTypes = {
theme: PropTypes.string,
testID: PropTypes.string,
children: PropTypes.element
};
FormContainerInner.propTypes = {
children: PropTypes.element
};
export default FormContainer; export default FormContainer;

View File

@ -1,7 +1,7 @@
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types';
import { SafeAreaView } from 'react-native-safe-area-context'; import { SafeAreaView } from 'react-native-safe-area-context';
import { View, StyleSheet } from 'react-native'; import { StyleSheet, View } from 'react-native';
import { themes } from '../../constants/colors'; import { themes } from '../../constants/colors';
import { themedHeader } from '../../utils/navigation'; import { themedHeader } from '../../utils/navigation';
import { isIOS, isTablet } from '../../utils/deviceInfo'; import { isIOS, isTablet } from '../../utils/deviceInfo';
@ -10,18 +10,25 @@ import { withTheme } from '../../theme';
// Get from https://github.com/react-navigation/react-navigation/blob/master/packages/stack/src/views/Header/HeaderSegment.tsx#L69 // Get from https://github.com/react-navigation/react-navigation/blob/master/packages/stack/src/views/Header/HeaderSegment.tsx#L69
export const headerHeight = isIOS ? 44 : 56; export const headerHeight = isIOS ? 44 : 56;
export const getHeaderHeight = (isLandscape) => { export const getHeaderHeight = (isLandscape: boolean) => {
if (isIOS) { if (isIOS) {
if (isLandscape && !isTablet) { if (isLandscape && !isTablet) {
return 32; return 32;
} else {
return 44;
} }
return 44;
} }
return 56; return 56;
}; };
export const getHeaderTitlePosition = ({ insets, numIconsRight }) => ({ interface IHeaderTitlePosition {
insets: {
left: number;
right: number;
};
numIconsRight: number;
}
export const getHeaderTitlePosition = ({ insets, numIconsRight }: IHeaderTitlePosition) => ({
left: insets.left + 60, left: insets.left + 60,
right: insets.right + Math.max(45 * numIconsRight, 15) right: insets.right + Math.max(45 * numIconsRight, 15)
}); });
@ -35,9 +42,14 @@ const styles = StyleSheet.create({
} }
}); });
const Header = ({ interface IHeader {
theme, headerLeft, headerTitle, headerRight theme: string;
}) => ( headerLeft(): void;
headerTitle(): void;
headerRight(): void;
}
const Header = ({ theme, headerLeft, headerTitle, headerRight }: IHeader) => (
<SafeAreaView style={{ backgroundColor: themes[theme].headerBackground }} edges={['top', 'left', 'right']}> <SafeAreaView style={{ backgroundColor: themes[theme].headerBackground }} edges={['top', 'left', 'right']}>
<View style={[styles.container, { ...themedHeader(theme).headerStyle }]}> <View style={[styles.container, { ...themedHeader(theme).headerStyle }]}>
{headerLeft ? headerLeft() : null} {headerLeft ? headerLeft() : null}
@ -47,11 +59,4 @@ const Header = ({
</SafeAreaView> </SafeAreaView>
); );
Header.propTypes = {
theme: PropTypes.string,
headerLeft: PropTypes.element,
headerTitle: PropTypes.element,
headerRight: PropTypes.element
};
export default withTheme(Header); export default withTheme(Header);

View File

@ -1,84 +0,0 @@
import React from 'react';
import PropTypes from 'prop-types';
import { isIOS } from '../../utils/deviceInfo';
import I18n from '../../i18n';
import Container from './HeaderButtonContainer';
import Item from './HeaderButtonItem';
// Left
export const Drawer = React.memo(({ navigation, testID, ...props }) => (
<Container left>
<Item iconName='hamburguer' onPress={() => navigation.toggleDrawer()} testID={testID} {...props} />
</Container>
));
export const CloseModal = React.memo(({
navigation, testID, onPress = () => navigation.pop(), ...props
}) => (
<Container left>
<Item iconName='close' onPress={onPress} testID={testID} {...props} />
</Container>
));
export const CancelModal = React.memo(({ onPress, testID }) => (
<Container left>
{isIOS
? <Item title={I18n.t('Cancel')} onPress={onPress} testID={testID} />
: <Item iconName='close' onPress={onPress} testID={testID} />
}
</Container>
));
// Right
export const More = React.memo(({ onPress, testID }) => (
<Container>
<Item iconName='kebab' onPress={onPress} testID={testID} />
</Container>
));
export const Download = React.memo(({ onPress, testID, ...props }) => (
<Container>
<Item iconName='download' onPress={onPress} testID={testID} {...props} />
</Container>
));
export const Preferences = React.memo(({ onPress, testID, ...props }) => (
<Container>
<Item iconName='settings' onPress={onPress} testID={testID} {...props} />
</Container>
));
export const Legal = React.memo(({ navigation, testID }) => (
<More onPress={() => navigation.navigate('LegalView')} testID={testID} />
));
Drawer.propTypes = {
navigation: PropTypes.object.isRequired,
testID: PropTypes.string.isRequired
};
CloseModal.propTypes = {
navigation: PropTypes.object.isRequired,
testID: PropTypes.string.isRequired,
onPress: PropTypes.func
};
CancelModal.propTypes = {
onPress: PropTypes.func.isRequired,
testID: PropTypes.string.isRequired
};
More.propTypes = {
onPress: PropTypes.func.isRequired,
testID: PropTypes.string.isRequired
};
Download.propTypes = {
onPress: PropTypes.func.isRequired,
testID: PropTypes.string.isRequired
};
Preferences.propTypes = {
onPress: PropTypes.func.isRequired,
testID: PropTypes.string.isRequired
};
Legal.propTypes = {
navigation: PropTypes.object.isRequired,
testID: PropTypes.string.isRequired
};

View File

@ -0,0 +1,60 @@
import React from 'react';
import { isIOS } from '../../utils/deviceInfo';
import I18n from '../../i18n';
import Container from './HeaderButtonContainer';
import Item from './HeaderButtonItem';
interface IHeaderButtonCommon {
navigation: any;
onPress?(): void;
testID?: string;
}
// Left
export const Drawer = React.memo(({ navigation, testID, ...props }: Partial<IHeaderButtonCommon>) => (
<Container left>
<Item iconName='hamburguer' onPress={() => navigation.toggleDrawer()} testID={testID} {...props} />
</Container>
));
export const CloseModal = React.memo(
({ navigation, testID, onPress = () => navigation.pop(), ...props }: IHeaderButtonCommon) => (
<Container left>
<Item iconName='close' onPress={onPress} testID={testID} {...props} />
</Container>
)
);
export const CancelModal = React.memo(({ onPress, testID }: Partial<IHeaderButtonCommon>) => (
<Container left>
{isIOS ? (
<Item title={I18n.t('Cancel')} onPress={onPress} testID={testID} />
) : (
<Item iconName='close' onPress={onPress} testID={testID} />
)}
</Container>
));
// Right
export const More = React.memo(({ onPress, testID }: Partial<IHeaderButtonCommon>) => (
<Container>
<Item iconName='kebab' onPress={onPress} testID={testID} />
</Container>
));
export const Download = React.memo(({ onPress, testID, ...props }: Partial<IHeaderButtonCommon>) => (
<Container>
<Item iconName='download' onPress={onPress} testID={testID} {...props} />
</Container>
));
export const Preferences = React.memo(({ onPress, testID, ...props }: Partial<IHeaderButtonCommon>) => (
<Container>
<Item iconName='settings' onPress={onPress} testID={testID} {...props} />
</Container>
));
export const Legal = React.memo(({ navigation, testID }: Partial<IHeaderButtonCommon>) => (
<More onPress={() => navigation.navigate('LegalView')} testID={testID} />
));

View File

@ -1,6 +1,10 @@
import React from 'react'; import React from 'react';
import { View, StyleSheet } from 'react-native'; import { StyleSheet, View } from 'react-native';
import PropTypes from 'prop-types';
interface IHeaderButtonContainer {
children: JSX.Element;
left?: boolean;
}
const styles = StyleSheet.create({ const styles = StyleSheet.create({
container: { container: {
@ -16,21 +20,10 @@ const styles = StyleSheet.create({
} }
}); });
const Container = ({ children, left }) => ( const Container = ({ children, left = false }: IHeaderButtonContainer) => (
<View style={[styles.container, left ? styles.left : styles.right]}> <View style={[styles.container, left ? styles.left : styles.right]}>{children}</View>
{children}
</View>
); );
Container.propTypes = {
children: PropTypes.arrayOf(PropTypes.element),
left: PropTypes.bool
};
Container.defaultProps = {
left: false
};
Container.displayName = 'HeaderButton.Container'; Container.displayName = 'HeaderButton.Container';
export default Container; export default Container;

View File

@ -1,6 +1,5 @@
import React from 'react'; import React from 'react';
import { Text, StyleSheet, Platform } from 'react-native'; import { Platform, StyleSheet, Text } from 'react-native';
import PropTypes from 'prop-types';
import Touchable from 'react-native-platform-touchable'; import Touchable from 'react-native-platform-touchable';
import { CustomIcon } from '../../lib/Icons'; import { CustomIcon } from '../../lib/Icons';
@ -8,8 +7,20 @@ import { withTheme } from '../../theme';
import { themes } from '../../constants/colors'; import { themes } from '../../constants/colors';
import sharedStyles from '../../views/Styles'; import sharedStyles from '../../views/Styles';
interface IHeaderButtonItem {
title: string;
iconName: string;
onPress(): void;
testID: string;
theme: string;
badge(): void;
}
export const BUTTON_HIT_SLOP = { export const BUTTON_HIT_SLOP = {
top: 5, right: 5, bottom: 5, left: 5 top: 5,
right: 5,
bottom: 5,
left: 5
}; };
const styles = StyleSheet.create({ const styles = StyleSheet.create({
@ -29,30 +40,19 @@ const styles = StyleSheet.create({
} }
}); });
const Item = ({ const Item = ({ title, iconName, onPress, testID, theme, badge }: IHeaderButtonItem) => (
title, iconName, onPress, testID, theme, badge
}) => (
<Touchable onPress={onPress} testID={testID} hitSlop={BUTTON_HIT_SLOP} style={styles.container}> <Touchable onPress={onPress} testID={testID} hitSlop={BUTTON_HIT_SLOP} style={styles.container}>
<> <>
{ {iconName ? (
iconName <CustomIcon name={iconName} size={24} color={themes[theme].headerTintColor} />
? <CustomIcon name={iconName} size={24} color={themes[theme].headerTintColor} /> ) : (
: <Text style={[styles.title, { color: themes[theme].headerTintColor }]}>{title}</Text> <Text style={[styles.title, { color: themes[theme].headerTintColor }]}>{title}</Text>
} )}
{badge ? badge() : null} {badge ? badge() : null}
</> </>
</Touchable> </Touchable>
); );
Item.propTypes = {
onPress: PropTypes.func.isRequired,
title: PropTypes.string,
iconName: PropTypes.string,
testID: PropTypes.string,
theme: PropTypes.string,
badge: PropTypes.func
};
Item.displayName = 'HeaderButton.Item'; Item.displayName = 'HeaderButton.Item';
export default withTheme(Item); export default withTheme(Item);

View File

@ -15,12 +15,6 @@ const styles = StyleSheet.create({
} }
}); });
export const Badge = ({ ...props }) => ( export const Badge = ({ ...props }) => <UnreadBadge {...props} style={styles.badgeContainer} small />;
<UnreadBadge
{...props}
style={styles.badgeContainer}
small
/>
);
export default Badge; export default Badge;

View File

@ -1,6 +1,5 @@
import React from 'react'; import React from 'react';
import { StyleSheet, View, Text } from 'react-native'; import { StyleSheet, Text, View } from 'react-native';
import PropTypes from 'prop-types';
import Touchable from 'react-native-platform-touchable'; import Touchable from 'react-native-platform-touchable';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { Notifier } from 'react-native-notifier'; import { Notifier } from 'react-native-notifier';
@ -16,10 +15,13 @@ import { goRoom } from '../../utils/goRoom';
import Navigation from '../../lib/Navigation'; import Navigation from '../../lib/Navigation';
import { useOrientation } from '../../dimensions'; import { useOrientation } from '../../dimensions';
interface INotifierComponent {
notification: object;
isMasterDetail: boolean;
}
const AVATAR_SIZE = 48; const AVATAR_SIZE = 48;
const BUTTON_HIT_SLOP = { const BUTTON_HIT_SLOP = { top: 12, right: 12, bottom: 12, left: 12 };
top: 12, right: 12, bottom: 12, left: 12
};
const styles = StyleSheet.create({ const styles = StyleSheet.create({
container: { container: {
@ -64,16 +66,16 @@ const styles = StyleSheet.create({
const hideNotification = () => Notifier.hideNotification(); const hideNotification = () => Notifier.hideNotification();
const NotifierComponent = React.memo(({ notification, isMasterDetail }) => { const NotifierComponent = React.memo(({ notification, isMasterDetail }: INotifierComponent) => {
const { theme } = useTheme(); const { theme }: any = useTheme();
const insets = useSafeAreaInsets(); const insets = useSafeAreaInsets();
const { isLandscape } = useOrientation(); const { isLandscape } = useOrientation();
const { text, payload } = notification; const { text, payload }: any = notification;
const { type, rid } = payload; const { type, rid } = payload;
const name = type === 'd' ? payload.sender.username : payload.name; const name = type === 'd' ? payload.sender.username : payload.name;
// if sub is not on local database, title and avatar will be null, so we use payload from notification // if sub is not on local database, title and avatar will be null, so we use payload from notification
const { title = name, avatar = name } = notification; const { title = name, avatar = name }: any = notification;
const onPress = () => { const onPress = () => {
const { prid, _id } = payload; const { prid, _id } = payload;
@ -81,7 +83,10 @@ const NotifierComponent = React.memo(({ notification, isMasterDetail }) => {
return; return;
} }
const item = { const item = {
rid, name: title, t: type, prid rid,
name: title,
t: type,
prid
}; };
if (isMasterDetail) { if (isMasterDetail) {
@ -94,47 +99,41 @@ const NotifierComponent = React.memo(({ notification, isMasterDetail }) => {
}; };
return ( return (
<View style={[ <View
styles.container, style={[
(isMasterDetail || isLandscape) && styles.small, styles.container,
{ (isMasterDetail || isLandscape) && styles.small,
backgroundColor: themes[theme].focusedBackground, {
borderColor: themes[theme].separatorColor, backgroundColor: themes[theme].focusedBackground,
marginTop: insets.top borderColor: themes[theme].separatorColor,
} marginTop: insets.top
]} }
> ]}>
<Touchable <Touchable
style={styles.content} style={styles.content}
onPress={onPress} onPress={onPress}
hitSlop={BUTTON_HIT_SLOP} hitSlop={BUTTON_HIT_SLOP}
background={Touchable.SelectableBackgroundBorderless()} background={Touchable.SelectableBackgroundBorderless()}>
>
<> <>
<Avatar text={avatar} size={AVATAR_SIZE} type={type} rid={rid} style={styles.avatar} /> <Avatar text={avatar} size={AVATAR_SIZE} type={type} rid={rid} style={styles.avatar} />
<View style={styles.inner}> <View style={styles.inner}>
<Text style={[styles.roomName, { color: themes[theme].titleText }]} numberOfLines={1}>{title}</Text> <Text style={[styles.roomName, { color: themes[theme].titleText }]} numberOfLines={1}>
<Text style={[styles.message, { color: themes[theme].titleText }]} numberOfLines={1}>{text}</Text> {title}
</Text>
<Text style={[styles.message, { color: themes[theme].titleText }]} numberOfLines={1}>
{text}
</Text>
</View> </View>
</> </>
</Touchable> </Touchable>
<Touchable <Touchable onPress={hideNotification} hitSlop={BUTTON_HIT_SLOP} background={Touchable.SelectableBackgroundBorderless()}>
onPress={hideNotification}
hitSlop={BUTTON_HIT_SLOP}
background={Touchable.SelectableBackgroundBorderless()}
>
<CustomIcon name='close' style={[styles.close, { color: themes[theme].titleText }]} size={20} /> <CustomIcon name='close' style={[styles.close, { color: themes[theme].titleText }]} size={20} />
</Touchable> </Touchable>
</View> </View>
); );
}); });
NotifierComponent.propTypes = { const mapStateToProps = (state: any) => ({
notification: PropTypes.object,
isMasterDetail: PropTypes.bool
};
const mapStateToProps = state => ({
isMasterDetail: state.app.isMasterDetail isMasterDetail: state.app.isMasterDetail
}); });

View File

@ -1,57 +0,0 @@
import React, { memo, useEffect } from 'react';
import PropTypes from 'prop-types';
import { NotifierRoot, Notifier, Easing } from 'react-native-notifier';
import { connect } from 'react-redux';
import { dequal } from 'dequal';
import NotifierComponent from './NotifierComponent';
import EventEmitter from '../../utils/events';
import Navigation from '../../lib/Navigation';
import { getActiveRoute } from '../../utils/navigation';
export const INAPP_NOTIFICATION_EMITTER = 'NotificationInApp';
const InAppNotification = memo(({ rooms, appState }) => {
const show = (notification) => {
if (appState !== 'foreground') {
return;
}
const { payload } = notification;
const state = Navigation.navigationRef.current?.getRootState();
const route = getActiveRoute(state);
if (payload.rid) {
if (rooms.includes(payload.rid) || route?.name === 'JitsiMeetView') {
return;
}
Notifier.showNotification({
showEasing: Easing.inOut(Easing.quad),
Component: NotifierComponent,
componentProps: {
notification
}
});
}
};
useEffect(() => {
const listener = EventEmitter.addEventListener(INAPP_NOTIFICATION_EMITTER, show);
return () => {
EventEmitter.removeListener(INAPP_NOTIFICATION_EMITTER, listener);
};
}, [rooms]);
return <NotifierRoot />;
}, (prevProps, nextProps) => dequal(prevProps.rooms, nextProps.rooms));
const mapStateToProps = state => ({
rooms: state.room.rooms,
appState: state.app.ready && state.app.foreground ? 'foreground' : 'background'
});
InAppNotification.propTypes = {
rooms: PropTypes.array,
appState: PropTypes.string
};
export default connect(mapStateToProps)(InAppNotification);

View File

@ -0,0 +1,54 @@
import React, { memo, useEffect } from 'react';
import { Easing, Notifier, NotifierRoot } from 'react-native-notifier';
import { connect } from 'react-redux';
import { dequal } from 'dequal';
import NotifierComponent from './NotifierComponent';
import EventEmitter from '../../utils/events';
import Navigation from '../../lib/Navigation';
import { getActiveRoute } from '../../utils/navigation';
export const INAPP_NOTIFICATION_EMITTER = 'NotificationInApp';
const InAppNotification = memo(
({ rooms, appState }: { rooms: any; appState: string }) => {
const show = (notification: any) => {
if (appState !== 'foreground') {
return;
}
const { payload } = notification;
const state = Navigation.navigationRef.current?.getRootState();
const route = getActiveRoute(state);
if (payload.rid) {
if (rooms.includes(payload.rid) || route?.name === 'JitsiMeetView') {
return;
}
Notifier.showNotification({
showEasing: Easing.inOut(Easing.quad),
Component: NotifierComponent,
componentProps: {
notification
}
});
}
};
useEffect(() => {
const listener = EventEmitter.addEventListener(INAPP_NOTIFICATION_EMITTER, show);
return () => {
EventEmitter.removeListener(INAPP_NOTIFICATION_EMITTER, listener);
};
}, [rooms]);
return <NotifierRoot />;
},
(prevProps, nextProps) => dequal(prevProps.rooms, nextProps.rooms)
);
const mapStateToProps = (state: any) => ({
rooms: state.room.rooms,
appState: state.app.ready && state.app.foreground ? 'foreground' : 'background'
});
export default connect(mapStateToProps)(InAppNotification);

View File

@ -1,6 +1,6 @@
import React from 'react'; import React from 'react';
import { ScrollView, StyleSheet } from 'react-native'; import { ScrollView, StyleSheet } from 'react-native';
import PropTypes from 'prop-types';
import { withTheme } from '../../theme'; import { withTheme } from '../../theme';
import scrollPersistTaps from '../../utils/scrollPersistTaps'; import scrollPersistTaps from '../../utils/scrollPersistTaps';
@ -10,21 +10,20 @@ const styles = StyleSheet.create({
} }
}); });
const ListContainer = React.memo(({ children, ...props }) => ( interface IListContainer {
children: JSX.Element;
}
const ListContainer = React.memo(({ children, ...props }: IListContainer) => (
// @ts-ignore
<ScrollView <ScrollView
contentContainerStyle={styles.container} contentContainerStyle={styles.container}
scrollIndicatorInsets={{ right: 1 }} // https://github.com/facebook/react-native/issues/26610#issuecomment-539843444 scrollIndicatorInsets={{ right: 1 }} // https://github.com/facebook/react-native/issues/26610#issuecomment-539843444
{...scrollPersistTaps} {...scrollPersistTaps}
{...props} {...props}>
>
{children} {children}
</ScrollView> </ScrollView>
)); ));
ListContainer.propTypes = {
children: PropTypes.array.isRequired
};
ListContainer.displayName = 'List.Container'; ListContainer.displayName = 'List.Container';
export default withTheme(ListContainer); export default withTheme(ListContainer);

View File

@ -1,6 +1,5 @@
import React from 'react'; import React from 'react';
import { View, Text, StyleSheet } from 'react-native'; import { StyleSheet, Text, View } from 'react-native';
import PropTypes from 'prop-types';
import sharedStyles from '../../views/Styles'; import sharedStyles from '../../views/Styles';
import { themes } from '../../constants/colors'; import { themes } from '../../constants/colors';
@ -19,22 +18,20 @@ const styles = StyleSheet.create({
} }
}); });
const ListHeader = React.memo(({ title, theme, translateTitle }) => ( interface IListHeader {
title: string;
theme: string;
translateTitle: boolean;
}
const ListHeader = React.memo(({ title, theme, translateTitle = true }: IListHeader) => (
<View style={styles.container}> <View style={styles.container}>
<Text style={[styles.title, { color: themes[theme].infoText }]} numberOfLines={1}>{translateTitle ? I18n.t(title) : title}</Text> <Text style={[styles.title, { color: themes[theme].infoText }]} numberOfLines={1}>
{translateTitle ? I18n.t(title) : title}
</Text>
</View> </View>
)); ));
ListHeader.propTypes = {
title: PropTypes.string,
theme: PropTypes.string,
translateTitle: PropTypes.bool
};
ListHeader.defaultProps = {
translateTitle: true
};
ListHeader.displayName = 'List.Header'; ListHeader.displayName = 'List.Header';
export default withTheme(ListHeader); export default withTheme(ListHeader);

View File

@ -1,44 +0,0 @@
import React from 'react';
import { View, StyleSheet } from 'react-native';
import PropTypes from 'prop-types';
import { themes } from '../../constants/colors';
import { CustomIcon } from '../../lib/Icons';
import { withTheme } from '../../theme';
import { ICON_SIZE } from './constants';
const styles = StyleSheet.create({
icon: {
alignItems: 'center',
justifyContent: 'center'
}
});
const ListIcon = React.memo(({
theme,
name,
color,
style,
testID
}) => (
<View style={[styles.icon, style]}>
<CustomIcon
name={name}
color={color ?? themes[theme].auxiliaryText}
size={ICON_SIZE}
testID={testID}
/>
</View>
));
ListIcon.propTypes = {
theme: PropTypes.string,
name: PropTypes.string,
color: PropTypes.string,
style: PropTypes.object,
testID: PropTypes.string
};
ListIcon.displayName = 'List.Icon';
export default withTheme(ListIcon);

View File

@ -0,0 +1,32 @@
import React from 'react';
import { StyleSheet, View } from 'react-native';
import { themes } from '../../constants/colors';
import { CustomIcon } from '../../lib/Icons';
import { withTheme } from '../../theme';
import { ICON_SIZE } from './constants';
interface IListIcon {
theme: string;
name: string;
color: string;
style: object;
testID: string;
}
const styles = StyleSheet.create({
icon: {
alignItems: 'center',
justifyContent: 'center'
}
});
const ListIcon = React.memo(({ theme, name, color, style, testID }: IListIcon) => (
<View style={[styles.icon, style]}>
<CustomIcon name={name} color={color ?? themes[theme].auxiliaryText} size={ICON_SIZE} testID={testID} />
</View>
));
ListIcon.displayName = 'List.Icon';
export default withTheme(ListIcon);

View File

@ -1,6 +1,5 @@
import React from 'react'; import React from 'react';
import { View, Text, StyleSheet } from 'react-native'; import { StyleSheet, Text, View } from 'react-native';
import PropTypes from 'prop-types';
import sharedStyles from '../../views/Styles'; import sharedStyles from '../../views/Styles';
import { themes } from '../../constants/colors'; import { themes } from '../../constants/colors';
@ -19,22 +18,18 @@ const styles = StyleSheet.create({
} }
}); });
const ListInfo = React.memo(({ info, translateInfo, theme }) => ( interface IListHeader {
info: string;
theme: string;
translateInfo: boolean;
}
const ListInfo = React.memo(({ info, theme, translateInfo = true }: IListHeader) => (
<View style={styles.container}> <View style={styles.container}>
<Text style={[styles.text, { color: themes[theme].infoText }]}>{translateInfo ? I18n.t(info) : info}</Text> <Text style={[styles.text, { color: themes[theme].infoText }]}>{translateInfo ? I18n.t(info) : info}</Text>
</View> </View>
)); ));
ListInfo.propTypes = {
info: PropTypes.string,
theme: PropTypes.string,
translateInfo: PropTypes.bool
};
ListInfo.defaultProps = {
translateInfo: true
};
ListInfo.displayName = 'List.Info'; ListInfo.displayName = 'List.Info';
export default withTheme(ListInfo); export default withTheme(ListInfo);

View File

@ -1,163 +0,0 @@
import React from 'react';
import {
View, Text, StyleSheet, I18nManager
} from 'react-native';
import PropTypes from 'prop-types';
import Touch from '../../utils/touch';
import { themes } from '../../constants/colors';
import sharedStyles from '../../views/Styles';
import { withTheme } from '../../theme';
import I18n from '../../i18n';
import { Icon } from '.';
import { BASE_HEIGHT, ICON_SIZE, PADDING_HORIZONTAL } from './constants';
import { withDimensions } from '../../dimensions';
import { CustomIcon } from '../../lib/Icons';
const styles = StyleSheet.create({
container: {
flex: 1,
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center',
paddingHorizontal: PADDING_HORIZONTAL
},
leftContainer: {
paddingRight: PADDING_HORIZONTAL
},
rightContainer: {
paddingLeft: PADDING_HORIZONTAL
},
disabled: {
opacity: 0.3
},
textContainer: {
flex: 1,
justifyContent: 'center'
},
textAlertContainer: {
flexDirection: 'row',
alignItems: 'center'
},
alertIcon: {
paddingLeft: 4
},
title: {
flexShrink: 1,
fontSize: 16,
...sharedStyles.textRegular
},
subtitle: {
fontSize: 14,
...sharedStyles.textRegular
},
actionIndicator: {
...I18nManager.isRTL
? { transform: [{ rotate: '180deg' }] }
: {}
}
});
const Content = React.memo(({
title, subtitle, disabled, testID, left, right, color, theme, translateTitle, translateSubtitle, showActionIndicator, fontScale, alert
}) => (
<View style={[styles.container, disabled && styles.disabled, { height: BASE_HEIGHT * fontScale }]} testID={testID}>
{left
? (
<View style={styles.leftContainer}>
{left()}
</View>
)
: null}
<View style={styles.textContainer}>
<View style={styles.textAlertContainer}>
<Text style={[styles.title, { color: color || themes[theme].titleText }]} numberOfLines={1}>{translateTitle ? I18n.t(title) : title}</Text>
{alert ? (
<CustomIcon style={[styles.alertIcon, { color: themes[theme].dangerColor }]} size={ICON_SIZE} name='info' />
) : null}
</View>
{subtitle
? <Text style={[styles.subtitle, { color: themes[theme].auxiliaryText }]} numberOfLines={1}>{translateSubtitle ? I18n.t(subtitle) : subtitle}</Text>
: null
}
</View>
{right || showActionIndicator
? (
<View style={styles.rightContainer}>
{right ? right() : null}
{showActionIndicator ? <Icon name='chevron-right' style={styles.actionIndicator} /> : null}
</View>
)
: null}
</View>
));
const Button = React.memo(({
onPress, backgroundColor, underlayColor, ...props
}) => (
<Touch
onPress={() => onPress(props.title)}
style={{ backgroundColor: backgroundColor || themes[props.theme].backgroundColor }}
underlayColor={underlayColor}
enabled={!props.disabled}
theme={props.theme}
>
<Content {...props} />
</Touch>
));
const ListItem = React.memo(({ ...props }) => {
if (props.onPress) {
return <Button {...props} />;
}
return (
<View style={{ backgroundColor: props.backgroundColor || themes[props.theme].backgroundColor }}>
<Content {...props} />
</View>
);
});
ListItem.propTypes = {
onPress: PropTypes.func,
theme: PropTypes.string,
backgroundColor: PropTypes.string
};
ListItem.displayName = 'List.Item';
Content.propTypes = {
title: PropTypes.string.isRequired,
subtitle: PropTypes.string,
left: PropTypes.func,
right: PropTypes.func,
disabled: PropTypes.bool,
testID: PropTypes.string,
theme: PropTypes.string,
color: PropTypes.string,
translateTitle: PropTypes.bool,
translateSubtitle: PropTypes.bool,
showActionIndicator: PropTypes.bool,
fontScale: PropTypes.number,
alert: PropTypes.bool
};
Content.defaultProps = {
translateTitle: true,
translateSubtitle: true,
showActionIndicator: false
};
Button.propTypes = {
title: PropTypes.string,
onPress: PropTypes.func,
disabled: PropTypes.bool,
theme: PropTypes.string,
backgroundColor: PropTypes.string,
underlayColor: PropTypes.string
};
Button.defaultProps = {
disabled: false
};
export default withTheme(withDimensions(ListItem));

View File

@ -0,0 +1,154 @@
import React from 'react';
import { I18nManager, StyleSheet, Text, View } from 'react-native';
import Touch from '../../utils/touch';
import { themes } from '../../constants/colors';
import sharedStyles from '../../views/Styles';
import { withTheme } from '../../theme';
import I18n from '../../i18n';
import { Icon } from '.';
import { BASE_HEIGHT, ICON_SIZE, PADDING_HORIZONTAL } from './constants';
import { withDimensions } from '../../dimensions';
import { CustomIcon } from '../../lib/Icons';
const styles = StyleSheet.create({
container: {
flex: 1,
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center',
paddingHorizontal: PADDING_HORIZONTAL
},
leftContainer: {
paddingRight: PADDING_HORIZONTAL
},
rightContainer: {
paddingLeft: PADDING_HORIZONTAL
},
disabled: {
opacity: 0.3
},
textContainer: {
flex: 1,
justifyContent: 'center'
},
textAlertContainer: {
flexDirection: 'row',
alignItems: 'center'
},
alertIcon: {
paddingLeft: 4
},
title: {
flexShrink: 1,
fontSize: 16,
...sharedStyles.textRegular
},
subtitle: {
fontSize: 14,
...sharedStyles.textRegular
},
actionIndicator: {
...(I18nManager.isRTL ? { transform: [{ rotate: '180deg' }] } : {})
}
});
interface IListItemContent {
title?: string;
subtitle?: string;
left?: Function;
right?: Function;
disabled?: boolean;
testID?: string;
theme: string;
color?: string;
translateTitle?: boolean;
translateSubtitle?: boolean;
showActionIndicator?: boolean;
fontScale?: number;
alert?: boolean;
}
const Content = React.memo(
({
title,
subtitle,
disabled,
testID,
left,
right,
color,
theme,
fontScale,
alert,
translateTitle = true,
translateSubtitle = true,
showActionIndicator = false
}: IListItemContent) => (
<View style={[styles.container, disabled && styles.disabled, { height: BASE_HEIGHT * fontScale! }]} testID={testID}>
{left ? <View style={styles.leftContainer}>{left()}</View> : null}
<View style={styles.textContainer}>
<View style={styles.textAlertContainer}>
<Text style={[styles.title, { color: color || themes[theme].titleText }]} numberOfLines={1}>
{translateTitle ? I18n.t(title) : title}
</Text>
{alert ? (
<CustomIcon style={[styles.alertIcon, { color: themes[theme].dangerColor }]} size={ICON_SIZE} name='info' />
) : null}
</View>
{subtitle ? (
<Text style={[styles.subtitle, { color: themes[theme].auxiliaryText }]} numberOfLines={1}>
{translateSubtitle ? I18n.t(subtitle) : subtitle}
</Text>
) : null}
</View>
{right || showActionIndicator ? (
<View style={styles.rightContainer}>
{right ? right() : null}
{showActionIndicator ? <Icon name='chevron-right' style={styles.actionIndicator} /> : null}
</View>
) : null}
</View>
)
);
interface IListItemButton {
title?: string;
onPress: Function;
disabled?: boolean;
theme: string;
backgroundColor: string;
underlayColor?: string;
}
const Button = React.memo(({ onPress, backgroundColor, underlayColor, ...props }: IListItemButton) => (
<Touch
onPress={() => onPress(props.title)}
style={{ backgroundColor: backgroundColor || themes[props.theme].backgroundColor }}
underlayColor={underlayColor}
enabled={!props.disabled}
theme={props.theme}>
<Content {...props} />
</Touch>
));
interface IListItem {
onPress: Function;
theme: string;
backgroundColor: string;
}
const ListItem = React.memo(({ ...props }: IListItem) => {
if (props.onPress) {
return <Button {...props} />;
}
return (
<View style={{ backgroundColor: props.backgroundColor || themes[props.theme].backgroundColor }}>
<Content {...props} />
</View>
);
});
ListItem.displayName = 'List.Item';
export default withTheme(withDimensions(ListItem));

View File

@ -1,6 +1,6 @@
import React from 'react'; import React from 'react';
import { View, StyleSheet } from 'react-native'; import { StyleSheet, View } from 'react-native';
import PropTypes from 'prop-types';
import { withTheme } from '../../theme'; import { withTheme } from '../../theme';
import { Header } from '.'; import { Header } from '.';
@ -10,19 +10,19 @@ const styles = StyleSheet.create({
} }
}); });
const ListSection = React.memo(({ children, title, translateTitle }) => ( interface IListSection {
children: JSX.Element;
title: string;
translateTitle: boolean;
}
const ListSection = React.memo(({ children, title, translateTitle }: IListSection) => (
<View style={styles.container}> <View style={styles.container}>
{title ? <Header {...{ title, translateTitle }} /> : null} {title ? <Header {...{ title, translateTitle }} /> : null}
{children} {children}
</View> </View>
)); ));
ListSection.propTypes = {
children: PropTypes.array.isRequired,
title: PropTypes.string,
translateTitle: PropTypes.bool
};
ListSection.displayName = 'List.Section'; ListSection.displayName = 'List.Section';
export default withTheme(ListSection); export default withTheme(ListSection);

View File

@ -1,32 +0,0 @@
import React from 'react';
import { View, StyleSheet } from 'react-native';
import PropTypes from 'prop-types';
import { themes } from '../../constants/colors';
import { withTheme } from '../../theme';
const styles = StyleSheet.create({
separator: {
height: StyleSheet.hairlineWidth
}
});
const ListSeparator = React.memo(({ style, theme }) => (
<View
style={[
styles.separator,
style,
{ backgroundColor: themes[theme].separatorColor }
]}
/>
));
ListSeparator.propTypes = {
style: PropTypes.object,
theme: PropTypes.string
};
ListSeparator.displayName = 'List.Separator';
export default withTheme(ListSeparator);

View File

@ -0,0 +1,24 @@
import React from 'react';
import { StyleSheet, View } from 'react-native';
import { themes } from '../../constants/colors';
import { withTheme } from '../../theme';
const styles = StyleSheet.create({
separator: {
height: StyleSheet.hairlineWidth
}
});
interface IListSeparator {
style: object;
theme: string;
}
const ListSeparator = React.memo(({ style, theme }: IListSeparator) => (
<View style={[styles.separator, style, { backgroundColor: themes[theme].separatorColor }]} />
));
ListSeparator.displayName = 'List.Separator';
export default withTheme(ListSeparator);

View File

@ -1,8 +1,6 @@
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types'; import { Animated, Modal, StyleSheet, View } from 'react-native';
import {
StyleSheet, Modal, Animated, View
} from 'react-native';
import { withTheme } from '../theme'; import { withTheme } from '../theme';
import { themes } from '../constants/colors'; import { themes } from '../constants/colors';
@ -19,54 +17,51 @@ const styles = StyleSheet.create({
} }
}); });
class Loading extends React.PureComponent { interface ILoadingProps {
static propTypes = { visible: boolean;
visible: PropTypes.bool, theme: string;
theme: PropTypes.string }
}
class Loading extends React.PureComponent<ILoadingProps, any> {
state = { state = {
scale: new Animated.Value(1), scale: new Animated.Value(1),
opacity: new Animated.Value(0) opacity: new Animated.Value(0)
} };
private opacityAnimation: any;
private scaleAnimation: any;
componentDidMount() { componentDidMount() {
const { opacity, scale } = this.state; const { opacity, scale } = this.state;
const { visible } = this.props; const { visible } = this.props;
this.opacityAnimation = Animated.timing( this.opacityAnimation = Animated.timing(opacity, {
opacity, toValue: 1,
{ duration: 200,
toValue: 1, useNativeDriver: true
duration: 200, });
useNativeDriver: true this.scaleAnimation = Animated.loop(
} Animated.sequence([
); Animated.timing(scale, {
this.scaleAnimation = Animated.loop(Animated.sequence([
Animated.timing(
scale,
{
toValue: 0, toValue: 0,
duration: 1000, duration: 1000,
useNativeDriver: true useNativeDriver: true
} }),
), Animated.timing(scale, {
Animated.timing(
scale,
{
toValue: 1, toValue: 1,
duration: 1000, duration: 1000,
useNativeDriver: true useNativeDriver: true
} })
) ])
])); );
if (visible) { if (visible) {
this.startAnimations(); this.startAnimations();
} }
} }
componentDidUpdate(prevProps) { componentDidUpdate(prevProps: any) {
const { visible } = this.props; const { visible } = this.props;
if (visible && visible !== prevProps.visible) { if (visible && visible !== prevProps.visible) {
this.startAnimations(); this.startAnimations();
@ -107,29 +102,30 @@ class Loading extends React.PureComponent {
}); });
return ( return (
<Modal <Modal visible={visible} transparent onRequestClose={() => {}}>
visible={visible} <View style={styles.container} testID='loading'>
transparent
onRequestClose={() => {}}
>
<View
style={styles.container}
testID='loading'
>
<Animated.View <Animated.View
style={[{ style={[
...StyleSheet.absoluteFill, {
backgroundColor: themes[theme].backdropColor, // @ts-ignore
opacity: opacityAnimation ...StyleSheet.absoluteFill,
}]} backgroundColor: themes[theme].backdropColor,
opacity: opacityAnimation
}
]}
/> />
<Animated.Image <Animated.Image
source={require('../static/images/logo.png')} source={require('../static/images/logo.png')}
style={[styles.image, { style={[
transform: [{ styles.image,
scale: scaleAnimation {
}] transform: [
}]} {
scale: scaleAnimation
}
]
}
]}
/> />
</View> </View>
</Modal> </Modal>
@ -137,4 +133,4 @@ class Loading extends React.PureComponent {
} }
} }
export default (withTheme(Loading)); export default withTheme(Loading);

View File

@ -1,8 +1,5 @@
import React from 'react'; import React from 'react';
import { import { Animated, Easing, Linking, StyleSheet, Text, View } from 'react-native';
View, StyleSheet, Text, Animated, Easing, Linking
} from 'react-native';
import PropTypes from 'prop-types';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { Base64 } from 'js-base64'; import { Base64 } from 'js-base64';
import * as AppleAuthentication from 'expo-apple-authentication'; import * as AppleAuthentication from 'expo-apple-authentication';
@ -15,7 +12,7 @@ import OrSeparator from './OrSeparator';
import Touch from '../utils/touch'; import Touch from '../utils/touch';
import I18n from '../i18n'; import I18n from '../i18n';
import random from '../utils/random'; import random from '../utils/random';
import { logEvent, events } from '../utils/log'; import { events, logEvent } from '../utils/log';
import RocketChat from '../lib/rocketchat'; import RocketChat from '../lib/rocketchat';
import { CustomIcon } from '../lib/Icons'; import { CustomIcon } from '../lib/Icons';
@ -60,153 +57,175 @@ const styles = StyleSheet.create({
} }
}); });
class LoginServices extends React.PureComponent { interface IOpenOAuth {
static propTypes = { url?: string;
navigation: PropTypes.object, ssoToken?: string;
server: PropTypes.string, authType?: string;
services: PropTypes.object, }
Gitlab_URL: PropTypes.string,
CAS_enabled: PropTypes.bool, interface IService {
CAS_login_url: PropTypes.string, name: string;
separator: PropTypes.bool, service: string;
theme: PropTypes.string authType: string;
} buttonColor: string;
buttonLabelColor: string;
}
interface ILoginServicesProps {
navigation: any;
server: string;
services: {
facebook: { clientId: string };
github: { clientId: string };
gitlab: { clientId: string };
google: { clientId: string };
linkedin: { clientId: string };
'meteor-developer': { clientId: string };
wordpress: { clientId: string; serverURL: string };
};
Gitlab_URL: string;
CAS_enabled: boolean;
CAS_login_url: string;
separator: boolean;
theme: string;
}
class LoginServices extends React.PureComponent<ILoginServicesProps, any> {
private _animation: any;
static defaultProps = { static defaultProps = {
separator: true separator: true
} };
state = { state = {
collapsed: true, collapsed: true,
servicesHeight: new Animated.Value(SERVICES_COLLAPSED_HEIGHT) servicesHeight: new Animated.Value(SERVICES_COLLAPSED_HEIGHT)
} };
onPressFacebook = () => { onPressFacebook = () => {
logEvent(events.ENTER_WITH_FACEBOOK); logEvent(events.ENTER_WITH_FACEBOOK);
const { services, server } = this.props; const { services, server } = this.props;
const { clientId } = services.facebook; const { clientId } = services.facebook;
const endpoint = 'https://m.facebook.com/v2.9/dialog/oauth'; const endpoint = 'https://m.facebook.com/v2.9/dialog/oauth';
const redirect_uri = `${ server }/_oauth/facebook?close`; const redirect_uri = `${server}/_oauth/facebook?close`;
const scope = 'email'; const scope = 'email';
const state = this.getOAuthState(); const state = this.getOAuthState();
const params = `?client_id=${ clientId }&redirect_uri=${ redirect_uri }&scope=${ scope }&state=${ state }&display=touch`; const params = `?client_id=${clientId}&redirect_uri=${redirect_uri}&scope=${scope}&state=${state}&display=touch`;
this.openOAuth({ url: `${ endpoint }${ params }` }); this.openOAuth({ url: `${endpoint}${params}` });
} };
onPressGithub = () => { onPressGithub = () => {
logEvent(events.ENTER_WITH_GITHUB); logEvent(events.ENTER_WITH_GITHUB);
const { services, server } = this.props; const { services, server } = this.props;
const { clientId } = services.github; const { clientId } = services.github;
const endpoint = `https://github.com/login?client_id=${ clientId }&return_to=${ encodeURIComponent('/login/oauth/authorize') }`; const endpoint = `https://github.com/login?client_id=${clientId}&return_to=${encodeURIComponent('/login/oauth/authorize')}`;
const redirect_uri = `${ server }/_oauth/github?close`; const redirect_uri = `${server}/_oauth/github?close`;
const scope = 'user:email'; const scope = 'user:email';
const state = this.getOAuthState(); const state = this.getOAuthState();
const params = `?client_id=${ clientId }&redirect_uri=${ redirect_uri }&scope=${ scope }&state=${ state }`; const params = `?client_id=${clientId}&redirect_uri=${redirect_uri}&scope=${scope}&state=${state}`;
this.openOAuth({ url: `${ endpoint }${ encodeURIComponent(params) }` }); this.openOAuth({ url: `${endpoint}${encodeURIComponent(params)}` });
} };
onPressGitlab = () => { onPressGitlab = () => {
logEvent(events.ENTER_WITH_GITLAB); logEvent(events.ENTER_WITH_GITLAB);
const { services, server, Gitlab_URL } = this.props; const { services, server, Gitlab_URL } = this.props;
const { clientId } = services.gitlab; const { clientId } = services.gitlab;
const baseURL = Gitlab_URL ? Gitlab_URL.trim().replace(/\/*$/, '') : 'https://gitlab.com'; const baseURL = Gitlab_URL ? Gitlab_URL.trim().replace(/\/*$/, '') : 'https://gitlab.com';
const endpoint = `${ baseURL }/oauth/authorize`; const endpoint = `${baseURL}/oauth/authorize`;
const redirect_uri = `${ server }/_oauth/gitlab?close`; const redirect_uri = `${server}/_oauth/gitlab?close`;
const scope = 'read_user'; const scope = 'read_user';
const state = this.getOAuthState(); const state = this.getOAuthState();
const params = `?client_id=${ clientId }&redirect_uri=${ redirect_uri }&scope=${ scope }&state=${ state }&response_type=code`; const params = `?client_id=${clientId}&redirect_uri=${redirect_uri}&scope=${scope}&state=${state}&response_type=code`;
this.openOAuth({ url: `${ endpoint }${ params }` }); this.openOAuth({ url: `${endpoint}${params}` });
} };
onPressGoogle = () => { onPressGoogle = () => {
logEvent(events.ENTER_WITH_GOOGLE); logEvent(events.ENTER_WITH_GOOGLE);
const { services, server } = this.props; const { services, server } = this.props;
const { clientId } = services.google; const { clientId } = services.google;
const endpoint = 'https://accounts.google.com/o/oauth2/auth'; const endpoint = 'https://accounts.google.com/o/oauth2/auth';
const redirect_uri = `${ server }/_oauth/google?close`; const redirect_uri = `${server}/_oauth/google?close`;
const scope = 'email'; const scope = 'email';
const state = this.getOAuthState(LOGIN_STYPE_REDIRECT); const state = this.getOAuthState(LOGIN_STYPE_REDIRECT);
const params = `?client_id=${ clientId }&redirect_uri=${ redirect_uri }&scope=${ scope }&state=${ state }&response_type=code`; const params = `?client_id=${clientId}&redirect_uri=${redirect_uri}&scope=${scope}&state=${state}&response_type=code`;
Linking.openURL(`${ endpoint }${ params }`); Linking.openURL(`${endpoint}${params}`);
} };
onPressLinkedin = () => { onPressLinkedin = () => {
logEvent(events.ENTER_WITH_LINKEDIN); logEvent(events.ENTER_WITH_LINKEDIN);
const { services, server } = this.props; const { services, server } = this.props;
const { clientId } = services.linkedin; const { clientId } = services.linkedin;
const endpoint = 'https://www.linkedin.com/oauth/v2/authorization'; const endpoint = 'https://www.linkedin.com/oauth/v2/authorization';
const redirect_uri = `${ server }/_oauth/linkedin?close`; const redirect_uri = `${server}/_oauth/linkedin?close`;
const scope = 'r_liteprofile,r_emailaddress'; const scope = 'r_liteprofile,r_emailaddress';
const state = this.getOAuthState(); const state = this.getOAuthState();
const params = `?client_id=${ clientId }&redirect_uri=${ redirect_uri }&scope=${ scope }&state=${ state }&response_type=code`; const params = `?client_id=${clientId}&redirect_uri=${redirect_uri}&scope=${scope}&state=${state}&response_type=code`;
this.openOAuth({ url: `${ endpoint }${ params }` }); this.openOAuth({ url: `${endpoint}${params}` });
} };
onPressMeteor = () => { onPressMeteor = () => {
logEvent(events.ENTER_WITH_METEOR); logEvent(events.ENTER_WITH_METEOR);
const { services, server } = this.props; const { services, server } = this.props;
const { clientId } = services['meteor-developer']; const { clientId } = services['meteor-developer'];
const endpoint = 'https://www.meteor.com/oauth2/authorize'; const endpoint = 'https://www.meteor.com/oauth2/authorize';
const redirect_uri = `${ server }/_oauth/meteor-developer`; const redirect_uri = `${server}/_oauth/meteor-developer`;
const state = this.getOAuthState(); const state = this.getOAuthState();
const params = `?client_id=${ clientId }&redirect_uri=${ redirect_uri }&state=${ state }&response_type=code`; const params = `?client_id=${clientId}&redirect_uri=${redirect_uri}&state=${state}&response_type=code`;
this.openOAuth({ url: `${ endpoint }${ params }` }); this.openOAuth({ url: `${endpoint}${params}` });
} };
onPressTwitter = () => { onPressTwitter = () => {
logEvent(events.ENTER_WITH_TWITTER); logEvent(events.ENTER_WITH_TWITTER);
const { server } = this.props; const { server } = this.props;
const state = this.getOAuthState(); const state = this.getOAuthState();
const url = `${ server }/_oauth/twitter/?requestTokenAndRedirect=true&state=${ state }`; const url = `${server}/_oauth/twitter/?requestTokenAndRedirect=true&state=${state}`;
this.openOAuth({ url }); this.openOAuth({ url });
} };
onPressWordpress = () => { onPressWordpress = () => {
logEvent(events.ENTER_WITH_WORDPRESS); logEvent(events.ENTER_WITH_WORDPRESS);
const { services, server } = this.props; const { services, server } = this.props;
const { clientId, serverURL } = services.wordpress; const { clientId, serverURL } = services.wordpress;
const endpoint = `${ serverURL }/oauth/authorize`; const endpoint = `${serverURL}/oauth/authorize`;
const redirect_uri = `${ server }/_oauth/wordpress?close`; const redirect_uri = `${server}/_oauth/wordpress?close`;
const scope = 'openid'; const scope = 'openid';
const state = this.getOAuthState(); const state = this.getOAuthState();
const params = `?client_id=${ clientId }&redirect_uri=${ redirect_uri }&scope=${ scope }&state=${ state }&response_type=code`; const params = `?client_id=${clientId}&redirect_uri=${redirect_uri}&scope=${scope}&state=${state}&response_type=code`;
this.openOAuth({ url: `${ endpoint }${ params }` }); this.openOAuth({ url: `${endpoint}${params}` });
} };
onPressCustomOAuth = (loginService) => { onPressCustomOAuth = (loginService: any) => {
logEvent(events.ENTER_WITH_CUSTOM_OAUTH); logEvent(events.ENTER_WITH_CUSTOM_OAUTH);
const { server } = this.props; const { server } = this.props;
const { const { serverURL, authorizePath, clientId, scope, service } = loginService;
serverURL, authorizePath, clientId, scope, service const redirectUri = `${server}/_oauth/${service}`;
} = loginService;
const redirectUri = `${ server }/_oauth/${ service }`;
const state = this.getOAuthState(); const state = this.getOAuthState();
const params = `?client_id=${ clientId }&redirect_uri=${ redirectUri }&response_type=code&state=${ state }&scope=${ scope }`; const params = `?client_id=${clientId}&redirect_uri=${redirectUri}&response_type=code&state=${state}&scope=${scope}`;
const domain = `${ serverURL }`; const domain = `${serverURL}`;
const absolutePath = `${ authorizePath }${ params }`; const absolutePath = `${authorizePath}${params}`;
const url = absolutePath.includes(domain) ? absolutePath : domain + absolutePath; const url = absolutePath.includes(domain) ? absolutePath : domain + absolutePath;
this.openOAuth({ url }); this.openOAuth({ url });
} };
onPressSaml = (loginService) => { onPressSaml = (loginService: any) => {
logEvent(events.ENTER_WITH_SAML); logEvent(events.ENTER_WITH_SAML);
const { server } = this.props; const { server } = this.props;
const { clientConfig } = loginService; const { clientConfig } = loginService;
const { provider } = clientConfig; const { provider } = clientConfig;
const ssoToken = random(17); const ssoToken = random(17);
const url = `${ server }/_saml/authorize/${ provider }/${ ssoToken }`; const url = `${server}/_saml/authorize/${provider}/${ssoToken}`;
this.openOAuth({ url, ssoToken, authType: 'saml' }); this.openOAuth({ url, ssoToken, authType: 'saml' });
} };
onPressCas = () => { onPressCas = () => {
logEvent(events.ENTER_WITH_CAS); logEvent(events.ENTER_WITH_CAS);
const { server, CAS_login_url } = this.props; const { server, CAS_login_url } = this.props;
const ssoToken = random(17); const ssoToken = random(17);
const url = `${ CAS_login_url }?service=${ server }/_cas/${ ssoToken }`; const url = `${CAS_login_url}?service=${server}/_cas/${ssoToken}`;
this.openOAuth({ url, ssoToken, authType: 'cas' }); this.openOAuth({ url, ssoToken, authType: 'cas' });
} };
onPressAppleLogin = async() => { onPressAppleLogin = async () => {
logEvent(events.ENTER_WITH_APPLE); logEvent(events.ENTER_WITH_APPLE);
try { try {
const { fullName, email, identityToken } = await AppleAuthentication.signInAsync({ const { fullName, email, identityToken } = await AppleAuthentication.signInAsync({
@ -220,11 +239,11 @@ class LoginServices extends React.PureComponent {
} catch { } catch {
logEvent(events.ENTER_WITH_APPLE_F); logEvent(events.ENTER_WITH_APPLE_F);
} }
} };
getOAuthState = (loginStyle = LOGIN_STYPE_POPUP) => { getOAuthState = (loginStyle = LOGIN_STYPE_POPUP) => {
const credentialToken = random(43); const credentialToken = random(43);
let obj = { loginStyle, credentialToken, isCordova: true }; let obj: any = { loginStyle, credentialToken, isCordova: true };
if (loginStyle === LOGIN_STYPE_REDIRECT) { if (loginStyle === LOGIN_STYPE_REDIRECT) {
obj = { obj = {
...obj, ...obj,
@ -232,24 +251,26 @@ class LoginServices extends React.PureComponent {
}; };
} }
return Base64.encodeURI(JSON.stringify(obj)); return Base64.encodeURI(JSON.stringify(obj));
} };
openOAuth = ({ url, ssoToken, authType = 'oauth' }) => { openOAuth = ({ url, ssoToken, authType = 'oauth' }: IOpenOAuth) => {
const { navigation } = this.props; const { navigation } = this.props;
navigation.navigate('AuthenticationWebView', { url, authType, ssoToken }); navigation.navigate('AuthenticationWebView', { url, authType, ssoToken });
} };
transitionServicesTo = (height) => { transitionServicesTo = (height: number) => {
const { servicesHeight } = this.state; const { servicesHeight } = this.state;
if (this._animation) { if (this._animation) {
this._animation.stop(); this._animation.stop();
} }
// @ts-ignore
this._animation = Animated.timing(servicesHeight, { this._animation = Animated.timing(servicesHeight, {
toValue: height, toValue: height,
duration: 300, duration: 300,
// @ts-ignore
easing: Easing.easeOutCubic easing: Easing.easeOutCubic
}).start(); }).start();
} };
toggleServices = () => { toggleServices = () => {
const { collapsed } = this.state; const { collapsed } = this.state;
@ -260,11 +281,11 @@ class LoginServices extends React.PureComponent {
} else { } else {
this.transitionServicesTo(SERVICES_COLLAPSED_HEIGHT); this.transitionServicesTo(SERVICES_COLLAPSED_HEIGHT);
} }
this.setState(prevState => ({ collapsed: !prevState.collapsed })); this.setState((prevState: any) => ({ collapsed: !prevState.collapsed }));
} };
getSocialOauthProvider = (name) => { getSocialOauthProvider = (name: string) => {
const oauthProviders = { const oauthProviders: any = {
facebook: this.onPressFacebook, facebook: this.onPressFacebook,
github: this.onPressGithub, github: this.onPressGithub,
gitlab: this.onPressGitlab, gitlab: this.onPressGitlab,
@ -275,7 +296,7 @@ class LoginServices extends React.PureComponent {
wordpress: this.onPressWordpress wordpress: this.onPressWordpress
}; };
return oauthProviders[name]; return oauthProviders[name];
} };
renderServicesSeparator = () => { renderServicesSeparator = () => {
const { collapsed } = this.state; const { collapsed } = this.state;
@ -301,13 +322,13 @@ class LoginServices extends React.PureComponent {
return <OrSeparator theme={theme} />; return <OrSeparator theme={theme} />;
} }
return null; return null;
} };
renderItem = (service) => { renderItem = (service: IService) => {
const { CAS_enabled, theme } = this.props; const { CAS_enabled, theme } = this.props;
let { name } = service; let { name } = service;
name = name === 'meteor-developer' ? 'meteor' : name; name = name === 'meteor-developer' ? 'meteor' : name;
const icon = `${ name }-monochromatic`; const icon = `${name}-monochromatic`;
const isSaml = service.service === 'saml'; const isSaml = service.service === 'saml';
let onPress = () => {}; let onPress = () => {};
@ -357,15 +378,16 @@ class LoginServices extends React.PureComponent {
style={[styles.serviceButton, { backgroundColor }]} style={[styles.serviceButton, { backgroundColor }]}
theme={theme} theme={theme}
activeOpacity={0.5} activeOpacity={0.5}
underlayColor={themes[theme].buttonText} underlayColor={themes[theme].buttonText}>
>
<View style={styles.serviceButtonContainer}> <View style={styles.serviceButtonContainer}>
{service.authType === 'oauth' || service.authType === 'apple' ? <CustomIcon name={icon} size={24} color={themes[theme].titleText} style={styles.serviceIcon} /> : null} {service.authType === 'oauth' || service.authType === 'apple' ? (
<CustomIcon name={icon} size={24} color={themes[theme].titleText} style={styles.serviceIcon} />
) : null}
<Text style={[styles.serviceText, { color: themes[theme].titleText }]}>{buttonText}</Text> <Text style={[styles.serviceText, { color: themes[theme].titleText }]}>{buttonText}</Text>
</View> </View>
</Touch> </Touch>
); );
} };
render() { render() {
const { servicesHeight } = this.state; const { servicesHeight } = this.state;
@ -379,23 +401,21 @@ class LoginServices extends React.PureComponent {
if (length > 3 && separator) { if (length > 3 && separator) {
return ( return (
<> <>
<Animated.View style={style}> <Animated.View style={style}>{Object.values(services).map((service: any) => this.renderItem(service))}</Animated.View>
{Object.values(services).map(service => this.renderItem(service))}
</Animated.View>
{this.renderServicesSeparator()} {this.renderServicesSeparator()}
</> </>
); );
} }
return ( return (
<> <>
{Object.values(services).map(service => this.renderItem(service))} {Object.values(services).map((service: any) => this.renderItem(service))}
{this.renderServicesSeparator()} {this.renderServicesSeparator()}
</> </>
); );
} }
} }
const mapStateToProps = state => ({ const mapStateToProps = (state: any) => ({
server: state.server.server, server: state.server.server,
Gitlab_URL: state.settings.API_Gitlab_URL, Gitlab_URL: state.settings.API_Gitlab_URL,
CAS_enabled: state.settings.CAS_enabled, CAS_enabled: state.settings.CAS_enabled,

View File

@ -1,8 +1,5 @@
import React, { useEffect, useState, useCallback } from 'react'; import React, { useCallback, useEffect, useState } from 'react';
import PropTypes from 'prop-types'; import { FlatList, StyleSheet, Text, View } from 'react-native';
import {
View, Text, FlatList, StyleSheet
} from 'react-native';
import { withTheme } from '../../theme'; import { withTheme } from '../../theme';
import { themes } from '../../constants/colors'; import { themes } from '../../constants/colors';
@ -13,6 +10,27 @@ import database from '../../lib/database';
import { Button } from '../ActionSheet'; import { Button } from '../ActionSheet';
import { useDimensions } from '../../dimensions'; import { useDimensions } from '../../dimensions';
import sharedStyles from '../../views/Styles'; import sharedStyles from '../../views/Styles';
import { IEmoji } from '../EmojiPicker/interfaces';
interface IHeader {
handleReaction: Function;
server: string;
message: object;
isMasterDetail: boolean;
theme: string;
}
interface THeaderItem {
item: IEmoji;
onReaction: Function;
server: string;
theme: string;
}
interface THeaderFooter {
onReaction: any;
theme: string;
}
export const HEADER_HEIGHT = 36; export const HEADER_HEIGHT = 36;
const ITEM_SIZE = 36; const ITEM_SIZE = 36;
@ -43,65 +61,47 @@ const styles = StyleSheet.create({
} }
}); });
const keyExtractor = item => item?.id || item; const keyExtractor = (item: any) => item?.id || item;
const DEFAULT_EMOJIS = ['clap', '+1', 'heart_eyes', 'grinning', 'thinking_face', 'smiley']; const DEFAULT_EMOJIS = ['clap', '+1', 'heart_eyes', 'grinning', 'thinking_face', 'smiley'];
const HeaderItem = React.memo(({ const HeaderItem = React.memo(({ item, onReaction, server, theme }: THeaderItem) => (
item, onReaction, server, theme
}) => (
<Button <Button
testID={`message-actions-emoji-${ item.content || item }`} testID={`message-actions-emoji-${item.content || item}`}
onPress={() => onReaction({ emoji: `:${ item.content || item }:` })} onPress={() => onReaction({ emoji: `:${item.content || item}:` })}
style={[styles.headerItem, { backgroundColor: themes[theme].auxiliaryBackground }]} style={[styles.headerItem, { backgroundColor: themes[theme].auxiliaryBackground }]}
theme={theme} theme={theme}>
>
{item?.isCustom ? ( {item?.isCustom ? (
<CustomEmoji style={styles.customEmoji} emoji={item} baseUrl={server} /> <CustomEmoji style={styles.customEmoji} emoji={item} baseUrl={server} />
) : ( ) : (
<Text style={styles.headerIcon}> <Text style={styles.headerIcon}>{shortnameToUnicode(`:${item.content || item}:`)}</Text>
{shortnameToUnicode(`:${ item.content || item }:`)}
</Text>
)} )}
</Button> </Button>
)); ));
HeaderItem.propTypes = {
item: PropTypes.string,
onReaction: PropTypes.func,
server: PropTypes.string,
theme: PropTypes.string
};
const HeaderFooter = React.memo(({ onReaction, theme }) => ( const HeaderFooter = React.memo(({ onReaction, theme }: THeaderFooter) => (
<Button <Button
testID='add-reaction' testID='add-reaction'
onPress={onReaction} onPress={onReaction}
style={[styles.headerItem, { backgroundColor: themes[theme].auxiliaryBackground }]} style={[styles.headerItem, { backgroundColor: themes[theme].auxiliaryBackground }]}
theme={theme} theme={theme}>
>
<CustomIcon name='reaction-add' size={24} color={themes[theme].bodyText} /> <CustomIcon name='reaction-add' size={24} color={themes[theme].bodyText} />
</Button> </Button>
)); ));
HeaderFooter.propTypes = {
onReaction: PropTypes.func,
theme: PropTypes.string
};
const Header = React.memo(({ const Header = React.memo(({ handleReaction, server, message, isMasterDetail, theme }: IHeader) => {
handleReaction, server, message, isMasterDetail, theme
}) => {
const [items, setItems] = useState([]); const [items, setItems] = useState([]);
const { width, height } = useDimensions(); const { width, height }: any = useDimensions();
const setEmojis = async() => { const setEmojis = async () => {
try { try {
const db = database.active; const db = database.active;
const freqEmojiCollection = db.get('frequently_used_emojis'); const freqEmojiCollection = db.get('frequently_used_emojis');
let freqEmojis = await freqEmojiCollection.query().fetch(); let freqEmojis = await freqEmojiCollection.query().fetch();
const isLandscape = width > height; const isLandscape = width > height;
const size = (isLandscape || isMasterDetail ? width / 2 : width) - (CONTAINER_MARGIN * 2); const size = (isLandscape || isMasterDetail ? width / 2 : width) - CONTAINER_MARGIN * 2;
const quantity = (size / (ITEM_SIZE + (ITEM_MARGIN * 2))) - 1; const quantity = size / (ITEM_SIZE + ITEM_MARGIN * 2) - 1;
freqEmojis = freqEmojis.concat(DEFAULT_EMOJIS).slice(0, quantity); freqEmojis = freqEmojis.concat(DEFAULT_EMOJIS).slice(0, quantity);
setItems(freqEmojis); setItems(freqEmojis);
@ -114,11 +114,14 @@ const Header = React.memo(({
setEmojis(); setEmojis();
}, []); }, []);
const onReaction = ({ emoji }) => handleReaction(emoji, message); const onReaction = ({ emoji }: { emoji: IEmoji }) => handleReaction(emoji, message);
const renderItem = useCallback(({ item }) => <HeaderItem item={item} onReaction={onReaction} server={server} theme={theme} />); const renderItem = useCallback(
({ item }) => <HeaderItem item={item} onReaction={onReaction} server={server} theme={theme} />,
[]
);
const renderFooter = useCallback(() => <HeaderFooter onReaction={onReaction} theme={theme} />); const renderFooter = useCallback(() => <HeaderFooter onReaction={onReaction} theme={theme} />, []);
return ( return (
<View style={[styles.container, { backgroundColor: themes[theme].focusedBackground }]}> <View style={[styles.container, { backgroundColor: themes[theme].focusedBackground }]}>
@ -135,11 +138,5 @@ const Header = React.memo(({
</View> </View>
); );
}); });
Header.propTypes = {
handleReaction: PropTypes.func,
server: PropTypes.string,
message: PropTypes.object,
isMasterDetail: PropTypes.bool,
theme: PropTypes.string
};
export default withTheme(Header); export default withTheme(Header);

View File

@ -1,472 +0,0 @@
import React, { forwardRef, useImperativeHandle } from 'react';
import PropTypes from 'prop-types';
import { Alert, Clipboard, Share } from 'react-native';
import { connect } from 'react-redux';
import moment from 'moment';
import RocketChat from '../../lib/rocketchat';
import database from '../../lib/database';
import I18n from '../../i18n';
import log, { logEvent } from '../../utils/log';
import Navigation from '../../lib/Navigation';
import { getMessageTranslation } from '../message/utils';
import { LISTENER } from '../Toast';
import EventEmitter from '../../utils/events';
import { showConfirmationAlert } from '../../utils/info';
import { useActionSheet } from '../ActionSheet';
import Header, { HEADER_HEIGHT } from './Header';
import events from '../../utils/log/events';
const MessageActions = React.memo(forwardRef(({
room,
tmid,
user,
editInit,
reactionInit,
onReactionPress,
replyInit,
isReadOnly,
server,
Message_AllowDeleting,
Message_AllowDeleting_BlockDeleteInMinutes,
Message_AllowEditing,
Message_AllowEditing_BlockEditInMinutes,
Message_AllowPinning,
Message_AllowStarring,
Message_Read_Receipt_Store_Users,
isMasterDetail,
editMessagePermission,
deleteMessagePermission,
forceDeleteMessagePermission,
pinMessagePermission
}, ref) => {
let permissions = {};
const { showActionSheet, hideActionSheet } = useActionSheet();
const getPermissions = async() => {
try {
const permission = [editMessagePermission, deleteMessagePermission, forceDeleteMessagePermission, pinMessagePermission];
const result = await RocketChat.hasPermission(permission, room.rid);
permissions = {
hasEditPermission: result[0],
hasDeletePermission: result[1],
hasForceDeletePermission: result[2],
hasPinPermission: result[3]
};
} catch {
// Do nothing
}
};
const isOwn = message => message.u && message.u._id === user.id;
const allowEdit = (message) => {
if (isReadOnly) {
return false;
}
const editOwn = isOwn(message);
if (!(permissions.hasEditPermission || (Message_AllowEditing && editOwn))) {
return false;
}
const blockEditInMinutes = Message_AllowEditing_BlockEditInMinutes;
if (blockEditInMinutes) {
let msgTs;
if (message.ts != null) {
msgTs = moment(message.ts);
}
let currentTsDiff;
if (msgTs != null) {
currentTsDiff = moment().diff(msgTs, 'minutes');
}
return currentTsDiff < blockEditInMinutes;
}
return true;
};
const allowDelete = (message) => {
if (isReadOnly) {
return false;
}
// Prevent from deleting thread start message when positioned inside the thread
if (tmid === message.id) {
return false;
}
const deleteOwn = isOwn(message);
if (!(permissions.hasDeletePermission || (Message_AllowDeleting && deleteOwn) || permissions.hasForceDeletePermission)) {
return false;
}
if (permissions.hasForceDeletePermission) {
return true;
}
const blockDeleteInMinutes = Message_AllowDeleting_BlockDeleteInMinutes;
if (blockDeleteInMinutes != null && blockDeleteInMinutes !== 0) {
let msgTs;
if (message.ts != null) {
msgTs = moment(message.ts);
}
let currentTsDiff;
if (msgTs != null) {
currentTsDiff = moment().diff(msgTs, 'minutes');
}
return currentTsDiff < blockDeleteInMinutes;
}
return true;
};
const getPermalink = message => RocketChat.getPermalinkMessage(message);
const handleReply = (message) => {
logEvent(events.ROOM_MSG_ACTION_REPLY);
replyInit(message, true);
};
const handleEdit = (message) => {
logEvent(events.ROOM_MSG_ACTION_EDIT);
editInit(message);
};
const handleCreateDiscussion = (message) => {
logEvent(events.ROOM_MSG_ACTION_DISCUSSION);
const params = { message, channel: room, showCloseModal: true };
if (isMasterDetail) {
Navigation.navigate('ModalStackNavigator', { screen: 'CreateDiscussionView', params });
} else {
Navigation.navigate('NewMessageStackNavigator', { screen: 'CreateDiscussionView', params });
}
};
const handleUnread = async(message) => {
logEvent(events.ROOM_MSG_ACTION_UNREAD);
const { id: messageId, ts } = message;
const { rid } = room;
try {
const db = database.active;
const result = await RocketChat.markAsUnread({ messageId });
if (result.success) {
const subCollection = db.get('subscriptions');
const subRecord = await subCollection.find(rid);
await db.action(async() => {
try {
await subRecord.update(sub => sub.lastOpen = ts);
} catch {
// do nothing
}
});
Navigation.navigate('RoomsListView');
}
} catch (e) {
logEvent(events.ROOM_MSG_ACTION_UNREAD_F);
log(e);
}
};
const handlePermalink = async(message) => {
logEvent(events.ROOM_MSG_ACTION_PERMALINK);
try {
const permalink = await getPermalink(message);
Clipboard.setString(permalink);
EventEmitter.emit(LISTENER, { message: I18n.t('Permalink_copied_to_clipboard') });
} catch {
logEvent(events.ROOM_MSG_ACTION_PERMALINK_F);
}
};
const handleCopy = async(message) => {
logEvent(events.ROOM_MSG_ACTION_COPY);
await Clipboard.setString(message?.attachments?.[0]?.description || message.msg);
EventEmitter.emit(LISTENER, { message: I18n.t('Copied_to_clipboard') });
};
const handleShare = async(message) => {
logEvent(events.ROOM_MSG_ACTION_SHARE);
try {
const permalink = await getPermalink(message);
Share.share({ message: permalink });
} catch {
logEvent(events.ROOM_MSG_ACTION_SHARE_F);
}
};
const handleQuote = (message) => {
logEvent(events.ROOM_MSG_ACTION_QUOTE);
replyInit(message, false);
};
const handleStar = async(message) => {
logEvent(message.starred ? events.ROOM_MSG_ACTION_UNSTAR : events.ROOM_MSG_ACTION_STAR);
try {
await RocketChat.toggleStarMessage(message.id, message.starred);
EventEmitter.emit(LISTENER, { message: message.starred ? I18n.t('Message_unstarred') : I18n.t('Message_starred') });
} catch (e) {
logEvent(events.ROOM_MSG_ACTION_STAR_F);
log(e);
}
};
const handlePin = async(message) => {
logEvent(events.ROOM_MSG_ACTION_PIN);
try {
await RocketChat.togglePinMessage(message.id, message.pinned);
} catch (e) {
logEvent(events.ROOM_MSG_ACTION_PIN_F);
log(e);
}
};
const handleReaction = (shortname, message) => {
logEvent(events.ROOM_MSG_ACTION_REACTION);
if (shortname) {
onReactionPress(shortname, message.id);
} else {
reactionInit(message);
}
// close actionSheet when click at header
hideActionSheet();
};
const handleReadReceipt = (message) => {
if (isMasterDetail) {
Navigation.navigate('ModalStackNavigator', { screen: 'ReadReceiptsView', params: { messageId: message.id } });
} else {
Navigation.navigate('ReadReceiptsView', { messageId: message.id });
}
};
const handleToggleTranslation = async(message) => {
try {
const db = database.active;
await db.action(async() => {
await message.update((m) => {
m.autoTranslate = !m.autoTranslate;
m._updatedAt = new Date();
});
});
const translatedMessage = getMessageTranslation(message, room.autoTranslateLanguage);
if (!translatedMessage) {
const m = {
_id: message.id,
rid: message.subscription.id,
u: message.u,
msg: message.msg
};
await RocketChat.translateMessage(m, room.autoTranslateLanguage);
}
} catch (e) {
log(e);
}
};
const handleReport = async(message) => {
logEvent(events.ROOM_MSG_ACTION_REPORT);
try {
await RocketChat.reportMessage(message.id);
Alert.alert(I18n.t('Message_Reported'));
} catch (e) {
logEvent(events.ROOM_MSG_ACTION_REPORT_F);
log(e);
}
};
const handleDelete = (message) => {
showConfirmationAlert({
message: I18n.t('You_will_not_be_able_to_recover_this_message'),
confirmationText: I18n.t('Delete'),
onPress: async() => {
try {
logEvent(events.ROOM_MSG_ACTION_DELETE);
await RocketChat.deleteMessage(message.id, message.subscription.id);
} catch (e) {
logEvent(events.ROOM_MSG_ACTION_DELETE_F);
log(e);
}
}
});
};
const getOptions = (message) => {
let options = [];
// Reply
if (!isReadOnly) {
options = [{
title: I18n.t('Reply_in_Thread'),
icon: 'threads',
onPress: () => handleReply(message)
}];
}
// Quote
if (!isReadOnly) {
options.push({
title: I18n.t('Quote'),
icon: 'quote',
onPress: () => handleQuote(message)
});
}
// Edit
if (allowEdit(message)) {
options.push({
title: I18n.t('Edit'),
icon: 'edit',
onPress: () => handleEdit(message)
});
}
// Permalink
options.push({
title: I18n.t('Permalink'),
icon: 'link',
onPress: () => handlePermalink(message)
});
// Create Discussion
options.push({
title: I18n.t('Start_a_Discussion'),
icon: 'discussions',
onPress: () => handleCreateDiscussion(message)
});
// Mark as unread
if (message.u && message.u._id !== user.id) {
options.push({
title: I18n.t('Mark_unread'),
icon: 'flag',
onPress: () => handleUnread(message)
});
}
// Copy
options.push({
title: I18n.t('Copy'),
icon: 'copy',
onPress: () => handleCopy(message)
});
// Share
options.push({
title: I18n.t('Share'),
icon: 'share',
onPress: () => handleShare(message)
});
// Star
if (Message_AllowStarring) {
options.push({
title: I18n.t(message.starred ? 'Unstar' : 'Star'),
icon: message.starred ? 'star-filled' : 'star',
onPress: () => handleStar(message)
});
}
// Pin
if (Message_AllowPinning && permissions?.hasPinPermission) {
options.push({
title: I18n.t(message.pinned ? 'Unpin' : 'Pin'),
icon: 'pin',
onPress: () => handlePin(message)
});
}
// Read Receipts
if (Message_Read_Receipt_Store_Users) {
options.push({
title: I18n.t('Read_Receipt'),
icon: 'info',
onPress: () => handleReadReceipt(message)
});
}
// Toggle Auto-translate
if (room.autoTranslate && message.u && message.u._id !== user.id) {
options.push({
title: I18n.t(message.autoTranslate ? 'View_Original' : 'Translate'),
icon: 'language',
onPress: () => handleToggleTranslation(message)
});
}
// Report
options.push({
title: I18n.t('Report'),
icon: 'warning',
danger: true,
onPress: () => handleReport(message)
});
// Delete
if (allowDelete(message)) {
options.push({
title: I18n.t('Delete'),
icon: 'delete',
danger: true,
onPress: () => handleDelete(message)
});
}
return options;
};
const showMessageActions = async(message) => {
logEvent(events.ROOM_SHOW_MSG_ACTIONS);
await getPermissions();
showActionSheet({
options: getOptions(message),
headerHeight: HEADER_HEIGHT,
customHeader: (!isReadOnly || room.reactWhenReadOnly ? (
<Header
server={server}
handleReaction={handleReaction}
isMasterDetail={isMasterDetail}
message={message}
/>
) : null)
});
};
useImperativeHandle(ref, () => ({ showMessageActions }));
return null;
}));
MessageActions.propTypes = {
room: PropTypes.object,
tmid: PropTypes.string,
user: PropTypes.object,
editInit: PropTypes.func,
reactionInit: PropTypes.func,
onReactionPress: PropTypes.func,
replyInit: PropTypes.func,
isReadOnly: PropTypes.bool,
Message_AllowDeleting: PropTypes.bool,
Message_AllowDeleting_BlockDeleteInMinutes: PropTypes.number,
Message_AllowEditing: PropTypes.bool,
Message_AllowEditing_BlockEditInMinutes: PropTypes.number,
Message_AllowPinning: PropTypes.bool,
Message_AllowStarring: PropTypes.bool,
Message_Read_Receipt_Store_Users: PropTypes.bool,
server: PropTypes.string,
editMessagePermission: PropTypes.array,
deleteMessagePermission: PropTypes.array,
forceDeleteMessagePermission: PropTypes.array,
pinMessagePermission: PropTypes.array
};
const mapStateToProps = state => ({
server: state.server.server,
Message_AllowDeleting: state.settings.Message_AllowDeleting,
Message_AllowDeleting_BlockDeleteInMinutes: state.settings.Message_AllowDeleting_BlockDeleteInMinutes,
Message_AllowEditing: state.settings.Message_AllowEditing,
Message_AllowEditing_BlockEditInMinutes: state.settings.Message_AllowEditing_BlockEditInMinutes,
Message_AllowPinning: state.settings.Message_AllowPinning,
Message_AllowStarring: state.settings.Message_AllowStarring,
Message_Read_Receipt_Store_Users: state.settings.Message_Read_Receipt_Store_Users,
isMasterDetail: state.app.isMasterDetail,
editMessagePermission: state.permissions['edit-message'],
deleteMessagePermission: state.permissions['delete-message'],
forceDeleteMessagePermission: state.permissions['force-delete-message'],
pinMessagePermission: state.permissions['pin-message']
});
export default connect(mapStateToProps, null, null, { forwardRef: true })(MessageActions);

View File

@ -0,0 +1,487 @@
import React, { forwardRef, useImperativeHandle } from 'react';
import { Alert, Clipboard, Share } from 'react-native';
import { connect } from 'react-redux';
import moment from 'moment';
import RocketChat from '../../lib/rocketchat';
import database from '../../lib/database';
import I18n from '../../i18n';
import log, { logEvent } from '../../utils/log';
import Navigation from '../../lib/Navigation';
import { getMessageTranslation } from '../message/utils';
import { LISTENER } from '../Toast';
import EventEmitter from '../../utils/events';
import { showConfirmationAlert } from '../../utils/info';
import { useActionSheet } from '../ActionSheet';
import Header, { HEADER_HEIGHT } from './Header';
import events from '../../utils/log/events';
interface IMessageActions {
room: {
rid: string | number;
autoTranslateLanguage: any;
autoTranslate: any;
reactWhenReadOnly: any;
};
tmid: string;
user: {
id: string | number;
};
editInit: Function;
reactionInit: Function;
onReactionPress: Function;
replyInit: Function;
isMasterDetail: boolean;
isReadOnly: boolean;
Message_AllowDeleting: boolean;
Message_AllowDeleting_BlockDeleteInMinutes: number;
Message_AllowEditing: boolean;
Message_AllowEditing_BlockEditInMinutes: number;
Message_AllowPinning: boolean;
Message_AllowStarring: boolean;
Message_Read_Receipt_Store_Users: boolean;
server: string;
editMessagePermission: [];
deleteMessagePermission: [];
forceDeleteMessagePermission: [];
pinMessagePermission: [];
}
const MessageActions = React.memo(
forwardRef(
(
{
room,
tmid,
user,
editInit,
reactionInit,
onReactionPress,
replyInit,
isReadOnly,
server,
Message_AllowDeleting,
Message_AllowDeleting_BlockDeleteInMinutes,
Message_AllowEditing,
Message_AllowEditing_BlockEditInMinutes,
Message_AllowPinning,
Message_AllowStarring,
Message_Read_Receipt_Store_Users,
isMasterDetail,
editMessagePermission,
deleteMessagePermission,
forceDeleteMessagePermission,
pinMessagePermission
}: IMessageActions,
ref
): any => {
let permissions: any = {};
const { showActionSheet, hideActionSheet }: any = useActionSheet();
const getPermissions = async () => {
try {
const permission = [editMessagePermission, deleteMessagePermission, forceDeleteMessagePermission, pinMessagePermission];
const result = await RocketChat.hasPermission(permission, room.rid);
permissions = {
hasEditPermission: result[0],
hasDeletePermission: result[1],
hasForceDeletePermission: result[2],
hasPinPermission: result[3]
};
} catch {
// Do nothing
}
};
const isOwn = (message: any) => message.u && message.u._id === user.id;
const allowEdit = (message: any) => {
if (isReadOnly) {
return false;
}
const editOwn = isOwn(message);
if (!(permissions.hasEditPermission || (Message_AllowEditing && editOwn))) {
return false;
}
const blockEditInMinutes = Message_AllowEditing_BlockEditInMinutes;
if (blockEditInMinutes) {
let msgTs;
if (message.ts != null) {
msgTs = moment(message.ts);
}
let currentTsDiff: any;
if (msgTs != null) {
currentTsDiff = moment().diff(msgTs, 'minutes');
}
return currentTsDiff < blockEditInMinutes;
}
return true;
};
const allowDelete = (message: any) => {
if (isReadOnly) {
return false;
}
// Prevent from deleting thread start message when positioned inside the thread
if (tmid === message.id) {
return false;
}
const deleteOwn = isOwn(message);
if (!(permissions.hasDeletePermission || (Message_AllowDeleting && deleteOwn) || permissions.hasForceDeletePermission)) {
return false;
}
if (permissions.hasForceDeletePermission) {
return true;
}
const blockDeleteInMinutes = Message_AllowDeleting_BlockDeleteInMinutes;
if (blockDeleteInMinutes != null && blockDeleteInMinutes !== 0) {
let msgTs;
if (message.ts != null) {
msgTs = moment(message.ts);
}
let currentTsDiff: any;
if (msgTs != null) {
currentTsDiff = moment().diff(msgTs, 'minutes');
}
return currentTsDiff < blockDeleteInMinutes;
}
return true;
};
const getPermalink = (message: any) => RocketChat.getPermalinkMessage(message);
const handleReply = (message: any) => {
logEvent(events.ROOM_MSG_ACTION_REPLY);
replyInit(message, true);
};
const handleEdit = (message: any) => {
logEvent(events.ROOM_MSG_ACTION_EDIT);
editInit(message);
};
const handleCreateDiscussion = (message: any) => {
logEvent(events.ROOM_MSG_ACTION_DISCUSSION);
const params = { message, channel: room, showCloseModal: true };
if (isMasterDetail) {
Navigation.navigate('ModalStackNavigator', { screen: 'CreateDiscussionView', params });
} else {
Navigation.navigate('NewMessageStackNavigator', { screen: 'CreateDiscussionView', params });
}
};
const handleUnread = async (message: any) => {
logEvent(events.ROOM_MSG_ACTION_UNREAD);
const { id: messageId, ts } = message;
const { rid } = room;
try {
const db = database.active;
const result = await RocketChat.markAsUnread({ messageId });
if (result.success) {
const subCollection = db.get('subscriptions');
const subRecord = await subCollection.find(rid);
await db.action(async () => {
try {
await subRecord.update((sub: any) => (sub.lastOpen = ts));
} catch {
// do nothing
}
});
Navigation.navigate('RoomsListView');
}
} catch (e) {
logEvent(events.ROOM_MSG_ACTION_UNREAD_F);
log(e);
}
};
const handlePermalink = async (message: any) => {
logEvent(events.ROOM_MSG_ACTION_PERMALINK);
try {
const permalink: any = await getPermalink(message);
Clipboard.setString(permalink);
EventEmitter.emit(LISTENER, { message: I18n.t('Permalink_copied_to_clipboard') });
} catch {
logEvent(events.ROOM_MSG_ACTION_PERMALINK_F);
}
};
const handleCopy = async (message: any) => {
logEvent(events.ROOM_MSG_ACTION_COPY);
await Clipboard.setString(message?.attachments?.[0]?.description || message.msg);
EventEmitter.emit(LISTENER, { message: I18n.t('Copied_to_clipboard') });
};
const handleShare = async (message: any) => {
logEvent(events.ROOM_MSG_ACTION_SHARE);
try {
const permalink: any = await getPermalink(message);
Share.share({ message: permalink });
} catch {
logEvent(events.ROOM_MSG_ACTION_SHARE_F);
}
};
const handleQuote = (message: any) => {
logEvent(events.ROOM_MSG_ACTION_QUOTE);
replyInit(message, false);
};
const handleStar = async (message: any) => {
logEvent(message.starred ? events.ROOM_MSG_ACTION_UNSTAR : events.ROOM_MSG_ACTION_STAR);
try {
await RocketChat.toggleStarMessage(message.id, message.starred);
EventEmitter.emit(LISTENER, { message: message.starred ? I18n.t('Message_unstarred') : I18n.t('Message_starred') });
} catch (e) {
logEvent(events.ROOM_MSG_ACTION_STAR_F);
log(e);
}
};
const handlePin = async (message: any) => {
logEvent(events.ROOM_MSG_ACTION_PIN);
try {
await RocketChat.togglePinMessage(message.id, message.pinned);
} catch (e) {
logEvent(events.ROOM_MSG_ACTION_PIN_F);
log(e);
}
};
const handleReaction = (shortname: any, message: any) => {
logEvent(events.ROOM_MSG_ACTION_REACTION);
if (shortname) {
onReactionPress(shortname, message.id);
} else {
reactionInit(message);
}
// close actionSheet when click at header
hideActionSheet();
};
const handleReadReceipt = (message: any) => {
if (isMasterDetail) {
Navigation.navigate('ModalStackNavigator', { screen: 'ReadReceiptsView', params: { messageId: message.id } });
} else {
Navigation.navigate('ReadReceiptsView', { messageId: message.id });
}
};
const handleToggleTranslation = async (message: any) => {
try {
const db = database.active;
await db.action(async () => {
await message.update((m: any) => {
m.autoTranslate = !m.autoTranslate;
m._updatedAt = new Date();
});
});
const translatedMessage = getMessageTranslation(message, room.autoTranslateLanguage);
if (!translatedMessage) {
const m = {
_id: message.id,
rid: message.subscription.id,
u: message.u,
msg: message.msg
};
await RocketChat.translateMessage(m, room.autoTranslateLanguage);
}
} catch (e) {
log(e);
}
};
const handleReport = async (message: any) => {
logEvent(events.ROOM_MSG_ACTION_REPORT);
try {
await RocketChat.reportMessage(message.id);
Alert.alert(I18n.t('Message_Reported'));
} catch (e) {
logEvent(events.ROOM_MSG_ACTION_REPORT_F);
log(e);
}
};
const handleDelete = (message: any) => {
// TODO - migrate this function for ts when fix the lint erros
// @ts-ignore
showConfirmationAlert({
message: I18n.t('You_will_not_be_able_to_recover_this_message'),
confirmationText: I18n.t('Delete'),
onPress: async () => {
try {
logEvent(events.ROOM_MSG_ACTION_DELETE);
await RocketChat.deleteMessage(message.id, message.subscription.id);
} catch (e) {
logEvent(events.ROOM_MSG_ACTION_DELETE_F);
log(e);
}
}
});
};
const getOptions = (message: any) => {
let options: any = [];
// Reply
if (!isReadOnly) {
options = [
{
title: I18n.t('Reply_in_Thread'),
icon: 'threads',
onPress: () => handleReply(message)
}
];
}
// Quote
if (!isReadOnly) {
options.push({
title: I18n.t('Quote'),
icon: 'quote',
onPress: () => handleQuote(message)
});
}
// Edit
if (allowEdit(message)) {
options.push({
title: I18n.t('Edit'),
icon: 'edit',
onPress: () => handleEdit(message)
});
}
// Permalink
options.push({
title: I18n.t('Permalink'),
icon: 'link',
onPress: () => handlePermalink(message)
});
// Create Discussion
options.push({
title: I18n.t('Start_a_Discussion'),
icon: 'discussions',
onPress: () => handleCreateDiscussion(message)
});
// Mark as unread
if (message.u && message.u._id !== user.id) {
options.push({
title: I18n.t('Mark_unread'),
icon: 'flag',
onPress: () => handleUnread(message)
});
}
// Copy
options.push({
title: I18n.t('Copy'),
icon: 'copy',
onPress: () => handleCopy(message)
});
// Share
options.push({
title: I18n.t('Share'),
icon: 'share',
onPress: () => handleShare(message)
});
// Star
if (Message_AllowStarring) {
options.push({
title: I18n.t(message.starred ? 'Unstar' : 'Star'),
icon: message.starred ? 'star-filled' : 'star',
onPress: () => handleStar(message)
});
}
// Pin
if (Message_AllowPinning && permissions?.hasPinPermission) {
options.push({
title: I18n.t(message.pinned ? 'Unpin' : 'Pin'),
icon: 'pin',
onPress: () => handlePin(message)
});
}
// Read Receipts
if (Message_Read_Receipt_Store_Users) {
options.push({
title: I18n.t('Read_Receipt'),
icon: 'info',
onPress: () => handleReadReceipt(message)
});
}
// Toggle Auto-translate
if (room.autoTranslate && message.u && message.u._id !== user.id) {
options.push({
title: I18n.t(message.autoTranslate ? 'View_Original' : 'Translate'),
icon: 'language',
onPress: () => handleToggleTranslation(message)
});
}
// Report
options.push({
title: I18n.t('Report'),
icon: 'warning',
danger: true,
onPress: () => handleReport(message)
});
// Delete
if (allowDelete(message)) {
options.push({
title: I18n.t('Delete'),
icon: 'delete',
danger: true,
onPress: () => handleDelete(message)
});
}
return options;
};
const showMessageActions = async (message: any) => {
logEvent(events.ROOM_SHOW_MSG_ACTIONS);
await getPermissions();
showActionSheet({
options: getOptions(message),
headerHeight: HEADER_HEIGHT,
customHeader:
!isReadOnly || room.reactWhenReadOnly ? (
<Header server={server} handleReaction={handleReaction} isMasterDetail={isMasterDetail} message={message} />
) : null
});
};
useImperativeHandle(ref, () => ({ showMessageActions }));
return null;
}
)
);
const mapStateToProps = (state: any) => ({
server: state.server.server,
Message_AllowDeleting: state.settings.Message_AllowDeleting,
Message_AllowDeleting_BlockDeleteInMinutes: state.settings.Message_AllowDeleting_BlockDeleteInMinutes,
Message_AllowEditing: state.settings.Message_AllowEditing,
Message_AllowEditing_BlockEditInMinutes: state.settings.Message_AllowEditing_BlockEditInMinutes,
Message_AllowPinning: state.settings.Message_AllowPinning,
Message_AllowStarring: state.settings.Message_AllowStarring,
Message_Read_Receipt_Store_Users: state.settings.Message_Read_Receipt_Store_Users,
isMasterDetail: state.app.isMasterDetail,
editMessagePermission: state.permissions['edit-message'],
deleteMessagePermission: state.permissions['delete-message'],
forceDeleteMessagePermission: state.permissions['force-delete-message'],
pinMessagePermission: state.permissions['pin-message']
});
export default connect(mapStateToProps, null, null, { forwardRef: true })(MessageActions);

View File

@ -1,5 +1,4 @@
import React, { useContext, useState } from 'react'; import React, { useContext, useState } from 'react';
import PropTypes from 'prop-types';
import { TouchableOpacity } from 'react-native'; import { TouchableOpacity } from 'react-native';
import FastImage from '@rocket.chat/react-native-fast-image'; import FastImage from '@rocket.chat/react-native-fast-image';
@ -9,7 +8,16 @@ import { themes } from '../../../constants/colors';
import MessageboxContext from '../Context'; import MessageboxContext from '../Context';
import ActivityIndicator from '../../ActivityIndicator'; import ActivityIndicator from '../../ActivityIndicator';
const Item = ({ item, theme }) => { interface IMessageBoxCommandsPreviewItem {
item: {
type: string;
id: string;
value: string;
};
theme: string;
}
const Item = ({ item, theme }: IMessageBoxCommandsPreviewItem) => {
const context = useContext(MessageboxContext); const context = useContext(MessageboxContext);
const { onPressCommandPreview } = context; const { onPressCommandPreview } = context;
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
@ -18,29 +26,21 @@ const Item = ({ item, theme }) => {
<TouchableOpacity <TouchableOpacity
style={styles.commandPreview} style={styles.commandPreview}
onPress={() => onPressCommandPreview(item)} onPress={() => onPressCommandPreview(item)}
testID={`command-preview-item${ item.id }`} testID={`command-preview-item${item.id}`}>
> {item.type === 'image' ? (
{item.type === 'image' <FastImage
? ( style={styles.commandPreviewImage}
<FastImage source={{ uri: item.value }}
style={styles.commandPreviewImage} resizeMode={FastImage.resizeMode.cover}
source={{ uri: item.value }} onLoadStart={() => setLoading(true)}
resizeMode={FastImage.resizeMode.cover} onLoad={() => setLoading(false)}>
onLoadStart={() => setLoading(true)} {loading ? <ActivityIndicator theme={theme} /> : null}
onLoad={() => setLoading(false)} </FastImage>
> ) : (
{ loading ? <ActivityIndicator theme={theme} /> : null } <CustomIcon name='attach' size={36} color={themes[theme].actionTintColor} />
</FastImage> )}
)
: <CustomIcon name='attach' size={36} color={themes[theme].actionTintColor} />
}
</TouchableOpacity> </TouchableOpacity>
); );
}; };
Item.propTypes = {
item: PropTypes.object,
theme: PropTypes.string
};
export default Item; export default Item;

View File

@ -1,46 +0,0 @@
import React from 'react';
import { FlatList } from 'react-native';
import PropTypes from 'prop-types';
import { dequal } from 'dequal';
import Item from './Item';
import styles from '../styles';
import { themes } from '../../../constants/colors';
import { withTheme } from '../../../theme';
const CommandsPreview = React.memo(({ theme, commandPreview, showCommandPreview }) => {
if (!showCommandPreview) {
return null;
}
return (
<FlatList
testID='commandbox-container'
style={[styles.mentionList, { backgroundColor: themes[theme].messageboxBackground }]}
data={commandPreview}
renderItem={({ item }) => <Item item={item} theme={theme} />}
keyExtractor={item => item.id}
keyboardShouldPersistTaps='always'
horizontal
showsHorizontalScrollIndicator={false}
/>
);
}, (prevProps, nextProps) => {
if (prevProps.theme !== nextProps.theme) {
return false;
}
if (prevProps.showCommandPreview !== nextProps.showCommandPreview) {
return false;
}
if (!dequal(prevProps.commandPreview, nextProps.commandPreview)) {
return false;
}
return true;
});
CommandsPreview.propTypes = {
commandPreview: PropTypes.array,
showCommandPreview: PropTypes.bool,
theme: PropTypes.string
};
export default withTheme(CommandsPreview);

View File

@ -0,0 +1,48 @@
import React from 'react';
import { FlatList } from 'react-native';
import { dequal } from 'dequal';
import Item from './Item';
import styles from '../styles';
import { themes } from '../../../constants/colors';
import { withTheme } from '../../../theme';
interface IMessageBoxCommandsPreview {
commandPreview: [];
showCommandPreview: boolean;
theme: string;
}
const CommandsPreview = React.memo(
({ theme, commandPreview, showCommandPreview }: IMessageBoxCommandsPreview) => {
if (!showCommandPreview) {
return null;
}
return (
<FlatList
testID='commandbox-container'
style={[styles.mentionList, { backgroundColor: themes[theme].messageboxBackground }]}
data={commandPreview}
renderItem={({ item }) => <Item item={item} theme={theme} />}
keyExtractor={(item: any) => item.id}
keyboardShouldPersistTaps='always'
horizontal
showsHorizontalScrollIndicator={false}
/>
);
},
(prevProps, nextProps) => {
if (prevProps.theme !== nextProps.theme) {
return false;
}
if (prevProps.showCommandPreview !== nextProps.showCommandPreview) {
return false;
}
if (!dequal(prevProps.commandPreview, nextProps.commandPreview)) {
return false;
}
return true;
}
);
export default withTheme(CommandsPreview);

View File

@ -1,4 +0,0 @@
import React from 'react';
const MessageboxContext = React.createContext();
export default MessageboxContext;

View File

@ -0,0 +1,5 @@
import React from 'react';
// @ts-ignore
const MessageboxContext = React.createContext<any>();
export default MessageboxContext;

View File

@ -1,7 +1,6 @@
import React from 'react'; import React from 'react';
import { View } from 'react-native'; import { View } from 'react-native';
import { KeyboardRegistry } from 'react-native-ui-lib/keyboard'; import { KeyboardRegistry } from 'react-native-ui-lib/keyboard';
import PropTypes from 'prop-types';
import store from '../../lib/createStore'; import store from '../../lib/createStore';
import EmojiPicker from '../EmojiPicker'; import EmojiPicker from '../EmojiPicker';
@ -9,25 +8,29 @@ import styles from './styles';
import { themes } from '../../constants/colors'; import { themes } from '../../constants/colors';
import { withTheme } from '../../theme'; import { withTheme } from '../../theme';
export default class EmojiKeyboard extends React.PureComponent { interface IMessageBoxEmojiKeyboard {
static propTypes = { theme: string;
theme: PropTypes.string }
};
constructor(props) { export default class EmojiKeyboard extends React.PureComponent<IMessageBoxEmojiKeyboard, any> {
private readonly baseUrl: any;
constructor(props: IMessageBoxEmojiKeyboard) {
super(props); super(props);
const state = store.getState(); const state = store.getState();
this.baseUrl = state.share.server.server || state.server.server; this.baseUrl = state.share.server.server || state.server.server;
} }
onEmojiSelected = (emoji) => { onEmojiSelected = (emoji: any) => {
KeyboardRegistry.onItemSelected('EmojiKeyboard', { emoji }); KeyboardRegistry.onItemSelected('EmojiKeyboard', { emoji });
} };
render() { render() {
const { theme } = this.props; const { theme } = this.props;
return ( return (
<View style={[styles.emojiKeyboardContainer, { borderTopColor: themes[theme].borderColor }]} testID='messagebox-keyboard-emoji'> <View
style={[styles.emojiKeyboardContainer, { borderTopColor: themes[theme].borderColor }]}
testID='messagebox-keyboard-emoji'>
<EmojiPicker onEmojiSelected={this.onEmojiSelected} baseUrl={this.baseUrl} /> <EmojiPicker onEmojiSelected={this.onEmojiSelected} baseUrl={this.baseUrl} />
</View> </View>
); );

View File

@ -1,31 +0,0 @@
import React from 'react';
import PropTypes from 'prop-types';
import { CancelEditingButton, ToggleEmojiButton } from './buttons';
const LeftButtons = React.memo(({
theme, showEmojiKeyboard, editing, editCancel, openEmoji, closeEmoji
}) => {
if (editing) {
return <CancelEditingButton onPress={editCancel} theme={theme} />;
}
return (
<ToggleEmojiButton
show={showEmojiKeyboard}
open={openEmoji}
close={closeEmoji}
theme={theme}
/>
);
});
LeftButtons.propTypes = {
theme: PropTypes.string,
showEmojiKeyboard: PropTypes.bool,
openEmoji: PropTypes.func.isRequired,
closeEmoji: PropTypes.func.isRequired,
editing: PropTypes.bool,
editCancel: PropTypes.func.isRequired
};
export default LeftButtons;

View File

@ -0,0 +1,23 @@
import React from 'react';
import { CancelEditingButton, ToggleEmojiButton } from './buttons';
interface IMessageBoxLeftButtons {
theme: string;
showEmojiKeyboard: boolean;
openEmoji(): void;
closeEmoji(): void;
editing: boolean;
editCancel(): void;
}
const LeftButtons = React.memo(
({ theme, showEmojiKeyboard, editing, editCancel, openEmoji, closeEmoji }: IMessageBoxLeftButtons) => {
if (editing) {
return <CancelEditingButton onPress={editCancel} theme={theme} />;
}
return <ToggleEmojiButton show={showEmojiKeyboard} open={openEmoji} close={closeEmoji} theme={theme} />;
}
);
export default LeftButtons;

View File

@ -1,28 +0,0 @@
import React from 'react';
import PropTypes from 'prop-types';
import { View } from 'react-native';
import { CancelEditingButton, ActionsButton } from './buttons';
import styles from './styles';
const LeftButtons = React.memo(({
theme, showMessageBoxActions, editing, editCancel, isActionsEnabled
}) => {
if (editing) {
return <CancelEditingButton onPress={editCancel} theme={theme} />;
}
if (isActionsEnabled) {
return <ActionsButton onPress={showMessageBoxActions} theme={theme} />;
}
return <View style={styles.buttonsWhitespace} />;
});
LeftButtons.propTypes = {
theme: PropTypes.string,
showMessageBoxActions: PropTypes.func.isRequired,
editing: PropTypes.bool,
editCancel: PropTypes.func.isRequired,
isActionsEnabled: PropTypes.bool
};
export default LeftButtons;

View File

@ -0,0 +1,27 @@
import React from 'react';
import { View } from 'react-native';
import { ActionsButton, CancelEditingButton } from './buttons';
import styles from './styles';
interface IMessageBoxLeftButtons {
theme: string;
showMessageBoxActions(): void;
editing: boolean;
editCancel(): void;
isActionsEnabled: boolean;
}
const LeftButtons = React.memo(
({ theme, showMessageBoxActions, editing, editCancel, isActionsEnabled }: IMessageBoxLeftButtons) => {
if (editing) {
return <CancelEditingButton onPress={editCancel} theme={theme} />;
}
if (isActionsEnabled) {
return <ActionsButton onPress={showMessageBoxActions} theme={theme} />;
}
return <View style={styles.buttonsWhitespace} />;
}
);
export default LeftButtons;

View File

@ -1,12 +1,19 @@
import React from 'react'; import React from 'react';
import { TouchableOpacity, Text } from 'react-native'; import { Text, TouchableOpacity } from 'react-native';
import PropTypes from 'prop-types';
import styles from '../styles'; import styles from '../styles';
import I18n from '../../../i18n'; import I18n from '../../../i18n';
import { themes } from '../../../constants/colors'; import { themes } from '../../../constants/colors';
const FixedMentionItem = ({ item, onPress, theme }) => ( interface IMessageBoxFixedMentionItem {
item: {
username: string;
};
onPress: Function;
theme: string;
}
const FixedMentionItem = ({ item, onPress, theme }: IMessageBoxFixedMentionItem) => (
<TouchableOpacity <TouchableOpacity
style={[ style={[
styles.mentionItem, styles.mentionItem,
@ -15,8 +22,7 @@ const FixedMentionItem = ({ item, onPress, theme }) => (
borderTopColor: themes[theme].separatorColor borderTopColor: themes[theme].separatorColor
} }
]} ]}
onPress={() => onPress(item)} onPress={() => onPress(item)}>
>
<Text style={[styles.fixedMentionAvatar, { color: themes[theme].titleText }]}>{item.username}</Text> <Text style={[styles.fixedMentionAvatar, { color: themes[theme].titleText }]}>{item.username}</Text>
<Text style={[styles.mentionText, { color: themes[theme].titleText }]}> <Text style={[styles.mentionText, { color: themes[theme].titleText }]}>
{item.username === 'here' ? I18n.t('Notify_active_in_this_room') : I18n.t('Notify_all_in_this_room')} {item.username === 'here' ? I18n.t('Notify_active_in_this_room') : I18n.t('Notify_all_in_this_room')}
@ -24,10 +30,4 @@ const FixedMentionItem = ({ item, onPress, theme }) => (
</TouchableOpacity> </TouchableOpacity>
); );
FixedMentionItem.propTypes = {
item: PropTypes.object,
onPress: PropTypes.func,
theme: PropTypes.string
};
export default FixedMentionItem; export default FixedMentionItem;

View File

@ -6,25 +6,20 @@ import shortnameToUnicode from '../../../utils/shortnameToUnicode';
import styles from '../styles'; import styles from '../styles';
import MessageboxContext from '../Context'; import MessageboxContext from '../Context';
import CustomEmoji from '../../EmojiPicker/CustomEmoji'; import CustomEmoji from '../../EmojiPicker/CustomEmoji';
import { IEmoji } from '../../EmojiPicker/interfaces';
const MentionEmoji = ({ item }) => { interface IMessageBoxMentionEmoji {
item: IEmoji;
}
const MentionEmoji = ({ item }: IMessageBoxMentionEmoji) => {
const context = useContext(MessageboxContext); const context = useContext(MessageboxContext);
const { baseUrl } = context; const { baseUrl } = context;
if (item.name) { if (item.name) {
return ( return <CustomEmoji style={styles.mentionItemCustomEmoji} emoji={item} baseUrl={baseUrl} />;
<CustomEmoji
style={styles.mentionItemCustomEmoji}
emoji={item}
baseUrl={baseUrl}
/>
);
} }
return ( return <Text style={styles.mentionItemEmoji}>{shortnameToUnicode(`:${item}:`)}</Text>;
<Text style={styles.mentionItemEmoji}>
{shortnameToUnicode(`:${ item }:`)}
</Text>
);
}; };
MentionEmoji.propTypes = { MentionEmoji.propTypes = {

View File

@ -0,0 +1,49 @@
import React, { useContext } from 'react';
import { View, Text, ActivityIndicator, TouchableOpacity } from 'react-native';
import PropTypes from 'prop-types';
import { MENTIONS_TRACKING_TYPE_CANNED } from '../constants';
import styles from '../styles';
import sharedStyles from '../../../views/Styles';
import I18n from '../../../i18n';
import { themes } from '../../../constants/colors';
import { CustomIcon } from '../../../lib/Icons';
import MessageboxContext from '../Context';
const MentionHeaderList = ({ trackingType, hasMentions, theme, loading }) => {
const context = useContext(MessageboxContext);
const { onPressNoMatchCanned } = context;
if (trackingType === MENTIONS_TRACKING_TYPE_CANNED) {
if (loading) {
return (
<View style={styles.wrapMentionHeaderListRow}>
<ActivityIndicator style={styles.loadingPaddingHeader} size='small' />
<Text style={[styles.mentionHeaderList, { color: themes[theme].auxiliaryText }]}>{I18n.t('Searching')}</Text>
</View>
);
}
if (!hasMentions) {
return (
<TouchableOpacity style={[styles.wrapMentionHeaderListRow, styles.mentionNoMatchHeader]} onPress={onPressNoMatchCanned}>
<Text style={[styles.mentionHeaderListNoMatchFound, { color: themes[theme].auxiliaryText }]}>
{I18n.t('No_match_found')} <Text style={sharedStyles.textSemibold}>{I18n.t('Check_canned_responses')}</Text>
</Text>
<CustomIcon name='chevron-right' size={24} color={themes[theme].auxiliaryText} />
</TouchableOpacity>
);
}
}
return null;
};
MentionHeaderList.propTypes = {
trackingType: PropTypes.string,
hasMentions: PropTypes.bool,
theme: PropTypes.string,
loading: PropTypes.bool
};
export default MentionHeaderList;

View File

@ -1,32 +1,43 @@
import React, { useContext } from 'react'; import React, { useContext } from 'react';
import { TouchableOpacity, Text } from 'react-native'; import { Text, TouchableOpacity } from 'react-native';
import PropTypes from 'prop-types';
import styles from '../styles'; import styles from '../styles';
import Avatar from '../../Avatar'; import Avatar from '../../Avatar';
import MessageboxContext from '../Context'; import MessageboxContext from '../Context';
import FixedMentionItem from './FixedMentionItem'; import FixedMentionItem from './FixedMentionItem';
import MentionEmoji from './MentionEmoji'; import MentionEmoji from './MentionEmoji';
import { import { MENTIONS_TRACKING_TYPE_EMOJIS, MENTIONS_TRACKING_TYPE_COMMANDS, MENTIONS_TRACKING_TYPE_CANNED } from '../constants';
MENTIONS_TRACKING_TYPE_EMOJIS,
MENTIONS_TRACKING_TYPE_COMMANDS
} from '../constants';
import { themes } from '../../../constants/colors'; import { themes } from '../../../constants/colors';
import { IEmoji } from '../../EmojiPicker/interfaces';
const MentionItem = ({ interface IMessageBoxMentionItem {
item, trackingType, theme item: {
}) => { name: string;
command: string;
username: string;
t: string;
id: string;
shortcut: string;
text: string;
} & IEmoji;
trackingType: string;
theme: string;
}
const MentionItem = ({ item, trackingType, theme }: IMessageBoxMentionItem) => {
const context = useContext(MessageboxContext); const context = useContext(MessageboxContext);
const { onPressMention } = context; const { onPressMention } = context;
const defineTestID = (type) => { const defineTestID = (type: string) => {
switch (type) { switch (type) {
case MENTIONS_TRACKING_TYPE_EMOJIS: case MENTIONS_TRACKING_TYPE_EMOJIS:
return `mention-item-${ item.name || item }`; return `mention-item-${item.name || item}`;
case MENTIONS_TRACKING_TYPE_COMMANDS: case MENTIONS_TRACKING_TYPE_COMMANDS:
return `mention-item-${ item.command || item }`; return `mention-item-${item.command || item}`;
case MENTIONS_TRACKING_TYPE_CANNED:
return `mention-item-${item.shortcut || item}`;
default: default:
return `mention-item-${ item.username || item.name || item }`; return `mention-item-${item.username || item.name || item}`;
} }
}; };
@ -38,13 +49,8 @@ const MentionItem = ({
let content = ( let content = (
<> <>
<Avatar <Avatar style={styles.avatar} text={item.username || item.name} size={30} type={item.t} />
style={styles.avatar} <Text style={[styles.mentionText, { color: themes[theme].titleText }]}>{item.username || item.name || item}</Text>
text={item.username || item.name}
size={30}
type={item.t}
/>
<Text style={[styles.mentionText, { color: themes[theme].titleText }]}>{ item.username || item.name || item }</Text>
</> </>
); );
@ -52,7 +58,7 @@ const MentionItem = ({
content = ( content = (
<> <>
<MentionEmoji item={item} /> <MentionEmoji item={item} />
<Text style={[styles.mentionText, { color: themes[theme].titleText }]}>:{ item.name || item }:</Text> <Text style={[styles.mentionText, { color: themes[theme].titleText }]}>:{item.name || item}:</Text>
</> </>
); );
} }
@ -66,6 +72,17 @@ const MentionItem = ({
); );
} }
if (trackingType === MENTIONS_TRACKING_TYPE_CANNED) {
content = (
<>
<Text style={[styles.cannedItem, { color: themes[theme].titleText }]}>!{item.shortcut}</Text>
<Text numberOfLines={1} style={[styles.cannedMentionText, { color: themes[theme].auxiliaryTintColor }]}>
{item.text}
</Text>
</>
);
}
return ( return (
<TouchableOpacity <TouchableOpacity
style={[ style={[
@ -76,17 +93,10 @@ const MentionItem = ({
} }
]} ]}
onPress={() => onPressMention(item)} onPress={() => onPressMention(item)}
testID={testID} testID={testID}>
>
{content} {content}
</TouchableOpacity> </TouchableOpacity>
); );
}; };
MentionItem.propTypes = {
item: PropTypes.object,
trackingType: PropTypes.string,
theme: PropTypes.string
};
export default MentionItem; export default MentionItem;

View File

@ -1,45 +0,0 @@
import React from 'react';
import { FlatList, View } from 'react-native';
import PropTypes from 'prop-types';
import { dequal } from 'dequal';
import styles from '../styles';
import MentionItem from './MentionItem';
import { themes } from '../../../constants/colors';
const Mentions = React.memo(({ mentions, trackingType, theme }) => {
if (!trackingType) {
return null;
}
return (
<View testID='messagebox-container'>
<FlatList
style={[styles.mentionList, { backgroundColor: themes[theme].auxiliaryBackground }]}
data={mentions}
extraData={mentions}
renderItem={({ item }) => <MentionItem item={item} trackingType={trackingType} theme={theme} />}
keyExtractor={item => item.rid || item.name || item.command || item}
keyboardShouldPersistTaps='always'
/>
</View>
);
}, (prevProps, nextProps) => {
if (prevProps.theme !== nextProps.theme) {
return false;
}
if (prevProps.trackingType !== nextProps.trackingType) {
return false;
}
if (!dequal(prevProps.mentions, nextProps.mentions)) {
return false;
}
return true;
});
Mentions.propTypes = {
mentions: PropTypes.array,
trackingType: PropTypes.string,
theme: PropTypes.string
};
export default Mentions;

View File

@ -0,0 +1,55 @@
import React from 'react';
import { FlatList, View } from 'react-native';
import { dequal } from 'dequal';
import MentionHeaderList from './MentionHeaderList';
import styles from '../styles';
import MentionItem from './MentionItem';
import { themes } from '../../../constants/colors';
interface IMessageBoxMentions {
mentions: any[];
trackingType: string;
theme: string;
loading: boolean;
}
const Mentions = React.memo(
({ mentions, trackingType, theme, loading }: IMessageBoxMentions) => {
if (!trackingType) {
return null;
}
return (
<View testID='messagebox-container'>
<FlatList
style={[styles.mentionList, { backgroundColor: themes[theme].auxiliaryBackground }]}
ListHeaderComponent={() => (
<MentionHeaderList trackingType={trackingType} hasMentions={mentions.length > 0} theme={theme} loading={loading} />
)}
data={mentions}
extraData={mentions}
renderItem={({ item }) => <MentionItem item={item} trackingType={trackingType} theme={theme} />}
keyExtractor={item => item.rid || item.name || item.command || item.shortcut || item}
keyboardShouldPersistTaps='always'
/>
</View>
);
},
(prevProps, nextProps) => {
if (prevProps.loading !== nextProps.loading) {
return false;
}
if (prevProps.theme !== nextProps.theme) {
return false;
}
if (prevProps.trackingType !== nextProps.trackingType) {
return false;
}
if (!dequal(prevProps.mentions, nextProps.mentions)) {
return false;
}
return true;
}
);
export default Mentions;

View File

@ -1,22 +1,27 @@
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types'; import { Text, View } from 'react-native';
import { View, Text } from 'react-native';
import { Audio } from 'expo-av'; import { Audio } from 'expo-av';
import { BorderlessButton } from 'react-native-gesture-handler'; import { BorderlessButton } from 'react-native-gesture-handler';
import { getInfoAsync } from 'expo-file-system'; import { getInfoAsync } from 'expo-file-system';
import { deactivateKeepAwake, activateKeepAwake } from 'expo-keep-awake'; import { activateKeepAwake, deactivateKeepAwake } from 'expo-keep-awake';
import styles from './styles'; import styles from './styles';
import I18n from '../../i18n'; import I18n from '../../i18n';
import { themes } from '../../constants/colors'; import { themes } from '../../constants/colors';
import { CustomIcon } from '../../lib/Icons'; import { CustomIcon } from '../../lib/Icons';
import { logEvent, events } from '../../utils/log'; import { events, logEvent } from '../../utils/log';
const RECORDING_EXTENSION = '.aac'; interface IMessageBoxRecordAudioProps {
theme: string;
recordingCallback: Function;
onFinish: Function;
}
const RECORDING_EXTENSION = '.m4a';
const RECORDING_SETTINGS = { const RECORDING_SETTINGS = {
android: { android: {
extension: RECORDING_EXTENSION, extension: RECORDING_EXTENSION,
outputFormat: Audio.RECORDING_OPTION_ANDROID_OUTPUT_FORMAT_AAC_ADTS, outputFormat: Audio.RECORDING_OPTION_ANDROID_OUTPUT_FORMAT_MPEG_4,
audioEncoder: Audio.RECORDING_OPTION_ANDROID_AUDIO_ENCODER_AAC, audioEncoder: Audio.RECORDING_OPTION_ANDROID_AUDIO_ENCODER_AAC,
sampleRate: Audio.RECORDING_OPTIONS_PRESET_LOW_QUALITY.android.sampleRate, sampleRate: Audio.RECORDING_OPTIONS_PRESET_LOW_QUALITY.android.sampleRate,
numberOfChannels: Audio.RECORDING_OPTIONS_PRESET_LOW_QUALITY.android.numberOfChannels, numberOfChannels: Audio.RECORDING_OPTIONS_PRESET_LOW_QUALITY.android.numberOfChannels,
@ -34,42 +39,48 @@ const RECORDING_SETTINGS = {
const RECORDING_MODE = { const RECORDING_MODE = {
allowsRecordingIOS: true, allowsRecordingIOS: true,
playsInSilentModeIOS: true, playsInSilentModeIOS: true,
staysActiveInBackground: false, staysActiveInBackground: true,
shouldDuckAndroid: true, shouldDuckAndroid: true,
playThroughEarpieceAndroid: false, playThroughEarpieceAndroid: false,
interruptionModeIOS: Audio.INTERRUPTION_MODE_IOS_DO_NOT_MIX, interruptionModeIOS: Audio.INTERRUPTION_MODE_IOS_DO_NOT_MIX,
interruptionModeAndroid: Audio.INTERRUPTION_MODE_ANDROID_DO_NOT_MIX interruptionModeAndroid: Audio.INTERRUPTION_MODE_ANDROID_DO_NOT_MIX
}; };
const formatTime = function(seconds) { const formatTime = function (seconds: any) {
let minutes = Math.floor(seconds / 60); let minutes: any = Math.floor(seconds / 60);
seconds %= 60; seconds %= 60;
if (minutes < 10) { minutes = `0${ minutes }`; } if (minutes < 10) {
if (seconds < 10) { seconds = `0${ seconds }`; } minutes = `0${minutes}`;
return `${ minutes }:${ seconds }`; }
if (seconds < 10) {
seconds = `0${seconds}`;
}
return `${minutes}:${seconds}`;
}; };
export default class RecordAudio extends React.PureComponent { export default class RecordAudio extends React.PureComponent<IMessageBoxRecordAudioProps, any> {
static propTypes = { private isRecorderBusy: boolean;
theme: PropTypes.string,
recordingCallback: PropTypes.func,
onFinish: PropTypes.func
}
constructor(props) { private recording: any;
private LastDuration: number;
constructor(props: IMessageBoxRecordAudioProps) {
super(props); super(props);
this.isRecorderBusy = false; this.isRecorderBusy = false;
this.LastDuration = 0;
this.state = { this.state = {
isRecording: false, isRecording: false,
isRecorderActive: false,
recordingDurationMillis: 0 recordingDurationMillis: 0
}; };
} }
componentDidUpdate() { componentDidUpdate() {
const { recordingCallback } = this.props; const { recordingCallback } = this.props;
const { isRecording } = this.state; const { isRecorderActive } = this.state;
recordingCallback(isRecording); recordingCallback(isRecorderActive);
} }
componentWillUnmount() { componentWillUnmount() {
@ -83,7 +94,11 @@ export default class RecordAudio extends React.PureComponent {
return formatTime(Math.floor(recordingDurationMillis / 1000)); return formatTime(Math.floor(recordingDurationMillis / 1000));
} }
isRecordingPermissionGranted = async() => { get GetLastDuration() {
return formatTime(Math.floor(this.LastDuration / 1000));
}
isRecordingPermissionGranted = async () => {
try { try {
const permission = await Audio.getPermissionsAsync(); const permission = await Audio.getPermissionsAsync();
if (permission.status === 'granted') { if (permission.status === 'granted') {
@ -94,24 +109,27 @@ export default class RecordAudio extends React.PureComponent {
// Do nothing // Do nothing
} }
return false; return false;
} };
onRecordingStatusUpdate = (status) => { onRecordingStatusUpdate = (status: any) => {
this.setState({ this.setState({
isRecording: status.isRecording, isRecording: status.isRecording,
recordingDurationMillis: status.durationMillis recordingDurationMillis: status.durationMillis
}); });
} this.LastDuration = status.durationMillis;
};
startRecordingAudio = async() => { startRecordingAudio = async () => {
logEvent(events.ROOM_AUDIO_RECORD); logEvent(events.ROOM_AUDIO_RECORD);
if (!this.isRecorderBusy) { if (!this.isRecorderBusy) {
this.isRecorderBusy = true; this.isRecorderBusy = true;
this.LastDuration = 0;
try { try {
const canRecord = await this.isRecordingPermissionGranted(); const canRecord = await this.isRecordingPermissionGranted();
if (canRecord) { if (canRecord) {
await Audio.setAudioModeAsync(RECORDING_MODE); await Audio.setAudioModeAsync(RECORDING_MODE);
this.setState({ isRecorderActive: true });
this.recording = new Audio.Recording(); this.recording = new Audio.Recording();
await this.recording.prepareToRecordAsync(RECORDING_SETTINGS); await this.recording.prepareToRecordAsync(RECORDING_SETTINGS);
this.recording.setOnRecordingStatusUpdate(this.onRecordingStatusUpdate); this.recording.setOnRecordingStatusUpdate(this.onRecordingStatusUpdate);
@ -128,7 +146,7 @@ export default class RecordAudio extends React.PureComponent {
} }
}; };
finishRecordingAudio = async() => { finishRecordingAudio = async () => {
logEvent(events.ROOM_AUDIO_FINISH); logEvent(events.ROOM_AUDIO_FINISH);
if (!this.isRecorderBusy) { if (!this.isRecorderBusy) {
const { onFinish } = this.props; const { onFinish } = this.props;
@ -140,7 +158,7 @@ export default class RecordAudio extends React.PureComponent {
const fileURI = this.recording.getURI(); const fileURI = this.recording.getURI();
const fileData = await getInfoAsync(fileURI); const fileData = await getInfoAsync(fileURI);
const fileInfo = { const fileInfo = {
name: `${ Date.now() }.aac`, name: `${Date.now()}.m4a`,
mime: 'audio/aac', mime: 'audio/aac',
type: 'audio/aac', type: 'audio/aac',
store: 'Uploads', store: 'Uploads',
@ -152,13 +170,13 @@ export default class RecordAudio extends React.PureComponent {
} catch (error) { } catch (error) {
logEvent(events.ROOM_AUDIO_FINISH_F); logEvent(events.ROOM_AUDIO_FINISH_F);
} }
this.setState({ isRecording: false, recordingDurationMillis: 0 }); this.setState({ isRecording: false, isRecorderActive: false, recordingDurationMillis: 0 });
deactivateKeepAwake(); deactivateKeepAwake();
this.isRecorderBusy = false; this.isRecorderBusy = false;
} }
}; };
cancelRecordingAudio = async() => { cancelRecordingAudio = async () => {
logEvent(events.ROOM_AUDIO_CANCEL); logEvent(events.ROOM_AUDIO_CANCEL);
if (!this.isRecorderBusy) { if (!this.isRecorderBusy) {
this.isRecorderBusy = true; this.isRecorderBusy = true;
@ -167,7 +185,7 @@ export default class RecordAudio extends React.PureComponent {
} catch (error) { } catch (error) {
logEvent(events.ROOM_AUDIO_CANCEL_F); logEvent(events.ROOM_AUDIO_CANCEL_F);
} }
this.setState({ isRecording: false, recordingDurationMillis: 0 }); this.setState({ isRecording: false, isRecorderActive: false, recordingDurationMillis: 0 });
deactivateKeepAwake(); deactivateKeepAwake();
this.isRecorderBusy = false; this.isRecorderBusy = false;
} }
@ -175,54 +193,69 @@ export default class RecordAudio extends React.PureComponent {
render() { render() {
const { theme } = this.props; const { theme } = this.props;
const { isRecording } = this.state; const { isRecording, isRecorderActive } = this.state;
if (!isRecording) { if (!isRecording && !isRecorderActive) {
return ( return (
<BorderlessButton <BorderlessButton
onPress={this.startRecordingAudio} onPress={this.startRecordingAudio}
style={styles.actionButton} style={styles.actionButton}
testID='messagebox-send-audio' testID='messagebox-send-audio'
// @ts-ignore
accessibilityLabel={I18n.t('Send_audio_message')} accessibilityLabel={I18n.t('Send_audio_message')}
accessibilityTraits='button' accessibilityTraits='button'>
>
<CustomIcon name='microphone' size={24} color={themes[theme].auxiliaryTintColor} /> <CustomIcon name='microphone' size={24} color={themes[theme].auxiliaryTintColor} />
</BorderlessButton> </BorderlessButton>
); );
} }
if (!isRecording && isRecorderActive) {
return (
<View style={styles.recordingContent}>
<View style={styles.textArea}>
<BorderlessButton
onPress={this.cancelRecordingAudio}
// @ts-ignore
accessibilityLabel={I18n.t('Cancel_recording')}
accessibilityTraits='button'
style={styles.actionButton}>
<CustomIcon size={24} color={themes[theme].dangerColor} name='delete' />
</BorderlessButton>
<Text style={[styles.recordingDurationText, { color: themes[theme].titleText }]}>{this.GetLastDuration}</Text>
</View>
<BorderlessButton
onPress={this.finishRecordingAudio}
// @ts-ignore
accessibilityLabel={I18n.t('Finish_recording')}
accessibilityTraits='button'
style={styles.actionButton}>
<CustomIcon size={24} color={themes[theme].tintColor} name='send-filled' />
</BorderlessButton>
</View>
);
}
return ( return (
<View style={styles.recordingContent}> <View style={styles.recordingContent}>
<View style={styles.textArea}> <View style={styles.textArea}>
<BorderlessButton <BorderlessButton
onPress={this.cancelRecordingAudio} onPress={this.cancelRecordingAudio}
// @ts-ignore
accessibilityLabel={I18n.t('Cancel_recording')} accessibilityLabel={I18n.t('Cancel_recording')}
accessibilityTraits='button' accessibilityTraits='button'
style={styles.actionButton} style={styles.actionButton}>
> <CustomIcon size={24} color={themes[theme].dangerColor} name='delete' />
<CustomIcon
size={24}
color={themes[theme].dangerColor}
name='close'
/>
</BorderlessButton> </BorderlessButton>
<Text <Text style={[styles.recordingDurationText, { color: themes[theme].titleText }]}>{this.duration}</Text>
style={[styles.recordingCancelText, { color: themes[theme].titleText }]} <CustomIcon size={24} color={themes[theme].dangerColor} name='record' />
>
{this.duration}
</Text>
</View> </View>
<BorderlessButton <BorderlessButton
onPress={this.finishRecordingAudio} onPress={this.finishRecordingAudio}
// @ts-ignore
accessibilityLabel={I18n.t('Finish_recording')} accessibilityLabel={I18n.t('Finish_recording')}
accessibilityTraits='button' accessibilityTraits='button'
style={styles.actionButton} style={styles.actionButton}>
> <CustomIcon size={24} color={themes[theme].tintColor} name='send-filled' />
<CustomIcon
size={24}
color={themes[theme].successColor}
name='check'
/>
</BorderlessButton> </BorderlessButton>
</View> </View>
); );

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