Merge 4.26.0 into master (#3978)
* test * Remove console.log * test * [FIX] StoryShots not working for async rendered components (#3677) * remove console.log * Add missing DiscussionsView snapshot * fix build and useless done and async generator * update snapshot * Chore: fix build and useless done and async generator (#3678) * fix build and useless done and async generator * update snapshot * Chore: Migrate Database to Typescript (#3580) Co-authored-by: Gleidson Daniel Silva <gleidson10daniel@hotmail.com> Co-authored-by: Diego Mello <diegolmello@gmail.com> * Chore: Migrate redux module permissions to typescript (#3630) * Chore: Migrate redux module share to typescript (#3612) * chore: migrate redux module share to typescript * chore: fix types * chore: update types * chore: migrate redux module share to typescript * remove double import * chore: fix import * Chore: Migrate redux module createChannel to typescript (#3602) * chore: migrate createChannel to ts and add tests * chore: fix naming * chore: add more types and remove mapDispatchToProps from components * remove todo * update tests * chore: migrate interface to reducer and fix errors on return * chore: insert IApplicationState to mapStateToProps state type * Remove spread * fix type * fix import and state type Co-authored-by: Diego Mello <diegolmello@gmail.com> * Chore: Migrate redux module app to typescript (#3598) * chore: migrate activeUsers reducer and action to TS * chore: init types folder and set redux and BaseScreen interface * chore: remove mapDispatchToProps to use dispatch prop and clear some types * chore: type selectedUsers action and reducer and improvement in the code of other files * chore: move IUser to base types * chore: move state props to ISelectedUsersViewProps * chore: create mocketStore * chore: remove applyAppStateMiddleware * test: create activeUser and selectedUser tests * test: add more selectedUsers tests * chore: fix action type * chore: move types to definition folder and fix imports * chore: remove unused const * chore: migrate redux tests to reducer folder and add eslint jest plugin * chore: exprot initial state and then import on tests * chore: move interfaces to reducer and import on screen * chore: set eslint-plugin-jest version to 24.7.0 * chore: fix IUser import * chore: update interfaces and types names * chore: update definitions * chore: update IBaseScreen definitions * chore: init reducer/app migration to ts * chore: add tests and migrate RootEnum * wip: migrate fixed consts to RootEnum * chore: remove redux action inferences * fix types Co-authored-by: Diego Mello <diegolmello@gmail.com> * Chore: Migrate redux module createDiscussion to typescript (#3604) * chore: migrate createDiscussion to ts and add tests * chore: add TActionCreateDiscussion to TApplicationActions * fix types * update types * fix types Co-authored-by: Diego Mello <diegolmello@gmail.com> * [FIX] ios-testflight-experimental unable to find cache (#3684) * Chore: Remove Non-null assertion operator in ThreadMessagesView (#3632) Co-authored-by: Diego Mello <diegolmello@gmail.com> * Chore: Migrate CannedResponsesListView to Typescript (#3553) * Chore: Migrate CannedResponsesListView to TS * Moved IcannedResponse to definitions and fixed the index * Chore: Migrate CannedResponseDetail to TS * minor tweaks * refactor: update new types and interfaces for use ISubscription * fix lint error and canned responses's dropdown Co-authored-by: AlexAlexandre <alexalexandrejr@gmail.com> Co-authored-by: Diego Mello <diegolmello@gmail.com> * Chore: Migrate LivechatEditView to Typescript (#3499) * Chore: Migrate LivechatEditView to Typescript * refactor: minor tweak * refactor: fix the interfaces for input * refactor: fix lint erros * minor tweak with new navigation types * function * iroom tweak * livechateditview tweak * TextInput tweak * refactor: update new types and interfaces for use ISubscription * refactor to default useState type * change the component name in SearchBox * changed state type Co-authored-by: AlexAlexandre <alexalexandrejr@gmail.com> Co-authored-by: Gerzon Z <gerzonc@icloud.com> Co-authored-by: Diego Mello <diegolmello@gmail.com> * Regression: Message press navigating to empty RoomView (#3680) Co-authored-by: Diego Mello <diegolmello@gmail.com> * Regression: Prevent duplicated .jpg on file upload (#3658) * [FIX] Regression: Prevent duplicated .jpg on file upload * refactor to all files typed as image/jpeg * isolate regexp to function * refactor forceJpgExtension * clean * minor tweak * [FIX] Regression: Prevent duplicated .jpg on file upload * refactor to all files typed as image/jpeg * isolate regexp to function * refactor forceJpgExtension * clean * minor tweak * refactored comment * Chore: Migrate lib/utils to TypeScript (#3637) * Migrate utils to TypeScript * Add @types/semver * Refactor compareServerVersion(currentVersion, oldVersion, func) to compareServerVersion(current, func, oldVersion) Co-authored-by: Diego Mello <diegolmello@gmail.com> * Chore: Migrate readMessages to TS (#3669) * Migrate readMessages to TS * Update IRocketChat interface * [FIX] Unnecessary login dispatch on adding new server (#3693) * [FIX] Disable tap gesture on call messages (#3694) * [IMPROVE] Keep biometry option from last session (#3668) Co-authored-by: GleidsonDaniel <gleidson10daniel@hotmail.com> Co-authored-by: Reinaldo Neto <reinaldonetof@hotmail.com> * Fix reactotron multiple connections (#3622) * Chore: Fix rocketchat interface (#3705) * Chore: Migrate logout to Typescript (#3688) * [NEW] Stream to get individual presence updates (#3606) Co-authored-by: Gerzon Z <gerzonzcanario@gmail.com> * [FIX] Inject Redux store to prevent/remove require cycles (#3691) Co-authored-by: Diego Mello <diegolmello@gmail.com> * Chore: Migrate lib/rocketchat.js to TS - structure PoC (#3661) Co-authored-by: Diego Mello <diegolmello@gmail.com> Co-authored-by: Gleidson Daniel Silva <gleidson10daniel@hotmail.com> * [FIX] #3606 merged using wrong JS SDK branch (#3709) * [FIX] Remove deprecated database methods and other database operations (#3686) * Fix PK error on subscriptions/room * Instead of checking for pending update, wrap the call on a try catch and return null in case of error * Generate delete operations before create/update to prevent errors * Apply same logic on encryption * Fix database operations on getRoles * Fix a few database issues found on Bugsnag on ThreadMessagesView * Run prettier :( * Chore: Add REST API definitions from server (#3721) * create first definitions * chore: implements get and post types * fix lint * add ts-ignore * add teams.removeRoom method * Remove unused endpoints Co-authored-by: Diego Mello <diegolmello@gmail.com> * Chore: Move some methods to SDK (#3736) * [IMPROVE] Add support for ephemeral messages inside threads (#3687) * Chore: dehydrate small server requests away from rocketchat.js (#3740) * Bump version to 4.25.0 (#3745) * [Snyk] Security upgrade url-parse from 1.5.1 to 1.5.6 (#3746) The following vulnerabilities are fixed with an upgrade: - https://snyk.io/vuln/SNYK-JS-URLPARSE-2401205 * Language update from LingoHub 🤖 on 2022-02-14Z (#3730) * Language update from LingoHub 🤖 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 🚀 * remove draft gl Co-authored-by: Robot LingoHub <robot@lingohub.com> Co-authored-by: Diego Mello <diegolmello@gmail.com> * Chore: Migrate methods/getSingleMessage to TS (#3700) * migrate getSingleMessage to TS * minor tweak * Chore: Migrate methods/getRooms to TS (#3702) * migrate getRooms to TS * add sdk and set any types * Moved the new variable around and added ts-ignore to follow the pattern from /services/restApi.ts Co-authored-by: Diego Mello <diegolmello@gmail.com> * Chore: Migrate updateMessages to Typescript (#3715) * Chore: Migrate selector/login to TS (#3731) * migrate selector/login to TS * Fix lint errors * set aliases for returns * Chore: Migrate helpers/parseUrls to Typescript (#3735) * Chore: Migrate methods/helpers/parseQuery to Typescript (#3742) * Chore: Migrate methods/helpers/parseQuery to Typescript * tweak in example * Chore: Migrate app/commands to typescript (#3697) * Chore: Migrate lib/encryption folder to TypeScript (#3639) * Initial commit * add types/bytebuffer, add type definitions to params and update interfaces * add more types and type assertions * update types * change bang operator by type assertion and update class variables definitions * add types for deferred class * minor tweaks on types definitions * add ts-ignore * Update encryption.ts * update deferred and encryption * update encryption.ts * Update room.ts * update toDecrypt type * initialize sessionKeyExportedString * remove return types * Chore: Migrate redux actions/enterpriseModules to TS (#3698) * migrate enterpriseModules to TS * update test file * Chore: Migrate database/services and database/utils to TS (#3708) * migrate database services and utils to ts * Migrate tests Co-authored-by: Diego Mello <diegolmello@gmail.com> * Chore: Migrate buildMessage to TS (#3732) * migrate buildMessage to TS * Fix lint * minor tweak * minor tweaks * Chore: Migrate getPermissions to Typescript (#3720) * Migrating... * Fix IPermission * Playing with types * Remove `as const` * Fix lint * Fix test * Apply sdk * Fix lint and autocomplete * [FIX] Add search and fix pagination for omnichannels departments (#3621) * [FIX] Search and pagination for omnichannels departments * pagination complete * minor tweak * renamed a param and workaround for a ux bug * fix style of flatlist and search as header scrollable * stick the header * Merge branch 'fix.forward-department-list' of https://github.com/RocketChat/Rocket.Chat.ReactNative into fix.forward-department-list * refactor pagination * fix value type * refactor render search * refactor layout * make ts happy * Chore: Migrate Markdown to Typescript (#3558) * Chore: Migrate Markdown to TS * Chore: Migrate Markdown to TS * minor tweak * added preview where markdown was preview and fixed params within markdown * removed ts-ignore * fix lint * removed numbersofline={0} and default value to numberOfLines=1 * change how to import markdown preview and remove numberOfLines * using useTheme inside markdownPreview and remove theme from components * minor tweak on interfaces * isNewMarkdown return as boolean * minor tweaks * minor tweaks * removed unused component * fixed markdown stories * updated snapshot because removed numberOfLines={0} from message/content * create IEmoji.ts in definitions and refactor all places where getCustomEmoji was called * onLinkPress typed * todo: refactor navtoroominfo * formatText.test.ts * markdown stories to typescript too * minor tweak * IMessage definition * refactor: update new types and interfaces for use ISubscription * refactor: update threadItem for use new MarkdownPreview * refactor: rollback wrong file commited * formatHyperlink * fix lint * updated item story shot * refactor and refactor some types * Remove non-null assertion * Minor change on useRealName * tweak Co-authored-by: AlexAlexandre <alexalexandrejr@gmail.com> Co-authored-by: Diego Mello <diegolmello@gmail.com> * Chore: Migrate methods/callJitsi to typescript (#3660) * chore: migrate callJitsi to typescript * change fixed string to type * wip * wip * back to old times :) * back to typescript * Chore: Migrate redux module room to typescript (#3636) * chore: migrate redux module room to typescript and remove dispatch on dependencies * chore: add tests to redux module room * chore: create ERoomType and use on implemention * chore: update enum name * fix test id * Chore: Migrate redux module login to typescript (#3647) * chore: migrate redux module login to typescript chore: update redux module login tests * update workers * wip * fix type * remove partial * add more status * migrate the rest of the stuff to typescript * fix tests and types * fix types and tests * Chore: Migrate method getSettings to typescript (#3703) * chore: migrate getSettings to typescript and and some types * chore: remove this and add current to code * chore: add current * Chore: Migrate getCustomEmojis to TS (#3724) * update customEmoji interface and getCustomEmoji * add sdk * updated emojiCustom rest definition * minor refactor * update params object * [FIX] getRooms request using param with wrong name (#3761) * Chore: Migrate methods/getRoomInfo to TS (#3695) * migrate getRoomInfo to TS * update room type * update types * Fix lint error * Chore: Migrate getSlashCommands to TS (#3711) * migrate getSlashCommands to TS * use sdk and update getSlashCommands * minor tweak * Remove implicit anys Co-authored-by: Diego Mello <diegolmello@gmail.com> * Chore: Migrate getUsersPresence to TS (#3717) * migrate getUsersPresence to ts * use sdk and remove this context from getUsersPresence * Chore: Migrate loadMissedMessages to typescript (#3704) * chore: migrate loadMissedMessages to typescript * remove loaderItem * remove this from functions * Chore: Migrate methods/getRoles to Typescript (#3741) * chore: migrate getRoles to ts * chore: removing unused const * chore: minor tweak * Type batch Co-authored-by: Diego Mello <diegolmello@gmail.com> * Chore: Migrate methods/loadMessagesForRoom to Typescript (#3701) * chore: change loadMessagesForRoom to typescript * minor tweak * chore: minor tweaks after merge with developer * chore: minor tweaks after merge with developer * chore: minor tweak * chore: minor tweaks * Fix return Co-authored-by: Diego Mello <diegolmello@gmail.com> Co-authored-by: Gleidson Daniel Silva <gleidson10daniel@hotmail.com> * Chore: Migrate methods/sendFileMessage to typescript (#3683) * chore: start the migration * chore: update sendFileMessage to ts * chore: removing an `any` from uploadQueue * chore: minor tweak * chore: minor tweak * chore: minor tweaks after merge with developer * chore: minor tweak after merge develop into current * [FIX] Differ to Last Session Authenticated (#3667) * [FIX] Differ to Last Session Authenticated * Added timesync * [FIX] Differ to Last Session Authenticated * Added timesync * timesync tweaks * refactor diffLastLocalSession and saveLastLocalAuthentication * did a race * Update comment in app/utils/localAuthentication.ts Co-authored-by: Diego Mello <diegolmello@gmail.com> * refactor getServerTimeSync and when use this route * tweak Co-authored-by: Diego Mello <diegolmello@gmail.com> * Chore: Migrate methods/loadNextMessages to typescript (#3719) * feat: update loadNextMessages to ts * minor tweak * chore: minor tweaks after merge with developer * chore: migrate getFileUrlFromMessage to ts (#3734) * Chore: Migrate to Typescript mergeSubscriptionRooms and findSubscriptionsRooms (#3747) * fix fromJSONValue type * migrate findSubscription and mergeSubscription to typescript * chore: fix subscription param returning null * Chore: Migrate sendMessage to TS (#3712) Co-authored-by: Diego Mello <diegolmello@gmail.com> * Chore: Migrate methods/enterpriseModules to TS (#3706) Co-authored-by: Diego Mello <diegolmello@gmail.com> * Chore: Migrate methods/getThreadName to typescript (#3707) * chore: change getThreadName to typescript * chore: change types after merge develop into current * chore: minor tweak * Chore: Migrate method canOpenRoom to Typescript (#3650) * chore: migrate canOpenRoom to ts * chore: update rocketchat types * change types to Isubscription types * fix: fix mergeSubscriptionsRooms (#3770) * [IMPROVE] Team system messages feedback (#3771) * almost there * Update stories * chore: removing unused function getTeamInfo (#3773) * Chore: create definitions for e2e.setUserPublicAndPrivateKeys api call (#3775) * Chore: Migrate RoomMembersView to Typescript (#3769) * chore: migrate RoomMembersView to ts * fix goRoom item interface * Chore: Migrate views/RoomListView to typescript (#3758) * chore: migrating RoomListView to ts * chore: migrating RoomListView to ts * chore: implementing types for RoomListView * chore: change ChatsStackParamList for fix RoomListView errors * chore: minor tweak * chore: minor tweak * chore: fix setTimeout type * chore: applying changes requested * chore: minor tweak * Chore: Migrate to TS RommInfoEditView (#3766) * initial commit * fix last types * fix import * fix lint * Chore: Server API types POC - loadMessagesForRoom (#3765) * create interface and implements base types * fix some types * Update app/lib/methods/updateMessages.ts Co-authored-by: Diego Mello <diegolmello@gmail.com> * fix date type * apply types changes * fix type * fix date value * fix types * typescript things... Co-authored-by: Diego Mello <diegolmello@gmail.com> * Chore: Migrate RoomView to Typescript (#3754) * Chore: Migrate RoomActionsView to Typescript (#3750) * Chore: Migrate loadThreadMessages to TS (#3718) * Migrate loadThreadMessages to TypeScript * Update interfaces * Bump version to 4.26.0 (#3806) * Chore: Migrate REST API - markAsUnread to TS (#3801) * Migrate `subscriptions.unread` to typescript * Chore: Migrate REST API - convertChannelToTeam to TS (#3792) * migrate channels.convertToTeam and groups.convertToTeam to ts Co-authored-by: Gleidson Daniel Silva <gleidson10daniel@hotmail.com> * Chore: Migrate REST API - getDepartmentInfo to Typescript (#3795) * Chore: Migrate REST API - sendConfirmationEmail to Typescript (#3807) * Chore: Migrate REST API - getRoutingConfig to Typescript (#3794) * Chore: Migrate REST API - getRoutingConfig to Typescript * minor tweak * Chore: Migrate REST API - createChannel to Typescript (#3786) * Chore: Migrate REST API - createChannel to Typescript * removed success * iserverroomitem * Chore: Migrate REST API - e2eGetUsersOfRoomWithoutKey to Typescript (#3793) * Chore: Migrate REST API - e2eGetUsersOfRoomWithoutKey to Typescript * Update app/definitions/rest/v1/e2e.ts Co-authored-by: Gleidson Daniel Silva <gleidson10daniel@hotmail.com> Co-authored-by: Gleidson Daniel Silva <gleidson10daniel@hotmail.com> * Chore: Migrate REST API - register to TS (#3796) * Migrate `user.register` to TypeScript * Chore: Migrate REST API - createTeam to TS (#3788) * Migrate `teams.create` REST API to TypeScript * Chore: Migrate REST API - addRoomsToTeam to TS (#3797) * Migrate REST API `teams.addRooms` to TypeScript * Chore: Migrate REST API - removeTeamMember to TS (#3799) * Migrate REST API `teams.removeMember` to TypeScript * Chore: Migrate REST API - toggleArchiveRoom to Typescript (#3791) * Chore: Migrate REST API - toggleArchiveRoom to Typescript * minor tweak * removed success param * minor tweak * Chore: Migrate REST API - convertTeamToChannel to TS (#3800) * Migrate REST API `teams.convertToChannel` to TypeScript * Chore: Migrate REST API - deleteMessage to TS (#3802) * Migrate REST API `chats.delete` to TypeScript * Chore: Server API types - user.setPreferences (#3781) * chore: implementing type for test api - user.setPreferences * chore: minor tweak * Chore: Server API types - teams.updateRoom (#3774) * chore: type the API call `teams.updateRoom` * chore: creating the interface `IServerTeamUpdateRoom` * chore: minor tweak after merge * Chore: Migrate REST API - teamListRoomsOfUser to TS (#3805) * migrate REST API `teams.listRoomsOfUser` to TypeScript * update: `rooms` type on `teams.listRoomsOfUser` * update: if-conditionals on `RoomActionsView` * Chore: Migrate REST API - toggleBlockUser to Typescript (#3808) * Chore: Migrate REST API - saveRoomSettings to Typescript (#3787) * Chore: Migrate methods/loadSurroundingMessages to Typescript (#3733) * Chore: Migrate methods/loadSurroundingMessages to Typescript * tweaks * tweak * tweak * tweaks to make ts happy * instead as IMessage is optional u * enum to MessageTypeLoad * minor tweaks * Chore: Migrate RoomInfoView to Typescript (#3778) * Chore: Migrate RoomInfoView to Typescript * tweak in avatar * tweak with SubscriptionType * minor tweak package * Chore: Migrate RoomInfoView to Typescript * tweak in avatar * tweak with SubscriptionType * minor tweak package * react.reactelement | null * minor tweak * minor tweak livechatvisitor * remove console.log * Tweaks * Fix: fix the command to run detox on android (#3812) * fix the command to run detox on android (#3812) * Chore: Migrate subscriptions/rooms to TS (#3748) * chore: migrate subscriptions rooms to ts * chore: adding a TODO to remember this problem * chore: removing unnecessary todos * chore: minor tweak after develop updates * chore: migrate message service to ts * chore: minor tweaks * chore: minor tweak * chore: minor tweak after merge develop into this branch * chore: minor tweak after merge with dev * minor tweak * Chore: Migrate subscriptions/room to TS (#3752) * chore: initial commit * chore: fix readMessages * chore: removing some `any` * chore: removing some `any` * chore: removing some `any` * chore: fix erros after merge develop inside this branch * chore: minor tweak * chore: applying changes requested * minor tweak Co-authored-by: Gleidson Daniel Silva <gleidson10daniel@hotmail.com> * Chore: Migrate REST API - e2eRequestSubscriptionKeys to Typescript (#3813) * Chore: Migrate REST API - merge v1/user in v1/users (#3827) * Chore: Migrate REST API - toggleRead to Typescript (#3820) * Chore: Migrate REST API - e2eSetRoomKeyID to Typescript (#3816) * Chore: Migrate REST API - removeTeamRoom to Typescript (#3819) * Chore: Migrate REST API - updateGroupKey to Typescript (#3817) * Chore: Migrate REST API - forgotPassword to Typescript (#3818) * Chore: Migrate REST API - updateJitsiTimeout to Typescript (#3822) * chore: add rest api return * chore: add rest api return * chore: add rest api return (#3824) * chore: add rest api return (#3825) * chore: add rest api return (#3829) * chore: add rest api return (#3826) * Chore: Delete unused function getUserRoles (#3836) * Chore: Migrate REST API - leaveRoom to Typescript (#3833) * chore: add rest api type * remove any * Chore: Migrate REST API - deleteRoom to Typescript (#3834) * Chore: Migrate REST API - removeUserFromRoom to Typescript (#3837) * Chore: Migrate REST API - hideRoom to Typescript (#3832) * chore: add rest api return (#3850) * chore: add rest api return (#3849) * Chore: Migrate REST API - setUserStatus to Typescript (#3828) * chore: add rest api return * chore: add rest api return * Chore: Migrate REST API - spotlight to Typescript (#3821) * Chore: Migrate REST API - spotlight to Typescript * minor tweak * chore: add rest api return (#3844) * Chore: Migrate REST API - usersAutoComplete to Typescript (#3845) * chore: add rest api return (#3847) * chore: add rest api return (#3848) * Chore: Migrate REST API - joinRoom to Typescript (#3835) * Chore: Migrate REST API - joinRoom to Typescript * join to discussion * Chore: Migrate REST API - returnLivechat to Typescript (#3843) * Chore: Migrate REST API - getRoomCounters to Typescript (#3842) * Chore: Migrate REST API - getRoomCounters to Typescript * minor tweak * [FIX] Condensed layout cutting text on smaller text sizes (#3831) * Chore: Migrate ee/omnichannel folder to Typescript (#3749) * Chore: Migrate ee/omnichannel folder to Typescript * omnichannelstatus and queue list * boolean searching and react.ref * test initi * test and refactor interfaces * minor tweak * minor tweaks * [FIX] Hardcoded E2E password for Detox workflow (#3809) * [FIX] Removed account from E2E Data and create a file responsible for this account * Updated e2e/README.md * minor tweak * Update e2e/README.md Co-authored-by: Gerzon Z. <gerzonzcanario@gmail.com> Co-authored-by: Gerzon Z. <gerzonzcanario@gmail.com> * Chore: Migrate REST API - getTagsList to Typescript (#3854) * Chore: Migrate normalizeMessage to TS (#3743) * migrate normalizeMessage to ts * fix: missing null validations and type aliases * Chore: Migrate REST API - e2eRequestRoomKey to Typescript (#3814) * Chore: Remove Teams migration (#3857) * Chore: Migrate REST API - getChannelInfo to TS (#3839) * add: type for REST API `channels.info` * add: `ts-ignore` to `RoomActionsView` * Chore: Migrate search and localSearch methods to Typescript (#3751) * chore: migrate search to typescript * fix types * fix type Co-authored-by: Diego Mello <diegolmello@gmail.com> * Chore: Migrate REST API - getListCannedResponse to Typescript (#3858) * Chore: Migrate REST API - getCustomFields to Typescript (#3856) * Chore: Migrate REST API - getCustomFields to Typescript * minor tweak * Chore: Migrate REST API - getAgentDepartments to Typescript (#3855) * Chore: Create IServerRoom and IServerSubscription (#3782) * Chore: Server API types - chat.getDiscussions (#3776) * chore: implementing type for test api - getDiscussions * Fix DiscussionDetails count usage * chore: update getDiscussions to use IMessageFromServer Co-authored-by: Diego Mello <diegolmello@gmail.com> * Chore: Migrate REST API - getTeamListRoom to TS (#3840) * add: `teams.listRooms` types * add: `PaginatedResult<T>` to `teams.ts` * Chore: Dehydrate login methods from rocketchat.js (#3759) * dehydrate login methods from rocketchat.js * [FIX] Merge subscription and room logic using unparsed data (#3859) * chore: add rest api return (#3864) * chore: add rest api return (#3865) * Chore: Migrate REST API - saveUserProfile to Typescript (#3863) * Chore: Migrate REST API - saveUserProfile to Typescript * minor tweak * Chore: Update react-native-mmkv-storage to 0.6.12 (#3634) * chore: updating mmkv library * feat: updating android ejson to use the getSecureKey new version * feat: updating IOS files to use the getSecureKey new version * feat: changing all mmkv methods to use sync calls * feat: changing mmkv methods from Screen lock * feat: changing all mmkv methods from login, ssl certificate and change/add server * feat: changing all mmkv methods from login, ssl certificate and change/add server * feat: changing all mmkv methods from logout * feat: changing all mmkv methods from e2e * fix: small fix at encryption and server drop down * feat: changing all mmkv methods from set theme * feat: changing all mmkv methods from openLink * fix: setting up mmkv to works property on Android * fix: fix an error to set the theme when open the app * refactor: change the react-native branch (temporary) * refactor: removing all `Async` from mmkv functions name * refactor: removing await from unnecessary functions, removing console.log and update cocoapods * refactor: removing unnecessary undefined from methods * feat: creating a custom hook for mmkv * refactor: changing the fetchPasscode to use the useUserPreferences hook * refactor: changing setTheme from app/index * refactor: small fix on setTheme * chore: update mmkv to 0.6.11 * chore: update mmkv to 0.6.11 * chore: minor tweak * chore: update mmkv to 0.6.12 * chore: mock NativeModules * chore: fix test mmkv * chore: removing custom MMKV JSI module, since is no more necessary after 0.6.11 version * feat: removing some async calls from mmkv after update from develop * feat: creating a function to get the initialTheme * feat: removing unnecessary try/catch * fix: fixing the blink white when open the app * feat: changing useMMKVStorage to create from mmkv lib * test: creating a mock for mmkv create function * chore: fix errors on tablet * minor tweak * Chore: Migrate REST API - getUserPreferences to Typescript (#3830) * Chore: Migrate REST API - getAvatarSuggestion to Typescript (#3869) * Chore: Migrate REST API - resetAvatar to Typescript (#3870) * Chore: Migrate REST API - setAvatarFromService to Typescript (#3871) * Chore: Migrate REST API - getUsernameSuggestion to Typescript (#3872) * Chore: Migrate REST API - getMessages to Typescript (#3875) * Chore: Migrate REST API - searchMessages to Typescript (#3874) * Chore: Migrate REST API - getFiles to Typescript (#3873) * [FIX] Issues after reconnecting (#3815) * [FIX] Fix synchronization of removed subscriptions (#3768) When a subscription is removed from the server via another client, when the mobile app is relaunched the subscriptions were not being removed properly. Changed the logic to use the subscriptionResult.remove array from the server and the _id property to fix. Co-authored-by: Christian King <cking@vonix.io> * Chore: Migrate REST API - editLivechat to Typescript (#3878) * Chore: Migrate REST API - getReadReceipts to Typescript (#3877) * [FIX] ReactNativeUiLib are not installed after run pod install (#3882) * fix ReactNativeUiLib not installed when run pod install * committing Podfile.lock * Chore: Migrate REST API - editMessage to Typescript (#3887) * Chore: Migrate REST API - editMessage to Typescript * minor tweak * Chore: Migrate REST API - e2eResetOwnKey to Typescript (#3888) * Chore: Migrate REST API - e2eResetOwnKey to Typescript * Update app/lib/rocketchat/services/restApi.ts Co-authored-by: Gleidson Daniel Silva <gleidson10daniel@hotmail.com> * Chore: Migrate REST API - emitTyping to Typescript (#3886) * Chore: Migrate REST API - getRoomRoles to Typescript and fix getRoomMembers (#3868) * Chore: Migrate REST API - getRoomRoles to Typescript and fix getRoomMembers * change GetRoomRoles local * Chore: Migrate REST API - addUsersToRoom to Typescript (#3884) * Chore: Migrate REST API - addUsersToRoom to Typescript * Update app/lib/rocketchat/services/restApi.ts Co-authored-by: Gleidson Daniel Silva <gleidson10daniel@hotmail.com> * minor tweak Co-authored-by: Gleidson Daniel Silva <gleidson10daniel@hotmail.com> * Chore: Migrate REST API - createGroupChat to Typescript (#3885) * Chore: Migrate REST API - createGroupChat to Typescript * Update app/lib/rocketchat/services/restApi.ts Co-authored-by: Gleidson Daniel Silva <gleidson10daniel@hotmail.com> * minor tweak Co-authored-by: Gleidson Daniel Silva <gleidson10daniel@hotmail.com> * Chore: Migrate REST API - getCommandPreview to Typescript (#3894) * Chore: Migrate REST API - getCommandPreview and executeCommandPreview to Typescript (#3897) * Chore: Migrate REST API - getDirectory to Typescript (#3898) * Chore: Migrate REST API - getServerInfo to Typescript (#3900) * [FIX] Mention from suggestions concatenates to query string on autocomplete (#3696) * [FIX] Mention suggestion concatenate to query string * Add function to get regexp and its tests in separate files * Update getRegexp.ts * Update file names * Try new regex * One regex for all mention types Co-authored-by: Gleidson Daniel Silva <gleidson10daniel@hotmail.com> * Chore: Migrate REST API - readThreads to Typescript (#3866) * chore: migrate readThreads to typescript * fix imports * Chore: Migrate REST API - getRoomInfo to Typescript (#3823) * chore: add rest api return * chore: add rest api return * Chore: Migrate REST API - sendEmailCode to Typescript (#3891) * Chore: Migrate REST API - sendEmailCode to Typescript (#3905) * Chore: Migrate REST API - saveAutoTranslate to Typescript (#3892) * Chore: Migrate REST API - runSlashCommand to Typescript (#3893) * Chore: Migrate REST API - getSyncThreadsList to Typescript (#3896) * Chore: Migrate REST API - getRoomMembers to Typescript (#3899) * Chore: Migrate methods/actions to Typescript and refactor UiKit folder (#3716) * Chore: Migrate methods/actions to Typescript * tweak in actions * Co-authored-by: Gleidson Daniel Silva <gleidson10daniel@hotmail.com> * refactor sdk.current * interface and uikit done * refactor interface, index and utils from UiKit * minor tweak * minor tweak Co-authored-by: Gleidson Daniel Silva <gleidson10daniel@hotmail.com> * Chore: Migrate REST API - removePushToken to Typescript (#3903) * Chore: Migrate REST API - registerPushToken to Typescript (#3902) * Chore: Dehydrate share extension from rocketchat.js (#3753) Co-authored-by: Gleidson Daniel Silva <gleidson10daniel@hotmail.com> * Chore: Migrate REST API - getThreadsList to Typescript (#3895) * chore: add rest api return * chore: removing sort param * Chore: evaluate Header components - TypeScript (#3918) * update: Header components * Chore: Evaluate BackgroundContainer - TypeScript (#3917) * update: `BackgroundContainer` * remove: `theme` from `IBackgroundContainer` * update: return type for `BackgroundContainer` * Chore: Evaluate ActivityIndicator - TypeScript (#3914) * update: `BackgroundContainer` and `ActivityIndicator` * update: return type for `RCActivityIndicator` * update: return type for `BackgroundContainer` * [NEW] Collapsible Message (#3879) * create new collapsible component * create collapsible tests and update snapshot * fix quote :) * update snapshot * add support to color * add collapsed prop * fix some styles * fix tests * wip * clean * add CollapsibleQuote story * better style * update snapshots * add better tests * remove testID * update storyshot * Chore: Migrate containers: Toast to Typescript (#3913) * Chore: Migrate containers: Loading to Typescript (#3915) * Chore: Migrate REST API - e2eFetchMyKeys to Typescript (#3942) * [FIX] Messages not loading for unjoined channels (#3904) * Fix message loading for unjoined channels * Update updateMessages.ts * log -> console.log Co-authored-by: Diego Mello <diegolmello@gmail.com> * Chore: StatusBar theme and style props (#3906) * chore: clean status bar and remove useless theme * mend * wip * remove unused props * chore: Evaluate SearchBox (#3909) * [FIX] Wrong param sent to system message (#3943) * [FIX] Display added user to team * tweak when remove members from team too * update storyshot Co-authored-by: AlexAlexandre <alexalexandrejr@gmail.com> * [NEW] Redesign quoted messages (#3883) * Language update from LingoHub 🤖 on 2022-03-21Z (#3940) Co-authored-by: Diego Mello <diegolmello@gmail.com> * Chore: Clean TwoFactor - Typescript (#3912) * chore: clean TwoFactor * minor tweak * minor refactor * chore: removing console log * Chore: Clean Check component - TypeScript (#3919) * chore: clear Check component * chore: update tests * Chore: Clean InAppNotification - TypeScript (#3920) * Chore: Clean ThreadDetails - TypeScript (#3924) * Chore: Clean MessageErrorActions - TypeScript (#3928) * Chore: Evaluate TextInput - TypeScript (#3908) * update `TextInput` component update: ActivityIndicator * remove: `any` * update: `TextInput` on `UIKit` * Fix returnKeyType Co-authored-by: Diego Mello <diegolmello@gmail.com> * Chore: Evaluate ActionSheet - TypeScript (#3927) * Chore: Clean LoginServices - TypeScript (#3935) * Chore: Evaluate Passcode - TypeScript (#3931) * Chore: Migrate containers: Passcode to Typescript * minor tweak * minor tweak * [FIX] "Sent an attachment" text on media preview for threads (#3947) * update: `Content` component * update: `Message` snapshots * [FIX] Audio thumb's size (#3945) * update: Audio component * Chore: Update codecov version (#3954) * Chore: Evaluate HeaderButton - TypeScript (#3925) * update: `HeaderButton` components * update: types * fix types * fix lint and update snapshot Co-authored-by: GleidsonDaniel <gleidson10daniel@hotmail.com> * [FIX] Navigate to team from directory (#3953) * [FIX] Param privacy when editing room info (#3962) * [FIX] Drawer failing to open/close on ProfileView (#3963) Co-authored-by: Gleidson Daniel Silva <gleidson10daniel@hotmail.com> Co-authored-by: Gerzon Z <gerzonzcanario@gmail.com> Co-authored-by: Gerzon Z <gerzonc@icloud.com> Co-authored-by: Reinaldo Neto <47038980+reinaldonetof@users.noreply.github.com> Co-authored-by: AlexAlexandre <alexalexandrejr@gmail.com> Co-authored-by: Danish Ahmed Mirza <77742477+try-catch-stack@users.noreply.github.com> Co-authored-by: Reinaldo Neto <reinaldonetof@hotmail.com> Co-authored-by: Snyk bot <snyk-bot@snyk.io> Co-authored-by: lingohub[bot] <69908207+lingohub[bot]@users.noreply.github.com> Co-authored-by: Robot LingoHub <robot@lingohub.com> Co-authored-by: Christian King <cking@vonix.io>
This commit is contained in:
parent
bd4d48528d
commit
ed3868eeb1
|
@ -64,5 +64,6 @@ artifacts
|
|||
.vscode/
|
||||
e2e/docker/rc_test_env/docker-compose.yml
|
||||
e2e/docker/data/db
|
||||
e2e/e2e_account.js
|
||||
|
||||
*.p8
|
|
@ -20,6 +20,26 @@ jest.mock('react-native-file-viewer', () => ({
|
|||
jest.mock('../app/lib/database', () => jest.fn(() => null));
|
||||
global.Date.now = jest.fn(() => new Date('2019-10-10').getTime());
|
||||
|
||||
jest.mock('react-native-mmkv-storage', () => {
|
||||
return {
|
||||
Loader: jest.fn().mockImplementation(() => {
|
||||
return {
|
||||
setProcessingMode: jest.fn().mockImplementation(() => {
|
||||
return {
|
||||
withEncryption: jest.fn().mockImplementation(() => {
|
||||
return {
|
||||
initialize: jest.fn()
|
||||
};
|
||||
})
|
||||
};
|
||||
})
|
||||
};
|
||||
}),
|
||||
create: jest.fn(),
|
||||
MODES: { MULTI_PROCESS: '' }
|
||||
};
|
||||
});
|
||||
|
||||
const converter = new Stories2SnapsConverter();
|
||||
|
||||
initStoryshots({
|
||||
|
|
|
@ -144,7 +144,7 @@ android {
|
|||
minSdkVersion rootProject.ext.minSdkVersion
|
||||
targetSdkVersion rootProject.ext.targetSdkVersion
|
||||
versionCode VERSIONCODE as Integer
|
||||
versionName "4.25.0"
|
||||
versionName "4.26.0"
|
||||
vectorDrawables.useSupportLibrary = true
|
||||
if (!isFoss) {
|
||||
manifestPlaceholders = [BugsnagAPIKey: BugsnagAPIKey as String]
|
||||
|
|
|
@ -12,7 +12,6 @@ import com.facebook.soloader.SoLoader;
|
|||
import com.reactnativecommunity.viewpager.RNCViewPagerPackage;
|
||||
import com.facebook.react.bridge.JSIModulePackage;
|
||||
import com.swmansion.reanimated.ReanimatedJSIModulePackage;
|
||||
|
||||
import org.unimodules.adapters.react.ModuleRegistryAdapter;
|
||||
import org.unimodules.adapters.react.ReactModuleRegistryProvider;
|
||||
|
||||
|
@ -54,7 +53,7 @@ public class MainApplication extends Application implements ReactApplication {
|
|||
|
||||
@Override
|
||||
protected JSIModulePackage getJSIModulePackage() {
|
||||
return new ReanimatedJSIModulePackage(); // <- add
|
||||
return new ReanimatedJSIModulePackage();
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
@ -53,17 +53,9 @@ public class Ejson {
|
|||
String alias = Utils.toHex("com.MMKV.default");
|
||||
|
||||
// Retrieve container password
|
||||
secureKeystore.getSecureKey(alias, new RNCallback() {
|
||||
@Override
|
||||
public void invoke(Object... args) {
|
||||
String error = (String) args[0];
|
||||
if (error == null) {
|
||||
String password = (String) args[1];
|
||||
String password = secureKeystore.getSecureKey(alias);
|
||||
mmkv = MMKV.mmkvWithID("default", MMKV.SINGLE_PROCESS_MODE, password);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public String getAvatarUri() {
|
||||
if (type == null) {
|
||||
|
|
|
@ -11,7 +11,7 @@ function createRequestTypes(base = {}, types = defaultTypes): Record<string, str
|
|||
// Login events
|
||||
export const LOGIN = createRequestTypes('LOGIN', [...defaultTypes, 'SET_SERVICES', 'SET_PREFERENCE', 'SET_LOCAL_AUTHENTICATED']);
|
||||
export const SHARE = createRequestTypes('SHARE', ['SELECT_SERVER', 'SET_USER', 'SET_SETTINGS', 'SET_SERVER_INFO']);
|
||||
export const USER = createRequestTypes('USER', ['SET']);
|
||||
export const USER = createRequestTypes('USER', ['SET', 'CLEAR']);
|
||||
export const ROOMS = createRequestTypes('ROOMS', [
|
||||
...defaultTypes,
|
||||
'REFRESH',
|
||||
|
|
|
@ -93,6 +93,12 @@ export function setUser(user: Partial<IUser>): ISetUser {
|
|||
};
|
||||
}
|
||||
|
||||
export function clearUser(): Action {
|
||||
return {
|
||||
type: types.USER.CLEAR
|
||||
};
|
||||
}
|
||||
|
||||
export function setLoginServices(data: Record<string, any>): ISetServices {
|
||||
return {
|
||||
type: types.LOGIN.SET_SERVICES,
|
||||
|
|
|
@ -4,7 +4,7 @@ import { ERoomType } from '../definitions/ERoomType';
|
|||
import { ROOM } from './actionsTypes';
|
||||
|
||||
// TYPE RETURN RELATED
|
||||
type ISelected = Record<string, string>;
|
||||
type ISelected = string[];
|
||||
|
||||
export interface ITransferData {
|
||||
roomId: string;
|
||||
|
|
|
@ -1,26 +1,26 @@
|
|||
import { Action } from 'redux';
|
||||
|
||||
import { ISettings, TSettings } from '../reducers/settings';
|
||||
import { TSettingsState, TSupportedSettings, TSettingsValues } from '../reducers/settings';
|
||||
import { SETTINGS } from './actionsTypes';
|
||||
|
||||
interface IAddSettings extends Action {
|
||||
payload: ISettings;
|
||||
payload: TSettingsState;
|
||||
}
|
||||
|
||||
interface IUpdateSettings extends Action {
|
||||
payload: { id: string; value: TSettings };
|
||||
payload: { id: TSupportedSettings; value: TSettingsValues };
|
||||
}
|
||||
|
||||
export type IActionSettings = IAddSettings & IUpdateSettings;
|
||||
|
||||
export function addSettings(settings: ISettings): IAddSettings {
|
||||
export function addSettings(settings: TSettingsState): IAddSettings {
|
||||
return {
|
||||
type: SETTINGS.ADD,
|
||||
payload: settings
|
||||
};
|
||||
}
|
||||
|
||||
export function updateSettings(id: string, value: TSettings): IUpdateSettings {
|
||||
export function updateSettings(id: TSupportedSettings, value: TSettingsValues): IUpdateSettings {
|
||||
return {
|
||||
type: SETTINGS.UPDATE,
|
||||
payload: { id, value }
|
||||
|
|
|
@ -143,8 +143,8 @@ export const deleteKeyCommands = (): void => KeyCommands.deleteKeyCommands(keyCo
|
|||
|
||||
export const KEY_COMMAND = 'KEY_COMMAND';
|
||||
|
||||
interface IKeyCommandEvent extends NativeSyntheticEvent<typeof KeyCommand> {
|
||||
input: string;
|
||||
export interface IKeyCommandEvent extends NativeSyntheticEvent<typeof KeyCommand> {
|
||||
input: number & string;
|
||||
modifierFlags: string | number;
|
||||
}
|
||||
|
||||
|
|
|
@ -37,6 +37,7 @@ export const themes: any = {
|
|||
infoText: '#6d6d72',
|
||||
tintColor: '#1d74f5',
|
||||
tintActive: '#549df9',
|
||||
tintDisabled: '#88B4F5',
|
||||
auxiliaryTintColor: '#6C727A',
|
||||
actionTintColor: '#1d74f5',
|
||||
separatorColor: '#cbcbcc',
|
||||
|
@ -66,6 +67,8 @@ export const themes: any = {
|
|||
previewTintColor: '#ffffff',
|
||||
backdropOpacity: 0.3,
|
||||
attachmentLoadingOpacity: 0.7,
|
||||
collapsibleQuoteBorder: '#CBCED1',
|
||||
collapsibleChevron: '#6C727A',
|
||||
...mentions
|
||||
},
|
||||
dark: {
|
||||
|
@ -85,6 +88,7 @@ export const themes: any = {
|
|||
infoText: '#6D6D72',
|
||||
tintColor: '#1d74f5',
|
||||
tintActive: '#549df9',
|
||||
tintDisabled: '#88B4F5',
|
||||
auxiliaryTintColor: '#f9f9f9',
|
||||
actionTintColor: '#1d74f5',
|
||||
separatorColor: '#2b2b2d',
|
||||
|
@ -114,6 +118,8 @@ export const themes: any = {
|
|||
previewTintColor: '#ffffff',
|
||||
backdropOpacity: 0.9,
|
||||
attachmentLoadingOpacity: 0.3,
|
||||
collapsibleQuoteBorder: '#CBCED1',
|
||||
collapsibleChevron: '#6C727A',
|
||||
...mentions
|
||||
},
|
||||
black: {
|
||||
|
@ -133,6 +139,7 @@ export const themes: any = {
|
|||
infoText: '#6d6d72',
|
||||
tintColor: '#1e9bfe',
|
||||
tintActive: '#76b7fc',
|
||||
tintDisabled: '#88B4F5', // TODO: Evaluate this with design team
|
||||
auxiliaryTintColor: '#f9f9f9',
|
||||
actionTintColor: '#1e9bfe',
|
||||
separatorColor: '#272728',
|
||||
|
@ -162,6 +169,8 @@ export const themes: any = {
|
|||
previewTintColor: '#ffffff',
|
||||
backdropOpacity: 0.9,
|
||||
attachmentLoadingOpacity: 0.3,
|
||||
collapsibleQuoteBorder: '#CBCED1',
|
||||
collapsibleChevron: '#6C727A',
|
||||
...mentions
|
||||
}
|
||||
};
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
export const MESSAGE_TYPE_LOAD_MORE = 'load_more';
|
||||
export const MESSAGE_TYPE_LOAD_PREVIOUS_CHUNK = 'load_previous_chunk';
|
||||
export const MESSAGE_TYPE_LOAD_NEXT_CHUNK = 'load_next_chunk';
|
||||
export enum MessageTypeLoad {
|
||||
MORE = 'load_more',
|
||||
PREVIOUS_CHUNK = 'load_previous_chunk',
|
||||
NEXT_CHUNK = 'load_next_chunk'
|
||||
}
|
||||
|
||||
export const MESSAGE_TYPE_ANY_LOAD = [MESSAGE_TYPE_LOAD_MORE, MESSAGE_TYPE_LOAD_PREVIOUS_CHUNK, MESSAGE_TYPE_LOAD_NEXT_CHUNK];
|
||||
export const MESSAGE_TYPE_ANY_LOAD = [MessageTypeLoad.MORE, MessageTypeLoad.PREVIOUS_CHUNK, MessageTypeLoad.NEXT_CHUNK];
|
||||
|
|
|
@ -206,4 +206,4 @@ export default {
|
|||
Canned_Responses_Enable: {
|
||||
type: 'valueAsBoolean'
|
||||
}
|
||||
};
|
||||
} as const;
|
||||
|
|
|
@ -1,30 +1,29 @@
|
|||
import { useBackHandler } from '@react-native-community/hooks';
|
||||
import * as Haptics from 'expo-haptics';
|
||||
import React, { forwardRef, isValidElement, useEffect, useImperativeHandle, useRef, useState } from 'react';
|
||||
import { Keyboard, Text } from 'react-native';
|
||||
import { HandlerStateChangeEventPayload, State, TapGestureHandler } from 'react-native-gesture-handler';
|
||||
import Animated, { Easing, Extrapolate, interpolateNode, Value } from 'react-native-reanimated';
|
||||
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, interpolateNode } 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 { useDimensions, useOrientation } from '../../dimensions';
|
||||
import I18n from '../../i18n';
|
||||
import { useTheme } from '../../theme';
|
||||
import { isIOS, isTablet } from '../../utils/deviceInfo';
|
||||
import * as List from '../List';
|
||||
import I18n from '../../i18n';
|
||||
import { IDimensionsContextProps, useDimensions, useOrientation } from '../../dimensions';
|
||||
import { Button } from './Button';
|
||||
import { Handle } from './Handle';
|
||||
import { IActionSheetItem, Item } from './Item';
|
||||
import { TActionSheetOptions, TActionSheetOptionsItem } from './Provider';
|
||||
import styles, { ITEM_HEIGHT } from './styles';
|
||||
|
||||
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 getItemLayout = (data: TActionSheetOptionsItem[] | null | undefined, index: number) => ({
|
||||
length: ITEM_HEIGHT,
|
||||
offset: ITEM_HEIGHT * index,
|
||||
index
|
||||
});
|
||||
|
||||
const HANDLE_HEIGHT = isIOS ? 40 : 56;
|
||||
const MAX_SNAP_HEIGHT = 16;
|
||||
|
@ -39,16 +38,17 @@ const ANIMATION_CONFIG = {
|
|||
};
|
||||
|
||||
const ActionSheet = React.memo(
|
||||
forwardRef(({ children, theme }: { children: JSX.Element; theme: string }, ref) => {
|
||||
const bottomSheetRef: any = useRef();
|
||||
const [data, setData] = useState<IActionSheetData>({} as IActionSheetData);
|
||||
forwardRef(({ children }: { children: React.ReactElement }, ref) => {
|
||||
const { theme } = useTheme();
|
||||
const bottomSheetRef = useRef<ScrollBottomSheet<TActionSheetOptionsItem>>(null);
|
||||
const [data, setData] = useState<TActionSheetOptions>({} as TActionSheetOptions);
|
||||
const [isVisible, setVisible] = useState(false);
|
||||
const { height }: Partial<IDimensionsContextProps> = useDimensions();
|
||||
const { height } = useDimensions();
|
||||
const { isLandscape } = useOrientation();
|
||||
const insets = useSafeAreaInsets();
|
||||
|
||||
const maxSnap = Math.max(
|
||||
height! -
|
||||
height -
|
||||
// Items height
|
||||
ITEM_HEIGHT * (data?.options?.length || 0) -
|
||||
// Handle height
|
||||
|
@ -69,7 +69,7 @@ const ActionSheet = React.memo(
|
|||
* 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 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;
|
||||
|
||||
|
@ -79,12 +79,12 @@ const ActionSheet = React.memo(
|
|||
bottomSheetRef.current?.snapTo(closedSnapIndex);
|
||||
};
|
||||
|
||||
const show = (options: any) => {
|
||||
const show = (options: TActionSheetOptions) => {
|
||||
setData(options);
|
||||
toggleVisible();
|
||||
};
|
||||
|
||||
const onBackdropPressed = ({ nativeEvent }: any) => {
|
||||
const onBackdropPressed = ({ nativeEvent }: { nativeEvent: HandlerStateChangeEventPayload }) => {
|
||||
if (nativeEvent.oldState === State.ACTIVE) {
|
||||
hide();
|
||||
}
|
||||
|
@ -117,7 +117,7 @@ const ActionSheet = React.memo(
|
|||
|
||||
const renderHandle = () => (
|
||||
<>
|
||||
<Handle theme={theme} />
|
||||
<Handle />
|
||||
{isValidElement(data?.customHeader) ? data.customHeader : null}
|
||||
</>
|
||||
);
|
||||
|
@ -127,21 +127,23 @@ const ActionSheet = React.memo(
|
|||
<Button
|
||||
onPress={hide}
|
||||
style={[styles.button, { backgroundColor: themes[theme].auxiliaryBackground }]}
|
||||
// TODO: Remove when migrate Touch
|
||||
theme={theme}
|
||||
accessibilityLabel={I18n.t('Cancel')}>
|
||||
<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 renderItem = ({ item }: { item: IActionSheetItem['item'] }) => <Item item={item} hide={hide} />;
|
||||
|
||||
const animatedPosition = React.useRef(new Value(0));
|
||||
// TODO: Similar to https://github.com/wcandillon/react-native-redash/issues/307#issuecomment-827442320
|
||||
const opacity = interpolateNode(animatedPosition.current, {
|
||||
inputRange: [0, 1],
|
||||
outputRange: [0, themes[theme].backdropOpacity],
|
||||
extrapolate: Extrapolate.CLAMP
|
||||
}) as any;
|
||||
}) as any; // The function's return differs from the expected type of opacity, however this problem is something related to lib, maybe when updating the types will be fixed.
|
||||
|
||||
const bottomSheet = isLandscape || isTablet ? styles.bottomSheet : {};
|
||||
|
||||
return (
|
||||
<>
|
||||
|
@ -160,7 +162,7 @@ const ActionSheet = React.memo(
|
|||
]}
|
||||
/>
|
||||
</TapGestureHandler>
|
||||
<ScrollBottomSheet
|
||||
<ScrollBottomSheet<TActionSheetOptionsItem>
|
||||
testID='action-sheet'
|
||||
ref={bottomSheetRef}
|
||||
componentType='FlatList'
|
||||
|
@ -169,18 +171,11 @@ const ActionSheet = React.memo(
|
|||
renderHandle={renderHandle}
|
||||
onSettle={index => index === closedSnapIndex && toggleVisible()}
|
||||
animatedPosition={animatedPosition.current}
|
||||
containerStyle={
|
||||
[
|
||||
styles.container,
|
||||
{ backgroundColor: themes[theme].focusedBackground },
|
||||
(isLandscape || isTablet) && styles.bottomSheet
|
||||
] as any
|
||||
}
|
||||
containerStyle={{ ...styles.container, ...bottomSheet, backgroundColor: themes[theme].focusedBackground }}
|
||||
animationConfig={ANIMATION_CONFIG}
|
||||
// FlatList props
|
||||
data={data?.options}
|
||||
data={data.options}
|
||||
renderItem={renderItem}
|
||||
keyExtractor={(item: any) => item.title}
|
||||
keyExtractor={item => item.title}
|
||||
style={{ backgroundColor: themes[theme].focusedBackground }}
|
||||
contentContainerStyle={styles.content}
|
||||
ItemSeparatorComponent={List.Separator}
|
||||
|
|
|
@ -3,9 +3,13 @@ import { View } from 'react-native';
|
|||
|
||||
import styles from './styles';
|
||||
import { themes } from '../../constants/colors';
|
||||
import { useTheme } from '../../theme';
|
||||
|
||||
export const Handle = React.memo(({ theme }: { theme: string }) => (
|
||||
export const Handle = React.memo(() => {
|
||||
const { theme } = useTheme();
|
||||
return (
|
||||
<View style={[styles.handle, { backgroundColor: themes[theme].focusedBackground }]} testID='action-sheet-handle'>
|
||||
<View style={[styles.handleIndicator, { backgroundColor: themes[theme].auxiliaryText }]} />
|
||||
</View>
|
||||
));
|
||||
);
|
||||
});
|
||||
|
|
|
@ -3,23 +3,24 @@ import { Text, View } from 'react-native';
|
|||
|
||||
import { themes } from '../../constants/colors';
|
||||
import { CustomIcon } from '../../lib/Icons';
|
||||
import { useTheme } from '../../theme';
|
||||
import { Button } from './Button';
|
||||
import styles from './styles';
|
||||
|
||||
interface IActionSheetItem {
|
||||
export interface IActionSheetItem {
|
||||
item: {
|
||||
title: string;
|
||||
icon: string;
|
||||
danger: boolean;
|
||||
testID: string;
|
||||
onPress(): void;
|
||||
right: Function;
|
||||
danger?: boolean;
|
||||
testID?: string;
|
||||
onPress: () => void;
|
||||
right?: Function;
|
||||
};
|
||||
theme: string;
|
||||
hide(): void;
|
||||
}
|
||||
|
||||
export const Item = React.memo(({ item, hide, theme }: IActionSheetItem) => {
|
||||
export const Item = React.memo(({ item, hide }: IActionSheetItem) => {
|
||||
const { theme } = useTheme();
|
||||
const onPress = () => {
|
||||
hide();
|
||||
item?.onPress();
|
||||
|
|
|
@ -1,14 +1,21 @@
|
|||
import React, { ForwardedRef, forwardRef, useContext, useRef } from 'react';
|
||||
|
||||
import ActionSheet from './ActionSheet';
|
||||
import { useTheme } from '../../theme';
|
||||
|
||||
export type TActionSheetOptionsItem = { title: string; icon: string; onPress: () => void };
|
||||
|
||||
export type TActionSheetOptions = {
|
||||
options: TActionSheetOptionsItem[];
|
||||
headerHeight: number;
|
||||
customHeader: React.ReactElement | null;
|
||||
hasCancel?: boolean;
|
||||
};
|
||||
interface IActionSheetProvider {
|
||||
Provider: any;
|
||||
Consumer: any;
|
||||
showActionSheet: (item: TActionSheetOptions) => void;
|
||||
hideActionSheet: () => void;
|
||||
}
|
||||
|
||||
const context: IActionSheetProvider = React.createContext({
|
||||
const context = React.createContext<IActionSheetProvider>({
|
||||
showActionSheet: () => {},
|
||||
hideActionSheet: () => {}
|
||||
});
|
||||
|
@ -17,17 +24,16 @@ export const useActionSheet = () => useContext(context);
|
|||
|
||||
const { Provider, Consumer } = context;
|
||||
|
||||
export const withActionSheet = (Component: any): any =>
|
||||
forwardRef((props: any, ref: ForwardedRef<any>) => (
|
||||
<Consumer>{(contexts: any) => <Component {...props} {...contexts} ref={ref} />}</Consumer>
|
||||
export const withActionSheet = (Component: React.ComponentType<any>): typeof Component =>
|
||||
forwardRef((props: typeof React.Component, ref: ForwardedRef<IActionSheetProvider>) => (
|
||||
<Consumer>{(contexts: IActionSheetProvider) => <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();
|
||||
export const ActionSheetProvider = React.memo(({ children }: { children: React.ReactElement | React.ReactElement[] }) => {
|
||||
const ref: ForwardedRef<IActionSheetProvider> = useRef(null);
|
||||
|
||||
const getContext = () => ({
|
||||
showActionSheet: (options: any) => {
|
||||
showActionSheet: (options: TActionSheetOptions) => {
|
||||
ref.current?.showActionSheet(options);
|
||||
},
|
||||
hideActionSheet: () => {
|
||||
|
@ -37,7 +43,7 @@ export const ActionSheetProvider = React.memo(({ children }: { children: JSX.Ele
|
|||
|
||||
return (
|
||||
<Provider value={getContext()}>
|
||||
<ActionSheet ref={ref} theme={theme}>
|
||||
<ActionSheet ref={ref}>
|
||||
<>{children}</>
|
||||
</ActionSheet>
|
||||
</Provider>
|
||||
|
|
|
@ -1,14 +1,11 @@
|
|||
import React from 'react';
|
||||
import { ActivityIndicator, ActivityIndicatorProps, StyleSheet } from 'react-native';
|
||||
|
||||
import { useTheme } from '../theme';
|
||||
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({
|
||||
|
@ -27,8 +24,11 @@ const styles = StyleSheet.create({
|
|||
}
|
||||
});
|
||||
|
||||
const RCActivityIndicator = ({ theme = 'light', absolute, ...props }: IActivityIndicator) => (
|
||||
const RCActivityIndicator = ({ absolute, ...props }: IActivityIndicator): React.ReactElement => {
|
||||
const { theme } = useTheme();
|
||||
return (
|
||||
<ActivityIndicator style={[styles.indicator, absolute && styles.absolute]} color={themes[theme].auxiliaryText} {...props} />
|
||||
);
|
||||
);
|
||||
};
|
||||
|
||||
export default RCActivityIndicator;
|
||||
|
|
|
@ -37,6 +37,21 @@ class AvatarContainer extends React.Component<IAvatar, any> {
|
|||
}
|
||||
}
|
||||
|
||||
shouldComponentUpdate(nextProps: IAvatar, nextState: { avatarETag: string }) {
|
||||
const { avatarETag } = this.state;
|
||||
const { text, type } = this.props;
|
||||
if (nextState.avatarETag !== avatarETag) {
|
||||
return true;
|
||||
}
|
||||
if (nextProps.text !== text) {
|
||||
return true;
|
||||
}
|
||||
if (nextProps.type !== type) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
if (this.subscription?.unsubscribe) {
|
||||
this.subscription.unsubscribe();
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
import React from 'react';
|
||||
|
||||
import { TGetCustomEmoji } from '../../definitions/IEmoji';
|
||||
|
||||
export interface IAvatar {
|
||||
|
@ -9,7 +11,7 @@ export interface IAvatar {
|
|||
size?: number;
|
||||
borderRadius?: number;
|
||||
type?: string;
|
||||
children?: JSX.Element;
|
||||
children?: React.ReactElement | null;
|
||||
user?: {
|
||||
id?: string;
|
||||
token?: string;
|
||||
|
|
|
@ -1,13 +1,12 @@
|
|||
import React from 'react';
|
||||
import { ActivityIndicator, ImageBackground, StyleSheet, Text, View } from 'react-native';
|
||||
|
||||
import { withTheme } from '../../theme';
|
||||
import { useTheme } from '../../theme';
|
||||
import sharedStyles from '../../views/Styles';
|
||||
import { themes } from '../../constants/colors';
|
||||
|
||||
interface IBackgroundContainer {
|
||||
text?: string;
|
||||
theme?: string;
|
||||
loading?: boolean;
|
||||
}
|
||||
|
||||
|
@ -32,12 +31,15 @@ const styles = StyleSheet.create({
|
|||
}
|
||||
});
|
||||
|
||||
const BackgroundContainer = ({ theme, text, loading }: IBackgroundContainer) => (
|
||||
const BackgroundContainer = ({ text, loading }: IBackgroundContainer): React.ReactElement => {
|
||||
const { theme } = useTheme();
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<ImageBackground source={{ uri: `message_empty_${theme}` }} style={styles.image} />
|
||||
{text && !loading ? <Text style={[styles.text, { color: themes[theme!].auxiliaryTintColor }]}>{text}</Text> : null}
|
||||
{loading ? <ActivityIndicator style={styles.text} color={themes[theme!].auxiliaryTintColor} /> : null}
|
||||
{text && !loading ? <Text style={[styles.text, { color: themes[theme].auxiliaryTintColor }]}>{text}</Text> : null}
|
||||
{loading ? <ActivityIndicator style={styles.text} color={themes[theme].auxiliaryTintColor} /> : null}
|
||||
</View>
|
||||
);
|
||||
);
|
||||
};
|
||||
|
||||
export default withTheme(BackgroundContainer);
|
||||
export default BackgroundContainer;
|
||||
|
|
|
@ -3,11 +3,8 @@ import { StyleSheet } from 'react-native';
|
|||
|
||||
import { CustomIcon } from '../lib/Icons';
|
||||
import { themes } from '../constants/colors';
|
||||
import { useTheme } from '../theme';
|
||||
|
||||
interface ICheck {
|
||||
style?: object;
|
||||
theme: string;
|
||||
}
|
||||
const styles = StyleSheet.create({
|
||||
icon: {
|
||||
width: 22,
|
||||
|
@ -16,8 +13,9 @@ const styles = StyleSheet.create({
|
|||
}
|
||||
});
|
||||
|
||||
const Check = React.memo(({ theme, style }: ICheck) => (
|
||||
<CustomIcon style={[styles.icon, style]} color={themes[theme].tintColor} size={22} name='check' />
|
||||
));
|
||||
const Check = React.memo(() => {
|
||||
const { theme } = useTheme();
|
||||
return <CustomIcon style={styles.icon} color={themes[theme].tintColor} size={22} name='check' />;
|
||||
});
|
||||
|
||||
export default Check;
|
||||
|
|
|
@ -5,12 +5,11 @@ import { StyleSheet, View } from 'react-native';
|
|||
import { themes } from '../../constants/colors';
|
||||
import { themedHeader } from '../../utils/navigation';
|
||||
import { isIOS, isTablet } from '../../utils/deviceInfo';
|
||||
import { withTheme } from '../../theme';
|
||||
import { useTheme } from '../../theme';
|
||||
|
||||
// Get from https://github.com/react-navigation/react-navigation/blob/master/packages/stack/src/views/Header/HeaderSegment.tsx#L69
|
||||
export const headerHeight = isIOS ? 44 : 56;
|
||||
|
||||
export const getHeaderHeight = (isLandscape: boolean) => {
|
||||
export const getHeaderHeight = (isLandscape: boolean): number => {
|
||||
if (isIOS) {
|
||||
if (isLandscape && !isTablet) {
|
||||
return 32;
|
||||
|
@ -28,7 +27,13 @@ interface IHeaderTitlePosition {
|
|||
numIconsRight: number;
|
||||
}
|
||||
|
||||
export const getHeaderTitlePosition = ({ insets, numIconsRight }: IHeaderTitlePosition) => ({
|
||||
export const getHeaderTitlePosition = ({
|
||||
insets,
|
||||
numIconsRight
|
||||
}: IHeaderTitlePosition): {
|
||||
left: number;
|
||||
right: number;
|
||||
} => ({
|
||||
left: insets.left + 60,
|
||||
right: insets.right + Math.max(45 * numIconsRight, 15)
|
||||
});
|
||||
|
@ -43,13 +48,14 @@ const styles = StyleSheet.create({
|
|||
});
|
||||
|
||||
interface IHeader {
|
||||
theme: string;
|
||||
headerLeft(): void;
|
||||
headerTitle(): void;
|
||||
headerRight(): void;
|
||||
headerLeft: () => React.ReactElement | null;
|
||||
headerTitle: () => React.ReactElement;
|
||||
headerRight: () => React.ReactElement | null;
|
||||
}
|
||||
|
||||
const Header = ({ theme, headerLeft, headerTitle, headerRight }: IHeader) => (
|
||||
const Header = ({ headerLeft, headerTitle, headerRight }: IHeader): React.ReactElement => {
|
||||
const { theme } = useTheme();
|
||||
return (
|
||||
<SafeAreaView style={{ backgroundColor: themes[theme].headerBackground }} edges={['top', 'left', 'right']}>
|
||||
<View style={[styles.container, { ...themedHeader(theme).headerStyle }]}>
|
||||
{headerLeft ? headerLeft() : null}
|
||||
|
@ -57,6 +63,7 @@ const Header = ({ theme, headerLeft, headerTitle, headerRight }: IHeader) => (
|
|||
{headerRight ? headerRight() : null}
|
||||
</View>
|
||||
</SafeAreaView>
|
||||
);
|
||||
);
|
||||
};
|
||||
|
||||
export default withTheme(Header);
|
||||
export default Header;
|
||||
|
|
|
@ -6,20 +6,22 @@ import Container from './HeaderButtonContainer';
|
|||
import Item from './HeaderButtonItem';
|
||||
|
||||
interface IHeaderButtonCommon {
|
||||
navigation: any;
|
||||
onPress?(): void;
|
||||
navigation?: any; // TODO: Evaluate proper type
|
||||
onPress?: () => void;
|
||||
testID?: string;
|
||||
}
|
||||
|
||||
// Left
|
||||
export const Drawer = React.memo(({ navigation, testID, ...props }: Partial<IHeaderButtonCommon>) => (
|
||||
export const Drawer = React.memo(
|
||||
({ navigation, testID, onPress = () => navigation?.toggleDrawer(), ...props }: IHeaderButtonCommon) => (
|
||||
<Container left>
|
||||
<Item iconName='hamburguer' onPress={() => navigation.toggleDrawer()} testID={testID} {...props} />
|
||||
<Item iconName='hamburguer' onPress={onPress} testID={testID} {...props} />
|
||||
</Container>
|
||||
));
|
||||
)
|
||||
);
|
||||
|
||||
export const CloseModal = React.memo(
|
||||
({ navigation, testID, onPress = () => navigation.pop(), ...props }: IHeaderButtonCommon) => (
|
||||
({ navigation, testID, onPress = () => navigation?.pop(), ...props }: IHeaderButtonCommon) => (
|
||||
<Container left>
|
||||
<Item iconName='close' onPress={onPress} testID={testID} {...props} />
|
||||
</Container>
|
||||
|
@ -29,9 +31,9 @@ export const CloseModal = React.memo(
|
|||
export const CancelModal = React.memo(({ onPress, testID }: Partial<IHeaderButtonCommon>) => (
|
||||
<Container left>
|
||||
{isIOS ? (
|
||||
<Item title={I18n.t('Cancel')} onPress={onPress!} testID={testID} />
|
||||
<Item title={I18n.t('Cancel')} onPress={onPress} testID={testID} />
|
||||
) : (
|
||||
<Item iconName='close' onPress={onPress!} testID={testID} />
|
||||
<Item iconName='close' onPress={onPress} testID={testID} />
|
||||
)}
|
||||
</Container>
|
||||
));
|
||||
|
@ -39,22 +41,24 @@ export const CancelModal = React.memo(({ onPress, testID }: Partial<IHeaderButto
|
|||
// Right
|
||||
export const More = React.memo(({ onPress, testID }: Partial<IHeaderButtonCommon>) => (
|
||||
<Container>
|
||||
<Item iconName='kebab' onPress={onPress!} testID={testID} />
|
||||
<Item iconName='kebab' onPress={onPress} testID={testID} />
|
||||
</Container>
|
||||
));
|
||||
|
||||
export const Download = React.memo(({ onPress, testID, ...props }: Partial<IHeaderButtonCommon>) => (
|
||||
export const Download = React.memo(({ onPress, testID, ...props }: IHeaderButtonCommon) => (
|
||||
<Container>
|
||||
<Item iconName='download' onPress={onPress!} testID={testID} {...props} />
|
||||
<Item iconName='download' onPress={onPress} testID={testID} {...props} />
|
||||
</Container>
|
||||
));
|
||||
|
||||
export const Preferences = React.memo(({ onPress, testID, ...props }: Partial<IHeaderButtonCommon>) => (
|
||||
export const Preferences = React.memo(({ onPress, testID, ...props }: IHeaderButtonCommon) => (
|
||||
<Container>
|
||||
<Item iconName='settings' onPress={onPress!} testID={testID} {...props} />
|
||||
<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} />
|
||||
));
|
||||
export const Legal = React.memo(
|
||||
({ navigation, testID, onPress = () => navigation?.navigate('LegalView') }: IHeaderButtonCommon) => (
|
||||
<More onPress={onPress} testID={testID} />
|
||||
)
|
||||
);
|
||||
|
|
|
@ -2,7 +2,7 @@ import React from 'react';
|
|||
import { StyleSheet, View } from 'react-native';
|
||||
|
||||
interface IHeaderButtonContainer {
|
||||
children: React.ReactNode;
|
||||
children?: React.ReactElement | (React.ReactElement | null)[] | null;
|
||||
left?: boolean;
|
||||
}
|
||||
|
||||
|
@ -20,7 +20,7 @@ const styles = StyleSheet.create({
|
|||
}
|
||||
});
|
||||
|
||||
const Container = ({ children, left = false }: IHeaderButtonContainer) => (
|
||||
const Container = ({ children, left = false }: IHeaderButtonContainer): React.ReactElement => (
|
||||
<View style={[styles.container, left ? styles.left : styles.right]}>{children}</View>
|
||||
);
|
||||
|
||||
|
|
|
@ -3,16 +3,15 @@ import { Platform, StyleSheet, Text } from 'react-native';
|
|||
import Touchable from 'react-native-platform-touchable';
|
||||
|
||||
import { CustomIcon } from '../../lib/Icons';
|
||||
import { withTheme } from '../../theme';
|
||||
import { useTheme } from '../../theme';
|
||||
import { themes } from '../../constants/colors';
|
||||
import sharedStyles from '../../views/Styles';
|
||||
|
||||
interface IHeaderButtonItem {
|
||||
title?: string;
|
||||
iconName?: string;
|
||||
onPress: <T>(arg: T) => void;
|
||||
onPress?: <T>(arg: T) => void;
|
||||
testID?: string;
|
||||
theme?: string;
|
||||
badge?(): void;
|
||||
}
|
||||
|
||||
|
@ -40,19 +39,22 @@ const styles = StyleSheet.create({
|
|||
}
|
||||
});
|
||||
|
||||
const Item = ({ title, iconName, onPress, testID, theme, badge }: IHeaderButtonItem) => (
|
||||
const Item = ({ title, iconName, onPress, testID, badge }: IHeaderButtonItem): React.ReactElement => {
|
||||
const { theme } = useTheme();
|
||||
return (
|
||||
<Touchable onPress={onPress} testID={testID} hitSlop={BUTTON_HIT_SLOP} style={styles.container}>
|
||||
<>
|
||||
{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}
|
||||
</>
|
||||
</Touchable>
|
||||
);
|
||||
);
|
||||
};
|
||||
|
||||
Item.displayName = 'HeaderButton.Item';
|
||||
|
||||
export default withTheme(Item);
|
||||
export default Item;
|
||||
|
|
|
@ -15,6 +15,6 @@ const styles = StyleSheet.create({
|
|||
}
|
||||
});
|
||||
|
||||
export const Badge = ({ ...props }) => <UnreadBadge {...props} style={styles.badgeContainer} small />;
|
||||
export const Badge = ({ ...props }): React.ReactElement => <UnreadBadge {...props} style={styles.badgeContainer} small />;
|
||||
|
||||
export default Badge;
|
||||
|
|
|
@ -14,9 +14,18 @@ import { ROW_HEIGHT } from '../../presentation/RoomItem';
|
|||
import { goRoom } from '../../utils/goRoom';
|
||||
import Navigation from '../../lib/Navigation';
|
||||
import { useOrientation } from '../../dimensions';
|
||||
import { IApplicationState, ISubscription, SubscriptionType } from '../../definitions';
|
||||
|
||||
interface INotifierComponent {
|
||||
notification: object;
|
||||
export interface INotifierComponent {
|
||||
notification: {
|
||||
text: string;
|
||||
payload: {
|
||||
sender: { username: string };
|
||||
type: SubscriptionType;
|
||||
} & Pick<ISubscription, '_id' | 'name' | 'rid' | 'prid'>;
|
||||
title: string;
|
||||
avatar: string;
|
||||
};
|
||||
isMasterDetail: boolean;
|
||||
}
|
||||
|
||||
|
@ -67,15 +76,15 @@ const styles = StyleSheet.create({
|
|||
const hideNotification = () => Notifier.hideNotification();
|
||||
|
||||
const NotifierComponent = React.memo(({ notification, isMasterDetail }: INotifierComponent) => {
|
||||
const { theme }: any = useTheme();
|
||||
const { theme } = useTheme();
|
||||
const insets = useSafeAreaInsets();
|
||||
const { isLandscape } = useOrientation();
|
||||
|
||||
const { text, payload }: any = notification;
|
||||
const { text, payload } = notification;
|
||||
const { type, rid } = payload;
|
||||
const name = type === 'd' ? payload.sender.username : payload.name;
|
||||
// if sub is not on local database, title and avatar will be null, so we use payload from notification
|
||||
const { title = name, avatar = name }: any = notification;
|
||||
const { title = name, avatar = name } = notification;
|
||||
|
||||
const onPress = () => {
|
||||
const { prid, _id } = payload;
|
||||
|
@ -133,7 +142,7 @@ const NotifierComponent = React.memo(({ notification, isMasterDetail }: INotifie
|
|||
);
|
||||
});
|
||||
|
||||
const mapStateToProps = (state: any) => ({
|
||||
const mapStateToProps = (state: IApplicationState) => ({
|
||||
isMasterDetail: state.app.isMasterDetail
|
||||
});
|
||||
|
||||
|
|
|
@ -3,16 +3,18 @@ import { Easing, Notifier, NotifierRoot } from 'react-native-notifier';
|
|||
import { connect } from 'react-redux';
|
||||
import { dequal } from 'dequal';
|
||||
|
||||
import NotifierComponent from './NotifierComponent';
|
||||
import NotifierComponent, { INotifierComponent } from './NotifierComponent';
|
||||
import EventEmitter from '../../utils/events';
|
||||
import Navigation from '../../lib/Navigation';
|
||||
import { getActiveRoute } from '../../utils/navigation';
|
||||
import { IApplicationState } from '../../definitions';
|
||||
import { IRoom } from '../../reducers/room';
|
||||
|
||||
export const INAPP_NOTIFICATION_EMITTER = 'NotificationInApp';
|
||||
|
||||
const InAppNotification = memo(
|
||||
({ rooms, appState }: { rooms: any; appState: string }) => {
|
||||
const show = (notification: any) => {
|
||||
({ rooms, appState }: { rooms: IRoom['rooms']; appState: string }) => {
|
||||
const show = (notification: INotifierComponent['notification']) => {
|
||||
if (appState !== 'foreground') {
|
||||
return;
|
||||
}
|
||||
|
@ -46,7 +48,7 @@ const InAppNotification = memo(
|
|||
(prevProps, nextProps) => dequal(prevProps.rooms, nextProps.rooms)
|
||||
);
|
||||
|
||||
const mapStateToProps = (state: any) => ({
|
||||
const mapStateToProps = (state: IApplicationState) => ({
|
||||
rooms: state.room.rooms,
|
||||
appState: state.app.ready && state.app.foreground ? 'foreground' : 'background'
|
||||
});
|
||||
|
|
|
@ -4,7 +4,7 @@ import { StyleSheet, Text, View } from 'react-native';
|
|||
import sharedStyles from '../../views/Styles';
|
||||
import { themes } from '../../constants/colors';
|
||||
import I18n from '../../i18n';
|
||||
import { withTheme } from '../../theme';
|
||||
import { useTheme } from '../../theme';
|
||||
import { PADDING_HORIZONTAL } from './constants';
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
|
@ -20,18 +20,20 @@ const styles = StyleSheet.create({
|
|||
|
||||
interface IListHeader {
|
||||
title: string;
|
||||
theme?: string;
|
||||
translateTitle?: boolean;
|
||||
}
|
||||
|
||||
const ListHeader = React.memo(({ title, theme, translateTitle = true }: IListHeader) => (
|
||||
const ListHeader = React.memo(({ title, translateTitle = true }: IListHeader) => {
|
||||
const { theme } = useTheme();
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<Text style={[styles.title, { color: themes[theme!].infoText }]} numberOfLines={1}>
|
||||
<Text style={[styles.title, { color: themes[theme].infoText }]} numberOfLines={1}>
|
||||
{translateTitle ? I18n.t(title) : title}
|
||||
</Text>
|
||||
</View>
|
||||
));
|
||||
);
|
||||
});
|
||||
|
||||
ListHeader.displayName = 'List.Header';
|
||||
|
||||
export default withTheme(ListHeader);
|
||||
export default ListHeader;
|
||||
|
|
|
@ -3,7 +3,7 @@ import { StyleSheet, Text, View } from 'react-native';
|
|||
|
||||
import sharedStyles from '../../views/Styles';
|
||||
import { themes } from '../../constants/colors';
|
||||
import { withTheme } from '../../theme';
|
||||
import { useTheme } from '../../theme';
|
||||
import { PADDING_HORIZONTAL } from './constants';
|
||||
import I18n from '../../i18n';
|
||||
|
||||
|
@ -18,18 +18,20 @@ const styles = StyleSheet.create({
|
|||
}
|
||||
});
|
||||
|
||||
interface IListHeader {
|
||||
interface IListInfo {
|
||||
info: string;
|
||||
theme?: string;
|
||||
translateInfo?: boolean;
|
||||
}
|
||||
|
||||
const ListInfo = React.memo(({ info, theme, translateInfo = true }: IListHeader) => (
|
||||
const ListInfo = React.memo(({ info, translateInfo = true }: IListInfo) => {
|
||||
const { theme } = useTheme();
|
||||
return (
|
||||
<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>
|
||||
));
|
||||
);
|
||||
});
|
||||
|
||||
ListInfo.displayName = 'List.Info';
|
||||
|
||||
export default withTheme(ListInfo);
|
||||
export default ListInfo;
|
||||
|
|
|
@ -135,7 +135,7 @@ const Button = React.memo<IListItemButton>(({ onPress, backgroundColor, underlay
|
|||
</Touch>
|
||||
));
|
||||
|
||||
interface IListItem extends IListItemContent, IListButtonPress {
|
||||
interface IListItem extends IListItemContent, IListItemButton {
|
||||
backgroundColor?: string;
|
||||
}
|
||||
|
||||
|
|
|
@ -22,15 +22,20 @@ interface ILoadingProps {
|
|||
theme?: string;
|
||||
}
|
||||
|
||||
class Loading extends React.PureComponent<ILoadingProps, any> {
|
||||
interface ILoadingState {
|
||||
scale: Animated.Value;
|
||||
opacity: Animated.Value;
|
||||
}
|
||||
|
||||
class Loading extends React.PureComponent<ILoadingProps, ILoadingState> {
|
||||
state = {
|
||||
scale: new Animated.Value(1),
|
||||
opacity: new Animated.Value(0)
|
||||
};
|
||||
|
||||
private opacityAnimation: any;
|
||||
private opacityAnimation?: Animated.CompositeAnimation;
|
||||
|
||||
private scaleAnimation: any;
|
||||
private scaleAnimation?: Animated.CompositeAnimation;
|
||||
|
||||
componentDidMount() {
|
||||
const { opacity, scale } = this.state;
|
||||
|
@ -61,7 +66,7 @@ class Loading extends React.PureComponent<ILoadingProps, any> {
|
|||
}
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps: any) {
|
||||
componentDidUpdate(prevProps: ILoadingProps) {
|
||||
const { visible } = this.props;
|
||||
if (visible && visible !== prevProps.visible) {
|
||||
this.startAnimations();
|
||||
|
@ -107,8 +112,7 @@ class Loading extends React.PureComponent<ILoadingProps, any> {
|
|||
<Animated.View
|
||||
style={[
|
||||
{
|
||||
// @ts-ignore
|
||||
...StyleSheet.absoluteFill,
|
||||
...StyleSheet.absoluteFillObject,
|
||||
backgroundColor: themes[theme!].backdropColor,
|
||||
opacity: opacityAnimation
|
||||
}
|
||||
|
|
|
@ -3,6 +3,7 @@ import { Animated, Easing, Linking, StyleSheet, Text, View } from 'react-native'
|
|||
import { connect } from 'react-redux';
|
||||
import { Base64 } from 'js-base64';
|
||||
import * as AppleAuthentication from 'expo-apple-authentication';
|
||||
import { StackNavigationProp } from '@react-navigation/stack';
|
||||
|
||||
import { withTheme } from '../theme';
|
||||
import sharedStyles from '../views/Styles';
|
||||
|
@ -15,6 +16,9 @@ import random from '../utils/random';
|
|||
import { events, logEvent } from '../utils/log';
|
||||
import RocketChat from '../lib/rocketchat';
|
||||
import { CustomIcon } from '../lib/Icons';
|
||||
import { IServices } from '../selectors/login';
|
||||
import { OutsideParamList } from '../stacks/types';
|
||||
import { IApplicationState } from '../definitions';
|
||||
|
||||
const BUTTON_HEIGHT = 48;
|
||||
const SERVICE_HEIGHT = 58;
|
||||
|
@ -58,31 +62,40 @@ const styles = StyleSheet.create({
|
|||
});
|
||||
|
||||
interface IOpenOAuth {
|
||||
url?: string;
|
||||
url: string;
|
||||
ssoToken?: string;
|
||||
authType?: string;
|
||||
}
|
||||
|
||||
interface IService {
|
||||
interface IItemService {
|
||||
name: string;
|
||||
service: string;
|
||||
authType: string;
|
||||
buttonColor: string;
|
||||
buttonLabelColor: string;
|
||||
clientConfig: { provider: string };
|
||||
serverURL: string;
|
||||
authorizePath: string;
|
||||
clientId: string;
|
||||
scope: string;
|
||||
}
|
||||
|
||||
interface IOauthProvider {
|
||||
[key: string]: () => void;
|
||||
facebook: () => void;
|
||||
github: () => void;
|
||||
gitlab: () => void;
|
||||
google: () => void;
|
||||
linkedin: () => void;
|
||||
'meteor-developer': () => void;
|
||||
twitter: () => void;
|
||||
wordpress: () => void;
|
||||
}
|
||||
|
||||
interface ILoginServicesProps {
|
||||
navigation: any;
|
||||
navigation: StackNavigationProp<OutsideParamList>;
|
||||
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 };
|
||||
};
|
||||
services: IServices;
|
||||
Gitlab_URL: string;
|
||||
CAS_enabled: boolean;
|
||||
CAS_login_url: string;
|
||||
|
@ -90,12 +103,13 @@ interface ILoginServicesProps {
|
|||
theme: string;
|
||||
}
|
||||
|
||||
class LoginServices extends React.PureComponent<ILoginServicesProps, any> {
|
||||
private _animation: any;
|
||||
interface ILoginServicesState {
|
||||
collapsed: boolean;
|
||||
servicesHeight: Animated.Value;
|
||||
}
|
||||
|
||||
static defaultProps = {
|
||||
separator: true
|
||||
};
|
||||
class LoginServices extends React.PureComponent<ILoginServicesProps, ILoginServicesState> {
|
||||
private _animation?: Animated.CompositeAnimation | void;
|
||||
|
||||
state = {
|
||||
collapsed: true,
|
||||
|
@ -194,7 +208,7 @@ class LoginServices extends React.PureComponent<ILoginServicesProps, any> {
|
|||
this.openOAuth({ url: `${endpoint}${params}` });
|
||||
};
|
||||
|
||||
onPressCustomOAuth = (loginService: any) => {
|
||||
onPressCustomOAuth = (loginService: IItemService) => {
|
||||
logEvent(events.ENTER_WITH_CUSTOM_OAUTH);
|
||||
const { server } = this.props;
|
||||
const { serverURL, authorizePath, clientId, scope, service } = loginService;
|
||||
|
@ -207,7 +221,7 @@ class LoginServices extends React.PureComponent<ILoginServicesProps, any> {
|
|||
this.openOAuth({ url });
|
||||
};
|
||||
|
||||
onPressSaml = (loginService: any) => {
|
||||
onPressSaml = (loginService: IItemService) => {
|
||||
logEvent(events.ENTER_WITH_SAML);
|
||||
const { server } = this.props;
|
||||
const { clientConfig } = loginService;
|
||||
|
@ -234,7 +248,6 @@ class LoginServices extends React.PureComponent<ILoginServicesProps, any> {
|
|||
AppleAuthentication.AppleAuthenticationScope.EMAIL
|
||||
]
|
||||
});
|
||||
|
||||
await RocketChat.loginOAuthOrSso({ fullName, email, identityToken });
|
||||
} catch {
|
||||
logEvent(events.ENTER_WITH_APPLE_F);
|
||||
|
@ -243,7 +256,12 @@ class LoginServices extends React.PureComponent<ILoginServicesProps, any> {
|
|||
|
||||
getOAuthState = (loginStyle = LOGIN_STYPE_POPUP) => {
|
||||
const credentialToken = random(43);
|
||||
let obj: any = { loginStyle, credentialToken, isCordova: true };
|
||||
let obj: {
|
||||
loginStyle: string;
|
||||
credentialToken: string;
|
||||
isCordova: boolean;
|
||||
redirectUrl?: string;
|
||||
} = { loginStyle, credentialToken, isCordova: true };
|
||||
if (loginStyle === LOGIN_STYPE_REDIRECT) {
|
||||
obj = {
|
||||
...obj,
|
||||
|
@ -263,12 +281,11 @@ class LoginServices extends React.PureComponent<ILoginServicesProps, any> {
|
|||
if (this._animation) {
|
||||
this._animation.stop();
|
||||
}
|
||||
// @ts-ignore
|
||||
this._animation = Animated.timing(servicesHeight, {
|
||||
toValue: height,
|
||||
duration: 300,
|
||||
// @ts-ignore
|
||||
easing: Easing.easeOutCubic
|
||||
easing: Easing.inOut(Easing.quad),
|
||||
useNativeDriver: true
|
||||
}).start();
|
||||
};
|
||||
|
||||
|
@ -281,11 +298,11 @@ class LoginServices extends React.PureComponent<ILoginServicesProps, any> {
|
|||
} else {
|
||||
this.transitionServicesTo(SERVICES_COLLAPSED_HEIGHT);
|
||||
}
|
||||
this.setState((prevState: any) => ({ collapsed: !prevState.collapsed }));
|
||||
this.setState((prevState: ILoginServicesState) => ({ collapsed: !prevState.collapsed }));
|
||||
};
|
||||
|
||||
getSocialOauthProvider = (name: string) => {
|
||||
const oauthProviders: any = {
|
||||
const oauthProviders: IOauthProvider = {
|
||||
facebook: this.onPressFacebook,
|
||||
github: this.onPressGithub,
|
||||
gitlab: this.onPressGitlab,
|
||||
|
@ -324,7 +341,7 @@ class LoginServices extends React.PureComponent<ILoginServicesProps, any> {
|
|||
return null;
|
||||
};
|
||||
|
||||
renderItem = (service: IService) => {
|
||||
renderItem = (service: IItemService) => {
|
||||
const { CAS_enabled, theme } = this.props;
|
||||
let { name } = service;
|
||||
name = name === 'meteor-developer' ? 'meteor' : name;
|
||||
|
@ -401,26 +418,28 @@ class LoginServices extends React.PureComponent<ILoginServicesProps, any> {
|
|||
if (length > 3 && separator) {
|
||||
return (
|
||||
<>
|
||||
<Animated.View style={style}>{Object.values(services).map((service: any) => this.renderItem(service))}</Animated.View>
|
||||
<Animated.View style={style}>
|
||||
{Object.values(services).map((service: IItemService) => this.renderItem(service))}
|
||||
</Animated.View>
|
||||
{this.renderServicesSeparator()}
|
||||
</>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<>
|
||||
{Object.values(services).map((service: any) => this.renderItem(service))}
|
||||
{Object.values(services).map((service: IItemService) => this.renderItem(service))}
|
||||
{this.renderServicesSeparator()}
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const mapStateToProps = (state: any) => ({
|
||||
const mapStateToProps = (state: IApplicationState) => ({
|
||||
server: state.server.server,
|
||||
Gitlab_URL: state.settings.API_Gitlab_URL,
|
||||
CAS_enabled: state.settings.CAS_enabled,
|
||||
CAS_login_url: state.settings.CAS_login_url,
|
||||
services: state.login.services
|
||||
Gitlab_URL: state.settings.API_Gitlab_URL as string,
|
||||
CAS_enabled: state.settings.CAS_enabled as boolean,
|
||||
CAS_login_url: state.settings.CAS_login_url as string,
|
||||
services: state.login.services as IServices
|
||||
});
|
||||
|
||||
export default connect(mapStateToProps)(withTheme(LoginServices)) as any;
|
||||
export default connect(mapStateToProps)(withTheme(LoginServices));
|
||||
|
|
|
@ -15,19 +15,12 @@ import { showConfirmationAlert } from '../../utils/info';
|
|||
import { useActionSheet } from '../ActionSheet';
|
||||
import Header, { HEADER_HEIGHT } from './Header';
|
||||
import events from '../../utils/log/events';
|
||||
import { TMessageModel } from '../../definitions/IMessage';
|
||||
import { ILoggedUser, TAnyMessageModel, TSubscriptionModel } from '../../definitions';
|
||||
|
||||
interface IMessageActions {
|
||||
room: {
|
||||
rid: string;
|
||||
autoTranslateLanguage: any;
|
||||
autoTranslate: any;
|
||||
reactWhenReadOnly: any;
|
||||
};
|
||||
tmid: string;
|
||||
user: {
|
||||
id: string | number;
|
||||
};
|
||||
export interface IMessageActions {
|
||||
room: TSubscriptionModel;
|
||||
tmid?: string;
|
||||
user: Pick<ILoggedUser, 'id'>;
|
||||
editInit: Function;
|
||||
reactionInit: Function;
|
||||
onReactionPress: Function;
|
||||
|
@ -270,8 +263,11 @@ const MessageActions = React.memo(
|
|||
}
|
||||
};
|
||||
|
||||
const handleToggleTranslation = async (message: TMessageModel) => {
|
||||
const handleToggleTranslation = async (message: TAnyMessageModel) => {
|
||||
try {
|
||||
if (!room.autoTranslateLanguage) {
|
||||
return;
|
||||
}
|
||||
const db = database.active;
|
||||
await db.write(async () => {
|
||||
await message.update(m => {
|
||||
|
@ -321,7 +317,7 @@ const MessageActions = React.memo(
|
|||
});
|
||||
};
|
||||
|
||||
const getOptions = (message: TMessageModel) => {
|
||||
const getOptions = (message: TAnyMessageModel) => {
|
||||
let options: any = [];
|
||||
|
||||
// Reply
|
||||
|
@ -447,7 +443,7 @@ const MessageActions = React.memo(
|
|||
return options;
|
||||
};
|
||||
|
||||
const showMessageActions = async (message: TMessageModel) => {
|
||||
const showMessageActions = async (message: TAnyMessageModel) => {
|
||||
logEvent(events.ROOM_SHOW_MSG_ACTIONS);
|
||||
await getPermissions();
|
||||
showActionSheet({
|
||||
|
|
|
@ -34,7 +34,7 @@ const Item = ({ item, theme }: IMessageBoxCommandsPreviewItem) => {
|
|||
resizeMode={FastImage.resizeMode.cover}
|
||||
onLoadStart={() => setLoading(true)}
|
||||
onLoad={() => setLoading(false)}>
|
||||
{loading ? <ActivityIndicator theme={theme} /> : null}
|
||||
{loading ? <ActivityIndicator /> : null}
|
||||
</FastImage>
|
||||
) : (
|
||||
<CustomIcon name='attach' size={36} color={themes[theme!].actionTintColor} />
|
||||
|
|
|
@ -6,9 +6,10 @@ import Item from './Item';
|
|||
import styles from '../styles';
|
||||
import { themes } from '../../../constants/colors';
|
||||
import { withTheme } from '../../../theme';
|
||||
import { IPreviewItem } from '../../../definitions';
|
||||
|
||||
interface IMessageBoxCommandsPreview {
|
||||
commandPreview: [];
|
||||
commandPreview: IPreviewItem[];
|
||||
showCommandPreview: boolean;
|
||||
theme?: string;
|
||||
}
|
||||
|
|
|
@ -0,0 +1,56 @@
|
|||
import getMentionRegexp from './getMentionRegexp';
|
||||
|
||||
const regexp = getMentionRegexp();
|
||||
|
||||
describe('getMentionRegexpUser', function () {
|
||||
test('removing query text on user suggestion autocomplete (latin)', () => {
|
||||
const message = 'Hey @test123';
|
||||
expect(message.replace(regexp, '')).toBe('Hey @');
|
||||
});
|
||||
|
||||
test('removing query text on user suggestion autocomplete (arabic)', () => {
|
||||
const message = 'Hey @اختبار123';
|
||||
expect(message.replace(regexp, '')).toBe('Hey @');
|
||||
});
|
||||
|
||||
test('removing query text on user suggestion autocomplete (russian)', () => {
|
||||
const message = 'Hey @тест123';
|
||||
expect(message.replace(regexp, '')).toBe('Hey @');
|
||||
});
|
||||
|
||||
test('removing query text on user suggestion autocomplete (chinese trad)', () => {
|
||||
const message = 'Hey @測試123';
|
||||
expect(message.replace(regexp, '')).toBe('Hey @');
|
||||
});
|
||||
|
||||
test('removing query text on user suggestion autocomplete (japanese)', () => {
|
||||
const message = 'Hey @テスト123';
|
||||
expect(message.replace(regexp, '')).toBe('Hey @');
|
||||
});
|
||||
|
||||
test('removing query text on user suggestion autocomplete (special characters in query)', () => {
|
||||
const message = "Hey @'=test123";
|
||||
expect(message.replace(regexp, '')).toBe('Hey @');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getMentionRegexpEmoji', function () {
|
||||
test('removing query text on emoji suggestion autocomplete ', () => {
|
||||
const message = 'Hey :smiley';
|
||||
expect(message.replace(regexp, '')).toBe('Hey :');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getMentionRegexpCommand', function () {
|
||||
test('removing query text on emoji suggestion autocomplete ', () => {
|
||||
const message = '/archive';
|
||||
expect(message.replace(regexp, '')).toBe('/');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getMentionRegexpRoom', function () {
|
||||
test('removing query text on emoji suggestion autocomplete ', () => {
|
||||
const message = 'Check #general';
|
||||
expect(message.replace(regexp, '')).toBe('Check #');
|
||||
});
|
||||
});
|
|
@ -0,0 +1,4 @@
|
|||
// Match query string from the message to replace it with the suggestion
|
||||
const getMentionRegexp = (): any => /[^@:#/!]*$/;
|
||||
|
||||
export default getMentionRegexp;
|
|
@ -9,7 +9,7 @@ import { Q } from '@nozbe/watermelondb';
|
|||
import { TouchableWithoutFeedback } from 'react-native-gesture-handler';
|
||||
|
||||
import { generateTriggerId } from '../../lib/methods/actions';
|
||||
import TextInput from '../../presentation/TextInput';
|
||||
import TextInput, { IThemedTextInput } from '../../presentation/TextInput';
|
||||
import { userTyping as userTypingAction } from '../../actions/room';
|
||||
import RocketChat from '../../lib/rocketchat';
|
||||
import styles from './styles';
|
||||
|
@ -31,6 +31,7 @@ import { isAndroid, isTablet } from '../../utils/deviceInfo';
|
|||
import { canUploadFile } from '../../utils/media';
|
||||
import EventEmiter from '../../utils/events';
|
||||
import { KEY_COMMAND, handleCommandShowUpload, handleCommandSubmit, handleCommandTyping } from '../../commands';
|
||||
import getMentionRegexp from './getMentionRegexp';
|
||||
import Mentions from './Mentions';
|
||||
import MessageboxContext from './Context';
|
||||
import {
|
||||
|
@ -49,6 +50,7 @@ import { sanitizeLikeString } from '../../lib/database/utils';
|
|||
import { CustomIcon } from '../../lib/Icons';
|
||||
import { IMessage } from '../../definitions/IMessage';
|
||||
import { forceJpgExtension } from './forceJpgExtension';
|
||||
import { IPreviewItem, IUser } from '../../definitions';
|
||||
|
||||
if (isAndroid) {
|
||||
require('./EmojiKeyboard');
|
||||
|
@ -72,7 +74,7 @@ const videoPickerConfig = {
|
|||
mediaType: 'video'
|
||||
};
|
||||
|
||||
interface IMessageBoxProps {
|
||||
export interface IMessageBoxProps {
|
||||
rid: string;
|
||||
baseUrl: string;
|
||||
message: IMessage;
|
||||
|
@ -80,12 +82,7 @@ interface IMessageBoxProps {
|
|||
editing: boolean;
|
||||
threadsEnabled: boolean;
|
||||
isFocused(): boolean;
|
||||
user: {
|
||||
id: string;
|
||||
_id: string;
|
||||
username: string;
|
||||
token: string;
|
||||
};
|
||||
user: IUser;
|
||||
roomType: string;
|
||||
tmid: string;
|
||||
replyWithMention: boolean;
|
||||
|
@ -118,7 +115,7 @@ interface IMessageBoxState {
|
|||
showSend: any;
|
||||
recording: boolean;
|
||||
trackingType: string;
|
||||
commandPreview: [];
|
||||
commandPreview: IPreviewItem[];
|
||||
showCommandPreview: boolean;
|
||||
command: {
|
||||
appId?: any;
|
||||
|
@ -493,7 +490,7 @@ class MessageBox extends Component<IMessageBoxProps, IMessageBoxState> {
|
|||
const msg = this.text;
|
||||
const { start, end } = this.selection;
|
||||
const cursor = Math.max(start, end);
|
||||
const regexp = /([a-z0-9._-]+)$/im;
|
||||
const regexp = getMentionRegexp();
|
||||
let result = msg.substr(0, cursor).replace(regexp, '');
|
||||
// Remove the ! after select the canned response
|
||||
if (trackingType === MENTIONS_TRACKING_TYPE_CANNED) {
|
||||
|
@ -609,7 +606,7 @@ class MessageBox extends Component<IMessageBoxProps, IMessageBoxState> {
|
|||
|
||||
getCannedResponses = debounce(async (text?: string) => {
|
||||
const res = await RocketChat.getListCannedResponse({ text });
|
||||
this.setState({ mentions: res?.cannedResponses || [], mentionLoading: false });
|
||||
this.setState({ mentions: res.success ? res.cannedResponses : [], mentionLoading: false });
|
||||
}, 500);
|
||||
|
||||
focus = () => {
|
||||
|
@ -642,12 +639,12 @@ class MessageBox extends Component<IMessageBoxProps, IMessageBoxState> {
|
|||
}, 1000);
|
||||
};
|
||||
|
||||
setCommandPreview = async (command: any, name: string, params: any) => {
|
||||
setCommandPreview = async (command: any, name: string, params: string) => {
|
||||
const { rid } = this.props;
|
||||
try {
|
||||
const { success, preview } = await RocketChat.getCommandPreview(name, rid, params);
|
||||
if (success) {
|
||||
return this.setState({ commandPreview: preview?.items, showCommandPreview: true, command });
|
||||
const response = await RocketChat.getCommandPreview(name, rid, params);
|
||||
if (response.success) {
|
||||
return this.setState({ commandPreview: response.preview?.items || [], showCommandPreview: true, command });
|
||||
}
|
||||
} catch (e) {
|
||||
log(e);
|
||||
|
@ -891,7 +888,7 @@ class MessageBox extends Component<IMessageBoxProps, IMessageBoxState> {
|
|||
const messageWithoutCommand = message.replace(/([^\s]+)/, '').trim();
|
||||
const [{ appId }] = slashCommand;
|
||||
const triggerId = generateTriggerId(appId);
|
||||
RocketChat.runSlashCommand(command, roomId, messageWithoutCommand, triggerId, tmid || messageTmid);
|
||||
await RocketChat.runSlashCommand(command, roomId, messageWithoutCommand, triggerId, tmid || messageTmid);
|
||||
replyCancel();
|
||||
} catch (e) {
|
||||
logEvent(events.COMMAND_RUN_F);
|
||||
|
@ -926,8 +923,8 @@ class MessageBox extends Component<IMessageBoxProps, IMessageBoxState> {
|
|||
let msg = `[ ](${permalink}) `;
|
||||
|
||||
// if original message wasn't sent by current user and neither from a direct room
|
||||
if (user.username !== replyingMessage.u.username && roomType !== 'd' && replyWithMention) {
|
||||
msg += `@${replyingMessage.u.username} `;
|
||||
if (user.username !== replyingMessage?.u?.username && roomType !== 'd' && replyWithMention) {
|
||||
msg += `@${replyingMessage?.u?.username} `;
|
||||
}
|
||||
|
||||
msg = `${msg} ${message}`;
|
||||
|
@ -1041,7 +1038,7 @@ class MessageBox extends Component<IMessageBoxProps, IMessageBoxState> {
|
|||
tmid
|
||||
} = this.props;
|
||||
|
||||
const isAndroidTablet =
|
||||
const isAndroidTablet: Partial<IThemedTextInput> =
|
||||
isTablet && isAndroid
|
||||
? {
|
||||
multiline: false,
|
||||
|
@ -1093,7 +1090,6 @@ class MessageBox extends Component<IMessageBoxProps, IMessageBoxState> {
|
|||
<TextInput
|
||||
ref={component => (this.component = component)}
|
||||
style={[styles.textBoxInput, { color: themes[theme].bodyText }]}
|
||||
// @ts-ignore
|
||||
returnKeyType='default'
|
||||
keyboardType='twitter'
|
||||
blurOnSubmit={false}
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import { forwardRef, useImperativeHandle } from 'react';
|
||||
import Model from '@nozbe/watermelondb/Model';
|
||||
|
||||
import RocketChat from '../lib/rocketchat';
|
||||
import database from '../lib/database';
|
||||
|
@ -6,18 +7,20 @@ import protectedFunction from '../lib/methods/helpers/protectedFunction';
|
|||
import { useActionSheet } from './ActionSheet';
|
||||
import I18n from '../i18n';
|
||||
import log from '../utils/log';
|
||||
import { TMessageModel } from '../definitions';
|
||||
|
||||
const MessageErrorActions = forwardRef(({ tmid }: any, ref): any => {
|
||||
const MessageErrorActions = forwardRef(({ tmid }: { tmid: string }, ref) => {
|
||||
// TODO - remove this any after merge ActionSheet evaluate
|
||||
const { showActionSheet }: any = useActionSheet();
|
||||
|
||||
const handleResend = protectedFunction(async (message: any) => {
|
||||
const handleResend = protectedFunction(async (message: TMessageModel) => {
|
||||
await RocketChat.resendMessage(message, tmid);
|
||||
});
|
||||
|
||||
const handleDelete = async (message: any) => {
|
||||
const handleDelete = async (message: TMessageModel) => {
|
||||
try {
|
||||
const db = database.active;
|
||||
const deleteBatch: any = [];
|
||||
const deleteBatch: Model[] = [];
|
||||
const msgCollection = db.get('messages');
|
||||
const threadCollection = db.get('threads');
|
||||
|
||||
|
@ -38,7 +41,7 @@ const MessageErrorActions = forwardRef(({ tmid }: any, ref): any => {
|
|||
const msg = await msgCollection.find(tmid);
|
||||
if (msg?.tcount && msg.tcount <= 1) {
|
||||
deleteBatch.push(
|
||||
msg.prepareUpdate((m: any) => {
|
||||
msg.prepareUpdate(m => {
|
||||
m.tcount = null;
|
||||
m.tlm = null;
|
||||
})
|
||||
|
@ -53,8 +56,10 @@ const MessageErrorActions = forwardRef(({ tmid }: any, ref): any => {
|
|||
}
|
||||
} else {
|
||||
deleteBatch.push(
|
||||
msg.prepareUpdate((m: any) => {
|
||||
msg.prepareUpdate(m => {
|
||||
if (m.tcount) {
|
||||
m.tcount -= 1;
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
|
@ -70,7 +75,7 @@ const MessageErrorActions = forwardRef(({ tmid }: any, ref): any => {
|
|||
}
|
||||
};
|
||||
|
||||
const showMessageErrorActions = (message: any) => {
|
||||
const showMessageErrorActions = (message: TMessageModel) => {
|
||||
showActionSheet({
|
||||
options: [
|
||||
{
|
||||
|
|
|
@ -5,16 +5,18 @@ import styles from './styles';
|
|||
import { themes } from '../../../constants/colors';
|
||||
import Touch from '../../../utils/touch';
|
||||
import { CustomIcon } from '../../../lib/Icons';
|
||||
import { useTheme } from '../../../theme';
|
||||
|
||||
interface IPasscodeButton {
|
||||
text?: string;
|
||||
icon?: string;
|
||||
theme: string;
|
||||
disabled?: boolean;
|
||||
onPress?: Function;
|
||||
}
|
||||
|
||||
const Button = React.memo(({ text, disabled, theme, onPress, icon }: IPasscodeButton) => {
|
||||
const Button = React.memo(({ text, disabled, onPress, icon }: IPasscodeButton) => {
|
||||
const { theme } = useTheme();
|
||||
|
||||
const press = () => onPress && onPress(text);
|
||||
|
||||
return (
|
||||
|
|
|
@ -4,17 +4,20 @@ import range from 'lodash/range';
|
|||
|
||||
import styles from './styles';
|
||||
import { themes } from '../../../constants/colors';
|
||||
import { useTheme } from '../../../theme';
|
||||
|
||||
const SIZE_EMPTY = 12;
|
||||
const SIZE_FULL = 16;
|
||||
|
||||
interface IPasscodeDots {
|
||||
passcode: string;
|
||||
theme: string;
|
||||
length: number;
|
||||
}
|
||||
|
||||
const Dots = React.memo(({ passcode, theme, length }: IPasscodeDots) => (
|
||||
const Dots = React.memo(({ passcode, length }: IPasscodeDots) => {
|
||||
const { theme } = useTheme();
|
||||
|
||||
return (
|
||||
<View style={styles.dotsContainer}>
|
||||
{range(length).map(val => {
|
||||
const lengthSup = passcode.length >= val + 1;
|
||||
|
@ -45,6 +48,7 @@ const Dots = React.memo(({ passcode, theme, length }: IPasscodeDots) => (
|
|||
);
|
||||
})}
|
||||
</View>
|
||||
));
|
||||
);
|
||||
});
|
||||
|
||||
export default Dots;
|
||||
|
|
|
@ -5,13 +5,18 @@ import { Row } from 'react-native-easy-grid';
|
|||
import styles from './styles';
|
||||
import { themes } from '../../../constants/colors';
|
||||
import { CustomIcon } from '../../../lib/Icons';
|
||||
import { useTheme } from '../../../theme';
|
||||
|
||||
const LockIcon = React.memo(({ theme }: { theme: string }) => (
|
||||
const LockIcon = React.memo(() => {
|
||||
const { theme } = useTheme();
|
||||
|
||||
return (
|
||||
<Row style={styles.row}>
|
||||
<View style={styles.iconView}>
|
||||
<CustomIcon name='auth' size={40} color={themes[theme].passcodeLockIcon} />
|
||||
</View>
|
||||
</Row>
|
||||
));
|
||||
);
|
||||
});
|
||||
|
||||
export default LockIcon;
|
||||
|
|
|
@ -6,36 +6,35 @@ import { resetAttempts } from '../../../utils/localAuthentication';
|
|||
import { TYPE } from '../constants';
|
||||
import { getDiff, getLockedUntil } from '../utils';
|
||||
import I18n from '../../../i18n';
|
||||
import { useTheme } from '../../../theme';
|
||||
import styles from './styles';
|
||||
import Title from './Title';
|
||||
import Subtitle from './Subtitle';
|
||||
import LockIcon from './LockIcon';
|
||||
|
||||
interface IPasscodeTimer {
|
||||
time: string;
|
||||
theme: string;
|
||||
time: Date | null;
|
||||
setStatus: Function;
|
||||
}
|
||||
|
||||
interface IPasscodeLocked {
|
||||
theme: string;
|
||||
setStatus: Function;
|
||||
}
|
||||
|
||||
const Timer = React.memo(({ time, theme, setStatus }: IPasscodeTimer) => {
|
||||
const Timer = React.memo(({ time, setStatus }: IPasscodeTimer) => {
|
||||
const calcTimeLeft = () => {
|
||||
const diff = getDiff(time);
|
||||
const diff = getDiff(time || 0);
|
||||
if (diff > 0) {
|
||||
return Math.floor((diff / 1000) % 60);
|
||||
}
|
||||
};
|
||||
|
||||
const [timeLeft, setTimeLeft] = useState<any>(calcTimeLeft());
|
||||
const [timeLeft, setTimeLeft] = useState(calcTimeLeft());
|
||||
|
||||
useEffect(() => {
|
||||
setTimeout(() => {
|
||||
setTimeLeft(calcTimeLeft());
|
||||
if (timeLeft <= 1) {
|
||||
if (timeLeft && timeLeft <= 1) {
|
||||
resetAttempts();
|
||||
setStatus(TYPE.ENTER);
|
||||
}
|
||||
|
@ -46,11 +45,12 @@ const Timer = React.memo(({ time, theme, setStatus }: IPasscodeTimer) => {
|
|||
return null;
|
||||
}
|
||||
|
||||
return <Subtitle text={I18n.t('Passcode_app_locked_subtitle', { timeLeft })} theme={theme} />;
|
||||
return <Subtitle text={I18n.t('Passcode_app_locked_subtitle', { timeLeft })} />;
|
||||
});
|
||||
|
||||
const Locked = React.memo(({ theme, setStatus }: IPasscodeLocked) => {
|
||||
const [lockedUntil, setLockedUntil] = useState<any>(null);
|
||||
const Locked = React.memo(({ setStatus }: IPasscodeLocked) => {
|
||||
const [lockedUntil, setLockedUntil] = useState<Date | null>(null);
|
||||
const { theme } = useTheme();
|
||||
|
||||
const readItemFromStorage = async () => {
|
||||
const l = await getLockedUntil();
|
||||
|
@ -63,9 +63,9 @@ const Locked = React.memo(({ theme, setStatus }: IPasscodeLocked) => {
|
|||
|
||||
return (
|
||||
<Grid style={[styles.grid, { backgroundColor: themes[theme].passcodeBackground }]}>
|
||||
<LockIcon theme={theme} />
|
||||
<Title text={I18n.t('Passcode_app_locked_title')} theme={theme} />
|
||||
<Timer theme={theme} time={lockedUntil} setStatus={setStatus} />
|
||||
<LockIcon />
|
||||
<Title text={I18n.t('Passcode_app_locked_title')} />
|
||||
<Timer time={lockedUntil} setStatus={setStatus} />
|
||||
</Grid>
|
||||
);
|
||||
});
|
||||
|
|
|
@ -4,18 +4,22 @@ import { Row } from 'react-native-easy-grid';
|
|||
|
||||
import styles from './styles';
|
||||
import { themes } from '../../../constants/colors';
|
||||
import { useTheme } from '../../../theme';
|
||||
|
||||
interface IPasscodeSubtitle {
|
||||
text: string;
|
||||
theme: string;
|
||||
}
|
||||
|
||||
const Subtitle = React.memo(({ text, theme }: IPasscodeSubtitle) => (
|
||||
const Subtitle = React.memo(({ text }: IPasscodeSubtitle) => {
|
||||
const { theme } = useTheme();
|
||||
|
||||
return (
|
||||
<Row style={styles.row}>
|
||||
<View style={styles.subtitleView}>
|
||||
<Text style={[styles.textSubtitle, { color: themes[theme].passcodeSecondary }]}>{text}</Text>
|
||||
</View>
|
||||
</Row>
|
||||
));
|
||||
);
|
||||
});
|
||||
|
||||
export default Subtitle;
|
||||
|
|
|
@ -4,18 +4,22 @@ import { Row } from 'react-native-easy-grid';
|
|||
|
||||
import styles from './styles';
|
||||
import { themes } from '../../../constants/colors';
|
||||
import { useTheme } from '../../../theme';
|
||||
|
||||
interface IPasscodeTitle {
|
||||
text: string;
|
||||
theme: string;
|
||||
}
|
||||
|
||||
const Title = React.memo(({ text, theme }: IPasscodeTitle) => (
|
||||
const Title = React.memo(({ text }: IPasscodeTitle) => {
|
||||
const { theme } = useTheme();
|
||||
|
||||
return (
|
||||
<Row style={styles.row}>
|
||||
<View style={styles.titleView}>
|
||||
<Text style={[styles.textTitle, { color: themes[theme].passcodePrimary }]}>{text}</Text>
|
||||
</View>
|
||||
</Row>
|
||||
));
|
||||
);
|
||||
});
|
||||
|
||||
export default Title;
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import React, { forwardRef, useImperativeHandle, useRef, useState } from 'react';
|
||||
import { Col, Grid, Row } from 'react-native-easy-grid';
|
||||
import range from 'lodash/range';
|
||||
import { View } from 'react-native';
|
||||
import * as Animatable from 'react-native-animatable';
|
||||
import * as Haptics from 'expo-haptics';
|
||||
|
||||
|
@ -10,12 +11,12 @@ import Dots from './Dots';
|
|||
import { TYPE } from '../constants';
|
||||
import { themes } from '../../../constants/colors';
|
||||
import { PASSCODE_LENGTH } from '../../../constants/localAuthentication';
|
||||
import { useTheme } from '../../../theme';
|
||||
import LockIcon from './LockIcon';
|
||||
import Title from './Title';
|
||||
import Subtitle from './Subtitle';
|
||||
|
||||
interface IPasscodeBase {
|
||||
theme: string;
|
||||
type: string;
|
||||
previousPasscode?: string;
|
||||
title: string;
|
||||
|
@ -26,25 +27,30 @@ interface IPasscodeBase {
|
|||
onBiometryPress?(): void;
|
||||
}
|
||||
|
||||
const Base = forwardRef(
|
||||
(
|
||||
{ theme, type, onEndProcess, previousPasscode, title, subtitle, onError, showBiometry, onBiometryPress }: IPasscodeBase,
|
||||
ref
|
||||
) => {
|
||||
const rootRef = useRef<any>();
|
||||
const dotsRef = useRef<any>();
|
||||
export interface IBase {
|
||||
clearPasscode: () => void;
|
||||
wrongPasscode: () => void;
|
||||
animate: (animation: Animatable.Animation, duration?: number) => void;
|
||||
}
|
||||
|
||||
const Base = forwardRef<IBase, IPasscodeBase>(
|
||||
({ type, onEndProcess, previousPasscode, title, subtitle, onError, showBiometry, onBiometryPress }, ref) => {
|
||||
const { theme } = useTheme();
|
||||
|
||||
const rootRef = useRef<Animatable.View & View>(null);
|
||||
const dotsRef = useRef<Animatable.View & View>(null);
|
||||
const [passcode, setPasscode] = useState('');
|
||||
|
||||
const clearPasscode = () => setPasscode('');
|
||||
|
||||
const wrongPasscode = () => {
|
||||
clearPasscode();
|
||||
dotsRef?.current?.shake(500);
|
||||
dotsRef?.current?.shake?.(500);
|
||||
Haptics.notificationAsync(Haptics.NotificationFeedbackType.Error);
|
||||
};
|
||||
|
||||
const animate = (animation: string, duration = 500) => {
|
||||
rootRef?.current?.[animation](duration);
|
||||
const animate = (animation: Animatable.Animation, duration = 500) => {
|
||||
rootRef?.current?.[animation]?.(duration);
|
||||
};
|
||||
|
||||
const onPressNumber = (text: string) =>
|
||||
|
@ -90,48 +96,48 @@ const Base = forwardRef(
|
|||
return (
|
||||
<Animatable.View ref={rootRef} style={styles.container}>
|
||||
<Grid style={[styles.grid, { backgroundColor: themes[theme].passcodeBackground }]}>
|
||||
<LockIcon theme={theme} />
|
||||
<Title text={title} theme={theme} />
|
||||
<Subtitle text={subtitle!} theme={theme} />
|
||||
<LockIcon />
|
||||
<Title text={title} />
|
||||
{subtitle ? <Subtitle text={subtitle} /> : null}
|
||||
<Row style={styles.row}>
|
||||
<Animatable.View ref={dotsRef}>
|
||||
<Dots passcode={passcode} theme={theme} length={PASSCODE_LENGTH} />
|
||||
<Dots passcode={passcode} length={PASSCODE_LENGTH} />
|
||||
</Animatable.View>
|
||||
</Row>
|
||||
<Row style={[styles.row, styles.buttonRow]}>
|
||||
{range(1, 4).map((i: any) => (
|
||||
{range(1, 4).map(i => (
|
||||
<Col key={i} style={styles.colButton}>
|
||||
<Button text={i} theme={theme} onPress={onPressNumber} />
|
||||
<Button text={i.toString()} onPress={onPressNumber} />
|
||||
</Col>
|
||||
))}
|
||||
</Row>
|
||||
<Row style={[styles.row, styles.buttonRow]}>
|
||||
{range(4, 7).map((i: any) => (
|
||||
{range(4, 7).map(i => (
|
||||
<Col key={i} style={styles.colButton}>
|
||||
<Button text={i} theme={theme} onPress={onPressNumber} />
|
||||
<Button text={i.toString()} onPress={onPressNumber} />
|
||||
</Col>
|
||||
))}
|
||||
</Row>
|
||||
<Row style={[styles.row, styles.buttonRow]}>
|
||||
{range(7, 10).map((i: any) => (
|
||||
{range(7, 10).map(i => (
|
||||
<Col key={i} style={styles.colButton}>
|
||||
<Button text={i} theme={theme} onPress={onPressNumber} />
|
||||
<Button text={i.toString()} onPress={onPressNumber} />
|
||||
</Col>
|
||||
))}
|
||||
</Row>
|
||||
<Row style={[styles.row, styles.buttonRow]}>
|
||||
{showBiometry ? (
|
||||
<Col style={styles.colButton}>
|
||||
<Button icon='fingerprint' theme={theme} onPress={onBiometryPress} />
|
||||
<Button icon='fingerprint' onPress={onBiometryPress} />
|
||||
</Col>
|
||||
) : (
|
||||
<Col style={styles.colButton} />
|
||||
)}
|
||||
<Col style={styles.colButton}>
|
||||
<Button text='0' theme={theme} onPress={onPressNumber} />
|
||||
<Button text='0' onPress={onPressNumber} />
|
||||
</Col>
|
||||
<Col style={styles.colButton}>
|
||||
<Button icon='backspace' theme={theme} onPress={onPressDelete} />
|
||||
<Button icon='backspace' onPress={onPressDelete} />
|
||||
</Col>
|
||||
</Row>
|
||||
</Grid>
|
||||
|
|
|
@ -2,24 +2,23 @@ import React, { useRef, useState } from 'react';
|
|||
import * as Haptics from 'expo-haptics';
|
||||
import { gestureHandlerRootHOC } from 'react-native-gesture-handler';
|
||||
|
||||
import Base from './Base';
|
||||
import Base, { IBase } from './Base';
|
||||
import { TYPE } from './constants';
|
||||
import I18n from '../../i18n';
|
||||
|
||||
interface IPasscodeChoose {
|
||||
theme: string;
|
||||
force?: boolean;
|
||||
finishProcess: Function;
|
||||
}
|
||||
|
||||
const PasscodeChoose = ({ theme, finishProcess, force = false }: IPasscodeChoose) => {
|
||||
const chooseRef = useRef<any>(null);
|
||||
const confirmRef = useRef<any>(null);
|
||||
const PasscodeChoose = ({ finishProcess, force = false }: IPasscodeChoose) => {
|
||||
const chooseRef = useRef<IBase>(null);
|
||||
const confirmRef = useRef<IBase>(null);
|
||||
const [subtitle, setSubtitle] = useState(null);
|
||||
const [status, setStatus] = useState(TYPE.CHOOSE);
|
||||
const [previousPasscode, setPreviouPasscode] = useState<any>(null);
|
||||
const [previousPasscode, setPreviouPasscode] = useState('');
|
||||
|
||||
const firstStep = (p: any) => {
|
||||
const firstStep = (p: string) => {
|
||||
setTimeout(() => {
|
||||
setStatus(TYPE.CONFIRM);
|
||||
setPreviouPasscode(p);
|
||||
|
@ -43,7 +42,6 @@ const PasscodeChoose = ({ theme, finishProcess, force = false }: IPasscodeChoose
|
|||
return (
|
||||
<Base
|
||||
ref={confirmRef}
|
||||
theme={theme}
|
||||
type={TYPE.CONFIRM}
|
||||
onEndProcess={changePasscode}
|
||||
previousPasscode={previousPasscode}
|
||||
|
@ -56,7 +54,6 @@ const PasscodeChoose = ({ theme, finishProcess, force = false }: IPasscodeChoose
|
|||
return (
|
||||
<Base
|
||||
ref={chooseRef}
|
||||
theme={theme}
|
||||
type={TYPE.CHOOSE}
|
||||
onEndProcess={firstStep}
|
||||
title={I18n.t('Passcode_choose_title')}
|
||||
|
|
|
@ -4,35 +4,29 @@ import { gestureHandlerRootHOC } from 'react-native-gesture-handler';
|
|||
import * as Haptics from 'expo-haptics';
|
||||
import { sha256 } from 'js-sha256';
|
||||
|
||||
import Base from './Base';
|
||||
import Base, { IBase } from './Base';
|
||||
import Locked from './Base/Locked';
|
||||
import { TYPE } from './constants';
|
||||
import { ATTEMPTS_KEY, LOCKED_OUT_TIMER_KEY, MAX_ATTEMPTS, PASSCODE_KEY } from '../../constants/localAuthentication';
|
||||
import { biometryAuth, resetAttempts } from '../../utils/localAuthentication';
|
||||
import { getDiff, getLockedUntil } from './utils';
|
||||
import UserPreferences from '../../lib/userPreferences';
|
||||
import { useUserPreferences } from '../../lib/userPreferences';
|
||||
import I18n from '../../i18n';
|
||||
|
||||
interface IPasscodePasscodeEnter {
|
||||
theme: string;
|
||||
hasBiometry: boolean;
|
||||
finishProcess: Function;
|
||||
}
|
||||
|
||||
const PasscodeEnter = ({ theme, hasBiometry, finishProcess }: IPasscodePasscodeEnter) => {
|
||||
const ref = useRef(null);
|
||||
let attempts: any = 0;
|
||||
const PasscodeEnter = ({ hasBiometry, finishProcess }: IPasscodePasscodeEnter) => {
|
||||
const ref = useRef<IBase>(null);
|
||||
let attempts = 0;
|
||||
let lockedUntil: any = false;
|
||||
const [passcode, setPasscode] = useState(null);
|
||||
const [status, setStatus] = useState(null);
|
||||
const { getItem: getAttempts, setItem: setAttempts } = useAsyncStorage(ATTEMPTS_KEY);
|
||||
const [passcode] = useUserPreferences(PASSCODE_KEY);
|
||||
const [status, setStatus] = useState<TYPE | null>(null);
|
||||
const { setItem: setAttempts } = useAsyncStorage(ATTEMPTS_KEY);
|
||||
const { setItem: setLockedUntil } = useAsyncStorage(LOCKED_OUT_TIMER_KEY);
|
||||
|
||||
const fetchPasscode = async () => {
|
||||
const p: any = await UserPreferences.getStringAsync(PASSCODE_KEY);
|
||||
setPasscode(p);
|
||||
};
|
||||
|
||||
const biometry = async () => {
|
||||
if (hasBiometry && status === TYPE.ENTER) {
|
||||
const result = await biometryAuth();
|
||||
|
@ -50,13 +44,11 @@ const PasscodeEnter = ({ theme, hasBiometry, finishProcess }: IPasscodePasscodeE
|
|||
await resetAttempts();
|
||||
setStatus(TYPE.ENTER);
|
||||
} else {
|
||||
attempts = await getAttempts();
|
||||
setStatus(TYPE.LOCKED);
|
||||
}
|
||||
} else {
|
||||
setStatus(TYPE.ENTER);
|
||||
}
|
||||
await fetchPasscode();
|
||||
biometry();
|
||||
};
|
||||
|
||||
|
@ -64,7 +56,7 @@ const PasscodeEnter = ({ theme, hasBiometry, finishProcess }: IPasscodePasscodeE
|
|||
readStorage();
|
||||
}, [status]);
|
||||
|
||||
const onEndProcess = (p: any) => {
|
||||
const onEndProcess = (p: string) => {
|
||||
setTimeout(() => {
|
||||
if (sha256(p) === passcode) {
|
||||
finishProcess();
|
||||
|
@ -75,8 +67,7 @@ const PasscodeEnter = ({ theme, hasBiometry, finishProcess }: IPasscodePasscodeE
|
|||
setLockedUntil(new Date().toISOString());
|
||||
Haptics.notificationAsync(Haptics.NotificationFeedbackType.Error);
|
||||
} else {
|
||||
// @ts-ignore
|
||||
ref.current.wrongPasscode();
|
||||
ref?.current?.wrongPasscode();
|
||||
setAttempts(attempts?.toString());
|
||||
Haptics.notificationAsync(Haptics.NotificationFeedbackType.Warning);
|
||||
}
|
||||
|
@ -85,13 +76,12 @@ const PasscodeEnter = ({ theme, hasBiometry, finishProcess }: IPasscodePasscodeE
|
|||
};
|
||||
|
||||
if (status === TYPE.LOCKED) {
|
||||
return <Locked theme={theme} setStatus={setStatus} />;
|
||||
return <Locked setStatus={setStatus} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<Base
|
||||
ref={ref}
|
||||
theme={theme}
|
||||
type={TYPE.ENTER}
|
||||
title={I18n.t('Passcode_enter_title')}
|
||||
showBiometry={hasBiometry}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
export const TYPE: any = {
|
||||
CHOOSE: 'choose',
|
||||
CONFIRM: 'confirm',
|
||||
ENTER: 'enter',
|
||||
LOCKED: 'locked'
|
||||
};
|
||||
export enum TYPE {
|
||||
CHOOSE = 'choose',
|
||||
CONFIRM = 'confirm',
|
||||
ENTER = 'enter',
|
||||
LOCKED = 'locked'
|
||||
}
|
||||
|
|
|
@ -4,11 +4,11 @@ import moment from 'moment';
|
|||
import { LOCKED_OUT_TIMER_KEY, TIME_TO_LOCK } from '../../constants/localAuthentication';
|
||||
|
||||
export const getLockedUntil = async () => {
|
||||
const t: any = await AsyncStorage.getItem(LOCKED_OUT_TIMER_KEY);
|
||||
const t = await AsyncStorage.getItem(LOCKED_OUT_TIMER_KEY);
|
||||
if (t) {
|
||||
return moment(t).add(TIME_TO_LOCK);
|
||||
return moment(t).add(TIME_TO_LOCK).toDate();
|
||||
}
|
||||
return null;
|
||||
};
|
||||
// @ts-ignore
|
||||
export const getDiff = t => new Date(t) - new Date();
|
||||
|
||||
export const getDiff = (t: string | number | Date) => new Date(t).getTime() - new Date().getTime();
|
||||
|
|
|
@ -10,6 +10,7 @@ import sharedStyles from '../views/Styles';
|
|||
import { themes } from '../constants/colors';
|
||||
import { withTheme } from '../theme';
|
||||
import { TGetCustomEmoji } from '../definitions/IEmoji';
|
||||
import { TMessageModel, ILoggedUser } from '../definitions';
|
||||
import SafeAreaView from './SafeAreaView';
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
|
@ -65,23 +66,25 @@ interface IItem {
|
|||
usernames: any;
|
||||
emoji: string;
|
||||
};
|
||||
user?: { username: any };
|
||||
user?: Pick<ILoggedUser, 'username'>;
|
||||
baseUrl?: string;
|
||||
getCustomEmoji?: TGetCustomEmoji;
|
||||
theme?: string;
|
||||
}
|
||||
|
||||
interface IModalContent {
|
||||
message?: {
|
||||
reactions: any;
|
||||
};
|
||||
message?: TMessageModel;
|
||||
onClose: Function;
|
||||
theme: string;
|
||||
}
|
||||
|
||||
interface IReactionsModal {
|
||||
message?: any;
|
||||
user?: Pick<ILoggedUser, 'username'>;
|
||||
isVisible: boolean;
|
||||
onClose(): void;
|
||||
baseUrl: string;
|
||||
getCustomEmoji?: TGetCustomEmoji;
|
||||
theme: string;
|
||||
}
|
||||
|
||||
|
|
|
@ -1,22 +1,14 @@
|
|||
import React from 'react';
|
||||
import {
|
||||
NativeSyntheticEvent,
|
||||
StyleSheet,
|
||||
TextInput as RNTextInput,
|
||||
Text,
|
||||
TextInputFocusEventData,
|
||||
TextInputProps,
|
||||
View
|
||||
} from 'react-native';
|
||||
import { StyleSheet, Text, TextInput as RNTextInput, TextInputProps, View } from 'react-native';
|
||||
import Touchable from 'react-native-platform-touchable';
|
||||
|
||||
import TextInput from '../presentation/TextInput';
|
||||
import { themes } from '../constants/colors';
|
||||
import I18n from '../i18n';
|
||||
import { CustomIcon } from '../lib/Icons';
|
||||
import sharedStyles from '../views/Styles';
|
||||
import { withTheme } from '../theme';
|
||||
import { themes } from '../constants/colors';
|
||||
import TextInput from '../presentation/TextInput';
|
||||
import { useTheme } from '../theme';
|
||||
import { isIOS } from '../utils/deviceInfo';
|
||||
import sharedStyles from '../views/Styles';
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
|
@ -52,41 +44,32 @@ const styles = StyleSheet.create({
|
|||
}
|
||||
});
|
||||
|
||||
interface ISearchBox {
|
||||
interface ISearchBox extends TextInputProps {
|
||||
value?: string;
|
||||
onChangeText: TextInputProps['onChangeText'];
|
||||
onSubmitEditing?: () => void;
|
||||
hasCancel?: boolean;
|
||||
onCancelPress?: Function;
|
||||
theme?: string;
|
||||
inputRef?: React.Ref<RNTextInput>;
|
||||
testID?: string;
|
||||
onFocus?: (e: NativeSyntheticEvent<TextInputFocusEventData>) => void;
|
||||
}
|
||||
|
||||
const CancelButton = (onCancelPress: Function, theme: string) => (
|
||||
const CancelButton = ({ onCancelPress }: { onCancelPress?: Function }) => {
|
||||
const { theme } = useTheme();
|
||||
return (
|
||||
<Touchable onPress={onCancelPress} style={styles.cancel}>
|
||||
<Text style={[styles.cancelText, { color: themes[theme].headerTintColor }]}>{I18n.t('Cancel')}</Text>
|
||||
</Touchable>
|
||||
);
|
||||
);
|
||||
};
|
||||
|
||||
const SearchBox = ({
|
||||
onChangeText,
|
||||
onSubmitEditing,
|
||||
testID,
|
||||
hasCancel,
|
||||
onCancelPress,
|
||||
inputRef,
|
||||
theme,
|
||||
...props
|
||||
}: ISearchBox) => (
|
||||
const SearchBox = ({ hasCancel, onCancelPress, inputRef, ...props }: ISearchBox): React.ReactElement => {
|
||||
const { theme } = useTheme();
|
||||
return (
|
||||
<View
|
||||
style={[
|
||||
styles.container,
|
||||
{ backgroundColor: isIOS ? themes[theme!].headerBackground : themes[theme!].headerSecondaryBackground }
|
||||
{ backgroundColor: isIOS ? themes[theme].headerBackground : themes[theme].headerSecondaryBackground }
|
||||
]}>
|
||||
<View style={[styles.searchBox, { backgroundColor: themes[theme!].searchboxBackground }]}>
|
||||
<CustomIcon name='search' size={14} color={themes[theme!].auxiliaryText} />
|
||||
<View style={[styles.searchBox, { backgroundColor: themes[theme].searchboxBackground }]}>
|
||||
<CustomIcon name='search' size={14} color={themes[theme].auxiliaryText} />
|
||||
<TextInput
|
||||
ref={inputRef}
|
||||
autoCapitalize='none'
|
||||
|
@ -96,16 +79,14 @@ const SearchBox = ({
|
|||
placeholder={I18n.t('Search')}
|
||||
returnKeyType='search'
|
||||
style={styles.input}
|
||||
testID={testID}
|
||||
underlineColorAndroid='transparent'
|
||||
onChangeText={onChangeText}
|
||||
onSubmitEditing={onSubmitEditing}
|
||||
theme={theme!}
|
||||
theme={theme}
|
||||
{...props}
|
||||
/>
|
||||
</View>
|
||||
{hasCancel ? CancelButton(onCancelPress!, theme!) : null}
|
||||
{hasCancel ? <CancelButton onCancelPress={onCancelPress} /> : null}
|
||||
</View>
|
||||
);
|
||||
);
|
||||
};
|
||||
|
||||
export default withTheme(SearchBox);
|
||||
export default SearchBox;
|
||||
|
|
|
@ -2,22 +2,27 @@ import React from 'react';
|
|||
import { StatusBar as StatusBarRN } from 'react-native';
|
||||
|
||||
import { themes } from '../constants/colors';
|
||||
import { withTheme } from '../theme';
|
||||
import { useTheme } from '../theme';
|
||||
|
||||
const supportedStyles = {
|
||||
'light-content': 'light-content',
|
||||
'dark-content': 'dark-content'
|
||||
};
|
||||
|
||||
interface IStatusBar {
|
||||
theme?: string;
|
||||
barStyle?: any;
|
||||
barStyle?: keyof typeof supportedStyles;
|
||||
backgroundColor?: string;
|
||||
}
|
||||
|
||||
const StatusBar = React.memo(({ theme, barStyle, backgroundColor }: IStatusBar) => {
|
||||
const StatusBar = React.memo(({ barStyle, backgroundColor }: IStatusBar) => {
|
||||
const { theme } = useTheme();
|
||||
if (!barStyle) {
|
||||
barStyle = 'light-content';
|
||||
if (theme === 'light') {
|
||||
barStyle = 'dark-content';
|
||||
}
|
||||
}
|
||||
return <StatusBarRN backgroundColor={backgroundColor ?? themes[theme!].headerBackground} barStyle={barStyle} animated />;
|
||||
return <StatusBarRN backgroundColor={backgroundColor ?? themes[theme].headerBackground} barStyle={barStyle} animated />;
|
||||
});
|
||||
|
||||
export default withTheme(StatusBar);
|
||||
export default StatusBar;
|
||||
|
|
|
@ -52,10 +52,7 @@ const styles = StyleSheet.create({
|
|||
|
||||
export interface IRCTextInputProps extends TextInputProps {
|
||||
label?: string;
|
||||
error?: {
|
||||
error: any;
|
||||
reason: any;
|
||||
};
|
||||
error?: any;
|
||||
loading?: boolean;
|
||||
containerStyle?: StyleProp<ViewStyle>;
|
||||
inputStyle?: StyleProp<TextStyle>;
|
||||
|
@ -68,7 +65,11 @@ export interface IRCTextInputProps extends TextInputProps {
|
|||
theme: string;
|
||||
}
|
||||
|
||||
export default class RCTextInput extends React.PureComponent<IRCTextInputProps, any> {
|
||||
interface IRCTextInputState {
|
||||
showPassword: boolean;
|
||||
}
|
||||
|
||||
export default class RCTextInput extends React.PureComponent<IRCTextInputProps, IRCTextInputState> {
|
||||
static defaultProps = {
|
||||
error: {},
|
||||
theme: 'light'
|
||||
|
@ -116,12 +117,11 @@ export default class RCTextInput extends React.PureComponent<IRCTextInputProps,
|
|||
|
||||
get loading() {
|
||||
const { theme } = this.props;
|
||||
// @ts-ignore
|
||||
return <ActivityIndicator style={[styles.iconContainer, styles.iconRight, { color: themes[theme].bodyText }]} />;
|
||||
return <ActivityIndicator style={[styles.iconContainer, styles.iconRight]} color={themes[theme].bodyText} />;
|
||||
}
|
||||
|
||||
tooglePassword = () => {
|
||||
this.setState((prevState: any) => ({ showPassword: !prevState.showPassword }));
|
||||
this.setState(prevState => ({ showPassword: !prevState.showPassword }));
|
||||
};
|
||||
|
||||
render() {
|
||||
|
|
|
@ -41,7 +41,7 @@ const styles = StyleSheet.create({
|
|||
});
|
||||
|
||||
interface IThreadDetails {
|
||||
item: Partial<TThreadModel>;
|
||||
item: Pick<TThreadModel, 'tcount' | 'replies' | 'id'>;
|
||||
user: {
|
||||
id: string;
|
||||
};
|
||||
|
@ -52,9 +52,9 @@ interface IThreadDetails {
|
|||
|
||||
const ThreadDetails = ({ item, user, badgeColor, toggleFollowThread, style }: IThreadDetails): JSX.Element => {
|
||||
const { theme } = useTheme();
|
||||
let { tcount } = item;
|
||||
if (tcount && tcount >= 1000) {
|
||||
tcount = '+999';
|
||||
let count: string | number | undefined | null = item.tcount;
|
||||
if (count && count >= 1000) {
|
||||
count = '+999';
|
||||
}
|
||||
|
||||
let replies: number | string = item?.replies?.length ?? 0;
|
||||
|
@ -62,21 +62,21 @@ const ThreadDetails = ({ item, user, badgeColor, toggleFollowThread, style }: IT
|
|||
replies = '+999';
|
||||
}
|
||||
|
||||
const isFollowing = item.replies?.find((u: any) => u === user?.id);
|
||||
const isFollowing = item.replies?.find((u: string) => u === user?.id);
|
||||
|
||||
return (
|
||||
<View style={[styles.container, style]}>
|
||||
<View style={styles.detailsContainer}>
|
||||
<View style={styles.detailContainer}>
|
||||
<CustomIcon name='threads' size={24} color={themes[theme!].auxiliaryText} />
|
||||
<Text style={[styles.detailText, { color: themes[theme!].auxiliaryText }]} numberOfLines={1}>
|
||||
{tcount}
|
||||
<CustomIcon name='threads' size={24} color={themes[theme].auxiliaryText} />
|
||||
<Text style={[styles.detailText, { color: themes[theme].auxiliaryText }]} numberOfLines={1}>
|
||||
{count}
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
<View style={styles.detailContainer}>
|
||||
<CustomIcon name='user' size={24} color={themes[theme!].auxiliaryText} />
|
||||
<Text style={[styles.detailText, { color: themes[theme!].auxiliaryText }]} numberOfLines={1}>
|
||||
<CustomIcon name='user' size={24} color={themes[theme].auxiliaryText} />
|
||||
<Text style={[styles.detailText, { color: themes[theme].auxiliaryText }]} numberOfLines={1}>
|
||||
{replies}
|
||||
</Text>
|
||||
</View>
|
||||
|
@ -87,7 +87,7 @@ const ThreadDetails = ({ item, user, badgeColor, toggleFollowThread, style }: IT
|
|||
<CustomIcon
|
||||
size={24}
|
||||
name={isFollowing ? 'notification' : 'notification-disabled'}
|
||||
color={themes[theme!].auxiliaryTintColor}
|
||||
color={themes[theme].auxiliaryTintColor}
|
||||
/>
|
||||
</Touchable>
|
||||
</View>
|
||||
|
|
|
@ -26,9 +26,9 @@ interface IToastProps {
|
|||
}
|
||||
|
||||
class Toast extends React.Component<IToastProps, any> {
|
||||
private listener: any;
|
||||
private listener?: Function;
|
||||
|
||||
private toast: any;
|
||||
private toast: EasyToast | null | undefined;
|
||||
|
||||
componentDidMount() {
|
||||
this.listener = EventEmitter.addEventListener(LISTENER, this.showToast);
|
||||
|
@ -43,12 +43,14 @@ class Toast extends React.Component<IToastProps, any> {
|
|||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
if (this.listener) {
|
||||
EventEmitter.removeListener(LISTENER, this.listener);
|
||||
}
|
||||
}
|
||||
|
||||
getToastRef = (toast: any) => (this.toast = toast);
|
||||
getToastRef = (toast: EasyToast | null) => (this.toast = toast);
|
||||
|
||||
showToast = ({ message }: any) => {
|
||||
showToast = ({ message }: { message: string }) => {
|
||||
if (this.toast && this.toast.show) {
|
||||
this.toast.show(message, 1000);
|
||||
}
|
||||
|
|
|
@ -9,21 +9,36 @@ import { connect } from 'react-redux';
|
|||
import TextInput from '../TextInput';
|
||||
import I18n from '../../i18n';
|
||||
import EventEmitter from '../../utils/events';
|
||||
import { withTheme } from '../../theme';
|
||||
import { useTheme } from '../../theme';
|
||||
import { themes } from '../../constants/colors';
|
||||
import Button from '../Button';
|
||||
import sharedStyles from '../../views/Styles';
|
||||
import RocketChat from '../../lib/rocketchat';
|
||||
import styles from './styles';
|
||||
import { IApplicationState } from '../../definitions';
|
||||
|
||||
export const TWO_FACTOR = 'TWO_FACTOR';
|
||||
|
||||
interface ITwoFactor {
|
||||
theme?: string;
|
||||
isMasterDetail: boolean;
|
||||
interface IMethodsProp {
|
||||
text: string;
|
||||
keyboardType: 'numeric' | 'default';
|
||||
title?: string;
|
||||
secureTextEntry?: boolean;
|
||||
}
|
||||
interface IMethods {
|
||||
totp: IMethodsProp;
|
||||
email: IMethodsProp;
|
||||
password: IMethodsProp;
|
||||
}
|
||||
|
||||
const methods: any = {
|
||||
interface EventListenerMethod {
|
||||
method?: keyof IMethods;
|
||||
submit?: (param: string) => void;
|
||||
cancel?: () => void;
|
||||
invalid?: boolean;
|
||||
}
|
||||
|
||||
const methods: IMethods = {
|
||||
totp: {
|
||||
text: 'Open_your_authentication_app_and_enter_the_code',
|
||||
keyboardType: 'numeric'
|
||||
|
@ -40,14 +55,14 @@ const methods: any = {
|
|||
}
|
||||
};
|
||||
|
||||
const TwoFactor = React.memo(({ theme, isMasterDetail }: ITwoFactor) => {
|
||||
const TwoFactor = React.memo(({ isMasterDetail }: { isMasterDetail: boolean }) => {
|
||||
const { theme } = useTheme();
|
||||
const [visible, setVisible] = useState(false);
|
||||
const [data, setData] = useState<any>({});
|
||||
const [code, setCode] = useState<any>('');
|
||||
const [data, setData] = useState<EventListenerMethod>({});
|
||||
const [code, setCode] = useState<string>('');
|
||||
|
||||
const method = methods[data.method];
|
||||
const method = data.method ? methods[data.method] : null;
|
||||
const isEmail = data.method === 'email';
|
||||
|
||||
const sendEmail = () => RocketChat.sendEmailCode();
|
||||
|
||||
useDeepCompareEffect(() => {
|
||||
|
@ -59,7 +74,7 @@ const TwoFactor = React.memo(({ theme, isMasterDetail }: ITwoFactor) => {
|
|||
}
|
||||
}, [data]);
|
||||
|
||||
const showTwoFactor = (args: any) => setData(args);
|
||||
const showTwoFactor = (args: EventListenerMethod) => setData(args);
|
||||
|
||||
useEffect(() => {
|
||||
const listener = EventEmitter.addEventListener(TWO_FACTOR, showTwoFactor);
|
||||
|
@ -87,26 +102,19 @@ const TwoFactor = React.memo(({ theme, isMasterDetail }: ITwoFactor) => {
|
|||
setData({});
|
||||
};
|
||||
|
||||
const color = themes[theme!].titleText;
|
||||
const color = themes[theme].titleText;
|
||||
return (
|
||||
<Modal
|
||||
// @ts-ignore
|
||||
transparent
|
||||
avoidKeyboard
|
||||
useNativeDriver
|
||||
isVisible={visible}
|
||||
hideModalContentWhileAnimating>
|
||||
<Modal avoidKeyboard useNativeDriver isVisible={visible} hideModalContentWhileAnimating>
|
||||
<View style={styles.container} testID='two-factor'>
|
||||
<View
|
||||
style={[
|
||||
styles.content,
|
||||
isMasterDetail && [sharedStyles.modalFormSheet, styles.tablet],
|
||||
{ backgroundColor: themes[theme!].backgroundColor }
|
||||
{ backgroundColor: themes[theme].backgroundColor }
|
||||
]}>
|
||||
<Text style={[styles.title, { color }]}>{I18n.t(method?.title || 'Two_Factor_Authentication')}</Text>
|
||||
{method?.text ? <Text style={[styles.subtitle, { color }]}>{I18n.t(method.text)}</Text> : null}
|
||||
<TextInput
|
||||
/* @ts-ignore*/
|
||||
value={code}
|
||||
theme={theme}
|
||||
inputRef={(e: any) => InteractionManager.runAfterInteractions(() => e?.getNativeRef()?.focus())}
|
||||
|
@ -116,19 +124,19 @@ const TwoFactor = React.memo(({ theme, isMasterDetail }: ITwoFactor) => {
|
|||
onSubmitEditing={onSubmit}
|
||||
keyboardType={method?.keyboardType}
|
||||
secureTextEntry={method?.secureTextEntry}
|
||||
error={data.invalid && { error: 'totp-invalid', reason: I18n.t('Code_or_password_invalid') }}
|
||||
error={data.invalid ? { error: 'totp-invalid', reason: I18n.t('Code_or_password_invalid') } : undefined}
|
||||
testID='two-factor-input'
|
||||
/>
|
||||
{isEmail && (
|
||||
{isEmail ? (
|
||||
<Text style={[styles.sendEmail, { color }]} onPress={sendEmail}>
|
||||
{I18n.t('Send_me_the_code_again')}
|
||||
</Text>
|
||||
)}
|
||||
) : null}
|
||||
<View style={styles.buttonContainer}>
|
||||
<Button
|
||||
title={I18n.t('Cancel')}
|
||||
type='secondary'
|
||||
backgroundColor={themes[theme!].chatComponentBackground}
|
||||
backgroundColor={themes[theme].chatComponentBackground}
|
||||
style={styles.button}
|
||||
onPress={onCancel}
|
||||
theme={theme}
|
||||
|
@ -148,8 +156,8 @@ const TwoFactor = React.memo(({ theme, isMasterDetail }: ITwoFactor) => {
|
|||
);
|
||||
});
|
||||
|
||||
const mapStateToProps = (state: any) => ({
|
||||
const mapStateToProps = (state: IApplicationState) => ({
|
||||
isMasterDetail: state.app.isMasterDetail
|
||||
});
|
||||
|
||||
export default connect(mapStateToProps)(withTheme(TwoFactor));
|
||||
export default connect(mapStateToProps)(TwoFactor);
|
||||
|
|
|
@ -3,25 +3,18 @@ import { BLOCK_CONTEXT } from '@rocket.chat/ui-kit';
|
|||
|
||||
import Button from '../Button';
|
||||
import I18n from '../../i18n';
|
||||
|
||||
interface IActions {
|
||||
blockId: string;
|
||||
appId: string;
|
||||
elements: any[];
|
||||
parser: any;
|
||||
theme: string;
|
||||
}
|
||||
import { IActions } from './interfaces';
|
||||
|
||||
export const Actions = ({ blockId, appId, elements, parser, theme }: IActions) => {
|
||||
const [showMoreVisible, setShowMoreVisible] = useState(() => elements.length > 5);
|
||||
const renderedElements = showMoreVisible ? elements.slice(0, 5) : elements;
|
||||
const [showMoreVisible, setShowMoreVisible] = useState(() => elements && elements.length > 5);
|
||||
const renderedElements = showMoreVisible ? elements?.slice(0, 5) : elements;
|
||||
|
||||
const Elements = () =>
|
||||
renderedElements.map((element: any) => parser.renderActions({ blockId, appId, ...element }, BLOCK_CONTEXT.ACTION, parser));
|
||||
const Elements = () => (
|
||||
<>{renderedElements?.map(element => parser?.renderActions({ blockId, appId, ...element }, BLOCK_CONTEXT.ACTION, parser))}</>
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* @ts-ignore*/}
|
||||
<Elements />
|
||||
{showMoreVisible && <Button theme={theme} title={I18n.t('Show_more')} onPress={() => setShowMoreVisible(false)} />}
|
||||
</>
|
||||
|
|
|
@ -1,8 +1,9 @@
|
|||
import React from 'react';
|
||||
import { StyleSheet, View } from 'react-native';
|
||||
import PropTypes from 'prop-types';
|
||||
import { BLOCK_CONTEXT } from '@rocket.chat/ui-kit';
|
||||
|
||||
import { IContext } from './interfaces';
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
minHeight: 36,
|
||||
|
@ -11,13 +12,6 @@ const styles = StyleSheet.create({
|
|||
}
|
||||
});
|
||||
|
||||
export const Context = ({ elements, parser }: any) => (
|
||||
<View style={styles.container}>
|
||||
{elements.map((element: any) => parser.renderContext(element, BLOCK_CONTEXT.CONTEXT, parser))}
|
||||
</View>
|
||||
export const Context = ({ elements, parser }: IContext) => (
|
||||
<View style={styles.container}>{elements?.map(element => parser?.renderContext(element, BLOCK_CONTEXT.CONTEXT, parser))}</View>
|
||||
);
|
||||
|
||||
Context.propTypes = {
|
||||
elements: PropTypes.array,
|
||||
parser: PropTypes.object
|
||||
};
|
||||
|
|
|
@ -12,6 +12,7 @@ import sharedStyles from '../../views/Styles';
|
|||
import { CustomIcon } from '../../lib/Icons';
|
||||
import { isAndroid } from '../../utils/deviceInfo';
|
||||
import ActivityIndicator from '../ActivityIndicator';
|
||||
import { IDatePicker } from './interfaces';
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
input: {
|
||||
|
@ -35,23 +36,11 @@ const styles = StyleSheet.create({
|
|||
}
|
||||
});
|
||||
|
||||
interface IDatePicker {
|
||||
element: {
|
||||
initial_date: any;
|
||||
placeholder: string;
|
||||
};
|
||||
language: string;
|
||||
action: Function;
|
||||
context: number;
|
||||
loading: boolean;
|
||||
theme: string;
|
||||
value: string;
|
||||
error: string;
|
||||
}
|
||||
|
||||
export const DatePicker = ({ element, language, action, context, theme, loading, value, error }: IDatePicker) => {
|
||||
const [show, onShow] = useState(false);
|
||||
const { initial_date, placeholder } = element;
|
||||
const initial_date = element?.initial_date;
|
||||
const placeholder = element?.placeholder;
|
||||
|
||||
const [currentDate, onChangeDate] = useState(new Date(initial_date || value));
|
||||
|
||||
const onChange = ({ nativeEvent: { timestamp } }: any, date: any) => {
|
||||
|
|
|
@ -5,6 +5,8 @@ import { BLOCK_CONTEXT } from '@rocket.chat/ui-kit';
|
|||
|
||||
import ImageContainer from '../message/Image';
|
||||
import Navigation from '../../lib/Navigation';
|
||||
import { IThumb, IImage, IElement } from './interfaces';
|
||||
import { TThemeMode } from '../../definitions/ITheme';
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
image: {
|
||||
|
@ -15,44 +17,25 @@ const styles = StyleSheet.create({
|
|||
}
|
||||
});
|
||||
|
||||
interface IThumb {
|
||||
element: {
|
||||
imageUrl: string;
|
||||
};
|
||||
size?: number;
|
||||
}
|
||||
|
||||
interface IMedia {
|
||||
element: {
|
||||
imageUrl: string;
|
||||
};
|
||||
theme: string;
|
||||
}
|
||||
|
||||
interface IImage {
|
||||
element: any;
|
||||
context: any;
|
||||
theme: string;
|
||||
}
|
||||
|
||||
const ThumbContext = (args: any) => (
|
||||
const ThumbContext = (args: IThumb) => (
|
||||
<View style={styles.mediaContext}>
|
||||
<Thumb size={20} {...args} />
|
||||
</View>
|
||||
);
|
||||
|
||||
export const Thumb = ({ element, size = 88 }: IThumb) => (
|
||||
<FastImage style={[{ width: size, height: size }, styles.image]} source={{ uri: element.imageUrl }} />
|
||||
<FastImage style={[{ width: size, height: size }, styles.image]} source={{ uri: element?.imageUrl }} />
|
||||
);
|
||||
|
||||
export const Media = ({ element, theme }: IMedia) => {
|
||||
export const Media = ({ element, theme }: IImage) => {
|
||||
const showAttachment = (attachment: any) => Navigation.navigate('AttachmentView', { attachment });
|
||||
const { imageUrl } = element;
|
||||
const imageUrl = element?.imageUrl ?? '';
|
||||
// @ts-ignore
|
||||
// TODO: delete ts-ignore after refactor Markdown and ImageContainer
|
||||
return <ImageContainer file={{ image_url: imageUrl }} imageUrl={imageUrl} showAttachment={showAttachment} theme={theme} />;
|
||||
};
|
||||
|
||||
const genericImage = (element: any, context: any, theme: string) => {
|
||||
const genericImage = (theme: TThemeMode, element: IElement, context?: number) => {
|
||||
switch (context) {
|
||||
case BLOCK_CONTEXT.SECTION:
|
||||
return <Thumb element={element} />;
|
||||
|
@ -63,4 +46,4 @@ const genericImage = (element: any, context: any, theme: string) => {
|
|||
}
|
||||
};
|
||||
|
||||
export const Image = ({ element, context, theme }: IImage) => genericImage(element, context, theme);
|
||||
export const Image = ({ element, context, theme }: IImage) => genericImage(theme, element, context);
|
||||
|
|
|
@ -4,6 +4,7 @@ import { BLOCK_CONTEXT } from '@rocket.chat/ui-kit';
|
|||
|
||||
import sharedStyles from '../../views/Styles';
|
||||
import { themes } from '../../constants/colors';
|
||||
import { IInput } from './interfaces';
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
|
@ -31,16 +32,6 @@ const styles = StyleSheet.create({
|
|||
}
|
||||
});
|
||||
|
||||
interface IInput {
|
||||
element: object;
|
||||
parser: any;
|
||||
label: string;
|
||||
description: string;
|
||||
error: string;
|
||||
hint: string;
|
||||
theme: string;
|
||||
}
|
||||
|
||||
export const Input = ({ element, parser, label, description, error, hint, theme }: IInput) => (
|
||||
<View style={styles.container}>
|
||||
{label ? (
|
||||
|
|
|
@ -41,7 +41,7 @@ const Item = ({ item, selected, onSelect, theme }: IItem) => {
|
|||
<>
|
||||
{item.imageUrl ? <FastImage style={styles.itemImage} source={{ uri: item.imageUrl }} /> : null}
|
||||
<Text style={{ color: themes[theme].titleText }}>{textParser([item.text])}</Text>
|
||||
{selected ? <Check theme={theme} /> : null}
|
||||
{selected ? <Check /> : null}
|
||||
</>
|
||||
</Touchable>
|
||||
);
|
||||
|
|
|
@ -24,7 +24,7 @@ interface IMultiSelect {
|
|||
multiselect?: boolean;
|
||||
onSearch?: () => void;
|
||||
onClose?: () => void;
|
||||
inputStyle: object;
|
||||
inputStyle?: object;
|
||||
value?: any[];
|
||||
disabled?: boolean | object;
|
||||
theme: string;
|
||||
|
@ -126,7 +126,6 @@ export const MultiSelect = React.memo(
|
|||
<View style={[styles.content, { backgroundColor: themes[theme].backgroundColor }]}>
|
||||
<TextInput
|
||||
testID='multi-select-search'
|
||||
/* @ts-ignore*/
|
||||
onChangeText={onSearch || onSearchChange}
|
||||
placeholder={I18n.t('Search')}
|
||||
theme={theme}
|
||||
|
|
|
@ -8,32 +8,7 @@ import ActivityIndicator from '../ActivityIndicator';
|
|||
import { themes } from '../../constants/colors';
|
||||
import { BUTTON_HIT_SLOP } from '../message/utils';
|
||||
import * as List from '../List';
|
||||
|
||||
interface IOption {
|
||||
option: {
|
||||
text: string;
|
||||
value: string;
|
||||
};
|
||||
onOptionPress: Function;
|
||||
parser: any;
|
||||
theme: string;
|
||||
}
|
||||
|
||||
interface IOptions {
|
||||
options: [];
|
||||
onOptionPress: Function;
|
||||
parser: object;
|
||||
theme: string;
|
||||
}
|
||||
|
||||
interface IOverflow {
|
||||
element: any;
|
||||
action: Function;
|
||||
loading: boolean;
|
||||
parser: object;
|
||||
theme: string;
|
||||
context: any;
|
||||
}
|
||||
import { IOption, IOptions, IOverflow } from './interfaces';
|
||||
|
||||
const keyExtractor = (item: any) => item.value;
|
||||
|
||||
|
@ -68,10 +43,11 @@ const Options = ({ options, onOptionPress, parser, theme }: IOptions) => (
|
|||
/>
|
||||
);
|
||||
|
||||
const touchable = {};
|
||||
const touchable: { [key: string]: any } = {};
|
||||
|
||||
export const Overflow = ({ element, loading, action, parser, theme }: IOverflow) => {
|
||||
const { options, blockId } = element;
|
||||
const options = element?.options || [];
|
||||
const blockId = element?.blockId || '';
|
||||
const [show, onShow] = useState(false);
|
||||
|
||||
const onOptionPress = ({ value }: any) => {
|
||||
|
@ -82,8 +58,7 @@ export const Overflow = ({ element, loading, action, parser, theme }: IOverflow)
|
|||
return (
|
||||
<>
|
||||
<Touchable
|
||||
/* @ts-ignore*/
|
||||
ref={ref => (touchable[blockId] = ref)}
|
||||
ref={(ref: any) => (touchable[blockId] = ref)}
|
||||
background={Touchable.Ripple(themes[theme].bannerBackground)}
|
||||
onPress={() => onShow(!show)}
|
||||
hitSlop={BUTTON_HIT_SLOP}
|
||||
|
@ -91,7 +66,7 @@ export const Overflow = ({ element, loading, action, parser, theme }: IOverflow)
|
|||
{!loading ? (
|
||||
<CustomIcon size={18} name='kebab' color={themes[theme].bodyText} />
|
||||
) : (
|
||||
<ActivityIndicator style={styles.loading} theme={theme} />
|
||||
<ActivityIndicator style={styles.loading} />
|
||||
)}
|
||||
</Touchable>
|
||||
<Popover
|
||||
|
|
|
@ -3,6 +3,7 @@ import { StyleSheet, Text, View } from 'react-native';
|
|||
import { BLOCK_CONTEXT } from '@rocket.chat/ui-kit';
|
||||
|
||||
import { themes } from '../../constants/colors';
|
||||
import { IAccessoryComponent, IFields, ISection } from './interfaces';
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
content: {
|
||||
|
@ -23,36 +24,16 @@ const styles = StyleSheet.create({
|
|||
}
|
||||
});
|
||||
|
||||
interface IAccessory {
|
||||
blockId?: string;
|
||||
appId?: string;
|
||||
element: any;
|
||||
parser: any;
|
||||
}
|
||||
const Accessory = ({ element, parser }: IAccessoryComponent) =>
|
||||
parser.renderAccessories({ ...element }, BLOCK_CONTEXT.SECTION, parser);
|
||||
|
||||
interface IFields {
|
||||
fields: any;
|
||||
parser: any;
|
||||
theme: string;
|
||||
}
|
||||
|
||||
interface ISection {
|
||||
blockId: string;
|
||||
appId: string;
|
||||
text: object;
|
||||
fields: [];
|
||||
accessory: any;
|
||||
theme: string;
|
||||
parser: any;
|
||||
}
|
||||
|
||||
const Accessory = ({ blockId, appId, element, parser }: IAccessory) =>
|
||||
parser.renderAccessories({ blockId, appId, ...element }, BLOCK_CONTEXT.SECTION, parser);
|
||||
|
||||
const Fields = ({ fields, parser, theme }: IFields) =>
|
||||
fields.map((field: any) => (
|
||||
const Fields = ({ fields, parser, theme }: IFields) => (
|
||||
<>
|
||||
{fields.map(field => (
|
||||
<Text style={[styles.text, styles.field, { color: themes[theme].bodyText }]}>{parser.text(field)}</Text>
|
||||
));
|
||||
))}
|
||||
</>
|
||||
);
|
||||
|
||||
const accessoriesRight = ['image', 'overflow'];
|
||||
|
||||
|
|
|
@ -20,6 +20,7 @@ import { Input } from './Input';
|
|||
import { DatePicker } from './DatePicker';
|
||||
import { Overflow } from './Overflow';
|
||||
import { ThemeContext } from '../../theme';
|
||||
import { BlockContext, IButton, IInputIndex, IParser, IText } from './interfaces';
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
input: {
|
||||
|
@ -42,8 +43,12 @@ const styles = StyleSheet.create({
|
|||
const plainText = ({ text } = { text: '' }) => text;
|
||||
|
||||
class MessageParser extends UiKitParserMessage {
|
||||
text({ text, type }: any = { text: '' }, context: any) {
|
||||
const { theme }: any = useContext(ThemeContext);
|
||||
get current() {
|
||||
return this as unknown as IParser;
|
||||
}
|
||||
|
||||
text({ text, type }: Partial<IText> = { text: '' }, context: BlockContext) {
|
||||
const { theme } = useContext(ThemeContext);
|
||||
if (type !== 'mrkdwn') {
|
||||
return <Text style={[styles.text, { color: themes[theme].bodyText }]}>{text}</Text>;
|
||||
}
|
||||
|
@ -55,9 +60,9 @@ class MessageParser extends UiKitParserMessage {
|
|||
return <Markdown msg={text} theme={theme} style={[isContext && { color: themes[theme].auxiliaryText }]} />;
|
||||
}
|
||||
|
||||
button(element: any, context: any) {
|
||||
button(element: IButton, context: BlockContext) {
|
||||
const { text, value, actionId, style } = element;
|
||||
const [{ loading }, action]: any = useBlockContext(element, context);
|
||||
const [{ loading }, action] = useBlockContext(element, context);
|
||||
const { theme } = useContext(ThemeContext);
|
||||
return (
|
||||
<Button
|
||||
|
@ -73,7 +78,7 @@ class MessageParser extends UiKitParserMessage {
|
|||
}
|
||||
|
||||
divider() {
|
||||
const { theme }: any = useContext(ThemeContext);
|
||||
const { theme } = useContext(ThemeContext);
|
||||
// @ts-ignore
|
||||
return <Divider theme={theme} />;
|
||||
}
|
||||
|
@ -91,7 +96,7 @@ class MessageParser extends UiKitParserMessage {
|
|||
overflow(element: any, context: any) {
|
||||
const [{ loading }, action]: any = useBlockContext(element, context);
|
||||
const { theme }: any = useContext(ThemeContext);
|
||||
return <Overflow element={element} context={context} loading={loading} action={action} theme={theme} parser={this} />;
|
||||
return <Overflow element={element} context={context} loading={loading} action={action} theme={theme} parser={this.current} />;
|
||||
}
|
||||
|
||||
datePicker(element: any, context: any) {
|
||||
|
@ -150,12 +155,16 @@ class ModalParser extends UiKitParserModal {
|
|||
});
|
||||
}
|
||||
|
||||
input({ element, blockId, appId, label, description, hint }: any, context: any) {
|
||||
get current() {
|
||||
return this as unknown as IParser;
|
||||
}
|
||||
|
||||
input({ element, blockId, appId, label, description, hint }: IInputIndex, context: number) {
|
||||
const [{ error }]: any = useBlockContext({ ...element, appId, blockId }, context);
|
||||
const { theme }: any = useContext(ThemeContext);
|
||||
return (
|
||||
<Input
|
||||
parser={this}
|
||||
parser={this.current}
|
||||
element={{ ...element, appId, blockId }}
|
||||
label={plainText(label)}
|
||||
description={plainText(description)}
|
||||
|
@ -178,16 +187,14 @@ class ModalParser extends UiKitParserModal {
|
|||
return (
|
||||
// @ts-ignore
|
||||
<TextInput
|
||||
id={actionId}
|
||||
key={actionId}
|
||||
placeholder={plainText(placeholder)}
|
||||
onInput={action}
|
||||
multiline={multiline}
|
||||
loading={loading}
|
||||
onChangeText={(text: any) => action({ value: text })}
|
||||
onChangeText={text => action({ value: text })}
|
||||
inputStyle={multiline && styles.multiline}
|
||||
containerStyle={styles.input}
|
||||
value={value}
|
||||
// @ts-ignore
|
||||
error={{ error }}
|
||||
theme={theme}
|
||||
/>
|
||||
|
|
|
@ -0,0 +1,273 @@
|
|||
import { TThemeMode } from '../../definitions/ITheme';
|
||||
|
||||
export enum ElementTypes {
|
||||
IMAGE = 'image',
|
||||
BUTTON = 'button',
|
||||
STATIC_SELECT = 'static_select',
|
||||
MULTI_STATIC_SELECT = 'multi_static_select',
|
||||
CONVERSATION_SELECT = 'conversations_select',
|
||||
CHANNEL_SELECT = 'channels_select',
|
||||
USER_SELECT = 'users_select',
|
||||
OVERFLOW = 'overflow',
|
||||
DATEPICKER = 'datepicker',
|
||||
PLAIN_TEXT_INPUT = 'plain_text_input',
|
||||
SECTION = 'section',
|
||||
DIVIDER = 'divider',
|
||||
ACTIONS = 'actions',
|
||||
CONTEXT = 'context',
|
||||
FIELDS = 'fields',
|
||||
INPUT = 'input',
|
||||
PLAIN_TEXT = 'plain_text',
|
||||
TEXT = 'text',
|
||||
MARKDOWN = 'mrkdwn'
|
||||
}
|
||||
|
||||
export enum BlockContext {
|
||||
BLOCK,
|
||||
SECTION,
|
||||
ACTION,
|
||||
FORM,
|
||||
CONTEXT
|
||||
}
|
||||
|
||||
export enum ActionTypes {
|
||||
ACTION = 'blockAction',
|
||||
SUBMIT = 'viewSubmit',
|
||||
CLOSED = 'viewClosed'
|
||||
}
|
||||
|
||||
export enum ContainerTypes {
|
||||
VIEW = 'view',
|
||||
MESSAGE = 'message'
|
||||
}
|
||||
|
||||
export enum ModalActions {
|
||||
MODAL = 'modal',
|
||||
OPEN = 'modal.open',
|
||||
CLOSE = 'modal.close',
|
||||
UPDATE = 'modal.update',
|
||||
ERRORS = 'errors'
|
||||
}
|
||||
|
||||
export interface IStateView {
|
||||
[key: string]: { [settings: string]: string | number };
|
||||
}
|
||||
|
||||
export interface IView {
|
||||
appId: string;
|
||||
type: ModalActions;
|
||||
id: string;
|
||||
title: IText;
|
||||
submit: IButton;
|
||||
close: IButton;
|
||||
blocks: Block[];
|
||||
showIcon: boolean;
|
||||
state?: IStateView;
|
||||
}
|
||||
|
||||
export interface Block {
|
||||
type: ElementTypes;
|
||||
blockId: string;
|
||||
element?: IElement;
|
||||
label?: string;
|
||||
appId: string;
|
||||
optional?: boolean;
|
||||
elements?: IElement[];
|
||||
}
|
||||
|
||||
export interface IElement {
|
||||
type: ElementTypes;
|
||||
placeholder?: IText;
|
||||
actionId: string;
|
||||
initialValue?: string;
|
||||
options?: Option[];
|
||||
text?: IText;
|
||||
value?: string;
|
||||
initial_date?: any;
|
||||
imageUrl?: string;
|
||||
appId?: string;
|
||||
blockId?: string;
|
||||
}
|
||||
|
||||
export interface IText {
|
||||
type: ElementTypes;
|
||||
text: string;
|
||||
emoji?: boolean;
|
||||
}
|
||||
|
||||
export interface Option {
|
||||
text: IText;
|
||||
value: string;
|
||||
}
|
||||
|
||||
export interface IButton {
|
||||
type: ElementTypes;
|
||||
text: IText;
|
||||
actionId: string;
|
||||
value?: any;
|
||||
style?: any;
|
||||
}
|
||||
|
||||
export interface IContainer {
|
||||
type: ContainerTypes;
|
||||
id: string;
|
||||
}
|
||||
|
||||
// methods/actions
|
||||
export interface IUserInteraction {
|
||||
triggerId: string;
|
||||
appId?: string;
|
||||
viewId?: string;
|
||||
view: IView;
|
||||
}
|
||||
|
||||
export interface IEmitUserInteraction extends IUserInteraction {
|
||||
type: ModalActions;
|
||||
}
|
||||
|
||||
export interface ITriggerAction {
|
||||
type: ActionTypes;
|
||||
actionId?: string;
|
||||
appId?: string;
|
||||
container?: IContainer;
|
||||
value?: number;
|
||||
blockId?: string;
|
||||
rid?: string;
|
||||
mid?: string;
|
||||
viewId?: string;
|
||||
payload?: any;
|
||||
view?: IView;
|
||||
}
|
||||
|
||||
export interface ITriggerBlockAction {
|
||||
container: IContainer;
|
||||
actionId: string;
|
||||
appId: string;
|
||||
value: number;
|
||||
blockId?: string;
|
||||
mid?: string;
|
||||
rid?: string;
|
||||
}
|
||||
|
||||
export interface ITriggerSubmitView {
|
||||
viewId: string;
|
||||
appId: string;
|
||||
payload: {
|
||||
view: {
|
||||
id: string;
|
||||
state: IStateView;
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
export interface ITriggerCancel {
|
||||
view: IView;
|
||||
appId: string;
|
||||
viewId: string;
|
||||
isCleared: boolean;
|
||||
}
|
||||
|
||||
// UiKit components
|
||||
export interface IParser {
|
||||
renderAccessories: (data: TElementAccessory, context: BlockContext, parser: IParser) => JSX.Element;
|
||||
renderActions: (data: Block, context: BlockContext, parser: IParser) => JSX.Element;
|
||||
renderContext: (data: IElement, context: BlockContext, parser: IParser) => JSX.Element;
|
||||
renderInputs: (data: Partial<IElement>, context: BlockContext, parser: IParser) => JSX.Element;
|
||||
text: (data: IText) => JSX.Element;
|
||||
}
|
||||
export interface IActions extends Block {
|
||||
parser?: IParser;
|
||||
theme: TThemeMode;
|
||||
}
|
||||
|
||||
export interface IContext extends Block {
|
||||
parser: IParser;
|
||||
}
|
||||
|
||||
export interface IDatePicker extends Partial<Block> {
|
||||
language: string;
|
||||
action: Function;
|
||||
context: number;
|
||||
loading: boolean;
|
||||
value: string;
|
||||
error: string;
|
||||
theme: TThemeMode;
|
||||
}
|
||||
|
||||
export interface IInput extends Partial<Block> {
|
||||
parser: IParser;
|
||||
description: string;
|
||||
error: string;
|
||||
hint: string;
|
||||
theme: TThemeMode;
|
||||
}
|
||||
|
||||
export interface IInputIndex {
|
||||
element: IElement;
|
||||
blockId: string;
|
||||
appId: string;
|
||||
label: IText;
|
||||
description: IText;
|
||||
hint: IText;
|
||||
}
|
||||
|
||||
export interface IThumb {
|
||||
element: IElement;
|
||||
size?: number;
|
||||
}
|
||||
export interface IImage {
|
||||
element: IElement;
|
||||
theme: TThemeMode;
|
||||
context?: number;
|
||||
}
|
||||
|
||||
// UiKit/Overflow
|
||||
export interface IOverflow extends Partial<Block> {
|
||||
action: Function;
|
||||
loading: boolean;
|
||||
parser: IParser;
|
||||
theme: TThemeMode;
|
||||
context: number;
|
||||
}
|
||||
|
||||
interface PropsOption {
|
||||
onOptionPress: Function;
|
||||
parser: IParser;
|
||||
theme: TThemeMode;
|
||||
}
|
||||
export interface IOptions extends PropsOption {
|
||||
options: Option[];
|
||||
}
|
||||
|
||||
export interface IOption extends PropsOption {
|
||||
option: Option;
|
||||
}
|
||||
|
||||
// UiKit/Section
|
||||
interface IAccessory {
|
||||
type: ElementTypes;
|
||||
actionId: string;
|
||||
value: number;
|
||||
text: IText;
|
||||
}
|
||||
|
||||
type TElementAccessory = IAccessory & { blockId: string; appId: string };
|
||||
export interface IAccessoryComponent {
|
||||
element: TElementAccessory;
|
||||
parser: IParser;
|
||||
}
|
||||
export interface ISection {
|
||||
blockId: string;
|
||||
appId: string;
|
||||
text?: IText;
|
||||
accessory?: IAccessory;
|
||||
parser: IParser;
|
||||
theme: TThemeMode;
|
||||
fields?: any[];
|
||||
}
|
||||
|
||||
export interface IFields {
|
||||
parser: IParser;
|
||||
theme: TThemeMode;
|
||||
fields: any[];
|
||||
}
|
|
@ -2,6 +2,8 @@
|
|||
import React, { useContext, useState } from 'react';
|
||||
import { BLOCK_CONTEXT } from '@rocket.chat/ui-kit';
|
||||
|
||||
import { BlockContext } from './interfaces';
|
||||
|
||||
export const textParser = ([{ text }]: any) => text;
|
||||
|
||||
export const defaultContext: any = {
|
||||
|
@ -13,7 +15,19 @@ export const defaultContext: any = {
|
|||
|
||||
export const KitContext = React.createContext(defaultContext);
|
||||
|
||||
export const useBlockContext = ({ blockId, actionId, appId, initialValue }: any, context: any) => {
|
||||
type TObjectReturn = {
|
||||
loading: boolean;
|
||||
setLoading: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
error: any;
|
||||
value: any;
|
||||
language: any;
|
||||
};
|
||||
|
||||
type TFunctionReturn = (value: any) => Promise<void>;
|
||||
|
||||
type TReturn = [TObjectReturn, TFunctionReturn];
|
||||
|
||||
export const useBlockContext = ({ blockId, actionId, appId, initialValue }: any, context: BlockContext): TReturn => {
|
||||
const { action, appId: appIdFromContext, viewId, state, language, errors, values = {} } = useContext(KitContext);
|
||||
const { value = initialValue } = values[actionId] || {};
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import React from 'react';
|
||||
import { ScrollView, Text, TouchableOpacity, View } from 'react-native';
|
||||
import { ScrollView, Text, TouchableOpacity, View, ViewStyle } from 'react-native';
|
||||
|
||||
import { CELL_WIDTH } from './TableCell';
|
||||
import styles from './styles';
|
||||
|
@ -19,7 +19,7 @@ const Table = React.memo(({ children, numColumns, theme }: ITable) => {
|
|||
const getTableWidth = () => numColumns * CELL_WIDTH;
|
||||
|
||||
const renderRows = (drawExtraBorders = true) => {
|
||||
const tableStyle = [styles.table, { borderColor: themes[theme].borderColor }];
|
||||
const tableStyle: ViewStyle[] = [styles.table, { borderColor: themes[theme].borderColor }];
|
||||
if (drawExtraBorders) {
|
||||
tableStyle.push(styles.tableExtraBorders);
|
||||
}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import React from 'react';
|
||||
import { Text, View } from 'react-native';
|
||||
import { Text, View, ViewStyle } from 'react-native';
|
||||
|
||||
import { themes } from '../../constants/colors';
|
||||
import styles from './styles';
|
||||
|
@ -14,7 +14,7 @@ interface ITableCell {
|
|||
export const CELL_WIDTH = 100;
|
||||
|
||||
const TableCell = React.memo(({ isLastCell, align, children, theme }: ITableCell) => {
|
||||
const cellStyle = [styles.cell, { borderColor: themes[theme].borderColor }];
|
||||
const cellStyle: ViewStyle[] = [styles.cell, { borderColor: themes[theme].borderColor }];
|
||||
if (!isLastCell) {
|
||||
cellStyle.push(styles.cellRightBorder);
|
||||
}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import React from 'react';
|
||||
import { View } from 'react-native';
|
||||
import { View, ViewStyle } from 'react-native';
|
||||
|
||||
import { themes } from '../../constants/colors';
|
||||
import styles from './styles';
|
||||
|
@ -11,7 +11,7 @@ interface ITableRow {
|
|||
}
|
||||
|
||||
const TableRow = React.memo(({ isLastRow, children: _children, theme }: ITableRow) => {
|
||||
const rowStyle = [styles.row, { borderColor: themes[theme].borderColor }];
|
||||
const rowStyle: ViewStyle[] = [styles.row, { borderColor: themes[theme].borderColor }];
|
||||
if (!isLastRow) {
|
||||
rowStyle.push(styles.rowBottomBorder);
|
||||
}
|
||||
|
|
|
@ -280,6 +280,7 @@ class Markdown extends PureComponent<IMarkdownProps, any> {
|
|||
|
||||
renderHeading = ({ children, level }: any) => {
|
||||
const { numberOfLines, theme } = this.props;
|
||||
// @ts-ignore
|
||||
const textStyle = styles[`heading${level}Text`];
|
||||
return (
|
||||
<Text numberOfLines={numberOfLines} style={[textStyle, { color: themes[theme].bodyText }]}>
|
||||
|
|
|
@ -7,7 +7,7 @@ const codeFontFamily = Platform.select({
|
|||
android: { fontFamily: 'monospace' }
|
||||
});
|
||||
|
||||
export default StyleSheet.create<any>({
|
||||
export default StyleSheet.create({
|
||||
container: {
|
||||
alignItems: 'flex-start',
|
||||
flexDirection: 'row'
|
||||
|
|
|
@ -10,9 +10,16 @@ import Reply from './Reply';
|
|||
import Button from '../Button';
|
||||
import styles from './styles';
|
||||
import MessageContext from './Context';
|
||||
import { useTheme } from '../../theme';
|
||||
import { IAttachment } from '../../definitions';
|
||||
import CollapsibleQuote from './Components/CollapsibleQuote';
|
||||
|
||||
const AttachedActions = ({ attachment, theme }: IMessageAttachedActions) => {
|
||||
const AttachedActions = ({ attachment }: IMessageAttachedActions) => {
|
||||
if (!attachment.actions) {
|
||||
return null;
|
||||
}
|
||||
const { onAnswerButtonPress } = useContext(MessageContext);
|
||||
const { theme } = useTheme();
|
||||
|
||||
const attachedButtons = attachment.actions.map((element: { type: string; msg: string; text: string }) => {
|
||||
if (element.type === 'button') {
|
||||
|
@ -29,42 +36,62 @@ const AttachedActions = ({ attachment, theme }: IMessageAttachedActions) => {
|
|||
};
|
||||
|
||||
const Attachments = React.memo(
|
||||
({ attachments, timeFormat, showAttachment, getCustomEmoji, theme }: IMessageAttachments) => {
|
||||
// @ts-ignore
|
||||
({ attachments, timeFormat, showAttachment, style, getCustomEmoji, isReply }: IMessageAttachments) => {
|
||||
if (!attachments || attachments.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return attachments.map((file: any, index: number) => {
|
||||
if (file.image_url) {
|
||||
return (
|
||||
<Image key={file.image_url} file={file} showAttachment={showAttachment} getCustomEmoji={getCustomEmoji} theme={theme} />
|
||||
);
|
||||
}
|
||||
if (file.audio_url) {
|
||||
return <Audio key={file.audio_url} file={file} getCustomEmoji={getCustomEmoji} theme={theme} />;
|
||||
}
|
||||
if (file.video_url) {
|
||||
return (
|
||||
<Video key={file.video_url} file={file} showAttachment={showAttachment} getCustomEmoji={getCustomEmoji} theme={theme} />
|
||||
);
|
||||
}
|
||||
if (file.actions && file.actions.length > 0) {
|
||||
return <AttachedActions attachment={file} theme={theme} />;
|
||||
}
|
||||
const { theme } = useTheme();
|
||||
|
||||
return attachments.map((file: IAttachment, index: number) => {
|
||||
if (file && file.image_url) {
|
||||
return (
|
||||
<Reply
|
||||
key={index}
|
||||
index={index}
|
||||
attachment={file}
|
||||
timeFormat={timeFormat}
|
||||
<Image
|
||||
key={file.image_url}
|
||||
file={file}
|
||||
showAttachment={showAttachment}
|
||||
getCustomEmoji={getCustomEmoji}
|
||||
style={style}
|
||||
isReply={isReply}
|
||||
theme={theme}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (file && file.audio_url) {
|
||||
return (
|
||||
<Audio key={file.audio_url} file={file} getCustomEmoji={getCustomEmoji} isReply={isReply} style={style} theme={theme} />
|
||||
);
|
||||
}
|
||||
|
||||
if (file.video_url) {
|
||||
return (
|
||||
<Video
|
||||
key={file.video_url}
|
||||
file={file}
|
||||
showAttachment={showAttachment}
|
||||
getCustomEmoji={getCustomEmoji}
|
||||
style={style}
|
||||
isReply={isReply}
|
||||
theme={theme}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (file && file.actions && file.actions.length > 0) {
|
||||
return <AttachedActions attachment={file} />;
|
||||
}
|
||||
if (file.title) {
|
||||
return (
|
||||
<CollapsibleQuote key={index} index={index} attachment={file} timeFormat={timeFormat} getCustomEmoji={getCustomEmoji} />
|
||||
);
|
||||
}
|
||||
|
||||
return <Reply key={index} index={index} attachment={file} timeFormat={timeFormat} getCustomEmoji={getCustomEmoji} />;
|
||||
});
|
||||
},
|
||||
(prevProps, nextProps) => dequal(prevProps.attachments, nextProps.attachments) && prevProps.theme === nextProps.theme
|
||||
(prevProps, nextProps) => dequal(prevProps.attachments, nextProps.attachments)
|
||||
);
|
||||
|
||||
Attachments.displayName = 'MessageAttachments';
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import React from 'react';
|
||||
import { Easing, StyleSheet, Text, View } from 'react-native';
|
||||
import { StyleProp, StyleSheet, Text, TextStyle, View } from 'react-native';
|
||||
import { Audio } from 'expo-av';
|
||||
import Slider from '@react-native-community/slider';
|
||||
import moment from 'moment';
|
||||
|
@ -16,19 +16,20 @@ import MessageContext from './Context';
|
|||
import ActivityIndicator from '../ActivityIndicator';
|
||||
import { withDimensions } from '../../dimensions';
|
||||
import { TGetCustomEmoji } from '../../definitions/IEmoji';
|
||||
import { IAttachment } from '../../definitions';
|
||||
|
||||
interface IButton {
|
||||
loading: boolean;
|
||||
paused: boolean;
|
||||
theme: string;
|
||||
disabled?: boolean;
|
||||
onPress: Function;
|
||||
}
|
||||
|
||||
interface IMessageAudioProps {
|
||||
file: {
|
||||
audio_url: string;
|
||||
description: string;
|
||||
};
|
||||
file: IAttachment;
|
||||
isReply?: boolean;
|
||||
style?: StyleProp<TextStyle>[];
|
||||
theme: string;
|
||||
getCustomEmoji: TGetCustomEmoji;
|
||||
scale?: number;
|
||||
|
@ -83,22 +84,21 @@ const formatTime = (seconds: number) => moment.utc(seconds * 1000).format('mm:ss
|
|||
|
||||
const BUTTON_HIT_SLOP = { top: 12, right: 12, bottom: 12, left: 12 };
|
||||
|
||||
const sliderAnimationConfig = {
|
||||
duration: 250,
|
||||
easing: Easing.linear,
|
||||
delay: 0
|
||||
};
|
||||
|
||||
const Button = React.memo(({ loading, paused, onPress, theme }: IButton) => (
|
||||
const Button = React.memo(({ loading, paused, onPress, disabled, theme }: IButton) => (
|
||||
<Touchable
|
||||
style={styles.playPauseButton}
|
||||
disabled={disabled}
|
||||
onPress={onPress}
|
||||
hitSlop={BUTTON_HIT_SLOP}
|
||||
background={Touchable.SelectableBackgroundBorderless()}>
|
||||
{loading ? (
|
||||
<ActivityIndicator style={[styles.playPauseButton, styles.audioLoading]} theme={theme} />
|
||||
<ActivityIndicator style={[styles.playPauseButton, styles.audioLoading]} />
|
||||
) : (
|
||||
<CustomIcon name={paused ? 'play-filled' : 'pause-filled'} size={36} color={themes[theme].tintColor} />
|
||||
<CustomIcon
|
||||
name={paused ? 'play-filled' : 'pause-filled'}
|
||||
size={36}
|
||||
color={disabled ? themes[theme].tintDisabled : themes[theme].tintColor}
|
||||
/>
|
||||
)}
|
||||
</Touchable>
|
||||
));
|
||||
|
@ -128,7 +128,7 @@ class MessageAudio extends React.Component<IMessageAudioProps, IMessageAudioStat
|
|||
const { baseUrl, user } = this.context;
|
||||
|
||||
let url = file.audio_url;
|
||||
if (!url.startsWith('http')) {
|
||||
if (url && !url.startsWith('http')) {
|
||||
url = `${baseUrl}${file.audio_url}`;
|
||||
}
|
||||
|
||||
|
@ -249,7 +249,7 @@ class MessageAudio extends React.Component<IMessageAudioProps, IMessageAudioStat
|
|||
|
||||
render() {
|
||||
const { loading, paused, currentTime, duration } = this.state;
|
||||
const { file, getCustomEmoji, theme, scale } = this.props;
|
||||
const { file, getCustomEmoji, theme, scale, isReply, style } = this.props;
|
||||
const { description } = file;
|
||||
const { baseUrl, user } = this.context;
|
||||
|
||||
|
@ -259,29 +259,34 @@ class MessageAudio extends React.Component<IMessageAudioProps, IMessageAudioStat
|
|||
|
||||
return (
|
||||
<>
|
||||
<Markdown
|
||||
msg={description}
|
||||
style={[isReply && style]}
|
||||
baseUrl={baseUrl}
|
||||
username={user.username}
|
||||
getCustomEmoji={getCustomEmoji}
|
||||
theme={theme}
|
||||
/>
|
||||
<View
|
||||
style={[
|
||||
styles.audioContainer,
|
||||
{ backgroundColor: themes[theme].chatComponentBackground, borderColor: themes[theme].borderColor }
|
||||
]}>
|
||||
<Button loading={loading} paused={paused} onPress={this.togglePlayPause} theme={theme} />
|
||||
<Button disabled={isReply} loading={loading} paused={paused} onPress={this.togglePlayPause} theme={theme} />
|
||||
<Slider
|
||||
disabled={isReply}
|
||||
style={styles.slider}
|
||||
value={currentTime}
|
||||
maximumValue={duration}
|
||||
minimumValue={0}
|
||||
animateTransitions
|
||||
animationConfig={sliderAnimationConfig}
|
||||
thumbTintColor={isAndroid && themes[theme].tintColor}
|
||||
thumbTintColor={isReply && isAndroid ? themes[theme].tintDisabled : isAndroid && themes[theme].tintColor}
|
||||
minimumTrackTintColor={themes[theme].tintColor}
|
||||
maximumTrackTintColor={themes[theme].auxiliaryText}
|
||||
onValueChange={this.onValueChange}
|
||||
/* @ts-ignore*/
|
||||
thumbImage={isIOS && { uri: 'audio_thumb', scale }}
|
||||
thumbImage={isIOS ? { uri: 'audio_thumb', scale } : undefined}
|
||||
/>
|
||||
<Text style={[styles.duration, { color: themes[theme].auxiliaryText }]}>{this.duration}</Text>
|
||||
</View>
|
||||
<Markdown msg={description} baseUrl={baseUrl} username={user.username} getCustomEmoji={getCustomEmoji} theme={theme} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -0,0 +1,34 @@
|
|||
import { storiesOf } from '@storybook/react-native';
|
||||
import React from 'react';
|
||||
import { View } from 'react-native';
|
||||
|
||||
import MessageContext from '../../Context';
|
||||
import CollapsibleQuote from '.';
|
||||
|
||||
const testAttachment = {
|
||||
ts: '1970-01-01T00:00:00.000Z',
|
||||
title: 'Engineering (9 today)',
|
||||
fields: [
|
||||
{
|
||||
title: 'Out Today:\n',
|
||||
value:
|
||||
'Ricardo Mellu, 1 day, until Fri Mar 11\nLoma, 1 day, until Fri Mar 11\nAnitta, 3 hours\nDiego Carlitos, 19 days, until Fri Mar 11\nGabriel Vasconcelos, 5 days, until Fri Mar 11\nJorge Leite, 1 day, until Fri Mar 11\nKevin Aleman, 1 day, until Fri Mar 11\nPierre, 1 day, until Fri Mar 11\nTiago Evangelista Pinto, 1 day, until Fri Mar 11'
|
||||
}
|
||||
],
|
||||
attachments: [],
|
||||
collapsed: true
|
||||
};
|
||||
|
||||
const stories = storiesOf('Message', module);
|
||||
|
||||
stories.add('Item', () => (
|
||||
<View style={{ padding: 10 }}>
|
||||
<MessageContext.Provider
|
||||
value={{
|
||||
onLongPress: () => {},
|
||||
user: { username: 'Marcos' }
|
||||
}}>
|
||||
<CollapsibleQuote key={0} index={0} attachment={testAttachment} getCustomEmoji={() => {}} timeFormat='LT' />
|
||||
</MessageContext.Provider>
|
||||
</View>
|
||||
));
|
|
@ -0,0 +1,94 @@
|
|||
import { fireEvent, render, within } from '@testing-library/react-native';
|
||||
import React from 'react';
|
||||
|
||||
import MessageContext from '../../Context';
|
||||
import CollapsibleQuote from '.';
|
||||
|
||||
// For some reason a general mock didn't work, I have to do a search
|
||||
jest.mock('react-native-mmkv-storage', () => ({
|
||||
Loader: jest.fn().mockImplementation(() => ({
|
||||
setProcessingMode: jest.fn().mockImplementation(() => ({
|
||||
withEncryption: jest.fn().mockImplementation(() => ({
|
||||
initialize: jest.fn()
|
||||
}))
|
||||
}))
|
||||
})),
|
||||
create: jest.fn(),
|
||||
MODES: { MULTI_PROCESS: '' }
|
||||
}));
|
||||
|
||||
const testAttachment = {
|
||||
ts: '1970-01-01T00:00:00.000Z',
|
||||
title: 'Engineering (9 today)',
|
||||
fields: [
|
||||
{
|
||||
title: 'Out Today:\n',
|
||||
value:
|
||||
'Ricardo Mellu, 1 day, until Fri Mar 11\nLoma, 1 day, until Fri Mar 11\nAnitta, 3 hours\nDiego Carlitos, 19 days, until Fri Mar 11\nGabriel Vasconcelos, 5 days, until Fri Mar 11\nJorge Leite, 1 day, until Fri Mar 11\nKevin Aleman, 1 day, until Fri Mar 11\nPierre, 1 day, until Fri Mar 11\nTiago Evangelista Pinto, 1 day, until Fri Mar 11'
|
||||
}
|
||||
],
|
||||
attachments: [],
|
||||
collapsed: true
|
||||
};
|
||||
|
||||
const mockFn = jest.fn();
|
||||
|
||||
const Render = () => (
|
||||
<MessageContext.Provider
|
||||
value={{
|
||||
onLongPress: () => {},
|
||||
user: { username: 'Marcos' }
|
||||
}}>
|
||||
<CollapsibleQuote key={0} index={0} attachment={testAttachment} getCustomEmoji={mockFn} timeFormat='LT' />
|
||||
</MessageContext.Provider>
|
||||
);
|
||||
|
||||
const touchableTestID = `collapsibleQuoteTouchable-${testAttachment.title}`;
|
||||
|
||||
describe('CollapsibleQuote', () => {
|
||||
test('rendered', async () => {
|
||||
const { findByTestId } = render(<Render />);
|
||||
const collapsibleQuoteTouchable = await findByTestId(touchableTestID);
|
||||
expect(collapsibleQuoteTouchable).toBeTruthy();
|
||||
});
|
||||
|
||||
test('title exists and is correct', async () => {
|
||||
const { findByText } = render(<Render />);
|
||||
const collapsibleQuoteTitle = await findByText(testAttachment.title);
|
||||
expect(collapsibleQuoteTitle).toBeTruthy();
|
||||
expect(collapsibleQuoteTitle.props.children).toEqual(testAttachment.title);
|
||||
});
|
||||
|
||||
test('fields render title correctly', async () => {
|
||||
const collapsibleQuote = render(<Render />);
|
||||
const collapsibleQuoteTouchable = await collapsibleQuote.findByTestId(touchableTestID);
|
||||
// open
|
||||
fireEvent.press(collapsibleQuoteTouchable);
|
||||
const open = within(collapsibleQuoteTouchable);
|
||||
const fieldTitleOpen = open.getByTestId('collapsibleQuoteTouchableFieldTitle');
|
||||
expect(fieldTitleOpen).toBeTruthy();
|
||||
expect(fieldTitleOpen.props.children).toEqual(testAttachment.fields[0].title);
|
||||
// close
|
||||
fireEvent.press(collapsibleQuoteTouchable);
|
||||
collapsibleQuote.rerender(<Render />);
|
||||
const close = within(collapsibleQuoteTouchable);
|
||||
const fieldTitleClosed = close.queryByTestId('collapsibleQuoteTouchableFieldTitle');
|
||||
expect(fieldTitleClosed).toBeNull();
|
||||
});
|
||||
|
||||
test('fields render fields correctly', async () => {
|
||||
const collapsibleQuote = render(<Render />);
|
||||
const collapsibleQuoteTouchable = await collapsibleQuote.findByTestId(touchableTestID);
|
||||
// open
|
||||
fireEvent.press(collapsibleQuoteTouchable);
|
||||
const open = within(collapsibleQuoteTouchable);
|
||||
const fieldValueOpen = open.getByLabelText(testAttachment.fields[0].value.split('\n')[0]);
|
||||
expect(fieldValueOpen).toBeTruthy();
|
||||
// close
|
||||
fireEvent.press(collapsibleQuoteTouchable);
|
||||
collapsibleQuote.rerender(<Render />);
|
||||
const close = within(collapsibleQuoteTouchable);
|
||||
const fieldValueClosed = close.queryByTestId(testAttachment.fields[0].value.split('\n')[0]);
|
||||
expect(fieldValueClosed).toBeNull();
|
||||
});
|
||||
});
|
|
@ -0,0 +1,3 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`Storyshots Message Item 1`] = `"{\\"type\\":\\"View\\",\\"props\\":{\\"style\\":{\\"padding\\":10}},\\"children\\":[{\\"type\\":\\"View\\",\\"props\\":{\\"accessible\\":true,\\"testID\\":\\"collapsibleQuoteTouchable-Engineering (9 today)\\",\\"hitSlop\\":{\\"top\\":4,\\"right\\":4,\\"bottom\\":4,\\"left\\":4},\\"focusable\\":true,\\"style\\":{\\"flexDirection\\":\\"row\\",\\"alignItems\\":\\"center\\",\\"marginTop\\":6,\\"borderWidth\\":1,\\"borderRadius\\":4,\\"minHeight\\":40,\\"backgroundColor\\":\\"#f3f4f5\\",\\"borderLeftColor\\":\\"#CBCED1\\",\\"borderTopColor\\":\\"#e1e5e8\\",\\"borderRightColor\\":\\"#e1e5e8\\",\\"borderBottomColor\\":\\"#e1e5e8\\",\\"borderLeftWidth\\":2,\\"opacity\\":1}},\\"children\\":[{\\"type\\":\\"View\\",\\"props\\":{\\"style\\":{\\"flexDirection\\":\\"row\\"}},\\"children\\":[{\\"type\\":\\"View\\",\\"props\\":{\\"style\\":{\\"flex\\":1,\\"borderRadius\\":4,\\"padding\\":8}},\\"children\\":[{\\"type\\":\\"View\\",\\"props\\":{\\"style\\":{\\"flexDirection\\":\\"row\\"}},\\"children\\":[{\\"type\\":\\"Text\\",\\"props\\":{\\"style\\":[{\\"fontSize\\":16,\\"textAlign\\":\\"left\\",\\"backgroundColor\\":\\"transparent\\",\\"fontFamily\\":\\"System\\",\\"fontWeight\\":\\"500\\"},{\\"color\\":\\"#6C727A\\"}]},\\"children\\":[\\"Engineering (9 today)\\"]}]}]},{\\"type\\":\\"View\\",\\"props\\":{\\"style\\":{\\"width\\":20,\\"height\\":20,\\"right\\":8,\\"top\\":8,\\"justifyContent\\":\\"center\\",\\"alignItems\\":\\"center\\"}},\\"children\\":[{\\"type\\":\\"Text\\",\\"props\\":{\\"allowFontScaling\\":false,\\"style\\":[{\\"fontSize\\":22,\\"color\\":\\"#6C727A\\"},null,{\\"fontFamily\\":\\"custom\\",\\"fontWeight\\":\\"normal\\",\\"fontStyle\\":\\"normal\\"},{}]},\\"children\\":[\\"\\"]}]}]}]}]}"`;
|
|
@ -0,0 +1,185 @@
|
|||
import { transparentize } from 'color2k';
|
||||
import { dequal } from 'dequal';
|
||||
import React, { useContext, useState } from 'react';
|
||||
import { StyleSheet, Text, View } from 'react-native';
|
||||
|
||||
import { themes } from '../../../../constants/colors';
|
||||
import { IAttachment } from '../../../../definitions/IAttachment';
|
||||
import { TGetCustomEmoji } from '../../../../definitions/IEmoji';
|
||||
import { CustomIcon } from '../../../../lib/Icons';
|
||||
import { useTheme } from '../../../../theme';
|
||||
import sharedStyles from '../../../../views/Styles';
|
||||
import Markdown from '../../../markdown';
|
||||
import MessageContext from '../../Context';
|
||||
import Touchable from '../../Touchable';
|
||||
import { BUTTON_HIT_SLOP } from '../../utils';
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
button: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
marginTop: 6,
|
||||
borderWidth: 1,
|
||||
borderRadius: 4,
|
||||
minHeight: 40
|
||||
},
|
||||
attachmentContainer: {
|
||||
flex: 1,
|
||||
borderRadius: 4,
|
||||
padding: 8
|
||||
},
|
||||
authorContainer: {
|
||||
flexDirection: 'row'
|
||||
},
|
||||
fieldContainer: {
|
||||
flexDirection: 'column',
|
||||
paddingLeft: 10,
|
||||
paddingTop: 10,
|
||||
paddingBottom: 10
|
||||
},
|
||||
fieldTitle: {
|
||||
fontSize: 15,
|
||||
...sharedStyles.textBold
|
||||
},
|
||||
marginTop: {
|
||||
marginTop: 4
|
||||
},
|
||||
marginBottom: {
|
||||
marginBottom: 4
|
||||
},
|
||||
title: {
|
||||
fontSize: 16,
|
||||
...sharedStyles.textMedium
|
||||
},
|
||||
touchableContainer: {
|
||||
flexDirection: 'row'
|
||||
},
|
||||
markdownFontSize: {
|
||||
fontSize: 15
|
||||
},
|
||||
iconContainer: {
|
||||
width: 20,
|
||||
height: 20,
|
||||
right: 8,
|
||||
top: 8,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center'
|
||||
}
|
||||
});
|
||||
|
||||
interface IMessageFields {
|
||||
attachment: IAttachment;
|
||||
getCustomEmoji: TGetCustomEmoji;
|
||||
}
|
||||
|
||||
interface IMessageReply {
|
||||
attachment: IAttachment;
|
||||
timeFormat?: string;
|
||||
index: number;
|
||||
getCustomEmoji: TGetCustomEmoji;
|
||||
}
|
||||
|
||||
const Fields = React.memo(
|
||||
({ attachment, getCustomEmoji }: IMessageFields) => {
|
||||
if (!attachment.fields) {
|
||||
return null;
|
||||
}
|
||||
const { baseUrl, user } = useContext(MessageContext);
|
||||
const { theme } = useTheme();
|
||||
return (
|
||||
<>
|
||||
{attachment.fields.map(field => (
|
||||
<View key={field.title} style={[styles.fieldContainer, { width: field.short ? '50%' : '100%' }]}>
|
||||
<Text testID='collapsibleQuoteTouchableFieldTitle' style={[styles.fieldTitle, { color: themes[theme].bodyText }]}>
|
||||
{field.title}
|
||||
</Text>
|
||||
<Markdown
|
||||
msg={field?.value || ''}
|
||||
baseUrl={baseUrl}
|
||||
username={user.username}
|
||||
getCustomEmoji={getCustomEmoji}
|
||||
theme={theme}
|
||||
style={[styles.markdownFontSize]}
|
||||
/>
|
||||
</View>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
},
|
||||
(prevProps, nextProps) => dequal(prevProps.attachment.fields, nextProps.attachment.fields)
|
||||
);
|
||||
|
||||
const CollapsibleQuote = React.memo(
|
||||
({ attachment, index, getCustomEmoji }: IMessageReply) => {
|
||||
if (!attachment) {
|
||||
return null;
|
||||
}
|
||||
const [collapsed, setCollapsed] = useState(attachment.collapsed);
|
||||
const { theme } = useTheme();
|
||||
|
||||
const onPress = () => {
|
||||
setCollapsed(!collapsed);
|
||||
};
|
||||
|
||||
let {
|
||||
borderColor,
|
||||
chatComponentBackground: backgroundColor,
|
||||
collapsibleQuoteBorder,
|
||||
collapsibleChevron,
|
||||
headerTintColor
|
||||
} = themes[theme];
|
||||
|
||||
try {
|
||||
if (attachment.color) {
|
||||
backgroundColor = transparentize(attachment.color, 0.8);
|
||||
borderColor = attachment.color;
|
||||
collapsibleQuoteBorder = attachment.color;
|
||||
collapsibleChevron = attachment.color;
|
||||
headerTintColor = headerTintColor;
|
||||
}
|
||||
} catch (e) {
|
||||
// fallback to default
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Touchable
|
||||
testID={`collapsibleQuoteTouchable-${attachment.title}`}
|
||||
onPress={onPress}
|
||||
style={[
|
||||
styles.button,
|
||||
index > 0 && styles.marginTop,
|
||||
attachment.description && styles.marginBottom,
|
||||
{
|
||||
backgroundColor,
|
||||
borderLeftColor: collapsibleQuoteBorder,
|
||||
borderTopColor: borderColor,
|
||||
borderRightColor: borderColor,
|
||||
borderBottomColor: borderColor,
|
||||
borderLeftWidth: 2
|
||||
}
|
||||
]}
|
||||
background={Touchable.Ripple(themes[theme].bannerBackground)}
|
||||
hitSlop={BUTTON_HIT_SLOP}>
|
||||
<View style={styles.touchableContainer}>
|
||||
<View style={styles.attachmentContainer}>
|
||||
<View style={styles.authorContainer}>
|
||||
<Text style={[styles.title, { color: headerTintColor }]}>{attachment.title}</Text>
|
||||
</View>
|
||||
{!collapsed && <Fields attachment={attachment} getCustomEmoji={getCustomEmoji} />}
|
||||
</View>
|
||||
<View style={styles.iconContainer}>
|
||||
<CustomIcon name={!collapsed ? 'chevron-up' : 'chevron-down'} size={22} color={collapsibleChevron} />
|
||||
</View>
|
||||
</View>
|
||||
</Touchable>
|
||||
</>
|
||||
);
|
||||
},
|
||||
(prevProps, nextProps) => dequal(prevProps.attachment, nextProps.attachment)
|
||||
);
|
||||
|
||||
CollapsibleQuote.displayName = 'CollapsibleQuote';
|
||||
Fields.displayName = 'CollapsibleQuoteFields';
|
||||
|
||||
export default CollapsibleQuote;
|
|
@ -39,9 +39,7 @@ const Content = React.memo(
|
|||
const isPreview: any = props.tmid && !props.isThreadRoom;
|
||||
let content = null;
|
||||
|
||||
if (props.tmid && !props.msg) {
|
||||
content = <Text style={[styles.text, { color: themes[props.theme].bodyText }]}>{I18n.t('Sent_an_attachment')}</Text>;
|
||||
} else if (props.isEncrypted) {
|
||||
if (props.isEncrypted) {
|
||||
content = (
|
||||
<Text
|
||||
style={[styles.textInfo, { color: themes[props.theme].auxiliaryText }]}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import React, { useContext } from 'react';
|
||||
import { View } from 'react-native';
|
||||
import { StyleProp, TextStyle, View } from 'react-native';
|
||||
import FastImage from '@rocket.chat/react-native-fast-image';
|
||||
import { dequal } from 'dequal';
|
||||
import { createImageProgress } from 'react-native-image-progress';
|
||||
|
@ -12,9 +12,11 @@ import { formatAttachmentUrl } from '../../lib/utils';
|
|||
import { themes } from '../../constants/colors';
|
||||
import MessageContext from './Context';
|
||||
import { TGetCustomEmoji } from '../../definitions/IEmoji';
|
||||
import { IAttachment } from '../../definitions';
|
||||
|
||||
type TMessageButton = {
|
||||
children: JSX.Element;
|
||||
disabled?: boolean;
|
||||
onPress: Function;
|
||||
theme: string;
|
||||
};
|
||||
|
@ -25,17 +27,23 @@ type TMessageImage = {
|
|||
};
|
||||
|
||||
interface IMessageImage {
|
||||
file: { image_url: string; description?: string };
|
||||
file: IAttachment;
|
||||
imageUrl?: string;
|
||||
showAttachment: Function;
|
||||
showAttachment?: Function;
|
||||
style?: StyleProp<TextStyle>[];
|
||||
isReply?: boolean;
|
||||
theme: string;
|
||||
getCustomEmoji: TGetCustomEmoji;
|
||||
}
|
||||
|
||||
const ImageProgress = createImageProgress(FastImage);
|
||||
|
||||
const Button = React.memo(({ children, onPress, theme }: TMessageButton) => (
|
||||
<Touchable onPress={onPress} style={styles.imageContainer} background={Touchable.Ripple(themes[theme].bannerBackground)}>
|
||||
const Button = React.memo(({ children, onPress, disabled, theme }: TMessageButton) => (
|
||||
<Touchable
|
||||
disabled={disabled}
|
||||
onPress={onPress}
|
||||
style={styles.imageContainer}
|
||||
background={Touchable.Ripple(themes[theme].bannerBackground)}>
|
||||
{children}
|
||||
</Touchable>
|
||||
));
|
||||
|
@ -53,34 +61,41 @@ export const MessageImage = React.memo(({ img, theme }: TMessageImage) => (
|
|||
));
|
||||
|
||||
const ImageContainer = React.memo(
|
||||
({ file, imageUrl, showAttachment, getCustomEmoji, theme }: IMessageImage) => {
|
||||
({ file, imageUrl, showAttachment, getCustomEmoji, style, isReply, theme }: IMessageImage) => {
|
||||
const { baseUrl, user } = useContext(MessageContext);
|
||||
const img = imageUrl || formatAttachmentUrl(file.image_url, user.id, user.token, baseUrl);
|
||||
if (!img) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const onPress = () => showAttachment(file);
|
||||
const onPress = () => {
|
||||
if (!showAttachment) {
|
||||
return;
|
||||
}
|
||||
|
||||
return showAttachment(file);
|
||||
};
|
||||
|
||||
if (file.description) {
|
||||
return (
|
||||
<Button theme={theme} onPress={onPress}>
|
||||
<Button disabled={isReply} theme={theme} onPress={onPress}>
|
||||
<View>
|
||||
<MessageImage img={img} theme={theme} />
|
||||
<Markdown
|
||||
msg={file.description}
|
||||
style={[isReply && style]}
|
||||
baseUrl={baseUrl}
|
||||
username={user.username}
|
||||
getCustomEmoji={getCustomEmoji}
|
||||
theme={theme}
|
||||
/>
|
||||
<MessageImage img={img} theme={theme} />
|
||||
</View>
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Button theme={theme} onPress={onPress}>
|
||||
<Button disabled={isReply} theme={theme} onPress={onPress}>
|
||||
<MessageImage img={img} theme={theme} />
|
||||
</Button>
|
||||
);
|
||||
|
|
|
@ -21,6 +21,9 @@ import { themes } from '../../constants/colors';
|
|||
import { IMessage, IMessageInner, IMessageTouchable } from './interfaces';
|
||||
|
||||
const MessageInner = React.memo((props: IMessageInner) => {
|
||||
const { attachments } = props;
|
||||
const isCollapsible = attachments ? attachments[0] && attachments[0].collapsed : false;
|
||||
|
||||
if (props.type === 'discussion-created') {
|
||||
return (
|
||||
<>
|
||||
|
@ -29,6 +32,7 @@ const MessageInner = React.memo((props: IMessageInner) => {
|
|||
</>
|
||||
);
|
||||
}
|
||||
|
||||
if (props.type === 'jitsi_call_started') {
|
||||
return (
|
||||
<>
|
||||
|
@ -38,6 +42,7 @@ const MessageInner = React.memo((props: IMessageInner) => {
|
|||
</>
|
||||
);
|
||||
}
|
||||
|
||||
if (props.blocks && props.blocks.length) {
|
||||
return (
|
||||
<>
|
||||
|
@ -48,11 +53,22 @@ const MessageInner = React.memo((props: IMessageInner) => {
|
|||
</>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<User {...props} />
|
||||
{isCollapsible ? (
|
||||
<>
|
||||
<Content {...props} />
|
||||
<Attachments {...props} />
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Attachments {...props} />
|
||||
<Content {...props} />
|
||||
</>
|
||||
)}
|
||||
|
||||
<Urls {...props} />
|
||||
<Thread {...props} />
|
||||
<Reactions {...props} />
|
||||
|
@ -87,7 +103,7 @@ const Message = React.memo((props: IMessage) => {
|
|||
<View style={[styles.messageContent, props.isHeader && styles.messageContentWithHeader]}>
|
||||
<MessageInner {...props} />
|
||||
</View>
|
||||
<ReadReceipt isReadReceiptEnabled={props.isReadReceiptEnabled} unread={props.unread} theme={props.theme} />
|
||||
<ReadReceipt isReadReceiptEnabled={props.isReadReceiptEnabled} unread={props.unread || false} theme={props.theme} />
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
import React, { useContext, useState } from 'react';
|
||||
import { StyleSheet, Text, View } from 'react-native';
|
||||
import moment from 'moment';
|
||||
import { transparentize } from 'color2k';
|
||||
import { dequal } from 'dequal';
|
||||
import FastImage from '@rocket.chat/react-native-fast-image';
|
||||
|
||||
|
@ -16,22 +15,24 @@ import { formatAttachmentUrl } from '../../lib/utils';
|
|||
import { IAttachment } from '../../definitions/IAttachment';
|
||||
import { TGetCustomEmoji } from '../../definitions/IEmoji';
|
||||
import RCActivityIndicator from '../ActivityIndicator';
|
||||
import Attachments from './Attachments';
|
||||
import { useTheme } from '../../theme';
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
button: {
|
||||
flex: 1,
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
marginTop: 6,
|
||||
marginVertical: 4,
|
||||
alignSelf: 'flex-start',
|
||||
borderWidth: 1,
|
||||
borderRadius: 4
|
||||
borderLeftWidth: 2
|
||||
},
|
||||
attachmentContainer: {
|
||||
flex: 1,
|
||||
borderRadius: 4,
|
||||
flexDirection: 'column',
|
||||
padding: 15
|
||||
paddingVertical: 4,
|
||||
paddingLeft: 8
|
||||
},
|
||||
backdrop: {
|
||||
...StyleSheet.absoluteFillObject
|
||||
|
@ -39,7 +40,8 @@ const styles = StyleSheet.create({
|
|||
authorContainer: {
|
||||
flex: 1,
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center'
|
||||
alignItems: 'center',
|
||||
marginBottom: 8
|
||||
},
|
||||
author: {
|
||||
flex: 1,
|
||||
|
@ -48,9 +50,8 @@ const styles = StyleSheet.create({
|
|||
},
|
||||
time: {
|
||||
fontSize: 12,
|
||||
marginLeft: 10,
|
||||
...sharedStyles.textRegular,
|
||||
fontWeight: '300'
|
||||
marginLeft: 8,
|
||||
...sharedStyles.textRegular
|
||||
},
|
||||
fieldsContainer: {
|
||||
flex: 1,
|
||||
|
@ -94,7 +95,7 @@ const styles = StyleSheet.create({
|
|||
|
||||
interface IMessageTitle {
|
||||
attachment: IAttachment;
|
||||
timeFormat: string;
|
||||
timeFormat?: string;
|
||||
theme: string;
|
||||
}
|
||||
|
||||
|
@ -112,9 +113,8 @@ interface IMessageFields {
|
|||
|
||||
interface IMessageReply {
|
||||
attachment: IAttachment;
|
||||
timeFormat: string;
|
||||
timeFormat?: string;
|
||||
index: number;
|
||||
theme: string;
|
||||
getCustomEmoji: TGetCustomEmoji;
|
||||
}
|
||||
|
||||
|
@ -123,10 +123,10 @@ const Title = React.memo(({ attachment, timeFormat, theme }: IMessageTitle) => {
|
|||
return (
|
||||
<View style={styles.authorContainer}>
|
||||
{attachment.author_name ? (
|
||||
<Text style={[styles.author, { color: themes[theme].bodyText }]}>{attachment.author_name}</Text>
|
||||
<Text style={[styles.author, { color: themes[theme].auxiliaryTintColor }]}>{attachment.author_name}</Text>
|
||||
) : null}
|
||||
{attachment.title ? <Text style={[styles.title, { color: themes[theme].bodyText }]}>{attachment.title}</Text> : null}
|
||||
{time ? <Text style={[styles.time, { color: themes[theme].auxiliaryText }]}>{time}</Text> : null}
|
||||
{time ? <Text style={[styles.time, { color: themes[theme].auxiliaryTintColor }]}>{time}</Text> : null}
|
||||
</View>
|
||||
);
|
||||
});
|
||||
|
@ -138,7 +138,16 @@ const Description = React.memo(
|
|||
return null;
|
||||
}
|
||||
const { baseUrl, user } = useContext(MessageContext);
|
||||
return <Markdown msg={text} baseUrl={baseUrl} username={user.username} getCustomEmoji={getCustomEmoji} theme={theme} />;
|
||||
return (
|
||||
<Markdown
|
||||
msg={text}
|
||||
style={[{ color: themes[theme].auxiliaryTintColor, fontSize: 14 }]}
|
||||
baseUrl={baseUrl}
|
||||
username={user.username}
|
||||
getCustomEmoji={getCustomEmoji}
|
||||
theme={theme}
|
||||
/>
|
||||
);
|
||||
},
|
||||
(prevProps, nextProps) => {
|
||||
if (prevProps.attachment.text !== nextProps.attachment.text) {
|
||||
|
@ -195,12 +204,14 @@ const Fields = React.memo(
|
|||
);
|
||||
|
||||
const Reply = React.memo(
|
||||
({ attachment, timeFormat, index, getCustomEmoji, theme }: IMessageReply) => {
|
||||
({ attachment, timeFormat, index, getCustomEmoji }: IMessageReply) => {
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
if (!attachment) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const { theme } = useTheme();
|
||||
const { baseUrl, user, jumpToMessage } = useContext(MessageContext);
|
||||
|
||||
const onPress = async () => {
|
||||
|
@ -221,15 +232,10 @@ const Reply = React.memo(
|
|||
openLink(url, theme);
|
||||
};
|
||||
|
||||
let { borderColor, chatComponentBackground: backgroundColor } = themes[theme];
|
||||
try {
|
||||
let { borderColor } = themes[theme];
|
||||
if (attachment.color) {
|
||||
backgroundColor = transparentize(attachment.color, 0.8);
|
||||
borderColor = attachment.color;
|
||||
}
|
||||
} catch (e) {
|
||||
// fallback to default
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
|
@ -240,7 +246,6 @@ const Reply = React.memo(
|
|||
index > 0 && styles.marginTop,
|
||||
attachment.description && styles.marginBottom,
|
||||
{
|
||||
backgroundColor,
|
||||
borderColor
|
||||
}
|
||||
]}
|
||||
|
@ -248,6 +253,13 @@ const Reply = React.memo(
|
|||
disabled={loading}>
|
||||
<View style={styles.attachmentContainer}>
|
||||
<Title attachment={attachment} timeFormat={timeFormat} theme={theme} />
|
||||
<Attachments
|
||||
attachments={attachment.attachments}
|
||||
getCustomEmoji={getCustomEmoji}
|
||||
timeFormat={timeFormat}
|
||||
style={[{ color: themes[theme].auxiliaryTintColor, fontSize: 14, marginBottom: 8 }]}
|
||||
isReply
|
||||
/>
|
||||
<UrlImage image={attachment.thumb_url} />
|
||||
<Description attachment={attachment} getCustomEmoji={getCustomEmoji} theme={theme} />
|
||||
<Fields attachment={attachment} getCustomEmoji={getCustomEmoji} theme={theme} />
|
||||
|
@ -258,7 +270,7 @@ const Reply = React.memo(
|
|||
styles.backdrop,
|
||||
{ backgroundColor: themes[theme].bannerBackground, opacity: themes[theme].attachmentLoadingOpacity }
|
||||
]}></View>
|
||||
<RCActivityIndicator theme={theme} />
|
||||
<RCActivityIndicator />
|
||||
</View>
|
||||
) : null}
|
||||
</View>
|
||||
|
@ -273,7 +285,7 @@ const Reply = React.memo(
|
|||
</>
|
||||
);
|
||||
},
|
||||
(prevProps, nextProps) => dequal(prevProps.attachment, nextProps.attachment) && prevProps.theme === nextProps.theme
|
||||
(prevProps, nextProps) => dequal(prevProps.attachment, nextProps.attachment)
|
||||
);
|
||||
|
||||
Reply.displayName = 'MessageReply';
|
||||
|
|
|
@ -40,7 +40,7 @@ const styles = StyleSheet.create({
|
|||
interface IMessageUser {
|
||||
isHeader?: boolean;
|
||||
hasError?: boolean;
|
||||
useRealName: boolean;
|
||||
useRealName?: boolean;
|
||||
author?: {
|
||||
_id: string;
|
||||
name?: string;
|
||||
|
@ -50,25 +50,26 @@ interface IMessageUser {
|
|||
ts?: Date;
|
||||
timeFormat?: string;
|
||||
theme: string;
|
||||
navToRoomInfo: Function;
|
||||
navToRoomInfo?: Function;
|
||||
type: string;
|
||||
}
|
||||
|
||||
const User = React.memo(
|
||||
({ isHeader, useRealName, author, alias, ts, timeFormat, hasError, theme, navToRoomInfo, type, ...props }: IMessageUser) => {
|
||||
if (isHeader || hasError) {
|
||||
const navParam = {
|
||||
t: 'd',
|
||||
rid: author!._id
|
||||
};
|
||||
const { user } = useContext(MessageContext);
|
||||
const username = (useRealName && author!.name) || author!.username;
|
||||
const username = (useRealName && author?.name) || author?.username;
|
||||
const aliasUsername = alias ? (
|
||||
<Text style={[styles.alias, { color: themes[theme].auxiliaryText }]}> @{username}</Text>
|
||||
) : null;
|
||||
const time = moment(ts).format(timeFormat);
|
||||
const onUserPress = () => navToRoomInfo(navParam);
|
||||
const isDisabled = author!._id === user.id;
|
||||
const onUserPress = () => {
|
||||
navToRoomInfo?.({
|
||||
t: 'd',
|
||||
rid: author?._id
|
||||
});
|
||||
};
|
||||
const isDisabled = author?._id === user.id;
|
||||
|
||||
const textContent = (
|
||||
<>
|
||||
|
@ -96,7 +97,7 @@ const User = React.memo(
|
|||
{textContent}
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
<Text style={[messageStyles.time, { color: themes[theme].auxiliaryText }]}>{time}</Text>
|
||||
<Text style={[messageStyles.time, { color: themes[theme].auxiliaryTintColor }]}>{time}</Text>
|
||||
{hasError && <MessageError hasError={hasError} theme={theme} {...props} />}
|
||||
</View>
|
||||
);
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import React, { useContext, useState } from 'react';
|
||||
import { StyleSheet } from 'react-native';
|
||||
import { StyleProp, StyleSheet, TextStyle } from 'react-native';
|
||||
import { dequal } from 'dequal';
|
||||
|
||||
import Touchable from './Touchable';
|
||||
|
@ -33,13 +33,15 @@ const styles = StyleSheet.create({
|
|||
|
||||
interface IMessageVideo {
|
||||
file: IAttachment;
|
||||
showAttachment: Function;
|
||||
showAttachment?: Function;
|
||||
getCustomEmoji: TGetCustomEmoji;
|
||||
style?: StyleProp<TextStyle>[];
|
||||
isReply?: boolean;
|
||||
theme: string;
|
||||
}
|
||||
|
||||
const Video = React.memo(
|
||||
({ file, showAttachment, getCustomEmoji, theme }: IMessageVideo) => {
|
||||
({ file, showAttachment, getCustomEmoji, style, isReply, theme }: IMessageVideo) => {
|
||||
const { baseUrl, user } = useContext(MessageContext);
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
|
@ -47,7 +49,7 @@ const Video = React.memo(
|
|||
return null;
|
||||
}
|
||||
const onPress = async () => {
|
||||
if (isTypeSupported(file.video_type)) {
|
||||
if (isTypeSupported(file.video_type) && showAttachment) {
|
||||
return showAttachment(file);
|
||||
}
|
||||
|
||||
|
@ -73,23 +75,21 @@ const Video = React.memo(
|
|||
|
||||
return (
|
||||
<>
|
||||
<Touchable
|
||||
onPress={onPress}
|
||||
style={[styles.button, { backgroundColor: themes[theme].videoBackground }]}
|
||||
background={Touchable.Ripple(themes[theme].bannerBackground)}>
|
||||
{loading ? (
|
||||
<RCActivityIndicator theme={theme} />
|
||||
) : (
|
||||
<CustomIcon name='play-filled' size={54} color={themes[theme].buttonText} />
|
||||
)}
|
||||
</Touchable>
|
||||
<Markdown
|
||||
msg={file.description}
|
||||
baseUrl={baseUrl}
|
||||
username={user.username}
|
||||
getCustomEmoji={getCustomEmoji}
|
||||
style={[isReply && style]}
|
||||
theme={theme}
|
||||
/>
|
||||
<Touchable
|
||||
disabled={isReply}
|
||||
onPress={onPress}
|
||||
style={[styles.button, { backgroundColor: themes[theme].videoBackground }]}
|
||||
background={Touchable.Ripple(themes[theme].bannerBackground)}>
|
||||
{loading ? <RCActivityIndicator /> : <CustomIcon name='play-filled' size={54} color={themes[theme].buttonText} />}
|
||||
</Touchable>
|
||||
</>
|
||||
);
|
||||
},
|
||||
|
|
|
@ -10,9 +10,10 @@ import messagesStatus from '../../constants/messagesStatus';
|
|||
import { withTheme } from '../../theme';
|
||||
import openLink from '../../utils/openLink';
|
||||
import { TGetCustomEmoji } from '../../definitions/IEmoji';
|
||||
import { TAnyMessageModel } from '../../definitions';
|
||||
|
||||
interface IMessageContainerProps {
|
||||
item: any;
|
||||
item: TAnyMessageModel;
|
||||
user: {
|
||||
id: string;
|
||||
username: string;
|
||||
|
@ -20,24 +21,16 @@ interface IMessageContainerProps {
|
|||
};
|
||||
msg?: string;
|
||||
rid?: string;
|
||||
timeFormat: string;
|
||||
timeFormat?: string;
|
||||
style?: ViewStyle;
|
||||
archived?: boolean;
|
||||
broadcast?: boolean;
|
||||
previousItem?: {
|
||||
ts: any;
|
||||
u: any;
|
||||
groupable: any;
|
||||
id: string;
|
||||
tmid: string;
|
||||
status: any;
|
||||
};
|
||||
isHeader: boolean;
|
||||
previousItem?: TAnyMessageModel;
|
||||
baseUrl: string;
|
||||
Message_GroupingPeriod?: number;
|
||||
isReadReceiptEnabled?: boolean;
|
||||
isThreadRoom: boolean;
|
||||
useRealName: boolean;
|
||||
useRealName?: boolean;
|
||||
autoTranslateRoom?: boolean;
|
||||
autoTranslateLanguage?: string;
|
||||
status?: number;
|
||||
|
@ -59,11 +52,11 @@ interface IMessageContainerProps {
|
|||
callJitsi?: Function;
|
||||
blockAction?: Function;
|
||||
onAnswerButtonPress?: Function;
|
||||
theme: string;
|
||||
theme?: string;
|
||||
threadBadgeColor?: string;
|
||||
toggleFollowThread?: Function;
|
||||
jumpToMessage?: Function;
|
||||
onPress: Function;
|
||||
onPress?: Function;
|
||||
}
|
||||
|
||||
class MessageContainer extends React.Component<IMessageContainerProps> {
|
||||
|
@ -222,7 +215,7 @@ class MessageContainer extends React.Component<IMessageContainerProps> {
|
|||
this.setState({ isManualUnignored: true });
|
||||
};
|
||||
|
||||
get isHeader() {
|
||||
get isHeader(): boolean {
|
||||
const { item, previousItem, broadcast, Message_GroupingPeriod } = this.props;
|
||||
if (this.hasError || (previousItem && previousItem.status === messagesStatus.ERROR)) {
|
||||
return true;
|
||||
|
@ -230,9 +223,11 @@ class MessageContainer extends React.Component<IMessageContainerProps> {
|
|||
try {
|
||||
if (
|
||||
previousItem &&
|
||||
// @ts-ignore TODO: IMessage vs IMessageFromServer non-sense
|
||||
previousItem.ts.toDateString() === item.ts.toDateString() &&
|
||||
previousItem.u.username === item.u.username &&
|
||||
!(previousItem.groupable === false || item.groupable === false || broadcast === true) &&
|
||||
// @ts-ignore TODO: IMessage vs IMessageFromServer non-sense
|
||||
item.ts - previousItem.ts < Message_GroupingPeriod! * 1000 &&
|
||||
previousItem.tmid === item.tmid
|
||||
) {
|
||||
|
@ -244,7 +239,7 @@ class MessageContainer extends React.Component<IMessageContainerProps> {
|
|||
}
|
||||
}
|
||||
|
||||
get isThreadReply() {
|
||||
get isThreadReply(): boolean {
|
||||
const { item, previousItem, isThreadRoom } = this.props;
|
||||
if (isThreadRoom) {
|
||||
return false;
|
||||
|
@ -255,37 +250,40 @@ class MessageContainer extends React.Component<IMessageContainerProps> {
|
|||
return false;
|
||||
}
|
||||
|
||||
get isThreadSequential() {
|
||||
get isThreadSequential(): boolean {
|
||||
const { item, isThreadRoom } = this.props;
|
||||
if (isThreadRoom) {
|
||||
return false;
|
||||
}
|
||||
return item.tmid;
|
||||
return !!item.tmid;
|
||||
}
|
||||
|
||||
get isEncrypted() {
|
||||
get isEncrypted(): boolean {
|
||||
const { item } = this.props;
|
||||
const { t, e2e } = item;
|
||||
return t === E2E_MESSAGE_TYPE && e2e !== E2E_STATUS.DONE;
|
||||
}
|
||||
|
||||
get isInfo() {
|
||||
get isInfo(): boolean {
|
||||
const { item } = this.props;
|
||||
return SYSTEM_MESSAGES.includes(item.t);
|
||||
return (item.t && SYSTEM_MESSAGES.includes(item.t)) ?? false;
|
||||
}
|
||||
|
||||
get isTemp() {
|
||||
get isTemp(): boolean {
|
||||
const { item } = this.props;
|
||||
return item.status === messagesStatus.TEMP || item.status === messagesStatus.ERROR;
|
||||
}
|
||||
|
||||
get isIgnored() {
|
||||
get isIgnored(): boolean {
|
||||
const { isManualUnignored } = this.state;
|
||||
const { isIgnored } = this.props;
|
||||
return isManualUnignored ? false : isIgnored;
|
||||
if (isManualUnignored) {
|
||||
return false;
|
||||
}
|
||||
return isIgnored ?? false;
|
||||
}
|
||||
|
||||
get hasError() {
|
||||
get hasError(): boolean {
|
||||
const { item } = this.props;
|
||||
return item.status === messagesStatus.ERROR;
|
||||
}
|
||||
|
@ -405,13 +403,12 @@ class MessageContainer extends React.Component<IMessageContainerProps> {
|
|||
rid={rid!}
|
||||
author={u}
|
||||
ts={ts}
|
||||
type={t}
|
||||
type={t as any}
|
||||
attachments={attachments}
|
||||
blocks={blocks}
|
||||
urls={urls}
|
||||
reactions={reactions}
|
||||
alias={alias}
|
||||
/* @ts-ignore*/
|
||||
avatar={avatar}
|
||||
emoji={emoji}
|
||||
timeFormat={timeFormat}
|
||||
|
@ -424,16 +421,19 @@ class MessageContainer extends React.Component<IMessageContainerProps> {
|
|||
role={role}
|
||||
drid={drid}
|
||||
dcount={dcount}
|
||||
// @ts-ignore
|
||||
dlm={dlm}
|
||||
tmid={tmid}
|
||||
tcount={tcount}
|
||||
// @ts-ignore
|
||||
tlm={tlm}
|
||||
tmsg={tmsg}
|
||||
fetchThreadName={fetchThreadName!}
|
||||
// @ts-ignore
|
||||
mentions={mentions}
|
||||
channels={channels}
|
||||
isIgnored={this.isIgnored!}
|
||||
isEdited={editedBy && !!editedBy.username}
|
||||
isIgnored={this.isIgnored}
|
||||
isEdited={(editedBy && !!editedBy.username) ?? false}
|
||||
isHeader={this.isHeader}
|
||||
isThreadReply={this.isThreadReply}
|
||||
isThreadSequential={this.isThreadSequential}
|
||||
|
@ -447,7 +447,7 @@ class MessageContainer extends React.Component<IMessageContainerProps> {
|
|||
navToRoomInfo={navToRoomInfo!}
|
||||
callJitsi={callJitsi!}
|
||||
blockAction={blockAction!}
|
||||
theme={theme}
|
||||
theme={theme as string}
|
||||
highlighted={highlighted!}
|
||||
/>
|
||||
</MessageContext.Provider>
|
||||
|
|
|
@ -1,24 +1,23 @@
|
|||
import { MarkdownAST } from '@rocket.chat/message-parser';
|
||||
import { StyleProp, TextStyle } from 'react-native';
|
||||
|
||||
import { IUserChannel, IUserMention } from '../markdown/interfaces';
|
||||
import { TGetCustomEmoji } from '../../definitions/IEmoji';
|
||||
import { IAttachment } from '../../definitions';
|
||||
|
||||
export type TMessageType = 'discussion-created' | 'jitsi_call_started';
|
||||
|
||||
export interface IMessageAttachments {
|
||||
attachments: any;
|
||||
timeFormat: string;
|
||||
showAttachment: Function;
|
||||
attachments?: IAttachment[];
|
||||
timeFormat?: string;
|
||||
style?: StyleProp<TextStyle>[];
|
||||
isReply?: boolean;
|
||||
showAttachment?: Function;
|
||||
getCustomEmoji: TGetCustomEmoji;
|
||||
theme: string;
|
||||
}
|
||||
|
||||
export interface IMessageAttachedActions {
|
||||
attachment: {
|
||||
actions: [];
|
||||
text: string;
|
||||
};
|
||||
theme: string;
|
||||
attachment: IAttachment;
|
||||
}
|
||||
|
||||
export interface IMessageAvatar {
|
||||
|
@ -66,26 +65,26 @@ export interface IMessageContent {
|
|||
_id: string;
|
||||
isTemp: boolean;
|
||||
isInfo: boolean;
|
||||
tmid: string;
|
||||
tmid?: string;
|
||||
isThreadRoom: boolean;
|
||||
msg: string;
|
||||
md: MarkdownAST;
|
||||
msg?: string;
|
||||
md?: MarkdownAST;
|
||||
theme: string;
|
||||
isEdited: boolean;
|
||||
isEncrypted: boolean;
|
||||
getCustomEmoji: TGetCustomEmoji;
|
||||
channels: IUserChannel[];
|
||||
mentions: IUserMention[];
|
||||
navToRoomInfo: Function;
|
||||
useRealName: boolean;
|
||||
channels?: IUserChannel[];
|
||||
mentions?: IUserMention[];
|
||||
navToRoomInfo?: Function;
|
||||
useRealName?: boolean;
|
||||
isIgnored: boolean;
|
||||
type: string;
|
||||
}
|
||||
|
||||
export interface IMessageDiscussion {
|
||||
msg: string;
|
||||
dcount: number;
|
||||
dlm: Date;
|
||||
msg?: string;
|
||||
dcount?: number;
|
||||
dlm?: Date;
|
||||
theme: string;
|
||||
}
|
||||
|
||||
|
@ -98,10 +97,10 @@ export interface IMessageEmoji {
|
|||
}
|
||||
|
||||
export interface IMessageThread {
|
||||
msg: string;
|
||||
tcount: number;
|
||||
msg?: string;
|
||||
tcount?: number | null;
|
||||
theme: string;
|
||||
tlm: Date;
|
||||
tlm?: Date;
|
||||
isThreadRoom: boolean;
|
||||
id: string;
|
||||
}
|
||||
|
@ -123,8 +122,8 @@ export interface IMessageTouchable {
|
|||
}
|
||||
|
||||
export interface IMessageRepliedThread {
|
||||
tmid: string;
|
||||
tmsg: string;
|
||||
tmid?: string;
|
||||
tmsg?: string;
|
||||
id: string;
|
||||
isHeader: boolean;
|
||||
theme: string;
|
||||
|
@ -154,7 +153,7 @@ export interface IMessage extends IMessageRepliedThread, IMessageInner {
|
|||
style: any;
|
||||
onLongPress: Function;
|
||||
isReadReceiptEnabled: boolean;
|
||||
unread: boolean;
|
||||
unread?: boolean;
|
||||
theme: string;
|
||||
isIgnored: boolean;
|
||||
}
|
||||
|
|
|
@ -3,7 +3,7 @@ import { StyleSheet } from 'react-native';
|
|||
import sharedStyles from '../../views/Styles';
|
||||
import { isTablet } from '../../utils/deviceInfo';
|
||||
|
||||
export default StyleSheet.create<any>({
|
||||
export default StyleSheet.create({
|
||||
root: {
|
||||
flexDirection: 'row'
|
||||
},
|
||||
|
@ -28,7 +28,6 @@ export default StyleSheet.create<any>({
|
|||
},
|
||||
flex: {
|
||||
flexDirection: 'row'
|
||||
// flex: 1
|
||||
},
|
||||
temp: { opacity: 0.3 },
|
||||
marginTop: {
|
||||
|
@ -100,7 +99,6 @@ export default StyleSheet.create<any>({
|
|||
...sharedStyles.textSemibold
|
||||
},
|
||||
imageContainer: {
|
||||
// flex: 1,
|
||||
flexDirection: 'column',
|
||||
borderRadius: 4
|
||||
},
|
||||
|
@ -141,7 +139,6 @@ export default StyleSheet.create<any>({
|
|||
},
|
||||
repliedThread: {
|
||||
flexDirection: 'row',
|
||||
// flex: 1,
|
||||
alignItems: 'center',
|
||||
marginTop: 6,
|
||||
marginBottom: 12
|
||||
|
|
|
@ -2,9 +2,12 @@ import { TMessageModel } from '../../definitions/IMessage';
|
|||
import I18n from '../../i18n';
|
||||
import { DISCUSSION } from './constants';
|
||||
|
||||
export const formatMessageCount = (count: number, type: string) => {
|
||||
export const formatMessageCount = (count?: number, type?: string): string => {
|
||||
const discussion = type === DISCUSSION;
|
||||
let text = discussion ? I18n.t('No_messages_yet') : null;
|
||||
if (!count) {
|
||||
return text;
|
||||
}
|
||||
if (count === 1) {
|
||||
text = `${count} ${discussion ? I18n.t('message') : I18n.t('reply')}`;
|
||||
} else if (count > 1 && count < 1000) {
|
||||
|
@ -170,10 +173,10 @@ export const getInfoMessage = ({ type, role, msg, author }: TInfoMessage): strin
|
|||
return I18n.t('This_room_encryption_has_been_enabled_by__username_', { username });
|
||||
}
|
||||
if (type === 'removed-user-from-team') {
|
||||
return I18n.t('Removed__username__from_team', { user_removed: username });
|
||||
return I18n.t('Removed__username__from_team', { user_removed: msg });
|
||||
}
|
||||
if (type === 'added-user-to-team') {
|
||||
return I18n.t('Added__username__to_team', { user_added: username });
|
||||
return I18n.t('Added__username__to_team', { user_added: msg });
|
||||
}
|
||||
if (type === 'user-added-room-to-team') {
|
||||
return I18n.t('added__roomName__to_team', { roomName: msg });
|
||||
|
|
|
@ -1,20 +1,25 @@
|
|||
import { IUser } from './IUser';
|
||||
|
||||
export interface IAttachment {
|
||||
ts: Date;
|
||||
ts?: string | Date;
|
||||
title: string;
|
||||
type: string;
|
||||
description: string;
|
||||
type?: string;
|
||||
description?: string;
|
||||
title_link?: string;
|
||||
image_url?: string;
|
||||
image_type?: string;
|
||||
video_url?: string;
|
||||
video_type?: string;
|
||||
audio_url?: string;
|
||||
title_link_download?: boolean;
|
||||
attachments?: IAttachment[];
|
||||
fields?: IAttachment[];
|
||||
image_dimensions?: { width?: number; height?: number };
|
||||
image_preview?: string;
|
||||
image_size?: number;
|
||||
author_name?: string;
|
||||
author_icon?: string;
|
||||
actions?: [];
|
||||
message_link?: string;
|
||||
text?: string;
|
||||
short?: boolean;
|
||||
|
@ -22,4 +27,32 @@ export interface IAttachment {
|
|||
author_link?: string;
|
||||
color?: string;
|
||||
thumb_url?: string;
|
||||
collapsed?: boolean;
|
||||
}
|
||||
|
||||
export interface IServerAttachment {
|
||||
_id: string;
|
||||
name: string;
|
||||
size: number;
|
||||
type: string;
|
||||
rid: string;
|
||||
userId: string;
|
||||
AmazonS3: { path: string };
|
||||
store: string;
|
||||
identify: {
|
||||
format: string;
|
||||
size: {
|
||||
width: number;
|
||||
height: number;
|
||||
};
|
||||
};
|
||||
complete: boolean;
|
||||
etag: string;
|
||||
path: string;
|
||||
progress: boolean;
|
||||
token: string;
|
||||
uploadedAt: string | Date;
|
||||
uploading: boolean;
|
||||
url: string;
|
||||
user: Pick<IUser, '_id' | 'username' | 'name'>;
|
||||
}
|
||||
|
|
|
@ -0,0 +1,21 @@
|
|||
import { AppleAuthenticationFullName } from 'expo-apple-authentication';
|
||||
|
||||
export interface ICredentials {
|
||||
resume?: string;
|
||||
user?: string;
|
||||
password?: string;
|
||||
username?: string;
|
||||
ldapPass?: string;
|
||||
ldap?: boolean;
|
||||
ldapOptions?: object;
|
||||
crowdPassword?: string;
|
||||
crowd?: boolean;
|
||||
code?: string;
|
||||
totp?: {
|
||||
login: ICredentials;
|
||||
code: string;
|
||||
};
|
||||
fullName?: AppleAuthenticationFullName | null;
|
||||
email?: string | null;
|
||||
identityToken?: string | null;
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
export interface IDDPMessage {
|
||||
msg: string;
|
||||
fields: {
|
||||
eventName: string;
|
||||
args: any;
|
||||
};
|
||||
}
|
|
@ -21,4 +21,10 @@ export interface IEmojiCategory {
|
|||
tabLabel: string;
|
||||
}
|
||||
|
||||
export type TGetCustomEmoji = (name: string) => IEmoji | null;
|
||||
// TODO: copied from reducers/customEmojis. We can unify later.
|
||||
export interface IReduxEmoji {
|
||||
name: string;
|
||||
extension: any;
|
||||
}
|
||||
|
||||
export type TGetCustomEmoji = (name: string) => any;
|
||||
|
|
|
@ -0,0 +1,40 @@
|
|||
import { TextInput } from 'react-native';
|
||||
|
||||
import { ILivechatVisitor } from './ILivechatVisitor';
|
||||
import { ISubscription } from './ISubscription';
|
||||
|
||||
export interface ITitle {
|
||||
title: string;
|
||||
theme: string;
|
||||
}
|
||||
|
||||
export interface IInputs {
|
||||
livechatData: {
|
||||
[key: string]: any;
|
||||
};
|
||||
name: string;
|
||||
email: string;
|
||||
phone?: string;
|
||||
topic: string;
|
||||
tag: string[];
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
export type TParams = ILivechatVisitor & IInputs;
|
||||
|
||||
export interface ILivechat extends ISubscription {
|
||||
// Param dynamic depends on server
|
||||
sms?: string;
|
||||
}
|
||||
|
||||
export interface IInputsRefs {
|
||||
[index: string]: TextInput | null;
|
||||
name: TextInput | null;
|
||||
phone: TextInput | null;
|
||||
topic: TextInput | null;
|
||||
}
|
||||
|
||||
export interface ICustomFields {
|
||||
visitor?: { [key: string]: string };
|
||||
livechat?: { [key: string]: string };
|
||||
}
|
|
@ -32,6 +32,8 @@ export interface ILivechatVisitor extends IRocketChatRecord {
|
|||
ip?: string;
|
||||
host?: string;
|
||||
visitorEmails?: IVisitorEmail[];
|
||||
livechatData?: any;
|
||||
utc?: number;
|
||||
}
|
||||
|
||||
export interface ILivechatVisitorDTO {
|
||||
|
|
|
@ -1,13 +1,21 @@
|
|||
import Model from '@nozbe/watermelondb/Model';
|
||||
|
||||
import { IUserEmail, IUserSettings } from './IUser';
|
||||
import { UserStatus } from './UserStatus';
|
||||
|
||||
export interface ILoggedUser {
|
||||
id: string;
|
||||
token: string;
|
||||
username: string;
|
||||
name: string;
|
||||
language?: string;
|
||||
status: string;
|
||||
status: UserStatus;
|
||||
statusText?: string;
|
||||
customFields?: {
|
||||
[key: string]: any;
|
||||
};
|
||||
statusLivechat?: string;
|
||||
emails?: IUserEmail[];
|
||||
roles?: string[];
|
||||
avatarETag?: string;
|
||||
showMessageInMainThread?: boolean;
|
||||
|
@ -15,4 +23,16 @@ export interface ILoggedUser {
|
|||
enableMessageParserEarlyAdoption?: boolean;
|
||||
}
|
||||
|
||||
export interface ILoggedUserResultFromServer
|
||||
extends Omit<ILoggedUser, 'enableMessageParserEarlyAdoption' | 'showMessageInMainThread'> {
|
||||
settings: IUserSettings;
|
||||
}
|
||||
|
||||
export interface ILoginResultFromServer {
|
||||
status: string;
|
||||
authToken: string;
|
||||
userId: string;
|
||||
me: ILoggedUserResultFromServer;
|
||||
}
|
||||
|
||||
export type TLoggedUserModel = ILoggedUser & Model;
|
||||
|
|
|
@ -1,11 +1,14 @@
|
|||
import Model from '@nozbe/watermelondb/Model';
|
||||
import { MarkdownAST } from '@rocket.chat/message-parser';
|
||||
|
||||
import { MessageTypeLoad } from '../constants/messageTypeLoad';
|
||||
import { IAttachment } from './IAttachment';
|
||||
import { IReaction } from './IReaction';
|
||||
import { IUrl } from './IUrl';
|
||||
import { TThreadMessageModel } from './IThreadMessage';
|
||||
import { TThreadModel } from './IThread';
|
||||
import { IUrlFromServer } from './IUrl';
|
||||
|
||||
export type MessageType = 'jitsi_call_started' | 'discussion-created' | 'e2e' | 'load_more' | 'rm' | 'uj';
|
||||
export type MessageType = 'jitsi_call_started' | 'discussion-created' | 'e2e' | 'load_more' | 'rm' | 'uj' | MessageTypeLoad;
|
||||
|
||||
export interface IUserMessage {
|
||||
_id: string;
|
||||
|
@ -41,64 +44,116 @@ export type E2EType = 'pending' | 'done';
|
|||
export interface ILastMessage {
|
||||
_id: string;
|
||||
rid: string;
|
||||
tshow: boolean;
|
||||
t: MessageType;
|
||||
tmid: string;
|
||||
msg: string;
|
||||
e2e: E2EType;
|
||||
ts: Date;
|
||||
tshow?: boolean;
|
||||
t?: MessageType;
|
||||
tmid?: string;
|
||||
msg?: string;
|
||||
e2e?: E2EType;
|
||||
ts: string | Date;
|
||||
u: IUserMessage;
|
||||
_updatedAt: Date;
|
||||
urls: string[];
|
||||
mentions: IUserMention[];
|
||||
channels: IUserChannel[];
|
||||
md: MarkdownAST;
|
||||
attachments: IAttachment[];
|
||||
reactions: IReaction[];
|
||||
unread: boolean;
|
||||
status: boolean;
|
||||
_updatedAt: string | Date;
|
||||
urls?: IUrlFromServer[];
|
||||
mentions?: IUserMention[];
|
||||
channels?: IUserChannel[];
|
||||
md?: MarkdownAST;
|
||||
attachments?: IAttachment[];
|
||||
reactions?: IReaction[];
|
||||
unread?: boolean;
|
||||
status?: number;
|
||||
}
|
||||
|
||||
export interface IMessage {
|
||||
interface IMessageFile {
|
||||
_id: string;
|
||||
name: string;
|
||||
type: string;
|
||||
}
|
||||
|
||||
export interface IMessageFromServer {
|
||||
_id: string;
|
||||
rid: string;
|
||||
msg?: string;
|
||||
id?: string;
|
||||
t?: MessageType;
|
||||
ts: string | Date;
|
||||
ts: string | Date; // wm date issue
|
||||
u: IUserMessage;
|
||||
_updatedAt: string | Date;
|
||||
urls?: IUrlFromServer[];
|
||||
mentions?: IUserMention[];
|
||||
channels?: IUserChannel[];
|
||||
md?: MarkdownAST;
|
||||
file?: IMessageFile;
|
||||
files?: IMessageFile[];
|
||||
groupable?: boolean;
|
||||
attachments?: IAttachment[];
|
||||
t?: MessageType;
|
||||
drid?: string;
|
||||
dcount?: number;
|
||||
dml: string | Date;
|
||||
starred?:
|
||||
| {
|
||||
_id: string;
|
||||
}
|
||||
| boolean;
|
||||
pinned?: boolean;
|
||||
pinnedAt?: string | Date;
|
||||
pinnedBy?: {
|
||||
_id: string;
|
||||
username: string;
|
||||
};
|
||||
score?: number;
|
||||
}
|
||||
|
||||
export interface ILoadMoreMessage {
|
||||
_id: string;
|
||||
rid: string;
|
||||
ts: string;
|
||||
t: string;
|
||||
msg: string;
|
||||
}
|
||||
|
||||
export interface IMessage extends IMessageFromServer {
|
||||
id: string;
|
||||
t?: MessageType;
|
||||
alias?: string;
|
||||
parseUrls?: boolean;
|
||||
groupable?: boolean;
|
||||
avatar?: string;
|
||||
emoji?: string;
|
||||
attachments?: IAttachment[];
|
||||
urls?: IUrl[];
|
||||
_updatedAt: Date;
|
||||
status?: number;
|
||||
pinned?: boolean;
|
||||
starred?: boolean;
|
||||
starred?:
|
||||
| {
|
||||
_id: string;
|
||||
}
|
||||
| boolean;
|
||||
editedBy?: IEditedBy;
|
||||
reactions?: IReaction[];
|
||||
role?: string;
|
||||
drid?: string;
|
||||
dcount?: number;
|
||||
dlm?: Date;
|
||||
dlm?: string | Date;
|
||||
tmid?: string;
|
||||
tcount?: number;
|
||||
tlm?: Date;
|
||||
tcount?: number | null;
|
||||
tlm?: string | Date | null;
|
||||
replies?: string[];
|
||||
mentions?: IUserMention[];
|
||||
channels?: IUserChannel[];
|
||||
unread?: boolean;
|
||||
autoTranslate?: boolean;
|
||||
translations?: ITranslations[];
|
||||
tmsg?: string;
|
||||
blocks?: any;
|
||||
e2e?: string;
|
||||
e2e?: E2EType;
|
||||
tshow?: boolean;
|
||||
md?: MarkdownAST;
|
||||
subscription?: { id: string };
|
||||
}
|
||||
|
||||
export type TMessageModel = IMessage & Model;
|
||||
|
||||
export type TAnyMessageModel = TMessageModel | TThreadModel | TThreadMessageModel;
|
||||
export type TTypeMessages = IMessageFromServer | ILoadMoreMessage | IMessage;
|
||||
|
||||
// Read receipts to ReadReceiptView and chat.getMessageReadReceipts
|
||||
export interface IReadReceipts {
|
||||
_id: string;
|
||||
roomId: string;
|
||||
userId: string;
|
||||
messageId: string;
|
||||
ts: string;
|
||||
user?: IUserMessage;
|
||||
}
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue