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/
e2e/docker/rc_test_env/docker-compose.yml
e2e/docker/data/db
e2e/e2e_account.js
*.p8

View File

@ -20,6 +20,26 @@ jest.mock('react-native-file-viewer', () => ({
jest.mock('../app/lib/database', () => jest.fn(() => null));
global.Date.now = jest.fn(() => new Date('2019-10-10').getTime());
jest.mock('react-native-mmkv-storage', () => {
return {
Loader: jest.fn().mockImplementation(() => {
return {
setProcessingMode: jest.fn().mockImplementation(() => {
return {
withEncryption: jest.fn().mockImplementation(() => {
return {
initialize: jest.fn()
};
})
};
})
};
}),
create: jest.fn(),
MODES: { MULTI_PROCESS: '' }
};
});
const converter = new Stories2SnapsConverter();
initStoryshots({

View File

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

View File

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

View File

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

View File

@ -11,7 +11,7 @@ function createRequestTypes(base = {}, types = defaultTypes): Record<string, str
// Login events
export const LOGIN = createRequestTypes('LOGIN', [...defaultTypes, 'SET_SERVICES', 'SET_PREFERENCE', 'SET_LOCAL_AUTHENTICATED']);
export const SHARE = createRequestTypes('SHARE', ['SELECT_SERVER', 'SET_USER', 'SET_SETTINGS', 'SET_SERVER_INFO']);
export const USER = createRequestTypes('USER', ['SET']);
export const USER = createRequestTypes('USER', ['SET', 'CLEAR']);
export const ROOMS = createRequestTypes('ROOMS', [
...defaultTypes,
'REFRESH',

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 {
return {
type: types.LOGIN.SET_SERVICES,

View File

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

View File

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

View File

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

View File

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

View File

@ -1,5 +1,7 @@
export const MESSAGE_TYPE_LOAD_MORE = 'load_more';
export const MESSAGE_TYPE_LOAD_PREVIOUS_CHUNK = 'load_previous_chunk';
export const MESSAGE_TYPE_LOAD_NEXT_CHUNK = 'load_next_chunk';
export enum MessageTypeLoad {
MORE = 'load_more',
PREVIOUS_CHUNK = 'load_previous_chunk',
NEXT_CHUNK = 'load_next_chunk'
}
export const MESSAGE_TYPE_ANY_LOAD = [MESSAGE_TYPE_LOAD_MORE, MESSAGE_TYPE_LOAD_PREVIOUS_CHUNK, MESSAGE_TYPE_LOAD_NEXT_CHUNK];
export const MESSAGE_TYPE_ANY_LOAD = [MessageTypeLoad.MORE, MessageTypeLoad.PREVIOUS_CHUNK, MessageTypeLoad.NEXT_CHUNK];

View File

@ -206,4 +206,4 @@ export default {
Canned_Responses_Enable: {
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 { Keyboard, Text } from 'react-native';
import { HandlerStateChangeEventPayload, State, TapGestureHandler } from 'react-native-gesture-handler';
import Animated, { Easing, Extrapolate, interpolateNode, Value } from 'react-native-reanimated';
import { useSafeAreaInsets } from 'react-native-safe-area-context';
import { State, TapGestureHandler } from 'react-native-gesture-handler';
import ScrollBottomSheet from 'react-native-scroll-bottom-sheet';
import Animated, { Easing, Extrapolate, Value, interpolateNode } from 'react-native-reanimated';
import * as Haptics from 'expo-haptics';
import { useBackHandler } from '@react-native-community/hooks';
import { Item } from './Item';
import { Handle } from './Handle';
import { Button } from './Button';
import { themes } from '../../constants/colors';
import styles, { ITEM_HEIGHT } from './styles';
import { useDimensions, useOrientation } from '../../dimensions';
import I18n from '../../i18n';
import { useTheme } from '../../theme';
import { isIOS, isTablet } from '../../utils/deviceInfo';
import * as List from '../List';
import I18n from '../../i18n';
import { IDimensionsContextProps, useDimensions, useOrientation } from '../../dimensions';
import { Button } from './Button';
import { Handle } from './Handle';
import { IActionSheetItem, Item } from './Item';
import { TActionSheetOptions, TActionSheetOptionsItem } from './Provider';
import styles, { ITEM_HEIGHT } from './styles';
interface IActionSheetData {
options: any;
headerHeight?: number;
hasCancel?: boolean;
customHeader: any;
}
const getItemLayout = (data: any, index: number) => ({ length: ITEM_HEIGHT, offset: ITEM_HEIGHT * index, index });
const getItemLayout = (data: TActionSheetOptionsItem[] | null | undefined, index: number) => ({
length: ITEM_HEIGHT,
offset: ITEM_HEIGHT * index,
index
});
const HANDLE_HEIGHT = isIOS ? 40 : 56;
const MAX_SNAP_HEIGHT = 16;
@ -39,16 +38,17 @@ const ANIMATION_CONFIG = {
};
const ActionSheet = React.memo(
forwardRef(({ children, theme }: { children: JSX.Element; theme: string }, ref) => {
const bottomSheetRef: any = useRef();
const [data, setData] = useState<IActionSheetData>({} as IActionSheetData);
forwardRef(({ children }: { children: React.ReactElement }, ref) => {
const { theme } = useTheme();
const bottomSheetRef = useRef<ScrollBottomSheet<TActionSheetOptionsItem>>(null);
const [data, setData] = useState<TActionSheetOptions>({} as TActionSheetOptions);
const [isVisible, setVisible] = useState(false);
const { height }: Partial<IDimensionsContextProps> = useDimensions();
const { height } = useDimensions();
const { isLandscape } = useOrientation();
const insets = useSafeAreaInsets();
const maxSnap = Math.max(
height! -
height -
// Items height
ITEM_HEIGHT * (data?.options?.length || 0) -
// Handle height
@ -69,7 +69,7 @@ const ActionSheet = React.memo(
* we'll provide more one snap
* that point 50% of the whole screen
*/
const snaps: any = height! - maxSnap > height! * 0.6 && !isLandscape ? [maxSnap, height! * 0.5, height] : [maxSnap, height];
const snaps = height - maxSnap > height * 0.6 && !isLandscape ? [maxSnap, height * 0.5, height] : [maxSnap, height];
const openedSnapIndex = snaps.length > 2 ? 1 : 0;
const closedSnapIndex = snaps.length - 1;
@ -79,12 +79,12 @@ const ActionSheet = React.memo(
bottomSheetRef.current?.snapTo(closedSnapIndex);
};
const show = (options: any) => {
const show = (options: TActionSheetOptions) => {
setData(options);
toggleVisible();
};
const onBackdropPressed = ({ nativeEvent }: any) => {
const onBackdropPressed = ({ nativeEvent }: { nativeEvent: HandlerStateChangeEventPayload }) => {
if (nativeEvent.oldState === State.ACTIVE) {
hide();
}
@ -117,7 +117,7 @@ const ActionSheet = React.memo(
const renderHandle = () => (
<>
<Handle theme={theme} />
<Handle />
{isValidElement(data?.customHeader) ? data.customHeader : null}
</>
);
@ -127,21 +127,23 @@ const ActionSheet = React.memo(
<Button
onPress={hide}
style={[styles.button, { backgroundColor: themes[theme].auxiliaryBackground }]}
// TODO: Remove when migrate Touch
theme={theme}
accessibilityLabel={I18n.t('Cancel')}>
<Text style={[styles.text, { color: themes[theme].bodyText }]}>{I18n.t('Cancel')}</Text>
</Button>
) : null;
const renderItem = ({ item }: any) => <Item item={item} hide={hide} theme={theme} />;
const renderItem = ({ item }: { item: IActionSheetItem['item'] }) => <Item item={item} hide={hide} />;
const animatedPosition = React.useRef(new Value(0));
// TODO: Similar to https://github.com/wcandillon/react-native-redash/issues/307#issuecomment-827442320
const opacity = interpolateNode(animatedPosition.current, {
inputRange: [0, 1],
outputRange: [0, themes[theme].backdropOpacity],
extrapolate: Extrapolate.CLAMP
}) as any;
}) as any; // The function's return differs from the expected type of opacity, however this problem is something related to lib, maybe when updating the types will be fixed.
const bottomSheet = isLandscape || isTablet ? styles.bottomSheet : {};
return (
<>
@ -160,7 +162,7 @@ const ActionSheet = React.memo(
]}
/>
</TapGestureHandler>
<ScrollBottomSheet
<ScrollBottomSheet<TActionSheetOptionsItem>
testID='action-sheet'
ref={bottomSheetRef}
componentType='FlatList'
@ -169,18 +171,11 @@ const ActionSheet = React.memo(
renderHandle={renderHandle}
onSettle={index => index === closedSnapIndex && toggleVisible()}
animatedPosition={animatedPosition.current}
containerStyle={
[
styles.container,
{ backgroundColor: themes[theme].focusedBackground },
(isLandscape || isTablet) && styles.bottomSheet
] as any
}
containerStyle={{ ...styles.container, ...bottomSheet, backgroundColor: themes[theme].focusedBackground }}
animationConfig={ANIMATION_CONFIG}
// FlatList props
data={data?.options}
data={data.options}
renderItem={renderItem}
keyExtractor={(item: any) => item.title}
keyExtractor={item => item.title}
style={{ backgroundColor: themes[theme].focusedBackground }}
contentContainerStyle={styles.content}
ItemSeparatorComponent={List.Separator}

View File

@ -3,9 +3,13 @@ import { View } from 'react-native';
import styles from './styles';
import { themes } from '../../constants/colors';
import { useTheme } from '../../theme';
export const Handle = React.memo(({ theme }: { theme: string }) => (
<View style={[styles.handle, { backgroundColor: themes[theme].focusedBackground }]} testID='action-sheet-handle'>
<View style={[styles.handleIndicator, { backgroundColor: themes[theme].auxiliaryText }]} />
</View>
));
export const Handle = React.memo(() => {
const { theme } = useTheme();
return (
<View style={[styles.handle, { backgroundColor: themes[theme].focusedBackground }]} testID='action-sheet-handle'>
<View style={[styles.handleIndicator, { backgroundColor: themes[theme].auxiliaryText }]} />
</View>
);
});

View File

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

View File

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

View File

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

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

View File

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

View File

@ -1,13 +1,12 @@
import React from 'react';
import { ActivityIndicator, ImageBackground, StyleSheet, Text, View } from 'react-native';
import { withTheme } from '../../theme';
import { useTheme } from '../../theme';
import sharedStyles from '../../views/Styles';
import { themes } from '../../constants/colors';
interface IBackgroundContainer {
text?: string;
theme?: string;
loading?: boolean;
}
@ -32,12 +31,15 @@ const styles = StyleSheet.create({
}
});
const BackgroundContainer = ({ theme, text, loading }: IBackgroundContainer) => (
<View style={styles.container}>
<ImageBackground source={{ uri: `message_empty_${theme}` }} style={styles.image} />
{text && !loading ? <Text style={[styles.text, { color: themes[theme!].auxiliaryTintColor }]}>{text}</Text> : null}
{loading ? <ActivityIndicator style={styles.text} color={themes[theme!].auxiliaryTintColor} /> : null}
</View>
);
const BackgroundContainer = ({ text, loading }: IBackgroundContainer): React.ReactElement => {
const { theme } = useTheme();
return (
<View style={styles.container}>
<ImageBackground source={{ uri: `message_empty_${theme}` }} style={styles.image} />
{text && !loading ? <Text style={[styles.text, { color: themes[theme].auxiliaryTintColor }]}>{text}</Text> : null}
{loading ? <ActivityIndicator style={styles.text} color={themes[theme].auxiliaryTintColor} /> : null}
</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 { themes } from '../constants/colors';
import { useTheme } from '../theme';
interface ICheck {
style?: object;
theme: string;
}
const styles = StyleSheet.create({
icon: {
width: 22,
@ -16,8 +13,9 @@ const styles = StyleSheet.create({
}
});
const Check = React.memo(({ theme, style }: ICheck) => (
<CustomIcon style={[styles.icon, style]} color={themes[theme].tintColor} size={22} name='check' />
));
const Check = React.memo(() => {
const { theme } = useTheme();
return <CustomIcon style={styles.icon} color={themes[theme].tintColor} size={22} name='check' />;
});
export default Check;

View File

@ -5,12 +5,11 @@ import { StyleSheet, View } from 'react-native';
import { themes } from '../../constants/colors';
import { themedHeader } from '../../utils/navigation';
import { isIOS, isTablet } from '../../utils/deviceInfo';
import { withTheme } from '../../theme';
import { useTheme } from '../../theme';
// Get from https://github.com/react-navigation/react-navigation/blob/master/packages/stack/src/views/Header/HeaderSegment.tsx#L69
export const headerHeight = isIOS ? 44 : 56;
export const getHeaderHeight = (isLandscape: boolean) => {
export const getHeaderHeight = (isLandscape: boolean): number => {
if (isIOS) {
if (isLandscape && !isTablet) {
return 32;
@ -28,7 +27,13 @@ interface IHeaderTitlePosition {
numIconsRight: number;
}
export const getHeaderTitlePosition = ({ insets, numIconsRight }: IHeaderTitlePosition) => ({
export const getHeaderTitlePosition = ({
insets,
numIconsRight
}: IHeaderTitlePosition): {
left: number;
right: number;
} => ({
left: insets.left + 60,
right: insets.right + Math.max(45 * numIconsRight, 15)
});
@ -43,20 +48,22 @@ const styles = StyleSheet.create({
});
interface IHeader {
theme: string;
headerLeft(): void;
headerTitle(): void;
headerRight(): void;
headerLeft: () => React.ReactElement | null;
headerTitle: () => React.ReactElement;
headerRight: () => React.ReactElement | null;
}
const Header = ({ theme, headerLeft, headerTitle, headerRight }: IHeader) => (
<SafeAreaView style={{ backgroundColor: themes[theme].headerBackground }} edges={['top', 'left', 'right']}>
<View style={[styles.container, { ...themedHeader(theme).headerStyle }]}>
{headerLeft ? headerLeft() : null}
{headerTitle ? headerTitle() : null}
{headerRight ? headerRight() : null}
</View>
</SafeAreaView>
);
const Header = ({ headerLeft, headerTitle, headerRight }: IHeader): React.ReactElement => {
const { theme } = useTheme();
return (
<SafeAreaView style={{ backgroundColor: themes[theme].headerBackground }} edges={['top', 'left', 'right']}>
<View style={[styles.container, { ...themedHeader(theme).headerStyle }]}>
{headerLeft ? headerLeft() : null}
{headerTitle ? headerTitle() : null}
{headerRight ? headerRight() : null}
</View>
</SafeAreaView>
);
};
export default withTheme(Header);
export default Header;

View File

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

View File

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

View File

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

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;

View File

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

View File

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

View File

@ -4,7 +4,7 @@ import { StyleSheet, Text, View } from 'react-native';
import sharedStyles from '../../views/Styles';
import { themes } from '../../constants/colors';
import I18n from '../../i18n';
import { withTheme } from '../../theme';
import { useTheme } from '../../theme';
import { PADDING_HORIZONTAL } from './constants';
const styles = StyleSheet.create({
@ -20,18 +20,20 @@ const styles = StyleSheet.create({
interface IListHeader {
title: string;
theme?: string;
translateTitle?: boolean;
}
const ListHeader = React.memo(({ title, theme, translateTitle = true }: IListHeader) => (
<View style={styles.container}>
<Text style={[styles.title, { color: themes[theme!].infoText }]} numberOfLines={1}>
{translateTitle ? I18n.t(title) : title}
</Text>
</View>
));
const ListHeader = React.memo(({ title, translateTitle = true }: IListHeader) => {
const { theme } = useTheme();
return (
<View style={styles.container}>
<Text style={[styles.title, { color: themes[theme].infoText }]} numberOfLines={1}>
{translateTitle ? I18n.t(title) : title}
</Text>
</View>
);
});
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 { themes } from '../../constants/colors';
import { withTheme } from '../../theme';
import { useTheme } from '../../theme';
import { PADDING_HORIZONTAL } from './constants';
import I18n from '../../i18n';
@ -18,18 +18,20 @@ const styles = StyleSheet.create({
}
});
interface IListHeader {
interface IListInfo {
info: string;
theme?: string;
translateInfo?: boolean;
}
const ListInfo = React.memo(({ info, theme, translateInfo = true }: IListHeader) => (
<View style={styles.container}>
<Text style={[styles.text, { color: themes[theme!].infoText }]}>{translateInfo ? I18n.t(info) : info}</Text>
</View>
));
const ListInfo = React.memo(({ info, translateInfo = true }: IListInfo) => {
const { theme } = useTheme();
return (
<View style={styles.container}>
<Text style={[styles.text, { color: themes[theme].infoText }]}>{translateInfo ? I18n.t(info) : info}</Text>
</View>
);
});
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>
));
interface IListItem extends IListItemContent, IListButtonPress {
interface IListItem extends IListItemContent, IListItemButton {
backgroundColor?: string;
}

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

@ -4,47 +4,51 @@ import range from 'lodash/range';
import styles from './styles';
import { themes } from '../../../constants/colors';
import { useTheme } from '../../../theme';
const SIZE_EMPTY = 12;
const SIZE_FULL = 16;
interface IPasscodeDots {
passcode: string;
theme: string;
length: number;
}
const Dots = React.memo(({ passcode, theme, length }: IPasscodeDots) => (
<View style={styles.dotsContainer}>
{range(length).map(val => {
const lengthSup = passcode.length >= val + 1;
const height = lengthSup ? SIZE_FULL : SIZE_EMPTY;
const width = lengthSup ? SIZE_FULL : SIZE_EMPTY;
let backgroundColor = '';
if (lengthSup && passcode.length > 0) {
backgroundColor = themes[theme].passcodeDotFull;
} else {
backgroundColor = themes[theme].passcodeDotEmpty;
}
const borderRadius = lengthSup ? SIZE_FULL / 2 : SIZE_EMPTY / 2;
const marginRight = lengthSup ? 10 - (SIZE_FULL - SIZE_EMPTY) / 2 : 10;
const marginLeft = lengthSup ? 10 - (SIZE_FULL - SIZE_EMPTY) / 2 : 10;
return (
<View style={styles.dotsView}>
<View
style={{
height,
width,
borderRadius,
backgroundColor,
marginRight,
marginLeft
}}
/>
</View>
);
})}
</View>
));
const Dots = React.memo(({ passcode, length }: IPasscodeDots) => {
const { theme } = useTheme();
return (
<View style={styles.dotsContainer}>
{range(length).map(val => {
const lengthSup = passcode.length >= val + 1;
const height = lengthSup ? SIZE_FULL : SIZE_EMPTY;
const width = lengthSup ? SIZE_FULL : SIZE_EMPTY;
let backgroundColor = '';
if (lengthSup && passcode.length > 0) {
backgroundColor = themes[theme].passcodeDotFull;
} else {
backgroundColor = themes[theme].passcodeDotEmpty;
}
const borderRadius = lengthSup ? SIZE_FULL / 2 : SIZE_EMPTY / 2;
const marginRight = lengthSup ? 10 - (SIZE_FULL - SIZE_EMPTY) / 2 : 10;
const marginLeft = lengthSup ? 10 - (SIZE_FULL - SIZE_EMPTY) / 2 : 10;
return (
<View style={styles.dotsView}>
<View
style={{
height,
width,
borderRadius,
backgroundColor,
marginRight,
marginLeft
}}
/>
</View>
);
})}
</View>
);
});
export default Dots;

View File

@ -5,13 +5,18 @@ import { Row } from 'react-native-easy-grid';
import styles from './styles';
import { themes } from '../../../constants/colors';
import { CustomIcon } from '../../../lib/Icons';
import { useTheme } from '../../../theme';
const LockIcon = React.memo(({ theme }: { theme: string }) => (
<Row style={styles.row}>
<View style={styles.iconView}>
<CustomIcon name='auth' size={40} color={themes[theme].passcodeLockIcon} />
</View>
</Row>
));
const LockIcon = React.memo(() => {
const { theme } = useTheme();
return (
<Row style={styles.row}>
<View style={styles.iconView}>
<CustomIcon name='auth' size={40} color={themes[theme].passcodeLockIcon} />
</View>
</Row>
);
});
export default LockIcon;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -4,11 +4,11 @@ import moment from 'moment';
import { LOCKED_OUT_TIMER_KEY, TIME_TO_LOCK } from '../../constants/localAuthentication';
export const getLockedUntil = async () => {
const t: any = await AsyncStorage.getItem(LOCKED_OUT_TIMER_KEY);
const t = await AsyncStorage.getItem(LOCKED_OUT_TIMER_KEY);
if (t) {
return moment(t).add(TIME_TO_LOCK);
return moment(t).add(TIME_TO_LOCK).toDate();
}
return null;
};
// @ts-ignore
export const getDiff = t => new Date(t) - new Date();
export const getDiff = (t: string | number | Date) => new Date(t).getTime() - new Date().getTime();

View File

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

View File

@ -1,22 +1,14 @@
import React from 'react';
import {
NativeSyntheticEvent,
StyleSheet,
TextInput as RNTextInput,
Text,
TextInputFocusEventData,
TextInputProps,
View
} from 'react-native';
import { StyleSheet, Text, TextInput as RNTextInput, TextInputProps, View } from 'react-native';
import Touchable from 'react-native-platform-touchable';
import TextInput from '../presentation/TextInput';
import { themes } from '../constants/colors';
import I18n from '../i18n';
import { CustomIcon } from '../lib/Icons';
import sharedStyles from '../views/Styles';
import { withTheme } from '../theme';
import { themes } from '../constants/colors';
import TextInput from '../presentation/TextInput';
import { useTheme } from '../theme';
import { isIOS } from '../utils/deviceInfo';
import sharedStyles from '../views/Styles';
const styles = StyleSheet.create({
container: {
@ -52,60 +44,49 @@ const styles = StyleSheet.create({
}
});
interface ISearchBox {
interface ISearchBox extends TextInputProps {
value?: string;
onChangeText: TextInputProps['onChangeText'];
onSubmitEditing?: () => void;
hasCancel?: boolean;
onCancelPress?: Function;
theme?: string;
inputRef?: React.Ref<RNTextInput>;
testID?: string;
onFocus?: (e: NativeSyntheticEvent<TextInputFocusEventData>) => void;
}
const CancelButton = (onCancelPress: Function, theme: string) => (
<Touchable onPress={onCancelPress} style={styles.cancel}>
<Text style={[styles.cancelText, { color: themes[theme].headerTintColor }]}>{I18n.t('Cancel')}</Text>
</Touchable>
);
const CancelButton = ({ onCancelPress }: { onCancelPress?: Function }) => {
const { theme } = useTheme();
return (
<Touchable onPress={onCancelPress} style={styles.cancel}>
<Text style={[styles.cancelText, { color: themes[theme].headerTintColor }]}>{I18n.t('Cancel')}</Text>
</Touchable>
);
};
const SearchBox = ({
onChangeText,
onSubmitEditing,
testID,
hasCancel,
onCancelPress,
inputRef,
theme,
...props
}: ISearchBox) => (
<View
style={[
styles.container,
{ backgroundColor: isIOS ? themes[theme!].headerBackground : themes[theme!].headerSecondaryBackground }
]}>
<View style={[styles.searchBox, { backgroundColor: themes[theme!].searchboxBackground }]}>
<CustomIcon name='search' size={14} color={themes[theme!].auxiliaryText} />
<TextInput
ref={inputRef}
autoCapitalize='none'
autoCorrect={false}
blurOnSubmit
clearButtonMode='while-editing'
placeholder={I18n.t('Search')}
returnKeyType='search'
style={styles.input}
testID={testID}
underlineColorAndroid='transparent'
onChangeText={onChangeText}
onSubmitEditing={onSubmitEditing}
theme={theme!}
{...props}
/>
const SearchBox = ({ hasCancel, onCancelPress, inputRef, ...props }: ISearchBox): React.ReactElement => {
const { theme } = useTheme();
return (
<View
style={[
styles.container,
{ backgroundColor: isIOS ? themes[theme].headerBackground : themes[theme].headerSecondaryBackground }
]}>
<View style={[styles.searchBox, { backgroundColor: themes[theme].searchboxBackground }]}>
<CustomIcon name='search' size={14} color={themes[theme].auxiliaryText} />
<TextInput
ref={inputRef}
autoCapitalize='none'
autoCorrect={false}
blurOnSubmit
clearButtonMode='while-editing'
placeholder={I18n.t('Search')}
returnKeyType='search'
style={styles.input}
underlineColorAndroid='transparent'
theme={theme}
{...props}
/>
</View>
{hasCancel ? <CancelButton onCancelPress={onCancelPress} /> : null}
</View>
{hasCancel ? CancelButton(onCancelPress!, theme!) : null}
</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 { themes } from '../constants/colors';
import { withTheme } from '../theme';
import { useTheme } from '../theme';
const supportedStyles = {
'light-content': 'light-content',
'dark-content': 'dark-content'
};
interface IStatusBar {
theme?: string;
barStyle?: any;
barStyle?: keyof typeof supportedStyles;
backgroundColor?: string;
}
const StatusBar = React.memo(({ theme, barStyle, backgroundColor }: IStatusBar) => {
const StatusBar = React.memo(({ barStyle, backgroundColor }: IStatusBar) => {
const { theme } = useTheme();
if (!barStyle) {
barStyle = 'light-content';
if (theme === 'light') {
barStyle = 'dark-content';
}
}
return <StatusBarRN backgroundColor={backgroundColor ?? themes[theme!].headerBackground} barStyle={barStyle} animated />;
return <StatusBarRN backgroundColor={backgroundColor ?? themes[theme].headerBackground} barStyle={barStyle} animated />;
});
export default withTheme(StatusBar);
export default StatusBar;

View File

@ -52,10 +52,7 @@ const styles = StyleSheet.create({
export interface IRCTextInputProps extends TextInputProps {
label?: string;
error?: {
error: any;
reason: any;
};
error?: any;
loading?: boolean;
containerStyle?: StyleProp<ViewStyle>;
inputStyle?: StyleProp<TextStyle>;
@ -68,7 +65,11 @@ export interface IRCTextInputProps extends TextInputProps {
theme: string;
}
export default class RCTextInput extends React.PureComponent<IRCTextInputProps, any> {
interface IRCTextInputState {
showPassword: boolean;
}
export default class RCTextInput extends React.PureComponent<IRCTextInputProps, IRCTextInputState> {
static defaultProps = {
error: {},
theme: 'light'
@ -116,12 +117,11 @@ export default class RCTextInput extends React.PureComponent<IRCTextInputProps,
get loading() {
const { theme } = this.props;
// @ts-ignore
return <ActivityIndicator style={[styles.iconContainer, styles.iconRight, { color: themes[theme].bodyText }]} />;
return <ActivityIndicator style={[styles.iconContainer, styles.iconRight]} color={themes[theme].bodyText} />;
}
tooglePassword = () => {
this.setState((prevState: any) => ({ showPassword: !prevState.showPassword }));
this.setState(prevState => ({ showPassword: !prevState.showPassword }));
};
render() {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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 { BLOCK_CONTEXT } from '@rocket.chat/ui-kit';
import { BlockContext } from './interfaces';
export const textParser = ([{ text }]: any) => text;
export const defaultContext: any = {
@ -13,7 +15,19 @@ export const defaultContext: any = {
export const KitContext = React.createContext(defaultContext);
export const useBlockContext = ({ blockId, actionId, appId, initialValue }: any, context: any) => {
type TObjectReturn = {
loading: boolean;
setLoading: React.Dispatch<React.SetStateAction<boolean>>;
error: any;
value: any;
language: any;
};
type TFunctionReturn = (value: any) => Promise<void>;
type TReturn = [TObjectReturn, TFunctionReturn];
export const useBlockContext = ({ blockId, actionId, appId, initialValue }: any, context: BlockContext): TReturn => {
const { action, appId: appIdFromContext, viewId, state, language, errors, values = {} } = useContext(KitContext);
const { value = initialValue } = values[actionId] || {};
const [loading, setLoading] = useState(false);

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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;
let content = null;
if (props.tmid && !props.msg) {
content = <Text style={[styles.text, { color: themes[props.theme].bodyText }]}>{I18n.t('Sent_an_attachment')}</Text>;
} else if (props.isEncrypted) {
if (props.isEncrypted) {
content = (
<Text
style={[styles.textInfo, { color: themes[props.theme].auxiliaryText }]}

View File

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

View File

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

View File

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

View File

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

View File

@ -1,5 +1,5 @@
import React, { useContext, useState } from 'react';
import { StyleSheet } from 'react-native';
import { StyleProp, StyleSheet, TextStyle } from 'react-native';
import { dequal } from 'dequal';
import Touchable from './Touchable';
@ -33,13 +33,15 @@ const styles = StyleSheet.create({
interface IMessageVideo {
file: IAttachment;
showAttachment: Function;
showAttachment?: Function;
getCustomEmoji: TGetCustomEmoji;
style?: StyleProp<TextStyle>[];
isReply?: boolean;
theme: string;
}
const Video = React.memo(
({ file, showAttachment, getCustomEmoji, theme }: IMessageVideo) => {
({ file, showAttachment, getCustomEmoji, style, isReply, theme }: IMessageVideo) => {
const { baseUrl, user } = useContext(MessageContext);
const [loading, setLoading] = useState(false);
@ -47,7 +49,7 @@ const Video = React.memo(
return null;
}
const onPress = async () => {
if (isTypeSupported(file.video_type)) {
if (isTypeSupported(file.video_type) && showAttachment) {
return showAttachment(file);
}
@ -73,23 +75,21 @@ const Video = React.memo(
return (
<>
<Touchable
onPress={onPress}
style={[styles.button, { backgroundColor: themes[theme].videoBackground }]}
background={Touchable.Ripple(themes[theme].bannerBackground)}>
{loading ? (
<RCActivityIndicator theme={theme} />
) : (
<CustomIcon name='play-filled' size={54} color={themes[theme].buttonText} />
)}
</Touchable>
<Markdown
msg={file.description}
baseUrl={baseUrl}
username={user.username}
getCustomEmoji={getCustomEmoji}
style={[isReply && style]}
theme={theme}
/>
<Touchable
disabled={isReply}
onPress={onPress}
style={[styles.button, { backgroundColor: themes[theme].videoBackground }]}
background={Touchable.Ripple(themes[theme].bannerBackground)}>
{loading ? <RCActivityIndicator /> : <CustomIcon name='play-filled' size={54} color={themes[theme].buttonText} />}
</Touchable>
</>
);
},

View File

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

View File

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

View File

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

View File

@ -2,9 +2,12 @@ import { TMessageModel } from '../../definitions/IMessage';
import I18n from '../../i18n';
import { DISCUSSION } from './constants';
export const formatMessageCount = (count: number, type: string) => {
export const formatMessageCount = (count?: number, type?: string): string => {
const discussion = type === DISCUSSION;
let text = discussion ? I18n.t('No_messages_yet') : null;
if (!count) {
return text;
}
if (count === 1) {
text = `${count} ${discussion ? I18n.t('message') : I18n.t('reply')}`;
} else if (count > 1 && count < 1000) {
@ -170,10 +173,10 @@ export const getInfoMessage = ({ type, role, msg, author }: TInfoMessage): strin
return I18n.t('This_room_encryption_has_been_enabled_by__username_', { username });
}
if (type === 'removed-user-from-team') {
return I18n.t('Removed__username__from_team', { user_removed: username });
return I18n.t('Removed__username__from_team', { user_removed: msg });
}
if (type === 'added-user-to-team') {
return I18n.t('Added__username__to_team', { user_added: username });
return I18n.t('Added__username__to_team', { user_added: msg });
}
if (type === 'user-added-room-to-team') {
return I18n.t('added__roomName__to_team', { roomName: msg });

View File

@ -1,20 +1,25 @@
import { IUser } from './IUser';
export interface IAttachment {
ts: Date;
ts?: string | Date;
title: string;
type: string;
description: string;
type?: string;
description?: string;
title_link?: string;
image_url?: string;
image_type?: string;
video_url?: string;
video_type?: string;
audio_url?: string;
title_link_download?: boolean;
attachments?: IAttachment[];
fields?: IAttachment[];
image_dimensions?: { width?: number; height?: number };
image_preview?: string;
image_size?: number;
author_name?: string;
author_icon?: string;
actions?: [];
message_link?: string;
text?: string;
short?: boolean;
@ -22,4 +27,32 @@ export interface IAttachment {
author_link?: string;
color?: string;
thumb_url?: string;
collapsed?: boolean;
}
export interface IServerAttachment {
_id: string;
name: string;
size: number;
type: string;
rid: string;
userId: string;
AmazonS3: { path: string };
store: string;
identify: {
format: string;
size: {
width: number;
height: number;
};
};
complete: boolean;
etag: string;
path: string;
progress: boolean;
token: string;
uploadedAt: string | Date;
uploading: boolean;
url: string;
user: Pick<IUser, '_id' | 'username' | 'name'>;
}

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;
}
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;
host?: string;
visitorEmails?: IVisitorEmail[];
livechatData?: any;
utc?: number;
}
export interface ILivechatVisitorDTO {

View File

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

View File

@ -1,11 +1,14 @@
import Model from '@nozbe/watermelondb/Model';
import { MarkdownAST } from '@rocket.chat/message-parser';
import { MessageTypeLoad } from '../constants/messageTypeLoad';
import { IAttachment } from './IAttachment';
import { IReaction } from './IReaction';
import { IUrl } from './IUrl';
import { TThreadMessageModel } from './IThreadMessage';
import { TThreadModel } from './IThread';
import { IUrlFromServer } from './IUrl';
export type MessageType = 'jitsi_call_started' | 'discussion-created' | 'e2e' | 'load_more' | 'rm' | 'uj';
export type MessageType = 'jitsi_call_started' | 'discussion-created' | 'e2e' | 'load_more' | 'rm' | 'uj' | MessageTypeLoad;
export interface IUserMessage {
_id: string;
@ -41,64 +44,116 @@ export type E2EType = 'pending' | 'done';
export interface ILastMessage {
_id: string;
rid: string;
tshow: boolean;
t: MessageType;
tmid: string;
msg: string;
e2e: E2EType;
ts: Date;
tshow?: boolean;
t?: MessageType;
tmid?: string;
msg?: string;
e2e?: E2EType;
ts: string | Date;
u: IUserMessage;
_updatedAt: Date;
urls: string[];
mentions: IUserMention[];
channels: IUserChannel[];
md: MarkdownAST;
attachments: IAttachment[];
reactions: IReaction[];
unread: boolean;
status: boolean;
_updatedAt: string | Date;
urls?: IUrlFromServer[];
mentions?: IUserMention[];
channels?: IUserChannel[];
md?: MarkdownAST;
attachments?: IAttachment[];
reactions?: IReaction[];
unread?: boolean;
status?: number;
}
export interface IMessage {
interface IMessageFile {
_id: string;
name: string;
type: string;
}
export interface IMessageFromServer {
_id: string;
rid: string;
msg?: string;
id?: string;
t?: MessageType;
ts: string | Date;
ts: string | Date; // wm date issue
u: IUserMessage;
_updatedAt: string | Date;
urls?: IUrlFromServer[];
mentions?: IUserMention[];
channels?: IUserChannel[];
md?: MarkdownAST;
file?: IMessageFile;
files?: IMessageFile[];
groupable?: boolean;
attachments?: IAttachment[];
t?: MessageType;
drid?: string;
dcount?: number;
dml: string | Date;
starred?:
| {
_id: string;
}
| boolean;
pinned?: boolean;
pinnedAt?: string | Date;
pinnedBy?: {
_id: string;
username: string;
};
score?: number;
}
export interface ILoadMoreMessage {
_id: string;
rid: string;
ts: string;
t: string;
msg: string;
}
export interface IMessage extends IMessageFromServer {
id: string;
t?: MessageType;
alias?: string;
parseUrls?: boolean;
groupable?: boolean;
avatar?: string;
emoji?: string;
attachments?: IAttachment[];
urls?: IUrl[];
_updatedAt: Date;
status?: number;
pinned?: boolean;
starred?: boolean;
starred?:
| {
_id: string;
}
| boolean;
editedBy?: IEditedBy;
reactions?: IReaction[];
role?: string;
drid?: string;
dcount?: number;
dlm?: Date;
dlm?: string | Date;
tmid?: string;
tcount?: number;
tlm?: Date;
tcount?: number | null;
tlm?: string | Date | null;
replies?: string[];
mentions?: IUserMention[];
channels?: IUserChannel[];
unread?: boolean;
autoTranslate?: boolean;
translations?: ITranslations[];
tmsg?: string;
blocks?: any;
e2e?: string;
e2e?: E2EType;
tshow?: boolean;
md?: MarkdownAST;
subscription?: { id: string };
}
export type TMessageModel = IMessage & Model;
export type TAnyMessageModel = TMessageModel | TThreadModel | TThreadMessageModel;
export type TTypeMessages = IMessageFromServer | ILoadMoreMessage | IMessage;
// Read receipts to ReadReceiptView and chat.getMessageReadReceipts
export interface IReadReceipts {
_id: string;
roomId: string;
userId: string;
messageId: string;
ts: string;
user?: IUserMessage;
}

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