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:
Diego Mello 2022-03-28 15:14:26 -03:00 committed by GitHub
parent bd4d48528d
commit ed3868eeb1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
349 changed files with 7644 additions and 5055 deletions

1
.gitignore vendored
View File

@ -64,5 +64,6 @@ artifacts
.vscode/ .vscode/
e2e/docker/rc_test_env/docker-compose.yml e2e/docker/rc_test_env/docker-compose.yml
e2e/docker/data/db e2e/docker/data/db
e2e/e2e_account.js
*.p8 *.p8

View File

@ -20,6 +20,26 @@ jest.mock('react-native-file-viewer', () => ({
jest.mock('../app/lib/database', () => jest.fn(() => null)); jest.mock('../app/lib/database', () => jest.fn(() => null));
global.Date.now = jest.fn(() => new Date('2019-10-10').getTime()); 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(); const converter = new Stories2SnapsConverter();
initStoryshots({ initStoryshots({

View File

@ -144,7 +144,7 @@ android {
minSdkVersion rootProject.ext.minSdkVersion minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion
versionCode VERSIONCODE as Integer versionCode VERSIONCODE as Integer
versionName "4.25.0" versionName "4.26.0"
vectorDrawables.useSupportLibrary = true vectorDrawables.useSupportLibrary = true
if (!isFoss) { if (!isFoss) {
manifestPlaceholders = [BugsnagAPIKey: BugsnagAPIKey as String] manifestPlaceholders = [BugsnagAPIKey: BugsnagAPIKey as String]

View File

@ -12,7 +12,6 @@ import com.facebook.soloader.SoLoader;
import com.reactnativecommunity.viewpager.RNCViewPagerPackage; import com.reactnativecommunity.viewpager.RNCViewPagerPackage;
import com.facebook.react.bridge.JSIModulePackage; import com.facebook.react.bridge.JSIModulePackage;
import com.swmansion.reanimated.ReanimatedJSIModulePackage; import com.swmansion.reanimated.ReanimatedJSIModulePackage;
import org.unimodules.adapters.react.ModuleRegistryAdapter; import org.unimodules.adapters.react.ModuleRegistryAdapter;
import org.unimodules.adapters.react.ReactModuleRegistryProvider; import org.unimodules.adapters.react.ReactModuleRegistryProvider;
@ -54,7 +53,7 @@ public class MainApplication extends Application implements ReactApplication {
@Override @Override
protected JSIModulePackage getJSIModulePackage() { protected JSIModulePackage getJSIModulePackage() {
return new ReanimatedJSIModulePackage(); // <- add return new ReanimatedJSIModulePackage();
} }
@Override @Override

View File

@ -53,17 +53,9 @@ public class Ejson {
String alias = Utils.toHex("com.MMKV.default"); String alias = Utils.toHex("com.MMKV.default");
// Retrieve container password // Retrieve container password
secureKeystore.getSecureKey(alias, new RNCallback() { String password = secureKeystore.getSecureKey(alias);
@Override
public void invoke(Object... args) {
String error = (String) args[0];
if (error == null) {
String password = (String) args[1];
mmkv = MMKV.mmkvWithID("default", MMKV.SINGLE_PROCESS_MODE, password); mmkv = MMKV.mmkvWithID("default", MMKV.SINGLE_PROCESS_MODE, password);
} }
}
});
}
public String getAvatarUri() { public String getAvatarUri() {
if (type == null) { if (type == null) {

View File

@ -11,7 +11,7 @@ function createRequestTypes(base = {}, types = defaultTypes): Record<string, str
// Login events // Login events
export const LOGIN = createRequestTypes('LOGIN', [...defaultTypes, 'SET_SERVICES', 'SET_PREFERENCE', 'SET_LOCAL_AUTHENTICATED']); 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 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', [ export const ROOMS = createRequestTypes('ROOMS', [
...defaultTypes, ...defaultTypes,
'REFRESH', 'REFRESH',

View File

@ -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 { export function setLoginServices(data: Record<string, any>): ISetServices {
return { return {
type: types.LOGIN.SET_SERVICES, type: types.LOGIN.SET_SERVICES,

View File

@ -4,7 +4,7 @@ import { ERoomType } from '../definitions/ERoomType';
import { ROOM } from './actionsTypes'; import { ROOM } from './actionsTypes';
// TYPE RETURN RELATED // TYPE RETURN RELATED
type ISelected = Record<string, string>; type ISelected = string[];
export interface ITransferData { export interface ITransferData {
roomId: string; roomId: string;

View File

@ -1,26 +1,26 @@
import { Action } from 'redux'; import { Action } from 'redux';
import { ISettings, TSettings } from '../reducers/settings'; import { TSettingsState, TSupportedSettings, TSettingsValues } from '../reducers/settings';
import { SETTINGS } from './actionsTypes'; import { SETTINGS } from './actionsTypes';
interface IAddSettings extends Action { interface IAddSettings extends Action {
payload: ISettings; payload: TSettingsState;
} }
interface IUpdateSettings extends Action { interface IUpdateSettings extends Action {
payload: { id: string; value: TSettings }; payload: { id: TSupportedSettings; value: TSettingsValues };
} }
export type IActionSettings = IAddSettings & IUpdateSettings; export type IActionSettings = IAddSettings & IUpdateSettings;
export function addSettings(settings: ISettings): IAddSettings { export function addSettings(settings: TSettingsState): IAddSettings {
return { return {
type: SETTINGS.ADD, type: SETTINGS.ADD,
payload: settings payload: settings
}; };
} }
export function updateSettings(id: string, value: TSettings): IUpdateSettings { export function updateSettings(id: TSupportedSettings, value: TSettingsValues): IUpdateSettings {
return { return {
type: SETTINGS.UPDATE, type: SETTINGS.UPDATE,
payload: { id, value } payload: { id, value }

View File

@ -143,8 +143,8 @@ export const deleteKeyCommands = (): void => KeyCommands.deleteKeyCommands(keyCo
export const KEY_COMMAND = 'KEY_COMMAND'; export const KEY_COMMAND = 'KEY_COMMAND';
interface IKeyCommandEvent extends NativeSyntheticEvent<typeof KeyCommand> { export interface IKeyCommandEvent extends NativeSyntheticEvent<typeof KeyCommand> {
input: string; input: number & string;
modifierFlags: string | number; modifierFlags: string | number;
} }

View File

@ -37,6 +37,7 @@ export const themes: any = {
infoText: '#6d6d72', infoText: '#6d6d72',
tintColor: '#1d74f5', tintColor: '#1d74f5',
tintActive: '#549df9', tintActive: '#549df9',
tintDisabled: '#88B4F5',
auxiliaryTintColor: '#6C727A', auxiliaryTintColor: '#6C727A',
actionTintColor: '#1d74f5', actionTintColor: '#1d74f5',
separatorColor: '#cbcbcc', separatorColor: '#cbcbcc',
@ -66,6 +67,8 @@ export const themes: any = {
previewTintColor: '#ffffff', previewTintColor: '#ffffff',
backdropOpacity: 0.3, backdropOpacity: 0.3,
attachmentLoadingOpacity: 0.7, attachmentLoadingOpacity: 0.7,
collapsibleQuoteBorder: '#CBCED1',
collapsibleChevron: '#6C727A',
...mentions ...mentions
}, },
dark: { dark: {
@ -85,6 +88,7 @@ export const themes: any = {
infoText: '#6D6D72', infoText: '#6D6D72',
tintColor: '#1d74f5', tintColor: '#1d74f5',
tintActive: '#549df9', tintActive: '#549df9',
tintDisabled: '#88B4F5',
auxiliaryTintColor: '#f9f9f9', auxiliaryTintColor: '#f9f9f9',
actionTintColor: '#1d74f5', actionTintColor: '#1d74f5',
separatorColor: '#2b2b2d', separatorColor: '#2b2b2d',
@ -114,6 +118,8 @@ export const themes: any = {
previewTintColor: '#ffffff', previewTintColor: '#ffffff',
backdropOpacity: 0.9, backdropOpacity: 0.9,
attachmentLoadingOpacity: 0.3, attachmentLoadingOpacity: 0.3,
collapsibleQuoteBorder: '#CBCED1',
collapsibleChevron: '#6C727A',
...mentions ...mentions
}, },
black: { black: {
@ -133,6 +139,7 @@ export const themes: any = {
infoText: '#6d6d72', infoText: '#6d6d72',
tintColor: '#1e9bfe', tintColor: '#1e9bfe',
tintActive: '#76b7fc', tintActive: '#76b7fc',
tintDisabled: '#88B4F5', // TODO: Evaluate this with design team
auxiliaryTintColor: '#f9f9f9', auxiliaryTintColor: '#f9f9f9',
actionTintColor: '#1e9bfe', actionTintColor: '#1e9bfe',
separatorColor: '#272728', separatorColor: '#272728',
@ -162,6 +169,8 @@ export const themes: any = {
previewTintColor: '#ffffff', previewTintColor: '#ffffff',
backdropOpacity: 0.9, backdropOpacity: 0.9,
attachmentLoadingOpacity: 0.3, attachmentLoadingOpacity: 0.3,
collapsibleQuoteBorder: '#CBCED1',
collapsibleChevron: '#6C727A',
...mentions ...mentions
} }
}; };

View File

@ -1,5 +1,7 @@
export const MESSAGE_TYPE_LOAD_MORE = 'load_more'; export enum MessageTypeLoad {
export const MESSAGE_TYPE_LOAD_PREVIOUS_CHUNK = 'load_previous_chunk'; MORE = 'load_more',
export const MESSAGE_TYPE_LOAD_NEXT_CHUNK = 'load_next_chunk'; 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];

View File

@ -206,4 +206,4 @@ export default {
Canned_Responses_Enable: { Canned_Responses_Enable: {
type: 'valueAsBoolean' type: 'valueAsBoolean'
} }
}; } as const;

View File

@ -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 React, { forwardRef, isValidElement, useEffect, useImperativeHandle, useRef, useState } from 'react';
import { Keyboard, Text } from 'react-native'; 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 { useSafeAreaInsets } from 'react-native-safe-area-context';
import { State, TapGestureHandler } from 'react-native-gesture-handler';
import ScrollBottomSheet from 'react-native-scroll-bottom-sheet'; 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 { 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 { isIOS, isTablet } from '../../utils/deviceInfo';
import * as List from '../List'; import * as List from '../List';
import I18n from '../../i18n'; import { Button } from './Button';
import { IDimensionsContextProps, useDimensions, useOrientation } from '../../dimensions'; import { Handle } from './Handle';
import { IActionSheetItem, Item } from './Item';
import { TActionSheetOptions, TActionSheetOptionsItem } from './Provider';
import styles, { ITEM_HEIGHT } from './styles';
interface IActionSheetData { const getItemLayout = (data: TActionSheetOptionsItem[] | null | undefined, index: number) => ({
options: any; length: ITEM_HEIGHT,
headerHeight?: number; offset: ITEM_HEIGHT * index,
hasCancel?: boolean; index
customHeader: any; });
}
const getItemLayout = (data: any, index: number) => ({ length: ITEM_HEIGHT, offset: ITEM_HEIGHT * index, index });
const HANDLE_HEIGHT = isIOS ? 40 : 56; const HANDLE_HEIGHT = isIOS ? 40 : 56;
const MAX_SNAP_HEIGHT = 16; const MAX_SNAP_HEIGHT = 16;
@ -39,16 +38,17 @@ const ANIMATION_CONFIG = {
}; };
const ActionSheet = React.memo( const ActionSheet = React.memo(
forwardRef(({ children, theme }: { children: JSX.Element; theme: string }, ref) => { forwardRef(({ children }: { children: React.ReactElement }, ref) => {
const bottomSheetRef: any = useRef(); const { theme } = useTheme();
const [data, setData] = useState<IActionSheetData>({} as IActionSheetData); const bottomSheetRef = useRef<ScrollBottomSheet<TActionSheetOptionsItem>>(null);
const [data, setData] = useState<TActionSheetOptions>({} as TActionSheetOptions);
const [isVisible, setVisible] = useState(false); const [isVisible, setVisible] = useState(false);
const { height }: Partial<IDimensionsContextProps> = useDimensions(); const { height } = useDimensions();
const { isLandscape } = useOrientation(); const { isLandscape } = useOrientation();
const insets = useSafeAreaInsets(); const insets = useSafeAreaInsets();
const maxSnap = Math.max( const maxSnap = Math.max(
height! - height -
// Items height // Items height
ITEM_HEIGHT * (data?.options?.length || 0) - ITEM_HEIGHT * (data?.options?.length || 0) -
// Handle height // Handle height
@ -69,7 +69,7 @@ const ActionSheet = React.memo(
* we'll provide more one snap * we'll provide more one snap
* that point 50% of the whole screen * 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 openedSnapIndex = snaps.length > 2 ? 1 : 0;
const closedSnapIndex = snaps.length - 1; const closedSnapIndex = snaps.length - 1;
@ -79,12 +79,12 @@ const ActionSheet = React.memo(
bottomSheetRef.current?.snapTo(closedSnapIndex); bottomSheetRef.current?.snapTo(closedSnapIndex);
}; };
const show = (options: any) => { const show = (options: TActionSheetOptions) => {
setData(options); setData(options);
toggleVisible(); toggleVisible();
}; };
const onBackdropPressed = ({ nativeEvent }: any) => { const onBackdropPressed = ({ nativeEvent }: { nativeEvent: HandlerStateChangeEventPayload }) => {
if (nativeEvent.oldState === State.ACTIVE) { if (nativeEvent.oldState === State.ACTIVE) {
hide(); hide();
} }
@ -117,7 +117,7 @@ const ActionSheet = React.memo(
const renderHandle = () => ( const renderHandle = () => (
<> <>
<Handle theme={theme} /> <Handle />
{isValidElement(data?.customHeader) ? data.customHeader : null} {isValidElement(data?.customHeader) ? data.customHeader : null}
</> </>
); );
@ -127,21 +127,23 @@ const ActionSheet = React.memo(
<Button <Button
onPress={hide} onPress={hide}
style={[styles.button, { backgroundColor: themes[theme].auxiliaryBackground }]} style={[styles.button, { backgroundColor: themes[theme].auxiliaryBackground }]}
// TODO: Remove when migrate Touch
theme={theme} theme={theme}
accessibilityLabel={I18n.t('Cancel')}> accessibilityLabel={I18n.t('Cancel')}>
<Text style={[styles.text, { color: themes[theme].bodyText }]}>{I18n.t('Cancel')}</Text> <Text style={[styles.text, { color: themes[theme].bodyText }]}>{I18n.t('Cancel')}</Text>
</Button> </Button>
) : null; ) : 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)); 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, { const opacity = interpolateNode(animatedPosition.current, {
inputRange: [0, 1], inputRange: [0, 1],
outputRange: [0, themes[theme].backdropOpacity], outputRange: [0, themes[theme].backdropOpacity],
extrapolate: Extrapolate.CLAMP 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 ( return (
<> <>
@ -160,7 +162,7 @@ const ActionSheet = React.memo(
]} ]}
/> />
</TapGestureHandler> </TapGestureHandler>
<ScrollBottomSheet <ScrollBottomSheet<TActionSheetOptionsItem>
testID='action-sheet' testID='action-sheet'
ref={bottomSheetRef} ref={bottomSheetRef}
componentType='FlatList' componentType='FlatList'
@ -169,18 +171,11 @@ const ActionSheet = React.memo(
renderHandle={renderHandle} renderHandle={renderHandle}
onSettle={index => index === closedSnapIndex && toggleVisible()} onSettle={index => index === closedSnapIndex && toggleVisible()}
animatedPosition={animatedPosition.current} animatedPosition={animatedPosition.current}
containerStyle={ containerStyle={{ ...styles.container, ...bottomSheet, backgroundColor: themes[theme].focusedBackground }}
[
styles.container,
{ backgroundColor: themes[theme].focusedBackground },
(isLandscape || isTablet) && styles.bottomSheet
] as any
}
animationConfig={ANIMATION_CONFIG} animationConfig={ANIMATION_CONFIG}
// FlatList props data={data.options}
data={data?.options}
renderItem={renderItem} renderItem={renderItem}
keyExtractor={(item: any) => item.title} keyExtractor={item => item.title}
style={{ backgroundColor: themes[theme].focusedBackground }} style={{ backgroundColor: themes[theme].focusedBackground }}
contentContainerStyle={styles.content} contentContainerStyle={styles.content}
ItemSeparatorComponent={List.Separator} ItemSeparatorComponent={List.Separator}

View File

@ -3,9 +3,13 @@ import { View } from 'react-native';
import styles from './styles'; import styles from './styles';
import { themes } from '../../constants/colors'; 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.handle, { backgroundColor: themes[theme].focusedBackground }]} testID='action-sheet-handle'>
<View style={[styles.handleIndicator, { backgroundColor: themes[theme].auxiliaryText }]} /> <View style={[styles.handleIndicator, { backgroundColor: themes[theme].auxiliaryText }]} />
</View> </View>
)); );
});

View File

@ -3,23 +3,24 @@ import { Text, View } from 'react-native';
import { themes } from '../../constants/colors'; import { themes } from '../../constants/colors';
import { CustomIcon } from '../../lib/Icons'; import { CustomIcon } from '../../lib/Icons';
import { useTheme } from '../../theme';
import { Button } from './Button'; import { Button } from './Button';
import styles from './styles'; import styles from './styles';
interface IActionSheetItem { export interface IActionSheetItem {
item: { item: {
title: string; title: string;
icon: string; icon: string;
danger: boolean; danger?: boolean;
testID: string; testID?: string;
onPress(): void; onPress: () => void;
right: Function; right?: Function;
}; };
theme: string;
hide(): void; hide(): void;
} }
export const Item = React.memo(({ item, hide, theme }: IActionSheetItem) => { export const Item = React.memo(({ item, hide }: IActionSheetItem) => {
const { theme } = useTheme();
const onPress = () => { const onPress = () => {
hide(); hide();
item?.onPress(); item?.onPress();

View File

@ -1,14 +1,21 @@
import React, { ForwardedRef, forwardRef, useContext, useRef } from 'react'; import React, { ForwardedRef, forwardRef, useContext, useRef } from 'react';
import ActionSheet from './ActionSheet'; 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 { interface IActionSheetProvider {
Provider: any; showActionSheet: (item: TActionSheetOptions) => void;
Consumer: any; hideActionSheet: () => void;
} }
const context: IActionSheetProvider = React.createContext({ const context = React.createContext<IActionSheetProvider>({
showActionSheet: () => {}, showActionSheet: () => {},
hideActionSheet: () => {} hideActionSheet: () => {}
}); });
@ -17,17 +24,16 @@ export const useActionSheet = () => useContext(context);
const { Provider, Consumer } = context; const { Provider, Consumer } = context;
export const withActionSheet = (Component: any): any => export const withActionSheet = (Component: React.ComponentType<any>): typeof Component =>
forwardRef((props: any, ref: ForwardedRef<any>) => ( forwardRef((props: typeof React.Component, ref: ForwardedRef<IActionSheetProvider>) => (
<Consumer>{(contexts: any) => <Component {...props} {...contexts} ref={ref} />}</Consumer> <Consumer>{(contexts: IActionSheetProvider) => <Component {...props} {...contexts} ref={ref} />}</Consumer>
)); ));
export const ActionSheetProvider = React.memo(({ children }: { children: JSX.Element | JSX.Element[] }) => { export const ActionSheetProvider = React.memo(({ children }: { children: React.ReactElement | React.ReactElement[] }) => {
const ref: ForwardedRef<any> = useRef(); const ref: ForwardedRef<IActionSheetProvider> = useRef(null);
const { theme }: any = useTheme();
const getContext = () => ({ const getContext = () => ({
showActionSheet: (options: any) => { showActionSheet: (options: TActionSheetOptions) => {
ref.current?.showActionSheet(options); ref.current?.showActionSheet(options);
}, },
hideActionSheet: () => { hideActionSheet: () => {
@ -37,7 +43,7 @@ export const ActionSheetProvider = React.memo(({ children }: { children: JSX.Ele
return ( return (
<Provider value={getContext()}> <Provider value={getContext()}>
<ActionSheet ref={ref} theme={theme}> <ActionSheet ref={ref}>
<>{children}</> <>{children}</>
</ActionSheet> </ActionSheet>
</Provider> </Provider>

View File

@ -1,14 +1,11 @@
import React from 'react'; import React from 'react';
import { ActivityIndicator, ActivityIndicatorProps, StyleSheet } from 'react-native'; import { ActivityIndicator, ActivityIndicatorProps, StyleSheet } from 'react-native';
import { useTheme } from '../theme';
import { themes } from '../constants/colors'; import { themes } from '../constants/colors';
type TTheme = 'light' | 'dark' | 'black' | string;
interface IActivityIndicator extends ActivityIndicatorProps { interface IActivityIndicator extends ActivityIndicatorProps {
theme?: TTheme;
absolute?: boolean; absolute?: boolean;
props?: object;
} }
const styles = StyleSheet.create({ 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} /> <ActivityIndicator style={[styles.indicator, absolute && styles.absolute]} color={themes[theme].auxiliaryText} {...props} />
); );
};
export default RCActivityIndicator; export default RCActivityIndicator;

View File

@ -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() { componentWillUnmount() {
if (this.subscription?.unsubscribe) { if (this.subscription?.unsubscribe) {
this.subscription.unsubscribe(); this.subscription.unsubscribe();

View File

@ -1,3 +1,5 @@
import React from 'react';
import { TGetCustomEmoji } from '../../definitions/IEmoji'; import { TGetCustomEmoji } from '../../definitions/IEmoji';
export interface IAvatar { export interface IAvatar {
@ -9,7 +11,7 @@ export interface IAvatar {
size?: number; size?: number;
borderRadius?: number; borderRadius?: number;
type?: string; type?: string;
children?: JSX.Element; children?: React.ReactElement | null;
user?: { user?: {
id?: string; id?: string;
token?: string; token?: string;

View File

@ -1,13 +1,12 @@
import React from 'react'; import React from 'react';
import { ActivityIndicator, ImageBackground, StyleSheet, Text, View } from 'react-native'; import { ActivityIndicator, ImageBackground, StyleSheet, Text, View } from 'react-native';
import { withTheme } from '../../theme'; import { useTheme } from '../../theme';
import sharedStyles from '../../views/Styles'; import sharedStyles from '../../views/Styles';
import { themes } from '../../constants/colors'; import { themes } from '../../constants/colors';
interface IBackgroundContainer { interface IBackgroundContainer {
text?: string; text?: string;
theme?: string;
loading?: boolean; 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}> <View style={styles.container}>
<ImageBackground source={{ uri: `message_empty_${theme}` }} style={styles.image} /> <ImageBackground source={{ uri: `message_empty_${theme}` }} style={styles.image} />
{text && !loading ? <Text style={[styles.text, { color: themes[theme!].auxiliaryTintColor }]}>{text}</Text> : null} {text && !loading ? <Text style={[styles.text, { color: themes[theme].auxiliaryTintColor }]}>{text}</Text> : null}
{loading ? <ActivityIndicator style={styles.text} color={themes[theme!].auxiliaryTintColor} /> : null} {loading ? <ActivityIndicator style={styles.text} color={themes[theme].auxiliaryTintColor} /> : null}
</View> </View>
); );
};
export default withTheme(BackgroundContainer); export default BackgroundContainer;

View File

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

View File

@ -5,12 +5,11 @@ import { StyleSheet, View } from 'react-native';
import { themes } from '../../constants/colors'; import { themes } from '../../constants/colors';
import { themedHeader } from '../../utils/navigation'; import { themedHeader } from '../../utils/navigation';
import { isIOS, isTablet } from '../../utils/deviceInfo'; import { isIOS, isTablet } from '../../utils/deviceInfo';
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 headerHeight = isIOS ? 44 : 56;
export const getHeaderHeight = (isLandscape: boolean) => { export const getHeaderHeight = (isLandscape: boolean): number => {
if (isIOS) { if (isIOS) {
if (isLandscape && !isTablet) { if (isLandscape && !isTablet) {
return 32; return 32;
@ -28,7 +27,13 @@ interface IHeaderTitlePosition {
numIconsRight: number; numIconsRight: number;
} }
export const getHeaderTitlePosition = ({ insets, numIconsRight }: IHeaderTitlePosition) => ({ export const getHeaderTitlePosition = ({
insets,
numIconsRight
}: IHeaderTitlePosition): {
left: number;
right: number;
} => ({
left: insets.left + 60, left: insets.left + 60,
right: insets.right + Math.max(45 * numIconsRight, 15) right: insets.right + Math.max(45 * numIconsRight, 15)
}); });
@ -43,13 +48,14 @@ const styles = StyleSheet.create({
}); });
interface IHeader { interface IHeader {
theme: string; headerLeft: () => React.ReactElement | null;
headerLeft(): void; headerTitle: () => React.ReactElement;
headerTitle(): void; headerRight: () => React.ReactElement | null;
headerRight(): void;
} }
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']}> <SafeAreaView style={{ backgroundColor: themes[theme].headerBackground }} edges={['top', 'left', 'right']}>
<View style={[styles.container, { ...themedHeader(theme).headerStyle }]}> <View style={[styles.container, { ...themedHeader(theme).headerStyle }]}>
{headerLeft ? headerLeft() : null} {headerLeft ? headerLeft() : null}
@ -57,6 +63,7 @@ const Header = ({ theme, headerLeft, headerTitle, headerRight }: IHeader) => (
{headerRight ? headerRight() : null} {headerRight ? headerRight() : null}
</View> </View>
</SafeAreaView> </SafeAreaView>
); );
};
export default withTheme(Header); export default Header;

View File

@ -6,20 +6,22 @@ import Container from './HeaderButtonContainer';
import Item from './HeaderButtonItem'; import Item from './HeaderButtonItem';
interface IHeaderButtonCommon { interface IHeaderButtonCommon {
navigation: any; navigation?: any; // TODO: Evaluate proper type
onPress?(): void; onPress?: () => void;
testID?: string; testID?: string;
} }
// Left // 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> <Container left>
<Item iconName='hamburguer' onPress={() => navigation.toggleDrawer()} testID={testID} {...props} /> <Item iconName='hamburguer' onPress={onPress} testID={testID} {...props} />
</Container> </Container>
)); )
);
export const CloseModal = React.memo( export const CloseModal = React.memo(
({ navigation, testID, onPress = () => navigation.pop(), ...props }: IHeaderButtonCommon) => ( ({ navigation, testID, onPress = () => navigation?.pop(), ...props }: IHeaderButtonCommon) => (
<Container left> <Container left>
<Item iconName='close' onPress={onPress} testID={testID} {...props} /> <Item iconName='close' onPress={onPress} testID={testID} {...props} />
</Container> </Container>
@ -29,9 +31,9 @@ export const CloseModal = React.memo(
export const CancelModal = React.memo(({ onPress, testID }: Partial<IHeaderButtonCommon>) => ( export const CancelModal = React.memo(({ onPress, testID }: Partial<IHeaderButtonCommon>) => (
<Container left> <Container left>
{isIOS ? ( {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> </Container>
)); ));
@ -39,22 +41,24 @@ export const CancelModal = React.memo(({ onPress, testID }: Partial<IHeaderButto
// Right // Right
export const More = React.memo(({ onPress, testID }: Partial<IHeaderButtonCommon>) => ( export const More = React.memo(({ onPress, testID }: Partial<IHeaderButtonCommon>) => (
<Container> <Container>
<Item iconName='kebab' onPress={onPress!} testID={testID} /> <Item iconName='kebab' onPress={onPress} testID={testID} />
</Container> </Container>
)); ));
export const Download = React.memo(({ onPress, testID, ...props }: Partial<IHeaderButtonCommon>) => ( export const Download = React.memo(({ onPress, testID, ...props }: IHeaderButtonCommon) => (
<Container> <Container>
<Item iconName='download' onPress={onPress!} testID={testID} {...props} /> <Item iconName='download' onPress={onPress} testID={testID} {...props} />
</Container> </Container>
)); ));
export const Preferences = React.memo(({ onPress, testID, ...props }: Partial<IHeaderButtonCommon>) => ( export const Preferences = React.memo(({ onPress, testID, ...props }: IHeaderButtonCommon) => (
<Container> <Container>
<Item iconName='settings' onPress={onPress!} testID={testID} {...props} /> <Item iconName='settings' onPress={onPress} testID={testID} {...props} />
</Container> </Container>
)); ));
export const Legal = React.memo(({ navigation, testID }: Partial<IHeaderButtonCommon>) => ( export const Legal = React.memo(
<More onPress={() => navigation.navigate('LegalView')} testID={testID} /> ({ navigation, testID, onPress = () => navigation?.navigate('LegalView') }: IHeaderButtonCommon) => (
)); <More onPress={onPress} testID={testID} />
)
);

View File

@ -2,7 +2,7 @@ import React from 'react';
import { StyleSheet, View } from 'react-native'; import { StyleSheet, View } from 'react-native';
interface IHeaderButtonContainer { interface IHeaderButtonContainer {
children: React.ReactNode; children?: React.ReactElement | (React.ReactElement | null)[] | null;
left?: boolean; 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> <View style={[styles.container, left ? styles.left : styles.right]}>{children}</View>
); );

View File

@ -3,16 +3,15 @@ import { Platform, StyleSheet, Text } from 'react-native';
import Touchable from 'react-native-platform-touchable'; import Touchable from 'react-native-platform-touchable';
import { CustomIcon } from '../../lib/Icons'; import { CustomIcon } from '../../lib/Icons';
import { withTheme } from '../../theme'; import { useTheme } from '../../theme';
import { themes } from '../../constants/colors'; import { themes } from '../../constants/colors';
import sharedStyles from '../../views/Styles'; import sharedStyles from '../../views/Styles';
interface IHeaderButtonItem { interface IHeaderButtonItem {
title?: string; title?: string;
iconName?: string; iconName?: string;
onPress: <T>(arg: T) => void; onPress?: <T>(arg: T) => void;
testID?: string; testID?: string;
theme?: string;
badge?(): void; 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}> <Touchable onPress={onPress} testID={testID} hitSlop={BUTTON_HIT_SLOP} style={styles.container}>
<> <>
{iconName ? ( {iconName ? (
<CustomIcon name={iconName} size={24} color={themes[theme!].headerTintColor} /> <CustomIcon name={iconName} size={24} color={themes[theme].headerTintColor} />
) : ( ) : (
<Text style={[styles.title, { color: themes[theme!].headerTintColor }]}>{title}</Text> <Text style={[styles.title, { color: themes[theme].headerTintColor }]}>{title}</Text>
)} )}
{badge ? badge() : null} {badge ? badge() : null}
</> </>
</Touchable> </Touchable>
); );
};
Item.displayName = 'HeaderButton.Item'; Item.displayName = 'HeaderButton.Item';
export default withTheme(Item); export default Item;

View File

@ -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; export default Badge;

View File

@ -14,9 +14,18 @@ import { ROW_HEIGHT } from '../../presentation/RoomItem';
import { goRoom } from '../../utils/goRoom'; import { goRoom } from '../../utils/goRoom';
import Navigation from '../../lib/Navigation'; import Navigation from '../../lib/Navigation';
import { useOrientation } from '../../dimensions'; import { useOrientation } from '../../dimensions';
import { IApplicationState, ISubscription, SubscriptionType } from '../../definitions';
interface INotifierComponent { export interface INotifierComponent {
notification: object; notification: {
text: string;
payload: {
sender: { username: string };
type: SubscriptionType;
} & Pick<ISubscription, '_id' | 'name' | 'rid' | 'prid'>;
title: string;
avatar: string;
};
isMasterDetail: boolean; isMasterDetail: boolean;
} }
@ -67,15 +76,15 @@ const styles = StyleSheet.create({
const hideNotification = () => Notifier.hideNotification(); const hideNotification = () => Notifier.hideNotification();
const NotifierComponent = React.memo(({ notification, isMasterDetail }: INotifierComponent) => { const NotifierComponent = React.memo(({ notification, isMasterDetail }: INotifierComponent) => {
const { theme }: any = useTheme(); const { theme } = useTheme();
const insets = useSafeAreaInsets(); const insets = useSafeAreaInsets();
const { isLandscape } = useOrientation(); const { isLandscape } = useOrientation();
const { text, payload }: any = notification; const { text, payload } = notification;
const { type, rid } = payload; const { type, rid } = payload;
const name = type === 'd' ? payload.sender.username : payload.name; const name = type === 'd' ? payload.sender.username : payload.name;
// if sub is not on local database, title and avatar will be null, so we use payload from notification // if sub is not on local database, title and avatar will be null, so we use payload from notification
const { title = name, avatar = name }: any = notification; const { title = name, avatar = name } = notification;
const onPress = () => { const onPress = () => {
const { prid, _id } = payload; 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 isMasterDetail: state.app.isMasterDetail
}); });

View File

@ -3,16 +3,18 @@ import { Easing, Notifier, NotifierRoot } from 'react-native-notifier';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { dequal } from 'dequal'; import { dequal } from 'dequal';
import NotifierComponent from './NotifierComponent'; import NotifierComponent, { INotifierComponent } from './NotifierComponent';
import EventEmitter from '../../utils/events'; import EventEmitter from '../../utils/events';
import Navigation from '../../lib/Navigation'; import Navigation from '../../lib/Navigation';
import { getActiveRoute } from '../../utils/navigation'; import { getActiveRoute } from '../../utils/navigation';
import { IApplicationState } from '../../definitions';
import { IRoom } from '../../reducers/room';
export const INAPP_NOTIFICATION_EMITTER = 'NotificationInApp'; export const INAPP_NOTIFICATION_EMITTER = 'NotificationInApp';
const InAppNotification = memo( const InAppNotification = memo(
({ rooms, appState }: { rooms: any; appState: string }) => { ({ rooms, appState }: { rooms: IRoom['rooms']; appState: string }) => {
const show = (notification: any) => { const show = (notification: INotifierComponent['notification']) => {
if (appState !== 'foreground') { if (appState !== 'foreground') {
return; return;
} }
@ -46,7 +48,7 @@ const InAppNotification = memo(
(prevProps, nextProps) => dequal(prevProps.rooms, nextProps.rooms) (prevProps, nextProps) => dequal(prevProps.rooms, nextProps.rooms)
); );
const mapStateToProps = (state: any) => ({ const mapStateToProps = (state: IApplicationState) => ({
rooms: state.room.rooms, rooms: state.room.rooms,
appState: state.app.ready && state.app.foreground ? 'foreground' : 'background' appState: state.app.ready && state.app.foreground ? 'foreground' : 'background'
}); });

View File

@ -4,7 +4,7 @@ import { StyleSheet, Text, View } from 'react-native';
import sharedStyles from '../../views/Styles'; import sharedStyles from '../../views/Styles';
import { themes } from '../../constants/colors'; import { themes } from '../../constants/colors';
import I18n from '../../i18n'; import I18n from '../../i18n';
import { withTheme } from '../../theme'; import { useTheme } from '../../theme';
import { PADDING_HORIZONTAL } from './constants'; import { PADDING_HORIZONTAL } from './constants';
const styles = StyleSheet.create({ const styles = StyleSheet.create({
@ -20,18 +20,20 @@ const styles = StyleSheet.create({
interface IListHeader { interface IListHeader {
title: string; title: string;
theme?: string;
translateTitle?: boolean; 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}> <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} {translateTitle ? I18n.t(title) : title}
</Text> </Text>
</View> </View>
)); );
});
ListHeader.displayName = 'List.Header'; ListHeader.displayName = 'List.Header';
export default withTheme(ListHeader); export default ListHeader;

View File

@ -3,7 +3,7 @@ import { StyleSheet, Text, View } from 'react-native';
import sharedStyles from '../../views/Styles'; import sharedStyles from '../../views/Styles';
import { themes } from '../../constants/colors'; import { themes } from '../../constants/colors';
import { withTheme } from '../../theme'; import { useTheme } from '../../theme';
import { PADDING_HORIZONTAL } from './constants'; import { PADDING_HORIZONTAL } from './constants';
import I18n from '../../i18n'; import I18n from '../../i18n';
@ -18,18 +18,20 @@ const styles = StyleSheet.create({
} }
}); });
interface IListHeader { interface IListInfo {
info: string; info: string;
theme?: string;
translateInfo?: boolean; 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}> <View style={styles.container}>
<Text style={[styles.text, { color: themes[theme!].infoText }]}>{translateInfo ? I18n.t(info) : info}</Text> <Text style={[styles.text, { color: themes[theme].infoText }]}>{translateInfo ? I18n.t(info) : info}</Text>
</View> </View>
)); );
});
ListInfo.displayName = 'List.Info'; ListInfo.displayName = 'List.Info';
export default withTheme(ListInfo); export default ListInfo;

View File

@ -135,7 +135,7 @@ const Button = React.memo<IListItemButton>(({ onPress, backgroundColor, underlay
</Touch> </Touch>
)); ));
interface IListItem extends IListItemContent, IListButtonPress { interface IListItem extends IListItemContent, IListItemButton {
backgroundColor?: string; backgroundColor?: string;
} }

View File

@ -22,15 +22,20 @@ interface ILoadingProps {
theme?: string; 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 = { state = {
scale: new Animated.Value(1), scale: new Animated.Value(1),
opacity: new Animated.Value(0) opacity: new Animated.Value(0)
}; };
private opacityAnimation: any; private opacityAnimation?: Animated.CompositeAnimation;
private scaleAnimation: any; private scaleAnimation?: Animated.CompositeAnimation;
componentDidMount() { componentDidMount() {
const { opacity, scale } = this.state; 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; const { visible } = this.props;
if (visible && visible !== prevProps.visible) { if (visible && visible !== prevProps.visible) {
this.startAnimations(); this.startAnimations();
@ -107,8 +112,7 @@ class Loading extends React.PureComponent<ILoadingProps, any> {
<Animated.View <Animated.View
style={[ style={[
{ {
// @ts-ignore ...StyleSheet.absoluteFillObject,
...StyleSheet.absoluteFill,
backgroundColor: themes[theme!].backdropColor, backgroundColor: themes[theme!].backdropColor,
opacity: opacityAnimation opacity: opacityAnimation
} }

View File

@ -3,6 +3,7 @@ import { Animated, Easing, Linking, StyleSheet, Text, View } from 'react-native'
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { Base64 } from 'js-base64'; import { Base64 } from 'js-base64';
import * as AppleAuthentication from 'expo-apple-authentication'; import * as AppleAuthentication from 'expo-apple-authentication';
import { StackNavigationProp } from '@react-navigation/stack';
import { withTheme } from '../theme'; import { withTheme } from '../theme';
import sharedStyles from '../views/Styles'; import sharedStyles from '../views/Styles';
@ -15,6 +16,9 @@ import random from '../utils/random';
import { events, logEvent } from '../utils/log'; import { events, logEvent } from '../utils/log';
import RocketChat from '../lib/rocketchat'; import RocketChat from '../lib/rocketchat';
import { CustomIcon } from '../lib/Icons'; import { CustomIcon } from '../lib/Icons';
import { IServices } from '../selectors/login';
import { OutsideParamList } from '../stacks/types';
import { IApplicationState } from '../definitions';
const BUTTON_HEIGHT = 48; const BUTTON_HEIGHT = 48;
const SERVICE_HEIGHT = 58; const SERVICE_HEIGHT = 58;
@ -58,31 +62,40 @@ const styles = StyleSheet.create({
}); });
interface IOpenOAuth { interface IOpenOAuth {
url?: string; url: string;
ssoToken?: string; ssoToken?: string;
authType?: string; authType?: string;
} }
interface IService { interface IItemService {
name: string; name: string;
service: string; service: string;
authType: string; authType: string;
buttonColor: string; buttonColor: string;
buttonLabelColor: 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 { interface ILoginServicesProps {
navigation: any; navigation: StackNavigationProp<OutsideParamList>;
server: string; server: string;
services: { services: IServices;
facebook: { clientId: string };
github: { clientId: string };
gitlab: { clientId: string };
google: { clientId: string };
linkedin: { clientId: string };
'meteor-developer': { clientId: string };
wordpress: { clientId: string; serverURL: string };
};
Gitlab_URL: string; Gitlab_URL: string;
CAS_enabled: boolean; CAS_enabled: boolean;
CAS_login_url: string; CAS_login_url: string;
@ -90,12 +103,13 @@ interface ILoginServicesProps {
theme: string; theme: string;
} }
class LoginServices extends React.PureComponent<ILoginServicesProps, any> { interface ILoginServicesState {
private _animation: any; collapsed: boolean;
servicesHeight: Animated.Value;
}
static defaultProps = { class LoginServices extends React.PureComponent<ILoginServicesProps, ILoginServicesState> {
separator: true private _animation?: Animated.CompositeAnimation | void;
};
state = { state = {
collapsed: true, collapsed: true,
@ -194,7 +208,7 @@ class LoginServices extends React.PureComponent<ILoginServicesProps, any> {
this.openOAuth({ url: `${endpoint}${params}` }); this.openOAuth({ url: `${endpoint}${params}` });
}; };
onPressCustomOAuth = (loginService: any) => { onPressCustomOAuth = (loginService: IItemService) => {
logEvent(events.ENTER_WITH_CUSTOM_OAUTH); logEvent(events.ENTER_WITH_CUSTOM_OAUTH);
const { server } = this.props; const { server } = this.props;
const { serverURL, authorizePath, clientId, scope, service } = loginService; const { serverURL, authorizePath, clientId, scope, service } = loginService;
@ -207,7 +221,7 @@ class LoginServices extends React.PureComponent<ILoginServicesProps, any> {
this.openOAuth({ url }); this.openOAuth({ url });
}; };
onPressSaml = (loginService: any) => { onPressSaml = (loginService: IItemService) => {
logEvent(events.ENTER_WITH_SAML); logEvent(events.ENTER_WITH_SAML);
const { server } = this.props; const { server } = this.props;
const { clientConfig } = loginService; const { clientConfig } = loginService;
@ -234,7 +248,6 @@ class LoginServices extends React.PureComponent<ILoginServicesProps, any> {
AppleAuthentication.AppleAuthenticationScope.EMAIL AppleAuthentication.AppleAuthenticationScope.EMAIL
] ]
}); });
await RocketChat.loginOAuthOrSso({ fullName, email, identityToken }); await RocketChat.loginOAuthOrSso({ fullName, email, identityToken });
} catch { } catch {
logEvent(events.ENTER_WITH_APPLE_F); logEvent(events.ENTER_WITH_APPLE_F);
@ -243,7 +256,12 @@ class LoginServices extends React.PureComponent<ILoginServicesProps, any> {
getOAuthState = (loginStyle = LOGIN_STYPE_POPUP) => { getOAuthState = (loginStyle = LOGIN_STYPE_POPUP) => {
const credentialToken = random(43); 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) { if (loginStyle === LOGIN_STYPE_REDIRECT) {
obj = { obj = {
...obj, ...obj,
@ -263,12 +281,11 @@ class LoginServices extends React.PureComponent<ILoginServicesProps, any> {
if (this._animation) { if (this._animation) {
this._animation.stop(); this._animation.stop();
} }
// @ts-ignore
this._animation = Animated.timing(servicesHeight, { this._animation = Animated.timing(servicesHeight, {
toValue: height, toValue: height,
duration: 300, duration: 300,
// @ts-ignore easing: Easing.inOut(Easing.quad),
easing: Easing.easeOutCubic useNativeDriver: true
}).start(); }).start();
}; };
@ -281,11 +298,11 @@ class LoginServices extends React.PureComponent<ILoginServicesProps, any> {
} else { } else {
this.transitionServicesTo(SERVICES_COLLAPSED_HEIGHT); this.transitionServicesTo(SERVICES_COLLAPSED_HEIGHT);
} }
this.setState((prevState: any) => ({ collapsed: !prevState.collapsed })); this.setState((prevState: ILoginServicesState) => ({ collapsed: !prevState.collapsed }));
}; };
getSocialOauthProvider = (name: string) => { getSocialOauthProvider = (name: string) => {
const oauthProviders: any = { const oauthProviders: IOauthProvider = {
facebook: this.onPressFacebook, facebook: this.onPressFacebook,
github: this.onPressGithub, github: this.onPressGithub,
gitlab: this.onPressGitlab, gitlab: this.onPressGitlab,
@ -324,7 +341,7 @@ class LoginServices extends React.PureComponent<ILoginServicesProps, any> {
return null; return null;
}; };
renderItem = (service: IService) => { renderItem = (service: IItemService) => {
const { CAS_enabled, theme } = this.props; const { CAS_enabled, theme } = this.props;
let { name } = service; let { name } = service;
name = name === 'meteor-developer' ? 'meteor' : name; name = name === 'meteor-developer' ? 'meteor' : name;
@ -401,26 +418,28 @@ class LoginServices extends React.PureComponent<ILoginServicesProps, any> {
if (length > 3 && separator) { if (length > 3 && separator) {
return ( 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()} {this.renderServicesSeparator()}
</> </>
); );
} }
return ( return (
<> <>
{Object.values(services).map((service: any) => this.renderItem(service))} {Object.values(services).map((service: IItemService) => this.renderItem(service))}
{this.renderServicesSeparator()} {this.renderServicesSeparator()}
</> </>
); );
} }
} }
const mapStateToProps = (state: any) => ({ const mapStateToProps = (state: IApplicationState) => ({
server: state.server.server, server: state.server.server,
Gitlab_URL: state.settings.API_Gitlab_URL, Gitlab_URL: state.settings.API_Gitlab_URL as string,
CAS_enabled: state.settings.CAS_enabled, CAS_enabled: state.settings.CAS_enabled as boolean,
CAS_login_url: state.settings.CAS_login_url, CAS_login_url: state.settings.CAS_login_url as string,
services: state.login.services services: state.login.services as IServices
}); });
export default connect(mapStateToProps)(withTheme(LoginServices)) as any; export default connect(mapStateToProps)(withTheme(LoginServices));

View File

@ -15,19 +15,12 @@ import { showConfirmationAlert } from '../../utils/info';
import { useActionSheet } from '../ActionSheet'; import { useActionSheet } from '../ActionSheet';
import Header, { HEADER_HEIGHT } from './Header'; import Header, { HEADER_HEIGHT } from './Header';
import events from '../../utils/log/events'; import events from '../../utils/log/events';
import { TMessageModel } from '../../definitions/IMessage'; import { ILoggedUser, TAnyMessageModel, TSubscriptionModel } from '../../definitions';
interface IMessageActions { export interface IMessageActions {
room: { room: TSubscriptionModel;
rid: string; tmid?: string;
autoTranslateLanguage: any; user: Pick<ILoggedUser, 'id'>;
autoTranslate: any;
reactWhenReadOnly: any;
};
tmid: string;
user: {
id: string | number;
};
editInit: Function; editInit: Function;
reactionInit: Function; reactionInit: Function;
onReactionPress: Function; onReactionPress: Function;
@ -270,8 +263,11 @@ const MessageActions = React.memo(
} }
}; };
const handleToggleTranslation = async (message: TMessageModel) => { const handleToggleTranslation = async (message: TAnyMessageModel) => {
try { try {
if (!room.autoTranslateLanguage) {
return;
}
const db = database.active; const db = database.active;
await db.write(async () => { await db.write(async () => {
await message.update(m => { await message.update(m => {
@ -321,7 +317,7 @@ const MessageActions = React.memo(
}); });
}; };
const getOptions = (message: TMessageModel) => { const getOptions = (message: TAnyMessageModel) => {
let options: any = []; let options: any = [];
// Reply // Reply
@ -447,7 +443,7 @@ const MessageActions = React.memo(
return options; return options;
}; };
const showMessageActions = async (message: TMessageModel) => { const showMessageActions = async (message: TAnyMessageModel) => {
logEvent(events.ROOM_SHOW_MSG_ACTIONS); logEvent(events.ROOM_SHOW_MSG_ACTIONS);
await getPermissions(); await getPermissions();
showActionSheet({ showActionSheet({

View File

@ -34,7 +34,7 @@ const Item = ({ item, theme }: IMessageBoxCommandsPreviewItem) => {
resizeMode={FastImage.resizeMode.cover} resizeMode={FastImage.resizeMode.cover}
onLoadStart={() => setLoading(true)} onLoadStart={() => setLoading(true)}
onLoad={() => setLoading(false)}> onLoad={() => setLoading(false)}>
{loading ? <ActivityIndicator theme={theme} /> : null} {loading ? <ActivityIndicator /> : null}
</FastImage> </FastImage>
) : ( ) : (
<CustomIcon name='attach' size={36} color={themes[theme!].actionTintColor} /> <CustomIcon name='attach' size={36} color={themes[theme!].actionTintColor} />

View File

@ -6,9 +6,10 @@ import Item from './Item';
import styles from '../styles'; import styles from '../styles';
import { themes } from '../../../constants/colors'; import { themes } from '../../../constants/colors';
import { withTheme } from '../../../theme'; import { withTheme } from '../../../theme';
import { IPreviewItem } from '../../../definitions';
interface IMessageBoxCommandsPreview { interface IMessageBoxCommandsPreview {
commandPreview: []; commandPreview: IPreviewItem[];
showCommandPreview: boolean; showCommandPreview: boolean;
theme?: string; theme?: string;
} }

View File

@ -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 #');
});
});

View File

@ -0,0 +1,4 @@
// Match query string from the message to replace it with the suggestion
const getMentionRegexp = (): any => /[^@:#/!]*$/;
export default getMentionRegexp;

View File

@ -9,7 +9,7 @@ import { Q } from '@nozbe/watermelondb';
import { TouchableWithoutFeedback } from 'react-native-gesture-handler'; import { TouchableWithoutFeedback } from 'react-native-gesture-handler';
import { generateTriggerId } from '../../lib/methods/actions'; 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 { userTyping as userTypingAction } from '../../actions/room';
import RocketChat from '../../lib/rocketchat'; import RocketChat from '../../lib/rocketchat';
import styles from './styles'; import styles from './styles';
@ -31,6 +31,7 @@ import { isAndroid, isTablet } from '../../utils/deviceInfo';
import { canUploadFile } from '../../utils/media'; import { canUploadFile } from '../../utils/media';
import EventEmiter from '../../utils/events'; import EventEmiter from '../../utils/events';
import { KEY_COMMAND, handleCommandShowUpload, handleCommandSubmit, handleCommandTyping } from '../../commands'; import { KEY_COMMAND, handleCommandShowUpload, handleCommandSubmit, handleCommandTyping } from '../../commands';
import getMentionRegexp from './getMentionRegexp';
import Mentions from './Mentions'; import Mentions from './Mentions';
import MessageboxContext from './Context'; import MessageboxContext from './Context';
import { import {
@ -49,6 +50,7 @@ import { sanitizeLikeString } from '../../lib/database/utils';
import { CustomIcon } from '../../lib/Icons'; import { CustomIcon } from '../../lib/Icons';
import { IMessage } from '../../definitions/IMessage'; import { IMessage } from '../../definitions/IMessage';
import { forceJpgExtension } from './forceJpgExtension'; import { forceJpgExtension } from './forceJpgExtension';
import { IPreviewItem, IUser } from '../../definitions';
if (isAndroid) { if (isAndroid) {
require('./EmojiKeyboard'); require('./EmojiKeyboard');
@ -72,7 +74,7 @@ const videoPickerConfig = {
mediaType: 'video' mediaType: 'video'
}; };
interface IMessageBoxProps { export interface IMessageBoxProps {
rid: string; rid: string;
baseUrl: string; baseUrl: string;
message: IMessage; message: IMessage;
@ -80,12 +82,7 @@ interface IMessageBoxProps {
editing: boolean; editing: boolean;
threadsEnabled: boolean; threadsEnabled: boolean;
isFocused(): boolean; isFocused(): boolean;
user: { user: IUser;
id: string;
_id: string;
username: string;
token: string;
};
roomType: string; roomType: string;
tmid: string; tmid: string;
replyWithMention: boolean; replyWithMention: boolean;
@ -118,7 +115,7 @@ interface IMessageBoxState {
showSend: any; showSend: any;
recording: boolean; recording: boolean;
trackingType: string; trackingType: string;
commandPreview: []; commandPreview: IPreviewItem[];
showCommandPreview: boolean; showCommandPreview: boolean;
command: { command: {
appId?: any; appId?: any;
@ -493,7 +490,7 @@ class MessageBox extends Component<IMessageBoxProps, IMessageBoxState> {
const msg = this.text; const msg = this.text;
const { start, end } = this.selection; const { start, end } = this.selection;
const cursor = Math.max(start, end); const cursor = Math.max(start, end);
const regexp = /([a-z0-9._-]+)$/im; const regexp = getMentionRegexp();
let result = msg.substr(0, cursor).replace(regexp, ''); let result = msg.substr(0, cursor).replace(regexp, '');
// Remove the ! after select the canned response // Remove the ! after select the canned response
if (trackingType === MENTIONS_TRACKING_TYPE_CANNED) { if (trackingType === MENTIONS_TRACKING_TYPE_CANNED) {
@ -609,7 +606,7 @@ class MessageBox extends Component<IMessageBoxProps, IMessageBoxState> {
getCannedResponses = debounce(async (text?: string) => { getCannedResponses = debounce(async (text?: string) => {
const res = await RocketChat.getListCannedResponse({ text }); const res = await RocketChat.getListCannedResponse({ text });
this.setState({ mentions: res?.cannedResponses || [], mentionLoading: false }); this.setState({ mentions: res.success ? res.cannedResponses : [], mentionLoading: false });
}, 500); }, 500);
focus = () => { focus = () => {
@ -642,12 +639,12 @@ class MessageBox extends Component<IMessageBoxProps, IMessageBoxState> {
}, 1000); }, 1000);
}; };
setCommandPreview = async (command: any, name: string, params: any) => { setCommandPreview = async (command: any, name: string, params: string) => {
const { rid } = this.props; const { rid } = this.props;
try { try {
const { success, preview } = await RocketChat.getCommandPreview(name, rid, params); const response = await RocketChat.getCommandPreview(name, rid, params);
if (success) { if (response.success) {
return this.setState({ commandPreview: preview?.items, showCommandPreview: true, command }); return this.setState({ commandPreview: response.preview?.items || [], showCommandPreview: true, command });
} }
} catch (e) { } catch (e) {
log(e); log(e);
@ -891,7 +888,7 @@ class MessageBox extends Component<IMessageBoxProps, IMessageBoxState> {
const messageWithoutCommand = message.replace(/([^\s]+)/, '').trim(); const messageWithoutCommand = message.replace(/([^\s]+)/, '').trim();
const [{ appId }] = slashCommand; const [{ appId }] = slashCommand;
const triggerId = generateTriggerId(appId); const triggerId = generateTriggerId(appId);
RocketChat.runSlashCommand(command, roomId, messageWithoutCommand, triggerId, tmid || messageTmid); await RocketChat.runSlashCommand(command, roomId, messageWithoutCommand, triggerId, tmid || messageTmid);
replyCancel(); replyCancel();
} catch (e) { } catch (e) {
logEvent(events.COMMAND_RUN_F); logEvent(events.COMMAND_RUN_F);
@ -926,8 +923,8 @@ class MessageBox extends Component<IMessageBoxProps, IMessageBoxState> {
let msg = `[ ](${permalink}) `; let msg = `[ ](${permalink}) `;
// if original message wasn't sent by current user and neither from a direct room // if original message wasn't sent by current user and neither from a direct room
if (user.username !== replyingMessage.u.username && roomType !== 'd' && replyWithMention) { if (user.username !== replyingMessage?.u?.username && roomType !== 'd' && replyWithMention) {
msg += `@${replyingMessage.u.username} `; msg += `@${replyingMessage?.u?.username} `;
} }
msg = `${msg} ${message}`; msg = `${msg} ${message}`;
@ -1041,7 +1038,7 @@ class MessageBox extends Component<IMessageBoxProps, IMessageBoxState> {
tmid tmid
} = this.props; } = this.props;
const isAndroidTablet = const isAndroidTablet: Partial<IThemedTextInput> =
isTablet && isAndroid isTablet && isAndroid
? { ? {
multiline: false, multiline: false,
@ -1093,7 +1090,6 @@ class MessageBox extends Component<IMessageBoxProps, IMessageBoxState> {
<TextInput <TextInput
ref={component => (this.component = component)} ref={component => (this.component = component)}
style={[styles.textBoxInput, { color: themes[theme].bodyText }]} style={[styles.textBoxInput, { color: themes[theme].bodyText }]}
// @ts-ignore
returnKeyType='default' returnKeyType='default'
keyboardType='twitter' keyboardType='twitter'
blurOnSubmit={false} blurOnSubmit={false}

View File

@ -1,4 +1,5 @@
import { forwardRef, useImperativeHandle } from 'react'; import { forwardRef, useImperativeHandle } from 'react';
import Model from '@nozbe/watermelondb/Model';
import RocketChat from '../lib/rocketchat'; import RocketChat from '../lib/rocketchat';
import database from '../lib/database'; import database from '../lib/database';
@ -6,18 +7,20 @@ import protectedFunction from '../lib/methods/helpers/protectedFunction';
import { useActionSheet } from './ActionSheet'; import { useActionSheet } from './ActionSheet';
import I18n from '../i18n'; import I18n from '../i18n';
import log from '../utils/log'; 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 { showActionSheet }: any = useActionSheet();
const handleResend = protectedFunction(async (message: any) => { const handleResend = protectedFunction(async (message: TMessageModel) => {
await RocketChat.resendMessage(message, tmid); await RocketChat.resendMessage(message, tmid);
}); });
const handleDelete = async (message: any) => { const handleDelete = async (message: TMessageModel) => {
try { try {
const db = database.active; const db = database.active;
const deleteBatch: any = []; const deleteBatch: Model[] = [];
const msgCollection = db.get('messages'); const msgCollection = db.get('messages');
const threadCollection = db.get('threads'); const threadCollection = db.get('threads');
@ -38,7 +41,7 @@ const MessageErrorActions = forwardRef(({ tmid }: any, ref): any => {
const msg = await msgCollection.find(tmid); const msg = await msgCollection.find(tmid);
if (msg?.tcount && msg.tcount <= 1) { if (msg?.tcount && msg.tcount <= 1) {
deleteBatch.push( deleteBatch.push(
msg.prepareUpdate((m: any) => { msg.prepareUpdate(m => {
m.tcount = null; m.tcount = null;
m.tlm = null; m.tlm = null;
}) })
@ -53,8 +56,10 @@ const MessageErrorActions = forwardRef(({ tmid }: any, ref): any => {
} }
} else { } else {
deleteBatch.push( deleteBatch.push(
msg.prepareUpdate((m: any) => { msg.prepareUpdate(m => {
if (m.tcount) {
m.tcount -= 1; m.tcount -= 1;
}
}) })
); );
} }
@ -70,7 +75,7 @@ const MessageErrorActions = forwardRef(({ tmid }: any, ref): any => {
} }
}; };
const showMessageErrorActions = (message: any) => { const showMessageErrorActions = (message: TMessageModel) => {
showActionSheet({ showActionSheet({
options: [ options: [
{ {

View File

@ -5,16 +5,18 @@ import styles from './styles';
import { themes } from '../../../constants/colors'; import { themes } from '../../../constants/colors';
import Touch from '../../../utils/touch'; import Touch from '../../../utils/touch';
import { CustomIcon } from '../../../lib/Icons'; import { CustomIcon } from '../../../lib/Icons';
import { useTheme } from '../../../theme';
interface IPasscodeButton { interface IPasscodeButton {
text?: string; text?: string;
icon?: string; icon?: string;
theme: string;
disabled?: boolean; disabled?: boolean;
onPress?: Function; 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); const press = () => onPress && onPress(text);
return ( return (

View File

@ -4,17 +4,20 @@ import range from 'lodash/range';
import styles from './styles'; import styles from './styles';
import { themes } from '../../../constants/colors'; import { themes } from '../../../constants/colors';
import { useTheme } from '../../../theme';
const SIZE_EMPTY = 12; const SIZE_EMPTY = 12;
const SIZE_FULL = 16; const SIZE_FULL = 16;
interface IPasscodeDots { interface IPasscodeDots {
passcode: string; passcode: string;
theme: string;
length: number; 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}> <View style={styles.dotsContainer}>
{range(length).map(val => { {range(length).map(val => {
const lengthSup = passcode.length >= val + 1; const lengthSup = passcode.length >= val + 1;
@ -45,6 +48,7 @@ const Dots = React.memo(({ passcode, theme, length }: IPasscodeDots) => (
); );
})} })}
</View> </View>
)); );
});
export default Dots; export default Dots;

View File

@ -5,13 +5,18 @@ import { Row } from 'react-native-easy-grid';
import styles from './styles'; import styles from './styles';
import { themes } from '../../../constants/colors'; import { themes } from '../../../constants/colors';
import { CustomIcon } from '../../../lib/Icons'; 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}> <Row style={styles.row}>
<View style={styles.iconView}> <View style={styles.iconView}>
<CustomIcon name='auth' size={40} color={themes[theme].passcodeLockIcon} /> <CustomIcon name='auth' size={40} color={themes[theme].passcodeLockIcon} />
</View> </View>
</Row> </Row>
)); );
});
export default LockIcon; export default LockIcon;

View File

@ -6,36 +6,35 @@ import { resetAttempts } from '../../../utils/localAuthentication';
import { TYPE } from '../constants'; import { TYPE } from '../constants';
import { getDiff, getLockedUntil } from '../utils'; import { getDiff, getLockedUntil } from '../utils';
import I18n from '../../../i18n'; import I18n from '../../../i18n';
import { useTheme } from '../../../theme';
import styles from './styles'; import styles from './styles';
import Title from './Title'; import Title from './Title';
import Subtitle from './Subtitle'; import Subtitle from './Subtitle';
import LockIcon from './LockIcon'; import LockIcon from './LockIcon';
interface IPasscodeTimer { interface IPasscodeTimer {
time: string; time: Date | null;
theme: string;
setStatus: Function; setStatus: Function;
} }
interface IPasscodeLocked { interface IPasscodeLocked {
theme: string;
setStatus: Function; setStatus: Function;
} }
const Timer = React.memo(({ time, theme, setStatus }: IPasscodeTimer) => { const Timer = React.memo(({ time, setStatus }: IPasscodeTimer) => {
const calcTimeLeft = () => { const calcTimeLeft = () => {
const diff = getDiff(time); const diff = getDiff(time || 0);
if (diff > 0) { if (diff > 0) {
return Math.floor((diff / 1000) % 60); return Math.floor((diff / 1000) % 60);
} }
}; };
const [timeLeft, setTimeLeft] = useState<any>(calcTimeLeft()); const [timeLeft, setTimeLeft] = useState(calcTimeLeft());
useEffect(() => { useEffect(() => {
setTimeout(() => { setTimeout(() => {
setTimeLeft(calcTimeLeft()); setTimeLeft(calcTimeLeft());
if (timeLeft <= 1) { if (timeLeft && timeLeft <= 1) {
resetAttempts(); resetAttempts();
setStatus(TYPE.ENTER); setStatus(TYPE.ENTER);
} }
@ -46,11 +45,12 @@ const Timer = React.memo(({ time, theme, setStatus }: IPasscodeTimer) => {
return null; 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 Locked = React.memo(({ setStatus }: IPasscodeLocked) => {
const [lockedUntil, setLockedUntil] = useState<any>(null); const [lockedUntil, setLockedUntil] = useState<Date | null>(null);
const { theme } = useTheme();
const readItemFromStorage = async () => { const readItemFromStorage = async () => {
const l = await getLockedUntil(); const l = await getLockedUntil();
@ -63,9 +63,9 @@ const Locked = React.memo(({ theme, setStatus }: IPasscodeLocked) => {
return ( return (
<Grid style={[styles.grid, { backgroundColor: themes[theme].passcodeBackground }]}> <Grid style={[styles.grid, { backgroundColor: themes[theme].passcodeBackground }]}>
<LockIcon theme={theme} /> <LockIcon />
<Title text={I18n.t('Passcode_app_locked_title')} theme={theme} /> <Title text={I18n.t('Passcode_app_locked_title')} />
<Timer theme={theme} time={lockedUntil} setStatus={setStatus} /> <Timer time={lockedUntil} setStatus={setStatus} />
</Grid> </Grid>
); );
}); });

View File

@ -4,18 +4,22 @@ import { Row } from 'react-native-easy-grid';
import styles from './styles'; import styles from './styles';
import { themes } from '../../../constants/colors'; import { themes } from '../../../constants/colors';
import { useTheme } from '../../../theme';
interface IPasscodeSubtitle { interface IPasscodeSubtitle {
text: string; 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}> <Row style={styles.row}>
<View style={styles.subtitleView}> <View style={styles.subtitleView}>
<Text style={[styles.textSubtitle, { color: themes[theme].passcodeSecondary }]}>{text}</Text> <Text style={[styles.textSubtitle, { color: themes[theme].passcodeSecondary }]}>{text}</Text>
</View> </View>
</Row> </Row>
)); );
});
export default Subtitle; export default Subtitle;

View File

@ -4,18 +4,22 @@ import { Row } from 'react-native-easy-grid';
import styles from './styles'; import styles from './styles';
import { themes } from '../../../constants/colors'; import { themes } from '../../../constants/colors';
import { useTheme } from '../../../theme';
interface IPasscodeTitle { interface IPasscodeTitle {
text: string; 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}> <Row style={styles.row}>
<View style={styles.titleView}> <View style={styles.titleView}>
<Text style={[styles.textTitle, { color: themes[theme].passcodePrimary }]}>{text}</Text> <Text style={[styles.textTitle, { color: themes[theme].passcodePrimary }]}>{text}</Text>
</View> </View>
</Row> </Row>
)); );
});
export default Title; export default Title;

View File

@ -1,6 +1,7 @@
import React, { forwardRef, useImperativeHandle, useRef, useState } from 'react'; import React, { forwardRef, useImperativeHandle, useRef, useState } from 'react';
import { Col, Grid, Row } from 'react-native-easy-grid'; import { Col, Grid, Row } from 'react-native-easy-grid';
import range from 'lodash/range'; import range from 'lodash/range';
import { View } from 'react-native';
import * as Animatable from 'react-native-animatable'; import * as Animatable from 'react-native-animatable';
import * as Haptics from 'expo-haptics'; import * as Haptics from 'expo-haptics';
@ -10,12 +11,12 @@ import Dots from './Dots';
import { TYPE } from '../constants'; import { TYPE } from '../constants';
import { themes } from '../../../constants/colors'; import { themes } from '../../../constants/colors';
import { PASSCODE_LENGTH } from '../../../constants/localAuthentication'; import { PASSCODE_LENGTH } from '../../../constants/localAuthentication';
import { useTheme } from '../../../theme';
import LockIcon from './LockIcon'; import LockIcon from './LockIcon';
import Title from './Title'; import Title from './Title';
import Subtitle from './Subtitle'; import Subtitle from './Subtitle';
interface IPasscodeBase { interface IPasscodeBase {
theme: string;
type: string; type: string;
previousPasscode?: string; previousPasscode?: string;
title: string; title: string;
@ -26,25 +27,30 @@ interface IPasscodeBase {
onBiometryPress?(): void; onBiometryPress?(): void;
} }
const Base = forwardRef( export interface IBase {
( clearPasscode: () => void;
{ theme, type, onEndProcess, previousPasscode, title, subtitle, onError, showBiometry, onBiometryPress }: IPasscodeBase, wrongPasscode: () => void;
ref animate: (animation: Animatable.Animation, duration?: number) => void;
) => { }
const rootRef = useRef<any>();
const dotsRef = useRef<any>(); 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 [passcode, setPasscode] = useState('');
const clearPasscode = () => setPasscode(''); const clearPasscode = () => setPasscode('');
const wrongPasscode = () => { const wrongPasscode = () => {
clearPasscode(); clearPasscode();
dotsRef?.current?.shake(500); dotsRef?.current?.shake?.(500);
Haptics.notificationAsync(Haptics.NotificationFeedbackType.Error); Haptics.notificationAsync(Haptics.NotificationFeedbackType.Error);
}; };
const animate = (animation: string, duration = 500) => { const animate = (animation: Animatable.Animation, duration = 500) => {
rootRef?.current?.[animation](duration); rootRef?.current?.[animation]?.(duration);
}; };
const onPressNumber = (text: string) => const onPressNumber = (text: string) =>
@ -90,48 +96,48 @@ const Base = forwardRef(
return ( return (
<Animatable.View ref={rootRef} style={styles.container}> <Animatable.View ref={rootRef} style={styles.container}>
<Grid style={[styles.grid, { backgroundColor: themes[theme].passcodeBackground }]}> <Grid style={[styles.grid, { backgroundColor: themes[theme].passcodeBackground }]}>
<LockIcon theme={theme} /> <LockIcon />
<Title text={title} theme={theme} /> <Title text={title} />
<Subtitle text={subtitle!} theme={theme} /> {subtitle ? <Subtitle text={subtitle} /> : null}
<Row style={styles.row}> <Row style={styles.row}>
<Animatable.View ref={dotsRef}> <Animatable.View ref={dotsRef}>
<Dots passcode={passcode} theme={theme} length={PASSCODE_LENGTH} /> <Dots passcode={passcode} length={PASSCODE_LENGTH} />
</Animatable.View> </Animatable.View>
</Row> </Row>
<Row style={[styles.row, styles.buttonRow]}> <Row style={[styles.row, styles.buttonRow]}>
{range(1, 4).map((i: any) => ( {range(1, 4).map(i => (
<Col key={i} style={styles.colButton}> <Col key={i} style={styles.colButton}>
<Button text={i} theme={theme} onPress={onPressNumber} /> <Button text={i.toString()} onPress={onPressNumber} />
</Col> </Col>
))} ))}
</Row> </Row>
<Row style={[styles.row, styles.buttonRow]}> <Row style={[styles.row, styles.buttonRow]}>
{range(4, 7).map((i: any) => ( {range(4, 7).map(i => (
<Col key={i} style={styles.colButton}> <Col key={i} style={styles.colButton}>
<Button text={i} theme={theme} onPress={onPressNumber} /> <Button text={i.toString()} onPress={onPressNumber} />
</Col> </Col>
))} ))}
</Row> </Row>
<Row style={[styles.row, styles.buttonRow]}> <Row style={[styles.row, styles.buttonRow]}>
{range(7, 10).map((i: any) => ( {range(7, 10).map(i => (
<Col key={i} style={styles.colButton}> <Col key={i} style={styles.colButton}>
<Button text={i} theme={theme} onPress={onPressNumber} /> <Button text={i.toString()} onPress={onPressNumber} />
</Col> </Col>
))} ))}
</Row> </Row>
<Row style={[styles.row, styles.buttonRow]}> <Row style={[styles.row, styles.buttonRow]}>
{showBiometry ? ( {showBiometry ? (
<Col style={styles.colButton}> <Col style={styles.colButton}>
<Button icon='fingerprint' theme={theme} onPress={onBiometryPress} /> <Button icon='fingerprint' onPress={onBiometryPress} />
</Col> </Col>
) : ( ) : (
<Col style={styles.colButton} /> <Col style={styles.colButton} />
)} )}
<Col style={styles.colButton}> <Col style={styles.colButton}>
<Button text='0' theme={theme} onPress={onPressNumber} /> <Button text='0' onPress={onPressNumber} />
</Col> </Col>
<Col style={styles.colButton}> <Col style={styles.colButton}>
<Button icon='backspace' theme={theme} onPress={onPressDelete} /> <Button icon='backspace' onPress={onPressDelete} />
</Col> </Col>
</Row> </Row>
</Grid> </Grid>

View File

@ -2,24 +2,23 @@ import React, { useRef, useState } from 'react';
import * as Haptics from 'expo-haptics'; import * as Haptics from 'expo-haptics';
import { gestureHandlerRootHOC } from 'react-native-gesture-handler'; import { gestureHandlerRootHOC } from 'react-native-gesture-handler';
import Base from './Base'; import Base, { IBase } from './Base';
import { TYPE } from './constants'; import { TYPE } from './constants';
import I18n from '../../i18n'; import I18n from '../../i18n';
interface IPasscodeChoose { interface IPasscodeChoose {
theme: string;
force?: boolean; force?: boolean;
finishProcess: Function; finishProcess: Function;
} }
const PasscodeChoose = ({ theme, finishProcess, force = false }: IPasscodeChoose) => { const PasscodeChoose = ({ finishProcess, force = false }: IPasscodeChoose) => {
const chooseRef = useRef<any>(null); const chooseRef = useRef<IBase>(null);
const confirmRef = useRef<any>(null); const confirmRef = useRef<IBase>(null);
const [subtitle, setSubtitle] = useState(null); const [subtitle, setSubtitle] = useState(null);
const [status, setStatus] = useState(TYPE.CHOOSE); 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(() => { setTimeout(() => {
setStatus(TYPE.CONFIRM); setStatus(TYPE.CONFIRM);
setPreviouPasscode(p); setPreviouPasscode(p);
@ -43,7 +42,6 @@ const PasscodeChoose = ({ theme, finishProcess, force = false }: IPasscodeChoose
return ( return (
<Base <Base
ref={confirmRef} ref={confirmRef}
theme={theme}
type={TYPE.CONFIRM} type={TYPE.CONFIRM}
onEndProcess={changePasscode} onEndProcess={changePasscode}
previousPasscode={previousPasscode} previousPasscode={previousPasscode}
@ -56,7 +54,6 @@ const PasscodeChoose = ({ theme, finishProcess, force = false }: IPasscodeChoose
return ( return (
<Base <Base
ref={chooseRef} ref={chooseRef}
theme={theme}
type={TYPE.CHOOSE} type={TYPE.CHOOSE}
onEndProcess={firstStep} onEndProcess={firstStep}
title={I18n.t('Passcode_choose_title')} title={I18n.t('Passcode_choose_title')}

View File

@ -4,35 +4,29 @@ import { gestureHandlerRootHOC } from 'react-native-gesture-handler';
import * as Haptics from 'expo-haptics'; import * as Haptics from 'expo-haptics';
import { sha256 } from 'js-sha256'; import { sha256 } from 'js-sha256';
import Base from './Base'; import Base, { IBase } from './Base';
import Locked from './Base/Locked'; import Locked from './Base/Locked';
import { TYPE } from './constants'; import { TYPE } from './constants';
import { ATTEMPTS_KEY, LOCKED_OUT_TIMER_KEY, MAX_ATTEMPTS, PASSCODE_KEY } from '../../constants/localAuthentication'; import { ATTEMPTS_KEY, LOCKED_OUT_TIMER_KEY, MAX_ATTEMPTS, PASSCODE_KEY } from '../../constants/localAuthentication';
import { biometryAuth, resetAttempts } from '../../utils/localAuthentication'; import { biometryAuth, resetAttempts } from '../../utils/localAuthentication';
import { getDiff, getLockedUntil } from './utils'; import { getDiff, getLockedUntil } from './utils';
import UserPreferences from '../../lib/userPreferences'; import { useUserPreferences } from '../../lib/userPreferences';
import I18n from '../../i18n'; import I18n from '../../i18n';
interface IPasscodePasscodeEnter { interface IPasscodePasscodeEnter {
theme: string;
hasBiometry: boolean; hasBiometry: boolean;
finishProcess: Function; finishProcess: Function;
} }
const PasscodeEnter = ({ theme, hasBiometry, finishProcess }: IPasscodePasscodeEnter) => { const PasscodeEnter = ({ hasBiometry, finishProcess }: IPasscodePasscodeEnter) => {
const ref = useRef(null); const ref = useRef<IBase>(null);
let attempts: any = 0; let attempts = 0;
let lockedUntil: any = false; let lockedUntil: any = false;
const [passcode, setPasscode] = useState(null); const [passcode] = useUserPreferences(PASSCODE_KEY);
const [status, setStatus] = useState(null); const [status, setStatus] = useState<TYPE | null>(null);
const { getItem: getAttempts, setItem: setAttempts } = useAsyncStorage(ATTEMPTS_KEY); const { setItem: setAttempts } = useAsyncStorage(ATTEMPTS_KEY);
const { setItem: setLockedUntil } = useAsyncStorage(LOCKED_OUT_TIMER_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 () => { const biometry = async () => {
if (hasBiometry && status === TYPE.ENTER) { if (hasBiometry && status === TYPE.ENTER) {
const result = await biometryAuth(); const result = await biometryAuth();
@ -50,13 +44,11 @@ const PasscodeEnter = ({ theme, hasBiometry, finishProcess }: IPasscodePasscodeE
await resetAttempts(); await resetAttempts();
setStatus(TYPE.ENTER); setStatus(TYPE.ENTER);
} else { } else {
attempts = await getAttempts();
setStatus(TYPE.LOCKED); setStatus(TYPE.LOCKED);
} }
} else { } else {
setStatus(TYPE.ENTER); setStatus(TYPE.ENTER);
} }
await fetchPasscode();
biometry(); biometry();
}; };
@ -64,7 +56,7 @@ const PasscodeEnter = ({ theme, hasBiometry, finishProcess }: IPasscodePasscodeE
readStorage(); readStorage();
}, [status]); }, [status]);
const onEndProcess = (p: any) => { const onEndProcess = (p: string) => {
setTimeout(() => { setTimeout(() => {
if (sha256(p) === passcode) { if (sha256(p) === passcode) {
finishProcess(); finishProcess();
@ -75,8 +67,7 @@ const PasscodeEnter = ({ theme, hasBiometry, finishProcess }: IPasscodePasscodeE
setLockedUntil(new Date().toISOString()); setLockedUntil(new Date().toISOString());
Haptics.notificationAsync(Haptics.NotificationFeedbackType.Error); Haptics.notificationAsync(Haptics.NotificationFeedbackType.Error);
} else { } else {
// @ts-ignore ref?.current?.wrongPasscode();
ref.current.wrongPasscode();
setAttempts(attempts?.toString()); setAttempts(attempts?.toString());
Haptics.notificationAsync(Haptics.NotificationFeedbackType.Warning); Haptics.notificationAsync(Haptics.NotificationFeedbackType.Warning);
} }
@ -85,13 +76,12 @@ const PasscodeEnter = ({ theme, hasBiometry, finishProcess }: IPasscodePasscodeE
}; };
if (status === TYPE.LOCKED) { if (status === TYPE.LOCKED) {
return <Locked theme={theme} setStatus={setStatus} />; return <Locked setStatus={setStatus} />;
} }
return ( return (
<Base <Base
ref={ref} ref={ref}
theme={theme}
type={TYPE.ENTER} type={TYPE.ENTER}
title={I18n.t('Passcode_enter_title')} title={I18n.t('Passcode_enter_title')}
showBiometry={hasBiometry} showBiometry={hasBiometry}

View File

@ -1,6 +1,6 @@
export const TYPE: any = { export enum TYPE {
CHOOSE: 'choose', CHOOSE = 'choose',
CONFIRM: 'confirm', CONFIRM = 'confirm',
ENTER: 'enter', ENTER = 'enter',
LOCKED: 'locked' LOCKED = 'locked'
}; }

View File

@ -4,11 +4,11 @@ import moment from 'moment';
import { LOCKED_OUT_TIMER_KEY, TIME_TO_LOCK } from '../../constants/localAuthentication'; import { LOCKED_OUT_TIMER_KEY, TIME_TO_LOCK } from '../../constants/localAuthentication';
export const getLockedUntil = async () => { 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) { if (t) {
return moment(t).add(TIME_TO_LOCK); return moment(t).add(TIME_TO_LOCK).toDate();
} }
return null; 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();

View File

@ -10,6 +10,7 @@ import sharedStyles from '../views/Styles';
import { themes } from '../constants/colors'; import { themes } from '../constants/colors';
import { withTheme } from '../theme'; import { withTheme } from '../theme';
import { TGetCustomEmoji } from '../definitions/IEmoji'; import { TGetCustomEmoji } from '../definitions/IEmoji';
import { TMessageModel, ILoggedUser } from '../definitions';
import SafeAreaView from './SafeAreaView'; import SafeAreaView from './SafeAreaView';
const styles = StyleSheet.create({ const styles = StyleSheet.create({
@ -65,23 +66,25 @@ interface IItem {
usernames: any; usernames: any;
emoji: string; emoji: string;
}; };
user?: { username: any }; user?: Pick<ILoggedUser, 'username'>;
baseUrl?: string; baseUrl?: string;
getCustomEmoji?: TGetCustomEmoji; getCustomEmoji?: TGetCustomEmoji;
theme?: string; theme?: string;
} }
interface IModalContent { interface IModalContent {
message?: { message?: TMessageModel;
reactions: any;
};
onClose: Function; onClose: Function;
theme: string; theme: string;
} }
interface IReactionsModal { interface IReactionsModal {
message?: any;
user?: Pick<ILoggedUser, 'username'>;
isVisible: boolean; isVisible: boolean;
onClose(): void; onClose(): void;
baseUrl: string;
getCustomEmoji?: TGetCustomEmoji;
theme: string; theme: string;
} }

View File

@ -1,22 +1,14 @@
import React from 'react'; import React from 'react';
import { import { StyleSheet, Text, TextInput as RNTextInput, TextInputProps, View } from 'react-native';
NativeSyntheticEvent,
StyleSheet,
TextInput as RNTextInput,
Text,
TextInputFocusEventData,
TextInputProps,
View
} from 'react-native';
import Touchable from 'react-native-platform-touchable'; import Touchable from 'react-native-platform-touchable';
import TextInput from '../presentation/TextInput'; import { themes } from '../constants/colors';
import I18n from '../i18n'; import I18n from '../i18n';
import { CustomIcon } from '../lib/Icons'; import { CustomIcon } from '../lib/Icons';
import sharedStyles from '../views/Styles'; import TextInput from '../presentation/TextInput';
import { withTheme } from '../theme'; import { useTheme } from '../theme';
import { themes } from '../constants/colors';
import { isIOS } from '../utils/deviceInfo'; import { isIOS } from '../utils/deviceInfo';
import sharedStyles from '../views/Styles';
const styles = StyleSheet.create({ const styles = StyleSheet.create({
container: { container: {
@ -52,41 +44,32 @@ const styles = StyleSheet.create({
} }
}); });
interface ISearchBox { interface ISearchBox extends TextInputProps {
value?: string; value?: string;
onChangeText: TextInputProps['onChangeText'];
onSubmitEditing?: () => void;
hasCancel?: boolean; hasCancel?: boolean;
onCancelPress?: Function; onCancelPress?: Function;
theme?: string;
inputRef?: React.Ref<RNTextInput>; 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}> <Touchable onPress={onCancelPress} style={styles.cancel}>
<Text style={[styles.cancelText, { color: themes[theme].headerTintColor }]}>{I18n.t('Cancel')}</Text> <Text style={[styles.cancelText, { color: themes[theme].headerTintColor }]}>{I18n.t('Cancel')}</Text>
</Touchable> </Touchable>
); );
};
const SearchBox = ({ const SearchBox = ({ hasCancel, onCancelPress, inputRef, ...props }: ISearchBox): React.ReactElement => {
onChangeText, const { theme } = useTheme();
onSubmitEditing, return (
testID,
hasCancel,
onCancelPress,
inputRef,
theme,
...props
}: ISearchBox) => (
<View <View
style={[ style={[
styles.container, 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 }]}> <View style={[styles.searchBox, { backgroundColor: themes[theme].searchboxBackground }]}>
<CustomIcon name='search' size={14} color={themes[theme!].auxiliaryText} /> <CustomIcon name='search' size={14} color={themes[theme].auxiliaryText} />
<TextInput <TextInput
ref={inputRef} ref={inputRef}
autoCapitalize='none' autoCapitalize='none'
@ -96,16 +79,14 @@ const SearchBox = ({
placeholder={I18n.t('Search')} placeholder={I18n.t('Search')}
returnKeyType='search' returnKeyType='search'
style={styles.input} style={styles.input}
testID={testID}
underlineColorAndroid='transparent' underlineColorAndroid='transparent'
onChangeText={onChangeText} theme={theme}
onSubmitEditing={onSubmitEditing}
theme={theme!}
{...props} {...props}
/> />
</View> </View>
{hasCancel ? CancelButton(onCancelPress!, theme!) : null} {hasCancel ? <CancelButton onCancelPress={onCancelPress} /> : null}
</View> </View>
); );
};
export default withTheme(SearchBox); export default SearchBox;

View File

@ -2,22 +2,27 @@ import React from 'react';
import { StatusBar as StatusBarRN } from 'react-native'; import { StatusBar as StatusBarRN } from 'react-native';
import { themes } from '../constants/colors'; 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 { interface IStatusBar {
theme?: string; barStyle?: keyof typeof supportedStyles;
barStyle?: any;
backgroundColor?: string; backgroundColor?: string;
} }
const StatusBar = React.memo(({ theme, barStyle, backgroundColor }: IStatusBar) => { const StatusBar = React.memo(({ barStyle, backgroundColor }: IStatusBar) => {
const { theme } = useTheme();
if (!barStyle) { if (!barStyle) {
barStyle = 'light-content'; barStyle = 'light-content';
if (theme === 'light') { if (theme === 'light') {
barStyle = 'dark-content'; 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;

View File

@ -52,10 +52,7 @@ const styles = StyleSheet.create({
export interface IRCTextInputProps extends TextInputProps { export interface IRCTextInputProps extends TextInputProps {
label?: string; label?: string;
error?: { error?: any;
error: any;
reason: any;
};
loading?: boolean; loading?: boolean;
containerStyle?: StyleProp<ViewStyle>; containerStyle?: StyleProp<ViewStyle>;
inputStyle?: StyleProp<TextStyle>; inputStyle?: StyleProp<TextStyle>;
@ -68,7 +65,11 @@ export interface IRCTextInputProps extends TextInputProps {
theme: string; 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 = { static defaultProps = {
error: {}, error: {},
theme: 'light' theme: 'light'
@ -116,12 +117,11 @@ export default class RCTextInput extends React.PureComponent<IRCTextInputProps,
get loading() { get loading() {
const { theme } = this.props; 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 = () => { tooglePassword = () => {
this.setState((prevState: any) => ({ showPassword: !prevState.showPassword })); this.setState(prevState => ({ showPassword: !prevState.showPassword }));
}; };
render() { render() {

View File

@ -41,7 +41,7 @@ const styles = StyleSheet.create({
}); });
interface IThreadDetails { interface IThreadDetails {
item: Partial<TThreadModel>; item: Pick<TThreadModel, 'tcount' | 'replies' | 'id'>;
user: { user: {
id: string; id: string;
}; };
@ -52,9 +52,9 @@ interface IThreadDetails {
const ThreadDetails = ({ item, user, badgeColor, toggleFollowThread, style }: IThreadDetails): JSX.Element => { const ThreadDetails = ({ item, user, badgeColor, toggleFollowThread, style }: IThreadDetails): JSX.Element => {
const { theme } = useTheme(); const { theme } = useTheme();
let { tcount } = item; let count: string | number | undefined | null = item.tcount;
if (tcount && tcount >= 1000) { if (count && count >= 1000) {
tcount = '+999'; count = '+999';
} }
let replies: number | string = item?.replies?.length ?? 0; let replies: number | string = item?.replies?.length ?? 0;
@ -62,21 +62,21 @@ const ThreadDetails = ({ item, user, badgeColor, toggleFollowThread, style }: IT
replies = '+999'; replies = '+999';
} }
const isFollowing = item.replies?.find((u: any) => u === user?.id); const isFollowing = item.replies?.find((u: string) => u === user?.id);
return ( return (
<View style={[styles.container, style]}> <View style={[styles.container, style]}>
<View style={styles.detailsContainer}> <View style={styles.detailsContainer}>
<View style={styles.detailContainer}> <View style={styles.detailContainer}>
<CustomIcon name='threads' size={24} color={themes[theme!].auxiliaryText} /> <CustomIcon name='threads' size={24} color={themes[theme].auxiliaryText} />
<Text style={[styles.detailText, { color: themes[theme!].auxiliaryText }]} numberOfLines={1}> <Text style={[styles.detailText, { color: themes[theme].auxiliaryText }]} numberOfLines={1}>
{tcount} {count}
</Text> </Text>
</View> </View>
<View style={styles.detailContainer}> <View style={styles.detailContainer}>
<CustomIcon name='user' size={24} color={themes[theme!].auxiliaryText} /> <CustomIcon name='user' size={24} color={themes[theme].auxiliaryText} />
<Text style={[styles.detailText, { color: themes[theme!].auxiliaryText }]} numberOfLines={1}> <Text style={[styles.detailText, { color: themes[theme].auxiliaryText }]} numberOfLines={1}>
{replies} {replies}
</Text> </Text>
</View> </View>
@ -87,7 +87,7 @@ const ThreadDetails = ({ item, user, badgeColor, toggleFollowThread, style }: IT
<CustomIcon <CustomIcon
size={24} size={24}
name={isFollowing ? 'notification' : 'notification-disabled'} name={isFollowing ? 'notification' : 'notification-disabled'}
color={themes[theme!].auxiliaryTintColor} color={themes[theme].auxiliaryTintColor}
/> />
</Touchable> </Touchable>
</View> </View>

View File

@ -26,9 +26,9 @@ interface IToastProps {
} }
class Toast extends React.Component<IToastProps, any> { class Toast extends React.Component<IToastProps, any> {
private listener: any; private listener?: Function;
private toast: any; private toast: EasyToast | null | undefined;
componentDidMount() { componentDidMount() {
this.listener = EventEmitter.addEventListener(LISTENER, this.showToast); this.listener = EventEmitter.addEventListener(LISTENER, this.showToast);
@ -43,12 +43,14 @@ class Toast extends React.Component<IToastProps, any> {
} }
componentWillUnmount() { componentWillUnmount() {
if (this.listener) {
EventEmitter.removeListener(LISTENER, 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) { if (this.toast && this.toast.show) {
this.toast.show(message, 1000); this.toast.show(message, 1000);
} }

View File

@ -9,21 +9,36 @@ import { connect } from 'react-redux';
import TextInput from '../TextInput'; import TextInput from '../TextInput';
import I18n from '../../i18n'; import I18n from '../../i18n';
import EventEmitter from '../../utils/events'; import EventEmitter from '../../utils/events';
import { withTheme } from '../../theme'; import { useTheme } from '../../theme';
import { themes } from '../../constants/colors'; import { themes } from '../../constants/colors';
import Button from '../Button'; import Button from '../Button';
import sharedStyles from '../../views/Styles'; import sharedStyles from '../../views/Styles';
import RocketChat from '../../lib/rocketchat'; import RocketChat from '../../lib/rocketchat';
import styles from './styles'; import styles from './styles';
import { IApplicationState } from '../../definitions';
export const TWO_FACTOR = 'TWO_FACTOR'; export const TWO_FACTOR = 'TWO_FACTOR';
interface ITwoFactor { interface IMethodsProp {
theme?: string; text: string;
isMasterDetail: boolean; 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: { totp: {
text: 'Open_your_authentication_app_and_enter_the_code', text: 'Open_your_authentication_app_and_enter_the_code',
keyboardType: 'numeric' 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 [visible, setVisible] = useState(false);
const [data, setData] = useState<any>({}); const [data, setData] = useState<EventListenerMethod>({});
const [code, setCode] = useState<any>(''); const [code, setCode] = useState<string>('');
const method = methods[data.method]; const method = data.method ? methods[data.method] : null;
const isEmail = data.method === 'email'; const isEmail = data.method === 'email';
const sendEmail = () => RocketChat.sendEmailCode(); const sendEmail = () => RocketChat.sendEmailCode();
useDeepCompareEffect(() => { useDeepCompareEffect(() => {
@ -59,7 +74,7 @@ const TwoFactor = React.memo(({ theme, isMasterDetail }: ITwoFactor) => {
} }
}, [data]); }, [data]);
const showTwoFactor = (args: any) => setData(args); const showTwoFactor = (args: EventListenerMethod) => setData(args);
useEffect(() => { useEffect(() => {
const listener = EventEmitter.addEventListener(TWO_FACTOR, showTwoFactor); const listener = EventEmitter.addEventListener(TWO_FACTOR, showTwoFactor);
@ -87,26 +102,19 @@ const TwoFactor = React.memo(({ theme, isMasterDetail }: ITwoFactor) => {
setData({}); setData({});
}; };
const color = themes[theme!].titleText; const color = themes[theme].titleText;
return ( return (
<Modal <Modal avoidKeyboard useNativeDriver isVisible={visible} hideModalContentWhileAnimating>
// @ts-ignore
transparent
avoidKeyboard
useNativeDriver
isVisible={visible}
hideModalContentWhileAnimating>
<View style={styles.container} testID='two-factor'> <View style={styles.container} testID='two-factor'>
<View <View
style={[ style={[
styles.content, styles.content,
isMasterDetail && [sharedStyles.modalFormSheet, styles.tablet], 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> <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} {method?.text ? <Text style={[styles.subtitle, { color }]}>{I18n.t(method.text)}</Text> : null}
<TextInput <TextInput
/* @ts-ignore*/
value={code} value={code}
theme={theme} theme={theme}
inputRef={(e: any) => InteractionManager.runAfterInteractions(() => e?.getNativeRef()?.focus())} inputRef={(e: any) => InteractionManager.runAfterInteractions(() => e?.getNativeRef()?.focus())}
@ -116,19 +124,19 @@ const TwoFactor = React.memo(({ theme, isMasterDetail }: ITwoFactor) => {
onSubmitEditing={onSubmit} onSubmitEditing={onSubmit}
keyboardType={method?.keyboardType} keyboardType={method?.keyboardType}
secureTextEntry={method?.secureTextEntry} 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' testID='two-factor-input'
/> />
{isEmail && ( {isEmail ? (
<Text style={[styles.sendEmail, { color }]} onPress={sendEmail}> <Text style={[styles.sendEmail, { color }]} onPress={sendEmail}>
{I18n.t('Send_me_the_code_again')} {I18n.t('Send_me_the_code_again')}
</Text> </Text>
)} ) : null}
<View style={styles.buttonContainer}> <View style={styles.buttonContainer}>
<Button <Button
title={I18n.t('Cancel')} title={I18n.t('Cancel')}
type='secondary' type='secondary'
backgroundColor={themes[theme!].chatComponentBackground} backgroundColor={themes[theme].chatComponentBackground}
style={styles.button} style={styles.button}
onPress={onCancel} onPress={onCancel}
theme={theme} 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 isMasterDetail: state.app.isMasterDetail
}); });
export default connect(mapStateToProps)(withTheme(TwoFactor)); export default connect(mapStateToProps)(TwoFactor);

View File

@ -3,25 +3,18 @@ import { BLOCK_CONTEXT } from '@rocket.chat/ui-kit';
import Button from '../Button'; import Button from '../Button';
import I18n from '../../i18n'; import I18n from '../../i18n';
import { IActions } from './interfaces';
interface IActions {
blockId: string;
appId: string;
elements: any[];
parser: any;
theme: string;
}
export const Actions = ({ blockId, appId, elements, parser, theme }: IActions) => { export const Actions = ({ blockId, appId, elements, parser, theme }: IActions) => {
const [showMoreVisible, setShowMoreVisible] = useState(() => elements.length > 5); const [showMoreVisible, setShowMoreVisible] = useState(() => elements && elements.length > 5);
const renderedElements = showMoreVisible ? elements.slice(0, 5) : elements; const renderedElements = showMoreVisible ? elements?.slice(0, 5) : elements;
const Elements = () => const Elements = () => (
renderedElements.map((element: any) => parser.renderActions({ blockId, appId, ...element }, BLOCK_CONTEXT.ACTION, parser)); <>{renderedElements?.map(element => parser?.renderActions({ blockId, appId, ...element }, BLOCK_CONTEXT.ACTION, parser))}</>
);
return ( return (
<> <>
{/* @ts-ignore*/}
<Elements /> <Elements />
{showMoreVisible && <Button theme={theme} title={I18n.t('Show_more')} onPress={() => setShowMoreVisible(false)} />} {showMoreVisible && <Button theme={theme} title={I18n.t('Show_more')} onPress={() => setShowMoreVisible(false)} />}
</> </>

View File

@ -1,8 +1,9 @@
import React from 'react'; import React from 'react';
import { StyleSheet, View } from 'react-native'; import { StyleSheet, View } from 'react-native';
import PropTypes from 'prop-types';
import { BLOCK_CONTEXT } from '@rocket.chat/ui-kit'; import { BLOCK_CONTEXT } from '@rocket.chat/ui-kit';
import { IContext } from './interfaces';
const styles = StyleSheet.create({ const styles = StyleSheet.create({
container: { container: {
minHeight: 36, minHeight: 36,
@ -11,13 +12,6 @@ const styles = StyleSheet.create({
} }
}); });
export const Context = ({ elements, parser }: any) => ( export const Context = ({ elements, parser }: IContext) => (
<View style={styles.container}> <View style={styles.container}>{elements?.map(element => parser?.renderContext(element, BLOCK_CONTEXT.CONTEXT, parser))}</View>
{elements.map((element: any) => parser.renderContext(element, BLOCK_CONTEXT.CONTEXT, parser))}
</View>
); );
Context.propTypes = {
elements: PropTypes.array,
parser: PropTypes.object
};

View File

@ -12,6 +12,7 @@ import sharedStyles from '../../views/Styles';
import { CustomIcon } from '../../lib/Icons'; import { CustomIcon } from '../../lib/Icons';
import { isAndroid } from '../../utils/deviceInfo'; import { isAndroid } from '../../utils/deviceInfo';
import ActivityIndicator from '../ActivityIndicator'; import ActivityIndicator from '../ActivityIndicator';
import { IDatePicker } from './interfaces';
const styles = StyleSheet.create({ const styles = StyleSheet.create({
input: { 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) => { export const DatePicker = ({ element, language, action, context, theme, loading, value, error }: IDatePicker) => {
const [show, onShow] = useState(false); 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 [currentDate, onChangeDate] = useState(new Date(initial_date || value));
const onChange = ({ nativeEvent: { timestamp } }: any, date: any) => { const onChange = ({ nativeEvent: { timestamp } }: any, date: any) => {

View File

@ -5,6 +5,8 @@ import { BLOCK_CONTEXT } from '@rocket.chat/ui-kit';
import ImageContainer from '../message/Image'; import ImageContainer from '../message/Image';
import Navigation from '../../lib/Navigation'; import Navigation from '../../lib/Navigation';
import { IThumb, IImage, IElement } from './interfaces';
import { TThemeMode } from '../../definitions/ITheme';
const styles = StyleSheet.create({ const styles = StyleSheet.create({
image: { image: {
@ -15,44 +17,25 @@ const styles = StyleSheet.create({
} }
}); });
interface IThumb { const ThumbContext = (args: 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) => (
<View style={styles.mediaContext}> <View style={styles.mediaContext}>
<Thumb size={20} {...args} /> <Thumb size={20} {...args} />
</View> </View>
); );
export const Thumb = ({ element, size = 88 }: IThumb) => ( 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 showAttachment = (attachment: any) => Navigation.navigate('AttachmentView', { attachment });
const { imageUrl } = element; const imageUrl = element?.imageUrl ?? '';
// @ts-ignore // @ts-ignore
// TODO: delete ts-ignore after refactor Markdown and ImageContainer
return <ImageContainer file={{ image_url: imageUrl }} imageUrl={imageUrl} showAttachment={showAttachment} theme={theme} />; 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) { switch (context) {
case BLOCK_CONTEXT.SECTION: case BLOCK_CONTEXT.SECTION:
return <Thumb element={element} />; 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);

View File

@ -4,6 +4,7 @@ import { BLOCK_CONTEXT } from '@rocket.chat/ui-kit';
import sharedStyles from '../../views/Styles'; import sharedStyles from '../../views/Styles';
import { themes } from '../../constants/colors'; import { themes } from '../../constants/colors';
import { IInput } from './interfaces';
const styles = StyleSheet.create({ const styles = StyleSheet.create({
container: { 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) => ( export const Input = ({ element, parser, label, description, error, hint, theme }: IInput) => (
<View style={styles.container}> <View style={styles.container}>
{label ? ( {label ? (

View File

@ -41,7 +41,7 @@ const Item = ({ item, selected, onSelect, theme }: IItem) => {
<> <>
{item.imageUrl ? <FastImage style={styles.itemImage} source={{ uri: item.imageUrl }} /> : null} {item.imageUrl ? <FastImage style={styles.itemImage} source={{ uri: item.imageUrl }} /> : null}
<Text style={{ color: themes[theme].titleText }}>{textParser([item.text])}</Text> <Text style={{ color: themes[theme].titleText }}>{textParser([item.text])}</Text>
{selected ? <Check theme={theme} /> : null} {selected ? <Check /> : null}
</> </>
</Touchable> </Touchable>
); );

View File

@ -24,7 +24,7 @@ interface IMultiSelect {
multiselect?: boolean; multiselect?: boolean;
onSearch?: () => void; onSearch?: () => void;
onClose?: () => void; onClose?: () => void;
inputStyle: object; inputStyle?: object;
value?: any[]; value?: any[];
disabled?: boolean | object; disabled?: boolean | object;
theme: string; theme: string;
@ -126,7 +126,6 @@ export const MultiSelect = React.memo(
<View style={[styles.content, { backgroundColor: themes[theme].backgroundColor }]}> <View style={[styles.content, { backgroundColor: themes[theme].backgroundColor }]}>
<TextInput <TextInput
testID='multi-select-search' testID='multi-select-search'
/* @ts-ignore*/
onChangeText={onSearch || onSearchChange} onChangeText={onSearch || onSearchChange}
placeholder={I18n.t('Search')} placeholder={I18n.t('Search')}
theme={theme} theme={theme}

View File

@ -8,32 +8,7 @@ import ActivityIndicator from '../ActivityIndicator';
import { themes } from '../../constants/colors'; import { themes } from '../../constants/colors';
import { BUTTON_HIT_SLOP } from '../message/utils'; import { BUTTON_HIT_SLOP } from '../message/utils';
import * as List from '../List'; import * as List from '../List';
import { IOption, IOptions, IOverflow } from './interfaces';
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;
}
const keyExtractor = (item: any) => item.value; 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) => { 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 [show, onShow] = useState(false);
const onOptionPress = ({ value }: any) => { const onOptionPress = ({ value }: any) => {
@ -82,8 +58,7 @@ export const Overflow = ({ element, loading, action, parser, theme }: IOverflow)
return ( return (
<> <>
<Touchable <Touchable
/* @ts-ignore*/ ref={(ref: any) => (touchable[blockId] = ref)}
ref={ref => (touchable[blockId] = ref)}
background={Touchable.Ripple(themes[theme].bannerBackground)} background={Touchable.Ripple(themes[theme].bannerBackground)}
onPress={() => onShow(!show)} onPress={() => onShow(!show)}
hitSlop={BUTTON_HIT_SLOP} hitSlop={BUTTON_HIT_SLOP}
@ -91,7 +66,7 @@ export const Overflow = ({ element, loading, action, parser, theme }: IOverflow)
{!loading ? ( {!loading ? (
<CustomIcon size={18} name='kebab' color={themes[theme].bodyText} /> <CustomIcon size={18} name='kebab' color={themes[theme].bodyText} />
) : ( ) : (
<ActivityIndicator style={styles.loading} theme={theme} /> <ActivityIndicator style={styles.loading} />
)} )}
</Touchable> </Touchable>
<Popover <Popover

View File

@ -3,6 +3,7 @@ import { StyleSheet, Text, View } from 'react-native';
import { BLOCK_CONTEXT } from '@rocket.chat/ui-kit'; import { BLOCK_CONTEXT } from '@rocket.chat/ui-kit';
import { themes } from '../../constants/colors'; import { themes } from '../../constants/colors';
import { IAccessoryComponent, IFields, ISection } from './interfaces';
const styles = StyleSheet.create({ const styles = StyleSheet.create({
content: { content: {
@ -23,36 +24,16 @@ const styles = StyleSheet.create({
} }
}); });
interface IAccessory { const Accessory = ({ element, parser }: IAccessoryComponent) =>
blockId?: string; parser.renderAccessories({ ...element }, BLOCK_CONTEXT.SECTION, parser);
appId?: string;
element: any;
parser: any;
}
interface IFields { const Fields = ({ fields, parser, theme }: IFields) => (
fields: any; <>
parser: any; {fields.map(field => (
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) => (
<Text style={[styles.text, styles.field, { color: themes[theme].bodyText }]}>{parser.text(field)}</Text> <Text style={[styles.text, styles.field, { color: themes[theme].bodyText }]}>{parser.text(field)}</Text>
)); ))}
</>
);
const accessoriesRight = ['image', 'overflow']; const accessoriesRight = ['image', 'overflow'];

View File

@ -20,6 +20,7 @@ import { Input } from './Input';
import { DatePicker } from './DatePicker'; import { DatePicker } from './DatePicker';
import { Overflow } from './Overflow'; import { Overflow } from './Overflow';
import { ThemeContext } from '../../theme'; import { ThemeContext } from '../../theme';
import { BlockContext, IButton, IInputIndex, IParser, IText } from './interfaces';
const styles = StyleSheet.create({ const styles = StyleSheet.create({
input: { input: {
@ -42,8 +43,12 @@ const styles = StyleSheet.create({
const plainText = ({ text } = { text: '' }) => text; const plainText = ({ text } = { text: '' }) => text;
class MessageParser extends UiKitParserMessage { class MessageParser extends UiKitParserMessage {
text({ text, type }: any = { text: '' }, context: any) { get current() {
const { theme }: any = useContext(ThemeContext); return this as unknown as IParser;
}
text({ text, type }: Partial<IText> = { text: '' }, context: BlockContext) {
const { theme } = useContext(ThemeContext);
if (type !== 'mrkdwn') { if (type !== 'mrkdwn') {
return <Text style={[styles.text, { color: themes[theme].bodyText }]}>{text}</Text>; 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 }]} />; 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 { text, value, actionId, style } = element;
const [{ loading }, action]: any = useBlockContext(element, context); const [{ loading }, action] = useBlockContext(element, context);
const { theme } = useContext(ThemeContext); const { theme } = useContext(ThemeContext);
return ( return (
<Button <Button
@ -73,7 +78,7 @@ class MessageParser extends UiKitParserMessage {
} }
divider() { divider() {
const { theme }: any = useContext(ThemeContext); const { theme } = useContext(ThemeContext);
// @ts-ignore // @ts-ignore
return <Divider theme={theme} />; return <Divider theme={theme} />;
} }
@ -91,7 +96,7 @@ class MessageParser extends UiKitParserMessage {
overflow(element: any, context: any) { overflow(element: any, context: any) {
const [{ loading }, action]: any = useBlockContext(element, context); const [{ loading }, action]: any = useBlockContext(element, context);
const { theme }: any = useContext(ThemeContext); 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) { 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 [{ error }]: any = useBlockContext({ ...element, appId, blockId }, context);
const { theme }: any = useContext(ThemeContext); const { theme }: any = useContext(ThemeContext);
return ( return (
<Input <Input
parser={this} parser={this.current}
element={{ ...element, appId, blockId }} element={{ ...element, appId, blockId }}
label={plainText(label)} label={plainText(label)}
description={plainText(description)} description={plainText(description)}
@ -178,16 +187,14 @@ class ModalParser extends UiKitParserModal {
return ( return (
// @ts-ignore // @ts-ignore
<TextInput <TextInput
id={actionId} key={actionId}
placeholder={plainText(placeholder)} placeholder={plainText(placeholder)}
onInput={action}
multiline={multiline} multiline={multiline}
loading={loading} loading={loading}
onChangeText={(text: any) => action({ value: text })} onChangeText={text => action({ value: text })}
inputStyle={multiline && styles.multiline} inputStyle={multiline && styles.multiline}
containerStyle={styles.input} containerStyle={styles.input}
value={value} value={value}
// @ts-ignore
error={{ error }} error={{ error }}
theme={theme} theme={theme}
/> />

View File

@ -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[];
}

View File

@ -2,6 +2,8 @@
import React, { useContext, useState } from 'react'; import React, { useContext, useState } from 'react';
import { BLOCK_CONTEXT } from '@rocket.chat/ui-kit'; import { BLOCK_CONTEXT } from '@rocket.chat/ui-kit';
import { BlockContext } from './interfaces';
export const textParser = ([{ text }]: any) => text; export const textParser = ([{ text }]: any) => text;
export const defaultContext: any = { export const defaultContext: any = {
@ -13,7 +15,19 @@ export const defaultContext: any = {
export const KitContext = React.createContext(defaultContext); 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 { action, appId: appIdFromContext, viewId, state, language, errors, values = {} } = useContext(KitContext);
const { value = initialValue } = values[actionId] || {}; const { value = initialValue } = values[actionId] || {};
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);

View File

@ -1,5 +1,5 @@
import React from 'react'; 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 { CELL_WIDTH } from './TableCell';
import styles from './styles'; import styles from './styles';
@ -19,7 +19,7 @@ const Table = React.memo(({ children, numColumns, theme }: ITable) => {
const getTableWidth = () => numColumns * CELL_WIDTH; const getTableWidth = () => numColumns * CELL_WIDTH;
const renderRows = (drawExtraBorders = true) => { const renderRows = (drawExtraBorders = true) => {
const tableStyle = [styles.table, { borderColor: themes[theme].borderColor }]; const tableStyle: ViewStyle[] = [styles.table, { borderColor: themes[theme].borderColor }];
if (drawExtraBorders) { if (drawExtraBorders) {
tableStyle.push(styles.tableExtraBorders); tableStyle.push(styles.tableExtraBorders);
} }

View File

@ -1,5 +1,5 @@
import React from 'react'; import React from 'react';
import { Text, View } from 'react-native'; import { Text, View, ViewStyle } from 'react-native';
import { themes } from '../../constants/colors'; import { themes } from '../../constants/colors';
import styles from './styles'; import styles from './styles';
@ -14,7 +14,7 @@ interface ITableCell {
export const CELL_WIDTH = 100; export const CELL_WIDTH = 100;
const TableCell = React.memo(({ isLastCell, align, children, theme }: ITableCell) => { 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) { if (!isLastCell) {
cellStyle.push(styles.cellRightBorder); cellStyle.push(styles.cellRightBorder);
} }

View File

@ -1,5 +1,5 @@
import React from 'react'; import React from 'react';
import { View } from 'react-native'; import { View, ViewStyle } from 'react-native';
import { themes } from '../../constants/colors'; import { themes } from '../../constants/colors';
import styles from './styles'; import styles from './styles';
@ -11,7 +11,7 @@ interface ITableRow {
} }
const TableRow = React.memo(({ isLastRow, children: _children, theme }: 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) { if (!isLastRow) {
rowStyle.push(styles.rowBottomBorder); rowStyle.push(styles.rowBottomBorder);
} }

View File

@ -280,6 +280,7 @@ class Markdown extends PureComponent<IMarkdownProps, any> {
renderHeading = ({ children, level }: any) => { renderHeading = ({ children, level }: any) => {
const { numberOfLines, theme } = this.props; const { numberOfLines, theme } = this.props;
// @ts-ignore
const textStyle = styles[`heading${level}Text`]; const textStyle = styles[`heading${level}Text`];
return ( return (
<Text numberOfLines={numberOfLines} style={[textStyle, { color: themes[theme].bodyText }]}> <Text numberOfLines={numberOfLines} style={[textStyle, { color: themes[theme].bodyText }]}>

View File

@ -7,7 +7,7 @@ const codeFontFamily = Platform.select({
android: { fontFamily: 'monospace' } android: { fontFamily: 'monospace' }
}); });
export default StyleSheet.create<any>({ export default StyleSheet.create({
container: { container: {
alignItems: 'flex-start', alignItems: 'flex-start',
flexDirection: 'row' flexDirection: 'row'

View File

@ -10,9 +10,16 @@ import Reply from './Reply';
import Button from '../Button'; import Button from '../Button';
import styles from './styles'; import styles from './styles';
import MessageContext from './Context'; 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 { onAnswerButtonPress } = useContext(MessageContext);
const { theme } = useTheme();
const attachedButtons = attachment.actions.map((element: { type: string; msg: string; text: string }) => { const attachedButtons = attachment.actions.map((element: { type: string; msg: string; text: string }) => {
if (element.type === 'button') { if (element.type === 'button') {
@ -29,42 +36,62 @@ const AttachedActions = ({ attachment, theme }: IMessageAttachedActions) => {
}; };
const Attachments = React.memo( const Attachments = React.memo(
({ attachments, timeFormat, showAttachment, getCustomEmoji, theme }: IMessageAttachments) => { // @ts-ignore
({ attachments, timeFormat, showAttachment, style, getCustomEmoji, isReply }: IMessageAttachments) => {
if (!attachments || attachments.length === 0) { if (!attachments || attachments.length === 0) {
return null; return null;
} }
return attachments.map((file: any, index: number) => { const { theme } = useTheme();
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} />;
}
return attachments.map((file: IAttachment, index: number) => {
if (file && file.image_url) {
return ( return (
<Reply <Image
key={index} key={file.image_url}
index={index} file={file}
attachment={file} showAttachment={showAttachment}
timeFormat={timeFormat}
getCustomEmoji={getCustomEmoji} getCustomEmoji={getCustomEmoji}
style={style}
isReply={isReply}
theme={theme} 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'; Attachments.displayName = 'MessageAttachments';

View File

@ -1,5 +1,5 @@
import React from 'react'; 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 { Audio } from 'expo-av';
import Slider from '@react-native-community/slider'; import Slider from '@react-native-community/slider';
import moment from 'moment'; import moment from 'moment';
@ -16,19 +16,20 @@ import MessageContext from './Context';
import ActivityIndicator from '../ActivityIndicator'; import ActivityIndicator from '../ActivityIndicator';
import { withDimensions } from '../../dimensions'; import { withDimensions } from '../../dimensions';
import { TGetCustomEmoji } from '../../definitions/IEmoji'; import { TGetCustomEmoji } from '../../definitions/IEmoji';
import { IAttachment } from '../../definitions';
interface IButton { interface IButton {
loading: boolean; loading: boolean;
paused: boolean; paused: boolean;
theme: string; theme: string;
disabled?: boolean;
onPress: Function; onPress: Function;
} }
interface IMessageAudioProps { interface IMessageAudioProps {
file: { file: IAttachment;
audio_url: string; isReply?: boolean;
description: string; style?: StyleProp<TextStyle>[];
};
theme: string; theme: string;
getCustomEmoji: TGetCustomEmoji; getCustomEmoji: TGetCustomEmoji;
scale?: number; 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 BUTTON_HIT_SLOP = { top: 12, right: 12, bottom: 12, left: 12 };
const sliderAnimationConfig = { const Button = React.memo(({ loading, paused, onPress, disabled, theme }: IButton) => (
duration: 250,
easing: Easing.linear,
delay: 0
};
const Button = React.memo(({ loading, paused, onPress, theme }: IButton) => (
<Touchable <Touchable
style={styles.playPauseButton} style={styles.playPauseButton}
disabled={disabled}
onPress={onPress} onPress={onPress}
hitSlop={BUTTON_HIT_SLOP} hitSlop={BUTTON_HIT_SLOP}
background={Touchable.SelectableBackgroundBorderless()}> background={Touchable.SelectableBackgroundBorderless()}>
{loading ? ( {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> </Touchable>
)); ));
@ -128,7 +128,7 @@ class MessageAudio extends React.Component<IMessageAudioProps, IMessageAudioStat
const { baseUrl, user } = this.context; const { baseUrl, user } = this.context;
let url = file.audio_url; let url = file.audio_url;
if (!url.startsWith('http')) { if (url && !url.startsWith('http')) {
url = `${baseUrl}${file.audio_url}`; url = `${baseUrl}${file.audio_url}`;
} }
@ -249,7 +249,7 @@ class MessageAudio extends React.Component<IMessageAudioProps, IMessageAudioStat
render() { render() {
const { loading, paused, currentTime, duration } = this.state; 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 { description } = file;
const { baseUrl, user } = this.context; const { baseUrl, user } = this.context;
@ -259,29 +259,34 @@ class MessageAudio extends React.Component<IMessageAudioProps, IMessageAudioStat
return ( return (
<> <>
<Markdown
msg={description}
style={[isReply && style]}
baseUrl={baseUrl}
username={user.username}
getCustomEmoji={getCustomEmoji}
theme={theme}
/>
<View <View
style={[ style={[
styles.audioContainer, styles.audioContainer,
{ backgroundColor: themes[theme].chatComponentBackground, borderColor: themes[theme].borderColor } { 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 <Slider
disabled={isReply}
style={styles.slider} style={styles.slider}
value={currentTime} value={currentTime}
maximumValue={duration} maximumValue={duration}
minimumValue={0} minimumValue={0}
animateTransitions thumbTintColor={isReply && isAndroid ? themes[theme].tintDisabled : isAndroid && themes[theme].tintColor}
animationConfig={sliderAnimationConfig}
thumbTintColor={isAndroid && themes[theme].tintColor}
minimumTrackTintColor={themes[theme].tintColor} minimumTrackTintColor={themes[theme].tintColor}
maximumTrackTintColor={themes[theme].auxiliaryText} maximumTrackTintColor={themes[theme].auxiliaryText}
onValueChange={this.onValueChange} onValueChange={this.onValueChange}
/* @ts-ignore*/ thumbImage={isIOS ? { uri: 'audio_thumb', scale } : undefined}
thumbImage={isIOS && { uri: 'audio_thumb', scale }}
/> />
<Text style={[styles.duration, { color: themes[theme].auxiliaryText }]}>{this.duration}</Text> <Text style={[styles.duration, { color: themes[theme].auxiliaryText }]}>{this.duration}</Text>
</View> </View>
<Markdown msg={description} baseUrl={baseUrl} username={user.username} getCustomEmoji={getCustomEmoji} theme={theme} />
</> </>
); );
} }

View File

@ -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>
));

View File

@ -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();
});
});

View File

@ -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\\":[\\"\\"]}]}]}]}]}"`;

View File

@ -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;

View File

@ -39,9 +39,7 @@ const Content = React.memo(
const isPreview: any = props.tmid && !props.isThreadRoom; const isPreview: any = props.tmid && !props.isThreadRoom;
let content = null; let content = null;
if (props.tmid && !props.msg) { if (props.isEncrypted) {
content = <Text style={[styles.text, { color: themes[props.theme].bodyText }]}>{I18n.t('Sent_an_attachment')}</Text>;
} else if (props.isEncrypted) {
content = ( content = (
<Text <Text
style={[styles.textInfo, { color: themes[props.theme].auxiliaryText }]} style={[styles.textInfo, { color: themes[props.theme].auxiliaryText }]}

View File

@ -1,5 +1,5 @@
import React, { useContext } from 'react'; 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 FastImage from '@rocket.chat/react-native-fast-image';
import { dequal } from 'dequal'; import { dequal } from 'dequal';
import { createImageProgress } from 'react-native-image-progress'; import { createImageProgress } from 'react-native-image-progress';
@ -12,9 +12,11 @@ import { formatAttachmentUrl } from '../../lib/utils';
import { themes } from '../../constants/colors'; import { themes } from '../../constants/colors';
import MessageContext from './Context'; import MessageContext from './Context';
import { TGetCustomEmoji } from '../../definitions/IEmoji'; import { TGetCustomEmoji } from '../../definitions/IEmoji';
import { IAttachment } from '../../definitions';
type TMessageButton = { type TMessageButton = {
children: JSX.Element; children: JSX.Element;
disabled?: boolean;
onPress: Function; onPress: Function;
theme: string; theme: string;
}; };
@ -25,17 +27,23 @@ type TMessageImage = {
}; };
interface IMessageImage { interface IMessageImage {
file: { image_url: string; description?: string }; file: IAttachment;
imageUrl?: string; imageUrl?: string;
showAttachment: Function; showAttachment?: Function;
style?: StyleProp<TextStyle>[];
isReply?: boolean;
theme: string; theme: string;
getCustomEmoji: TGetCustomEmoji; getCustomEmoji: TGetCustomEmoji;
} }
const ImageProgress = createImageProgress(FastImage); const ImageProgress = createImageProgress(FastImage);
const Button = React.memo(({ children, onPress, theme }: TMessageButton) => ( const Button = React.memo(({ children, onPress, disabled, theme }: TMessageButton) => (
<Touchable onPress={onPress} style={styles.imageContainer} background={Touchable.Ripple(themes[theme].bannerBackground)}> <Touchable
disabled={disabled}
onPress={onPress}
style={styles.imageContainer}
background={Touchable.Ripple(themes[theme].bannerBackground)}>
{children} {children}
</Touchable> </Touchable>
)); ));
@ -53,34 +61,41 @@ export const MessageImage = React.memo(({ img, theme }: TMessageImage) => (
)); ));
const ImageContainer = React.memo( const ImageContainer = React.memo(
({ file, imageUrl, showAttachment, getCustomEmoji, theme }: IMessageImage) => { ({ file, imageUrl, showAttachment, getCustomEmoji, style, isReply, theme }: IMessageImage) => {
const { baseUrl, user } = useContext(MessageContext); const { baseUrl, user } = useContext(MessageContext);
const img = imageUrl || formatAttachmentUrl(file.image_url, user.id, user.token, baseUrl); const img = imageUrl || formatAttachmentUrl(file.image_url, user.id, user.token, baseUrl);
if (!img) { if (!img) {
return null; return null;
} }
const onPress = () => showAttachment(file); const onPress = () => {
if (!showAttachment) {
return;
}
return showAttachment(file);
};
if (file.description) { if (file.description) {
return ( return (
<Button theme={theme} onPress={onPress}> <Button disabled={isReply} theme={theme} onPress={onPress}>
<View> <View>
<MessageImage img={img} theme={theme} />
<Markdown <Markdown
msg={file.description} msg={file.description}
style={[isReply && style]}
baseUrl={baseUrl} baseUrl={baseUrl}
username={user.username} username={user.username}
getCustomEmoji={getCustomEmoji} getCustomEmoji={getCustomEmoji}
theme={theme} theme={theme}
/> />
<MessageImage img={img} theme={theme} />
</View> </View>
</Button> </Button>
); );
} }
return ( return (
<Button theme={theme} onPress={onPress}> <Button disabled={isReply} theme={theme} onPress={onPress}>
<MessageImage img={img} theme={theme} /> <MessageImage img={img} theme={theme} />
</Button> </Button>
); );

View File

@ -21,6 +21,9 @@ import { themes } from '../../constants/colors';
import { IMessage, IMessageInner, IMessageTouchable } from './interfaces'; import { IMessage, IMessageInner, IMessageTouchable } from './interfaces';
const MessageInner = React.memo((props: IMessageInner) => { const MessageInner = React.memo((props: IMessageInner) => {
const { attachments } = props;
const isCollapsible = attachments ? attachments[0] && attachments[0].collapsed : false;
if (props.type === 'discussion-created') { if (props.type === 'discussion-created') {
return ( return (
<> <>
@ -29,6 +32,7 @@ const MessageInner = React.memo((props: IMessageInner) => {
</> </>
); );
} }
if (props.type === 'jitsi_call_started') { if (props.type === 'jitsi_call_started') {
return ( return (
<> <>
@ -38,6 +42,7 @@ const MessageInner = React.memo((props: IMessageInner) => {
</> </>
); );
} }
if (props.blocks && props.blocks.length) { if (props.blocks && props.blocks.length) {
return ( return (
<> <>
@ -48,11 +53,22 @@ const MessageInner = React.memo((props: IMessageInner) => {
</> </>
); );
} }
return ( return (
<> <>
<User {...props} /> <User {...props} />
{isCollapsible ? (
<>
<Content {...props} /> <Content {...props} />
<Attachments {...props} /> <Attachments {...props} />
</>
) : (
<>
<Attachments {...props} />
<Content {...props} />
</>
)}
<Urls {...props} /> <Urls {...props} />
<Thread {...props} /> <Thread {...props} />
<Reactions {...props} /> <Reactions {...props} />
@ -87,7 +103,7 @@ const Message = React.memo((props: IMessage) => {
<View style={[styles.messageContent, props.isHeader && styles.messageContentWithHeader]}> <View style={[styles.messageContent, props.isHeader && styles.messageContentWithHeader]}>
<MessageInner {...props} /> <MessageInner {...props} />
</View> </View>
<ReadReceipt isReadReceiptEnabled={props.isReadReceiptEnabled} unread={props.unread} theme={props.theme} /> <ReadReceipt isReadReceiptEnabled={props.isReadReceiptEnabled} unread={props.unread || false} theme={props.theme} />
</View> </View>
</View> </View>
); );

View File

@ -1,7 +1,6 @@
import React, { useContext, useState } from 'react'; import React, { useContext, useState } from 'react';
import { StyleSheet, Text, View } from 'react-native'; import { StyleSheet, Text, View } from 'react-native';
import moment from 'moment'; import moment from 'moment';
import { transparentize } from 'color2k';
import { dequal } from 'dequal'; import { dequal } from 'dequal';
import FastImage from '@rocket.chat/react-native-fast-image'; import FastImage from '@rocket.chat/react-native-fast-image';
@ -16,22 +15,24 @@ import { formatAttachmentUrl } from '../../lib/utils';
import { IAttachment } from '../../definitions/IAttachment'; import { IAttachment } from '../../definitions/IAttachment';
import { TGetCustomEmoji } from '../../definitions/IEmoji'; import { TGetCustomEmoji } from '../../definitions/IEmoji';
import RCActivityIndicator from '../ActivityIndicator'; import RCActivityIndicator from '../ActivityIndicator';
import Attachments from './Attachments';
import { useTheme } from '../../theme';
const styles = StyleSheet.create({ const styles = StyleSheet.create({
button: { button: {
flex: 1, flex: 1,
flexDirection: 'row', flexDirection: 'row',
alignItems: 'center', alignItems: 'center',
marginTop: 6, marginVertical: 4,
alignSelf: 'flex-start', alignSelf: 'flex-start',
borderWidth: 1, borderLeftWidth: 2
borderRadius: 4
}, },
attachmentContainer: { attachmentContainer: {
flex: 1, flex: 1,
borderRadius: 4, borderRadius: 4,
flexDirection: 'column', flexDirection: 'column',
padding: 15 paddingVertical: 4,
paddingLeft: 8
}, },
backdrop: { backdrop: {
...StyleSheet.absoluteFillObject ...StyleSheet.absoluteFillObject
@ -39,7 +40,8 @@ const styles = StyleSheet.create({
authorContainer: { authorContainer: {
flex: 1, flex: 1,
flexDirection: 'row', flexDirection: 'row',
alignItems: 'center' alignItems: 'center',
marginBottom: 8
}, },
author: { author: {
flex: 1, flex: 1,
@ -48,9 +50,8 @@ const styles = StyleSheet.create({
}, },
time: { time: {
fontSize: 12, fontSize: 12,
marginLeft: 10, marginLeft: 8,
...sharedStyles.textRegular, ...sharedStyles.textRegular
fontWeight: '300'
}, },
fieldsContainer: { fieldsContainer: {
flex: 1, flex: 1,
@ -94,7 +95,7 @@ const styles = StyleSheet.create({
interface IMessageTitle { interface IMessageTitle {
attachment: IAttachment; attachment: IAttachment;
timeFormat: string; timeFormat?: string;
theme: string; theme: string;
} }
@ -112,9 +113,8 @@ interface IMessageFields {
interface IMessageReply { interface IMessageReply {
attachment: IAttachment; attachment: IAttachment;
timeFormat: string; timeFormat?: string;
index: number; index: number;
theme: string;
getCustomEmoji: TGetCustomEmoji; getCustomEmoji: TGetCustomEmoji;
} }
@ -123,10 +123,10 @@ const Title = React.memo(({ attachment, timeFormat, theme }: IMessageTitle) => {
return ( return (
<View style={styles.authorContainer}> <View style={styles.authorContainer}>
{attachment.author_name ? ( {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} ) : null}
{attachment.title ? <Text style={[styles.title, { color: themes[theme].bodyText }]}>{attachment.title}</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> </View>
); );
}); });
@ -138,7 +138,16 @@ const Description = React.memo(
return null; return null;
} }
const { baseUrl, user } = useContext(MessageContext); 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) => { (prevProps, nextProps) => {
if (prevProps.attachment.text !== nextProps.attachment.text) { if (prevProps.attachment.text !== nextProps.attachment.text) {
@ -195,12 +204,14 @@ const Fields = React.memo(
); );
const Reply = React.memo( const Reply = React.memo(
({ attachment, timeFormat, index, getCustomEmoji, theme }: IMessageReply) => { ({ attachment, timeFormat, index, getCustomEmoji }: IMessageReply) => {
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
if (!attachment) { if (!attachment) {
return null; return null;
} }
const { theme } = useTheme();
const { baseUrl, user, jumpToMessage } = useContext(MessageContext); const { baseUrl, user, jumpToMessage } = useContext(MessageContext);
const onPress = async () => { const onPress = async () => {
@ -221,15 +232,10 @@ const Reply = React.memo(
openLink(url, theme); openLink(url, theme);
}; };
let { borderColor, chatComponentBackground: backgroundColor } = themes[theme]; let { borderColor } = themes[theme];
try {
if (attachment.color) { if (attachment.color) {
backgroundColor = transparentize(attachment.color, 0.8);
borderColor = attachment.color; borderColor = attachment.color;
} }
} catch (e) {
// fallback to default
}
return ( return (
<> <>
@ -240,7 +246,6 @@ const Reply = React.memo(
index > 0 && styles.marginTop, index > 0 && styles.marginTop,
attachment.description && styles.marginBottom, attachment.description && styles.marginBottom,
{ {
backgroundColor,
borderColor borderColor
} }
]} ]}
@ -248,6 +253,13 @@ const Reply = React.memo(
disabled={loading}> disabled={loading}>
<View style={styles.attachmentContainer}> <View style={styles.attachmentContainer}>
<Title attachment={attachment} timeFormat={timeFormat} theme={theme} /> <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} /> <UrlImage image={attachment.thumb_url} />
<Description attachment={attachment} getCustomEmoji={getCustomEmoji} theme={theme} /> <Description attachment={attachment} getCustomEmoji={getCustomEmoji} theme={theme} />
<Fields attachment={attachment} getCustomEmoji={getCustomEmoji} theme={theme} /> <Fields attachment={attachment} getCustomEmoji={getCustomEmoji} theme={theme} />
@ -258,7 +270,7 @@ const Reply = React.memo(
styles.backdrop, styles.backdrop,
{ backgroundColor: themes[theme].bannerBackground, opacity: themes[theme].attachmentLoadingOpacity } { backgroundColor: themes[theme].bannerBackground, opacity: themes[theme].attachmentLoadingOpacity }
]}></View> ]}></View>
<RCActivityIndicator theme={theme} /> <RCActivityIndicator />
</View> </View>
) : null} ) : null}
</View> </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'; Reply.displayName = 'MessageReply';

View File

@ -40,7 +40,7 @@ const styles = StyleSheet.create({
interface IMessageUser { interface IMessageUser {
isHeader?: boolean; isHeader?: boolean;
hasError?: boolean; hasError?: boolean;
useRealName: boolean; useRealName?: boolean;
author?: { author?: {
_id: string; _id: string;
name?: string; name?: string;
@ -50,25 +50,26 @@ interface IMessageUser {
ts?: Date; ts?: Date;
timeFormat?: string; timeFormat?: string;
theme: string; theme: string;
navToRoomInfo: Function; navToRoomInfo?: Function;
type: string; type: string;
} }
const User = React.memo( const User = React.memo(
({ isHeader, useRealName, author, alias, ts, timeFormat, hasError, theme, navToRoomInfo, type, ...props }: IMessageUser) => { ({ isHeader, useRealName, author, alias, ts, timeFormat, hasError, theme, navToRoomInfo, type, ...props }: IMessageUser) => {
if (isHeader || hasError) { if (isHeader || hasError) {
const navParam = {
t: 'd',
rid: author!._id
};
const { user } = useContext(MessageContext); const { user } = useContext(MessageContext);
const username = (useRealName && author!.name) || author!.username; const username = (useRealName && author?.name) || author?.username;
const aliasUsername = alias ? ( const aliasUsername = alias ? (
<Text style={[styles.alias, { color: themes[theme].auxiliaryText }]}> @{username}</Text> <Text style={[styles.alias, { color: themes[theme].auxiliaryText }]}> @{username}</Text>
) : null; ) : null;
const time = moment(ts).format(timeFormat); const time = moment(ts).format(timeFormat);
const onUserPress = () => navToRoomInfo(navParam); const onUserPress = () => {
const isDisabled = author!._id === user.id; navToRoomInfo?.({
t: 'd',
rid: author?._id
});
};
const isDisabled = author?._id === user.id;
const textContent = ( const textContent = (
<> <>
@ -96,7 +97,7 @@ const User = React.memo(
{textContent} {textContent}
</Text> </Text>
</TouchableOpacity> </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} />} {hasError && <MessageError hasError={hasError} theme={theme} {...props} />}
</View> </View>
); );

View File

@ -1,5 +1,5 @@
import React, { useContext, useState } from 'react'; import React, { useContext, useState } from 'react';
import { StyleSheet } from 'react-native'; import { StyleProp, StyleSheet, TextStyle } from 'react-native';
import { dequal } from 'dequal'; import { dequal } from 'dequal';
import Touchable from './Touchable'; import Touchable from './Touchable';
@ -33,13 +33,15 @@ const styles = StyleSheet.create({
interface IMessageVideo { interface IMessageVideo {
file: IAttachment; file: IAttachment;
showAttachment: Function; showAttachment?: Function;
getCustomEmoji: TGetCustomEmoji; getCustomEmoji: TGetCustomEmoji;
style?: StyleProp<TextStyle>[];
isReply?: boolean;
theme: string; theme: string;
} }
const Video = React.memo( const Video = React.memo(
({ file, showAttachment, getCustomEmoji, theme }: IMessageVideo) => { ({ file, showAttachment, getCustomEmoji, style, isReply, theme }: IMessageVideo) => {
const { baseUrl, user } = useContext(MessageContext); const { baseUrl, user } = useContext(MessageContext);
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
@ -47,7 +49,7 @@ const Video = React.memo(
return null; return null;
} }
const onPress = async () => { const onPress = async () => {
if (isTypeSupported(file.video_type)) { if (isTypeSupported(file.video_type) && showAttachment) {
return showAttachment(file); return showAttachment(file);
} }
@ -73,23 +75,21 @@ const Video = React.memo(
return ( 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 <Markdown
msg={file.description} msg={file.description}
baseUrl={baseUrl} baseUrl={baseUrl}
username={user.username} username={user.username}
getCustomEmoji={getCustomEmoji} getCustomEmoji={getCustomEmoji}
style={[isReply && style]}
theme={theme} 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>
</> </>
); );
}, },

View File

@ -10,9 +10,10 @@ import messagesStatus from '../../constants/messagesStatus';
import { withTheme } from '../../theme'; import { withTheme } from '../../theme';
import openLink from '../../utils/openLink'; import openLink from '../../utils/openLink';
import { TGetCustomEmoji } from '../../definitions/IEmoji'; import { TGetCustomEmoji } from '../../definitions/IEmoji';
import { TAnyMessageModel } from '../../definitions';
interface IMessageContainerProps { interface IMessageContainerProps {
item: any; item: TAnyMessageModel;
user: { user: {
id: string; id: string;
username: string; username: string;
@ -20,24 +21,16 @@ interface IMessageContainerProps {
}; };
msg?: string; msg?: string;
rid?: string; rid?: string;
timeFormat: string; timeFormat?: string;
style?: ViewStyle; style?: ViewStyle;
archived?: boolean; archived?: boolean;
broadcast?: boolean; broadcast?: boolean;
previousItem?: { previousItem?: TAnyMessageModel;
ts: any;
u: any;
groupable: any;
id: string;
tmid: string;
status: any;
};
isHeader: boolean;
baseUrl: string; baseUrl: string;
Message_GroupingPeriod?: number; Message_GroupingPeriod?: number;
isReadReceiptEnabled?: boolean; isReadReceiptEnabled?: boolean;
isThreadRoom: boolean; isThreadRoom: boolean;
useRealName: boolean; useRealName?: boolean;
autoTranslateRoom?: boolean; autoTranslateRoom?: boolean;
autoTranslateLanguage?: string; autoTranslateLanguage?: string;
status?: number; status?: number;
@ -59,11 +52,11 @@ interface IMessageContainerProps {
callJitsi?: Function; callJitsi?: Function;
blockAction?: Function; blockAction?: Function;
onAnswerButtonPress?: Function; onAnswerButtonPress?: Function;
theme: string; theme?: string;
threadBadgeColor?: string; threadBadgeColor?: string;
toggleFollowThread?: Function; toggleFollowThread?: Function;
jumpToMessage?: Function; jumpToMessage?: Function;
onPress: Function; onPress?: Function;
} }
class MessageContainer extends React.Component<IMessageContainerProps> { class MessageContainer extends React.Component<IMessageContainerProps> {
@ -222,7 +215,7 @@ class MessageContainer extends React.Component<IMessageContainerProps> {
this.setState({ isManualUnignored: true }); this.setState({ isManualUnignored: true });
}; };
get isHeader() { get isHeader(): boolean {
const { item, previousItem, broadcast, Message_GroupingPeriod } = this.props; const { item, previousItem, broadcast, Message_GroupingPeriod } = this.props;
if (this.hasError || (previousItem && previousItem.status === messagesStatus.ERROR)) { if (this.hasError || (previousItem && previousItem.status === messagesStatus.ERROR)) {
return true; return true;
@ -230,9 +223,11 @@ class MessageContainer extends React.Component<IMessageContainerProps> {
try { try {
if ( if (
previousItem && previousItem &&
// @ts-ignore TODO: IMessage vs IMessageFromServer non-sense
previousItem.ts.toDateString() === item.ts.toDateString() && previousItem.ts.toDateString() === item.ts.toDateString() &&
previousItem.u.username === item.u.username && previousItem.u.username === item.u.username &&
!(previousItem.groupable === false || item.groupable === false || broadcast === true) && !(previousItem.groupable === false || item.groupable === false || broadcast === true) &&
// @ts-ignore TODO: IMessage vs IMessageFromServer non-sense
item.ts - previousItem.ts < Message_GroupingPeriod! * 1000 && item.ts - previousItem.ts < Message_GroupingPeriod! * 1000 &&
previousItem.tmid === item.tmid 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; const { item, previousItem, isThreadRoom } = this.props;
if (isThreadRoom) { if (isThreadRoom) {
return false; return false;
@ -255,37 +250,40 @@ class MessageContainer extends React.Component<IMessageContainerProps> {
return false; return false;
} }
get isThreadSequential() { get isThreadSequential(): boolean {
const { item, isThreadRoom } = this.props; const { item, isThreadRoom } = this.props;
if (isThreadRoom) { if (isThreadRoom) {
return false; return false;
} }
return item.tmid; return !!item.tmid;
} }
get isEncrypted() { get isEncrypted(): boolean {
const { item } = this.props; const { item } = this.props;
const { t, e2e } = item; const { t, e2e } = item;
return t === E2E_MESSAGE_TYPE && e2e !== E2E_STATUS.DONE; return t === E2E_MESSAGE_TYPE && e2e !== E2E_STATUS.DONE;
} }
get isInfo() { get isInfo(): boolean {
const { item } = this.props; 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; const { item } = this.props;
return item.status === messagesStatus.TEMP || item.status === messagesStatus.ERROR; return item.status === messagesStatus.TEMP || item.status === messagesStatus.ERROR;
} }
get isIgnored() { get isIgnored(): boolean {
const { isManualUnignored } = this.state; const { isManualUnignored } = this.state;
const { isIgnored } = this.props; const { isIgnored } = this.props;
return isManualUnignored ? false : isIgnored; if (isManualUnignored) {
return false;
}
return isIgnored ?? false;
} }
get hasError() { get hasError(): boolean {
const { item } = this.props; const { item } = this.props;
return item.status === messagesStatus.ERROR; return item.status === messagesStatus.ERROR;
} }
@ -405,13 +403,12 @@ class MessageContainer extends React.Component<IMessageContainerProps> {
rid={rid!} rid={rid!}
author={u} author={u}
ts={ts} ts={ts}
type={t} type={t as any}
attachments={attachments} attachments={attachments}
blocks={blocks} blocks={blocks}
urls={urls} urls={urls}
reactions={reactions} reactions={reactions}
alias={alias} alias={alias}
/* @ts-ignore*/
avatar={avatar} avatar={avatar}
emoji={emoji} emoji={emoji}
timeFormat={timeFormat} timeFormat={timeFormat}
@ -424,16 +421,19 @@ class MessageContainer extends React.Component<IMessageContainerProps> {
role={role} role={role}
drid={drid} drid={drid}
dcount={dcount} dcount={dcount}
// @ts-ignore
dlm={dlm} dlm={dlm}
tmid={tmid} tmid={tmid}
tcount={tcount} tcount={tcount}
// @ts-ignore
tlm={tlm} tlm={tlm}
tmsg={tmsg} tmsg={tmsg}
fetchThreadName={fetchThreadName!} fetchThreadName={fetchThreadName!}
// @ts-ignore
mentions={mentions} mentions={mentions}
channels={channels} channels={channels}
isIgnored={this.isIgnored!} isIgnored={this.isIgnored}
isEdited={editedBy && !!editedBy.username} isEdited={(editedBy && !!editedBy.username) ?? false}
isHeader={this.isHeader} isHeader={this.isHeader}
isThreadReply={this.isThreadReply} isThreadReply={this.isThreadReply}
isThreadSequential={this.isThreadSequential} isThreadSequential={this.isThreadSequential}
@ -447,7 +447,7 @@ class MessageContainer extends React.Component<IMessageContainerProps> {
navToRoomInfo={navToRoomInfo!} navToRoomInfo={navToRoomInfo!}
callJitsi={callJitsi!} callJitsi={callJitsi!}
blockAction={blockAction!} blockAction={blockAction!}
theme={theme} theme={theme as string}
highlighted={highlighted!} highlighted={highlighted!}
/> />
</MessageContext.Provider> </MessageContext.Provider>

View File

@ -1,24 +1,23 @@
import { MarkdownAST } from '@rocket.chat/message-parser'; import { MarkdownAST } from '@rocket.chat/message-parser';
import { StyleProp, TextStyle } from 'react-native';
import { IUserChannel, IUserMention } from '../markdown/interfaces'; import { IUserChannel, IUserMention } from '../markdown/interfaces';
import { TGetCustomEmoji } from '../../definitions/IEmoji'; import { TGetCustomEmoji } from '../../definitions/IEmoji';
import { IAttachment } from '../../definitions';
export type TMessageType = 'discussion-created' | 'jitsi_call_started'; export type TMessageType = 'discussion-created' | 'jitsi_call_started';
export interface IMessageAttachments { export interface IMessageAttachments {
attachments: any; attachments?: IAttachment[];
timeFormat: string; timeFormat?: string;
showAttachment: Function; style?: StyleProp<TextStyle>[];
isReply?: boolean;
showAttachment?: Function;
getCustomEmoji: TGetCustomEmoji; getCustomEmoji: TGetCustomEmoji;
theme: string;
} }
export interface IMessageAttachedActions { export interface IMessageAttachedActions {
attachment: { attachment: IAttachment;
actions: [];
text: string;
};
theme: string;
} }
export interface IMessageAvatar { export interface IMessageAvatar {
@ -66,26 +65,26 @@ export interface IMessageContent {
_id: string; _id: string;
isTemp: boolean; isTemp: boolean;
isInfo: boolean; isInfo: boolean;
tmid: string; tmid?: string;
isThreadRoom: boolean; isThreadRoom: boolean;
msg: string; msg?: string;
md: MarkdownAST; md?: MarkdownAST;
theme: string; theme: string;
isEdited: boolean; isEdited: boolean;
isEncrypted: boolean; isEncrypted: boolean;
getCustomEmoji: TGetCustomEmoji; getCustomEmoji: TGetCustomEmoji;
channels: IUserChannel[]; channels?: IUserChannel[];
mentions: IUserMention[]; mentions?: IUserMention[];
navToRoomInfo: Function; navToRoomInfo?: Function;
useRealName: boolean; useRealName?: boolean;
isIgnored: boolean; isIgnored: boolean;
type: string; type: string;
} }
export interface IMessageDiscussion { export interface IMessageDiscussion {
msg: string; msg?: string;
dcount: number; dcount?: number;
dlm: Date; dlm?: Date;
theme: string; theme: string;
} }
@ -98,10 +97,10 @@ export interface IMessageEmoji {
} }
export interface IMessageThread { export interface IMessageThread {
msg: string; msg?: string;
tcount: number; tcount?: number | null;
theme: string; theme: string;
tlm: Date; tlm?: Date;
isThreadRoom: boolean; isThreadRoom: boolean;
id: string; id: string;
} }
@ -123,8 +122,8 @@ export interface IMessageTouchable {
} }
export interface IMessageRepliedThread { export interface IMessageRepliedThread {
tmid: string; tmid?: string;
tmsg: string; tmsg?: string;
id: string; id: string;
isHeader: boolean; isHeader: boolean;
theme: string; theme: string;
@ -154,7 +153,7 @@ export interface IMessage extends IMessageRepliedThread, IMessageInner {
style: any; style: any;
onLongPress: Function; onLongPress: Function;
isReadReceiptEnabled: boolean; isReadReceiptEnabled: boolean;
unread: boolean; unread?: boolean;
theme: string; theme: string;
isIgnored: boolean; isIgnored: boolean;
} }

View File

@ -3,7 +3,7 @@ import { StyleSheet } from 'react-native';
import sharedStyles from '../../views/Styles'; import sharedStyles from '../../views/Styles';
import { isTablet } from '../../utils/deviceInfo'; import { isTablet } from '../../utils/deviceInfo';
export default StyleSheet.create<any>({ export default StyleSheet.create({
root: { root: {
flexDirection: 'row' flexDirection: 'row'
}, },
@ -28,7 +28,6 @@ export default StyleSheet.create<any>({
}, },
flex: { flex: {
flexDirection: 'row' flexDirection: 'row'
// flex: 1
}, },
temp: { opacity: 0.3 }, temp: { opacity: 0.3 },
marginTop: { marginTop: {
@ -100,7 +99,6 @@ export default StyleSheet.create<any>({
...sharedStyles.textSemibold ...sharedStyles.textSemibold
}, },
imageContainer: { imageContainer: {
// flex: 1,
flexDirection: 'column', flexDirection: 'column',
borderRadius: 4 borderRadius: 4
}, },
@ -141,7 +139,6 @@ export default StyleSheet.create<any>({
}, },
repliedThread: { repliedThread: {
flexDirection: 'row', flexDirection: 'row',
// flex: 1,
alignItems: 'center', alignItems: 'center',
marginTop: 6, marginTop: 6,
marginBottom: 12 marginBottom: 12

View File

@ -2,9 +2,12 @@ import { TMessageModel } from '../../definitions/IMessage';
import I18n from '../../i18n'; import I18n from '../../i18n';
import { DISCUSSION } from './constants'; import { DISCUSSION } from './constants';
export const formatMessageCount = (count: number, type: string) => { export const formatMessageCount = (count?: number, type?: string): string => {
const discussion = type === DISCUSSION; const discussion = type === DISCUSSION;
let text = discussion ? I18n.t('No_messages_yet') : null; let text = discussion ? I18n.t('No_messages_yet') : null;
if (!count) {
return text;
}
if (count === 1) { if (count === 1) {
text = `${count} ${discussion ? I18n.t('message') : I18n.t('reply')}`; text = `${count} ${discussion ? I18n.t('message') : I18n.t('reply')}`;
} else if (count > 1 && count < 1000) { } 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 }); return I18n.t('This_room_encryption_has_been_enabled_by__username_', { username });
} }
if (type === 'removed-user-from-team') { 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') { 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') { if (type === 'user-added-room-to-team') {
return I18n.t('added__roomName__to_team', { roomName: msg }); return I18n.t('added__roomName__to_team', { roomName: msg });

View File

@ -1,20 +1,25 @@
import { IUser } from './IUser';
export interface IAttachment { export interface IAttachment {
ts: Date; ts?: string | Date;
title: string; title: string;
type: string; type?: string;
description: string; description?: string;
title_link?: string; title_link?: string;
image_url?: string; image_url?: string;
image_type?: string; image_type?: string;
video_url?: string; video_url?: string;
video_type?: string; video_type?: string;
audio_url?: string;
title_link_download?: boolean; title_link_download?: boolean;
attachments?: IAttachment[];
fields?: IAttachment[]; fields?: IAttachment[];
image_dimensions?: { width?: number; height?: number }; image_dimensions?: { width?: number; height?: number };
image_preview?: string; image_preview?: string;
image_size?: number; image_size?: number;
author_name?: string; author_name?: string;
author_icon?: string; author_icon?: string;
actions?: [];
message_link?: string; message_link?: string;
text?: string; text?: string;
short?: boolean; short?: boolean;
@ -22,4 +27,32 @@ export interface IAttachment {
author_link?: string; author_link?: string;
color?: string; color?: string;
thumb_url?: 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'>;
} }

View File

@ -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;
}

View File

@ -0,0 +1,7 @@
export interface IDDPMessage {
msg: string;
fields: {
eventName: string;
args: any;
};
}

View File

@ -21,4 +21,10 @@ export interface IEmojiCategory {
tabLabel: string; 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;

View File

@ -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 };
}

View File

@ -32,6 +32,8 @@ export interface ILivechatVisitor extends IRocketChatRecord {
ip?: string; ip?: string;
host?: string; host?: string;
visitorEmails?: IVisitorEmail[]; visitorEmails?: IVisitorEmail[];
livechatData?: any;
utc?: number;
} }
export interface ILivechatVisitorDTO { export interface ILivechatVisitorDTO {

View File

@ -1,13 +1,21 @@
import Model from '@nozbe/watermelondb/Model'; import Model from '@nozbe/watermelondb/Model';
import { IUserEmail, IUserSettings } from './IUser';
import { UserStatus } from './UserStatus';
export interface ILoggedUser { export interface ILoggedUser {
id: string; id: string;
token: string; token: string;
username: string; username: string;
name: string; name: string;
language?: string; language?: string;
status: string; status: UserStatus;
statusText?: string; statusText?: string;
customFields?: {
[key: string]: any;
};
statusLivechat?: string;
emails?: IUserEmail[];
roles?: string[]; roles?: string[];
avatarETag?: string; avatarETag?: string;
showMessageInMainThread?: boolean; showMessageInMainThread?: boolean;
@ -15,4 +23,16 @@ export interface ILoggedUser {
enableMessageParserEarlyAdoption?: boolean; 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; export type TLoggedUserModel = ILoggedUser & Model;

View File

@ -1,11 +1,14 @@
import Model from '@nozbe/watermelondb/Model'; import Model from '@nozbe/watermelondb/Model';
import { MarkdownAST } from '@rocket.chat/message-parser'; import { MarkdownAST } from '@rocket.chat/message-parser';
import { MessageTypeLoad } from '../constants/messageTypeLoad';
import { IAttachment } from './IAttachment'; import { IAttachment } from './IAttachment';
import { IReaction } from './IReaction'; 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 { export interface IUserMessage {
_id: string; _id: string;
@ -41,64 +44,116 @@ export type E2EType = 'pending' | 'done';
export interface ILastMessage { export interface ILastMessage {
_id: string; _id: string;
rid: string; rid: string;
tshow: boolean; tshow?: boolean;
t: MessageType; t?: MessageType;
tmid: string; tmid?: string;
msg: string; msg?: string;
e2e: E2EType; e2e?: E2EType;
ts: Date; ts: string | Date;
u: IUserMessage; u: IUserMessage;
_updatedAt: Date; _updatedAt: string | Date;
urls: string[]; urls?: IUrlFromServer[];
mentions: IUserMention[]; mentions?: IUserMention[];
channels: IUserChannel[]; channels?: IUserChannel[];
md: MarkdownAST; md?: MarkdownAST;
attachments: IAttachment[]; attachments?: IAttachment[];
reactions: IReaction[]; reactions?: IReaction[];
unread: boolean; unread?: boolean;
status: boolean; status?: number;
} }
export interface IMessage { interface IMessageFile {
_id: string;
name: string;
type: string;
}
export interface IMessageFromServer {
_id: string; _id: string;
rid: string; rid: string;
msg?: string; msg?: string;
id?: string; ts: string | Date; // wm date issue
t?: MessageType;
ts: string | Date;
u: IUserMessage; 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; alias?: string;
parseUrls?: boolean; parseUrls?: boolean;
groupable?: boolean;
avatar?: string; avatar?: string;
emoji?: string; emoji?: string;
attachments?: IAttachment[];
urls?: IUrl[];
_updatedAt: Date;
status?: number; status?: number;
pinned?: boolean; pinned?: boolean;
starred?: boolean; starred?:
| {
_id: string;
}
| boolean;
editedBy?: IEditedBy; editedBy?: IEditedBy;
reactions?: IReaction[]; reactions?: IReaction[];
role?: string; role?: string;
drid?: string; drid?: string;
dcount?: number; dcount?: number;
dlm?: Date; dlm?: string | Date;
tmid?: string; tmid?: string;
tcount?: number; tcount?: number | null;
tlm?: Date; tlm?: string | Date | null;
replies?: string[]; replies?: string[];
mentions?: IUserMention[];
channels?: IUserChannel[];
unread?: boolean; unread?: boolean;
autoTranslate?: boolean; autoTranslate?: boolean;
translations?: ITranslations[]; translations?: ITranslations[];
tmsg?: string; tmsg?: string;
blocks?: any; blocks?: any;
e2e?: string; e2e?: E2EType;
tshow?: boolean; tshow?: boolean;
md?: MarkdownAST;
subscription?: { id: string }; subscription?: { id: string };
} }
export type TMessageModel = IMessage & Model; 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