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

View File

@ -1,161 +1,156 @@
module.exports = {
"settings": {
"import/resolver": {
"node": {
"extensions": [".js", ".ios.js", ".android.js", ".native.js", ".tsx"]
}
}
},
"parser": "@babel/eslint-parser",
"extends": "airbnb",
"parserOptions": {
"sourceType": "module",
"ecmaVersion": 2017,
"ecmaFeatures": {
"experimentalObjectRestSpread" : true,
"jsx": true,
"legacyDecorators": true
settings: {
'import/resolver': {
node: {
extensions: ['.js', '.ios.js', '.android.js', '.native.js', '.ts', '.tsx']
}
}
},
"plugins": [
"react",
"jsx-a11y",
"import",
"react-native",
"@babel"
],
"env": {
"browser": true,
"commonjs": true,
"es6": true,
"node": true,
"jquery": true,
"mocha": true
parser: '@babel/eslint-parser',
extends: ['@rocket.chat/eslint-config', 'prettier'],
parserOptions: {
sourceType: 'module',
ecmaVersion: 2017,
ecmaFeatures: {
experimentalObjectRestSpread: true,
jsx: true,
legacyDecorators: true
}
},
"rules": {
"react/jsx-filename-extension": [1, {
"extensions": [".js", ".jsx"]
}],
"react/require-default-props": [0],
"react/no-unused-prop-types": [2, {
"skipShapeProps": 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]
plugins: ['react', 'jsx-a11y', 'import', 'react-native', '@babel'],
env: {
browser: true,
commonjs: true,
es6: true,
node: true,
jquery: true,
mocha: true
},
"globals": {
"__DEV__": true
rules: {
'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: [
{
@ -173,6 +168,86 @@ module.exports = {
'no-await-in-loop': 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
```
## 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
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.
## 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
### Share your story

View File

@ -899,27 +899,22 @@ exports[`Storyshots BackgroundContainer black theme - loading 1`] = `
</View>
<ActivityIndicator
animating={true}
color="#999999"
color="#f9f9f9"
hidesWhenStopped={true}
size="small"
style={
Array [
Object {
"backgroundColor": "transparent",
"fontFamily": "System",
"fontSize": 16,
"fontWeight": "400",
"left": 0,
"paddingHorizontal": 24,
"position": "absolute",
"right": 0,
"textAlign": "center",
"top": 60,
},
Object {
"color": "#f9f9f9",
},
]
Object {
"backgroundColor": "transparent",
"fontFamily": "System",
"fontSize": 16,
"fontWeight": "400",
"left": 0,
"paddingHorizontal": 24,
"position": "absolute",
"right": 0,
"textAlign": "center",
"top": 60,
}
}
/>
</View>
@ -1037,27 +1032,22 @@ exports[`Storyshots BackgroundContainer dark theme - loading 1`] = `
</View>
<ActivityIndicator
animating={true}
color="#999999"
color="#f9f9f9"
hidesWhenStopped={true}
size="small"
style={
Array [
Object {
"backgroundColor": "transparent",
"fontFamily": "System",
"fontSize": 16,
"fontWeight": "400",
"left": 0,
"paddingHorizontal": 24,
"position": "absolute",
"right": 0,
"textAlign": "center",
"top": 60,
},
Object {
"color": "#f9f9f9",
},
]
Object {
"backgroundColor": "transparent",
"fontFamily": "System",
"fontSize": 16,
"fontWeight": "400",
"left": 0,
"paddingHorizontal": 24,
"position": "absolute",
"right": 0,
"textAlign": "center",
"top": 60,
}
}
/>
</View>
@ -1175,27 +1165,22 @@ exports[`Storyshots BackgroundContainer loading 1`] = `
</View>
<ActivityIndicator
animating={true}
color="#999999"
color="#6C727A"
hidesWhenStopped={true}
size="small"
style={
Array [
Object {
"backgroundColor": "transparent",
"fontFamily": "System",
"fontSize": 16,
"fontWeight": "400",
"left": 0,
"paddingHorizontal": 24,
"position": "absolute",
"right": 0,
"textAlign": "center",
"top": 60,
},
Object {
"color": "#6C727A",
},
]
Object {
"backgroundColor": "transparent",
"fontFamily": "System",
"fontSize": 16,
"fontWeight": "400",
"left": 0,
"paddingHorizontal": 24,
"position": "absolute",
"right": 0,
"textAlign": "center",
"top": 60,
}
}
/>
</View>
@ -1337,6 +1322,595 @@ exports[`Storyshots BackgroundContainer text 1`] = `
</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`] = `
<RNCSafeAreaView
edges={
@ -4693,7 +5267,7 @@ exports[`Storyshots List pressable 1`] = `
"justifyContent": "center",
"paddingHorizontal": 12,
},
false,
undefined,
Object {
"height": 92,
},
@ -6211,7 +6785,7 @@ exports[`Storyshots List with bigger font 1`] = `
"justifyContent": "center",
"paddingHorizontal": 12,
},
false,
undefined,
Object {
"height": 69,
},
@ -6625,7 +7199,7 @@ exports[`Storyshots List with bigger font 1`] = `
"justifyContent": "center",
"paddingHorizontal": 12,
},
false,
undefined,
Object {
"height": 69,
},
@ -7080,7 +7654,7 @@ exports[`Storyshots List with black theme 1`] = `
"justifyContent": "center",
"paddingHorizontal": 12,
},
false,
undefined,
Object {
"height": 92,
},
@ -7494,7 +8068,7 @@ exports[`Storyshots List with black theme 1`] = `
"justifyContent": "center",
"paddingHorizontal": 12,
},
false,
undefined,
Object {
"height": 92,
},
@ -7972,7 +8546,7 @@ exports[`Storyshots List with custom colors 1`] = `
"justifyContent": "center",
"paddingHorizontal": 12,
},
false,
undefined,
Object {
"height": 92,
},
@ -8129,7 +8703,7 @@ exports[`Storyshots List with dark theme 1`] = `
"justifyContent": "center",
"paddingHorizontal": 12,
},
false,
undefined,
Object {
"height": 92,
},
@ -8543,7 +9117,7 @@ exports[`Storyshots List with dark theme 1`] = `
"justifyContent": "center",
"paddingHorizontal": 12,
},
false,
undefined,
Object {
"height": 92,
},
@ -10410,7 +10984,7 @@ exports[`Storyshots List with small font 1`] = `
"justifyContent": "center",
"paddingHorizontal": 12,
},
false,
undefined,
Object {
"height": 36.800000000000004,
},
@ -10824,7 +11398,7 @@ exports[`Storyshots List with small font 1`] = `
"justifyContent": "center",
"paddingHorizontal": 12,
},
false,
undefined,
Object {
"height": 36.800000000000004,
},
@ -40581,6 +41155,7 @@ exports[`Storyshots Message Show a button as attachment 1`] = `
"color": "#ffffff",
},
undefined,
undefined,
]
}
>

View File

@ -25,7 +25,7 @@ import com.android.build.OutputFile
* bundleAssetName: "index.android.bundle",
*
* // 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.
* entryFile: "index.android.js",
*
@ -144,7 +144,7 @@ android {
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
versionCode VERSIONCODE as Integer
versionName "4.19.0"
versionName "4.20.0"
vectorDrawables.useSupportLibrary = true
if (!isFoss) {
manifestPlaceholders = [BugsnagAPIKey: BugsnagAPIKey as String]

View File

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

View File

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

View File

@ -2,21 +2,16 @@
import { NativeModules } from 'react-native';
import Reactotron from 'reactotron-react-native';
import { reactotronRedux } from 'reactotron-redux';
import sagaPlugin from 'reactotron-redux-saga'
import sagaPlugin from 'reactotron-redux-saga';
if (__DEV__) {
const scriptURL = NativeModules.SourceCode.scriptURL;
const scriptHostname = scriptURL.split('://')[1].split(':')[0];
Reactotron
.configure({ host: scriptHostname })
.useReactNative()
.use(reactotronRedux())
.use(sagaPlugin())
.connect();
// Running on android device
// $ adb reverse tcp:9090 tcp:9090
Reactotron.clear();
console.warn = Reactotron.log;
console.log = Reactotron.log;
console.disableYellowBox = true;
const scriptURL = NativeModules.SourceCode.scriptURL;
const scriptHostname = scriptURL.split('://')[1].split(':')[0];
Reactotron.configure({ host: scriptHostname }).useReactNative().use(reactotronRedux()).use(sagaPlugin()).connect();
// Running on android device
// $ 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];
function createRequestTypes(base, types = defaultTypes) {
const res = {};
types.forEach(type => (res[type] = `${ base }_${ type }`));
types.forEach(type => (res[type] = `${base}_${type}`));
return res;
}
// Login events
export const LOGIN = createRequestTypes('LOGIN', [
...defaultTypes,
'SET_SERVICES',
'SET_PREFERENCE',
'SET_LOCAL_AUTHENTICATED'
]);
export const SHARE = createRequestTypes('SHARE', [
'SELECT_SERVER',
'SET_USER',
'SET_SETTINGS',
'SET_SERVER_INFO'
]);
export const LOGIN = createRequestTypes('LOGIN', [...defaultTypes, '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 ROOMS = createRequestTypes('ROOMS', [
...defaultTypes,
@ -33,8 +23,24 @@ export const ROOMS = createRequestTypes('ROOMS', [
'OPEN_SEARCH_HEADER',
'CLOSE_SEARCH_HEADER'
]);
export const ROOM = createRequestTypes('ROOM', ['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 ROOM = createRequestTypes('ROOM', [
'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 MESSAGES = createRequestTypes('MESSAGES', ['REPLY_BROADCAST']);
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_INSIDE = 'inside';
export const ROOT_LOADING = 'loading';
export const ROOT_NEW_SERVER = 'newServer';
export const ROOT_SET_USERNAME = 'setUsername';
export function appStart({ root, ...args }) {

View File

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

View File

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

View File

@ -125,15 +125,15 @@ const keyCommands = [
discoverabilityTitle: I18n.t('Add_server')
},
// Refers to select rooms on list
...([1, 2, 3, 4, 5, 6, 7, 8, 9].map(value => ({
input: `${ value }`,
...[1, 2, 3, 4, 5, 6, 7, 8, 9].map(value => ({
input: `${value}`,
modifierFlags: constants.keyModifierCommand
}))),
})),
// Refers to select servers on list
...([1, 2, 3, 4, 5, 6, 7, 8, 9].map(value => ({
input: `${ value }`,
...[1, 2, 3, 4, 5, 6, 7, 8, 9].map(value => ({
input: `${value}`,
modifierFlags: constants.keyModifierCommand | constants.keyModifierAlternate
})))
}))
];
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 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']);

View File

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

View File

@ -2,8 +2,10 @@ import { getBundleId, isIOS } from '../utils/deviceInfo';
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 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 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: {
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 PropTypes from 'prop-types';
import { View } from 'react-native';
import styles from './styles';
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.handleIndicator, { backgroundColor: themes[theme].auxiliaryText }]} />
</View>
));
Handle.propTypes = {
theme: PropTypes.string
};

View File

@ -1,13 +1,25 @@
import React from 'react';
import PropTypes from 'prop-types';
import { Text, View } from 'react-native';
import { themes } from '../../constants/colors';
import { CustomIcon } from '../../lib/Icons';
import styles from './styles';
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 = () => {
hide();
item?.onPress();
@ -18,34 +30,16 @@ export const Item = React.memo(({ item, hide, theme }) => {
onPress={onPress}
style={[styles.item, { backgroundColor: themes[theme].focusedBackground }]}
theme={theme}
testID={item.testID}
>
testID={item.testID}>
<CustomIcon name={item.icon} size={20} color={item.danger ? themes[theme].dangerColor : themes[theme].bodyText} />
<View style={styles.titleContainer}>
<Text
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}
</Text>
</View>
{ item.right ? (
<View style={styles.rightContainer}>
{item.right ? item.right() : null}
</View>
) : null }
{item.right ? <View style={styles.rightContainer}>{item.right ? item.right() : null}</View> : null}
</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 { StyleSheet, View, Text } from 'react-native';
import PropTypes from 'prop-types';
import { StyleSheet, Text, View } from 'react-native';
import { themes } from '../constants/colors';
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}>
<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>
));
AppVersion.propTypes = {
theme: PropTypes.string
};
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 PropTypes from 'prop-types';
import { connect } from 'react-redux';
import { Q } from '@nozbe/watermelondb';
import database from '../../lib/database';
import { getUserSelector } from '../../selectors/login';
import Avatar from './Avatar';
import { IAvatar } from './interfaces';
class AvatarContainer extends React.Component {
static propTypes = {
rid: PropTypes.string,
text: PropTypes.string,
type: PropTypes.string,
blockUnauthenticatedAccess: PropTypes.bool,
serverVersion: PropTypes.string
};
class AvatarContainer extends React.Component<Partial<IAvatar>, any> {
private mounted: boolean;
private subscription!: any;
static defaultProps = {
text: '',
type: 'd'
};
constructor(props) {
constructor(props: Partial<IAvatar>) {
super(props);
this.mounted = false;
this.state = { avatarETag: '' };
@ -32,7 +28,7 @@ class AvatarContainer extends React.Component {
this.mounted = true;
}
componentDidUpdate(prevProps) {
componentDidUpdate(prevProps: any) {
const { text, type } = this.props;
if (prevProps.text !== text || prevProps.type !== type) {
this.init();
@ -50,7 +46,7 @@ class AvatarContainer extends React.Component {
return type === 'd';
}
init = async() => {
init = async () => {
const db = database.active;
const usersCollection = db.get('users');
const subsCollection = db.get('subscriptions');
@ -59,7 +55,7 @@ class AvatarContainer extends React.Component {
try {
if (this.isDirect) {
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;
} else {
const { rid } = this.props;
@ -71,37 +67,32 @@ class AvatarContainer extends React.Component {
if (record) {
const observable = record.observe();
this.subscription = observable.subscribe((r) => {
this.subscription = observable.subscribe((r: any) => {
const { avatarETag } = r;
if (this.mounted) {
this.setState({ avatarETag });
} else {
// @ts-ignore
this.state.avatarETag = avatarETag;
}
});
}
}
};
render() {
const { avatarETag } = this.state;
const { serverVersion } = this.props;
return (
<Avatar
avatarETag={avatarETag}
serverVersion={serverVersion}
{...this.props}
/>
);
return <Avatar avatarETag={avatarETag} serverVersion={serverVersion} {...this.props} />;
}
}
const mapStateToProps = state => ({
const mapStateToProps = (state: any) => ({
user: getUserSelector(state),
server: state.share.server.server || state.server.server,
serverVersion: state.share.server.version || state.server.version,
blockUnauthenticatedAccess:
state.share.settings?.Accounts_AvatarBlockUnauthenticatedAccess
?? state.settings.Accounts_AvatarBlockUnauthenticatedAccess
?? true
state.share.settings?.Accounts_AvatarBlockUnauthenticatedAccess ??
state.settings.Accounts_AvatarBlockUnauthenticatedAccess ??
true
});
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 { storiesOf } from '@storybook/react-native';
import BackgroundContainer from '.';
import { ThemeContext } from '../../theme';
import { longText } from '../../../storybook/utils';
import BackgroundContainer from '.';
const stories = storiesOf('BackgroundContainer', module);
stories.add('basic', () => (
<BackgroundContainer />
));
stories.add('basic', () => <BackgroundContainer />);
stories.add('loading', () => (
<BackgroundContainer loading />
));
stories.add('loading', () => <BackgroundContainer loading />);
stories.add('text', () => (
<BackgroundContainer text='Text here' />
));
stories.add('text', () => <BackgroundContainer text='Text here' />);
stories.add('long text', () => (
<BackgroundContainer text={longText} />
));
stories.add('long text', () => <BackgroundContainer text={longText} />);
const ThemeStory = ({ theme, ...props }) => (
<ThemeContext.Provider
value={{ theme }}
>
<ThemeContext.Provider value={{ theme }}>
<BackgroundContainer {...props} />
</ThemeContext.Provider>
);
stories.add('dark theme - loading', () => (
<ThemeStory theme='dark' loading />
));
stories.add('dark theme - loading', () => <ThemeStory theme='dark' loading />);
stories.add('dark theme - text', () => (
<ThemeStory theme='dark' text={longText} />
));
stories.add('dark theme - text', () => <ThemeStory theme='dark' text={longText} />);
stories.add('black theme - loading', () => (
<ThemeStory theme='black' loading />
));
stories.add('black theme - loading', () => <ThemeStory theme='black' loading />);
stories.add('black theme - text', () => (
<ThemeStory theme='black' text={longText} />
));
stories.add('black theme - text', () => <ThemeStory theme='black' text={longText} />);

View File

@ -1,13 +1,16 @@
import React from 'react';
import {
ImageBackground, StyleSheet, Text, View, ActivityIndicator
} from 'react-native';
import PropTypes from 'prop-types';
import { ActivityIndicator, ImageBackground, StyleSheet, Text, View } from 'react-native';
import { withTheme } from '../../theme';
import sharedStyles from '../../views/Styles';
import { themes } from '../../constants/colors';
interface IBackgroundContainer {
text: string;
theme: string;
loading: boolean;
}
const styles = StyleSheet.create({
container: {
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}>
<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}
{loading ? <ActivityIndicator style={[styles.text, { color: themes[theme].auxiliaryTintColor }]} /> : null}
{loading ? <ActivityIndicator style={styles.text} color={themes[theme].auxiliaryTintColor} /> : null}
</View>
);
BackgroundContainer.propTypes = {
text: PropTypes.string,
theme: PropTypes.string,
loading: PropTypes.bool
};
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 { StyleSheet } from 'react-native';
import PropTypes from 'prop-types';
import { CustomIcon } from '../lib/Icons';
import { themes } from '../constants/colors';
interface ICheck {
style?: object;
theme: string;
}
const styles = StyleSheet.create({
icon: {
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' />);
Check.propTypes = {
style: PropTypes.object,
theme: PropTypes.string
};
const Check = React.memo(({ theme, style }: ICheck) => (
<CustomIcon style={[styles.icon, style]} color={themes[theme].tintColor} size={22} name='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 PropTypes from 'prop-types';
import { Text, TouchableOpacity, FlatList } from 'react-native';
import { FlatList, Text, TouchableOpacity } from 'react-native';
import shortnameToUnicode from '../../utils/shortnameToUnicode';
import styles from './styles';
import CustomEmoji from './CustomEmoji';
import scrollPersistTaps from '../../utils/scrollPersistTaps';
import { IEmoji, IEmojiCategory } from './interfaces';
const EMOJI_SIZE = 50;
const renderEmoji = (emoji, size, baseUrl) => {
const renderEmoji = (emoji: IEmoji, size: number, baseUrl: string) => {
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 (
<Text style={[styles.categoryEmoji, { height: size, width: size, fontSize: size - 14 }]}>
{shortnameToUnicode(`:${ emoji }:`)}
{shortnameToUnicode(`:${emoji}:`)}
</Text>
);
};
class EmojiCategory extends React.Component {
static propTypes = {
baseUrl: PropTypes.string.isRequired,
emojis: PropTypes.any,
onEmojiSelected: PropTypes.func,
emojisPerRow: PropTypes.number,
width: PropTypes.number
}
renderItem(emoji) {
class EmojiCategory extends React.Component<Partial<IEmojiCategory>> {
renderItem(emoji: any) {
const { baseUrl, onEmojiSelected } = this.props;
return (
<TouchableOpacity
activeOpacity={0.7}
key={emoji && emoji.isCustom ? emoji.content : emoji}
onPress={() => onEmojiSelected(emoji)}
testID={`reaction-picker-${ emoji && emoji.isCustom ? emoji.content : emoji }`}
>
{renderEmoji(emoji, EMOJI_SIZE, baseUrl)}
onPress={() => onEmojiSelected!(emoji)}
testID={`reaction-picker-${emoji && emoji.isCustom ? emoji.content : emoji}`}>
{renderEmoji(emoji, EMOJI_SIZE, baseUrl!)}
</TouchableOpacity>
);
}
@ -51,13 +48,14 @@ class EmojiCategory extends React.Component {
}
const numColumns = Math.trunc(width / EMOJI_SIZE);
const marginHorizontal = (width - (numColumns * EMOJI_SIZE)) / 2;
const marginHorizontal = (width - numColumns * EMOJI_SIZE) / 2;
return (
// @ts-ignore
<FlatList
contentContainerStyle={{ marginHorizontal }}
// rerender FlatList in case of width changes
key={`emoji-category-${ width }`}
key={`emoji-category-${width}`}
keyExtractor={item => (item && item.isCustom && item.content) || item}
data={emojis}
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 { View } from 'react-native';
import PropTypes from 'prop-types';
import ScrollableTabView from 'react-native-scrollable-tab-view';
import { dequal } from 'dequal';
import { connect } from 'react-redux';
@ -18,22 +17,33 @@ import shortnameToUnicode from '../../utils/shortnameToUnicode';
import log from '../../utils/log';
import { themes } from '../../constants/colors';
import { withTheme } from '../../theme';
import { IEmoji } from './interfaces';
const scrollProps = {
keyboardShouldPersistTaps: 'always',
keyboardDismissMode: 'none'
};
class EmojiPicker extends Component {
static propTypes = {
baseUrl: PropTypes.string.isRequired,
customEmojis: PropTypes.object,
onEmojiSelected: PropTypes.func,
tabEmojiStyle: PropTypes.object,
theme: PropTypes.string
};
interface IEmojiPickerProps {
isMessageContainsOnlyEmoji: boolean;
getCustomEmoji?: Function;
baseUrl: string;
customEmojis?: any;
style: object;
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);
const customEmojis = Object.keys(props.customEmojis)
.filter(item => item === props.customEmojis[item].name)
@ -55,7 +65,7 @@ class EmojiPicker extends Component {
this.setState({ show: true });
}
shouldComponentUpdate(nextProps, nextState) {
shouldComponentUpdate(nextProps: any, nextState: any) {
const { frequentlyUsed, show, width } = this.state;
const { theme } = this.props;
if (nextProps.theme !== theme) {
@ -73,67 +83,72 @@ class EmojiPicker extends Component {
return false;
}
onEmojiSelected = (emoji) => {
onEmojiSelected = (emoji: IEmoji) => {
try {
const { onEmojiSelected } = this.props;
if (emoji.isCustom) {
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 {
const content = emoji;
this._addFrequentlyUsed({ content, isCustom: false });
const shortname = `:${ emoji }:`;
onEmojiSelected(shortnameToUnicode(shortname), shortname);
const shortname = `:${emoji}:`;
onEmojiSelected!(shortnameToUnicode(shortname), shortname);
}
} catch (e) {
log(e);
}
}
};
// eslint-disable-next-line react/sort-comp
_addFrequentlyUsed = protectedFunction(async(emoji) => {
_addFrequentlyUsed = protectedFunction(async (emoji: IEmoji) => {
const db = database.active;
const freqEmojiCollection = db.get('frequently_used_emojis');
let freqEmojiRecord;
let freqEmojiRecord: any;
try {
freqEmojiRecord = await freqEmojiCollection.find(emoji.content);
} catch (error) {
// Do nothing
}
await db.action(async() => {
await db.action(async () => {
if (freqEmojiRecord) {
await freqEmojiRecord.update((f) => {
await freqEmojiRecord.update((f: any) => {
f.count += 1;
});
} else {
await freqEmojiCollection.create((f) => {
await freqEmojiCollection.create((f: any) => {
f._raw = sanitizedRaw({ id: emoji.content }, freqEmojiCollection.schema);
Object.assign(f, emoji);
f.count = 1;
});
}
});
})
});
updateFrequentlyUsed = async() => {
updateFrequentlyUsed = async () => {
const db = database.active;
const frequentlyUsedRecords = await db.get('frequently_used_emojis').query().fetch();
let frequentlyUsed = orderBy(frequentlyUsedRecords, ['count'], ['desc']);
frequentlyUsed = frequentlyUsed.map((item) => {
let frequentlyUsed: any = orderBy(frequentlyUsedRecords, ['count'], ['desc']);
frequentlyUsed = frequentlyUsed.map((item: IEmoji) => {
if (item.isCustom) {
return { content: item.content, extension: item.extension, isCustom: item.isCustom };
}
return shortnameToUnicode(`${ item.content }`);
return shortnameToUnicode(`${item.content}`);
});
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 { baseUrl } = this.props;
@ -148,9 +163,9 @@ class EmojiPicker extends Component {
return (
<EmojiCategory
emojis={emojis}
onEmojiSelected={emoji => this.onEmojiSelected(emoji)}
onEmojiSelected={(emoji: IEmoji) => this.onEmojiSelected(emoji)}
style={styles.categoryContainer}
width={width}
width={width!}
baseUrl={baseUrl}
tabLabel={label}
/>
@ -168,23 +183,21 @@ class EmojiPicker extends Component {
<View onLayout={this.onLayout} style={{ flex: 1 }}>
<ScrollableTabView
renderTabBar={() => <TabBar tabEmojiStyle={tabEmojiStyle} theme={theme} />}
/* @ts-ignore*/
contentProps={scrollProps}
style={{ backgroundColor: themes[theme].focusedBackground }}
>
{
categories.tabs.map((tab, i) => (
(i === 0 && frequentlyUsed.length === 0) ? null // when no frequentlyUsed don't show the tab
: (
this.renderCategory(tab.category, i, tab.tabLabel)
)))
}
style={{ backgroundColor: themes[theme!].focusedBackground }}>
{categories.tabs.map((tab, i) =>
i === 0 && frequentlyUsed.length === 0
? null // when no frequentlyUsed don't show the tab
: this.renderCategory(tab.category, i, tab.tabLabel)
)}
</ScrollableTabView>
</View>
);
}
}
const mapStateToProps = state => ({
const mapStateToProps = (state: any) => ({
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 { ScrollView, StyleSheet, View } from 'react-native';
import PropTypes from 'prop-types';
import { themes } from '../constants/colors';
import sharedStyles from '../views/Styles';
@ -11,33 +10,35 @@ import AppVersion from './AppVersion';
import { isTablet } from '../utils/deviceInfo';
import SafeAreaView from './SafeAreaView';
interface IFormContainer {
theme: string;
testID: string;
children: JSX.Element;
}
const styles = StyleSheet.create({
scrollView: {
minHeight: '100%'
}
});
export const FormContainerInner = ({ children }) => (
<View style={[sharedStyles.container, isTablet && sharedStyles.tabletScreenContent]}>
{children}
</View>
export const FormContainerInner = ({ children }: { children: JSX.Element }) => (
<View style={[sharedStyles.container, isTablet && sharedStyles.tabletScreenContent]}>{children}</View>
);
const FormContainer = ({
children, theme, testID, ...props
}) => (
const FormContainer = ({ children, theme, testID, ...props }: IFormContainer) => (
// @ts-ignore
<KeyboardView
style={{ backgroundColor: themes[theme].backgroundColor }}
contentContainerStyle={sharedStyles.container}
keyboardVerticalOffset={128}
>
keyboardVerticalOffset={128}>
<StatusBar />
{/* @ts-ignore*/}
<ScrollView
style={sharedStyles.container}
contentContainerStyle={[sharedStyles.containerScrollView, styles.scrollView]}
{...scrollPersistTaps}
{...props}
>
{...props}>
<SafeAreaView testID={testID} style={{ backgroundColor: themes[theme].backgroundColor }}>
{children}
<AppVersion theme={theme} />
@ -46,14 +47,4 @@ const FormContainer = ({
</KeyboardView>
);
FormContainer.propTypes = {
theme: PropTypes.string,
testID: PropTypes.string,
children: PropTypes.element
};
FormContainerInner.propTypes = {
children: PropTypes.element
};
export default FormContainer;

View File

@ -1,7 +1,7 @@
import React from 'react';
import PropTypes from 'prop-types';
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 { themedHeader } from '../../utils/navigation';
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
export const headerHeight = isIOS ? 44 : 56;
export const getHeaderHeight = (isLandscape) => {
export const getHeaderHeight = (isLandscape: boolean) => {
if (isIOS) {
if (isLandscape && !isTablet) {
return 32;
} else {
return 44;
}
return 44;
}
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,
right: insets.right + Math.max(45 * numIconsRight, 15)
});
@ -35,9 +42,14 @@ const styles = StyleSheet.create({
}
});
const Header = ({
theme, headerLeft, headerTitle, headerRight
}) => (
interface IHeader {
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']}>
<View style={[styles.container, { ...themedHeader(theme).headerStyle }]}>
{headerLeft ? headerLeft() : null}
@ -47,11 +59,4 @@ const Header = ({
</SafeAreaView>
);
Header.propTypes = {
theme: PropTypes.string,
headerLeft: PropTypes.element,
headerTitle: PropTypes.element,
headerRight: PropTypes.element
};
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 { View, StyleSheet } from 'react-native';
import PropTypes from 'prop-types';
import { StyleSheet, View } from 'react-native';
interface IHeaderButtonContainer {
children: JSX.Element;
left?: boolean;
}
const styles = StyleSheet.create({
container: {
@ -16,21 +20,10 @@ const styles = StyleSheet.create({
}
});
const Container = ({ children, left }) => (
<View style={[styles.container, left ? styles.left : styles.right]}>
{children}
</View>
const Container = ({ children, left = false }: IHeaderButtonContainer) => (
<View style={[styles.container, left ? styles.left : styles.right]}>{children}</View>
);
Container.propTypes = {
children: PropTypes.arrayOf(PropTypes.element),
left: PropTypes.bool
};
Container.defaultProps = {
left: false
};
Container.displayName = 'HeaderButton.Container';
export default Container;

View File

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

View File

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

View File

@ -1,6 +1,5 @@
import React from 'react';
import { StyleSheet, View, Text } from 'react-native';
import PropTypes from 'prop-types';
import { StyleSheet, Text, View } from 'react-native';
import Touchable from 'react-native-platform-touchable';
import { connect } from 'react-redux';
import { Notifier } from 'react-native-notifier';
@ -16,10 +15,13 @@ import { goRoom } from '../../utils/goRoom';
import Navigation from '../../lib/Navigation';
import { useOrientation } from '../../dimensions';
interface INotifierComponent {
notification: object;
isMasterDetail: boolean;
}
const AVATAR_SIZE = 48;
const BUTTON_HIT_SLOP = {
top: 12, right: 12, bottom: 12, left: 12
};
const BUTTON_HIT_SLOP = { top: 12, right: 12, bottom: 12, left: 12 };
const styles = StyleSheet.create({
container: {
@ -64,16 +66,16 @@ const styles = StyleSheet.create({
const hideNotification = () => Notifier.hideNotification();
const NotifierComponent = React.memo(({ notification, isMasterDetail }) => {
const { theme } = useTheme();
const NotifierComponent = React.memo(({ notification, isMasterDetail }: INotifierComponent) => {
const { theme }: any = useTheme();
const insets = useSafeAreaInsets();
const { isLandscape } = useOrientation();
const { text, payload } = notification;
const { text, payload }: any = notification;
const { type, rid } = payload;
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
const { title = name, avatar = name } = notification;
const { title = name, avatar = name }: any = notification;
const onPress = () => {
const { prid, _id } = payload;
@ -81,7 +83,10 @@ const NotifierComponent = React.memo(({ notification, isMasterDetail }) => {
return;
}
const item = {
rid, name: title, t: type, prid
rid,
name: title,
t: type,
prid
};
if (isMasterDetail) {
@ -94,47 +99,41 @@ const NotifierComponent = React.memo(({ notification, isMasterDetail }) => {
};
return (
<View style={[
styles.container,
(isMasterDetail || isLandscape) && styles.small,
{
backgroundColor: themes[theme].focusedBackground,
borderColor: themes[theme].separatorColor,
marginTop: insets.top
}
]}
>
<View
style={[
styles.container,
(isMasterDetail || isLandscape) && styles.small,
{
backgroundColor: themes[theme].focusedBackground,
borderColor: themes[theme].separatorColor,
marginTop: insets.top
}
]}>
<Touchable
style={styles.content}
onPress={onPress}
hitSlop={BUTTON_HIT_SLOP}
background={Touchable.SelectableBackgroundBorderless()}
>
background={Touchable.SelectableBackgroundBorderless()}>
<>
<Avatar text={avatar} size={AVATAR_SIZE} type={type} rid={rid} style={styles.avatar} />
<View style={styles.inner}>
<Text style={[styles.roomName, { color: themes[theme].titleText }]} numberOfLines={1}>{title}</Text>
<Text style={[styles.message, { color: themes[theme].titleText }]} numberOfLines={1}>{text}</Text>
<Text style={[styles.roomName, { color: themes[theme].titleText }]} numberOfLines={1}>
{title}
</Text>
<Text style={[styles.message, { color: themes[theme].titleText }]} numberOfLines={1}>
{text}
</Text>
</View>
</>
</Touchable>
<Touchable
onPress={hideNotification}
hitSlop={BUTTON_HIT_SLOP}
background={Touchable.SelectableBackgroundBorderless()}
>
<Touchable onPress={hideNotification} hitSlop={BUTTON_HIT_SLOP} background={Touchable.SelectableBackgroundBorderless()}>
<CustomIcon name='close' style={[styles.close, { color: themes[theme].titleText }]} size={20} />
</Touchable>
</View>
);
});
NotifierComponent.propTypes = {
notification: PropTypes.object,
isMasterDetail: PropTypes.bool
};
const mapStateToProps = state => ({
const mapStateToProps = (state: any) => ({
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 { ScrollView, StyleSheet } from 'react-native';
import PropTypes from 'prop-types';
import { withTheme } from '../../theme';
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
contentContainerStyle={styles.container}
scrollIndicatorInsets={{ right: 1 }} // https://github.com/facebook/react-native/issues/26610#issuecomment-539843444
{...scrollPersistTaps}
{...props}
>
{...props}>
{children}
</ScrollView>
));
ListContainer.propTypes = {
children: PropTypes.array.isRequired
};
ListContainer.displayName = 'List.Container';
export default withTheme(ListContainer);

View File

@ -1,6 +1,5 @@
import React from 'react';
import { View, Text, StyleSheet } from 'react-native';
import PropTypes from 'prop-types';
import { StyleSheet, Text, View } from 'react-native';
import sharedStyles from '../../views/Styles';
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}>
<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>
));
ListHeader.propTypes = {
title: PropTypes.string,
theme: PropTypes.string,
translateTitle: PropTypes.bool
};
ListHeader.defaultProps = {
translateTitle: true
};
ListHeader.displayName = 'List.Header';
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 { View, Text, StyleSheet } from 'react-native';
import PropTypes from 'prop-types';
import { StyleSheet, Text, View } from 'react-native';
import sharedStyles from '../../views/Styles';
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}>
<Text style={[styles.text, { color: themes[theme].infoText }]}>{translateInfo ? I18n.t(info) : info}</Text>
</View>
));
ListInfo.propTypes = {
info: PropTypes.string,
theme: PropTypes.string,
translateInfo: PropTypes.bool
};
ListInfo.defaultProps = {
translateInfo: true
};
ListInfo.displayName = 'List.Info';
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 { View, StyleSheet } from 'react-native';
import PropTypes from 'prop-types';
import { StyleSheet, View } from 'react-native';
import { withTheme } from '../../theme';
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}>
{title ? <Header {...{ title, translateTitle }} /> : null}
{children}
</View>
));
ListSection.propTypes = {
children: PropTypes.array.isRequired,
title: PropTypes.string,
translateTitle: PropTypes.bool
};
ListSection.displayName = 'List.Section';
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 PropTypes from 'prop-types';
import {
StyleSheet, Modal, Animated, View
} from 'react-native';
import { Animated, Modal, StyleSheet, View } from 'react-native';
import { withTheme } from '../theme';
import { themes } from '../constants/colors';
@ -19,54 +17,51 @@ const styles = StyleSheet.create({
}
});
class Loading extends React.PureComponent {
static propTypes = {
visible: PropTypes.bool,
theme: PropTypes.string
}
interface ILoadingProps {
visible: boolean;
theme: string;
}
class Loading extends React.PureComponent<ILoadingProps, any> {
state = {
scale: new Animated.Value(1),
opacity: new Animated.Value(0)
}
};
private opacityAnimation: any;
private scaleAnimation: any;
componentDidMount() {
const { opacity, scale } = this.state;
const { visible } = this.props;
this.opacityAnimation = Animated.timing(
opacity,
{
toValue: 1,
duration: 200,
useNativeDriver: true
}
);
this.scaleAnimation = Animated.loop(Animated.sequence([
Animated.timing(
scale,
{
this.opacityAnimation = Animated.timing(opacity, {
toValue: 1,
duration: 200,
useNativeDriver: true
});
this.scaleAnimation = Animated.loop(
Animated.sequence([
Animated.timing(scale, {
toValue: 0,
duration: 1000,
useNativeDriver: true
}
),
Animated.timing(
scale,
{
}),
Animated.timing(scale, {
toValue: 1,
duration: 1000,
useNativeDriver: true
}
)
]));
})
])
);
if (visible) {
this.startAnimations();
}
}
componentDidUpdate(prevProps) {
componentDidUpdate(prevProps: any) {
const { visible } = this.props;
if (visible && visible !== prevProps.visible) {
this.startAnimations();
@ -107,29 +102,30 @@ class Loading extends React.PureComponent {
});
return (
<Modal
visible={visible}
transparent
onRequestClose={() => {}}
>
<View
style={styles.container}
testID='loading'
>
<Modal visible={visible} transparent onRequestClose={() => {}}>
<View style={styles.container} testID='loading'>
<Animated.View
style={[{
...StyleSheet.absoluteFill,
backgroundColor: themes[theme].backdropColor,
opacity: opacityAnimation
}]}
style={[
{
// @ts-ignore
...StyleSheet.absoluteFill,
backgroundColor: themes[theme].backdropColor,
opacity: opacityAnimation
}
]}
/>
<Animated.Image
source={require('../static/images/logo.png')}
style={[styles.image, {
transform: [{
scale: scaleAnimation
}]
}]}
style={[
styles.image,
{
transform: [
{
scale: scaleAnimation
}
]
}
]}
/>
</View>
</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 {
View, StyleSheet, Text, Animated, Easing, Linking
} from 'react-native';
import PropTypes from 'prop-types';
import { Animated, Easing, Linking, StyleSheet, Text, View } from 'react-native';
import { connect } from 'react-redux';
import { Base64 } from 'js-base64';
import * as AppleAuthentication from 'expo-apple-authentication';
@ -15,7 +12,7 @@ import OrSeparator from './OrSeparator';
import Touch from '../utils/touch';
import I18n from '../i18n';
import random from '../utils/random';
import { logEvent, events } from '../utils/log';
import { events, logEvent } from '../utils/log';
import RocketChat from '../lib/rocketchat';
import { CustomIcon } from '../lib/Icons';
@ -60,153 +57,175 @@ const styles = StyleSheet.create({
}
});
class LoginServices extends React.PureComponent {
static propTypes = {
navigation: PropTypes.object,
server: PropTypes.string,
services: PropTypes.object,
Gitlab_URL: PropTypes.string,
CAS_enabled: PropTypes.bool,
CAS_login_url: PropTypes.string,
separator: PropTypes.bool,
theme: PropTypes.string
}
interface IOpenOAuth {
url?: string;
ssoToken?: string;
authType?: string;
}
interface IService {
name: string;
service: 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 = {
separator: true
}
};
state = {
collapsed: true,
servicesHeight: new Animated.Value(SERVICES_COLLAPSED_HEIGHT)
}
};
onPressFacebook = () => {
logEvent(events.ENTER_WITH_FACEBOOK);
const { services, server } = this.props;
const { clientId } = services.facebook;
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 state = this.getOAuthState();
const params = `?client_id=${ clientId }&redirect_uri=${ redirect_uri }&scope=${ scope }&state=${ state }&display=touch`;
this.openOAuth({ url: `${ endpoint }${ params }` });
}
const params = `?client_id=${clientId}&redirect_uri=${redirect_uri}&scope=${scope}&state=${state}&display=touch`;
this.openOAuth({ url: `${endpoint}${params}` });
};
onPressGithub = () => {
logEvent(events.ENTER_WITH_GITHUB);
const { services, server } = this.props;
const { clientId } = services.github;
const endpoint = `https://github.com/login?client_id=${ clientId }&return_to=${ encodeURIComponent('/login/oauth/authorize') }`;
const redirect_uri = `${ server }/_oauth/github?close`;
const endpoint = `https://github.com/login?client_id=${clientId}&return_to=${encodeURIComponent('/login/oauth/authorize')}`;
const redirect_uri = `${server}/_oauth/github?close`;
const scope = 'user:email';
const state = this.getOAuthState();
const params = `?client_id=${ clientId }&redirect_uri=${ redirect_uri }&scope=${ scope }&state=${ state }`;
this.openOAuth({ url: `${ endpoint }${ encodeURIComponent(params) }` });
}
const params = `?client_id=${clientId}&redirect_uri=${redirect_uri}&scope=${scope}&state=${state}`;
this.openOAuth({ url: `${endpoint}${encodeURIComponent(params)}` });
};
onPressGitlab = () => {
logEvent(events.ENTER_WITH_GITLAB);
const { services, server, Gitlab_URL } = this.props;
const { clientId } = services.gitlab;
const baseURL = Gitlab_URL ? Gitlab_URL.trim().replace(/\/*$/, '') : 'https://gitlab.com';
const endpoint = `${ baseURL }/oauth/authorize`;
const redirect_uri = `${ server }/_oauth/gitlab?close`;
const endpoint = `${baseURL}/oauth/authorize`;
const redirect_uri = `${server}/_oauth/gitlab?close`;
const scope = 'read_user';
const state = this.getOAuthState();
const params = `?client_id=${ clientId }&redirect_uri=${ redirect_uri }&scope=${ scope }&state=${ state }&response_type=code`;
this.openOAuth({ url: `${ endpoint }${ params }` });
}
const params = `?client_id=${clientId}&redirect_uri=${redirect_uri}&scope=${scope}&state=${state}&response_type=code`;
this.openOAuth({ url: `${endpoint}${params}` });
};
onPressGoogle = () => {
logEvent(events.ENTER_WITH_GOOGLE);
const { services, server } = this.props;
const { clientId } = services.google;
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 state = this.getOAuthState(LOGIN_STYPE_REDIRECT);
const params = `?client_id=${ clientId }&redirect_uri=${ redirect_uri }&scope=${ scope }&state=${ state }&response_type=code`;
Linking.openURL(`${ endpoint }${ params }`);
}
const params = `?client_id=${clientId}&redirect_uri=${redirect_uri}&scope=${scope}&state=${state}&response_type=code`;
Linking.openURL(`${endpoint}${params}`);
};
onPressLinkedin = () => {
logEvent(events.ENTER_WITH_LINKEDIN);
const { services, server } = this.props;
const { clientId } = services.linkedin;
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 state = this.getOAuthState();
const params = `?client_id=${ clientId }&redirect_uri=${ redirect_uri }&scope=${ scope }&state=${ state }&response_type=code`;
this.openOAuth({ url: `${ endpoint }${ params }` });
}
const params = `?client_id=${clientId}&redirect_uri=${redirect_uri}&scope=${scope}&state=${state}&response_type=code`;
this.openOAuth({ url: `${endpoint}${params}` });
};
onPressMeteor = () => {
logEvent(events.ENTER_WITH_METEOR);
const { services, server } = this.props;
const { clientId } = services['meteor-developer'];
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 params = `?client_id=${ clientId }&redirect_uri=${ redirect_uri }&state=${ state }&response_type=code`;
this.openOAuth({ url: `${ endpoint }${ params }` });
}
const params = `?client_id=${clientId}&redirect_uri=${redirect_uri}&state=${state}&response_type=code`;
this.openOAuth({ url: `${endpoint}${params}` });
};
onPressTwitter = () => {
logEvent(events.ENTER_WITH_TWITTER);
const { server } = this.props;
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 });
}
};
onPressWordpress = () => {
logEvent(events.ENTER_WITH_WORDPRESS);
const { services, server } = this.props;
const { clientId, serverURL } = services.wordpress;
const endpoint = `${ serverURL }/oauth/authorize`;
const redirect_uri = `${ server }/_oauth/wordpress?close`;
const endpoint = `${serverURL}/oauth/authorize`;
const redirect_uri = `${server}/_oauth/wordpress?close`;
const scope = 'openid';
const state = this.getOAuthState();
const params = `?client_id=${ clientId }&redirect_uri=${ redirect_uri }&scope=${ scope }&state=${ state }&response_type=code`;
this.openOAuth({ url: `${ endpoint }${ params }` });
}
const params = `?client_id=${clientId}&redirect_uri=${redirect_uri}&scope=${scope}&state=${state}&response_type=code`;
this.openOAuth({ url: `${endpoint}${params}` });
};
onPressCustomOAuth = (loginService) => {
onPressCustomOAuth = (loginService: any) => {
logEvent(events.ENTER_WITH_CUSTOM_OAUTH);
const { server } = this.props;
const {
serverURL, authorizePath, clientId, scope, service
} = loginService;
const redirectUri = `${ server }/_oauth/${ service }`;
const { serverURL, authorizePath, clientId, scope, service } = loginService;
const redirectUri = `${server}/_oauth/${service}`;
const state = this.getOAuthState();
const params = `?client_id=${ clientId }&redirect_uri=${ redirectUri }&response_type=code&state=${ state }&scope=${ scope }`;
const domain = `${ serverURL }`;
const absolutePath = `${ authorizePath }${ params }`;
const params = `?client_id=${clientId}&redirect_uri=${redirectUri}&response_type=code&state=${state}&scope=${scope}`;
const domain = `${serverURL}`;
const absolutePath = `${authorizePath}${params}`;
const url = absolutePath.includes(domain) ? absolutePath : domain + absolutePath;
this.openOAuth({ url });
}
};
onPressSaml = (loginService) => {
onPressSaml = (loginService: any) => {
logEvent(events.ENTER_WITH_SAML);
const { server } = this.props;
const { clientConfig } = loginService;
const { provider } = clientConfig;
const { clientConfig } = loginService;
const { provider } = clientConfig;
const ssoToken = random(17);
const url = `${ server }/_saml/authorize/${ provider }/${ ssoToken }`;
const url = `${server}/_saml/authorize/${provider}/${ssoToken}`;
this.openOAuth({ url, ssoToken, authType: 'saml' });
}
};
onPressCas = () => {
logEvent(events.ENTER_WITH_CAS);
const { server, CAS_login_url } = this.props;
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' });
}
};
onPressAppleLogin = async() => {
onPressAppleLogin = async () => {
logEvent(events.ENTER_WITH_APPLE);
try {
const { fullName, email, identityToken } = await AppleAuthentication.signInAsync({
@ -220,11 +239,11 @@ class LoginServices extends React.PureComponent {
} catch {
logEvent(events.ENTER_WITH_APPLE_F);
}
}
};
getOAuthState = (loginStyle = LOGIN_STYPE_POPUP) => {
const credentialToken = random(43);
let obj = { loginStyle, credentialToken, isCordova: true };
let obj: any = { loginStyle, credentialToken, isCordova: true };
if (loginStyle === LOGIN_STYPE_REDIRECT) {
obj = {
...obj,
@ -232,24 +251,26 @@ class LoginServices extends React.PureComponent {
};
}
return Base64.encodeURI(JSON.stringify(obj));
}
};
openOAuth = ({ url, ssoToken, authType = 'oauth' }) => {
openOAuth = ({ url, ssoToken, authType = 'oauth' }: IOpenOAuth) => {
const { navigation } = this.props;
navigation.navigate('AuthenticationWebView', { url, authType, ssoToken });
}
};
transitionServicesTo = (height) => {
transitionServicesTo = (height: number) => {
const { servicesHeight } = this.state;
if (this._animation) {
this._animation.stop();
}
// @ts-ignore
this._animation = Animated.timing(servicesHeight, {
toValue: height,
duration: 300,
// @ts-ignore
easing: Easing.easeOutCubic
}).start();
}
};
toggleServices = () => {
const { collapsed } = this.state;
@ -260,11 +281,11 @@ class LoginServices extends React.PureComponent {
} else {
this.transitionServicesTo(SERVICES_COLLAPSED_HEIGHT);
}
this.setState(prevState => ({ collapsed: !prevState.collapsed }));
}
this.setState((prevState: any) => ({ collapsed: !prevState.collapsed }));
};
getSocialOauthProvider = (name) => {
const oauthProviders = {
getSocialOauthProvider = (name: string) => {
const oauthProviders: any = {
facebook: this.onPressFacebook,
github: this.onPressGithub,
gitlab: this.onPressGitlab,
@ -275,7 +296,7 @@ class LoginServices extends React.PureComponent {
wordpress: this.onPressWordpress
};
return oauthProviders[name];
}
};
renderServicesSeparator = () => {
const { collapsed } = this.state;
@ -301,13 +322,13 @@ class LoginServices extends React.PureComponent {
return <OrSeparator theme={theme} />;
}
return null;
}
};
renderItem = (service) => {
renderItem = (service: IService) => {
const { CAS_enabled, theme } = this.props;
let { name } = service;
name = name === 'meteor-developer' ? 'meteor' : name;
const icon = `${ name }-monochromatic`;
const icon = `${name}-monochromatic`;
const isSaml = service.service === 'saml';
let onPress = () => {};
@ -357,15 +378,16 @@ class LoginServices extends React.PureComponent {
style={[styles.serviceButton, { backgroundColor }]}
theme={theme}
activeOpacity={0.5}
underlayColor={themes[theme].buttonText}
>
underlayColor={themes[theme].buttonText}>
<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>
</View>
</Touch>
);
}
};
render() {
const { servicesHeight } = this.state;
@ -379,23 +401,21 @@ class LoginServices extends React.PureComponent {
if (length > 3 && separator) {
return (
<>
<Animated.View style={style}>
{Object.values(services).map(service => this.renderItem(service))}
</Animated.View>
<Animated.View style={style}>{Object.values(services).map((service: any) => this.renderItem(service))}</Animated.View>
{this.renderServicesSeparator()}
</>
);
}
return (
<>
{Object.values(services).map(service => this.renderItem(service))}
{Object.values(services).map((service: any) => this.renderItem(service))}
{this.renderServicesSeparator()}
</>
);
}
}
const mapStateToProps = state => ({
const mapStateToProps = (state: any) => ({
server: state.server.server,
Gitlab_URL: state.settings.API_Gitlab_URL,
CAS_enabled: state.settings.CAS_enabled,

View File

@ -1,8 +1,5 @@
import React, { useEffect, useState, useCallback } from 'react';
import PropTypes from 'prop-types';
import {
View, Text, FlatList, StyleSheet
} from 'react-native';
import React, { useCallback, useEffect, useState } from 'react';
import { FlatList, StyleSheet, Text, View } from 'react-native';
import { withTheme } from '../../theme';
import { themes } from '../../constants/colors';
@ -13,6 +10,27 @@ import database from '../../lib/database';
import { Button } from '../ActionSheet';
import { useDimensions } from '../../dimensions';
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;
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 HeaderItem = React.memo(({
item, onReaction, server, theme
}) => (
const HeaderItem = React.memo(({ item, onReaction, server, theme }: THeaderItem) => (
<Button
testID={`message-actions-emoji-${ item.content || item }`}
onPress={() => onReaction({ emoji: `:${ item.content || item }:` })}
testID={`message-actions-emoji-${item.content || item}`}
onPress={() => onReaction({ emoji: `:${item.content || item}:` })}
style={[styles.headerItem, { backgroundColor: themes[theme].auxiliaryBackground }]}
theme={theme}
>
theme={theme}>
{item?.isCustom ? (
<CustomEmoji style={styles.customEmoji} emoji={item} baseUrl={server} />
) : (
<Text style={styles.headerIcon}>
{shortnameToUnicode(`:${ item.content || item }:`)}
</Text>
<Text style={styles.headerIcon}>{shortnameToUnicode(`:${item.content || item}:`)}</Text>
)}
</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
testID='add-reaction'
onPress={onReaction}
style={[styles.headerItem, { backgroundColor: themes[theme].auxiliaryBackground }]}
theme={theme}
>
theme={theme}>
<CustomIcon name='reaction-add' size={24} color={themes[theme].bodyText} />
</Button>
));
HeaderFooter.propTypes = {
onReaction: PropTypes.func,
theme: PropTypes.string
};
const Header = React.memo(({
handleReaction, server, message, isMasterDetail, theme
}) => {
const Header = React.memo(({ handleReaction, server, message, isMasterDetail, theme }: IHeader) => {
const [items, setItems] = useState([]);
const { width, height } = useDimensions();
const { width, height }: any = useDimensions();
const setEmojis = async() => {
const setEmojis = async () => {
try {
const db = database.active;
const freqEmojiCollection = db.get('frequently_used_emojis');
let freqEmojis = await freqEmojiCollection.query().fetch();
const isLandscape = width > height;
const size = (isLandscape || isMasterDetail ? width / 2 : width) - (CONTAINER_MARGIN * 2);
const quantity = (size / (ITEM_SIZE + (ITEM_MARGIN * 2))) - 1;
const size = (isLandscape || isMasterDetail ? width / 2 : width) - CONTAINER_MARGIN * 2;
const quantity = size / (ITEM_SIZE + ITEM_MARGIN * 2) - 1;
freqEmojis = freqEmojis.concat(DEFAULT_EMOJIS).slice(0, quantity);
setItems(freqEmojis);
@ -114,11 +114,14 @@ const Header = React.memo(({
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 (
<View style={[styles.container, { backgroundColor: themes[theme].focusedBackground }]}>
@ -135,11 +138,5 @@ const Header = React.memo(({
</View>
);
});
Header.propTypes = {
handleReaction: PropTypes.func,
server: PropTypes.string,
message: PropTypes.object,
isMasterDetail: PropTypes.bool,
theme: PropTypes.string
};
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 PropTypes from 'prop-types';
import { TouchableOpacity } from 'react-native';
import FastImage from '@rocket.chat/react-native-fast-image';
@ -9,7 +8,16 @@ import { themes } from '../../../constants/colors';
import MessageboxContext from '../Context';
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 { onPressCommandPreview } = context;
const [loading, setLoading] = useState(true);
@ -18,29 +26,21 @@ const Item = ({ item, theme }) => {
<TouchableOpacity
style={styles.commandPreview}
onPress={() => onPressCommandPreview(item)}
testID={`command-preview-item${ item.id }`}
>
{item.type === 'image'
? (
<FastImage
style={styles.commandPreviewImage}
source={{ uri: item.value }}
resizeMode={FastImage.resizeMode.cover}
onLoadStart={() => setLoading(true)}
onLoad={() => setLoading(false)}
>
{ loading ? <ActivityIndicator theme={theme} /> : null }
</FastImage>
)
: <CustomIcon name='attach' size={36} color={themes[theme].actionTintColor} />
}
testID={`command-preview-item${item.id}`}>
{item.type === 'image' ? (
<FastImage
style={styles.commandPreviewImage}
source={{ uri: item.value }}
resizeMode={FastImage.resizeMode.cover}
onLoadStart={() => setLoading(true)}
onLoad={() => setLoading(false)}>
{loading ? <ActivityIndicator theme={theme} /> : null}
</FastImage>
) : (
<CustomIcon name='attach' size={36} color={themes[theme].actionTintColor} />
)}
</TouchableOpacity>
);
};
Item.propTypes = {
item: PropTypes.object,
theme: PropTypes.string
};
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 { View } from 'react-native';
import { KeyboardRegistry } from 'react-native-ui-lib/keyboard';
import PropTypes from 'prop-types';
import store from '../../lib/createStore';
import EmojiPicker from '../EmojiPicker';
@ -9,25 +8,29 @@ import styles from './styles';
import { themes } from '../../constants/colors';
import { withTheme } from '../../theme';
export default class EmojiKeyboard extends React.PureComponent {
static propTypes = {
theme: PropTypes.string
};
interface IMessageBoxEmojiKeyboard {
theme: string;
}
constructor(props) {
export default class EmojiKeyboard extends React.PureComponent<IMessageBoxEmojiKeyboard, any> {
private readonly baseUrl: any;
constructor(props: IMessageBoxEmojiKeyboard) {
super(props);
const state = store.getState();
this.baseUrl = state.share.server.server || state.server.server;
}
onEmojiSelected = (emoji) => {
onEmojiSelected = (emoji: any) => {
KeyboardRegistry.onItemSelected('EmojiKeyboard', { emoji });
}
};
render() {
const { theme } = this.props;
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} />
</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 { TouchableOpacity, Text } from 'react-native';
import PropTypes from 'prop-types';
import { Text, TouchableOpacity } from 'react-native';
import styles from '../styles';
import I18n from '../../../i18n';
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
style={[
styles.mentionItem,
@ -15,8 +22,7 @@ const FixedMentionItem = ({ item, onPress, theme }) => (
borderTopColor: themes[theme].separatorColor
}
]}
onPress={() => onPress(item)}
>
onPress={() => onPress(item)}>
<Text style={[styles.fixedMentionAvatar, { color: themes[theme].titleText }]}>{item.username}</Text>
<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')}
@ -24,10 +30,4 @@ const FixedMentionItem = ({ item, onPress, theme }) => (
</TouchableOpacity>
);
FixedMentionItem.propTypes = {
item: PropTypes.object,
onPress: PropTypes.func,
theme: PropTypes.string
};
export default FixedMentionItem;

View File

@ -6,25 +6,20 @@ import shortnameToUnicode from '../../../utils/shortnameToUnicode';
import styles from '../styles';
import MessageboxContext from '../Context';
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 { baseUrl } = context;
if (item.name) {
return (
<CustomEmoji
style={styles.mentionItemCustomEmoji}
emoji={item}
baseUrl={baseUrl}
/>
);
return <CustomEmoji style={styles.mentionItemCustomEmoji} emoji={item} baseUrl={baseUrl} />;
}
return (
<Text style={styles.mentionItemEmoji}>
{shortnameToUnicode(`:${ item }:`)}
</Text>
);
return <Text style={styles.mentionItemEmoji}>{shortnameToUnicode(`:${item}:`)}</Text>;
};
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 { TouchableOpacity, Text } from 'react-native';
import PropTypes from 'prop-types';
import { Text, TouchableOpacity } from 'react-native';
import styles from '../styles';
import Avatar from '../../Avatar';
import MessageboxContext from '../Context';
import FixedMentionItem from './FixedMentionItem';
import MentionEmoji from './MentionEmoji';
import {
MENTIONS_TRACKING_TYPE_EMOJIS,
MENTIONS_TRACKING_TYPE_COMMANDS
} from '../constants';
import { MENTIONS_TRACKING_TYPE_EMOJIS, MENTIONS_TRACKING_TYPE_COMMANDS, MENTIONS_TRACKING_TYPE_CANNED } from '../constants';
import { themes } from '../../../constants/colors';
import { IEmoji } from '../../EmojiPicker/interfaces';
const MentionItem = ({
item, trackingType, theme
}) => {
interface IMessageBoxMentionItem {
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 { onPressMention } = context;
const defineTestID = (type) => {
const defineTestID = (type: string) => {
switch (type) {
case MENTIONS_TRACKING_TYPE_EMOJIS:
return `mention-item-${ item.name || item }`;
return `mention-item-${item.name || item}`;
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:
return `mention-item-${ item.username || item.name || item }`;
return `mention-item-${item.username || item.name || item}`;
}
};
@ -38,13 +49,8 @@ const MentionItem = ({
let content = (
<>
<Avatar
style={styles.avatar}
text={item.username || item.name}
size={30}
type={item.t}
/>
<Text style={[styles.mentionText, { color: themes[theme].titleText }]}>{ item.username || item.name || item }</Text>
<Avatar style={styles.avatar} 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 = (
<>
<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 (
<TouchableOpacity
style={[
@ -76,17 +93,10 @@ const MentionItem = ({
}
]}
onPress={() => onPressMention(item)}
testID={testID}
>
testID={testID}>
{content}
</TouchableOpacity>
);
};
MentionItem.propTypes = {
item: PropTypes.object,
trackingType: PropTypes.string,
theme: PropTypes.string
};
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 PropTypes from 'prop-types';
import { View, Text } from 'react-native';
import { Text, View } from 'react-native';
import { Audio } from 'expo-av';
import { BorderlessButton } from 'react-native-gesture-handler';
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 I18n from '../../i18n';
import { themes } from '../../constants/colors';
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 = {
android: {
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,
sampleRate: Audio.RECORDING_OPTIONS_PRESET_LOW_QUALITY.android.sampleRate,
numberOfChannels: Audio.RECORDING_OPTIONS_PRESET_LOW_QUALITY.android.numberOfChannels,
@ -34,42 +39,48 @@ const RECORDING_SETTINGS = {
const RECORDING_MODE = {
allowsRecordingIOS: true,
playsInSilentModeIOS: true,
staysActiveInBackground: false,
staysActiveInBackground: true,
shouldDuckAndroid: true,
playThroughEarpieceAndroid: false,
interruptionModeIOS: Audio.INTERRUPTION_MODE_IOS_DO_NOT_MIX,
interruptionModeAndroid: Audio.INTERRUPTION_MODE_ANDROID_DO_NOT_MIX
};
const formatTime = function(seconds) {
let minutes = Math.floor(seconds / 60);
const formatTime = function (seconds: any) {
let minutes: any = Math.floor(seconds / 60);
seconds %= 60;
if (minutes < 10) { minutes = `0${ minutes }`; }
if (seconds < 10) { seconds = `0${ seconds }`; }
return `${ minutes }:${ seconds }`;
if (minutes < 10) {
minutes = `0${minutes}`;
}
if (seconds < 10) {
seconds = `0${seconds}`;
}
return `${minutes}:${seconds}`;
};
export default class RecordAudio extends React.PureComponent {
static propTypes = {
theme: PropTypes.string,
recordingCallback: PropTypes.func,
onFinish: PropTypes.func
}
export default class RecordAudio extends React.PureComponent<IMessageBoxRecordAudioProps, any> {
private isRecorderBusy: boolean;
constructor(props) {
private recording: any;
private LastDuration: number;
constructor(props: IMessageBoxRecordAudioProps) {
super(props);
this.isRecorderBusy = false;
this.LastDuration = 0;
this.state = {
isRecording: false,
isRecorderActive: false,
recordingDurationMillis: 0
};
}
componentDidUpdate() {
const { recordingCallback } = this.props;
const { isRecording } = this.state;
const { isRecorderActive } = this.state;
recordingCallback(isRecording);
recordingCallback(isRecorderActive);
}
componentWillUnmount() {
@ -83,7 +94,11 @@ export default class RecordAudio extends React.PureComponent {
return formatTime(Math.floor(recordingDurationMillis / 1000));
}
isRecordingPermissionGranted = async() => {
get GetLastDuration() {
return formatTime(Math.floor(this.LastDuration / 1000));
}
isRecordingPermissionGranted = async () => {
try {
const permission = await Audio.getPermissionsAsync();
if (permission.status === 'granted') {
@ -94,24 +109,27 @@ export default class RecordAudio extends React.PureComponent {
// Do nothing
}
return false;
}
};
onRecordingStatusUpdate = (status) => {
onRecordingStatusUpdate = (status: any) => {
this.setState({
isRecording: status.isRecording,
recordingDurationMillis: status.durationMillis
});
}
this.LastDuration = status.durationMillis;
};
startRecordingAudio = async() => {
startRecordingAudio = async () => {
logEvent(events.ROOM_AUDIO_RECORD);
if (!this.isRecorderBusy) {
this.isRecorderBusy = true;
this.LastDuration = 0;
try {
const canRecord = await this.isRecordingPermissionGranted();
if (canRecord) {
await Audio.setAudioModeAsync(RECORDING_MODE);
this.setState({ isRecorderActive: true });
this.recording = new Audio.Recording();
await this.recording.prepareToRecordAsync(RECORDING_SETTINGS);
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);
if (!this.isRecorderBusy) {
const { onFinish } = this.props;
@ -140,7 +158,7 @@ export default class RecordAudio extends React.PureComponent {
const fileURI = this.recording.getURI();
const fileData = await getInfoAsync(fileURI);
const fileInfo = {
name: `${ Date.now() }.aac`,
name: `${Date.now()}.m4a`,
mime: 'audio/aac',
type: 'audio/aac',
store: 'Uploads',
@ -152,13 +170,13 @@ export default class RecordAudio extends React.PureComponent {
} catch (error) {
logEvent(events.ROOM_AUDIO_FINISH_F);
}
this.setState({ isRecording: false, recordingDurationMillis: 0 });
this.setState({ isRecording: false, isRecorderActive: false, recordingDurationMillis: 0 });
deactivateKeepAwake();
this.isRecorderBusy = false;
}
};
cancelRecordingAudio = async() => {
cancelRecordingAudio = async () => {
logEvent(events.ROOM_AUDIO_CANCEL);
if (!this.isRecorderBusy) {
this.isRecorderBusy = true;
@ -167,7 +185,7 @@ export default class RecordAudio extends React.PureComponent {
} catch (error) {
logEvent(events.ROOM_AUDIO_CANCEL_F);
}
this.setState({ isRecording: false, recordingDurationMillis: 0 });
this.setState({ isRecording: false, isRecorderActive: false, recordingDurationMillis: 0 });
deactivateKeepAwake();
this.isRecorderBusy = false;
}
@ -175,54 +193,69 @@ export default class RecordAudio extends React.PureComponent {
render() {
const { theme } = this.props;
const { isRecording } = this.state;
const { isRecording, isRecorderActive } = this.state;
if (!isRecording) {
if (!isRecording && !isRecorderActive) {
return (
<BorderlessButton
onPress={this.startRecordingAudio}
style={styles.actionButton}
testID='messagebox-send-audio'
// @ts-ignore
accessibilityLabel={I18n.t('Send_audio_message')}
accessibilityTraits='button'
>
accessibilityTraits='button'>
<CustomIcon name='microphone' size={24} color={themes[theme].auxiliaryTintColor} />
</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 (
<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='close'
/>
style={styles.actionButton}>
<CustomIcon size={24} color={themes[theme].dangerColor} name='delete' />
</BorderlessButton>
<Text
style={[styles.recordingCancelText, { color: themes[theme].titleText }]}
>
{this.duration}
</Text>
<Text style={[styles.recordingDurationText, { color: themes[theme].titleText }]}>{this.duration}</Text>
<CustomIcon size={24} color={themes[theme].dangerColor} name='record' />
</View>
<BorderlessButton
onPress={this.finishRecordingAudio}
// @ts-ignore
accessibilityLabel={I18n.t('Finish_recording')}
accessibilityTraits='button'
style={styles.actionButton}
>
<CustomIcon
size={24}
color={themes[theme].successColor}
name='check'
/>
style={styles.actionButton}>
<CustomIcon size={24} color={themes[theme].tintColor} name='send-filled' />
</BorderlessButton>
</View>
);

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