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 commitd6200745c0
. * 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 commitfa00ef92ef
. * [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:
parent
00b4c9af33
commit
4db5db7fa0
|
@ -340,7 +340,7 @@ jobs:
|
||||||
<<: *defaults
|
<<: *defaults
|
||||||
docker:
|
docker:
|
||||||
- image: circleci/node:15
|
- image: circleci/node:15
|
||||||
|
resource_class: large
|
||||||
environment:
|
environment:
|
||||||
CODECOV_TOKEN: caa771ab-3d45-4756-8e2a-e1f25996fef6
|
CODECOV_TOKEN: caa771ab-3d45-4756-8e2a-e1f25996fef6
|
||||||
|
|
||||||
|
@ -376,6 +376,7 @@ jobs:
|
||||||
environment:
|
environment:
|
||||||
<<: *android-env
|
<<: *android-env
|
||||||
<<: *bash-env
|
<<: *bash-env
|
||||||
|
resource_class: large
|
||||||
steps:
|
steps:
|
||||||
- android-build
|
- android-build
|
||||||
|
|
||||||
|
@ -386,6 +387,7 @@ jobs:
|
||||||
environment:
|
environment:
|
||||||
<<: *android-env
|
<<: *android-env
|
||||||
<<: *bash-env
|
<<: *bash-env
|
||||||
|
resource_class: large
|
||||||
steps:
|
steps:
|
||||||
- android-build
|
- android-build
|
||||||
|
|
||||||
|
|
381
.eslintrc.js
381
.eslintrc.js
|
@ -1,161 +1,156 @@
|
||||||
module.exports = {
|
module.exports = {
|
||||||
"settings": {
|
settings: {
|
||||||
"import/resolver": {
|
'import/resolver': {
|
||||||
"node": {
|
node: {
|
||||||
"extensions": [".js", ".ios.js", ".android.js", ".native.js", ".tsx"]
|
extensions: ['.js', '.ios.js', '.android.js', '.native.js', '.ts', '.tsx']
|
||||||
}
|
}
|
||||||
}
|
|
||||||
},
|
|
||||||
"parser": "@babel/eslint-parser",
|
|
||||||
"extends": "airbnb",
|
|
||||||
"parserOptions": {
|
|
||||||
"sourceType": "module",
|
|
||||||
"ecmaVersion": 2017,
|
|
||||||
"ecmaFeatures": {
|
|
||||||
"experimentalObjectRestSpread" : true,
|
|
||||||
"jsx": true,
|
|
||||||
"legacyDecorators": true
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"plugins": [
|
parser: '@babel/eslint-parser',
|
||||||
"react",
|
extends: ['@rocket.chat/eslint-config', 'prettier'],
|
||||||
"jsx-a11y",
|
parserOptions: {
|
||||||
"import",
|
sourceType: 'module',
|
||||||
"react-native",
|
ecmaVersion: 2017,
|
||||||
"@babel"
|
ecmaFeatures: {
|
||||||
],
|
experimentalObjectRestSpread: true,
|
||||||
"env": {
|
jsx: true,
|
||||||
"browser": true,
|
legacyDecorators: true
|
||||||
"commonjs": true,
|
}
|
||||||
"es6": true,
|
|
||||||
"node": true,
|
|
||||||
"jquery": true,
|
|
||||||
"mocha": true
|
|
||||||
},
|
},
|
||||||
"rules": {
|
plugins: ['react', 'jsx-a11y', 'import', 'react-native', '@babel'],
|
||||||
"react/jsx-filename-extension": [1, {
|
env: {
|
||||||
"extensions": [".js", ".jsx"]
|
browser: true,
|
||||||
}],
|
commonjs: true,
|
||||||
"react/require-default-props": [0],
|
es6: true,
|
||||||
"react/no-unused-prop-types": [2, {
|
node: true,
|
||||||
"skipShapeProps": true
|
jquery: true,
|
||||||
}],
|
mocha: true
|
||||||
"react/no-did-mount-set-state": 0,
|
|
||||||
"react/no-multi-comp": [0],
|
|
||||||
"react/jsx-indent": [2, "tab"],
|
|
||||||
"react/jsx-indent-props": [2, "tab"],
|
|
||||||
"react/forbid-prop-types": 0,
|
|
||||||
"jsx-quotes": [2, "prefer-single"],
|
|
||||||
"jsx-a11y/href-no-hash": 0,
|
|
||||||
"jsx-a11y/aria-role": 0,
|
|
||||||
"import/prefer-default-export": 0,
|
|
||||||
"import/no-cycle": 0,
|
|
||||||
"camelcase": 0,
|
|
||||||
"no-underscore-dangle": 0,
|
|
||||||
"no-return-assign": 0,
|
|
||||||
"no-param-reassign": 0,
|
|
||||||
"no-tabs": 0,
|
|
||||||
"no-multi-spaces": 2,
|
|
||||||
"no-eval": 2,
|
|
||||||
"no-extend-native": 2,
|
|
||||||
"no-multi-str": 2,
|
|
||||||
"no-use-before-define": 2,
|
|
||||||
"no-const-assign": 2,
|
|
||||||
"no-cond-assign": 2,
|
|
||||||
"no-constant-condition": 2,
|
|
||||||
"no-control-regex": 2,
|
|
||||||
"no-debugger": 2,
|
|
||||||
"no-delete-var": 2,
|
|
||||||
"no-dupe-keys": 2,
|
|
||||||
"no-dupe-args": 2,
|
|
||||||
"no-dupe-class-members": 2,
|
|
||||||
"no-duplicate-case": 2,
|
|
||||||
"no-else-return": [0, {allowElseIf: true}],
|
|
||||||
"no-empty": 2,
|
|
||||||
"no-empty-character-class": 2,
|
|
||||||
"no-ex-assign": 2,
|
|
||||||
"no-extra-boolean-cast": 2,
|
|
||||||
"no-extra-semi": 2,
|
|
||||||
"no-fallthrough": 2,
|
|
||||||
"no-func-assign": 2,
|
|
||||||
"no-inner-declarations": [2, "functions"],
|
|
||||||
"no-invalid-regexp": 2,
|
|
||||||
"no-irregular-whitespace": 2,
|
|
||||||
"no-mixed-spaces-and-tabs": 2,
|
|
||||||
"no-sparse-arrays": 2,
|
|
||||||
"no-negated-in-lhs": 2,
|
|
||||||
"no-obj-calls": 2,
|
|
||||||
"no-octal": 2,
|
|
||||||
"no-redeclare": 2,
|
|
||||||
"no-regex-spaces": 2,
|
|
||||||
"no-undef": 2,
|
|
||||||
"no-unreachable": 2,
|
|
||||||
"no-unused-expressions": 0,
|
|
||||||
"no-unused-vars": [2, {
|
|
||||||
"vars": "all",
|
|
||||||
"args": "after-used"
|
|
||||||
}],
|
|
||||||
"max-len": 0,
|
|
||||||
"react/jsx-uses-vars": 2,
|
|
||||||
"no-void": 2,
|
|
||||||
"no-var": 2,
|
|
||||||
"one-var": [2, "never"],
|
|
||||||
"no-lonely-if": 2,
|
|
||||||
"no-trailing-spaces": 2,
|
|
||||||
"complexity": [1, 31],
|
|
||||||
"space-in-parens": [2, "never"],
|
|
||||||
"space-before-function-paren": [2, "never"],
|
|
||||||
"space-before-blocks": [2, "always"],
|
|
||||||
"indent": [2, "tab", {"SwitchCase": 1}],
|
|
||||||
"eol-last": [2, "always"],
|
|
||||||
"comma-dangle": [2, "never"],
|
|
||||||
"keyword-spacing": 2,
|
|
||||||
"block-spacing": 2,
|
|
||||||
"brace-style": [2, "1tbs", { "allowSingleLine": true }],
|
|
||||||
"computed-property-spacing": 2,
|
|
||||||
"comma-spacing": 2,
|
|
||||||
"comma-style": 2,
|
|
||||||
"guard-for-in": 2,
|
|
||||||
"wrap-iife": 2,
|
|
||||||
"block-scoped-var": 2,
|
|
||||||
"curly": [2, "all"],
|
|
||||||
"eqeqeq": [2, "allow-null"],
|
|
||||||
"new-cap": [2],
|
|
||||||
"use-isnan": 2,
|
|
||||||
"valid-typeof": 2,
|
|
||||||
"linebreak-style": 0,
|
|
||||||
"prefer-template": 2,
|
|
||||||
"template-curly-spacing": [2, "always"],
|
|
||||||
"quotes": [2, "single"],
|
|
||||||
"semi": [2, "always"],
|
|
||||||
"prefer-const": 2,
|
|
||||||
"object-shorthand": 2,
|
|
||||||
"consistent-return": 0,
|
|
||||||
"global-require": "off",
|
|
||||||
"react-native/no-unused-styles": 2,
|
|
||||||
"react/jsx-one-expression-per-line": 0,
|
|
||||||
"require-await": 2,
|
|
||||||
"func-names": 0,
|
|
||||||
"react/sort-comp": ["error", {
|
|
||||||
"order": [
|
|
||||||
"static-variables",
|
|
||||||
"static-methods",
|
|
||||||
"lifecycle",
|
|
||||||
"everything-else",
|
|
||||||
"render"
|
|
||||||
]
|
|
||||||
}],
|
|
||||||
"react/static-property-placement": [0],
|
|
||||||
"arrow-parens": ["error", "as-needed", { requireForBlockBody: true }],
|
|
||||||
"react/jsx-props-no-spreading": [1],
|
|
||||||
"react/jsx-curly-newline": [0],
|
|
||||||
"react/state-in-constructor": [0],
|
|
||||||
"no-async-promise-executor": [0],
|
|
||||||
"max-classes-per-file": [0],
|
|
||||||
"no-multiple-empty-lines": [0]
|
|
||||||
},
|
},
|
||||||
"globals": {
|
rules: {
|
||||||
"__DEV__": true
|
'import/extensions': [
|
||||||
|
'error',
|
||||||
|
'ignorePackages',
|
||||||
|
{
|
||||||
|
js: 'warning',
|
||||||
|
jsx: 'warning',
|
||||||
|
ts: 'warning',
|
||||||
|
tsx: 'warning'
|
||||||
|
}
|
||||||
|
],
|
||||||
|
'react/jsx-filename-extension': [
|
||||||
|
1,
|
||||||
|
{
|
||||||
|
extensions: ['.js', '.jsx', '.ts', '.tsx']
|
||||||
|
}
|
||||||
|
],
|
||||||
|
'react/require-default-props': [0],
|
||||||
|
'ordered-imports': [0],
|
||||||
|
'react/no-did-mount-set-state': 0,
|
||||||
|
'react/no-multi-comp': [0],
|
||||||
|
'react/jsx-indent-props': [2, 'tab'],
|
||||||
|
'jsx-quotes': [2, 'prefer-single'],
|
||||||
|
'jsx-a11y/href-no-hash': 0,
|
||||||
|
'jsx-a11y/aria-role': 0,
|
||||||
|
'import/prefer-default-export': 0,
|
||||||
|
'import/no-cycle': 0,
|
||||||
|
'import/order': [
|
||||||
|
'error',
|
||||||
|
{
|
||||||
|
'newlines-between': 'ignore'
|
||||||
|
}
|
||||||
|
],
|
||||||
|
camelcase: 0,
|
||||||
|
'no-underscore-dangle': 0,
|
||||||
|
'no-return-assign': 0,
|
||||||
|
'no-param-reassign': 0,
|
||||||
|
'no-tabs': 0,
|
||||||
|
'no-multi-spaces': 2,
|
||||||
|
'no-eval': 2,
|
||||||
|
'no-extend-native': 2,
|
||||||
|
'no-multi-str': 2,
|
||||||
|
'no-use-before-define': 2,
|
||||||
|
'no-const-assign': 2,
|
||||||
|
'no-cond-assign': 2,
|
||||||
|
'no-constant-condition': 2,
|
||||||
|
'no-control-regex': 2,
|
||||||
|
'no-debugger': 2,
|
||||||
|
'no-delete-var': 2,
|
||||||
|
'no-dupe-keys': 2,
|
||||||
|
'no-dupe-args': 2,
|
||||||
|
'no-dupe-class-members': 2,
|
||||||
|
'no-duplicate-case': 2,
|
||||||
|
'no-else-return': [0, { allowElseIf: true }],
|
||||||
|
'no-empty': 2,
|
||||||
|
'no-empty-character-class': 2,
|
||||||
|
'no-ex-assign': 2,
|
||||||
|
'no-extra-boolean-cast': 2,
|
||||||
|
'no-extra-semi': 2,
|
||||||
|
'no-fallthrough': 2,
|
||||||
|
'no-func-assign': 2,
|
||||||
|
'no-inner-declarations': [2, 'functions'],
|
||||||
|
'no-invalid-regexp': 2,
|
||||||
|
'no-irregular-whitespace': 2,
|
||||||
|
'no-mixed-spaces-and-tabs': 1,
|
||||||
|
'no-sparse-arrays': 2,
|
||||||
|
'no-negated-in-lhs': 2,
|
||||||
|
'no-obj-calls': 2,
|
||||||
|
'no-octal': 2,
|
||||||
|
'no-redeclare': 2,
|
||||||
|
'no-regex-spaces': 2,
|
||||||
|
'no-undef': 2,
|
||||||
|
'no-unreachable': 2,
|
||||||
|
'no-unused-expressions': 0,
|
||||||
|
'no-unused-vars': 'off',
|
||||||
|
'max-len': 0,
|
||||||
|
'react/jsx-uses-vars': 2,
|
||||||
|
'no-void': 2,
|
||||||
|
'no-var': 2,
|
||||||
|
'one-var': [2, 'never'],
|
||||||
|
'no-lonely-if': 2,
|
||||||
|
'no-trailing-spaces': 2,
|
||||||
|
complexity: [1, 31],
|
||||||
|
'space-in-parens': [2, 'never'],
|
||||||
|
'space-before-blocks': [2, 'always'],
|
||||||
|
indent: 'off',
|
||||||
|
'eol-last': [2, 'always'],
|
||||||
|
'comma-dangle': [2, 'never'],
|
||||||
|
'keyword-spacing': 2,
|
||||||
|
'block-spacing': 2,
|
||||||
|
'brace-style': [2, '1tbs', { allowSingleLine: true }],
|
||||||
|
'computed-property-spacing': 2,
|
||||||
|
'comma-spacing': 2,
|
||||||
|
'comma-style': 2,
|
||||||
|
'guard-for-in': 2,
|
||||||
|
'wrap-iife': 2,
|
||||||
|
'block-scoped-var': 2,
|
||||||
|
curly: [2, 'all'],
|
||||||
|
eqeqeq: [2, 'allow-null'],
|
||||||
|
'new-cap': 'off',
|
||||||
|
'use-isnan': 2,
|
||||||
|
'valid-typeof': 2,
|
||||||
|
'linebreak-style': 0,
|
||||||
|
'prefer-template': 2,
|
||||||
|
quotes: [1, 'single'],
|
||||||
|
semi: [2, 'always'],
|
||||||
|
'prefer-const': 2,
|
||||||
|
'object-shorthand': 2,
|
||||||
|
'consistent-return': 0,
|
||||||
|
'global-require': 'off',
|
||||||
|
'react-native/no-unused-styles': 2,
|
||||||
|
'react/jsx-one-expression-per-line': 0,
|
||||||
|
'require-await': 2,
|
||||||
|
'func-names': 0,
|
||||||
|
'react/static-property-placement': [0],
|
||||||
|
'arrow-parens': ['warn', 'as-needed', { requireForBlockBody: true }],
|
||||||
|
'react/jsx-curly-newline': [0],
|
||||||
|
'react/state-in-constructor': [0],
|
||||||
|
'no-async-promise-executor': [0],
|
||||||
|
'max-classes-per-file': [0],
|
||||||
|
'no-multiple-empty-lines': [0],
|
||||||
|
'no-sequences': 'off'
|
||||||
|
},
|
||||||
|
globals: {
|
||||||
|
__DEV__: true
|
||||||
},
|
},
|
||||||
overrides: [
|
overrides: [
|
||||||
{
|
{
|
||||||
|
@ -173,6 +168,86 @@ module.exports = {
|
||||||
'no-await-in-loop': 0,
|
'no-await-in-loop': 0,
|
||||||
'no-restricted-syntax': 0
|
'no-restricted-syntax': 0
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
files: ['**/*.ts', '**/*.tsx'],
|
||||||
|
extends: [
|
||||||
|
'plugin:@typescript-eslint/recommended',
|
||||||
|
'plugin:@typescript-eslint/eslint-recommended',
|
||||||
|
'@rocket.chat/eslint-config',
|
||||||
|
'prettier'
|
||||||
|
],
|
||||||
|
parser: '@typescript-eslint/parser',
|
||||||
|
parserOptions: {
|
||||||
|
sourceType: 'module',
|
||||||
|
ecmaVersion: 2018,
|
||||||
|
warnOnUnsupportedTypeScriptVersion: false,
|
||||||
|
ecmaFeatures: {
|
||||||
|
experimentalObjectRestSpread: true,
|
||||||
|
legacyDecorators: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
plugins: ['react', '@typescript-eslint'],
|
||||||
|
rules: {
|
||||||
|
'@typescript-eslint/no-var-requires': 'off',
|
||||||
|
'@typescript-eslint/no-empty-function': [0],
|
||||||
|
'@typescript-eslint/ban-types': [0],
|
||||||
|
'func-call-spacing': 'off',
|
||||||
|
'jsx-quotes': ['error', 'prefer-single'],
|
||||||
|
indent: 'off',
|
||||||
|
'comma-dangle': [2, 'never'],
|
||||||
|
'no-return-assign': 0,
|
||||||
|
'no-dupe-class-members': 'off',
|
||||||
|
'no-extra-parens': 'off',
|
||||||
|
'no-spaced-func': 'off',
|
||||||
|
'no-unused-vars': 'off',
|
||||||
|
'no-useless-constructor': 'off',
|
||||||
|
'no-use-before-define': 'off',
|
||||||
|
'react/jsx-uses-react': 'error',
|
||||||
|
'react/jsx-uses-vars': 'error',
|
||||||
|
'react/jsx-no-undef': 'error',
|
||||||
|
'react/jsx-fragments': ['error', 'syntax'],
|
||||||
|
'@typescript-eslint/ban-ts-comment': 'off',
|
||||||
|
'@typescript-eslint/indent': [
|
||||||
|
'warn',
|
||||||
|
'tab',
|
||||||
|
{
|
||||||
|
SwitchCase: 1
|
||||||
|
}
|
||||||
|
],
|
||||||
|
'@typescript-eslint/no-extra-parens': [
|
||||||
|
'warn',
|
||||||
|
'all',
|
||||||
|
{
|
||||||
|
conditionalAssign: true,
|
||||||
|
nestedBinaryExpressions: false,
|
||||||
|
returnAssign: true,
|
||||||
|
ignoreJSX: 'all',
|
||||||
|
enforceForArrowConditionals: false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
'@typescript-eslint/no-dupe-class-members': 'error',
|
||||||
|
'@typescript-eslint/no-explicit-any': 'off',
|
||||||
|
'@typescript-eslint/no-unused-vars': [
|
||||||
|
'error',
|
||||||
|
{
|
||||||
|
argsIgnorePattern: '^_',
|
||||||
|
ignoreRestSiblings: true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
'new-cap': 'off',
|
||||||
|
'lines-between-class-members': 'off'
|
||||||
|
},
|
||||||
|
globals: {
|
||||||
|
JSX: true
|
||||||
|
},
|
||||||
|
settings: {
|
||||||
|
'import/resolver': {
|
||||||
|
node: {
|
||||||
|
extensions: ['.js', '.ts', '.tsx']
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
};
|
};
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -0,0 +1,10 @@
|
||||||
|
module.exports = {
|
||||||
|
bracketSpacing: true,
|
||||||
|
jsxBracketSameLine: true,
|
||||||
|
singleQuote: true,
|
||||||
|
jsxSingleQuote: true,
|
||||||
|
trailingComma: 'none',
|
||||||
|
printWidth: 130,
|
||||||
|
useTabs: true,
|
||||||
|
arrowParens: 'avoid'
|
||||||
|
};
|
|
@ -54,6 +54,18 @@ To check for lint issues on your code, run this on your terminal:
|
||||||
yarn lint
|
yarn lint
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Code formatting
|
||||||
|
|
||||||
|
We use [Prettier](https://prettier.io) to format the code style in our project. We have a pre-commit hook enforcing commits to follow our style guides.
|
||||||
|
|
||||||
|
To fix your code formatting issues, run this on your terminal:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
yarn prettier
|
||||||
|
```
|
||||||
|
|
||||||
|
[Check this link](https://prettier.io/docs/en/editors.html) to see how to integrate Prettier with your preferred code editor, and run Prettier when save your file for example.
|
||||||
|
|
||||||
## Tests
|
## Tests
|
||||||
|
|
||||||
It's always important to ensure everything is working properly and that's why tests are great. We have unit and e2e tests on this project.
|
It's always important to ensure everything is working properly and that's why tests are great. We have unit and e2e tests on this project.
|
||||||
|
|
|
@ -31,7 +31,7 @@ Also check the [#react-native](https://open.rocket.chat/channel/react-native) co
|
||||||
Are you a dev and would like to help? Found a bug that you would like to report or a missing feature that you would like to work on? Great! We have written down a [Contribution guide](https://github.com/RocketChat/Rocket.Chat.ReactNative/blob/develop/CONTRIBUTING.md) so you can start easily.
|
Are you a dev and would like to help? Found a bug that you would like to report or a missing feature that you would like to work on? Great! We have written down a [Contribution guide](https://github.com/RocketChat/Rocket.Chat.ReactNative/blob/develop/CONTRIBUTING.md) so you can start easily.
|
||||||
|
|
||||||
## Whitelabel
|
## Whitelabel
|
||||||
Do you want to make the app run on your own server only? [Follow our whitelabel documentation.](https://docs.rocket.chat/guides/developer/mobile-apps/whitelabeling-mobile-apps)
|
Do you want to make the app run on your own server only? [Follow our whitelabel documentation.](https://developer.rocket.chat/mobile-app/mobile-app-white-labelling)
|
||||||
|
|
||||||
## Engage with us
|
## Engage with us
|
||||||
### Share your story
|
### Share your story
|
||||||
|
|
|
@ -899,27 +899,22 @@ exports[`Storyshots BackgroundContainer black theme - loading 1`] = `
|
||||||
</View>
|
</View>
|
||||||
<ActivityIndicator
|
<ActivityIndicator
|
||||||
animating={true}
|
animating={true}
|
||||||
color="#999999"
|
color="#f9f9f9"
|
||||||
hidesWhenStopped={true}
|
hidesWhenStopped={true}
|
||||||
size="small"
|
size="small"
|
||||||
style={
|
style={
|
||||||
Array [
|
Object {
|
||||||
Object {
|
"backgroundColor": "transparent",
|
||||||
"backgroundColor": "transparent",
|
"fontFamily": "System",
|
||||||
"fontFamily": "System",
|
"fontSize": 16,
|
||||||
"fontSize": 16,
|
"fontWeight": "400",
|
||||||
"fontWeight": "400",
|
"left": 0,
|
||||||
"left": 0,
|
"paddingHorizontal": 24,
|
||||||
"paddingHorizontal": 24,
|
"position": "absolute",
|
||||||
"position": "absolute",
|
"right": 0,
|
||||||
"right": 0,
|
"textAlign": "center",
|
||||||
"textAlign": "center",
|
"top": 60,
|
||||||
"top": 60,
|
}
|
||||||
},
|
|
||||||
Object {
|
|
||||||
"color": "#f9f9f9",
|
|
||||||
},
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
|
@ -1037,27 +1032,22 @@ exports[`Storyshots BackgroundContainer dark theme - loading 1`] = `
|
||||||
</View>
|
</View>
|
||||||
<ActivityIndicator
|
<ActivityIndicator
|
||||||
animating={true}
|
animating={true}
|
||||||
color="#999999"
|
color="#f9f9f9"
|
||||||
hidesWhenStopped={true}
|
hidesWhenStopped={true}
|
||||||
size="small"
|
size="small"
|
||||||
style={
|
style={
|
||||||
Array [
|
Object {
|
||||||
Object {
|
"backgroundColor": "transparent",
|
||||||
"backgroundColor": "transparent",
|
"fontFamily": "System",
|
||||||
"fontFamily": "System",
|
"fontSize": 16,
|
||||||
"fontSize": 16,
|
"fontWeight": "400",
|
||||||
"fontWeight": "400",
|
"left": 0,
|
||||||
"left": 0,
|
"paddingHorizontal": 24,
|
||||||
"paddingHorizontal": 24,
|
"position": "absolute",
|
||||||
"position": "absolute",
|
"right": 0,
|
||||||
"right": 0,
|
"textAlign": "center",
|
||||||
"textAlign": "center",
|
"top": 60,
|
||||||
"top": 60,
|
}
|
||||||
},
|
|
||||||
Object {
|
|
||||||
"color": "#f9f9f9",
|
|
||||||
},
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
|
@ -1175,27 +1165,22 @@ exports[`Storyshots BackgroundContainer loading 1`] = `
|
||||||
</View>
|
</View>
|
||||||
<ActivityIndicator
|
<ActivityIndicator
|
||||||
animating={true}
|
animating={true}
|
||||||
color="#999999"
|
color="#6C727A"
|
||||||
hidesWhenStopped={true}
|
hidesWhenStopped={true}
|
||||||
size="small"
|
size="small"
|
||||||
style={
|
style={
|
||||||
Array [
|
Object {
|
||||||
Object {
|
"backgroundColor": "transparent",
|
||||||
"backgroundColor": "transparent",
|
"fontFamily": "System",
|
||||||
"fontFamily": "System",
|
"fontSize": 16,
|
||||||
"fontSize": 16,
|
"fontWeight": "400",
|
||||||
"fontWeight": "400",
|
"left": 0,
|
||||||
"left": 0,
|
"paddingHorizontal": 24,
|
||||||
"paddingHorizontal": 24,
|
"position": "absolute",
|
||||||
"position": "absolute",
|
"right": 0,
|
||||||
"right": 0,
|
"textAlign": "center",
|
||||||
"textAlign": "center",
|
"top": 60,
|
||||||
"top": 60,
|
}
|
||||||
},
|
|
||||||
Object {
|
|
||||||
"color": "#6C727A",
|
|
||||||
},
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
|
@ -1337,6 +1322,595 @@ exports[`Storyshots BackgroundContainer text 1`] = `
|
||||||
</View>
|
</View>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
exports[`Storyshots CannedResponseItem Itens 1`] = `
|
||||||
|
Array [
|
||||||
|
<View
|
||||||
|
accessible={true}
|
||||||
|
focusable={true}
|
||||||
|
onClick={[Function]}
|
||||||
|
onResponderGrant={[Function]}
|
||||||
|
onResponderMove={[Function]}
|
||||||
|
onResponderRelease={[Function]}
|
||||||
|
onResponderTerminate={[Function]}
|
||||||
|
onResponderTerminationRequest={[Function]}
|
||||||
|
onStartShouldSetResponder={[Function]}
|
||||||
|
style={
|
||||||
|
Object {
|
||||||
|
"backgroundColor": "#ffffff",
|
||||||
|
"maxHeight": 141,
|
||||||
|
"minHeight": 117,
|
||||||
|
"opacity": 1,
|
||||||
|
"padding": 16,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<View
|
||||||
|
style={
|
||||||
|
Object {
|
||||||
|
"flexDirection": "row",
|
||||||
|
"height": 36,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<View
|
||||||
|
style={
|
||||||
|
Object {
|
||||||
|
"flex": 1,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Text
|
||||||
|
style={
|
||||||
|
Array [
|
||||||
|
Object {
|
||||||
|
"backgroundColor": "transparent",
|
||||||
|
"flex": 1,
|
||||||
|
"fontFamily": "System",
|
||||||
|
"fontSize": 14,
|
||||||
|
"fontWeight": "500",
|
||||||
|
"paddingBottom": 0,
|
||||||
|
"paddingTop": 0,
|
||||||
|
"textAlign": "left",
|
||||||
|
},
|
||||||
|
Object {
|
||||||
|
"color": "#0d0e12",
|
||||||
|
},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
>
|
||||||
|
!
|
||||||
|
!FAQ4
|
||||||
|
</Text>
|
||||||
|
<Text
|
||||||
|
style={
|
||||||
|
Array [
|
||||||
|
Object {
|
||||||
|
"backgroundColor": "transparent",
|
||||||
|
"flex": 1,
|
||||||
|
"fontFamily": "System",
|
||||||
|
"fontSize": 12,
|
||||||
|
"fontWeight": "400",
|
||||||
|
"paddingBottom": 0,
|
||||||
|
"paddingTop": 0,
|
||||||
|
"textAlign": "left",
|
||||||
|
},
|
||||||
|
Object {
|
||||||
|
"color": "#6C727A",
|
||||||
|
},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
>
|
||||||
|
Private
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
<View
|
||||||
|
accessible={true}
|
||||||
|
focusable={true}
|
||||||
|
onClick={[Function]}
|
||||||
|
onResponderGrant={[Function]}
|
||||||
|
onResponderMove={[Function]}
|
||||||
|
onResponderRelease={[Function]}
|
||||||
|
onResponderTerminate={[Function]}
|
||||||
|
onResponderTerminationRequest={[Function]}
|
||||||
|
onStartShouldSetResponder={[Function]}
|
||||||
|
style={
|
||||||
|
Object {
|
||||||
|
"backgroundColor": "#f3f4f5",
|
||||||
|
"borderRadius": 2,
|
||||||
|
"height": 28,
|
||||||
|
"justifyContent": "center",
|
||||||
|
"marginBottom": 12,
|
||||||
|
"marginLeft": 8,
|
||||||
|
"opacity": 1,
|
||||||
|
"paddingHorizontal": 14,
|
||||||
|
"width": 56,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Text
|
||||||
|
accessibilityLabel="Use"
|
||||||
|
style={
|
||||||
|
Array [
|
||||||
|
Object {
|
||||||
|
"backgroundColor": "transparent",
|
||||||
|
"fontFamily": "System",
|
||||||
|
"fontSize": 16,
|
||||||
|
"fontWeight": "500",
|
||||||
|
"textAlign": "center",
|
||||||
|
},
|
||||||
|
Object {
|
||||||
|
"color": "#0d0e12",
|
||||||
|
},
|
||||||
|
Object {
|
||||||
|
"fontSize": 12,
|
||||||
|
},
|
||||||
|
undefined,
|
||||||
|
]
|
||||||
|
}
|
||||||
|
>
|
||||||
|
Use
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
<Text
|
||||||
|
ellipsizeMode="tail"
|
||||||
|
numberOfLines={2}
|
||||||
|
style={
|
||||||
|
Array [
|
||||||
|
Object {
|
||||||
|
"backgroundColor": "transparent",
|
||||||
|
"fontFamily": "System",
|
||||||
|
"fontSize": 14,
|
||||||
|
"fontWeight": "400",
|
||||||
|
"marginTop": 8,
|
||||||
|
"paddingBottom": 0,
|
||||||
|
"paddingTop": 0,
|
||||||
|
"textAlign": "left",
|
||||||
|
},
|
||||||
|
Object {
|
||||||
|
"color": "#6C727A",
|
||||||
|
},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
>
|
||||||
|
“
|
||||||
|
ZCVXZVXCZVZXVZXCVZXCVXZCVZX
|
||||||
|
”
|
||||||
|
</Text>
|
||||||
|
<View
|
||||||
|
style={
|
||||||
|
Object {
|
||||||
|
"flexDirection": "row",
|
||||||
|
"overflow": "hidden",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</View>,
|
||||||
|
<View
|
||||||
|
accessible={true}
|
||||||
|
focusable={true}
|
||||||
|
onClick={[Function]}
|
||||||
|
onResponderGrant={[Function]}
|
||||||
|
onResponderMove={[Function]}
|
||||||
|
onResponderRelease={[Function]}
|
||||||
|
onResponderTerminate={[Function]}
|
||||||
|
onResponderTerminationRequest={[Function]}
|
||||||
|
onStartShouldSetResponder={[Function]}
|
||||||
|
style={
|
||||||
|
Object {
|
||||||
|
"backgroundColor": "#ffffff",
|
||||||
|
"maxHeight": 141,
|
||||||
|
"minHeight": 117,
|
||||||
|
"opacity": 1,
|
||||||
|
"padding": 16,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<View
|
||||||
|
style={
|
||||||
|
Object {
|
||||||
|
"flexDirection": "row",
|
||||||
|
"height": 36,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<View
|
||||||
|
style={
|
||||||
|
Object {
|
||||||
|
"flex": 1,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Text
|
||||||
|
style={
|
||||||
|
Array [
|
||||||
|
Object {
|
||||||
|
"backgroundColor": "transparent",
|
||||||
|
"flex": 1,
|
||||||
|
"fontFamily": "System",
|
||||||
|
"fontSize": 14,
|
||||||
|
"fontWeight": "500",
|
||||||
|
"paddingBottom": 0,
|
||||||
|
"paddingTop": 0,
|
||||||
|
"textAlign": "left",
|
||||||
|
},
|
||||||
|
Object {
|
||||||
|
"color": "#0d0e12",
|
||||||
|
},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
>
|
||||||
|
!
|
||||||
|
test4mobilePrivate
|
||||||
|
</Text>
|
||||||
|
<Text
|
||||||
|
style={
|
||||||
|
Array [
|
||||||
|
Object {
|
||||||
|
"backgroundColor": "transparent",
|
||||||
|
"flex": 1,
|
||||||
|
"fontFamily": "System",
|
||||||
|
"fontSize": 12,
|
||||||
|
"fontWeight": "400",
|
||||||
|
"paddingBottom": 0,
|
||||||
|
"paddingTop": 0,
|
||||||
|
"textAlign": "left",
|
||||||
|
},
|
||||||
|
Object {
|
||||||
|
"color": "#6C727A",
|
||||||
|
},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
>
|
||||||
|
Private
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
<View
|
||||||
|
accessible={true}
|
||||||
|
focusable={true}
|
||||||
|
onClick={[Function]}
|
||||||
|
onResponderGrant={[Function]}
|
||||||
|
onResponderMove={[Function]}
|
||||||
|
onResponderRelease={[Function]}
|
||||||
|
onResponderTerminate={[Function]}
|
||||||
|
onResponderTerminationRequest={[Function]}
|
||||||
|
onStartShouldSetResponder={[Function]}
|
||||||
|
style={
|
||||||
|
Object {
|
||||||
|
"backgroundColor": "#f3f4f5",
|
||||||
|
"borderRadius": 2,
|
||||||
|
"height": 28,
|
||||||
|
"justifyContent": "center",
|
||||||
|
"marginBottom": 12,
|
||||||
|
"marginLeft": 8,
|
||||||
|
"opacity": 1,
|
||||||
|
"paddingHorizontal": 14,
|
||||||
|
"width": 56,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Text
|
||||||
|
accessibilityLabel="Use"
|
||||||
|
style={
|
||||||
|
Array [
|
||||||
|
Object {
|
||||||
|
"backgroundColor": "transparent",
|
||||||
|
"fontFamily": "System",
|
||||||
|
"fontSize": 16,
|
||||||
|
"fontWeight": "500",
|
||||||
|
"textAlign": "center",
|
||||||
|
},
|
||||||
|
Object {
|
||||||
|
"color": "#0d0e12",
|
||||||
|
},
|
||||||
|
Object {
|
||||||
|
"fontSize": 12,
|
||||||
|
},
|
||||||
|
undefined,
|
||||||
|
]
|
||||||
|
}
|
||||||
|
>
|
||||||
|
Use
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
<Text
|
||||||
|
ellipsizeMode="tail"
|
||||||
|
numberOfLines={2}
|
||||||
|
style={
|
||||||
|
Array [
|
||||||
|
Object {
|
||||||
|
"backgroundColor": "transparent",
|
||||||
|
"fontFamily": "System",
|
||||||
|
"fontSize": 14,
|
||||||
|
"fontWeight": "400",
|
||||||
|
"marginTop": 8,
|
||||||
|
"paddingBottom": 0,
|
||||||
|
"paddingTop": 0,
|
||||||
|
"textAlign": "left",
|
||||||
|
},
|
||||||
|
Object {
|
||||||
|
"color": "#6C727A",
|
||||||
|
},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
>
|
||||||
|
“
|
||||||
|
test for mobile private
|
||||||
|
”
|
||||||
|
</Text>
|
||||||
|
<View
|
||||||
|
style={
|
||||||
|
Object {
|
||||||
|
"flexDirection": "row",
|
||||||
|
"overflow": "hidden",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<View
|
||||||
|
style={
|
||||||
|
Array [
|
||||||
|
Object {
|
||||||
|
"borderRadius": 4,
|
||||||
|
"height": 16,
|
||||||
|
"marginRight": 4,
|
||||||
|
"marginTop": 8,
|
||||||
|
},
|
||||||
|
Object {
|
||||||
|
"backgroundColor": "#E6E6E7",
|
||||||
|
},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Text
|
||||||
|
style={
|
||||||
|
Array [
|
||||||
|
Object {
|
||||||
|
"backgroundColor": "transparent",
|
||||||
|
"fontFamily": "System",
|
||||||
|
"fontSize": 12,
|
||||||
|
"fontWeight": "400",
|
||||||
|
"paddingBottom": 0,
|
||||||
|
"paddingHorizontal": 4,
|
||||||
|
"paddingTop": 0,
|
||||||
|
"textAlign": "left",
|
||||||
|
},
|
||||||
|
Object {
|
||||||
|
"color": "#6C727A",
|
||||||
|
},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
>
|
||||||
|
HQ
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
<View
|
||||||
|
style={
|
||||||
|
Array [
|
||||||
|
Object {
|
||||||
|
"borderRadius": 4,
|
||||||
|
"height": 16,
|
||||||
|
"marginRight": 4,
|
||||||
|
"marginTop": 8,
|
||||||
|
},
|
||||||
|
Object {
|
||||||
|
"backgroundColor": "#E6E6E7",
|
||||||
|
},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Text
|
||||||
|
style={
|
||||||
|
Array [
|
||||||
|
Object {
|
||||||
|
"backgroundColor": "transparent",
|
||||||
|
"fontFamily": "System",
|
||||||
|
"fontSize": 12,
|
||||||
|
"fontWeight": "400",
|
||||||
|
"paddingBottom": 0,
|
||||||
|
"paddingHorizontal": 4,
|
||||||
|
"paddingTop": 0,
|
||||||
|
"textAlign": "left",
|
||||||
|
},
|
||||||
|
Object {
|
||||||
|
"color": "#6C727A",
|
||||||
|
},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
>
|
||||||
|
Closed
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
<View
|
||||||
|
style={
|
||||||
|
Array [
|
||||||
|
Object {
|
||||||
|
"borderRadius": 4,
|
||||||
|
"height": 16,
|
||||||
|
"marginRight": 4,
|
||||||
|
"marginTop": 8,
|
||||||
|
},
|
||||||
|
Object {
|
||||||
|
"backgroundColor": "#E6E6E7",
|
||||||
|
},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Text
|
||||||
|
style={
|
||||||
|
Array [
|
||||||
|
Object {
|
||||||
|
"backgroundColor": "transparent",
|
||||||
|
"fontFamily": "System",
|
||||||
|
"fontSize": 12,
|
||||||
|
"fontWeight": "400",
|
||||||
|
"paddingBottom": 0,
|
||||||
|
"paddingHorizontal": 4,
|
||||||
|
"paddingTop": 0,
|
||||||
|
"textAlign": "left",
|
||||||
|
},
|
||||||
|
Object {
|
||||||
|
"color": "#6C727A",
|
||||||
|
},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
>
|
||||||
|
HQ
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
<View
|
||||||
|
style={
|
||||||
|
Array [
|
||||||
|
Object {
|
||||||
|
"borderRadius": 4,
|
||||||
|
"height": 16,
|
||||||
|
"marginRight": 4,
|
||||||
|
"marginTop": 8,
|
||||||
|
},
|
||||||
|
Object {
|
||||||
|
"backgroundColor": "#E6E6E7",
|
||||||
|
},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Text
|
||||||
|
style={
|
||||||
|
Array [
|
||||||
|
Object {
|
||||||
|
"backgroundColor": "transparent",
|
||||||
|
"fontFamily": "System",
|
||||||
|
"fontSize": 12,
|
||||||
|
"fontWeight": "400",
|
||||||
|
"paddingBottom": 0,
|
||||||
|
"paddingHorizontal": 4,
|
||||||
|
"paddingTop": 0,
|
||||||
|
"textAlign": "left",
|
||||||
|
},
|
||||||
|
Object {
|
||||||
|
"color": "#6C727A",
|
||||||
|
},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
>
|
||||||
|
Problem in Product Y
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
<View
|
||||||
|
style={
|
||||||
|
Array [
|
||||||
|
Object {
|
||||||
|
"borderRadius": 4,
|
||||||
|
"height": 16,
|
||||||
|
"marginRight": 4,
|
||||||
|
"marginTop": 8,
|
||||||
|
},
|
||||||
|
Object {
|
||||||
|
"backgroundColor": "#E6E6E7",
|
||||||
|
},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Text
|
||||||
|
style={
|
||||||
|
Array [
|
||||||
|
Object {
|
||||||
|
"backgroundColor": "transparent",
|
||||||
|
"fontFamily": "System",
|
||||||
|
"fontSize": 12,
|
||||||
|
"fontWeight": "400",
|
||||||
|
"paddingBottom": 0,
|
||||||
|
"paddingHorizontal": 4,
|
||||||
|
"paddingTop": 0,
|
||||||
|
"textAlign": "left",
|
||||||
|
},
|
||||||
|
Object {
|
||||||
|
"color": "#6C727A",
|
||||||
|
},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
>
|
||||||
|
HQ
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
<View
|
||||||
|
style={
|
||||||
|
Array [
|
||||||
|
Object {
|
||||||
|
"borderRadius": 4,
|
||||||
|
"height": 16,
|
||||||
|
"marginRight": 4,
|
||||||
|
"marginTop": 8,
|
||||||
|
},
|
||||||
|
Object {
|
||||||
|
"backgroundColor": "#E6E6E7",
|
||||||
|
},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Text
|
||||||
|
style={
|
||||||
|
Array [
|
||||||
|
Object {
|
||||||
|
"backgroundColor": "transparent",
|
||||||
|
"fontFamily": "System",
|
||||||
|
"fontSize": 12,
|
||||||
|
"fontWeight": "400",
|
||||||
|
"paddingBottom": 0,
|
||||||
|
"paddingHorizontal": 4,
|
||||||
|
"paddingTop": 0,
|
||||||
|
"textAlign": "left",
|
||||||
|
},
|
||||||
|
Object {
|
||||||
|
"color": "#6C727A",
|
||||||
|
},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
>
|
||||||
|
Closed
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
<View
|
||||||
|
style={
|
||||||
|
Array [
|
||||||
|
Object {
|
||||||
|
"borderRadius": 4,
|
||||||
|
"height": 16,
|
||||||
|
"marginRight": 4,
|
||||||
|
"marginTop": 8,
|
||||||
|
},
|
||||||
|
Object {
|
||||||
|
"backgroundColor": "#E6E6E7",
|
||||||
|
},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Text
|
||||||
|
style={
|
||||||
|
Array [
|
||||||
|
Object {
|
||||||
|
"backgroundColor": "transparent",
|
||||||
|
"fontFamily": "System",
|
||||||
|
"fontSize": 12,
|
||||||
|
"fontWeight": "400",
|
||||||
|
"paddingBottom": 0,
|
||||||
|
"paddingHorizontal": 4,
|
||||||
|
"paddingTop": 0,
|
||||||
|
"textAlign": "left",
|
||||||
|
},
|
||||||
|
Object {
|
||||||
|
"color": "#6C727A",
|
||||||
|
},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
>
|
||||||
|
Problem in Product Y
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
</View>,
|
||||||
|
]
|
||||||
|
`;
|
||||||
|
|
||||||
exports[`Storyshots Header Buttons badge 1`] = `
|
exports[`Storyshots Header Buttons badge 1`] = `
|
||||||
<RNCSafeAreaView
|
<RNCSafeAreaView
|
||||||
edges={
|
edges={
|
||||||
|
@ -4693,7 +5267,7 @@ exports[`Storyshots List pressable 1`] = `
|
||||||
"justifyContent": "center",
|
"justifyContent": "center",
|
||||||
"paddingHorizontal": 12,
|
"paddingHorizontal": 12,
|
||||||
},
|
},
|
||||||
false,
|
undefined,
|
||||||
Object {
|
Object {
|
||||||
"height": 92,
|
"height": 92,
|
||||||
},
|
},
|
||||||
|
@ -6211,7 +6785,7 @@ exports[`Storyshots List with bigger font 1`] = `
|
||||||
"justifyContent": "center",
|
"justifyContent": "center",
|
||||||
"paddingHorizontal": 12,
|
"paddingHorizontal": 12,
|
||||||
},
|
},
|
||||||
false,
|
undefined,
|
||||||
Object {
|
Object {
|
||||||
"height": 69,
|
"height": 69,
|
||||||
},
|
},
|
||||||
|
@ -6625,7 +7199,7 @@ exports[`Storyshots List with bigger font 1`] = `
|
||||||
"justifyContent": "center",
|
"justifyContent": "center",
|
||||||
"paddingHorizontal": 12,
|
"paddingHorizontal": 12,
|
||||||
},
|
},
|
||||||
false,
|
undefined,
|
||||||
Object {
|
Object {
|
||||||
"height": 69,
|
"height": 69,
|
||||||
},
|
},
|
||||||
|
@ -7080,7 +7654,7 @@ exports[`Storyshots List with black theme 1`] = `
|
||||||
"justifyContent": "center",
|
"justifyContent": "center",
|
||||||
"paddingHorizontal": 12,
|
"paddingHorizontal": 12,
|
||||||
},
|
},
|
||||||
false,
|
undefined,
|
||||||
Object {
|
Object {
|
||||||
"height": 92,
|
"height": 92,
|
||||||
},
|
},
|
||||||
|
@ -7494,7 +8068,7 @@ exports[`Storyshots List with black theme 1`] = `
|
||||||
"justifyContent": "center",
|
"justifyContent": "center",
|
||||||
"paddingHorizontal": 12,
|
"paddingHorizontal": 12,
|
||||||
},
|
},
|
||||||
false,
|
undefined,
|
||||||
Object {
|
Object {
|
||||||
"height": 92,
|
"height": 92,
|
||||||
},
|
},
|
||||||
|
@ -7972,7 +8546,7 @@ exports[`Storyshots List with custom colors 1`] = `
|
||||||
"justifyContent": "center",
|
"justifyContent": "center",
|
||||||
"paddingHorizontal": 12,
|
"paddingHorizontal": 12,
|
||||||
},
|
},
|
||||||
false,
|
undefined,
|
||||||
Object {
|
Object {
|
||||||
"height": 92,
|
"height": 92,
|
||||||
},
|
},
|
||||||
|
@ -8129,7 +8703,7 @@ exports[`Storyshots List with dark theme 1`] = `
|
||||||
"justifyContent": "center",
|
"justifyContent": "center",
|
||||||
"paddingHorizontal": 12,
|
"paddingHorizontal": 12,
|
||||||
},
|
},
|
||||||
false,
|
undefined,
|
||||||
Object {
|
Object {
|
||||||
"height": 92,
|
"height": 92,
|
||||||
},
|
},
|
||||||
|
@ -8543,7 +9117,7 @@ exports[`Storyshots List with dark theme 1`] = `
|
||||||
"justifyContent": "center",
|
"justifyContent": "center",
|
||||||
"paddingHorizontal": 12,
|
"paddingHorizontal": 12,
|
||||||
},
|
},
|
||||||
false,
|
undefined,
|
||||||
Object {
|
Object {
|
||||||
"height": 92,
|
"height": 92,
|
||||||
},
|
},
|
||||||
|
@ -10410,7 +10984,7 @@ exports[`Storyshots List with small font 1`] = `
|
||||||
"justifyContent": "center",
|
"justifyContent": "center",
|
||||||
"paddingHorizontal": 12,
|
"paddingHorizontal": 12,
|
||||||
},
|
},
|
||||||
false,
|
undefined,
|
||||||
Object {
|
Object {
|
||||||
"height": 36.800000000000004,
|
"height": 36.800000000000004,
|
||||||
},
|
},
|
||||||
|
@ -10824,7 +11398,7 @@ exports[`Storyshots List with small font 1`] = `
|
||||||
"justifyContent": "center",
|
"justifyContent": "center",
|
||||||
"paddingHorizontal": 12,
|
"paddingHorizontal": 12,
|
||||||
},
|
},
|
||||||
false,
|
undefined,
|
||||||
Object {
|
Object {
|
||||||
"height": 36.800000000000004,
|
"height": 36.800000000000004,
|
||||||
},
|
},
|
||||||
|
@ -40581,6 +41155,7 @@ exports[`Storyshots Message Show a button as attachment 1`] = `
|
||||||
"color": "#ffffff",
|
"color": "#ffffff",
|
||||||
},
|
},
|
||||||
undefined,
|
undefined,
|
||||||
|
undefined,
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
|
|
|
@ -25,7 +25,7 @@ import com.android.build.OutputFile
|
||||||
* bundleAssetName: "index.android.bundle",
|
* bundleAssetName: "index.android.bundle",
|
||||||
*
|
*
|
||||||
* // the entry file for bundle generation. If none specified and
|
* // the entry file for bundle generation. If none specified and
|
||||||
* // "index.android.js" exists, it will be used. Otherwise "index.js" is
|
* // "index.android.js" exists, it will be used. Otherwise "index.tsx" is
|
||||||
* // default. Can be overridden with ENTRY_FILE environment variable.
|
* // default. Can be overridden with ENTRY_FILE environment variable.
|
||||||
* entryFile: "index.android.js",
|
* entryFile: "index.android.js",
|
||||||
*
|
*
|
||||||
|
@ -144,7 +144,7 @@ android {
|
||||||
minSdkVersion rootProject.ext.minSdkVersion
|
minSdkVersion rootProject.ext.minSdkVersion
|
||||||
targetSdkVersion rootProject.ext.targetSdkVersion
|
targetSdkVersion rootProject.ext.targetSdkVersion
|
||||||
versionCode VERSIONCODE as Integer
|
versionCode VERSIONCODE as Integer
|
||||||
versionName "4.19.0"
|
versionName "4.20.0"
|
||||||
vectorDrawables.useSupportLibrary = true
|
vectorDrawables.useSupportLibrary = true
|
||||||
if (!isFoss) {
|
if (!isFoss) {
|
||||||
manifestPlaceholders = [BugsnagAPIKey: BugsnagAPIKey as String]
|
manifestPlaceholders = [BugsnagAPIKey: BugsnagAPIKey as String]
|
||||||
|
|
8
app.json
8
app.json
|
@ -1,5 +1,5 @@
|
||||||
{
|
{
|
||||||
"name": "RocketChatRN",
|
"name": "RocketChatRN",
|
||||||
"share": "ShareRocketChatRN",
|
"share": "ShareRocketChatRN",
|
||||||
"displayName": "RocketChatRN"
|
"displayName": "RocketChatRN"
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,21 +1,15 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import { NavigationContainer } from '@react-navigation/native';
|
import { NavigationContainer } from '@react-navigation/native';
|
||||||
import { createStackNavigator } from '@react-navigation/stack';
|
import { createStackNavigator } from '@react-navigation/stack';
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
|
|
||||||
import Navigation from './lib/Navigation';
|
import Navigation from './lib/Navigation';
|
||||||
import { defaultHeader, getActiveRouteName, navigationTheme } from './utils/navigation';
|
import { defaultHeader, getActiveRouteName, navigationTheme } from './utils/navigation';
|
||||||
import {
|
import { ROOT_INSIDE, ROOT_LOADING, ROOT_OUTSIDE, ROOT_SET_USERNAME } from './actions/app';
|
||||||
ROOT_LOADING, ROOT_OUTSIDE, ROOT_NEW_SERVER, ROOT_INSIDE, ROOT_SET_USERNAME
|
|
||||||
} from './actions/app';
|
|
||||||
|
|
||||||
// Stacks
|
// Stacks
|
||||||
import AuthLoadingView from './views/AuthLoadingView';
|
import AuthLoadingView from './views/AuthLoadingView';
|
||||||
|
|
||||||
// SetUsername Stack
|
// SetUsername Stack
|
||||||
import SetUsernameView from './views/SetUsernameView';
|
import SetUsernameView from './views/SetUsernameView';
|
||||||
|
|
||||||
import OutsideStack from './stacks/OutsideStack';
|
import OutsideStack from './stacks/OutsideStack';
|
||||||
import InsideStack from './stacks/InsideStack';
|
import InsideStack from './stacks/InsideStack';
|
||||||
import MasterDetailStack from './stacks/MasterDetailStack';
|
import MasterDetailStack from './stacks/MasterDetailStack';
|
||||||
|
@ -26,16 +20,13 @@ import { setCurrentScreen } from './utils/log';
|
||||||
const SetUsername = createStackNavigator();
|
const SetUsername = createStackNavigator();
|
||||||
const SetUsernameStack = () => (
|
const SetUsernameStack = () => (
|
||||||
<SetUsername.Navigator screenOptions={defaultHeader}>
|
<SetUsername.Navigator screenOptions={defaultHeader}>
|
||||||
<SetUsername.Screen
|
<SetUsername.Screen name='SetUsernameView' component={SetUsernameView} />
|
||||||
name='SetUsernameView'
|
|
||||||
component={SetUsernameView}
|
|
||||||
/>
|
|
||||||
</SetUsername.Navigator>
|
</SetUsername.Navigator>
|
||||||
);
|
);
|
||||||
|
|
||||||
// App
|
// App
|
||||||
const Stack = createStackNavigator();
|
const Stack = createStackNavigator();
|
||||||
const App = React.memo(({ root, isMasterDetail }) => {
|
const App = React.memo(({ root, isMasterDetail }: { root: string; isMasterDetail: boolean }) => {
|
||||||
if (!root) {
|
if (!root) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
@ -54,61 +45,32 @@ const App = React.memo(({ root, isMasterDetail }) => {
|
||||||
<NavigationContainer
|
<NavigationContainer
|
||||||
theme={navTheme}
|
theme={navTheme}
|
||||||
ref={Navigation.navigationRef}
|
ref={Navigation.navigationRef}
|
||||||
onStateChange={(state) => {
|
onStateChange={state => {
|
||||||
const previousRouteName = Navigation.routeNameRef.current;
|
const previousRouteName = Navigation.routeNameRef.current;
|
||||||
const currentRouteName = getActiveRouteName(state);
|
const currentRouteName = getActiveRouteName(state);
|
||||||
if (previousRouteName !== currentRouteName) {
|
if (previousRouteName !== currentRouteName) {
|
||||||
setCurrentScreen(currentRouteName);
|
setCurrentScreen(currentRouteName);
|
||||||
}
|
}
|
||||||
Navigation.routeNameRef.current = currentRouteName;
|
Navigation.routeNameRef.current = currentRouteName;
|
||||||
}}
|
}}>
|
||||||
>
|
|
||||||
<Stack.Navigator screenOptions={{ headerShown: false, animationEnabled: false }}>
|
<Stack.Navigator screenOptions={{ headerShown: false, animationEnabled: false }}>
|
||||||
<>
|
<>
|
||||||
{root === ROOT_LOADING ? (
|
{root === ROOT_LOADING ? <Stack.Screen name='AuthLoading' component={AuthLoadingView} /> : null}
|
||||||
<Stack.Screen
|
{root === ROOT_OUTSIDE ? <Stack.Screen name='OutsideStack' component={OutsideStack} /> : null}
|
||||||
name='AuthLoading'
|
|
||||||
component={AuthLoadingView}
|
|
||||||
/>
|
|
||||||
) : null}
|
|
||||||
{root === ROOT_OUTSIDE || root === ROOT_NEW_SERVER ? (
|
|
||||||
<Stack.Screen
|
|
||||||
name='OutsideStack'
|
|
||||||
component={OutsideStack}
|
|
||||||
/>
|
|
||||||
) : null}
|
|
||||||
{root === ROOT_INSIDE && isMasterDetail ? (
|
{root === ROOT_INSIDE && isMasterDetail ? (
|
||||||
<Stack.Screen
|
<Stack.Screen name='MasterDetailStack' component={MasterDetailStack} />
|
||||||
name='MasterDetailStack'
|
|
||||||
component={MasterDetailStack}
|
|
||||||
/>
|
|
||||||
) : null}
|
|
||||||
{root === ROOT_INSIDE && !isMasterDetail ? (
|
|
||||||
<Stack.Screen
|
|
||||||
name='InsideStack'
|
|
||||||
component={InsideStack}
|
|
||||||
/>
|
|
||||||
) : null}
|
|
||||||
{root === ROOT_SET_USERNAME ? (
|
|
||||||
<Stack.Screen
|
|
||||||
name='SetUsernameStack'
|
|
||||||
component={SetUsernameStack}
|
|
||||||
/>
|
|
||||||
) : null}
|
) : null}
|
||||||
|
{root === ROOT_INSIDE && !isMasterDetail ? <Stack.Screen name='InsideStack' component={InsideStack} /> : null}
|
||||||
|
{root === ROOT_SET_USERNAME ? <Stack.Screen name='SetUsernameStack' component={SetUsernameStack} /> : null}
|
||||||
</>
|
</>
|
||||||
</Stack.Navigator>
|
</Stack.Navigator>
|
||||||
</NavigationContainer>
|
</NavigationContainer>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
const mapStateToProps = state => ({
|
const mapStateToProps = (state: any) => ({
|
||||||
root: state.app.root,
|
root: state.app.root,
|
||||||
isMasterDetail: state.app.isMasterDetail
|
isMasterDetail: state.app.isMasterDetail
|
||||||
});
|
});
|
||||||
|
|
||||||
App.propTypes = {
|
|
||||||
root: PropTypes.string,
|
|
||||||
isMasterDetail: PropTypes.bool
|
|
||||||
};
|
|
||||||
|
|
||||||
const AppContainer = connect(mapStateToProps)(App);
|
const AppContainer = connect(mapStateToProps)(App);
|
||||||
export default AppContainer;
|
export default AppContainer;
|
|
@ -2,21 +2,16 @@
|
||||||
import { NativeModules } from 'react-native';
|
import { NativeModules } from 'react-native';
|
||||||
import Reactotron from 'reactotron-react-native';
|
import Reactotron from 'reactotron-react-native';
|
||||||
import { reactotronRedux } from 'reactotron-redux';
|
import { reactotronRedux } from 'reactotron-redux';
|
||||||
import sagaPlugin from 'reactotron-redux-saga'
|
import sagaPlugin from 'reactotron-redux-saga';
|
||||||
|
|
||||||
if (__DEV__) {
|
if (__DEV__) {
|
||||||
const scriptURL = NativeModules.SourceCode.scriptURL;
|
const scriptURL = NativeModules.SourceCode.scriptURL;
|
||||||
const scriptHostname = scriptURL.split('://')[1].split(':')[0];
|
const scriptHostname = scriptURL.split('://')[1].split(':')[0];
|
||||||
Reactotron
|
Reactotron.configure({ host: scriptHostname }).useReactNative().use(reactotronRedux()).use(sagaPlugin()).connect();
|
||||||
.configure({ host: scriptHostname })
|
// Running on android device
|
||||||
.useReactNative()
|
// $ adb reverse tcp:9090 tcp:9090
|
||||||
.use(reactotronRedux())
|
Reactotron.clear();
|
||||||
.use(sagaPlugin())
|
console.warn = Reactotron.log;
|
||||||
.connect();
|
console.log = Reactotron.log;
|
||||||
// Running on android device
|
console.disableYellowBox = true;
|
||||||
// $ adb reverse tcp:9090 tcp:9090
|
|
||||||
Reactotron.clear();
|
|
||||||
console.warn = Reactotron.log;
|
|
||||||
console.log = Reactotron.log;
|
|
||||||
console.disableYellowBox = true;
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,23 +4,13 @@ const FAILURE = 'FAILURE';
|
||||||
const defaultTypes = [REQUEST, SUCCESS, FAILURE];
|
const defaultTypes = [REQUEST, SUCCESS, FAILURE];
|
||||||
function createRequestTypes(base, types = defaultTypes) {
|
function createRequestTypes(base, types = defaultTypes) {
|
||||||
const res = {};
|
const res = {};
|
||||||
types.forEach(type => (res[type] = `${ base }_${ type }`));
|
types.forEach(type => (res[type] = `${base}_${type}`));
|
||||||
return res;
|
return res;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Login events
|
// Login events
|
||||||
export const LOGIN = createRequestTypes('LOGIN', [
|
export const LOGIN = createRequestTypes('LOGIN', [...defaultTypes, 'SET_SERVICES', 'SET_PREFERENCE', 'SET_LOCAL_AUTHENTICATED']);
|
||||||
...defaultTypes,
|
export const SHARE = createRequestTypes('SHARE', ['SELECT_SERVER', 'SET_USER', 'SET_SETTINGS', 'SET_SERVER_INFO']);
|
||||||
'SET_SERVICES',
|
|
||||||
'SET_PREFERENCE',
|
|
||||||
'SET_LOCAL_AUTHENTICATED'
|
|
||||||
]);
|
|
||||||
export const SHARE = createRequestTypes('SHARE', [
|
|
||||||
'SELECT_SERVER',
|
|
||||||
'SET_USER',
|
|
||||||
'SET_SETTINGS',
|
|
||||||
'SET_SERVER_INFO'
|
|
||||||
]);
|
|
||||||
export const USER = createRequestTypes('USER', ['SET']);
|
export const USER = createRequestTypes('USER', ['SET']);
|
||||||
export const ROOMS = createRequestTypes('ROOMS', [
|
export const ROOMS = createRequestTypes('ROOMS', [
|
||||||
...defaultTypes,
|
...defaultTypes,
|
||||||
|
@ -33,8 +23,24 @@ export const ROOMS = createRequestTypes('ROOMS', [
|
||||||
'OPEN_SEARCH_HEADER',
|
'OPEN_SEARCH_HEADER',
|
||||||
'CLOSE_SEARCH_HEADER'
|
'CLOSE_SEARCH_HEADER'
|
||||||
]);
|
]);
|
||||||
export const ROOM = createRequestTypes('ROOM', ['SUBSCRIBE', 'UNSUBSCRIBE', 'LEAVE', 'DELETE', 'REMOVED', 'CLOSE', 'FORWARD', 'USER_TYPING']);
|
export const ROOM = createRequestTypes('ROOM', [
|
||||||
export const INQUIRY = createRequestTypes('INQUIRY', [...defaultTypes, 'SET_ENABLED', 'RESET', 'QUEUE_ADD', 'QUEUE_UPDATE', 'QUEUE_REMOVE']);
|
'SUBSCRIBE',
|
||||||
|
'UNSUBSCRIBE',
|
||||||
|
'LEAVE',
|
||||||
|
'DELETE',
|
||||||
|
'REMOVED',
|
||||||
|
'CLOSE',
|
||||||
|
'FORWARD',
|
||||||
|
'USER_TYPING'
|
||||||
|
]);
|
||||||
|
export const INQUIRY = createRequestTypes('INQUIRY', [
|
||||||
|
...defaultTypes,
|
||||||
|
'SET_ENABLED',
|
||||||
|
'RESET',
|
||||||
|
'QUEUE_ADD',
|
||||||
|
'QUEUE_UPDATE',
|
||||||
|
'QUEUE_REMOVE'
|
||||||
|
]);
|
||||||
export const APP = createRequestTypes('APP', ['START', 'READY', 'INIT', 'INIT_LOCAL_SETTINGS', 'SET_MASTER_DETAIL']);
|
export const APP = createRequestTypes('APP', ['START', 'READY', 'INIT', 'INIT_LOCAL_SETTINGS', 'SET_MASTER_DETAIL']);
|
||||||
export const MESSAGES = createRequestTypes('MESSAGES', ['REPLY_BROADCAST']);
|
export const MESSAGES = createRequestTypes('MESSAGES', ['REPLY_BROADCAST']);
|
||||||
export const CREATE_CHANNEL = createRequestTypes('CREATE_CHANNEL', [...defaultTypes]);
|
export const CREATE_CHANNEL = createRequestTypes('CREATE_CHANNEL', [...defaultTypes]);
|
||||||
|
|
|
@ -3,7 +3,6 @@ import { APP } from './actionsTypes';
|
||||||
export const ROOT_OUTSIDE = 'outside';
|
export const ROOT_OUTSIDE = 'outside';
|
||||||
export const ROOT_INSIDE = 'inside';
|
export const ROOT_INSIDE = 'inside';
|
||||||
export const ROOT_LOADING = 'loading';
|
export const ROOT_LOADING = 'loading';
|
||||||
export const ROOT_NEW_SERVER = 'newServer';
|
|
||||||
export const ROOT_SET_USERNAME = 'setUsername';
|
export const ROOT_SET_USERNAME = 'setUsername';
|
||||||
|
|
||||||
export function appStart({ root, ...args }) {
|
export function appStart({ root, ...args }) {
|
||||||
|
|
|
@ -32,7 +32,6 @@ export function inviteLinksClear() {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
export function inviteLinksCreate(rid) {
|
export function inviteLinksCreate(rid) {
|
||||||
return {
|
return {
|
||||||
type: types.INVITE_LINKS.CREATE,
|
type: types.INVITE_LINKS.CREATE,
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
import * as types from './actionsTypes';
|
import * as types from './actionsTypes';
|
||||||
|
|
||||||
|
|
||||||
export function roomsRequest(params = { allData: false }) {
|
export function roomsRequest(params = { allData: false }) {
|
||||||
return {
|
return {
|
||||||
type: types.ROOMS.REQUEST,
|
type: types.ROOMS.REQUEST,
|
||||||
|
|
|
@ -125,15 +125,15 @@ const keyCommands = [
|
||||||
discoverabilityTitle: I18n.t('Add_server')
|
discoverabilityTitle: I18n.t('Add_server')
|
||||||
},
|
},
|
||||||
// Refers to select rooms on list
|
// Refers to select rooms on list
|
||||||
...([1, 2, 3, 4, 5, 6, 7, 8, 9].map(value => ({
|
...[1, 2, 3, 4, 5, 6, 7, 8, 9].map(value => ({
|
||||||
input: `${ value }`,
|
input: `${value}`,
|
||||||
modifierFlags: constants.keyModifierCommand
|
modifierFlags: constants.keyModifierCommand
|
||||||
}))),
|
})),
|
||||||
// Refers to select servers on list
|
// Refers to select servers on list
|
||||||
...([1, 2, 3, 4, 5, 6, 7, 8, 9].map(value => ({
|
...[1, 2, 3, 4, 5, 6, 7, 8, 9].map(value => ({
|
||||||
input: `${ value }`,
|
input: `${value}`,
|
||||||
modifierFlags: constants.keyModifierCommand | constants.keyModifierAlternate
|
modifierFlags: constants.keyModifierCommand | constants.keyModifierAlternate
|
||||||
})))
|
}))
|
||||||
];
|
];
|
||||||
|
|
||||||
export const setKeyCommands = () => KeyCommands.setKeyCommands(keyCommands);
|
export const setKeyCommands = () => KeyCommands.setKeyCommands(keyCommands);
|
||||||
|
@ -161,7 +161,8 @@ export const handleCommandSubmit = event => commandHandle(event, KEY_SEND_MESSAG
|
||||||
|
|
||||||
export const handleCommandShowUpload = event => commandHandle(event, KEY_UPLOAD, ['command']);
|
export const handleCommandShowUpload = event => commandHandle(event, KEY_UPLOAD, ['command']);
|
||||||
|
|
||||||
export const handleCommandScroll = event => commandHandle(event, [constants.keyInputUpArrow, constants.keyInputDownArrow], ['alternate']);
|
export const handleCommandScroll = event =>
|
||||||
|
commandHandle(event, [constants.keyInputUpArrow, constants.keyInputDownArrow], ['alternate']);
|
||||||
|
|
||||||
export const handleCommandRoomActions = event => commandHandle(event, KEY_ROOM_ACTIONS, ['command']);
|
export const handleCommandRoomActions = event => commandHandle(event, KEY_ROOM_ACTIONS, ['command']);
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
export const STATUS_COLORS = {
|
export const STATUS_COLORS: any = {
|
||||||
online: '#2de0a5',
|
online: '#2de0a5',
|
||||||
busy: '#f5455c',
|
busy: '#f5455c',
|
||||||
away: '#ffd21f',
|
away: '#ffd21f',
|
||||||
|
@ -19,7 +19,7 @@ const mentions = {
|
||||||
mentionOtherColor: '#F3BE08'
|
mentionOtherColor: '#F3BE08'
|
||||||
};
|
};
|
||||||
|
|
||||||
export const themes = {
|
export const themes: any = {
|
||||||
light: {
|
light: {
|
||||||
backgroundColor: '#ffffff',
|
backgroundColor: '#ffffff',
|
||||||
focusedBackground: '#ffffff',
|
focusedBackground: '#ffffff',
|
|
@ -2,8 +2,10 @@ import { getBundleId, isIOS } from '../utils/deviceInfo';
|
||||||
|
|
||||||
const APP_STORE_ID = '1148741252';
|
const APP_STORE_ID = '1148741252';
|
||||||
|
|
||||||
export const PLAY_MARKET_LINK = `https://play.google.com/store/apps/details?id=${ getBundleId }`;
|
export const PLAY_MARKET_LINK = `https://play.google.com/store/apps/details?id=${getBundleId}`;
|
||||||
export const FDROID_MARKET_LINK = 'https://f-droid.org/en/packages/chat.rocket.android';
|
export const FDROID_MARKET_LINK = 'https://f-droid.org/en/packages/chat.rocket.android';
|
||||||
export const APP_STORE_LINK = `https://itunes.apple.com/app/id${ APP_STORE_ID }`;
|
export const APP_STORE_LINK = `https://itunes.apple.com/app/id${APP_STORE_ID}`;
|
||||||
export const LICENSE_LINK = 'https://github.com/RocketChat/Rocket.Chat.ReactNative/blob/develop/LICENSE';
|
export const LICENSE_LINK = 'https://github.com/RocketChat/Rocket.Chat.ReactNative/blob/develop/LICENSE';
|
||||||
export const STORE_REVIEW_LINK = isIOS ? `itms-apps://itunes.apple.com/app/id${ APP_STORE_ID }?action=write-review` : `market://details?id=${ getBundleId }`;
|
export const STORE_REVIEW_LINK = isIOS
|
||||||
|
? `itms-apps://itunes.apple.com/app/id${APP_STORE_ID}?action=write-review`
|
||||||
|
: `market://details?id=${getBundleId}`;
|
|
@ -202,5 +202,8 @@ export default {
|
||||||
},
|
},
|
||||||
Jitsi_Enable_Channels: {
|
Jitsi_Enable_Channels: {
|
||||||
type: 'valuesAsBoolean'
|
type: 'valuesAsBoolean'
|
||||||
|
},
|
||||||
|
Canned_Responses_Enable: {
|
||||||
|
type: 'valueAsBoolean'
|
||||||
}
|
}
|
||||||
};
|
};
|
|
@ -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;
|
|
|
@ -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;
|
|
@ -1,15 +1,11 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import { View } from 'react-native';
|
import { View } from 'react-native';
|
||||||
|
|
||||||
import styles from './styles';
|
import styles from './styles';
|
||||||
import { themes } from '../../constants/colors';
|
import { themes } from '../../constants/colors';
|
||||||
|
|
||||||
export const Handle = React.memo(({ theme }) => (
|
export const Handle = React.memo(({ theme }: { theme: string }) => (
|
||||||
<View style={[styles.handle, { backgroundColor: themes[theme].focusedBackground }]} testID='action-sheet-handle'>
|
<View style={[styles.handle, { backgroundColor: themes[theme].focusedBackground }]} testID='action-sheet-handle'>
|
||||||
<View style={[styles.handleIndicator, { backgroundColor: themes[theme].auxiliaryText }]} />
|
<View style={[styles.handleIndicator, { backgroundColor: themes[theme].auxiliaryText }]} />
|
||||||
</View>
|
</View>
|
||||||
));
|
));
|
||||||
Handle.propTypes = {
|
|
||||||
theme: PropTypes.string
|
|
||||||
};
|
|
|
@ -1,13 +1,25 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import { Text, View } from 'react-native';
|
import { Text, View } from 'react-native';
|
||||||
|
|
||||||
import { themes } from '../../constants/colors';
|
import { themes } from '../../constants/colors';
|
||||||
import { CustomIcon } from '../../lib/Icons';
|
import { CustomIcon } from '../../lib/Icons';
|
||||||
import styles from './styles';
|
|
||||||
import { Button } from './Button';
|
import { Button } from './Button';
|
||||||
|
import styles from './styles';
|
||||||
|
|
||||||
export const Item = React.memo(({ item, hide, theme }) => {
|
interface IActionSheetItem {
|
||||||
|
item: {
|
||||||
|
title: string;
|
||||||
|
icon: string;
|
||||||
|
danger: boolean;
|
||||||
|
testID: string;
|
||||||
|
onPress(): void;
|
||||||
|
right: Function;
|
||||||
|
};
|
||||||
|
theme: string;
|
||||||
|
hide(): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Item = React.memo(({ item, hide, theme }: IActionSheetItem) => {
|
||||||
const onPress = () => {
|
const onPress = () => {
|
||||||
hide();
|
hide();
|
||||||
item?.onPress();
|
item?.onPress();
|
||||||
|
@ -18,34 +30,16 @@ export const Item = React.memo(({ item, hide, theme }) => {
|
||||||
onPress={onPress}
|
onPress={onPress}
|
||||||
style={[styles.item, { backgroundColor: themes[theme].focusedBackground }]}
|
style={[styles.item, { backgroundColor: themes[theme].focusedBackground }]}
|
||||||
theme={theme}
|
theme={theme}
|
||||||
testID={item.testID}
|
testID={item.testID}>
|
||||||
>
|
|
||||||
<CustomIcon name={item.icon} size={20} color={item.danger ? themes[theme].dangerColor : themes[theme].bodyText} />
|
<CustomIcon name={item.icon} size={20} color={item.danger ? themes[theme].dangerColor : themes[theme].bodyText} />
|
||||||
<View style={styles.titleContainer}>
|
<View style={styles.titleContainer}>
|
||||||
<Text
|
<Text
|
||||||
numberOfLines={1}
|
numberOfLines={1}
|
||||||
style={[styles.title, { color: item.danger ? themes[theme].dangerColor : themes[theme].bodyText }]}
|
style={[styles.title, { color: item.danger ? themes[theme].dangerColor : themes[theme].bodyText }]}>
|
||||||
>
|
|
||||||
{item.title}
|
{item.title}
|
||||||
</Text>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
{ item.right ? (
|
{item.right ? <View style={styles.rightContainer}>{item.right ? item.right() : null}</View> : null}
|
||||||
<View style={styles.rightContainer}>
|
|
||||||
{item.right ? item.right() : null}
|
|
||||||
</View>
|
|
||||||
) : null }
|
|
||||||
</Button>
|
</Button>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
Item.propTypes = {
|
|
||||||
item: PropTypes.shape({
|
|
||||||
title: PropTypes.string,
|
|
||||||
icon: PropTypes.string,
|
|
||||||
danger: PropTypes.bool,
|
|
||||||
onPress: PropTypes.func,
|
|
||||||
right: PropTypes.func,
|
|
||||||
testID: PropTypes.string
|
|
||||||
}),
|
|
||||||
hide: PropTypes.func,
|
|
||||||
theme: PropTypes.string
|
|
||||||
};
|
|
|
@ -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
|
|
||||||
};
|
|
|
@ -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>
|
||||||
|
);
|
||||||
|
});
|
|
@ -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;
|
|
|
@ -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;
|
|
@ -1,6 +1,5 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { StyleSheet, View, Text } from 'react-native';
|
import { StyleSheet, Text, View } from 'react-native';
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
|
|
||||||
import { themes } from '../constants/colors';
|
import { themes } from '../constants/colors';
|
||||||
import sharedStyles from '../views/Styles';
|
import sharedStyles from '../views/Styles';
|
||||||
|
@ -21,14 +20,13 @@ const styles = StyleSheet.create({
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const AppVersion = React.memo(({ theme }) => (
|
const AppVersion = React.memo(({ theme }: { theme: string }) => (
|
||||||
<View style={styles.container}>
|
<View style={styles.container}>
|
||||||
<Text style={[styles.text, { color: themes[theme].auxiliaryText }]}>{I18n.t('Version_no', { version: '' })}<Text style={styles.bold}>{getReadableVersion}</Text></Text>
|
<Text style={[styles.text, { color: themes[theme].auxiliaryText }]}>
|
||||||
|
{I18n.t('Version_no', { version: '' })}
|
||||||
|
<Text style={styles.bold}>{getReadableVersion}</Text>
|
||||||
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
));
|
));
|
||||||
|
|
||||||
AppVersion.propTypes = {
|
|
||||||
theme: PropTypes.string
|
|
||||||
};
|
|
||||||
|
|
||||||
export default AppVersion;
|
export default AppVersion;
|
|
@ -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;
|
|
|
@ -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;
|
|
@ -1,27 +1,23 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import { Q } from '@nozbe/watermelondb';
|
import { Q } from '@nozbe/watermelondb';
|
||||||
|
|
||||||
import database from '../../lib/database';
|
import database from '../../lib/database';
|
||||||
import { getUserSelector } from '../../selectors/login';
|
import { getUserSelector } from '../../selectors/login';
|
||||||
import Avatar from './Avatar';
|
import Avatar from './Avatar';
|
||||||
|
import { IAvatar } from './interfaces';
|
||||||
|
|
||||||
class AvatarContainer extends React.Component {
|
class AvatarContainer extends React.Component<Partial<IAvatar>, any> {
|
||||||
static propTypes = {
|
private mounted: boolean;
|
||||||
rid: PropTypes.string,
|
|
||||||
text: PropTypes.string,
|
private subscription!: any;
|
||||||
type: PropTypes.string,
|
|
||||||
blockUnauthenticatedAccess: PropTypes.bool,
|
|
||||||
serverVersion: PropTypes.string
|
|
||||||
};
|
|
||||||
|
|
||||||
static defaultProps = {
|
static defaultProps = {
|
||||||
text: '',
|
text: '',
|
||||||
type: 'd'
|
type: 'd'
|
||||||
};
|
};
|
||||||
|
|
||||||
constructor(props) {
|
constructor(props: Partial<IAvatar>) {
|
||||||
super(props);
|
super(props);
|
||||||
this.mounted = false;
|
this.mounted = false;
|
||||||
this.state = { avatarETag: '' };
|
this.state = { avatarETag: '' };
|
||||||
|
@ -32,7 +28,7 @@ class AvatarContainer extends React.Component {
|
||||||
this.mounted = true;
|
this.mounted = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidUpdate(prevProps) {
|
componentDidUpdate(prevProps: any) {
|
||||||
const { text, type } = this.props;
|
const { text, type } = this.props;
|
||||||
if (prevProps.text !== text || prevProps.type !== type) {
|
if (prevProps.text !== text || prevProps.type !== type) {
|
||||||
this.init();
|
this.init();
|
||||||
|
@ -50,7 +46,7 @@ class AvatarContainer extends React.Component {
|
||||||
return type === 'd';
|
return type === 'd';
|
||||||
}
|
}
|
||||||
|
|
||||||
init = async() => {
|
init = async () => {
|
||||||
const db = database.active;
|
const db = database.active;
|
||||||
const usersCollection = db.get('users');
|
const usersCollection = db.get('users');
|
||||||
const subsCollection = db.get('subscriptions');
|
const subsCollection = db.get('subscriptions');
|
||||||
|
@ -59,7 +55,7 @@ class AvatarContainer extends React.Component {
|
||||||
try {
|
try {
|
||||||
if (this.isDirect) {
|
if (this.isDirect) {
|
||||||
const { text } = this.props;
|
const { text } = this.props;
|
||||||
const [user] = await usersCollection.query(Q.where('username', text)).fetch();
|
const [user] = await usersCollection.query(Q.where('username', text!)).fetch();
|
||||||
record = user;
|
record = user;
|
||||||
} else {
|
} else {
|
||||||
const { rid } = this.props;
|
const { rid } = this.props;
|
||||||
|
@ -71,37 +67,32 @@ class AvatarContainer extends React.Component {
|
||||||
|
|
||||||
if (record) {
|
if (record) {
|
||||||
const observable = record.observe();
|
const observable = record.observe();
|
||||||
this.subscription = observable.subscribe((r) => {
|
this.subscription = observable.subscribe((r: any) => {
|
||||||
const { avatarETag } = r;
|
const { avatarETag } = r;
|
||||||
if (this.mounted) {
|
if (this.mounted) {
|
||||||
this.setState({ avatarETag });
|
this.setState({ avatarETag });
|
||||||
} else {
|
} else {
|
||||||
|
// @ts-ignore
|
||||||
this.state.avatarETag = avatarETag;
|
this.state.avatarETag = avatarETag;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { avatarETag } = this.state;
|
const { avatarETag } = this.state;
|
||||||
const { serverVersion } = this.props;
|
const { serverVersion } = this.props;
|
||||||
return (
|
return <Avatar avatarETag={avatarETag} serverVersion={serverVersion} {...this.props} />;
|
||||||
<Avatar
|
|
||||||
avatarETag={avatarETag}
|
|
||||||
serverVersion={serverVersion}
|
|
||||||
{...this.props}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const mapStateToProps = state => ({
|
const mapStateToProps = (state: any) => ({
|
||||||
user: getUserSelector(state),
|
user: getUserSelector(state),
|
||||||
server: state.share.server.server || state.server.server,
|
server: state.share.server.server || state.server.server,
|
||||||
serverVersion: state.share.server.version || state.server.version,
|
serverVersion: state.share.server.version || state.server.version,
|
||||||
blockUnauthenticatedAccess:
|
blockUnauthenticatedAccess:
|
||||||
state.share.settings?.Accounts_AvatarBlockUnauthenticatedAccess
|
state.share.settings?.Accounts_AvatarBlockUnauthenticatedAccess ??
|
||||||
?? state.settings.Accounts_AvatarBlockUnauthenticatedAccess
|
state.settings.Accounts_AvatarBlockUnauthenticatedAccess ??
|
||||||
?? true
|
true
|
||||||
});
|
});
|
||||||
export default connect(mapStateToProps)(AvatarContainer);
|
export default connect(mapStateToProps)(AvatarContainer);
|
|
@ -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;
|
||||||
|
}
|
|
@ -2,48 +2,30 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { storiesOf } from '@storybook/react-native';
|
import { storiesOf } from '@storybook/react-native';
|
||||||
|
|
||||||
import BackgroundContainer from '.';
|
|
||||||
import { ThemeContext } from '../../theme';
|
import { ThemeContext } from '../../theme';
|
||||||
import { longText } from '../../../storybook/utils';
|
import { longText } from '../../../storybook/utils';
|
||||||
|
import BackgroundContainer from '.';
|
||||||
|
|
||||||
const stories = storiesOf('BackgroundContainer', module);
|
const stories = storiesOf('BackgroundContainer', module);
|
||||||
|
|
||||||
stories.add('basic', () => (
|
stories.add('basic', () => <BackgroundContainer />);
|
||||||
<BackgroundContainer />
|
|
||||||
));
|
|
||||||
|
|
||||||
stories.add('loading', () => (
|
stories.add('loading', () => <BackgroundContainer loading />);
|
||||||
<BackgroundContainer loading />
|
|
||||||
));
|
|
||||||
|
|
||||||
stories.add('text', () => (
|
stories.add('text', () => <BackgroundContainer text='Text here' />);
|
||||||
<BackgroundContainer text='Text here' />
|
|
||||||
));
|
|
||||||
|
|
||||||
stories.add('long text', () => (
|
stories.add('long text', () => <BackgroundContainer text={longText} />);
|
||||||
<BackgroundContainer text={longText} />
|
|
||||||
));
|
|
||||||
|
|
||||||
const ThemeStory = ({ theme, ...props }) => (
|
const ThemeStory = ({ theme, ...props }) => (
|
||||||
<ThemeContext.Provider
|
<ThemeContext.Provider value={{ theme }}>
|
||||||
value={{ theme }}
|
|
||||||
>
|
|
||||||
<BackgroundContainer {...props} />
|
<BackgroundContainer {...props} />
|
||||||
</ThemeContext.Provider>
|
</ThemeContext.Provider>
|
||||||
);
|
);
|
||||||
|
|
||||||
stories.add('dark theme - loading', () => (
|
stories.add('dark theme - loading', () => <ThemeStory theme='dark' loading />);
|
||||||
<ThemeStory theme='dark' loading />
|
|
||||||
));
|
|
||||||
|
|
||||||
stories.add('dark theme - text', () => (
|
stories.add('dark theme - text', () => <ThemeStory theme='dark' text={longText} />);
|
||||||
<ThemeStory theme='dark' text={longText} />
|
|
||||||
));
|
|
||||||
|
|
||||||
stories.add('black theme - loading', () => (
|
stories.add('black theme - loading', () => <ThemeStory theme='black' loading />);
|
||||||
<ThemeStory theme='black' loading />
|
|
||||||
));
|
|
||||||
|
|
||||||
stories.add('black theme - text', () => (
|
stories.add('black theme - text', () => <ThemeStory theme='black' text={longText} />);
|
||||||
<ThemeStory theme='black' text={longText} />
|
|
||||||
));
|
|
||||||
|
|
|
@ -1,13 +1,16 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import {
|
import { ActivityIndicator, ImageBackground, StyleSheet, Text, View } from 'react-native';
|
||||||
ImageBackground, StyleSheet, Text, View, ActivityIndicator
|
|
||||||
} from 'react-native';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
|
|
||||||
import { withTheme } from '../../theme';
|
import { withTheme } from '../../theme';
|
||||||
import sharedStyles from '../../views/Styles';
|
import sharedStyles from '../../views/Styles';
|
||||||
import { themes } from '../../constants/colors';
|
import { themes } from '../../constants/colors';
|
||||||
|
|
||||||
|
interface IBackgroundContainer {
|
||||||
|
text: string;
|
||||||
|
theme: string;
|
||||||
|
loading: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
const styles = StyleSheet.create({
|
||||||
container: {
|
container: {
|
||||||
flex: 1
|
flex: 1
|
||||||
|
@ -29,17 +32,12 @@ const styles = StyleSheet.create({
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const BackgroundContainer = ({ theme, text, loading }) => (
|
const BackgroundContainer = ({ theme, text, loading }: IBackgroundContainer) => (
|
||||||
<View style={styles.container}>
|
<View style={styles.container}>
|
||||||
<ImageBackground source={{ uri: `message_empty_${ theme }` }} style={styles.image} />
|
<ImageBackground source={{ uri: `message_empty_${theme}` }} style={styles.image} />
|
||||||
{text ? <Text style={[styles.text, { color: themes[theme].auxiliaryTintColor }]}>{text}</Text> : null}
|
{text ? <Text style={[styles.text, { color: themes[theme].auxiliaryTintColor }]}>{text}</Text> : null}
|
||||||
{loading ? <ActivityIndicator style={[styles.text, { color: themes[theme].auxiliaryTintColor }]} /> : null}
|
{loading ? <ActivityIndicator style={styles.text} color={themes[theme].auxiliaryTintColor} /> : null}
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
|
|
||||||
BackgroundContainer.propTypes = {
|
|
||||||
text: PropTypes.string,
|
|
||||||
theme: PropTypes.string,
|
|
||||||
loading: PropTypes.bool
|
|
||||||
};
|
|
||||||
export default withTheme(BackgroundContainer);
|
export default withTheme(BackgroundContainer);
|
|
@ -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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,10 +1,13 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { StyleSheet } from 'react-native';
|
import { StyleSheet } from 'react-native';
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
|
|
||||||
import { CustomIcon } from '../lib/Icons';
|
import { CustomIcon } from '../lib/Icons';
|
||||||
import { themes } from '../constants/colors';
|
import { themes } from '../constants/colors';
|
||||||
|
|
||||||
|
interface ICheck {
|
||||||
|
style?: object;
|
||||||
|
theme: string;
|
||||||
|
}
|
||||||
const styles = StyleSheet.create({
|
const styles = StyleSheet.create({
|
||||||
icon: {
|
icon: {
|
||||||
width: 22,
|
width: 22,
|
||||||
|
@ -13,11 +16,8 @@ const styles = StyleSheet.create({
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const Check = React.memo(({ theme, style }) => <CustomIcon style={[styles.icon, style]} color={themes[theme].tintColor} size={22} name='check' />);
|
const Check = React.memo(({ theme, style }: ICheck) => (
|
||||||
|
<CustomIcon style={[styles.icon, style]} color={themes[theme].tintColor} size={22} name='check' />
|
||||||
Check.propTypes = {
|
));
|
||||||
style: PropTypes.object,
|
|
||||||
theme: PropTypes.string
|
|
||||||
};
|
|
||||||
|
|
||||||
export default Check;
|
export default Check;
|
|
@ -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;
|
|
|
@ -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;
|
|
@ -1,44 +1,41 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import { FlatList, Text, TouchableOpacity } from 'react-native';
|
||||||
import { Text, TouchableOpacity, FlatList } from 'react-native';
|
|
||||||
|
|
||||||
import shortnameToUnicode from '../../utils/shortnameToUnicode';
|
import shortnameToUnicode from '../../utils/shortnameToUnicode';
|
||||||
import styles from './styles';
|
import styles from './styles';
|
||||||
import CustomEmoji from './CustomEmoji';
|
import CustomEmoji from './CustomEmoji';
|
||||||
import scrollPersistTaps from '../../utils/scrollPersistTaps';
|
import scrollPersistTaps from '../../utils/scrollPersistTaps';
|
||||||
|
import { IEmoji, IEmojiCategory } from './interfaces';
|
||||||
|
|
||||||
const EMOJI_SIZE = 50;
|
const EMOJI_SIZE = 50;
|
||||||
|
|
||||||
const renderEmoji = (emoji, size, baseUrl) => {
|
const renderEmoji = (emoji: IEmoji, size: number, baseUrl: string) => {
|
||||||
if (emoji && emoji.isCustom) {
|
if (emoji && emoji.isCustom) {
|
||||||
return <CustomEmoji style={[styles.customCategoryEmoji, { height: size - 16, width: size - 16 }]} emoji={emoji} baseUrl={baseUrl} />;
|
return (
|
||||||
|
<CustomEmoji
|
||||||
|
style={[styles.customCategoryEmoji, { height: size - 16, width: size - 16 }]}
|
||||||
|
emoji={emoji}
|
||||||
|
baseUrl={baseUrl}
|
||||||
|
/>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<Text style={[styles.categoryEmoji, { height: size, width: size, fontSize: size - 14 }]}>
|
<Text style={[styles.categoryEmoji, { height: size, width: size, fontSize: size - 14 }]}>
|
||||||
{shortnameToUnicode(`:${ emoji }:`)}
|
{shortnameToUnicode(`:${emoji}:`)}
|
||||||
</Text>
|
</Text>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
class EmojiCategory extends React.Component {
|
class EmojiCategory extends React.Component<Partial<IEmojiCategory>> {
|
||||||
static propTypes = {
|
renderItem(emoji: any) {
|
||||||
baseUrl: PropTypes.string.isRequired,
|
|
||||||
emojis: PropTypes.any,
|
|
||||||
onEmojiSelected: PropTypes.func,
|
|
||||||
emojisPerRow: PropTypes.number,
|
|
||||||
width: PropTypes.number
|
|
||||||
}
|
|
||||||
|
|
||||||
renderItem(emoji) {
|
|
||||||
const { baseUrl, onEmojiSelected } = this.props;
|
const { baseUrl, onEmojiSelected } = this.props;
|
||||||
return (
|
return (
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
activeOpacity={0.7}
|
activeOpacity={0.7}
|
||||||
key={emoji && emoji.isCustom ? emoji.content : emoji}
|
key={emoji && emoji.isCustom ? emoji.content : emoji}
|
||||||
onPress={() => onEmojiSelected(emoji)}
|
onPress={() => onEmojiSelected!(emoji)}
|
||||||
testID={`reaction-picker-${ emoji && emoji.isCustom ? emoji.content : emoji }`}
|
testID={`reaction-picker-${emoji && emoji.isCustom ? emoji.content : emoji}`}>
|
||||||
>
|
{renderEmoji(emoji, EMOJI_SIZE, baseUrl!)}
|
||||||
{renderEmoji(emoji, EMOJI_SIZE, baseUrl)}
|
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -51,13 +48,14 @@ class EmojiCategory extends React.Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
const numColumns = Math.trunc(width / EMOJI_SIZE);
|
const numColumns = Math.trunc(width / EMOJI_SIZE);
|
||||||
const marginHorizontal = (width - (numColumns * EMOJI_SIZE)) / 2;
|
const marginHorizontal = (width - numColumns * EMOJI_SIZE) / 2;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
// @ts-ignore
|
||||||
<FlatList
|
<FlatList
|
||||||
contentContainerStyle={{ marginHorizontal }}
|
contentContainerStyle={{ marginHorizontal }}
|
||||||
// rerender FlatList in case of width changes
|
// rerender FlatList in case of width changes
|
||||||
key={`emoji-category-${ width }`}
|
key={`emoji-category-${width}`}
|
||||||
keyExtractor={item => (item && item.isCustom && item.content) || item}
|
keyExtractor={item => (item && item.isCustom && item.content) || item}
|
||||||
data={emojis}
|
data={emojis}
|
||||||
extraData={this.props}
|
extraData={this.props}
|
|
@ -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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,6 +1,5 @@
|
||||||
import React, { Component } from 'react';
|
import React, { Component } from 'react';
|
||||||
import { View } from 'react-native';
|
import { View } from 'react-native';
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import ScrollableTabView from 'react-native-scrollable-tab-view';
|
import ScrollableTabView from 'react-native-scrollable-tab-view';
|
||||||
import { dequal } from 'dequal';
|
import { dequal } from 'dequal';
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
|
@ -18,22 +17,33 @@ import shortnameToUnicode from '../../utils/shortnameToUnicode';
|
||||||
import log from '../../utils/log';
|
import log from '../../utils/log';
|
||||||
import { themes } from '../../constants/colors';
|
import { themes } from '../../constants/colors';
|
||||||
import { withTheme } from '../../theme';
|
import { withTheme } from '../../theme';
|
||||||
|
import { IEmoji } from './interfaces';
|
||||||
|
|
||||||
const scrollProps = {
|
const scrollProps = {
|
||||||
keyboardShouldPersistTaps: 'always',
|
keyboardShouldPersistTaps: 'always',
|
||||||
keyboardDismissMode: 'none'
|
keyboardDismissMode: 'none'
|
||||||
};
|
};
|
||||||
|
|
||||||
class EmojiPicker extends Component {
|
interface IEmojiPickerProps {
|
||||||
static propTypes = {
|
isMessageContainsOnlyEmoji: boolean;
|
||||||
baseUrl: PropTypes.string.isRequired,
|
getCustomEmoji?: Function;
|
||||||
customEmojis: PropTypes.object,
|
baseUrl: string;
|
||||||
onEmojiSelected: PropTypes.func,
|
customEmojis?: any;
|
||||||
tabEmojiStyle: PropTypes.object,
|
style: object;
|
||||||
theme: PropTypes.string
|
theme?: string;
|
||||||
};
|
onEmojiSelected?: Function;
|
||||||
|
tabEmojiStyle?: object;
|
||||||
|
}
|
||||||
|
|
||||||
constructor(props) {
|
interface IEmojiPickerState {
|
||||||
|
frequentlyUsed: [];
|
||||||
|
customEmojis: any;
|
||||||
|
show: boolean;
|
||||||
|
width: number | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
class EmojiPicker extends Component<IEmojiPickerProps, IEmojiPickerState> {
|
||||||
|
constructor(props: IEmojiPickerProps) {
|
||||||
super(props);
|
super(props);
|
||||||
const customEmojis = Object.keys(props.customEmojis)
|
const customEmojis = Object.keys(props.customEmojis)
|
||||||
.filter(item => item === props.customEmojis[item].name)
|
.filter(item => item === props.customEmojis[item].name)
|
||||||
|
@ -55,7 +65,7 @@ class EmojiPicker extends Component {
|
||||||
this.setState({ show: true });
|
this.setState({ show: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
shouldComponentUpdate(nextProps, nextState) {
|
shouldComponentUpdate(nextProps: any, nextState: any) {
|
||||||
const { frequentlyUsed, show, width } = this.state;
|
const { frequentlyUsed, show, width } = this.state;
|
||||||
const { theme } = this.props;
|
const { theme } = this.props;
|
||||||
if (nextProps.theme !== theme) {
|
if (nextProps.theme !== theme) {
|
||||||
|
@ -73,67 +83,72 @@ class EmojiPicker extends Component {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
onEmojiSelected = (emoji) => {
|
onEmojiSelected = (emoji: IEmoji) => {
|
||||||
try {
|
try {
|
||||||
const { onEmojiSelected } = this.props;
|
const { onEmojiSelected } = this.props;
|
||||||
if (emoji.isCustom) {
|
if (emoji.isCustom) {
|
||||||
this._addFrequentlyUsed({
|
this._addFrequentlyUsed({
|
||||||
content: emoji.content, extension: emoji.extension, isCustom: true
|
content: emoji.content,
|
||||||
|
extension: emoji.extension,
|
||||||
|
isCustom: true
|
||||||
});
|
});
|
||||||
onEmojiSelected(`:${ emoji.content }:`);
|
onEmojiSelected!(`:${emoji.content}:`);
|
||||||
} else {
|
} else {
|
||||||
const content = emoji;
|
const content = emoji;
|
||||||
this._addFrequentlyUsed({ content, isCustom: false });
|
this._addFrequentlyUsed({ content, isCustom: false });
|
||||||
const shortname = `:${ emoji }:`;
|
const shortname = `:${emoji}:`;
|
||||||
onEmojiSelected(shortnameToUnicode(shortname), shortname);
|
onEmojiSelected!(shortnameToUnicode(shortname), shortname);
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
log(e);
|
log(e);
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
// eslint-disable-next-line react/sort-comp
|
_addFrequentlyUsed = protectedFunction(async (emoji: IEmoji) => {
|
||||||
_addFrequentlyUsed = protectedFunction(async(emoji) => {
|
|
||||||
const db = database.active;
|
const db = database.active;
|
||||||
const freqEmojiCollection = db.get('frequently_used_emojis');
|
const freqEmojiCollection = db.get('frequently_used_emojis');
|
||||||
let freqEmojiRecord;
|
let freqEmojiRecord: any;
|
||||||
try {
|
try {
|
||||||
freqEmojiRecord = await freqEmojiCollection.find(emoji.content);
|
freqEmojiRecord = await freqEmojiCollection.find(emoji.content);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// Do nothing
|
// Do nothing
|
||||||
}
|
}
|
||||||
|
|
||||||
await db.action(async() => {
|
await db.action(async () => {
|
||||||
if (freqEmojiRecord) {
|
if (freqEmojiRecord) {
|
||||||
await freqEmojiRecord.update((f) => {
|
await freqEmojiRecord.update((f: any) => {
|
||||||
f.count += 1;
|
f.count += 1;
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
await freqEmojiCollection.create((f) => {
|
await freqEmojiCollection.create((f: any) => {
|
||||||
f._raw = sanitizedRaw({ id: emoji.content }, freqEmojiCollection.schema);
|
f._raw = sanitizedRaw({ id: emoji.content }, freqEmojiCollection.schema);
|
||||||
Object.assign(f, emoji);
|
Object.assign(f, emoji);
|
||||||
f.count = 1;
|
f.count = 1;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
})
|
});
|
||||||
|
|
||||||
updateFrequentlyUsed = async() => {
|
updateFrequentlyUsed = async () => {
|
||||||
const db = database.active;
|
const db = database.active;
|
||||||
const frequentlyUsedRecords = await db.get('frequently_used_emojis').query().fetch();
|
const frequentlyUsedRecords = await db.get('frequently_used_emojis').query().fetch();
|
||||||
let frequentlyUsed = orderBy(frequentlyUsedRecords, ['count'], ['desc']);
|
let frequentlyUsed: any = orderBy(frequentlyUsedRecords, ['count'], ['desc']);
|
||||||
frequentlyUsed = frequentlyUsed.map((item) => {
|
frequentlyUsed = frequentlyUsed.map((item: IEmoji) => {
|
||||||
if (item.isCustom) {
|
if (item.isCustom) {
|
||||||
return { content: item.content, extension: item.extension, isCustom: item.isCustom };
|
return { content: item.content, extension: item.extension, isCustom: item.isCustom };
|
||||||
}
|
}
|
||||||
return shortnameToUnicode(`${ item.content }`);
|
return shortnameToUnicode(`${item.content}`);
|
||||||
});
|
});
|
||||||
this.setState({ frequentlyUsed });
|
this.setState({ frequentlyUsed });
|
||||||
}
|
};
|
||||||
|
|
||||||
onLayout = ({ nativeEvent: { layout: { width } } }) => this.setState({ width });
|
onLayout = ({
|
||||||
|
nativeEvent: {
|
||||||
|
layout: { width }
|
||||||
|
}
|
||||||
|
}: any) => this.setState({ width });
|
||||||
|
|
||||||
renderCategory(category, i, label) {
|
renderCategory(category: any, i: number, label: string) {
|
||||||
const { frequentlyUsed, customEmojis, width } = this.state;
|
const { frequentlyUsed, customEmojis, width } = this.state;
|
||||||
const { baseUrl } = this.props;
|
const { baseUrl } = this.props;
|
||||||
|
|
||||||
|
@ -148,9 +163,9 @@ class EmojiPicker extends Component {
|
||||||
return (
|
return (
|
||||||
<EmojiCategory
|
<EmojiCategory
|
||||||
emojis={emojis}
|
emojis={emojis}
|
||||||
onEmojiSelected={emoji => this.onEmojiSelected(emoji)}
|
onEmojiSelected={(emoji: IEmoji) => this.onEmojiSelected(emoji)}
|
||||||
style={styles.categoryContainer}
|
style={styles.categoryContainer}
|
||||||
width={width}
|
width={width!}
|
||||||
baseUrl={baseUrl}
|
baseUrl={baseUrl}
|
||||||
tabLabel={label}
|
tabLabel={label}
|
||||||
/>
|
/>
|
||||||
|
@ -168,23 +183,21 @@ class EmojiPicker extends Component {
|
||||||
<View onLayout={this.onLayout} style={{ flex: 1 }}>
|
<View onLayout={this.onLayout} style={{ flex: 1 }}>
|
||||||
<ScrollableTabView
|
<ScrollableTabView
|
||||||
renderTabBar={() => <TabBar tabEmojiStyle={tabEmojiStyle} theme={theme} />}
|
renderTabBar={() => <TabBar tabEmojiStyle={tabEmojiStyle} theme={theme} />}
|
||||||
|
/* @ts-ignore*/
|
||||||
contentProps={scrollProps}
|
contentProps={scrollProps}
|
||||||
style={{ backgroundColor: themes[theme].focusedBackground }}
|
style={{ backgroundColor: themes[theme!].focusedBackground }}>
|
||||||
>
|
{categories.tabs.map((tab, i) =>
|
||||||
{
|
i === 0 && frequentlyUsed.length === 0
|
||||||
categories.tabs.map((tab, i) => (
|
? null // when no frequentlyUsed don't show the tab
|
||||||
(i === 0 && frequentlyUsed.length === 0) ? null // when no frequentlyUsed don't show the tab
|
: this.renderCategory(tab.category, i, tab.tabLabel)
|
||||||
: (
|
)}
|
||||||
this.renderCategory(tab.category, i, tab.tabLabel)
|
|
||||||
)))
|
|
||||||
}
|
|
||||||
</ScrollableTabView>
|
</ScrollableTabView>
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const mapStateToProps = state => ({
|
const mapStateToProps = (state: any) => ({
|
||||||
customEmojis: state.customEmojis
|
customEmojis: state.customEmojis
|
||||||
});
|
});
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
|
@ -1,6 +1,5 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { ScrollView, StyleSheet, View } from 'react-native';
|
import { ScrollView, StyleSheet, View } from 'react-native';
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
|
|
||||||
import { themes } from '../constants/colors';
|
import { themes } from '../constants/colors';
|
||||||
import sharedStyles from '../views/Styles';
|
import sharedStyles from '../views/Styles';
|
||||||
|
@ -11,33 +10,35 @@ import AppVersion from './AppVersion';
|
||||||
import { isTablet } from '../utils/deviceInfo';
|
import { isTablet } from '../utils/deviceInfo';
|
||||||
import SafeAreaView from './SafeAreaView';
|
import SafeAreaView from './SafeAreaView';
|
||||||
|
|
||||||
|
interface IFormContainer {
|
||||||
|
theme: string;
|
||||||
|
testID: string;
|
||||||
|
children: JSX.Element;
|
||||||
|
}
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
const styles = StyleSheet.create({
|
||||||
scrollView: {
|
scrollView: {
|
||||||
minHeight: '100%'
|
minHeight: '100%'
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
export const FormContainerInner = ({ children }) => (
|
export const FormContainerInner = ({ children }: { children: JSX.Element }) => (
|
||||||
<View style={[sharedStyles.container, isTablet && sharedStyles.tabletScreenContent]}>
|
<View style={[sharedStyles.container, isTablet && sharedStyles.tabletScreenContent]}>{children}</View>
|
||||||
{children}
|
|
||||||
</View>
|
|
||||||
);
|
);
|
||||||
|
|
||||||
const FormContainer = ({
|
const FormContainer = ({ children, theme, testID, ...props }: IFormContainer) => (
|
||||||
children, theme, testID, ...props
|
// @ts-ignore
|
||||||
}) => (
|
|
||||||
<KeyboardView
|
<KeyboardView
|
||||||
style={{ backgroundColor: themes[theme].backgroundColor }}
|
style={{ backgroundColor: themes[theme].backgroundColor }}
|
||||||
contentContainerStyle={sharedStyles.container}
|
contentContainerStyle={sharedStyles.container}
|
||||||
keyboardVerticalOffset={128}
|
keyboardVerticalOffset={128}>
|
||||||
>
|
|
||||||
<StatusBar />
|
<StatusBar />
|
||||||
|
{/* @ts-ignore*/}
|
||||||
<ScrollView
|
<ScrollView
|
||||||
style={sharedStyles.container}
|
style={sharedStyles.container}
|
||||||
contentContainerStyle={[sharedStyles.containerScrollView, styles.scrollView]}
|
contentContainerStyle={[sharedStyles.containerScrollView, styles.scrollView]}
|
||||||
{...scrollPersistTaps}
|
{...scrollPersistTaps}
|
||||||
{...props}
|
{...props}>
|
||||||
>
|
|
||||||
<SafeAreaView testID={testID} style={{ backgroundColor: themes[theme].backgroundColor }}>
|
<SafeAreaView testID={testID} style={{ backgroundColor: themes[theme].backgroundColor }}>
|
||||||
{children}
|
{children}
|
||||||
<AppVersion theme={theme} />
|
<AppVersion theme={theme} />
|
||||||
|
@ -46,14 +47,4 @@ const FormContainer = ({
|
||||||
</KeyboardView>
|
</KeyboardView>
|
||||||
);
|
);
|
||||||
|
|
||||||
FormContainer.propTypes = {
|
|
||||||
theme: PropTypes.string,
|
|
||||||
testID: PropTypes.string,
|
|
||||||
children: PropTypes.element
|
|
||||||
};
|
|
||||||
|
|
||||||
FormContainerInner.propTypes = {
|
|
||||||
children: PropTypes.element
|
|
||||||
};
|
|
||||||
|
|
||||||
export default FormContainer;
|
export default FormContainer;
|
|
@ -1,7 +1,7 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import { SafeAreaView } from 'react-native-safe-area-context';
|
import { SafeAreaView } from 'react-native-safe-area-context';
|
||||||
import { View, StyleSheet } from 'react-native';
|
import { StyleSheet, View } from 'react-native';
|
||||||
|
|
||||||
import { themes } from '../../constants/colors';
|
import { themes } from '../../constants/colors';
|
||||||
import { themedHeader } from '../../utils/navigation';
|
import { themedHeader } from '../../utils/navigation';
|
||||||
import { isIOS, isTablet } from '../../utils/deviceInfo';
|
import { isIOS, isTablet } from '../../utils/deviceInfo';
|
||||||
|
@ -10,18 +10,25 @@ import { withTheme } from '../../theme';
|
||||||
// Get from https://github.com/react-navigation/react-navigation/blob/master/packages/stack/src/views/Header/HeaderSegment.tsx#L69
|
// Get from https://github.com/react-navigation/react-navigation/blob/master/packages/stack/src/views/Header/HeaderSegment.tsx#L69
|
||||||
export const headerHeight = isIOS ? 44 : 56;
|
export const headerHeight = isIOS ? 44 : 56;
|
||||||
|
|
||||||
export const getHeaderHeight = (isLandscape) => {
|
export const getHeaderHeight = (isLandscape: boolean) => {
|
||||||
if (isIOS) {
|
if (isIOS) {
|
||||||
if (isLandscape && !isTablet) {
|
if (isLandscape && !isTablet) {
|
||||||
return 32;
|
return 32;
|
||||||
} else {
|
|
||||||
return 44;
|
|
||||||
}
|
}
|
||||||
|
return 44;
|
||||||
}
|
}
|
||||||
return 56;
|
return 56;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getHeaderTitlePosition = ({ insets, numIconsRight }) => ({
|
interface IHeaderTitlePosition {
|
||||||
|
insets: {
|
||||||
|
left: number;
|
||||||
|
right: number;
|
||||||
|
};
|
||||||
|
numIconsRight: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getHeaderTitlePosition = ({ insets, numIconsRight }: IHeaderTitlePosition) => ({
|
||||||
left: insets.left + 60,
|
left: insets.left + 60,
|
||||||
right: insets.right + Math.max(45 * numIconsRight, 15)
|
right: insets.right + Math.max(45 * numIconsRight, 15)
|
||||||
});
|
});
|
||||||
|
@ -35,9 +42,14 @@ const styles = StyleSheet.create({
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const Header = ({
|
interface IHeader {
|
||||||
theme, headerLeft, headerTitle, headerRight
|
theme: string;
|
||||||
}) => (
|
headerLeft(): void;
|
||||||
|
headerTitle(): void;
|
||||||
|
headerRight(): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const Header = ({ theme, headerLeft, headerTitle, headerRight }: IHeader) => (
|
||||||
<SafeAreaView style={{ backgroundColor: themes[theme].headerBackground }} edges={['top', 'left', 'right']}>
|
<SafeAreaView style={{ backgroundColor: themes[theme].headerBackground }} edges={['top', 'left', 'right']}>
|
||||||
<View style={[styles.container, { ...themedHeader(theme).headerStyle }]}>
|
<View style={[styles.container, { ...themedHeader(theme).headerStyle }]}>
|
||||||
{headerLeft ? headerLeft() : null}
|
{headerLeft ? headerLeft() : null}
|
||||||
|
@ -47,11 +59,4 @@ const Header = ({
|
||||||
</SafeAreaView>
|
</SafeAreaView>
|
||||||
);
|
);
|
||||||
|
|
||||||
Header.propTypes = {
|
|
||||||
theme: PropTypes.string,
|
|
||||||
headerLeft: PropTypes.element,
|
|
||||||
headerTitle: PropTypes.element,
|
|
||||||
headerRight: PropTypes.element
|
|
||||||
};
|
|
||||||
|
|
||||||
export default withTheme(Header);
|
export default withTheme(Header);
|
|
@ -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
|
|
||||||
};
|
|
|
@ -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} />
|
||||||
|
));
|
|
@ -1,6 +1,10 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { View, StyleSheet } from 'react-native';
|
import { StyleSheet, View } from 'react-native';
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
|
interface IHeaderButtonContainer {
|
||||||
|
children: JSX.Element;
|
||||||
|
left?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
const styles = StyleSheet.create({
|
||||||
container: {
|
container: {
|
||||||
|
@ -16,21 +20,10 @@ const styles = StyleSheet.create({
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const Container = ({ children, left }) => (
|
const Container = ({ children, left = false }: IHeaderButtonContainer) => (
|
||||||
<View style={[styles.container, left ? styles.left : styles.right]}>
|
<View style={[styles.container, left ? styles.left : styles.right]}>{children}</View>
|
||||||
{children}
|
|
||||||
</View>
|
|
||||||
);
|
);
|
||||||
|
|
||||||
Container.propTypes = {
|
|
||||||
children: PropTypes.arrayOf(PropTypes.element),
|
|
||||||
left: PropTypes.bool
|
|
||||||
};
|
|
||||||
|
|
||||||
Container.defaultProps = {
|
|
||||||
left: false
|
|
||||||
};
|
|
||||||
|
|
||||||
Container.displayName = 'HeaderButton.Container';
|
Container.displayName = 'HeaderButton.Container';
|
||||||
|
|
||||||
export default Container;
|
export default Container;
|
|
@ -1,6 +1,5 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Text, StyleSheet, Platform } from 'react-native';
|
import { Platform, StyleSheet, Text } from 'react-native';
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import Touchable from 'react-native-platform-touchable';
|
import Touchable from 'react-native-platform-touchable';
|
||||||
|
|
||||||
import { CustomIcon } from '../../lib/Icons';
|
import { CustomIcon } from '../../lib/Icons';
|
||||||
|
@ -8,8 +7,20 @@ import { withTheme } from '../../theme';
|
||||||
import { themes } from '../../constants/colors';
|
import { themes } from '../../constants/colors';
|
||||||
import sharedStyles from '../../views/Styles';
|
import sharedStyles from '../../views/Styles';
|
||||||
|
|
||||||
|
interface IHeaderButtonItem {
|
||||||
|
title: string;
|
||||||
|
iconName: string;
|
||||||
|
onPress(): void;
|
||||||
|
testID: string;
|
||||||
|
theme: string;
|
||||||
|
badge(): void;
|
||||||
|
}
|
||||||
|
|
||||||
export const BUTTON_HIT_SLOP = {
|
export const BUTTON_HIT_SLOP = {
|
||||||
top: 5, right: 5, bottom: 5, left: 5
|
top: 5,
|
||||||
|
right: 5,
|
||||||
|
bottom: 5,
|
||||||
|
left: 5
|
||||||
};
|
};
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
const styles = StyleSheet.create({
|
||||||
|
@ -29,30 +40,19 @@ const styles = StyleSheet.create({
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const Item = ({
|
const Item = ({ title, iconName, onPress, testID, theme, badge }: IHeaderButtonItem) => (
|
||||||
title, iconName, onPress, testID, theme, badge
|
|
||||||
}) => (
|
|
||||||
<Touchable onPress={onPress} testID={testID} hitSlop={BUTTON_HIT_SLOP} style={styles.container}>
|
<Touchable onPress={onPress} testID={testID} hitSlop={BUTTON_HIT_SLOP} style={styles.container}>
|
||||||
<>
|
<>
|
||||||
{
|
{iconName ? (
|
||||||
iconName
|
<CustomIcon name={iconName} size={24} color={themes[theme].headerTintColor} />
|
||||||
? <CustomIcon name={iconName} size={24} color={themes[theme].headerTintColor} />
|
) : (
|
||||||
: <Text style={[styles.title, { color: themes[theme].headerTintColor }]}>{title}</Text>
|
<Text style={[styles.title, { color: themes[theme].headerTintColor }]}>{title}</Text>
|
||||||
}
|
)}
|
||||||
{badge ? badge() : null}
|
{badge ? badge() : null}
|
||||||
</>
|
</>
|
||||||
</Touchable>
|
</Touchable>
|
||||||
);
|
);
|
||||||
|
|
||||||
Item.propTypes = {
|
|
||||||
onPress: PropTypes.func.isRequired,
|
|
||||||
title: PropTypes.string,
|
|
||||||
iconName: PropTypes.string,
|
|
||||||
testID: PropTypes.string,
|
|
||||||
theme: PropTypes.string,
|
|
||||||
badge: PropTypes.func
|
|
||||||
};
|
|
||||||
|
|
||||||
Item.displayName = 'HeaderButton.Item';
|
Item.displayName = 'HeaderButton.Item';
|
||||||
|
|
||||||
export default withTheme(Item);
|
export default withTheme(Item);
|
|
@ -15,12 +15,6 @@ const styles = StyleSheet.create({
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
export const Badge = ({ ...props }) => (
|
export const Badge = ({ ...props }) => <UnreadBadge {...props} style={styles.badgeContainer} small />;
|
||||||
<UnreadBadge
|
|
||||||
{...props}
|
|
||||||
style={styles.badgeContainer}
|
|
||||||
small
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
|
|
||||||
export default Badge;
|
export default Badge;
|
|
@ -1,6 +1,5 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { StyleSheet, View, Text } from 'react-native';
|
import { StyleSheet, Text, View } from 'react-native';
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import Touchable from 'react-native-platform-touchable';
|
import Touchable from 'react-native-platform-touchable';
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import { Notifier } from 'react-native-notifier';
|
import { Notifier } from 'react-native-notifier';
|
||||||
|
@ -16,10 +15,13 @@ import { goRoom } from '../../utils/goRoom';
|
||||||
import Navigation from '../../lib/Navigation';
|
import Navigation from '../../lib/Navigation';
|
||||||
import { useOrientation } from '../../dimensions';
|
import { useOrientation } from '../../dimensions';
|
||||||
|
|
||||||
|
interface INotifierComponent {
|
||||||
|
notification: object;
|
||||||
|
isMasterDetail: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
const AVATAR_SIZE = 48;
|
const AVATAR_SIZE = 48;
|
||||||
const BUTTON_HIT_SLOP = {
|
const BUTTON_HIT_SLOP = { top: 12, right: 12, bottom: 12, left: 12 };
|
||||||
top: 12, right: 12, bottom: 12, left: 12
|
|
||||||
};
|
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
const styles = StyleSheet.create({
|
||||||
container: {
|
container: {
|
||||||
|
@ -64,16 +66,16 @@ const styles = StyleSheet.create({
|
||||||
|
|
||||||
const hideNotification = () => Notifier.hideNotification();
|
const hideNotification = () => Notifier.hideNotification();
|
||||||
|
|
||||||
const NotifierComponent = React.memo(({ notification, isMasterDetail }) => {
|
const NotifierComponent = React.memo(({ notification, isMasterDetail }: INotifierComponent) => {
|
||||||
const { theme } = useTheme();
|
const { theme }: any = useTheme();
|
||||||
const insets = useSafeAreaInsets();
|
const insets = useSafeAreaInsets();
|
||||||
const { isLandscape } = useOrientation();
|
const { isLandscape } = useOrientation();
|
||||||
|
|
||||||
const { text, payload } = notification;
|
const { text, payload }: any = notification;
|
||||||
const { type, rid } = payload;
|
const { type, rid } = payload;
|
||||||
const name = type === 'd' ? payload.sender.username : payload.name;
|
const name = type === 'd' ? payload.sender.username : payload.name;
|
||||||
// if sub is not on local database, title and avatar will be null, so we use payload from notification
|
// if sub is not on local database, title and avatar will be null, so we use payload from notification
|
||||||
const { title = name, avatar = name } = notification;
|
const { title = name, avatar = name }: any = notification;
|
||||||
|
|
||||||
const onPress = () => {
|
const onPress = () => {
|
||||||
const { prid, _id } = payload;
|
const { prid, _id } = payload;
|
||||||
|
@ -81,7 +83,10 @@ const NotifierComponent = React.memo(({ notification, isMasterDetail }) => {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const item = {
|
const item = {
|
||||||
rid, name: title, t: type, prid
|
rid,
|
||||||
|
name: title,
|
||||||
|
t: type,
|
||||||
|
prid
|
||||||
};
|
};
|
||||||
|
|
||||||
if (isMasterDetail) {
|
if (isMasterDetail) {
|
||||||
|
@ -94,47 +99,41 @@ const NotifierComponent = React.memo(({ notification, isMasterDetail }) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View style={[
|
<View
|
||||||
styles.container,
|
style={[
|
||||||
(isMasterDetail || isLandscape) && styles.small,
|
styles.container,
|
||||||
{
|
(isMasterDetail || isLandscape) && styles.small,
|
||||||
backgroundColor: themes[theme].focusedBackground,
|
{
|
||||||
borderColor: themes[theme].separatorColor,
|
backgroundColor: themes[theme].focusedBackground,
|
||||||
marginTop: insets.top
|
borderColor: themes[theme].separatorColor,
|
||||||
}
|
marginTop: insets.top
|
||||||
]}
|
}
|
||||||
>
|
]}>
|
||||||
<Touchable
|
<Touchable
|
||||||
style={styles.content}
|
style={styles.content}
|
||||||
onPress={onPress}
|
onPress={onPress}
|
||||||
hitSlop={BUTTON_HIT_SLOP}
|
hitSlop={BUTTON_HIT_SLOP}
|
||||||
background={Touchable.SelectableBackgroundBorderless()}
|
background={Touchable.SelectableBackgroundBorderless()}>
|
||||||
>
|
|
||||||
<>
|
<>
|
||||||
<Avatar text={avatar} size={AVATAR_SIZE} type={type} rid={rid} style={styles.avatar} />
|
<Avatar text={avatar} size={AVATAR_SIZE} type={type} rid={rid} style={styles.avatar} />
|
||||||
<View style={styles.inner}>
|
<View style={styles.inner}>
|
||||||
<Text style={[styles.roomName, { color: themes[theme].titleText }]} numberOfLines={1}>{title}</Text>
|
<Text style={[styles.roomName, { color: themes[theme].titleText }]} numberOfLines={1}>
|
||||||
<Text style={[styles.message, { color: themes[theme].titleText }]} numberOfLines={1}>{text}</Text>
|
{title}
|
||||||
|
</Text>
|
||||||
|
<Text style={[styles.message, { color: themes[theme].titleText }]} numberOfLines={1}>
|
||||||
|
{text}
|
||||||
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
</>
|
</>
|
||||||
</Touchable>
|
</Touchable>
|
||||||
<Touchable
|
<Touchable onPress={hideNotification} hitSlop={BUTTON_HIT_SLOP} background={Touchable.SelectableBackgroundBorderless()}>
|
||||||
onPress={hideNotification}
|
|
||||||
hitSlop={BUTTON_HIT_SLOP}
|
|
||||||
background={Touchable.SelectableBackgroundBorderless()}
|
|
||||||
>
|
|
||||||
<CustomIcon name='close' style={[styles.close, { color: themes[theme].titleText }]} size={20} />
|
<CustomIcon name='close' style={[styles.close, { color: themes[theme].titleText }]} size={20} />
|
||||||
</Touchable>
|
</Touchable>
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
NotifierComponent.propTypes = {
|
const mapStateToProps = (state: any) => ({
|
||||||
notification: PropTypes.object,
|
|
||||||
isMasterDetail: PropTypes.bool
|
|
||||||
};
|
|
||||||
|
|
||||||
const mapStateToProps = state => ({
|
|
||||||
isMasterDetail: state.app.isMasterDetail
|
isMasterDetail: state.app.isMasterDetail
|
||||||
});
|
});
|
||||||
|
|
|
@ -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);
|
|
|
@ -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);
|
|
@ -1,6 +1,6 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { ScrollView, StyleSheet } from 'react-native';
|
import { ScrollView, StyleSheet } from 'react-native';
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import { withTheme } from '../../theme';
|
import { withTheme } from '../../theme';
|
||||||
import scrollPersistTaps from '../../utils/scrollPersistTaps';
|
import scrollPersistTaps from '../../utils/scrollPersistTaps';
|
||||||
|
|
||||||
|
@ -10,21 +10,20 @@ const styles = StyleSheet.create({
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const ListContainer = React.memo(({ children, ...props }) => (
|
interface IListContainer {
|
||||||
|
children: JSX.Element;
|
||||||
|
}
|
||||||
|
const ListContainer = React.memo(({ children, ...props }: IListContainer) => (
|
||||||
|
// @ts-ignore
|
||||||
<ScrollView
|
<ScrollView
|
||||||
contentContainerStyle={styles.container}
|
contentContainerStyle={styles.container}
|
||||||
scrollIndicatorInsets={{ right: 1 }} // https://github.com/facebook/react-native/issues/26610#issuecomment-539843444
|
scrollIndicatorInsets={{ right: 1 }} // https://github.com/facebook/react-native/issues/26610#issuecomment-539843444
|
||||||
{...scrollPersistTaps}
|
{...scrollPersistTaps}
|
||||||
{...props}
|
{...props}>
|
||||||
>
|
|
||||||
{children}
|
{children}
|
||||||
</ScrollView>
|
</ScrollView>
|
||||||
));
|
));
|
||||||
|
|
||||||
ListContainer.propTypes = {
|
|
||||||
children: PropTypes.array.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
ListContainer.displayName = 'List.Container';
|
ListContainer.displayName = 'List.Container';
|
||||||
|
|
||||||
export default withTheme(ListContainer);
|
export default withTheme(ListContainer);
|
|
@ -1,6 +1,5 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { View, Text, StyleSheet } from 'react-native';
|
import { StyleSheet, Text, View } from 'react-native';
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
|
|
||||||
import sharedStyles from '../../views/Styles';
|
import sharedStyles from '../../views/Styles';
|
||||||
import { themes } from '../../constants/colors';
|
import { themes } from '../../constants/colors';
|
||||||
|
@ -19,22 +18,20 @@ const styles = StyleSheet.create({
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const ListHeader = React.memo(({ title, theme, translateTitle }) => (
|
interface IListHeader {
|
||||||
|
title: string;
|
||||||
|
theme: string;
|
||||||
|
translateTitle: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ListHeader = React.memo(({ title, theme, translateTitle = true }: IListHeader) => (
|
||||||
<View style={styles.container}>
|
<View style={styles.container}>
|
||||||
<Text style={[styles.title, { color: themes[theme].infoText }]} numberOfLines={1}>{translateTitle ? I18n.t(title) : title}</Text>
|
<Text style={[styles.title, { color: themes[theme].infoText }]} numberOfLines={1}>
|
||||||
|
{translateTitle ? I18n.t(title) : title}
|
||||||
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
));
|
));
|
||||||
|
|
||||||
ListHeader.propTypes = {
|
|
||||||
title: PropTypes.string,
|
|
||||||
theme: PropTypes.string,
|
|
||||||
translateTitle: PropTypes.bool
|
|
||||||
};
|
|
||||||
|
|
||||||
ListHeader.defaultProps = {
|
|
||||||
translateTitle: true
|
|
||||||
};
|
|
||||||
|
|
||||||
ListHeader.displayName = 'List.Header';
|
ListHeader.displayName = 'List.Header';
|
||||||
|
|
||||||
export default withTheme(ListHeader);
|
export default withTheme(ListHeader);
|
|
@ -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);
|
|
|
@ -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);
|
|
@ -1,6 +1,5 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { View, Text, StyleSheet } from 'react-native';
|
import { StyleSheet, Text, View } from 'react-native';
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
|
|
||||||
import sharedStyles from '../../views/Styles';
|
import sharedStyles from '../../views/Styles';
|
||||||
import { themes } from '../../constants/colors';
|
import { themes } from '../../constants/colors';
|
||||||
|
@ -19,22 +18,18 @@ const styles = StyleSheet.create({
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const ListInfo = React.memo(({ info, translateInfo, theme }) => (
|
interface IListHeader {
|
||||||
|
info: string;
|
||||||
|
theme: string;
|
||||||
|
translateInfo: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ListInfo = React.memo(({ info, theme, translateInfo = true }: IListHeader) => (
|
||||||
<View style={styles.container}>
|
<View style={styles.container}>
|
||||||
<Text style={[styles.text, { color: themes[theme].infoText }]}>{translateInfo ? I18n.t(info) : info}</Text>
|
<Text style={[styles.text, { color: themes[theme].infoText }]}>{translateInfo ? I18n.t(info) : info}</Text>
|
||||||
</View>
|
</View>
|
||||||
));
|
));
|
||||||
|
|
||||||
ListInfo.propTypes = {
|
|
||||||
info: PropTypes.string,
|
|
||||||
theme: PropTypes.string,
|
|
||||||
translateInfo: PropTypes.bool
|
|
||||||
};
|
|
||||||
|
|
||||||
ListInfo.defaultProps = {
|
|
||||||
translateInfo: true
|
|
||||||
};
|
|
||||||
|
|
||||||
ListInfo.displayName = 'List.Info';
|
ListInfo.displayName = 'List.Info';
|
||||||
|
|
||||||
export default withTheme(ListInfo);
|
export default withTheme(ListInfo);
|
|
@ -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));
|
|
|
@ -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));
|
|
@ -1,6 +1,6 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { View, StyleSheet } from 'react-native';
|
import { StyleSheet, View } from 'react-native';
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import { withTheme } from '../../theme';
|
import { withTheme } from '../../theme';
|
||||||
import { Header } from '.';
|
import { Header } from '.';
|
||||||
|
|
||||||
|
@ -10,19 +10,19 @@ const styles = StyleSheet.create({
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const ListSection = React.memo(({ children, title, translateTitle }) => (
|
interface IListSection {
|
||||||
|
children: JSX.Element;
|
||||||
|
title: string;
|
||||||
|
translateTitle: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ListSection = React.memo(({ children, title, translateTitle }: IListSection) => (
|
||||||
<View style={styles.container}>
|
<View style={styles.container}>
|
||||||
{title ? <Header {...{ title, translateTitle }} /> : null}
|
{title ? <Header {...{ title, translateTitle }} /> : null}
|
||||||
{children}
|
{children}
|
||||||
</View>
|
</View>
|
||||||
));
|
));
|
||||||
|
|
||||||
ListSection.propTypes = {
|
|
||||||
children: PropTypes.array.isRequired,
|
|
||||||
title: PropTypes.string,
|
|
||||||
translateTitle: PropTypes.bool
|
|
||||||
};
|
|
||||||
|
|
||||||
ListSection.displayName = 'List.Section';
|
ListSection.displayName = 'List.Section';
|
||||||
|
|
||||||
export default withTheme(ListSection);
|
export default withTheme(ListSection);
|
|
@ -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);
|
|
|
@ -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);
|
|
@ -1,8 +1,6 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import { Animated, Modal, StyleSheet, View } from 'react-native';
|
||||||
import {
|
|
||||||
StyleSheet, Modal, Animated, View
|
|
||||||
} from 'react-native';
|
|
||||||
import { withTheme } from '../theme';
|
import { withTheme } from '../theme';
|
||||||
import { themes } from '../constants/colors';
|
import { themes } from '../constants/colors';
|
||||||
|
|
||||||
|
@ -19,54 +17,51 @@ const styles = StyleSheet.create({
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
class Loading extends React.PureComponent {
|
interface ILoadingProps {
|
||||||
static propTypes = {
|
visible: boolean;
|
||||||
visible: PropTypes.bool,
|
theme: string;
|
||||||
theme: PropTypes.string
|
}
|
||||||
}
|
|
||||||
|
|
||||||
|
class Loading extends React.PureComponent<ILoadingProps, any> {
|
||||||
state = {
|
state = {
|
||||||
scale: new Animated.Value(1),
|
scale: new Animated.Value(1),
|
||||||
opacity: new Animated.Value(0)
|
opacity: new Animated.Value(0)
|
||||||
}
|
};
|
||||||
|
|
||||||
|
private opacityAnimation: any;
|
||||||
|
|
||||||
|
private scaleAnimation: any;
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
const { opacity, scale } = this.state;
|
const { opacity, scale } = this.state;
|
||||||
const { visible } = this.props;
|
const { visible } = this.props;
|
||||||
|
|
||||||
this.opacityAnimation = Animated.timing(
|
this.opacityAnimation = Animated.timing(opacity, {
|
||||||
opacity,
|
toValue: 1,
|
||||||
{
|
duration: 200,
|
||||||
toValue: 1,
|
useNativeDriver: true
|
||||||
duration: 200,
|
});
|
||||||
useNativeDriver: true
|
this.scaleAnimation = Animated.loop(
|
||||||
}
|
Animated.sequence([
|
||||||
);
|
Animated.timing(scale, {
|
||||||
this.scaleAnimation = Animated.loop(Animated.sequence([
|
|
||||||
Animated.timing(
|
|
||||||
scale,
|
|
||||||
{
|
|
||||||
toValue: 0,
|
toValue: 0,
|
||||||
duration: 1000,
|
duration: 1000,
|
||||||
useNativeDriver: true
|
useNativeDriver: true
|
||||||
}
|
}),
|
||||||
),
|
Animated.timing(scale, {
|
||||||
Animated.timing(
|
|
||||||
scale,
|
|
||||||
{
|
|
||||||
toValue: 1,
|
toValue: 1,
|
||||||
duration: 1000,
|
duration: 1000,
|
||||||
useNativeDriver: true
|
useNativeDriver: true
|
||||||
}
|
})
|
||||||
)
|
])
|
||||||
]));
|
);
|
||||||
|
|
||||||
if (visible) {
|
if (visible) {
|
||||||
this.startAnimations();
|
this.startAnimations();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidUpdate(prevProps) {
|
componentDidUpdate(prevProps: any) {
|
||||||
const { visible } = this.props;
|
const { visible } = this.props;
|
||||||
if (visible && visible !== prevProps.visible) {
|
if (visible && visible !== prevProps.visible) {
|
||||||
this.startAnimations();
|
this.startAnimations();
|
||||||
|
@ -107,29 +102,30 @@ class Loading extends React.PureComponent {
|
||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal
|
<Modal visible={visible} transparent onRequestClose={() => {}}>
|
||||||
visible={visible}
|
<View style={styles.container} testID='loading'>
|
||||||
transparent
|
|
||||||
onRequestClose={() => {}}
|
|
||||||
>
|
|
||||||
<View
|
|
||||||
style={styles.container}
|
|
||||||
testID='loading'
|
|
||||||
>
|
|
||||||
<Animated.View
|
<Animated.View
|
||||||
style={[{
|
style={[
|
||||||
...StyleSheet.absoluteFill,
|
{
|
||||||
backgroundColor: themes[theme].backdropColor,
|
// @ts-ignore
|
||||||
opacity: opacityAnimation
|
...StyleSheet.absoluteFill,
|
||||||
}]}
|
backgroundColor: themes[theme].backdropColor,
|
||||||
|
opacity: opacityAnimation
|
||||||
|
}
|
||||||
|
]}
|
||||||
/>
|
/>
|
||||||
<Animated.Image
|
<Animated.Image
|
||||||
source={require('../static/images/logo.png')}
|
source={require('../static/images/logo.png')}
|
||||||
style={[styles.image, {
|
style={[
|
||||||
transform: [{
|
styles.image,
|
||||||
scale: scaleAnimation
|
{
|
||||||
}]
|
transform: [
|
||||||
}]}
|
{
|
||||||
|
scale: scaleAnimation
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]}
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
@ -137,4 +133,4 @@ class Loading extends React.PureComponent {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default (withTheme(Loading));
|
export default withTheme(Loading);
|
|
@ -1,8 +1,5 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import {
|
import { Animated, Easing, Linking, StyleSheet, Text, View } from 'react-native';
|
||||||
View, StyleSheet, Text, Animated, Easing, Linking
|
|
||||||
} from 'react-native';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import { Base64 } from 'js-base64';
|
import { Base64 } from 'js-base64';
|
||||||
import * as AppleAuthentication from 'expo-apple-authentication';
|
import * as AppleAuthentication from 'expo-apple-authentication';
|
||||||
|
@ -15,7 +12,7 @@ import OrSeparator from './OrSeparator';
|
||||||
import Touch from '../utils/touch';
|
import Touch from '../utils/touch';
|
||||||
import I18n from '../i18n';
|
import I18n from '../i18n';
|
||||||
import random from '../utils/random';
|
import random from '../utils/random';
|
||||||
import { logEvent, events } from '../utils/log';
|
import { events, logEvent } from '../utils/log';
|
||||||
import RocketChat from '../lib/rocketchat';
|
import RocketChat from '../lib/rocketchat';
|
||||||
import { CustomIcon } from '../lib/Icons';
|
import { CustomIcon } from '../lib/Icons';
|
||||||
|
|
||||||
|
@ -60,153 +57,175 @@ const styles = StyleSheet.create({
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
class LoginServices extends React.PureComponent {
|
interface IOpenOAuth {
|
||||||
static propTypes = {
|
url?: string;
|
||||||
navigation: PropTypes.object,
|
ssoToken?: string;
|
||||||
server: PropTypes.string,
|
authType?: string;
|
||||||
services: PropTypes.object,
|
}
|
||||||
Gitlab_URL: PropTypes.string,
|
|
||||||
CAS_enabled: PropTypes.bool,
|
interface IService {
|
||||||
CAS_login_url: PropTypes.string,
|
name: string;
|
||||||
separator: PropTypes.bool,
|
service: string;
|
||||||
theme: PropTypes.string
|
authType: string;
|
||||||
}
|
buttonColor: string;
|
||||||
|
buttonLabelColor: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ILoginServicesProps {
|
||||||
|
navigation: any;
|
||||||
|
server: string;
|
||||||
|
services: {
|
||||||
|
facebook: { clientId: string };
|
||||||
|
github: { clientId: string };
|
||||||
|
gitlab: { clientId: string };
|
||||||
|
google: { clientId: string };
|
||||||
|
linkedin: { clientId: string };
|
||||||
|
'meteor-developer': { clientId: string };
|
||||||
|
wordpress: { clientId: string; serverURL: string };
|
||||||
|
};
|
||||||
|
Gitlab_URL: string;
|
||||||
|
CAS_enabled: boolean;
|
||||||
|
CAS_login_url: string;
|
||||||
|
separator: boolean;
|
||||||
|
theme: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
class LoginServices extends React.PureComponent<ILoginServicesProps, any> {
|
||||||
|
private _animation: any;
|
||||||
|
|
||||||
static defaultProps = {
|
static defaultProps = {
|
||||||
separator: true
|
separator: true
|
||||||
}
|
};
|
||||||
|
|
||||||
state = {
|
state = {
|
||||||
collapsed: true,
|
collapsed: true,
|
||||||
servicesHeight: new Animated.Value(SERVICES_COLLAPSED_HEIGHT)
|
servicesHeight: new Animated.Value(SERVICES_COLLAPSED_HEIGHT)
|
||||||
}
|
};
|
||||||
|
|
||||||
onPressFacebook = () => {
|
onPressFacebook = () => {
|
||||||
logEvent(events.ENTER_WITH_FACEBOOK);
|
logEvent(events.ENTER_WITH_FACEBOOK);
|
||||||
const { services, server } = this.props;
|
const { services, server } = this.props;
|
||||||
const { clientId } = services.facebook;
|
const { clientId } = services.facebook;
|
||||||
const endpoint = 'https://m.facebook.com/v2.9/dialog/oauth';
|
const endpoint = 'https://m.facebook.com/v2.9/dialog/oauth';
|
||||||
const redirect_uri = `${ server }/_oauth/facebook?close`;
|
const redirect_uri = `${server}/_oauth/facebook?close`;
|
||||||
const scope = 'email';
|
const scope = 'email';
|
||||||
const state = this.getOAuthState();
|
const state = this.getOAuthState();
|
||||||
const params = `?client_id=${ clientId }&redirect_uri=${ redirect_uri }&scope=${ scope }&state=${ state }&display=touch`;
|
const params = `?client_id=${clientId}&redirect_uri=${redirect_uri}&scope=${scope}&state=${state}&display=touch`;
|
||||||
this.openOAuth({ url: `${ endpoint }${ params }` });
|
this.openOAuth({ url: `${endpoint}${params}` });
|
||||||
}
|
};
|
||||||
|
|
||||||
onPressGithub = () => {
|
onPressGithub = () => {
|
||||||
logEvent(events.ENTER_WITH_GITHUB);
|
logEvent(events.ENTER_WITH_GITHUB);
|
||||||
const { services, server } = this.props;
|
const { services, server } = this.props;
|
||||||
const { clientId } = services.github;
|
const { clientId } = services.github;
|
||||||
const endpoint = `https://github.com/login?client_id=${ clientId }&return_to=${ encodeURIComponent('/login/oauth/authorize') }`;
|
const endpoint = `https://github.com/login?client_id=${clientId}&return_to=${encodeURIComponent('/login/oauth/authorize')}`;
|
||||||
const redirect_uri = `${ server }/_oauth/github?close`;
|
const redirect_uri = `${server}/_oauth/github?close`;
|
||||||
const scope = 'user:email';
|
const scope = 'user:email';
|
||||||
const state = this.getOAuthState();
|
const state = this.getOAuthState();
|
||||||
const params = `?client_id=${ clientId }&redirect_uri=${ redirect_uri }&scope=${ scope }&state=${ state }`;
|
const params = `?client_id=${clientId}&redirect_uri=${redirect_uri}&scope=${scope}&state=${state}`;
|
||||||
this.openOAuth({ url: `${ endpoint }${ encodeURIComponent(params) }` });
|
this.openOAuth({ url: `${endpoint}${encodeURIComponent(params)}` });
|
||||||
}
|
};
|
||||||
|
|
||||||
onPressGitlab = () => {
|
onPressGitlab = () => {
|
||||||
logEvent(events.ENTER_WITH_GITLAB);
|
logEvent(events.ENTER_WITH_GITLAB);
|
||||||
const { services, server, Gitlab_URL } = this.props;
|
const { services, server, Gitlab_URL } = this.props;
|
||||||
const { clientId } = services.gitlab;
|
const { clientId } = services.gitlab;
|
||||||
const baseURL = Gitlab_URL ? Gitlab_URL.trim().replace(/\/*$/, '') : 'https://gitlab.com';
|
const baseURL = Gitlab_URL ? Gitlab_URL.trim().replace(/\/*$/, '') : 'https://gitlab.com';
|
||||||
const endpoint = `${ baseURL }/oauth/authorize`;
|
const endpoint = `${baseURL}/oauth/authorize`;
|
||||||
const redirect_uri = `${ server }/_oauth/gitlab?close`;
|
const redirect_uri = `${server}/_oauth/gitlab?close`;
|
||||||
const scope = 'read_user';
|
const scope = 'read_user';
|
||||||
const state = this.getOAuthState();
|
const state = this.getOAuthState();
|
||||||
const params = `?client_id=${ clientId }&redirect_uri=${ redirect_uri }&scope=${ scope }&state=${ state }&response_type=code`;
|
const params = `?client_id=${clientId}&redirect_uri=${redirect_uri}&scope=${scope}&state=${state}&response_type=code`;
|
||||||
this.openOAuth({ url: `${ endpoint }${ params }` });
|
this.openOAuth({ url: `${endpoint}${params}` });
|
||||||
}
|
};
|
||||||
|
|
||||||
onPressGoogle = () => {
|
onPressGoogle = () => {
|
||||||
logEvent(events.ENTER_WITH_GOOGLE);
|
logEvent(events.ENTER_WITH_GOOGLE);
|
||||||
const { services, server } = this.props;
|
const { services, server } = this.props;
|
||||||
const { clientId } = services.google;
|
const { clientId } = services.google;
|
||||||
const endpoint = 'https://accounts.google.com/o/oauth2/auth';
|
const endpoint = 'https://accounts.google.com/o/oauth2/auth';
|
||||||
const redirect_uri = `${ server }/_oauth/google?close`;
|
const redirect_uri = `${server}/_oauth/google?close`;
|
||||||
const scope = 'email';
|
const scope = 'email';
|
||||||
const state = this.getOAuthState(LOGIN_STYPE_REDIRECT);
|
const state = this.getOAuthState(LOGIN_STYPE_REDIRECT);
|
||||||
const params = `?client_id=${ clientId }&redirect_uri=${ redirect_uri }&scope=${ scope }&state=${ state }&response_type=code`;
|
const params = `?client_id=${clientId}&redirect_uri=${redirect_uri}&scope=${scope}&state=${state}&response_type=code`;
|
||||||
Linking.openURL(`${ endpoint }${ params }`);
|
Linking.openURL(`${endpoint}${params}`);
|
||||||
}
|
};
|
||||||
|
|
||||||
onPressLinkedin = () => {
|
onPressLinkedin = () => {
|
||||||
logEvent(events.ENTER_WITH_LINKEDIN);
|
logEvent(events.ENTER_WITH_LINKEDIN);
|
||||||
const { services, server } = this.props;
|
const { services, server } = this.props;
|
||||||
const { clientId } = services.linkedin;
|
const { clientId } = services.linkedin;
|
||||||
const endpoint = 'https://www.linkedin.com/oauth/v2/authorization';
|
const endpoint = 'https://www.linkedin.com/oauth/v2/authorization';
|
||||||
const redirect_uri = `${ server }/_oauth/linkedin?close`;
|
const redirect_uri = `${server}/_oauth/linkedin?close`;
|
||||||
const scope = 'r_liteprofile,r_emailaddress';
|
const scope = 'r_liteprofile,r_emailaddress';
|
||||||
const state = this.getOAuthState();
|
const state = this.getOAuthState();
|
||||||
const params = `?client_id=${ clientId }&redirect_uri=${ redirect_uri }&scope=${ scope }&state=${ state }&response_type=code`;
|
const params = `?client_id=${clientId}&redirect_uri=${redirect_uri}&scope=${scope}&state=${state}&response_type=code`;
|
||||||
this.openOAuth({ url: `${ endpoint }${ params }` });
|
this.openOAuth({ url: `${endpoint}${params}` });
|
||||||
}
|
};
|
||||||
|
|
||||||
onPressMeteor = () => {
|
onPressMeteor = () => {
|
||||||
logEvent(events.ENTER_WITH_METEOR);
|
logEvent(events.ENTER_WITH_METEOR);
|
||||||
const { services, server } = this.props;
|
const { services, server } = this.props;
|
||||||
const { clientId } = services['meteor-developer'];
|
const { clientId } = services['meteor-developer'];
|
||||||
const endpoint = 'https://www.meteor.com/oauth2/authorize';
|
const endpoint = 'https://www.meteor.com/oauth2/authorize';
|
||||||
const redirect_uri = `${ server }/_oauth/meteor-developer`;
|
const redirect_uri = `${server}/_oauth/meteor-developer`;
|
||||||
const state = this.getOAuthState();
|
const state = this.getOAuthState();
|
||||||
const params = `?client_id=${ clientId }&redirect_uri=${ redirect_uri }&state=${ state }&response_type=code`;
|
const params = `?client_id=${clientId}&redirect_uri=${redirect_uri}&state=${state}&response_type=code`;
|
||||||
this.openOAuth({ url: `${ endpoint }${ params }` });
|
this.openOAuth({ url: `${endpoint}${params}` });
|
||||||
}
|
};
|
||||||
|
|
||||||
onPressTwitter = () => {
|
onPressTwitter = () => {
|
||||||
logEvent(events.ENTER_WITH_TWITTER);
|
logEvent(events.ENTER_WITH_TWITTER);
|
||||||
const { server } = this.props;
|
const { server } = this.props;
|
||||||
const state = this.getOAuthState();
|
const state = this.getOAuthState();
|
||||||
const url = `${ server }/_oauth/twitter/?requestTokenAndRedirect=true&state=${ state }`;
|
const url = `${server}/_oauth/twitter/?requestTokenAndRedirect=true&state=${state}`;
|
||||||
this.openOAuth({ url });
|
this.openOAuth({ url });
|
||||||
}
|
};
|
||||||
|
|
||||||
onPressWordpress = () => {
|
onPressWordpress = () => {
|
||||||
logEvent(events.ENTER_WITH_WORDPRESS);
|
logEvent(events.ENTER_WITH_WORDPRESS);
|
||||||
const { services, server } = this.props;
|
const { services, server } = this.props;
|
||||||
const { clientId, serverURL } = services.wordpress;
|
const { clientId, serverURL } = services.wordpress;
|
||||||
const endpoint = `${ serverURL }/oauth/authorize`;
|
const endpoint = `${serverURL}/oauth/authorize`;
|
||||||
const redirect_uri = `${ server }/_oauth/wordpress?close`;
|
const redirect_uri = `${server}/_oauth/wordpress?close`;
|
||||||
const scope = 'openid';
|
const scope = 'openid';
|
||||||
const state = this.getOAuthState();
|
const state = this.getOAuthState();
|
||||||
const params = `?client_id=${ clientId }&redirect_uri=${ redirect_uri }&scope=${ scope }&state=${ state }&response_type=code`;
|
const params = `?client_id=${clientId}&redirect_uri=${redirect_uri}&scope=${scope}&state=${state}&response_type=code`;
|
||||||
this.openOAuth({ url: `${ endpoint }${ params }` });
|
this.openOAuth({ url: `${endpoint}${params}` });
|
||||||
}
|
};
|
||||||
|
|
||||||
onPressCustomOAuth = (loginService) => {
|
onPressCustomOAuth = (loginService: any) => {
|
||||||
logEvent(events.ENTER_WITH_CUSTOM_OAUTH);
|
logEvent(events.ENTER_WITH_CUSTOM_OAUTH);
|
||||||
const { server } = this.props;
|
const { server } = this.props;
|
||||||
const {
|
const { serverURL, authorizePath, clientId, scope, service } = loginService;
|
||||||
serverURL, authorizePath, clientId, scope, service
|
const redirectUri = `${server}/_oauth/${service}`;
|
||||||
} = loginService;
|
|
||||||
const redirectUri = `${ server }/_oauth/${ service }`;
|
|
||||||
const state = this.getOAuthState();
|
const state = this.getOAuthState();
|
||||||
const params = `?client_id=${ clientId }&redirect_uri=${ redirectUri }&response_type=code&state=${ state }&scope=${ scope }`;
|
const params = `?client_id=${clientId}&redirect_uri=${redirectUri}&response_type=code&state=${state}&scope=${scope}`;
|
||||||
const domain = `${ serverURL }`;
|
const domain = `${serverURL}`;
|
||||||
const absolutePath = `${ authorizePath }${ params }`;
|
const absolutePath = `${authorizePath}${params}`;
|
||||||
const url = absolutePath.includes(domain) ? absolutePath : domain + absolutePath;
|
const url = absolutePath.includes(domain) ? absolutePath : domain + absolutePath;
|
||||||
this.openOAuth({ url });
|
this.openOAuth({ url });
|
||||||
}
|
};
|
||||||
|
|
||||||
onPressSaml = (loginService) => {
|
onPressSaml = (loginService: any) => {
|
||||||
logEvent(events.ENTER_WITH_SAML);
|
logEvent(events.ENTER_WITH_SAML);
|
||||||
const { server } = this.props;
|
const { server } = this.props;
|
||||||
const { clientConfig } = loginService;
|
const { clientConfig } = loginService;
|
||||||
const { provider } = clientConfig;
|
const { provider } = clientConfig;
|
||||||
const ssoToken = random(17);
|
const ssoToken = random(17);
|
||||||
const url = `${ server }/_saml/authorize/${ provider }/${ ssoToken }`;
|
const url = `${server}/_saml/authorize/${provider}/${ssoToken}`;
|
||||||
this.openOAuth({ url, ssoToken, authType: 'saml' });
|
this.openOAuth({ url, ssoToken, authType: 'saml' });
|
||||||
}
|
};
|
||||||
|
|
||||||
onPressCas = () => {
|
onPressCas = () => {
|
||||||
logEvent(events.ENTER_WITH_CAS);
|
logEvent(events.ENTER_WITH_CAS);
|
||||||
const { server, CAS_login_url } = this.props;
|
const { server, CAS_login_url } = this.props;
|
||||||
const ssoToken = random(17);
|
const ssoToken = random(17);
|
||||||
const url = `${ CAS_login_url }?service=${ server }/_cas/${ ssoToken }`;
|
const url = `${CAS_login_url}?service=${server}/_cas/${ssoToken}`;
|
||||||
this.openOAuth({ url, ssoToken, authType: 'cas' });
|
this.openOAuth({ url, ssoToken, authType: 'cas' });
|
||||||
}
|
};
|
||||||
|
|
||||||
onPressAppleLogin = async() => {
|
onPressAppleLogin = async () => {
|
||||||
logEvent(events.ENTER_WITH_APPLE);
|
logEvent(events.ENTER_WITH_APPLE);
|
||||||
try {
|
try {
|
||||||
const { fullName, email, identityToken } = await AppleAuthentication.signInAsync({
|
const { fullName, email, identityToken } = await AppleAuthentication.signInAsync({
|
||||||
|
@ -220,11 +239,11 @@ class LoginServices extends React.PureComponent {
|
||||||
} catch {
|
} catch {
|
||||||
logEvent(events.ENTER_WITH_APPLE_F);
|
logEvent(events.ENTER_WITH_APPLE_F);
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
getOAuthState = (loginStyle = LOGIN_STYPE_POPUP) => {
|
getOAuthState = (loginStyle = LOGIN_STYPE_POPUP) => {
|
||||||
const credentialToken = random(43);
|
const credentialToken = random(43);
|
||||||
let obj = { loginStyle, credentialToken, isCordova: true };
|
let obj: any = { loginStyle, credentialToken, isCordova: true };
|
||||||
if (loginStyle === LOGIN_STYPE_REDIRECT) {
|
if (loginStyle === LOGIN_STYPE_REDIRECT) {
|
||||||
obj = {
|
obj = {
|
||||||
...obj,
|
...obj,
|
||||||
|
@ -232,24 +251,26 @@ class LoginServices extends React.PureComponent {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
return Base64.encodeURI(JSON.stringify(obj));
|
return Base64.encodeURI(JSON.stringify(obj));
|
||||||
}
|
};
|
||||||
|
|
||||||
openOAuth = ({ url, ssoToken, authType = 'oauth' }) => {
|
openOAuth = ({ url, ssoToken, authType = 'oauth' }: IOpenOAuth) => {
|
||||||
const { navigation } = this.props;
|
const { navigation } = this.props;
|
||||||
navigation.navigate('AuthenticationWebView', { url, authType, ssoToken });
|
navigation.navigate('AuthenticationWebView', { url, authType, ssoToken });
|
||||||
}
|
};
|
||||||
|
|
||||||
transitionServicesTo = (height) => {
|
transitionServicesTo = (height: number) => {
|
||||||
const { servicesHeight } = this.state;
|
const { servicesHeight } = this.state;
|
||||||
if (this._animation) {
|
if (this._animation) {
|
||||||
this._animation.stop();
|
this._animation.stop();
|
||||||
}
|
}
|
||||||
|
// @ts-ignore
|
||||||
this._animation = Animated.timing(servicesHeight, {
|
this._animation = Animated.timing(servicesHeight, {
|
||||||
toValue: height,
|
toValue: height,
|
||||||
duration: 300,
|
duration: 300,
|
||||||
|
// @ts-ignore
|
||||||
easing: Easing.easeOutCubic
|
easing: Easing.easeOutCubic
|
||||||
}).start();
|
}).start();
|
||||||
}
|
};
|
||||||
|
|
||||||
toggleServices = () => {
|
toggleServices = () => {
|
||||||
const { collapsed } = this.state;
|
const { collapsed } = this.state;
|
||||||
|
@ -260,11 +281,11 @@ class LoginServices extends React.PureComponent {
|
||||||
} else {
|
} else {
|
||||||
this.transitionServicesTo(SERVICES_COLLAPSED_HEIGHT);
|
this.transitionServicesTo(SERVICES_COLLAPSED_HEIGHT);
|
||||||
}
|
}
|
||||||
this.setState(prevState => ({ collapsed: !prevState.collapsed }));
|
this.setState((prevState: any) => ({ collapsed: !prevState.collapsed }));
|
||||||
}
|
};
|
||||||
|
|
||||||
getSocialOauthProvider = (name) => {
|
getSocialOauthProvider = (name: string) => {
|
||||||
const oauthProviders = {
|
const oauthProviders: any = {
|
||||||
facebook: this.onPressFacebook,
|
facebook: this.onPressFacebook,
|
||||||
github: this.onPressGithub,
|
github: this.onPressGithub,
|
||||||
gitlab: this.onPressGitlab,
|
gitlab: this.onPressGitlab,
|
||||||
|
@ -275,7 +296,7 @@ class LoginServices extends React.PureComponent {
|
||||||
wordpress: this.onPressWordpress
|
wordpress: this.onPressWordpress
|
||||||
};
|
};
|
||||||
return oauthProviders[name];
|
return oauthProviders[name];
|
||||||
}
|
};
|
||||||
|
|
||||||
renderServicesSeparator = () => {
|
renderServicesSeparator = () => {
|
||||||
const { collapsed } = this.state;
|
const { collapsed } = this.state;
|
||||||
|
@ -301,13 +322,13 @@ class LoginServices extends React.PureComponent {
|
||||||
return <OrSeparator theme={theme} />;
|
return <OrSeparator theme={theme} />;
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
};
|
||||||
|
|
||||||
renderItem = (service) => {
|
renderItem = (service: IService) => {
|
||||||
const { CAS_enabled, theme } = this.props;
|
const { CAS_enabled, theme } = this.props;
|
||||||
let { name } = service;
|
let { name } = service;
|
||||||
name = name === 'meteor-developer' ? 'meteor' : name;
|
name = name === 'meteor-developer' ? 'meteor' : name;
|
||||||
const icon = `${ name }-monochromatic`;
|
const icon = `${name}-monochromatic`;
|
||||||
const isSaml = service.service === 'saml';
|
const isSaml = service.service === 'saml';
|
||||||
let onPress = () => {};
|
let onPress = () => {};
|
||||||
|
|
||||||
|
@ -357,15 +378,16 @@ class LoginServices extends React.PureComponent {
|
||||||
style={[styles.serviceButton, { backgroundColor }]}
|
style={[styles.serviceButton, { backgroundColor }]}
|
||||||
theme={theme}
|
theme={theme}
|
||||||
activeOpacity={0.5}
|
activeOpacity={0.5}
|
||||||
underlayColor={themes[theme].buttonText}
|
underlayColor={themes[theme].buttonText}>
|
||||||
>
|
|
||||||
<View style={styles.serviceButtonContainer}>
|
<View style={styles.serviceButtonContainer}>
|
||||||
{service.authType === 'oauth' || service.authType === 'apple' ? <CustomIcon name={icon} size={24} color={themes[theme].titleText} style={styles.serviceIcon} /> : null}
|
{service.authType === 'oauth' || service.authType === 'apple' ? (
|
||||||
|
<CustomIcon name={icon} size={24} color={themes[theme].titleText} style={styles.serviceIcon} />
|
||||||
|
) : null}
|
||||||
<Text style={[styles.serviceText, { color: themes[theme].titleText }]}>{buttonText}</Text>
|
<Text style={[styles.serviceText, { color: themes[theme].titleText }]}>{buttonText}</Text>
|
||||||
</View>
|
</View>
|
||||||
</Touch>
|
</Touch>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { servicesHeight } = this.state;
|
const { servicesHeight } = this.state;
|
||||||
|
@ -379,23 +401,21 @@ class LoginServices extends React.PureComponent {
|
||||||
if (length > 3 && separator) {
|
if (length > 3 && separator) {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Animated.View style={style}>
|
<Animated.View style={style}>{Object.values(services).map((service: any) => this.renderItem(service))}</Animated.View>
|
||||||
{Object.values(services).map(service => this.renderItem(service))}
|
|
||||||
</Animated.View>
|
|
||||||
{this.renderServicesSeparator()}
|
{this.renderServicesSeparator()}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{Object.values(services).map(service => this.renderItem(service))}
|
{Object.values(services).map((service: any) => this.renderItem(service))}
|
||||||
{this.renderServicesSeparator()}
|
{this.renderServicesSeparator()}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const mapStateToProps = state => ({
|
const mapStateToProps = (state: any) => ({
|
||||||
server: state.server.server,
|
server: state.server.server,
|
||||||
Gitlab_URL: state.settings.API_Gitlab_URL,
|
Gitlab_URL: state.settings.API_Gitlab_URL,
|
||||||
CAS_enabled: state.settings.CAS_enabled,
|
CAS_enabled: state.settings.CAS_enabled,
|
|
@ -1,8 +1,5 @@
|
||||||
import React, { useEffect, useState, useCallback } from 'react';
|
import React, { useCallback, useEffect, useState } from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import { FlatList, StyleSheet, Text, View } from 'react-native';
|
||||||
import {
|
|
||||||
View, Text, FlatList, StyleSheet
|
|
||||||
} from 'react-native';
|
|
||||||
|
|
||||||
import { withTheme } from '../../theme';
|
import { withTheme } from '../../theme';
|
||||||
import { themes } from '../../constants/colors';
|
import { themes } from '../../constants/colors';
|
||||||
|
@ -13,6 +10,27 @@ import database from '../../lib/database';
|
||||||
import { Button } from '../ActionSheet';
|
import { Button } from '../ActionSheet';
|
||||||
import { useDimensions } from '../../dimensions';
|
import { useDimensions } from '../../dimensions';
|
||||||
import sharedStyles from '../../views/Styles';
|
import sharedStyles from '../../views/Styles';
|
||||||
|
import { IEmoji } from '../EmojiPicker/interfaces';
|
||||||
|
|
||||||
|
interface IHeader {
|
||||||
|
handleReaction: Function;
|
||||||
|
server: string;
|
||||||
|
message: object;
|
||||||
|
isMasterDetail: boolean;
|
||||||
|
theme: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface THeaderItem {
|
||||||
|
item: IEmoji;
|
||||||
|
onReaction: Function;
|
||||||
|
server: string;
|
||||||
|
theme: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface THeaderFooter {
|
||||||
|
onReaction: any;
|
||||||
|
theme: string;
|
||||||
|
}
|
||||||
|
|
||||||
export const HEADER_HEIGHT = 36;
|
export const HEADER_HEIGHT = 36;
|
||||||
const ITEM_SIZE = 36;
|
const ITEM_SIZE = 36;
|
||||||
|
@ -43,65 +61,47 @@ const styles = StyleSheet.create({
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const keyExtractor = item => item?.id || item;
|
const keyExtractor = (item: any) => item?.id || item;
|
||||||
|
|
||||||
const DEFAULT_EMOJIS = ['clap', '+1', 'heart_eyes', 'grinning', 'thinking_face', 'smiley'];
|
const DEFAULT_EMOJIS = ['clap', '+1', 'heart_eyes', 'grinning', 'thinking_face', 'smiley'];
|
||||||
|
|
||||||
const HeaderItem = React.memo(({
|
const HeaderItem = React.memo(({ item, onReaction, server, theme }: THeaderItem) => (
|
||||||
item, onReaction, server, theme
|
|
||||||
}) => (
|
|
||||||
<Button
|
<Button
|
||||||
testID={`message-actions-emoji-${ item.content || item }`}
|
testID={`message-actions-emoji-${item.content || item}`}
|
||||||
onPress={() => onReaction({ emoji: `:${ item.content || item }:` })}
|
onPress={() => onReaction({ emoji: `:${item.content || item}:` })}
|
||||||
style={[styles.headerItem, { backgroundColor: themes[theme].auxiliaryBackground }]}
|
style={[styles.headerItem, { backgroundColor: themes[theme].auxiliaryBackground }]}
|
||||||
theme={theme}
|
theme={theme}>
|
||||||
>
|
|
||||||
{item?.isCustom ? (
|
{item?.isCustom ? (
|
||||||
<CustomEmoji style={styles.customEmoji} emoji={item} baseUrl={server} />
|
<CustomEmoji style={styles.customEmoji} emoji={item} baseUrl={server} />
|
||||||
) : (
|
) : (
|
||||||
<Text style={styles.headerIcon}>
|
<Text style={styles.headerIcon}>{shortnameToUnicode(`:${item.content || item}:`)}</Text>
|
||||||
{shortnameToUnicode(`:${ item.content || item }:`)}
|
|
||||||
</Text>
|
|
||||||
)}
|
)}
|
||||||
</Button>
|
</Button>
|
||||||
));
|
));
|
||||||
HeaderItem.propTypes = {
|
|
||||||
item: PropTypes.string,
|
|
||||||
onReaction: PropTypes.func,
|
|
||||||
server: PropTypes.string,
|
|
||||||
theme: PropTypes.string
|
|
||||||
};
|
|
||||||
|
|
||||||
const HeaderFooter = React.memo(({ onReaction, theme }) => (
|
const HeaderFooter = React.memo(({ onReaction, theme }: THeaderFooter) => (
|
||||||
<Button
|
<Button
|
||||||
testID='add-reaction'
|
testID='add-reaction'
|
||||||
onPress={onReaction}
|
onPress={onReaction}
|
||||||
style={[styles.headerItem, { backgroundColor: themes[theme].auxiliaryBackground }]}
|
style={[styles.headerItem, { backgroundColor: themes[theme].auxiliaryBackground }]}
|
||||||
theme={theme}
|
theme={theme}>
|
||||||
>
|
|
||||||
<CustomIcon name='reaction-add' size={24} color={themes[theme].bodyText} />
|
<CustomIcon name='reaction-add' size={24} color={themes[theme].bodyText} />
|
||||||
</Button>
|
</Button>
|
||||||
));
|
));
|
||||||
HeaderFooter.propTypes = {
|
|
||||||
onReaction: PropTypes.func,
|
|
||||||
theme: PropTypes.string
|
|
||||||
};
|
|
||||||
|
|
||||||
const Header = React.memo(({
|
const Header = React.memo(({ handleReaction, server, message, isMasterDetail, theme }: IHeader) => {
|
||||||
handleReaction, server, message, isMasterDetail, theme
|
|
||||||
}) => {
|
|
||||||
const [items, setItems] = useState([]);
|
const [items, setItems] = useState([]);
|
||||||
const { width, height } = useDimensions();
|
const { width, height }: any = useDimensions();
|
||||||
|
|
||||||
const setEmojis = async() => {
|
const setEmojis = async () => {
|
||||||
try {
|
try {
|
||||||
const db = database.active;
|
const db = database.active;
|
||||||
const freqEmojiCollection = db.get('frequently_used_emojis');
|
const freqEmojiCollection = db.get('frequently_used_emojis');
|
||||||
let freqEmojis = await freqEmojiCollection.query().fetch();
|
let freqEmojis = await freqEmojiCollection.query().fetch();
|
||||||
|
|
||||||
const isLandscape = width > height;
|
const isLandscape = width > height;
|
||||||
const size = (isLandscape || isMasterDetail ? width / 2 : width) - (CONTAINER_MARGIN * 2);
|
const size = (isLandscape || isMasterDetail ? width / 2 : width) - CONTAINER_MARGIN * 2;
|
||||||
const quantity = (size / (ITEM_SIZE + (ITEM_MARGIN * 2))) - 1;
|
const quantity = size / (ITEM_SIZE + ITEM_MARGIN * 2) - 1;
|
||||||
|
|
||||||
freqEmojis = freqEmojis.concat(DEFAULT_EMOJIS).slice(0, quantity);
|
freqEmojis = freqEmojis.concat(DEFAULT_EMOJIS).slice(0, quantity);
|
||||||
setItems(freqEmojis);
|
setItems(freqEmojis);
|
||||||
|
@ -114,11 +114,14 @@ const Header = React.memo(({
|
||||||
setEmojis();
|
setEmojis();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const onReaction = ({ emoji }) => handleReaction(emoji, message);
|
const onReaction = ({ emoji }: { emoji: IEmoji }) => handleReaction(emoji, message);
|
||||||
|
|
||||||
const renderItem = useCallback(({ item }) => <HeaderItem item={item} onReaction={onReaction} server={server} theme={theme} />);
|
const renderItem = useCallback(
|
||||||
|
({ item }) => <HeaderItem item={item} onReaction={onReaction} server={server} theme={theme} />,
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
|
||||||
const renderFooter = useCallback(() => <HeaderFooter onReaction={onReaction} theme={theme} />);
|
const renderFooter = useCallback(() => <HeaderFooter onReaction={onReaction} theme={theme} />, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View style={[styles.container, { backgroundColor: themes[theme].focusedBackground }]}>
|
<View style={[styles.container, { backgroundColor: themes[theme].focusedBackground }]}>
|
||||||
|
@ -135,11 +138,5 @@ const Header = React.memo(({
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
Header.propTypes = {
|
|
||||||
handleReaction: PropTypes.func,
|
|
||||||
server: PropTypes.string,
|
|
||||||
message: PropTypes.object,
|
|
||||||
isMasterDetail: PropTypes.bool,
|
|
||||||
theme: PropTypes.string
|
|
||||||
};
|
|
||||||
export default withTheme(Header);
|
export default withTheme(Header);
|
|
@ -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);
|
|
|
@ -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);
|
|
@ -1,5 +1,4 @@
|
||||||
import React, { useContext, useState } from 'react';
|
import React, { useContext, useState } from 'react';
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import { TouchableOpacity } from 'react-native';
|
import { TouchableOpacity } from 'react-native';
|
||||||
import FastImage from '@rocket.chat/react-native-fast-image';
|
import FastImage from '@rocket.chat/react-native-fast-image';
|
||||||
|
|
||||||
|
@ -9,7 +8,16 @@ import { themes } from '../../../constants/colors';
|
||||||
import MessageboxContext from '../Context';
|
import MessageboxContext from '../Context';
|
||||||
import ActivityIndicator from '../../ActivityIndicator';
|
import ActivityIndicator from '../../ActivityIndicator';
|
||||||
|
|
||||||
const Item = ({ item, theme }) => {
|
interface IMessageBoxCommandsPreviewItem {
|
||||||
|
item: {
|
||||||
|
type: string;
|
||||||
|
id: string;
|
||||||
|
value: string;
|
||||||
|
};
|
||||||
|
theme: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const Item = ({ item, theme }: IMessageBoxCommandsPreviewItem) => {
|
||||||
const context = useContext(MessageboxContext);
|
const context = useContext(MessageboxContext);
|
||||||
const { onPressCommandPreview } = context;
|
const { onPressCommandPreview } = context;
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
|
@ -18,29 +26,21 @@ const Item = ({ item, theme }) => {
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
style={styles.commandPreview}
|
style={styles.commandPreview}
|
||||||
onPress={() => onPressCommandPreview(item)}
|
onPress={() => onPressCommandPreview(item)}
|
||||||
testID={`command-preview-item${ item.id }`}
|
testID={`command-preview-item${item.id}`}>
|
||||||
>
|
{item.type === 'image' ? (
|
||||||
{item.type === 'image'
|
<FastImage
|
||||||
? (
|
style={styles.commandPreviewImage}
|
||||||
<FastImage
|
source={{ uri: item.value }}
|
||||||
style={styles.commandPreviewImage}
|
resizeMode={FastImage.resizeMode.cover}
|
||||||
source={{ uri: item.value }}
|
onLoadStart={() => setLoading(true)}
|
||||||
resizeMode={FastImage.resizeMode.cover}
|
onLoad={() => setLoading(false)}>
|
||||||
onLoadStart={() => setLoading(true)}
|
{loading ? <ActivityIndicator theme={theme} /> : null}
|
||||||
onLoad={() => setLoading(false)}
|
</FastImage>
|
||||||
>
|
) : (
|
||||||
{ loading ? <ActivityIndicator theme={theme} /> : null }
|
<CustomIcon name='attach' size={36} color={themes[theme].actionTintColor} />
|
||||||
</FastImage>
|
)}
|
||||||
)
|
|
||||||
: <CustomIcon name='attach' size={36} color={themes[theme].actionTintColor} />
|
|
||||||
}
|
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
Item.propTypes = {
|
|
||||||
item: PropTypes.object,
|
|
||||||
theme: PropTypes.string
|
|
||||||
};
|
|
||||||
|
|
||||||
export default Item;
|
export default Item;
|
|
@ -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);
|
|
|
@ -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);
|
|
@ -1,4 +0,0 @@
|
||||||
import React from 'react';
|
|
||||||
|
|
||||||
const MessageboxContext = React.createContext();
|
|
||||||
export default MessageboxContext;
|
|
|
@ -0,0 +1,5 @@
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
// @ts-ignore
|
||||||
|
const MessageboxContext = React.createContext<any>();
|
||||||
|
export default MessageboxContext;
|
|
@ -1,7 +1,6 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { View } from 'react-native';
|
import { View } from 'react-native';
|
||||||
import { KeyboardRegistry } from 'react-native-ui-lib/keyboard';
|
import { KeyboardRegistry } from 'react-native-ui-lib/keyboard';
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
|
|
||||||
import store from '../../lib/createStore';
|
import store from '../../lib/createStore';
|
||||||
import EmojiPicker from '../EmojiPicker';
|
import EmojiPicker from '../EmojiPicker';
|
||||||
|
@ -9,25 +8,29 @@ import styles from './styles';
|
||||||
import { themes } from '../../constants/colors';
|
import { themes } from '../../constants/colors';
|
||||||
import { withTheme } from '../../theme';
|
import { withTheme } from '../../theme';
|
||||||
|
|
||||||
export default class EmojiKeyboard extends React.PureComponent {
|
interface IMessageBoxEmojiKeyboard {
|
||||||
static propTypes = {
|
theme: string;
|
||||||
theme: PropTypes.string
|
}
|
||||||
};
|
|
||||||
|
|
||||||
constructor(props) {
|
export default class EmojiKeyboard extends React.PureComponent<IMessageBoxEmojiKeyboard, any> {
|
||||||
|
private readonly baseUrl: any;
|
||||||
|
|
||||||
|
constructor(props: IMessageBoxEmojiKeyboard) {
|
||||||
super(props);
|
super(props);
|
||||||
const state = store.getState();
|
const state = store.getState();
|
||||||
this.baseUrl = state.share.server.server || state.server.server;
|
this.baseUrl = state.share.server.server || state.server.server;
|
||||||
}
|
}
|
||||||
|
|
||||||
onEmojiSelected = (emoji) => {
|
onEmojiSelected = (emoji: any) => {
|
||||||
KeyboardRegistry.onItemSelected('EmojiKeyboard', { emoji });
|
KeyboardRegistry.onItemSelected('EmojiKeyboard', { emoji });
|
||||||
}
|
};
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { theme } = this.props;
|
const { theme } = this.props;
|
||||||
return (
|
return (
|
||||||
<View style={[styles.emojiKeyboardContainer, { borderTopColor: themes[theme].borderColor }]} testID='messagebox-keyboard-emoji'>
|
<View
|
||||||
|
style={[styles.emojiKeyboardContainer, { borderTopColor: themes[theme].borderColor }]}
|
||||||
|
testID='messagebox-keyboard-emoji'>
|
||||||
<EmojiPicker onEmojiSelected={this.onEmojiSelected} baseUrl={this.baseUrl} />
|
<EmojiPicker onEmojiSelected={this.onEmojiSelected} baseUrl={this.baseUrl} />
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
|
@ -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;
|
|
|
@ -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;
|
|
@ -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;
|
|
|
@ -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;
|
|
@ -1,12 +1,19 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { TouchableOpacity, Text } from 'react-native';
|
import { Text, TouchableOpacity } from 'react-native';
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
|
|
||||||
import styles from '../styles';
|
import styles from '../styles';
|
||||||
import I18n from '../../../i18n';
|
import I18n from '../../../i18n';
|
||||||
import { themes } from '../../../constants/colors';
|
import { themes } from '../../../constants/colors';
|
||||||
|
|
||||||
const FixedMentionItem = ({ item, onPress, theme }) => (
|
interface IMessageBoxFixedMentionItem {
|
||||||
|
item: {
|
||||||
|
username: string;
|
||||||
|
};
|
||||||
|
onPress: Function;
|
||||||
|
theme: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const FixedMentionItem = ({ item, onPress, theme }: IMessageBoxFixedMentionItem) => (
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
style={[
|
style={[
|
||||||
styles.mentionItem,
|
styles.mentionItem,
|
||||||
|
@ -15,8 +22,7 @@ const FixedMentionItem = ({ item, onPress, theme }) => (
|
||||||
borderTopColor: themes[theme].separatorColor
|
borderTopColor: themes[theme].separatorColor
|
||||||
}
|
}
|
||||||
]}
|
]}
|
||||||
onPress={() => onPress(item)}
|
onPress={() => onPress(item)}>
|
||||||
>
|
|
||||||
<Text style={[styles.fixedMentionAvatar, { color: themes[theme].titleText }]}>{item.username}</Text>
|
<Text style={[styles.fixedMentionAvatar, { color: themes[theme].titleText }]}>{item.username}</Text>
|
||||||
<Text style={[styles.mentionText, { color: themes[theme].titleText }]}>
|
<Text style={[styles.mentionText, { color: themes[theme].titleText }]}>
|
||||||
{item.username === 'here' ? I18n.t('Notify_active_in_this_room') : I18n.t('Notify_all_in_this_room')}
|
{item.username === 'here' ? I18n.t('Notify_active_in_this_room') : I18n.t('Notify_all_in_this_room')}
|
||||||
|
@ -24,10 +30,4 @@ const FixedMentionItem = ({ item, onPress, theme }) => (
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
);
|
);
|
||||||
|
|
||||||
FixedMentionItem.propTypes = {
|
|
||||||
item: PropTypes.object,
|
|
||||||
onPress: PropTypes.func,
|
|
||||||
theme: PropTypes.string
|
|
||||||
};
|
|
||||||
|
|
||||||
export default FixedMentionItem;
|
export default FixedMentionItem;
|
|
@ -6,25 +6,20 @@ import shortnameToUnicode from '../../../utils/shortnameToUnicode';
|
||||||
import styles from '../styles';
|
import styles from '../styles';
|
||||||
import MessageboxContext from '../Context';
|
import MessageboxContext from '../Context';
|
||||||
import CustomEmoji from '../../EmojiPicker/CustomEmoji';
|
import CustomEmoji from '../../EmojiPicker/CustomEmoji';
|
||||||
|
import { IEmoji } from '../../EmojiPicker/interfaces';
|
||||||
|
|
||||||
const MentionEmoji = ({ item }) => {
|
interface IMessageBoxMentionEmoji {
|
||||||
|
item: IEmoji;
|
||||||
|
}
|
||||||
|
|
||||||
|
const MentionEmoji = ({ item }: IMessageBoxMentionEmoji) => {
|
||||||
const context = useContext(MessageboxContext);
|
const context = useContext(MessageboxContext);
|
||||||
const { baseUrl } = context;
|
const { baseUrl } = context;
|
||||||
|
|
||||||
if (item.name) {
|
if (item.name) {
|
||||||
return (
|
return <CustomEmoji style={styles.mentionItemCustomEmoji} emoji={item} baseUrl={baseUrl} />;
|
||||||
<CustomEmoji
|
|
||||||
style={styles.mentionItemCustomEmoji}
|
|
||||||
emoji={item}
|
|
||||||
baseUrl={baseUrl}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
return (
|
return <Text style={styles.mentionItemEmoji}>{shortnameToUnicode(`:${item}:`)}</Text>;
|
||||||
<Text style={styles.mentionItemEmoji}>
|
|
||||||
{shortnameToUnicode(`:${ item }:`)}
|
|
||||||
</Text>
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
MentionEmoji.propTypes = {
|
MentionEmoji.propTypes = {
|
|
@ -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;
|
|
@ -1,32 +1,43 @@
|
||||||
import React, { useContext } from 'react';
|
import React, { useContext } from 'react';
|
||||||
import { TouchableOpacity, Text } from 'react-native';
|
import { Text, TouchableOpacity } from 'react-native';
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
|
|
||||||
import styles from '../styles';
|
import styles from '../styles';
|
||||||
import Avatar from '../../Avatar';
|
import Avatar from '../../Avatar';
|
||||||
import MessageboxContext from '../Context';
|
import MessageboxContext from '../Context';
|
||||||
import FixedMentionItem from './FixedMentionItem';
|
import FixedMentionItem from './FixedMentionItem';
|
||||||
import MentionEmoji from './MentionEmoji';
|
import MentionEmoji from './MentionEmoji';
|
||||||
import {
|
import { MENTIONS_TRACKING_TYPE_EMOJIS, MENTIONS_TRACKING_TYPE_COMMANDS, MENTIONS_TRACKING_TYPE_CANNED } from '../constants';
|
||||||
MENTIONS_TRACKING_TYPE_EMOJIS,
|
|
||||||
MENTIONS_TRACKING_TYPE_COMMANDS
|
|
||||||
} from '../constants';
|
|
||||||
import { themes } from '../../../constants/colors';
|
import { themes } from '../../../constants/colors';
|
||||||
|
import { IEmoji } from '../../EmojiPicker/interfaces';
|
||||||
|
|
||||||
const MentionItem = ({
|
interface IMessageBoxMentionItem {
|
||||||
item, trackingType, theme
|
item: {
|
||||||
}) => {
|
name: string;
|
||||||
|
command: string;
|
||||||
|
username: string;
|
||||||
|
t: string;
|
||||||
|
id: string;
|
||||||
|
shortcut: string;
|
||||||
|
text: string;
|
||||||
|
} & IEmoji;
|
||||||
|
trackingType: string;
|
||||||
|
theme: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const MentionItem = ({ item, trackingType, theme }: IMessageBoxMentionItem) => {
|
||||||
const context = useContext(MessageboxContext);
|
const context = useContext(MessageboxContext);
|
||||||
const { onPressMention } = context;
|
const { onPressMention } = context;
|
||||||
|
|
||||||
const defineTestID = (type) => {
|
const defineTestID = (type: string) => {
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case MENTIONS_TRACKING_TYPE_EMOJIS:
|
case MENTIONS_TRACKING_TYPE_EMOJIS:
|
||||||
return `mention-item-${ item.name || item }`;
|
return `mention-item-${item.name || item}`;
|
||||||
case MENTIONS_TRACKING_TYPE_COMMANDS:
|
case MENTIONS_TRACKING_TYPE_COMMANDS:
|
||||||
return `mention-item-${ item.command || item }`;
|
return `mention-item-${item.command || item}`;
|
||||||
|
case MENTIONS_TRACKING_TYPE_CANNED:
|
||||||
|
return `mention-item-${item.shortcut || item}`;
|
||||||
default:
|
default:
|
||||||
return `mention-item-${ item.username || item.name || item }`;
|
return `mention-item-${item.username || item.name || item}`;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -38,13 +49,8 @@ const MentionItem = ({
|
||||||
|
|
||||||
let content = (
|
let content = (
|
||||||
<>
|
<>
|
||||||
<Avatar
|
<Avatar style={styles.avatar} text={item.username || item.name} size={30} type={item.t} />
|
||||||
style={styles.avatar}
|
<Text style={[styles.mentionText, { color: themes[theme].titleText }]}>{item.username || item.name || item}</Text>
|
||||||
text={item.username || item.name}
|
|
||||||
size={30}
|
|
||||||
type={item.t}
|
|
||||||
/>
|
|
||||||
<Text style={[styles.mentionText, { color: themes[theme].titleText }]}>{ item.username || item.name || item }</Text>
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -52,7 +58,7 @@ const MentionItem = ({
|
||||||
content = (
|
content = (
|
||||||
<>
|
<>
|
||||||
<MentionEmoji item={item} />
|
<MentionEmoji item={item} />
|
||||||
<Text style={[styles.mentionText, { color: themes[theme].titleText }]}>:{ item.name || item }:</Text>
|
<Text style={[styles.mentionText, { color: themes[theme].titleText }]}>:{item.name || item}:</Text>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -66,6 +72,17 @@ const MentionItem = ({
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (trackingType === MENTIONS_TRACKING_TYPE_CANNED) {
|
||||||
|
content = (
|
||||||
|
<>
|
||||||
|
<Text style={[styles.cannedItem, { color: themes[theme].titleText }]}>!{item.shortcut}</Text>
|
||||||
|
<Text numberOfLines={1} style={[styles.cannedMentionText, { color: themes[theme].auxiliaryTintColor }]}>
|
||||||
|
{item.text}
|
||||||
|
</Text>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
style={[
|
style={[
|
||||||
|
@ -76,17 +93,10 @@ const MentionItem = ({
|
||||||
}
|
}
|
||||||
]}
|
]}
|
||||||
onPress={() => onPressMention(item)}
|
onPress={() => onPressMention(item)}
|
||||||
testID={testID}
|
testID={testID}>
|
||||||
>
|
|
||||||
{content}
|
{content}
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
MentionItem.propTypes = {
|
|
||||||
item: PropTypes.object,
|
|
||||||
trackingType: PropTypes.string,
|
|
||||||
theme: PropTypes.string
|
|
||||||
};
|
|
||||||
|
|
||||||
export default MentionItem;
|
export default MentionItem;
|
|
@ -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;
|
|
|
@ -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;
|
|
@ -1,22 +1,27 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import { Text, View } from 'react-native';
|
||||||
import { View, Text } from 'react-native';
|
|
||||||
import { Audio } from 'expo-av';
|
import { Audio } from 'expo-av';
|
||||||
import { BorderlessButton } from 'react-native-gesture-handler';
|
import { BorderlessButton } from 'react-native-gesture-handler';
|
||||||
import { getInfoAsync } from 'expo-file-system';
|
import { getInfoAsync } from 'expo-file-system';
|
||||||
import { deactivateKeepAwake, activateKeepAwake } from 'expo-keep-awake';
|
import { activateKeepAwake, deactivateKeepAwake } from 'expo-keep-awake';
|
||||||
|
|
||||||
import styles from './styles';
|
import styles from './styles';
|
||||||
import I18n from '../../i18n';
|
import I18n from '../../i18n';
|
||||||
import { themes } from '../../constants/colors';
|
import { themes } from '../../constants/colors';
|
||||||
import { CustomIcon } from '../../lib/Icons';
|
import { CustomIcon } from '../../lib/Icons';
|
||||||
import { logEvent, events } from '../../utils/log';
|
import { events, logEvent } from '../../utils/log';
|
||||||
|
|
||||||
const RECORDING_EXTENSION = '.aac';
|
interface IMessageBoxRecordAudioProps {
|
||||||
|
theme: string;
|
||||||
|
recordingCallback: Function;
|
||||||
|
onFinish: Function;
|
||||||
|
}
|
||||||
|
|
||||||
|
const RECORDING_EXTENSION = '.m4a';
|
||||||
const RECORDING_SETTINGS = {
|
const RECORDING_SETTINGS = {
|
||||||
android: {
|
android: {
|
||||||
extension: RECORDING_EXTENSION,
|
extension: RECORDING_EXTENSION,
|
||||||
outputFormat: Audio.RECORDING_OPTION_ANDROID_OUTPUT_FORMAT_AAC_ADTS,
|
outputFormat: Audio.RECORDING_OPTION_ANDROID_OUTPUT_FORMAT_MPEG_4,
|
||||||
audioEncoder: Audio.RECORDING_OPTION_ANDROID_AUDIO_ENCODER_AAC,
|
audioEncoder: Audio.RECORDING_OPTION_ANDROID_AUDIO_ENCODER_AAC,
|
||||||
sampleRate: Audio.RECORDING_OPTIONS_PRESET_LOW_QUALITY.android.sampleRate,
|
sampleRate: Audio.RECORDING_OPTIONS_PRESET_LOW_QUALITY.android.sampleRate,
|
||||||
numberOfChannels: Audio.RECORDING_OPTIONS_PRESET_LOW_QUALITY.android.numberOfChannels,
|
numberOfChannels: Audio.RECORDING_OPTIONS_PRESET_LOW_QUALITY.android.numberOfChannels,
|
||||||
|
@ -34,42 +39,48 @@ const RECORDING_SETTINGS = {
|
||||||
const RECORDING_MODE = {
|
const RECORDING_MODE = {
|
||||||
allowsRecordingIOS: true,
|
allowsRecordingIOS: true,
|
||||||
playsInSilentModeIOS: true,
|
playsInSilentModeIOS: true,
|
||||||
staysActiveInBackground: false,
|
staysActiveInBackground: true,
|
||||||
shouldDuckAndroid: true,
|
shouldDuckAndroid: true,
|
||||||
playThroughEarpieceAndroid: false,
|
playThroughEarpieceAndroid: false,
|
||||||
interruptionModeIOS: Audio.INTERRUPTION_MODE_IOS_DO_NOT_MIX,
|
interruptionModeIOS: Audio.INTERRUPTION_MODE_IOS_DO_NOT_MIX,
|
||||||
interruptionModeAndroid: Audio.INTERRUPTION_MODE_ANDROID_DO_NOT_MIX
|
interruptionModeAndroid: Audio.INTERRUPTION_MODE_ANDROID_DO_NOT_MIX
|
||||||
};
|
};
|
||||||
|
|
||||||
const formatTime = function(seconds) {
|
const formatTime = function (seconds: any) {
|
||||||
let minutes = Math.floor(seconds / 60);
|
let minutes: any = Math.floor(seconds / 60);
|
||||||
seconds %= 60;
|
seconds %= 60;
|
||||||
if (minutes < 10) { minutes = `0${ minutes }`; }
|
if (minutes < 10) {
|
||||||
if (seconds < 10) { seconds = `0${ seconds }`; }
|
minutes = `0${minutes}`;
|
||||||
return `${ minutes }:${ seconds }`;
|
}
|
||||||
|
if (seconds < 10) {
|
||||||
|
seconds = `0${seconds}`;
|
||||||
|
}
|
||||||
|
return `${minutes}:${seconds}`;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default class RecordAudio extends React.PureComponent {
|
export default class RecordAudio extends React.PureComponent<IMessageBoxRecordAudioProps, any> {
|
||||||
static propTypes = {
|
private isRecorderBusy: boolean;
|
||||||
theme: PropTypes.string,
|
|
||||||
recordingCallback: PropTypes.func,
|
|
||||||
onFinish: PropTypes.func
|
|
||||||
}
|
|
||||||
|
|
||||||
constructor(props) {
|
private recording: any;
|
||||||
|
|
||||||
|
private LastDuration: number;
|
||||||
|
|
||||||
|
constructor(props: IMessageBoxRecordAudioProps) {
|
||||||
super(props);
|
super(props);
|
||||||
this.isRecorderBusy = false;
|
this.isRecorderBusy = false;
|
||||||
|
this.LastDuration = 0;
|
||||||
this.state = {
|
this.state = {
|
||||||
isRecording: false,
|
isRecording: false,
|
||||||
|
isRecorderActive: false,
|
||||||
recordingDurationMillis: 0
|
recordingDurationMillis: 0
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidUpdate() {
|
componentDidUpdate() {
|
||||||
const { recordingCallback } = this.props;
|
const { recordingCallback } = this.props;
|
||||||
const { isRecording } = this.state;
|
const { isRecorderActive } = this.state;
|
||||||
|
|
||||||
recordingCallback(isRecording);
|
recordingCallback(isRecorderActive);
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillUnmount() {
|
componentWillUnmount() {
|
||||||
|
@ -83,7 +94,11 @@ export default class RecordAudio extends React.PureComponent {
|
||||||
return formatTime(Math.floor(recordingDurationMillis / 1000));
|
return formatTime(Math.floor(recordingDurationMillis / 1000));
|
||||||
}
|
}
|
||||||
|
|
||||||
isRecordingPermissionGranted = async() => {
|
get GetLastDuration() {
|
||||||
|
return formatTime(Math.floor(this.LastDuration / 1000));
|
||||||
|
}
|
||||||
|
|
||||||
|
isRecordingPermissionGranted = async () => {
|
||||||
try {
|
try {
|
||||||
const permission = await Audio.getPermissionsAsync();
|
const permission = await Audio.getPermissionsAsync();
|
||||||
if (permission.status === 'granted') {
|
if (permission.status === 'granted') {
|
||||||
|
@ -94,24 +109,27 @@ export default class RecordAudio extends React.PureComponent {
|
||||||
// Do nothing
|
// Do nothing
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
};
|
||||||
|
|
||||||
onRecordingStatusUpdate = (status) => {
|
onRecordingStatusUpdate = (status: any) => {
|
||||||
this.setState({
|
this.setState({
|
||||||
isRecording: status.isRecording,
|
isRecording: status.isRecording,
|
||||||
recordingDurationMillis: status.durationMillis
|
recordingDurationMillis: status.durationMillis
|
||||||
});
|
});
|
||||||
}
|
this.LastDuration = status.durationMillis;
|
||||||
|
};
|
||||||
|
|
||||||
startRecordingAudio = async() => {
|
startRecordingAudio = async () => {
|
||||||
logEvent(events.ROOM_AUDIO_RECORD);
|
logEvent(events.ROOM_AUDIO_RECORD);
|
||||||
if (!this.isRecorderBusy) {
|
if (!this.isRecorderBusy) {
|
||||||
this.isRecorderBusy = true;
|
this.isRecorderBusy = true;
|
||||||
|
this.LastDuration = 0;
|
||||||
try {
|
try {
|
||||||
const canRecord = await this.isRecordingPermissionGranted();
|
const canRecord = await this.isRecordingPermissionGranted();
|
||||||
if (canRecord) {
|
if (canRecord) {
|
||||||
await Audio.setAudioModeAsync(RECORDING_MODE);
|
await Audio.setAudioModeAsync(RECORDING_MODE);
|
||||||
|
|
||||||
|
this.setState({ isRecorderActive: true });
|
||||||
this.recording = new Audio.Recording();
|
this.recording = new Audio.Recording();
|
||||||
await this.recording.prepareToRecordAsync(RECORDING_SETTINGS);
|
await this.recording.prepareToRecordAsync(RECORDING_SETTINGS);
|
||||||
this.recording.setOnRecordingStatusUpdate(this.onRecordingStatusUpdate);
|
this.recording.setOnRecordingStatusUpdate(this.onRecordingStatusUpdate);
|
||||||
|
@ -128,7 +146,7 @@ export default class RecordAudio extends React.PureComponent {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
finishRecordingAudio = async() => {
|
finishRecordingAudio = async () => {
|
||||||
logEvent(events.ROOM_AUDIO_FINISH);
|
logEvent(events.ROOM_AUDIO_FINISH);
|
||||||
if (!this.isRecorderBusy) {
|
if (!this.isRecorderBusy) {
|
||||||
const { onFinish } = this.props;
|
const { onFinish } = this.props;
|
||||||
|
@ -140,7 +158,7 @@ export default class RecordAudio extends React.PureComponent {
|
||||||
const fileURI = this.recording.getURI();
|
const fileURI = this.recording.getURI();
|
||||||
const fileData = await getInfoAsync(fileURI);
|
const fileData = await getInfoAsync(fileURI);
|
||||||
const fileInfo = {
|
const fileInfo = {
|
||||||
name: `${ Date.now() }.aac`,
|
name: `${Date.now()}.m4a`,
|
||||||
mime: 'audio/aac',
|
mime: 'audio/aac',
|
||||||
type: 'audio/aac',
|
type: 'audio/aac',
|
||||||
store: 'Uploads',
|
store: 'Uploads',
|
||||||
|
@ -152,13 +170,13 @@ export default class RecordAudio extends React.PureComponent {
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logEvent(events.ROOM_AUDIO_FINISH_F);
|
logEvent(events.ROOM_AUDIO_FINISH_F);
|
||||||
}
|
}
|
||||||
this.setState({ isRecording: false, recordingDurationMillis: 0 });
|
this.setState({ isRecording: false, isRecorderActive: false, recordingDurationMillis: 0 });
|
||||||
deactivateKeepAwake();
|
deactivateKeepAwake();
|
||||||
this.isRecorderBusy = false;
|
this.isRecorderBusy = false;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
cancelRecordingAudio = async() => {
|
cancelRecordingAudio = async () => {
|
||||||
logEvent(events.ROOM_AUDIO_CANCEL);
|
logEvent(events.ROOM_AUDIO_CANCEL);
|
||||||
if (!this.isRecorderBusy) {
|
if (!this.isRecorderBusy) {
|
||||||
this.isRecorderBusy = true;
|
this.isRecorderBusy = true;
|
||||||
|
@ -167,7 +185,7 @@ export default class RecordAudio extends React.PureComponent {
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logEvent(events.ROOM_AUDIO_CANCEL_F);
|
logEvent(events.ROOM_AUDIO_CANCEL_F);
|
||||||
}
|
}
|
||||||
this.setState({ isRecording: false, recordingDurationMillis: 0 });
|
this.setState({ isRecording: false, isRecorderActive: false, recordingDurationMillis: 0 });
|
||||||
deactivateKeepAwake();
|
deactivateKeepAwake();
|
||||||
this.isRecorderBusy = false;
|
this.isRecorderBusy = false;
|
||||||
}
|
}
|
||||||
|
@ -175,54 +193,69 @@ export default class RecordAudio extends React.PureComponent {
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { theme } = this.props;
|
const { theme } = this.props;
|
||||||
const { isRecording } = this.state;
|
const { isRecording, isRecorderActive } = this.state;
|
||||||
|
|
||||||
if (!isRecording) {
|
if (!isRecording && !isRecorderActive) {
|
||||||
return (
|
return (
|
||||||
<BorderlessButton
|
<BorderlessButton
|
||||||
onPress={this.startRecordingAudio}
|
onPress={this.startRecordingAudio}
|
||||||
style={styles.actionButton}
|
style={styles.actionButton}
|
||||||
testID='messagebox-send-audio'
|
testID='messagebox-send-audio'
|
||||||
|
// @ts-ignore
|
||||||
accessibilityLabel={I18n.t('Send_audio_message')}
|
accessibilityLabel={I18n.t('Send_audio_message')}
|
||||||
accessibilityTraits='button'
|
accessibilityTraits='button'>
|
||||||
>
|
|
||||||
<CustomIcon name='microphone' size={24} color={themes[theme].auxiliaryTintColor} />
|
<CustomIcon name='microphone' size={24} color={themes[theme].auxiliaryTintColor} />
|
||||||
</BorderlessButton>
|
</BorderlessButton>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!isRecording && isRecorderActive) {
|
||||||
|
return (
|
||||||
|
<View style={styles.recordingContent}>
|
||||||
|
<View style={styles.textArea}>
|
||||||
|
<BorderlessButton
|
||||||
|
onPress={this.cancelRecordingAudio}
|
||||||
|
// @ts-ignore
|
||||||
|
accessibilityLabel={I18n.t('Cancel_recording')}
|
||||||
|
accessibilityTraits='button'
|
||||||
|
style={styles.actionButton}>
|
||||||
|
<CustomIcon size={24} color={themes[theme].dangerColor} name='delete' />
|
||||||
|
</BorderlessButton>
|
||||||
|
<Text style={[styles.recordingDurationText, { color: themes[theme].titleText }]}>{this.GetLastDuration}</Text>
|
||||||
|
</View>
|
||||||
|
<BorderlessButton
|
||||||
|
onPress={this.finishRecordingAudio}
|
||||||
|
// @ts-ignore
|
||||||
|
accessibilityLabel={I18n.t('Finish_recording')}
|
||||||
|
accessibilityTraits='button'
|
||||||
|
style={styles.actionButton}>
|
||||||
|
<CustomIcon size={24} color={themes[theme].tintColor} name='send-filled' />
|
||||||
|
</BorderlessButton>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View style={styles.recordingContent}>
|
<View style={styles.recordingContent}>
|
||||||
<View style={styles.textArea}>
|
<View style={styles.textArea}>
|
||||||
<BorderlessButton
|
<BorderlessButton
|
||||||
onPress={this.cancelRecordingAudio}
|
onPress={this.cancelRecordingAudio}
|
||||||
|
// @ts-ignore
|
||||||
accessibilityLabel={I18n.t('Cancel_recording')}
|
accessibilityLabel={I18n.t('Cancel_recording')}
|
||||||
accessibilityTraits='button'
|
accessibilityTraits='button'
|
||||||
style={styles.actionButton}
|
style={styles.actionButton}>
|
||||||
>
|
<CustomIcon size={24} color={themes[theme].dangerColor} name='delete' />
|
||||||
<CustomIcon
|
|
||||||
size={24}
|
|
||||||
color={themes[theme].dangerColor}
|
|
||||||
name='close'
|
|
||||||
/>
|
|
||||||
</BorderlessButton>
|
</BorderlessButton>
|
||||||
<Text
|
<Text style={[styles.recordingDurationText, { color: themes[theme].titleText }]}>{this.duration}</Text>
|
||||||
style={[styles.recordingCancelText, { color: themes[theme].titleText }]}
|
<CustomIcon size={24} color={themes[theme].dangerColor} name='record' />
|
||||||
>
|
|
||||||
{this.duration}
|
|
||||||
</Text>
|
|
||||||
</View>
|
</View>
|
||||||
<BorderlessButton
|
<BorderlessButton
|
||||||
onPress={this.finishRecordingAudio}
|
onPress={this.finishRecordingAudio}
|
||||||
|
// @ts-ignore
|
||||||
accessibilityLabel={I18n.t('Finish_recording')}
|
accessibilityLabel={I18n.t('Finish_recording')}
|
||||||
accessibilityTraits='button'
|
accessibilityTraits='button'
|
||||||
style={styles.actionButton}
|
style={styles.actionButton}>
|
||||||
>
|
<CustomIcon size={24} color={themes[theme].tintColor} name='send-filled' />
|
||||||
<CustomIcon
|
|
||||||
size={24}
|
|
||||||
color={themes[theme].successColor}
|
|
||||||
name='check'
|
|
||||||
/>
|
|
||||||
</BorderlessButton>
|
</BorderlessButton>
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue