Merge 4.25.0 into single-server (#3790)

* Chore: Migrate DefaultBrowserView to Typescript (#3488)

Co-authored-by: AlexAlexandre <alexalexandrejr@gmail.com>

* Chore: Migrate PickerView to Typescript (#3501)

Co-authored-by: Diego Mello <diegolmello@gmail.com>

* Chore: Migrate AttachmentView to Typescript (#3483)

Co-authored-by: AlexAlexandre <alexalexandrejr@gmail.com>
Co-authored-by: Diego Mello <diegolmello@gmail.com>

* Chore: Migrate MarkdownTableView to Typescript (#3500)

Co-authored-by: Diego Mello <diegolmello@gmail.com>

* Chore: Migrate E2EEncryptionSecurityView to Typescript (#3489)

Co-authored-by: AlexAlexandre <alexalexandrejr@gmail.com>
Co-authored-by: Diego Mello <diegolmello@gmail.com>

* Chore: Migrate E2EEnterYourPasswordView to Typescript (#3490)

Co-authored-by: AlexAlexandre <alexalexandrejr@gmail.com>
Co-authored-by: Diego Mello <diegolmello@gmail.com>

* Chore: Migrate E2EHowItWorksView to Typescript (#3492)

Co-authored-by: Diego Mello <diegolmello@gmail.com>

* Chore: Migrate E2ESaveYourPasswordView to Typescript (#3493)

Co-authored-by: AlexAlexandre <alexalexandrejr@gmail.com>
Co-authored-by: Diego Mello <diegolmello@gmail.com>

* Chore: Migrate ForgotPasswordView to Typescript (#3496)

Co-authored-by: Diego Mello <diegolmello@gmail.com>

* Chore: Migrate ForwardLivechatView to Typescript (#3497)

Co-authored-by: AlexAlexandre <alexalexandrejr@gmail.com>
Co-authored-by: Diego Mello <diegolmello@gmail.com>

* Chore: Migrate JitsiMeetView to Typescript (#3498)

Co-authored-by: Diego Mello <diegolmello@gmail.com>

* [FIX] Push notifications user preference not syncing correctly (#3494)

Co-authored-by: Diego Mello <diegolmello@gmail.com>

* [FIX] Display prefs showing wrong header icon on tablet (#3510)

* Merge 4.22.0 into master (#3523)

* Tests: Make Detox work on Android (#3051)

* Chore: Migrate NewMessageView to Typescript (#3502)

* Chore: Migrate ScreenLockConfigView to Typescript (#3517)

Co-authored-by: Diego Mello <diegolmello@gmail.com>

* Chore: Migrate ScreenLockedView to Typescript (#3515)

* Chore: Migrate SecurityPrivacyView to Typescript (#3518)

* Chore: Migrate SelectListView to Typescript (#3519)

* Chore: Migrate SelectServerView to Typescript (#3521)

Co-authored-by: AlexAlexandre <alexalexandrejr@gmail.com>

* Chore: Migrate SetUsernameView to Typescript (#3526)

* Chore: Migrate ThemeView to Typescript (#3522)

Co-authored-by: AlexAlexandre <alexalexandrejr@gmail.com>

* Chore: Migrate StatusView to Typescript (#3527)

Co-authored-by: AlexAlexandre <alexalexandrejr@gmail.com>

* Chore: Migrate ShareListView to Typescript (#3459)

Co-authored-by: Gerzon Z <gerzonc@icloud.com>

* Chore: Migrate TeamChannelsView to Typescript (#3532)

Co-authored-by: Gerzon Z <gerzonzcanario@gmail.com>

* Language update from LingoHub 🤖 (#3529)

Project Name: Rocket.Chat.ReactNative
Project Link: https://translate.lingohub.com/rocketchat/dashboard/rocket-dot-chat-dot-reactnative
User: Robot LingoHub

Easy language translations with LingoHub 🚀

Co-authored-by: Robot LingoHub <robot@lingohub.com>
Co-authored-by: Diego Mello <diegolmello@gmail.com>

* Chore: Migrate react-navigation to TypeScript (#3480)

Co-authored-by: AlexAlexandre <alexalexandrejr@gmail.com>

* Bump version to 4.23.0 (#3546)

* [FIX] Certificate stops working after app update on iOS (#3537)

* [IMPROVE] Connection stability (#3531)

* [NEW] Permission for uploading files (#3505)

Co-authored-by: Diego Mello <diegolmello@gmail.com>

* [FIX] Files screen stopped listing content on server 4.2 (#3541)

Co-authored-by: Diego Mello <diegolmello@gmail.com>

* Chore: Migrate ModalBlockView to Typescript (#3503)

* Chore: Migrate ModalBlockView to Typescript

* minor tweaks

* update the navigator

Co-authored-by: AlexAlexandre <alexalexandrejr@gmail.com>
Co-authored-by: Diego Mello <diegolmello@gmail.com>

* Chore: Migrate SelectedUsersView to Typescript (#3520)

Co-authored-by: AlexAlexandre <alexalexandrejr@gmail.com>
Co-authored-by: Diego Mello <diegolmello@gmail.com>

* [IMPROVE] Remove Omnichannel visitor's navigation history (#3534)

Co-authored-by: Diego Mello <diegolmello@gmail.com>

* Merge 4.23.0 into master (#3574)

* [FIX] Download video/quicktime in iOS (#3581)

* Chore: Migrate Redux to Typescript PoC (#3565)

* Chore: Migrate Model's folder to Typescript (#3564)

* Chore: Migrate lib user preferences to Typescript (#3578)

* Chore: Update React Native Device Info to 8.4.8 (#3560)

* [FIX] Roles rendering on dark theme (#3589)

* fix: Add height verification to fix modal dimension (#3573)

* chore: Change the lib `@types/url-parse` to devDependencies (#3585)

* [FIX] teams.removeMembers mobile usage (#3557)

* Chore: Migrate DisplayPrefsView to Typescript (#3555)

* Chore: Migrate Utils Folder to Typescript (#3544)

Co-authored-by: AlexAlexandre <alexalexandrejr@gmail.com>

* Chore: Migrate ThreadMessagesView to Typescript (#3538)

Co-authored-by: AlexAlexandre <alexalexandrejr@gmail.com>

* [FIX] RoomInfoView displaying different info depending on the origin (#3586)

Co-authored-by: GleidsonDaniel <gleidson10daniel@hotmail.com>

* [FIX] Message parser switch not updating field properly (#3576)

* [FIX] Lint not ignoring Markdown props (#3600)

* Bump version to 4.24.0 (#3601)

* Chore: Migrate notification/push to Typescript (#3587)

* chore: migrate connect to ts and add tests

* chore: add more tests

* Chore: Update react-native-device-info patch-package and pods (#3605)

* [FIX] App crashes when entering server after applying certificate (Android) (#3579)

* chore: migrate redux module encryption to typescript

* chore: migrate customEmoji to typescript and add tests

* chore: create IPreferences interface

* chore: migrate redux module sortPreferences to typescript

* chore: fix IPreference interface and organize import

* chore: migrate to typescript

* chore: migrate usersTyping to typescript

* Add DiscussionDetails and Item for DiscussionsView; update ThreadDetails, BackgroundContainer and DiscussionsView

* chore: migrate settings to typescript

* chore: add interface to IStateAplication

* chore: migrate redux module room to typescript

* update definitions

* chore: fix error on error interface

* [FIX] Joining and leaving messages in teams (#3591)

Co-authored-by: Gleidson Daniel Silva <gleidson10daniel@hotmail.com>
Co-authored-by: Diego Mello <diegolmello@gmail.com>

* minor tweak

* [FIX] TypeScript's errors raised by HOCs (#3535)

Co-authored-by: AlexAlexandre <alexalexandrejr@gmail.com>
Co-authored-by: Diego Mello <diegolmello@gmail.com>

* chore: migrate redux module roles to typescript

* wip: add IRoles to IAplicationState interface

* add storybooks, update snapshots and DiscussionsView

* Minor tweaks

* Fix lint

* Remove unused import

* chore: migrate redux module inviteUsers to typescript

* chore: migrate messages action to typescript

* chore: fix any interface and change null to empty string

* chore: implements IAplicationState on type

* chore: remove mapDispatchToProps and continue ts migration

* chore: fix types and apply IAplicationState to types

* Migrate redux server action/reducer to ts

* add tests

* [FIX] App crashes when opening a notification while app is closed (#3629)

* [FIX] makeThreadName asserting undefined as non-null (#3628)

* [FIX] Threads' pagination not working (#3631)

* update tests

* chore: update settings value types

* Send missing params to selectServerRequest

* [IMPROVE] Convert HEIC images to JPG and remove compression (#3633)

* update interface

* update action definition

* Move onDiscussionpress logic on message, update SearchHeader and DiscussionDetails component,  add useLayoutEffect at DiscussionsView

* Update interfaces and minor tweaks to DiscussionsView screen and components

* Fix navigation logic and update interfaces

* Minor tweaks

* Undo change on project.pbxproj

* Update project.pbxproj

* Update project.pbxproj

* Remove style.ts

* Minor tweak

* update snapshots

* Merge 4.24.0 into master (#3648)

* Chore: Change console.log to console.error when logging error

* chore: add as string to fix type

* Fix lint

* fix types

* 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)

* [IMPROVE] Team system messages feedback (#3771) (#3772)

* almost there

* Update stories

Co-authored-by: Reinaldo Neto <47038980+reinaldonetof@users.noreply.github.com>
Co-authored-by: AlexAlexandre <alexalexandrejr@gmail.com>
Co-authored-by: Matheus Barbosa Silva <36537004+matheusbsilva137@users.noreply.github.com>
Co-authored-by: Gerzon Z <gerzonc@icloud.com>
Co-authored-by: Anant Bhasin <38764067+aKn1ghtOut@users.noreply.github.com>
Co-authored-by: Gerzon Z <gerzonzcanario@gmail.com>
Co-authored-by: lingohub[bot] <69908207+lingohub[bot]@users.noreply.github.com>
Co-authored-by: Robot LingoHub <robot@lingohub.com>
Co-authored-by: Gleidson Daniel Silva <gleidson10daniel@hotmail.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>
This commit is contained in:
Diego Mello 2022-02-28 16:03:42 -03:00 committed by GitHub
parent 14f0108325
commit fc9e9a4f2a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
417 changed files with 9093 additions and 87839 deletions

View File

@ -359,7 +359,7 @@ jobs:
- run: - run:
name: Test name: Test
command: | command: |
yarn test yarn test -w 8
- run: - run:
name: Codecov name: Codecov

View File

@ -1,4 +1,5 @@
import initStoryshots from '@storybook/addon-storyshots'; import initStoryshots, { Stories2SnapsConverter } from '@storybook/addon-storyshots';
import { render } from '@testing-library/react-native';
jest.mock('rn-fetch-blob', () => ({ jest.mock('rn-fetch-blob', () => ({
fs: { fs: {
@ -19,4 +20,15 @@ jest.mock('react-native-file-viewer', () => ({
jest.mock('../app/lib/database', () => jest.fn(() => null)); jest.mock('../app/lib/database', () => jest.fn(() => null));
global.Date.now = jest.fn(() => new Date('2019-10-10').getTime()); global.Date.now = jest.fn(() => new Date('2019-10-10').getTime());
initStoryshots(); const converter = new Stories2SnapsConverter();
initStoryshots({
test: ({ story, context }) => {
const snapshotFilename = converter.getSnapshotFileName(context);
const storyElement = story.render();
const { update, toJSON } = render(storyElement);
update(storyElement);
const json = toJSON();
expect(JSON.stringify(json)).toMatchSpecificSnapshot(snapshotFilename);
}
});

File diff suppressed because it is too large Load Diff

View File

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

View File

@ -6,7 +6,7 @@ import { connect } from 'react-redux';
import { SetUsernameStackParamList, StackParamList } from './definitions/navigationTypes'; import { SetUsernameStackParamList, StackParamList } from './definitions/navigationTypes';
import Navigation from './lib/Navigation'; import Navigation from './lib/Navigation';
import { defaultHeader, getActiveRouteName, navigationTheme } from './utils/navigation'; import { defaultHeader, getActiveRouteName, navigationTheme } from './utils/navigation';
import { ROOT_INSIDE, ROOT_LOADING, ROOT_OUTSIDE, ROOT_SET_USERNAME } from './actions/app'; import { RootEnum } from './definitions';
// Stacks // Stacks
import AuthLoadingView from './views/AuthLoadingView'; import AuthLoadingView from './views/AuthLoadingView';
// SetUsername Stack // SetUsername Stack
@ -56,13 +56,13 @@ const App = React.memo(({ root, isMasterDetail }: { root: string; isMasterDetail
}}> }}>
<Stack.Navigator screenOptions={{ headerShown: false, animationEnabled: false }}> <Stack.Navigator screenOptions={{ headerShown: false, animationEnabled: false }}>
<> <>
{root === ROOT_LOADING ? <Stack.Screen name='AuthLoading' component={AuthLoadingView} /> : null} {root === RootEnum.ROOT_LOADING ? <Stack.Screen name='AuthLoading' component={AuthLoadingView} /> : null}
{root === ROOT_OUTSIDE ? <Stack.Screen name='OutsideStack' component={OutsideStack} /> : null} {root === RootEnum.ROOT_OUTSIDE ? <Stack.Screen name='OutsideStack' component={OutsideStack} /> : null}
{root === ROOT_INSIDE && isMasterDetail ? ( {root === RootEnum.ROOT_INSIDE && isMasterDetail ? (
<Stack.Screen name='MasterDetailStack' component={MasterDetailStack} /> <Stack.Screen name='MasterDetailStack' component={MasterDetailStack} />
) : null} ) : null}
{root === ROOT_INSIDE && !isMasterDetail ? <Stack.Screen name='InsideStack' component={InsideStack} /> : null} {root === RootEnum.ROOT_INSIDE && !isMasterDetail ? <Stack.Screen name='InsideStack' component={InsideStack} /> : null}
{root === ROOT_SET_USERNAME ? <Stack.Screen name='SetUsernameStack' component={SetUsernameStack} /> : null} {root === RootEnum.ROOT_SET_USERNAME ? <Stack.Screen name='SetUsernameStack' component={SetUsernameStack} /> : null}
</> </>
</Stack.Navigator> </Stack.Navigator>
</NavigationContainer> </NavigationContainer>

View File

@ -1,4 +1,5 @@
/* eslint-disable */ /* eslint-disable */
import AsyncStorage from '@react-native-community/async-storage';
import { NativeModules } from 'react-native'; import { NativeModules } from 'react-native';
import Reactotron from 'reactotron-react-native'; import Reactotron from 'reactotron-react-native';
import { reactotronRedux } from 'reactotron-redux'; import { reactotronRedux } from 'reactotron-redux';
@ -7,7 +8,12 @@ import sagaPlugin from 'reactotron-redux-saga';
if (__DEV__) { if (__DEV__) {
const scriptURL = NativeModules.SourceCode.scriptURL; const scriptURL = NativeModules.SourceCode.scriptURL;
const scriptHostname = scriptURL.split('://')[1].split(':')[0]; const scriptHostname = scriptURL.split('://')[1].split(':')[0];
Reactotron.configure({ host: scriptHostname }).useReactNative().use(reactotronRedux()).use(sagaPlugin()).connect(); Reactotron.setAsyncStorageHandler(AsyncStorage)
.configure({ host: scriptHostname })
.useReactNative()
.use(reactotronRedux())
.use(sagaPlugin())
.connect();
// Running on android device // Running on android device
// $ adb reverse tcp:9090 tcp:9090 // $ adb reverse tcp:9090 tcp:9090
Reactotron.clear(); Reactotron.clear();

View File

@ -2,8 +2,8 @@ const REQUEST = 'REQUEST';
const SUCCESS = 'SUCCESS'; const SUCCESS = 'SUCCESS';
const FAILURE = 'FAILURE'; const FAILURE = 'FAILURE';
const defaultTypes = [REQUEST, SUCCESS, FAILURE]; const defaultTypes = [REQUEST, SUCCESS, FAILURE];
function createRequestTypes(base = {}, types = defaultTypes): Record<any, any> { function createRequestTypes(base = {}, types = defaultTypes): Record<string, string> {
const res: Record<any, any> = {}; const res: Record<string, string> = {};
types.forEach(type => (res[type] = `${base}_${type}`)); types.forEach(type => (res[type] = `${base}_${type}`));
return res; return res;
} }
@ -58,7 +58,7 @@ export const SNIPPETED_MESSAGES = createRequestTypes('SNIPPETED_MESSAGES', ['OPE
export const DEEP_LINKING = createRequestTypes('DEEP_LINKING', ['OPEN']); export const DEEP_LINKING = createRequestTypes('DEEP_LINKING', ['OPEN']);
export const SORT_PREFERENCES = createRequestTypes('SORT_PREFERENCES', ['SET_ALL', 'SET']); export const SORT_PREFERENCES = createRequestTypes('SORT_PREFERENCES', ['SET_ALL', 'SET']);
export const SET_CUSTOM_EMOJIS = 'SET_CUSTOM_EMOJIS'; export const SET_CUSTOM_EMOJIS = 'SET_CUSTOM_EMOJIS';
export const SET_ACTIVE_USERS = 'SET_ACTIVE_USERS'; export const ACTIVE_USERS = createRequestTypes('ACTIVE_USERS', ['SET', 'CLEAR']);
export const USERS_TYPING = createRequestTypes('USERS_TYPING', ['ADD', 'REMOVE', 'CLEAR']); export const USERS_TYPING = createRequestTypes('USERS_TYPING', ['ADD', 'REMOVE', 'CLEAR']);
export const INVITE_LINKS = createRequestTypes('INVITE_LINKS', [ export const INVITE_LINKS = createRequestTypes('INVITE_LINKS', [
'SET_TOKEN', 'SET_TOKEN',

View File

@ -1,15 +1,19 @@
import { Action } from 'redux'; import { Action } from 'redux';
import { IActiveUsers } from '../reducers/activeUsers'; import { IActiveUsers } from '../reducers/activeUsers';
import { SET_ACTIVE_USERS } from './actionsTypes'; import { ACTIVE_USERS } from './actionsTypes';
export interface ISetActiveUsers extends Action { interface ISetActiveUsers extends Action {
activeUsers: IActiveUsers; activeUsers: IActiveUsers;
} }
export type TActionActiveUsers = ISetActiveUsers; export type TActionActiveUsers = ISetActiveUsers;
export const setActiveUsers = (activeUsers: IActiveUsers): ISetActiveUsers => ({ export const setActiveUsers = (activeUsers: IActiveUsers): ISetActiveUsers => ({
type: SET_ACTIVE_USERS, type: ACTIVE_USERS.SET,
activeUsers activeUsers
}); });
export const clearActiveUsers = (): Action => ({
type: ACTIVE_USERS.CLEAR
});

View File

@ -1,39 +0,0 @@
import { APP } from './actionsTypes';
export const ROOT_OUTSIDE = 'outside';
export const ROOT_INSIDE = 'inside';
export const ROOT_LOADING = 'loading';
export const ROOT_SET_USERNAME = 'setUsername';
export function appStart({ root, ...args }) {
return {
type: APP.START,
root,
...args
};
}
export function appReady() {
return {
type: APP.READY
};
}
export function appInit() {
return {
type: APP.INIT
};
}
export function appInitLocalSettings() {
return {
type: APP.INIT_LOCAL_SETTINGS
};
}
export function setMasterDetail(isMasterDetail) {
return {
type: APP.SET_MASTER_DETAIL,
isMasterDetail
};
}

53
app/actions/app.ts Normal file
View File

@ -0,0 +1,53 @@
import { Action } from 'redux';
import { RootEnum } from '../definitions';
import { APP } from './actionsTypes';
interface IAppStart extends Action {
root: RootEnum;
text?: string;
}
interface ISetMasterDetail extends Action {
isMasterDetail: boolean;
}
export type TActionApp = IAppStart & ISetMasterDetail;
interface Params {
root: RootEnum;
[key: string]: any;
}
export function appStart({ root, ...args }: Params): IAppStart {
return {
type: APP.START,
root,
...args
};
}
export function appReady(): Action {
return {
type: APP.READY
};
}
export function appInit(): Action {
return {
type: APP.INIT
};
}
export function appInitLocalSettings(): Action {
return {
type: APP.INIT_LOCAL_SETTINGS
};
}
export function setMasterDetail(isMasterDetail: boolean): ISetMasterDetail {
return {
type: APP.SET_MASTER_DETAIL,
isMasterDetail
};
}

View File

@ -1,20 +0,0 @@
import * as types from './actionsTypes';
export function connectRequest() {
return {
type: types.METEOR.REQUEST
};
}
export function connectSuccess() {
return {
type: types.METEOR.SUCCESS
};
}
export function disconnect(err) {
return {
type: types.METEOR.DISCONNECT,
err
};
}

21
app/actions/connect.ts Normal file
View File

@ -0,0 +1,21 @@
import { Action } from 'redux';
import * as types from './actionsTypes';
export function connectRequest(): Action {
return {
type: types.METEOR.REQUEST
};
}
export function connectSuccess(): Action {
return {
type: types.METEOR.SUCCESS
};
}
export function disconnect(): Action {
return {
type: types.METEOR.DISCONNECT
};
}

View File

@ -1,23 +0,0 @@
import * as types from './actionsTypes';
export function createChannelRequest(data) {
return {
type: types.CREATE_CHANNEL.REQUEST,
data
};
}
export function createChannelSuccess(data) {
return {
type: types.CREATE_CHANNEL.SUCCESS,
data
};
}
export function createChannelFailure(err, isTeam) {
return {
type: types.CREATE_CHANNEL.FAILURE,
err,
isTeam
};
}

View File

@ -0,0 +1,41 @@
import { Action } from 'redux';
import { TCreateChannelResult } from '../reducers/createChannel';
import { CREATE_CHANNEL } from './actionsTypes';
interface ICreateChannelRequest extends Action {
data: TCreateChannelResult;
}
interface ICreateChannelSuccess extends Action {
data: TCreateChannelResult;
}
interface ICreateChannelFailure extends Action {
err: any;
isTeam: boolean;
}
export type TActionCreateChannel = ICreateChannelRequest & ICreateChannelSuccess & ICreateChannelFailure;
export function createChannelRequest(data: TCreateChannelResult): ICreateChannelRequest {
return {
type: CREATE_CHANNEL.REQUEST,
data
};
}
export function createChannelSuccess(data: TCreateChannelResult): ICreateChannelSuccess {
return {
type: CREATE_CHANNEL.SUCCESS,
data
};
}
export function createChannelFailure(err: any, isTeam: boolean): ICreateChannelFailure {
return {
type: CREATE_CHANNEL.FAILURE,
err,
isTeam
};
}

View File

@ -1,22 +0,0 @@
import * as types from './actionsTypes';
export function createDiscussionRequest(data) {
return {
type: types.CREATE_DISCUSSION.REQUEST,
data
};
}
export function createDiscussionSuccess(data) {
return {
type: types.CREATE_DISCUSSION.SUCCESS,
data
};
}
export function createDiscussionFailure(err) {
return {
type: types.CREATE_DISCUSSION.FAILURE,
err
};
}

View File

@ -0,0 +1,38 @@
import { Action } from 'redux';
import { CREATE_DISCUSSION } from './actionsTypes';
interface ICreateDiscussionRequest extends Action {
data: any;
}
interface ICreateDiscussionSuccess extends Action {
data: any;
}
interface ICreateDiscussionFailure extends Action {
err: any;
}
export type TActionCreateDiscussion = ICreateDiscussionRequest & ICreateDiscussionSuccess & ICreateDiscussionFailure;
export function createDiscussionRequest(data: any): ICreateDiscussionRequest {
return {
type: CREATE_DISCUSSION.REQUEST,
data
};
}
export function createDiscussionSuccess(data: any): ICreateDiscussionSuccess {
return {
type: CREATE_DISCUSSION.SUCCESS,
data
};
}
export function createDiscussionFailure(err: any): ICreateDiscussionFailure {
return {
type: CREATE_DISCUSSION.FAILURE,
err
};
}

View File

@ -1,8 +0,0 @@
import * as types from './actionsTypes';
export function setCustomEmojis(emojis) {
return {
type: types.SET_CUSTOM_EMOJIS,
emojis
};
}

View File

@ -0,0 +1,17 @@
import { Action } from 'redux';
import { ICustomEmojis } from '../reducers/customEmojis';
import { SET_CUSTOM_EMOJIS } from './actionsTypes';
export interface ISetCustomEmojis extends Action {
emojis: ICustomEmojis;
}
export type TActionCustomEmojis = ISetCustomEmojis;
export function setCustomEmojis(emojis: ICustomEmojis): ISetCustomEmojis {
return {
type: SET_CUSTOM_EMOJIS,
emojis
};
}

View File

@ -1,8 +0,0 @@
import * as types from './actionsTypes';
export function deepLinkingOpen(params) {
return {
type: types.DEEP_LINKING.OPEN,
params
};
}

View File

@ -0,0 +1,25 @@
import { Action } from 'redux';
import { DEEP_LINKING } from './actionsTypes';
interface IParams {
path: string;
rid: string;
messageId: string;
host: string;
isCall: boolean;
fullURL: string;
type: string;
token: string;
}
interface IDeepLinkingOpen extends Action {
params: Partial<IParams>;
}
export function deepLinkingOpen(params: Partial<IParams>): IDeepLinkingOpen {
return {
type: DEEP_LINKING.OPEN,
params
};
}

View File

@ -1,35 +0,0 @@
import * as types from './actionsTypes';
export function encryptionInit() {
return {
type: types.ENCRYPTION.INIT
};
}
export function encryptionStop() {
return {
type: types.ENCRYPTION.STOP
};
}
export function encryptionSet(enabled = false, banner = null) {
return {
type: types.ENCRYPTION.SET,
enabled,
banner
};
}
export function encryptionSetBanner(banner) {
return {
type: types.ENCRYPTION.SET_BANNER,
banner
};
}
export function encryptionDecodeKey(password) {
return {
type: types.ENCRYPTION.DECODE_KEY,
password
};
}

52
app/actions/encryption.ts Normal file
View File

@ -0,0 +1,52 @@
import { Action } from 'redux';
import { IBanner } from '../reducers/encryption';
import { ENCRYPTION } from './actionsTypes';
export interface IEncryptionSet extends Action {
enabled: boolean;
banner: IBanner;
}
export interface IEncryptionSetBanner extends Action {
banner: IBanner;
}
export interface IEncryptionDecodeKey extends Action {
password: string;
}
export type TActionEncryption = IEncryptionSet & IEncryptionSetBanner & IEncryptionDecodeKey;
export function encryptionInit(): Action {
return {
type: ENCRYPTION.INIT
};
}
export function encryptionStop(): Action {
return {
type: ENCRYPTION.STOP
};
}
export function encryptionSet(enabled = false, banner: IBanner = ''): IEncryptionSet {
return {
type: ENCRYPTION.SET,
enabled,
banner
};
}
export function encryptionSetBanner(banner: IBanner = ''): IEncryptionSetBanner {
return {
type: ENCRYPTION.SET_BANNER,
banner
};
}
export function encryptionDecodeKey(password: string): IEncryptionDecodeKey {
return {
type: ENCRYPTION.DECODE_KEY,
password
};
}

View File

@ -1,14 +0,0 @@
import { ENTERPRISE_MODULES } from './actionsTypes';
export function setEnterpriseModules(modules) {
return {
type: ENTERPRISE_MODULES.SET,
payload: modules
};
}
export function clearEnterpriseModules() {
return {
type: ENTERPRISE_MODULES.CLEAR
};
}

View File

@ -0,0 +1,23 @@
import { Action } from 'redux';
import { IEnterpriseModules } from '../reducers/enterpriseModules';
import { ENTERPRISE_MODULES } from './actionsTypes';
interface ISetEnterpriseModules extends Action {
payload: IEnterpriseModules[];
}
export type TActionEnterpriseModules = ISetEnterpriseModules & Action;
export function setEnterpriseModules(modules: IEnterpriseModules[]): ISetEnterpriseModules {
return {
type: ENTERPRISE_MODULES.SET,
payload: modules
};
}
export function clearEnterpriseModules(): Action {
return {
type: ENTERPRISE_MODULES.CLEAR
};
}

View File

@ -1,54 +0,0 @@
import * as types from './actionsTypes';
export function inviteLinksSetToken(token) {
return {
type: types.INVITE_LINKS.SET_TOKEN,
token
};
}
export function inviteLinksRequest(token) {
return {
type: types.INVITE_LINKS.REQUEST,
token
};
}
export function inviteLinksSuccess() {
return {
type: types.INVITE_LINKS.SUCCESS
};
}
export function inviteLinksFailure() {
return {
type: types.INVITE_LINKS.FAILURE
};
}
export function inviteLinksClear() {
return {
type: types.INVITE_LINKS.CLEAR
};
}
export function inviteLinksCreate(rid) {
return {
type: types.INVITE_LINKS.CREATE,
rid
};
}
export function inviteLinksSetParams(params) {
return {
type: types.INVITE_LINKS.SET_PARAMS,
params
};
}
export function inviteLinksSetInvite(invite) {
return {
type: types.INVITE_LINKS.SET_INVITE,
invite
};
}

View File

@ -0,0 +1,61 @@
import { Action } from 'redux';
import { TInvite } from '../reducers/inviteLinks';
import { INVITE_LINKS } from './actionsTypes';
interface IInviteLinksGeneric extends Action {
token: string;
}
interface IInviteLinksCreate extends Action {
rid: string;
}
interface IInviteLinksSetInvite extends Action {
invite: TInvite;
}
type TParams = Record<string, any>;
interface IInviteLinksSetParams extends Action {
params: TParams;
}
export type TActionInviteLinks = IInviteLinksGeneric & IInviteLinksCreate & IInviteLinksSetInvite & IInviteLinksSetParams;
export const inviteLinksSetToken = (token: string): IInviteLinksGeneric => ({
type: INVITE_LINKS.SET_TOKEN,
token
});
export const inviteLinksRequest = (token: string): IInviteLinksGeneric => ({
type: INVITE_LINKS.REQUEST,
token
});
export const inviteLinksSuccess = (): Action => ({
type: INVITE_LINKS.SUCCESS
});
export const inviteLinksFailure = (): Action => ({
type: INVITE_LINKS.FAILURE
});
export const inviteLinksClear = (): Action => ({
type: INVITE_LINKS.CLEAR
});
export const inviteLinksCreate = (rid: string): IInviteLinksCreate => ({
type: INVITE_LINKS.CREATE,
rid
});
export const inviteLinksSetParams = (params: TParams): IInviteLinksSetParams => ({
type: INVITE_LINKS.SET_PARAMS,
params
});
export const inviteLinksSetInvite = (invite: TInvite): IInviteLinksSetInvite => ({
type: INVITE_LINKS.SET_INVITE,
invite
});

View File

@ -1,59 +0,0 @@
import * as types from './actionsTypes';
export function loginRequest(credentials, logoutOnError, isFromWebView) {
return {
type: types.LOGIN.REQUEST,
credentials,
logoutOnError,
isFromWebView
};
}
export function loginSuccess(user) {
return {
type: types.LOGIN.SUCCESS,
user
};
}
export function loginFailure(err) {
return {
type: types.LOGIN.FAILURE,
err
};
}
export function logout(forcedByServer = false) {
return {
type: types.LOGOUT,
forcedByServer
};
}
export function setUser(user) {
return {
type: types.USER.SET,
user
};
}
export function setLoginServices(data) {
return {
type: types.LOGIN.SET_SERVICES,
data
};
}
export function setPreference(preference) {
return {
type: types.LOGIN.SET_PREFERENCE,
preference
};
}
export function setLocalAuthenticated(isLocalAuthenticated) {
return {
type: types.LOGIN.SET_LOCAL_AUTHENTICATED,
isLocalAuthenticated
};
}

115
app/actions/login.ts Normal file
View File

@ -0,0 +1,115 @@
import { Action } from 'redux';
import { IUser } from '../definitions';
import * as types from './actionsTypes';
interface ICredentials {
resume: string;
user: string;
password: string;
}
interface ILoginRequest extends Action {
credentials: any;
logoutOnError?: boolean;
isFromWebView?: boolean;
}
interface ILoginSuccess extends Action {
user: Partial<IUser>;
}
interface ILoginFailure extends Action {
err: Partial<IUser>;
}
interface ILogout extends Action {
forcedByServer: boolean;
}
interface ISetUser extends Action {
user: Partial<IUser>;
}
interface ISetServices extends Action {
data: Record<string, string>;
}
interface ISetPreference extends Action {
preference: Record<string, any>;
}
interface ISetLocalAuthenticated extends Action {
isLocalAuthenticated: boolean;
}
export type TActionsLogin = ILoginRequest &
ILoginSuccess &
ILoginFailure &
ILogout &
ISetUser &
ISetServices &
ISetPreference &
ISetLocalAuthenticated;
export function loginRequest(
credentials: Partial<ICredentials>,
logoutOnError?: boolean,
isFromWebView?: boolean
): ILoginRequest {
return {
type: types.LOGIN.REQUEST,
credentials,
logoutOnError,
isFromWebView
};
}
export function loginSuccess(user: Partial<IUser>): ILoginSuccess {
return {
type: types.LOGIN.SUCCESS,
user
};
}
export function loginFailure(err: Record<string, any>): ILoginFailure {
return {
type: types.LOGIN.FAILURE,
err
};
}
export function logout(forcedByServer = false): ILogout {
return {
type: types.LOGOUT,
forcedByServer
};
}
export function setUser(user: Partial<IUser>): ISetUser {
return {
type: types.USER.SET,
user
};
}
export function setLoginServices(data: Record<string, any>): ISetServices {
return {
type: types.LOGIN.SET_SERVICES,
data
};
}
export function setPreference(preference: Record<string, any>): ISetPreference {
return {
type: types.LOGIN.SET_PREFERENCE,
preference
};
}
export function setLocalAuthenticated(isLocalAuthenticated: boolean): ISetLocalAuthenticated {
return {
type: types.LOGIN.SET_LOCAL_AUTHENTICATED,
isLocalAuthenticated
};
}

View File

@ -1,8 +0,0 @@
import * as types from './actionsTypes';
export function replyBroadcast(message) {
return {
type: types.MESSAGES.REPLY_BROADCAST,
message
};
}

16
app/actions/messages.ts Normal file
View File

@ -0,0 +1,16 @@
import { Action } from 'redux';
import { MESSAGES } from './actionsTypes';
type IMessage = Record<string, string>;
interface IReplyBroadcast extends Action {
message: IMessage;
}
export function replyBroadcast(message: IMessage): IReplyBroadcast {
return {
type: MESSAGES.REPLY_BROADCAST,
message
};
}

View File

@ -1,15 +0,0 @@
import * as types from './actionsTypes';
export function setPermissions(permissions) {
return {
type: types.PERMISSIONS.SET,
permissions
};
}
export function updatePermission(id, roles) {
return {
type: types.PERMISSIONS.UPDATE,
payload: { id, roles }
};
}

View File

@ -0,0 +1,28 @@
import { Action } from 'redux';
import { IPermissionsState, TSupportedPermissions } from '../reducers/permissions';
import { PERMISSIONS } from './actionsTypes';
interface ISetPermissions extends Action {
permissions: IPermissionsState;
}
interface IUpdatePermissions extends Action {
payload: { id: TSupportedPermissions; roles: string[] };
}
export type TActionPermissions = ISetPermissions & IUpdatePermissions;
export function setPermissions(permissions: IPermissionsState): ISetPermissions {
return {
type: PERMISSIONS.SET,
permissions
};
}
export function updatePermission(id: TSupportedPermissions, roles: string[]): IUpdatePermissions {
return {
type: PERMISSIONS.UPDATE,
payload: { id, roles }
};
}

View File

@ -1,20 +0,0 @@
import * as types from './actionsTypes';
export function setRoles(roles) {
return {
type: types.ROLES.SET,
roles
};
}
export function updateRoles(id, desc) {
return {
type: types.ROLES.UPDATE,
payload: { id, desc }
};
}
export function removeRoles(id) {
return {
type: types.ROLES.REMOVE,
payload: { id }
};
}

39
app/actions/roles.ts Normal file
View File

@ -0,0 +1,39 @@
import { Action } from 'redux';
import { IRoles } from '../reducers/roles';
import { ROLES } from './actionsTypes';
export interface ISetRoles extends Action {
roles: IRoles;
}
export interface IUpdateRoles extends Action {
payload: { id: string; desc: string };
}
export interface IRemoveRoles extends Action {
payload: { id: string };
}
export type IActionRoles = ISetRoles & IUpdateRoles & IRemoveRoles;
export function setRoles(roles: IRoles): ISetRoles {
return {
type: ROLES.SET,
roles
};
}
export function updateRoles(id: string, desc: string): IUpdateRoles {
return {
type: ROLES.UPDATE,
payload: { id, desc }
};
}
export function removeRoles(id: string): IRemoveRoles {
return {
type: ROLES.REMOVE,
payload: { id }
};
}

View File

@ -1,62 +0,0 @@
import * as types from './actionsTypes';
export function subscribeRoom(rid) {
return {
type: types.ROOM.SUBSCRIBE,
rid
};
}
export function unsubscribeRoom(rid) {
return {
type: types.ROOM.UNSUBSCRIBE,
rid
};
}
export function leaveRoom(roomType, room, selected) {
return {
type: types.ROOM.LEAVE,
room,
roomType,
selected
};
}
export function deleteRoom(roomType, room, selected) {
return {
type: types.ROOM.DELETE,
room,
roomType,
selected
};
}
export function closeRoom(rid) {
return {
type: types.ROOM.CLOSE,
rid
};
}
export function forwardRoom(rid, transferData) {
return {
type: types.ROOM.FORWARD,
transferData,
rid
};
}
export function removedRoom() {
return {
type: types.ROOM.REMOVED
};
}
export function userTyping(rid, status = true) {
return {
type: types.ROOM.USER_TYPING,
rid,
status
};
}

109
app/actions/room.ts Normal file
View File

@ -0,0 +1,109 @@
import { Action } from 'redux';
import { ERoomType } from '../definitions/ERoomType';
import { ROOM } from './actionsTypes';
// TYPE RETURN RELATED
type ISelected = Record<string, string>;
export interface ITransferData {
roomId: string;
userId?: string;
departmentId?: string;
}
// ACTION RETURN RELATED
interface IBaseReturn extends Action {
rid: string;
}
type TSubscribeRoom = IBaseReturn;
type TUnsubscribeRoom = IBaseReturn;
type TCloseRoom = IBaseReturn;
type TRoom = Record<string, any>;
interface ILeaveRoom extends Action {
roomType: ERoomType;
room: TRoom;
selected?: ISelected;
}
interface IDeleteRoom extends Action {
roomType: ERoomType;
room: TRoom;
selected?: ISelected;
}
interface IForwardRoom extends Action {
transferData: ITransferData;
rid: string;
}
interface IUserTyping extends Action {
rid: string;
status: boolean;
}
export type TActionsRoom = TSubscribeRoom & TUnsubscribeRoom & TCloseRoom & ILeaveRoom & IDeleteRoom & IForwardRoom & IUserTyping;
export function subscribeRoom(rid: string): TSubscribeRoom {
return {
type: ROOM.SUBSCRIBE,
rid
};
}
export function unsubscribeRoom(rid: string): TUnsubscribeRoom {
return {
type: ROOM.UNSUBSCRIBE,
rid
};
}
export function leaveRoom(roomType: ERoomType, room: TRoom, selected?: ISelected): ILeaveRoom {
return {
type: ROOM.LEAVE,
room,
roomType,
selected
};
}
export function deleteRoom(roomType: ERoomType, room: TRoom, selected?: ISelected): IDeleteRoom {
return {
type: ROOM.DELETE,
room,
roomType,
selected
};
}
export function closeRoom(rid: string): TCloseRoom {
return {
type: ROOM.CLOSE,
rid
};
}
export function forwardRoom(rid: string, transferData: ITransferData): IForwardRoom {
return {
type: ROOM.FORWARD,
transferData,
rid
};
}
export function removedRoom(): Action {
return {
type: ROOM.REMOVED
};
}
export function userTyping(rid: string, status = true): IUserTyping {
return {
type: ROOM.USER_TYPING,
rid,
status
};
}

View File

@ -1,58 +0,0 @@
import * as types from './actionsTypes';
export function roomsRequest(params = { allData: false }) {
return {
type: types.ROOMS.REQUEST,
params
};
}
export function roomsSuccess() {
return {
type: types.ROOMS.SUCCESS
};
}
export function roomsFailure(err) {
return {
type: types.ROOMS.FAILURE,
err
};
}
export function roomsRefresh() {
return {
type: types.ROOMS.REFRESH
};
}
export function setSearch(searchText) {
return {
type: types.ROOMS.SET_SEARCH,
searchText
};
}
export function closeServerDropdown() {
return {
type: types.ROOMS.CLOSE_SERVER_DROPDOWN
};
}
export function toggleServerDropdown() {
return {
type: types.ROOMS.TOGGLE_SERVER_DROPDOWN
};
}
export function openSearchHeader() {
return {
type: types.ROOMS.OPEN_SEARCH_HEADER
};
}
export function closeSearchHeader() {
return {
type: types.ROOMS.CLOSE_SEARCH_HEADER
};
}

78
app/actions/rooms.ts Normal file
View File

@ -0,0 +1,78 @@
import { Action } from 'redux';
import { ROOMS } from './actionsTypes';
export interface IRoomsRequest extends Action {
params: any;
}
export interface ISetSearch extends Action {
searchText: string;
}
export interface IRoomsFailure extends Action {
err: Record<string, any> | string;
}
export type IRoomsAction = IRoomsRequest & ISetSearch & IRoomsFailure;
export function roomsRequest(
params: {
allData: boolean;
} = { allData: false }
): IRoomsRequest {
return {
type: ROOMS.REQUEST,
params
};
}
export function roomsSuccess(): Action {
return {
type: ROOMS.SUCCESS
};
}
export function roomsFailure(err: string): IRoomsFailure {
return {
type: ROOMS.FAILURE,
err
};
}
export function roomsRefresh(): Action {
return {
type: ROOMS.REFRESH
};
}
export function setSearch(searchText: string): ISetSearch {
return {
type: ROOMS.SET_SEARCH,
searchText
};
}
export function closeServerDropdown(): Action {
return {
type: ROOMS.CLOSE_SERVER_DROPDOWN
};
}
export function toggleServerDropdown(): Action {
return {
type: ROOMS.TOGGLE_SERVER_DROPDOWN
};
}
export function openSearchHeader(): Action {
return {
type: ROOMS.OPEN_SEARCH_HEADER
};
}
export function closeSearchHeader(): Action {
return {
type: ROOMS.CLOSE_SEARCH_HEADER
};
}

View File

@ -1,60 +0,0 @@
import { SERVER } from './actionsTypes';
export function selectServerRequest(server, version, fetchVersion = true, changeServer = false) {
return {
type: SERVER.SELECT_REQUEST,
server,
version,
fetchVersion,
changeServer
};
}
export function selectServerSuccess(server, version) {
return {
type: SERVER.SELECT_SUCCESS,
server,
version
};
}
export function selectServerFailure() {
return {
type: SERVER.SELECT_FAILURE
};
}
export function serverRequest(server, username = null, fromServerHistory = false) {
return {
type: SERVER.REQUEST,
server,
username,
fromServerHistory
};
}
export function serverSuccess() {
return {
type: SERVER.SUCCESS
};
}
export function serverFailure(err) {
return {
type: SERVER.FAILURE,
err
};
}
export function serverInitAdd(previousServer) {
return {
type: SERVER.INIT_ADD,
previousServer
};
}
export function serverFinishAdd() {
return {
type: SERVER.FINISH_ADD
};
}

90
app/actions/server.ts Normal file
View File

@ -0,0 +1,90 @@
import { Action } from 'redux';
import { SERVER } from './actionsTypes';
interface ISelectServer extends Action {
server: string;
version?: string;
fetchVersion: boolean;
changeServer: boolean;
}
interface ISelectServerSuccess extends Action {
server: string;
version: string;
}
interface IServer extends Action {
server: string;
username: string | null;
fromServerHistory: boolean;
}
interface IServerInit extends Action {
previousServer: string;
}
interface IServerFailure extends Action {
err: any;
}
export type TActionServer = ISelectServer & ISelectServerSuccess & IServer & IServerInit & IServerFailure;
export function selectServerRequest(server: string, version?: string, fetchVersion = true, changeServer = false): ISelectServer {
return {
type: SERVER.SELECT_REQUEST,
server,
version,
fetchVersion,
changeServer
};
}
export function selectServerSuccess(server: string, version: string): ISelectServerSuccess {
return {
type: SERVER.SELECT_SUCCESS,
server,
version
};
}
export function selectServerFailure(): Action {
return {
type: SERVER.SELECT_FAILURE
};
}
export function serverRequest(server: string, username: string | null = null, fromServerHistory = false): IServer {
return {
type: SERVER.REQUEST,
server,
username,
fromServerHistory
};
}
export function serverSuccess(): Action {
return {
type: SERVER.SUCCESS
};
}
export function serverFailure(err: any): IServerFailure {
return {
type: SERVER.FAILURE,
err
};
}
export function serverInitAdd(previousServer: string): IServerInit {
return {
type: SERVER.INIT_ADD,
previousServer
};
}
export function serverFinishAdd(): Action {
return {
type: SERVER.FINISH_ADD
};
}

View File

@ -1,21 +0,0 @@
import { SETTINGS } from './actionsTypes';
export function addSettings(settings) {
return {
type: SETTINGS.ADD,
payload: settings
};
}
export function updateSettings(id, value) {
return {
type: SETTINGS.UPDATE,
payload: { id, value }
};
}
export function clearSettings() {
return {
type: SETTINGS.CLEAR
};
}

34
app/actions/settings.ts Normal file
View File

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

View File

@ -1,22 +0,0 @@
import { SHARE } from './actionsTypes';
export function shareSelectServer(server) {
return {
type: SHARE.SELECT_SERVER,
server
};
}
export function shareSetSettings(settings) {
return {
type: SHARE.SET_SETTINGS,
settings
};
}
export function shareSetUser(user) {
return {
type: SHARE.SET_USER,
user
};
}

39
app/actions/share.ts Normal file
View File

@ -0,0 +1,39 @@
import { Action } from 'redux';
import { IShareServer, IShareUser, TShareSettings } from '../reducers/share';
import { SHARE } from './actionsTypes';
interface IShareSelectServer extends Action {
server: IShareServer;
}
interface IShareSetSettings extends Action {
settings: TShareSettings;
}
interface IShareSetUser extends Action {
user: IShareUser;
}
export type TActionsShare = IShareSelectServer & IShareSetSettings & IShareSetUser;
export function shareSelectServer(server: IShareServer): IShareSelectServer {
return {
type: SHARE.SELECT_SERVER,
server
};
}
export function shareSetSettings(settings: TShareSettings): IShareSetSettings {
return {
type: SHARE.SET_SETTINGS,
settings
};
}
export function shareSetUser(user: IShareUser): IShareSetUser {
return {
type: SHARE.SET_USER,
user
};
}

View File

@ -1,15 +0,0 @@
import * as types from './actionsTypes';
export function setAllPreferences(preferences) {
return {
type: types.SORT_PREFERENCES.SET_ALL,
preferences
};
}
export function setPreference(preference) {
return {
type: types.SORT_PREFERENCES.SET,
preference
};
}

View File

@ -0,0 +1,28 @@
import { Action } from 'redux';
import { IPreferences } from '../definitions';
import { SORT_PREFERENCES } from './actionsTypes';
interface ISetAllPreferences extends Action {
preferences: IPreferences;
}
interface ISetPreference extends Action {
preference: Partial<IPreferences>;
}
export type TActionSortPreferences = ISetAllPreferences & ISetPreference;
export function setAllPreferences(preferences: IPreferences): ISetAllPreferences {
return {
type: SORT_PREFERENCES.SET_ALL,
preferences
};
}
export function setPreference(preference: Partial<IPreferences>): ISetPreference {
return {
type: SORT_PREFERENCES.SET,
preference
};
}

View File

@ -1,21 +0,0 @@
import { USERS_TYPING } from './actionsTypes';
export function addUserTyping(username) {
return {
type: USERS_TYPING.ADD,
username
};
}
export function removeUserTyping(username) {
return {
type: USERS_TYPING.REMOVE,
username
};
}
export function clearUserTyping() {
return {
type: USERS_TYPING.CLEAR
};
}

View File

@ -0,0 +1,29 @@
import { Action } from 'redux';
import { USERS_TYPING } from './actionsTypes';
export interface IUsersTypingGenericAction extends Action {
username: string;
}
export type TActionUserTyping = IUsersTypingGenericAction & Action;
export function addUserTyping(username: string): IUsersTypingGenericAction {
return {
type: USERS_TYPING.ADD,
username
};
}
export function removeUserTyping(username: string): IUsersTypingGenericAction {
return {
type: USERS_TYPING.REMOVE,
username
};
}
export function clearUserTyping(): Action {
return {
type: USERS_TYPING.CLEAR
};
}

View File

@ -1,5 +1,6 @@
/* eslint-disable no-bitwise */ /* eslint-disable no-bitwise */
import KeyCommands, { constants } from 'react-native-keycommands'; import { NativeSyntheticEvent } from 'react-native';
import KeyCommands, { constants, KeyCommand } from 'react-native-keycommands';
import I18n from './i18n'; import I18n from './i18n';
@ -136,13 +137,18 @@ const keyCommands = [
})) }))
]; ];
export const setKeyCommands = () => KeyCommands.setKeyCommands(keyCommands); export const setKeyCommands = (): void => KeyCommands.setKeyCommands(keyCommands);
export const deleteKeyCommands = () => KeyCommands.deleteKeyCommands(keyCommands); export const deleteKeyCommands = (): void => KeyCommands.deleteKeyCommands(keyCommands);
export const KEY_COMMAND = 'KEY_COMMAND'; export const KEY_COMMAND = 'KEY_COMMAND';
export const commandHandle = (event, key, flags = []) => { interface IKeyCommandEvent extends NativeSyntheticEvent<typeof KeyCommand> {
input: string;
modifierFlags: string | number;
}
export const commandHandle = (event: IKeyCommandEvent, key: string | string[], flags: string[] = []): boolean => {
const { input, modifierFlags } = event; const { input, modifierFlags } = event;
let _flags = 0; let _flags = 0;
if (flags.includes('command') && flags.includes('alternate')) { if (flags.includes('command') && flags.includes('alternate')) {
@ -155,35 +161,41 @@ export const commandHandle = (event, key, flags = []) => {
return key.includes(input) && modifierFlags === _flags; return key.includes(input) && modifierFlags === _flags;
}; };
export const handleCommandTyping = event => commandHandle(event, KEY_TYPING); export const handleCommandTyping = (event: IKeyCommandEvent): boolean => commandHandle(event, KEY_TYPING);
export const handleCommandSubmit = event => commandHandle(event, KEY_SEND_MESSAGE); export const handleCommandSubmit = (event: IKeyCommandEvent): boolean => commandHandle(event, KEY_SEND_MESSAGE);
export const handleCommandShowUpload = event => commandHandle(event, KEY_UPLOAD, ['command']); export const handleCommandShowUpload = (event: IKeyCommandEvent): boolean => commandHandle(event, KEY_UPLOAD, ['command']);
export const handleCommandScroll = event => export const handleCommandScroll = (event: IKeyCommandEvent): boolean =>
commandHandle(event, [constants.keyInputUpArrow, constants.keyInputDownArrow], ['alternate']); commandHandle(event, [constants.keyInputUpArrow, constants.keyInputDownArrow], ['alternate']);
export const handleCommandRoomActions = event => commandHandle(event, KEY_ROOM_ACTIONS, ['command']); export const handleCommandRoomActions = (event: IKeyCommandEvent): boolean => commandHandle(event, KEY_ROOM_ACTIONS, ['command']);
export const handleCommandSearchMessages = event => commandHandle(event, KEY_SEARCH, ['command']); export const handleCommandSearchMessages = (event: IKeyCommandEvent): boolean => commandHandle(event, KEY_SEARCH, ['command']);
export const handleCommandReplyLatest = event => commandHandle(event, KEY_REPLY, ['command']); export const handleCommandReplyLatest = (event: IKeyCommandEvent): boolean => commandHandle(event, KEY_REPLY, ['command']);
export const handleCommandSelectServer = event => commandHandle(event, KEY_SELECT, ['command', 'alternate']); export const handleCommandSelectServer = (event: IKeyCommandEvent): boolean =>
commandHandle(event, KEY_SELECT, ['command', 'alternate']);
export const handleCommandShowPreferences = event => commandHandle(event, KEY_PREFERENCES, ['command']); export const handleCommandShowPreferences = (event: IKeyCommandEvent): boolean =>
commandHandle(event, KEY_PREFERENCES, ['command']);
export const handleCommandSearching = event => commandHandle(event, KEY_SEARCH, ['command', 'alternate']); export const handleCommandSearching = (event: IKeyCommandEvent): boolean =>
commandHandle(event, KEY_SEARCH, ['command', 'alternate']);
export const handleCommandSelectRoom = event => commandHandle(event, KEY_SELECT, ['command']); export const handleCommandSelectRoom = (event: IKeyCommandEvent): boolean => commandHandle(event, KEY_SELECT, ['command']);
export const handleCommandPreviousRoom = event => commandHandle(event, KEY_PREVIOUS_ROOM, ['command']); export const handleCommandPreviousRoom = (event: IKeyCommandEvent): boolean =>
commandHandle(event, KEY_PREVIOUS_ROOM, ['command']);
export const handleCommandNextRoom = event => commandHandle(event, KEY_NEXT_ROOM, ['command']); export const handleCommandNextRoom = (event: IKeyCommandEvent): boolean => commandHandle(event, KEY_NEXT_ROOM, ['command']);
export const handleCommandShowNewMessage = event => commandHandle(event, KEY_NEW_ROOM, ['command']); export const handleCommandShowNewMessage = (event: IKeyCommandEvent): boolean => commandHandle(event, KEY_NEW_ROOM, ['command']);
export const handleCommandAddNewServer = event => commandHandle(event, KEY_ADD_SERVER, ['command', 'alternate']); export const handleCommandAddNewServer = (event: IKeyCommandEvent): boolean =>
commandHandle(event, KEY_ADD_SERVER, ['command', 'alternate']);
export const handleCommandOpenServerDropdown = event => commandHandle(event, KEY_SERVER_SELECTION, ['command', 'alternate']); export const handleCommandOpenServerDropdown = (event: IKeyCommandEvent): boolean =>
commandHandle(event, KEY_SERVER_SELECTION, ['command', 'alternate']);

View File

@ -1,6 +1,7 @@
export const PASSCODE_KEY = 'kPasscode'; export const PASSCODE_KEY = 'kPasscode';
export const LOCKED_OUT_TIMER_KEY = 'kLockedOutTimer'; export const LOCKED_OUT_TIMER_KEY = 'kLockedOutTimer';
export const ATTEMPTS_KEY = 'kAttempts'; export const ATTEMPTS_KEY = 'kAttempts';
export const BIOMETRY_ENABLED_KEY = 'kBiometryEnabled';
export const LOCAL_AUTHENTICATE_EMITTER = 'LOCAL_AUTHENTICATE'; export const LOCAL_AUTHENTICATE_EMITTER = 'LOCAL_AUTHENTICATE';
export const CHANGE_PASSCODE_EMITTER = 'CHANGE_PASSCODE'; export const CHANGE_PASSCODE_EMITTER = 'CHANGE_PASSCODE';

View File

@ -44,7 +44,7 @@ const Avatar = React.memo(
if (emoji) { if (emoji) {
image = ( image = (
<Emoji <Emoji
theme={theme} theme={theme!}
baseUrl={server} baseUrl={server}
getCustomEmoji={getCustomEmoji} getCustomEmoji={getCustomEmoji}
isMessageContainsOnlyEmoji isMessageContainsOnlyEmoji

View File

@ -1,16 +1,18 @@
import React from 'react'; import React from 'react';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { Q } from '@nozbe/watermelondb'; import { Q } from '@nozbe/watermelondb';
import { Observable, Subscription } from 'rxjs';
import database from '../../lib/database'; import database from '../../lib/database';
import { getUserSelector } from '../../selectors/login'; import { getUserSelector } from '../../selectors/login';
import { TSubscriptionModel, TUserModel } from '../../definitions';
import Avatar from './Avatar'; import Avatar from './Avatar';
import { IAvatar } from './interfaces'; import { IAvatar } from './interfaces';
class AvatarContainer extends React.Component<IAvatar, any> { class AvatarContainer extends React.Component<IAvatar, any> {
private mounted: boolean; private mounted: boolean;
private subscription: any; private subscription?: Subscription;
static defaultProps = { static defaultProps = {
text: '', text: '',
@ -59,15 +61,17 @@ class AvatarContainer extends React.Component<IAvatar, any> {
record = user; record = user;
} else { } else {
const { rid } = this.props; const { rid } = this.props;
record = await subsCollection.find(rid); if (rid) {
record = await subsCollection.find(rid);
}
} }
} catch { } catch {
// Record not found // Record not found
} }
if (record) { if (record) {
const observable = record.observe(); const observable = record.observe() as Observable<TSubscriptionModel | TUserModel>;
this.subscription = observable.subscribe((r: any) => { this.subscription = observable.subscribe(r => {
const { avatarETag } = r; const { avatarETag } = r;
if (this.mounted) { if (this.mounted) {
this.setState({ avatarETag }); this.setState({ avatarETag });

View File

@ -1,3 +1,5 @@
import { TGetCustomEmoji } from '../../definitions/IEmoji';
export interface IAvatar { export interface IAvatar {
server?: string; server?: string;
style?: any; style?: any;
@ -14,10 +16,10 @@ export interface IAvatar {
}; };
theme?: string; theme?: string;
onPress?: () => void; onPress?: () => void;
getCustomEmoji?: () => any; getCustomEmoji?: TGetCustomEmoji;
avatarETag?: string; avatarETag?: string;
isStatic?: boolean | string; isStatic?: boolean | string;
rid?: string; rid?: string;
blockUnauthenticatedAccess?: boolean; blockUnauthenticatedAccess?: boolean;
serverVersion?: string; serverVersion: string;
} }

View File

@ -0,0 +1,17 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Storyshots BackgroundContainer basic 1`] = `"{\\"type\\":\\"View\\",\\"props\\":{\\"style\\":{\\"flex\\":1}},\\"children\\":[{\\"type\\":\\"View\\",\\"props\\":{\\"accessibilityIgnoresInvertColors\\":true,\\"style\\":{\\"width\\":\\"100%\\",\\"height\\":\\"100%\\",\\"position\\":\\"absolute\\"}},\\"children\\":[{\\"type\\":\\"Image\\",\\"props\\":{\\"source\\":{\\"uri\\":\\"message_empty_light\\"},\\"style\\":[{\\"position\\":\\"absolute\\",\\"left\\":0,\\"right\\":0,\\"top\\":0,\\"bottom\\":0},{\\"width\\":\\"100%\\",\\"height\\":\\"100%\\"},null]},\\"children\\":null}]}]}"`;
exports[`Storyshots BackgroundContainer black theme - loading 1`] = `"{\\"type\\":\\"View\\",\\"props\\":{\\"style\\":{\\"flex\\":1}},\\"children\\":[{\\"type\\":\\"View\\",\\"props\\":{\\"accessibilityIgnoresInvertColors\\":true,\\"style\\":{\\"width\\":\\"100%\\",\\"height\\":\\"100%\\",\\"position\\":\\"absolute\\"}},\\"children\\":[{\\"type\\":\\"Image\\",\\"props\\":{\\"source\\":{\\"uri\\":\\"message_empty_black\\"},\\"style\\":[{\\"position\\":\\"absolute\\",\\"left\\":0,\\"right\\":0,\\"top\\":0,\\"bottom\\":0},{\\"width\\":\\"100%\\",\\"height\\":\\"100%\\"},null]},\\"children\\":null}]},{\\"type\\":\\"ActivityIndicator\\",\\"props\\":{\\"animating\\":true,\\"color\\":\\"#f9f9f9\\",\\"hidesWhenStopped\\":true,\\"size\\":\\"small\\",\\"style\\":{\\"position\\":\\"absolute\\",\\"top\\":60,\\"left\\":0,\\"right\\":0,\\"fontSize\\":16,\\"paddingHorizontal\\":24,\\"textAlign\\":\\"center\\",\\"backgroundColor\\":\\"transparent\\",\\"fontFamily\\":\\"System\\",\\"fontWeight\\":\\"400\\"}},\\"children\\":null}]}"`;
exports[`Storyshots BackgroundContainer black theme - text 1`] = `"{\\"type\\":\\"View\\",\\"props\\":{\\"style\\":{\\"flex\\":1}},\\"children\\":[{\\"type\\":\\"View\\",\\"props\\":{\\"accessibilityIgnoresInvertColors\\":true,\\"style\\":{\\"width\\":\\"100%\\",\\"height\\":\\"100%\\",\\"position\\":\\"absolute\\"}},\\"children\\":[{\\"type\\":\\"Image\\",\\"props\\":{\\"source\\":{\\"uri\\":\\"message_empty_black\\"},\\"style\\":[{\\"position\\":\\"absolute\\",\\"left\\":0,\\"right\\":0,\\"top\\":0,\\"bottom\\":0},{\\"width\\":\\"100%\\",\\"height\\":\\"100%\\"},null]},\\"children\\":null}]},{\\"type\\":\\"Text\\",\\"props\\":{\\"style\\":[{\\"position\\":\\"absolute\\",\\"top\\":60,\\"left\\":0,\\"right\\":0,\\"fontSize\\":16,\\"paddingHorizontal\\":24,\\"textAlign\\":\\"center\\",\\"backgroundColor\\":\\"transparent\\",\\"fontFamily\\":\\"System\\",\\"fontWeight\\":\\"400\\"},{\\"color\\":\\"#f9f9f9\\"}]},\\"children\\":[\\"Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industrys standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries\\"]}]}"`;
exports[`Storyshots BackgroundContainer dark theme - loading 1`] = `"{\\"type\\":\\"View\\",\\"props\\":{\\"style\\":{\\"flex\\":1}},\\"children\\":[{\\"type\\":\\"View\\",\\"props\\":{\\"accessibilityIgnoresInvertColors\\":true,\\"style\\":{\\"width\\":\\"100%\\",\\"height\\":\\"100%\\",\\"position\\":\\"absolute\\"}},\\"children\\":[{\\"type\\":\\"Image\\",\\"props\\":{\\"source\\":{\\"uri\\":\\"message_empty_dark\\"},\\"style\\":[{\\"position\\":\\"absolute\\",\\"left\\":0,\\"right\\":0,\\"top\\":0,\\"bottom\\":0},{\\"width\\":\\"100%\\",\\"height\\":\\"100%\\"},null]},\\"children\\":null}]},{\\"type\\":\\"ActivityIndicator\\",\\"props\\":{\\"animating\\":true,\\"color\\":\\"#f9f9f9\\",\\"hidesWhenStopped\\":true,\\"size\\":\\"small\\",\\"style\\":{\\"position\\":\\"absolute\\",\\"top\\":60,\\"left\\":0,\\"right\\":0,\\"fontSize\\":16,\\"paddingHorizontal\\":24,\\"textAlign\\":\\"center\\",\\"backgroundColor\\":\\"transparent\\",\\"fontFamily\\":\\"System\\",\\"fontWeight\\":\\"400\\"}},\\"children\\":null}]}"`;
exports[`Storyshots BackgroundContainer dark theme - text 1`] = `"{\\"type\\":\\"View\\",\\"props\\":{\\"style\\":{\\"flex\\":1}},\\"children\\":[{\\"type\\":\\"View\\",\\"props\\":{\\"accessibilityIgnoresInvertColors\\":true,\\"style\\":{\\"width\\":\\"100%\\",\\"height\\":\\"100%\\",\\"position\\":\\"absolute\\"}},\\"children\\":[{\\"type\\":\\"Image\\",\\"props\\":{\\"source\\":{\\"uri\\":\\"message_empty_dark\\"},\\"style\\":[{\\"position\\":\\"absolute\\",\\"left\\":0,\\"right\\":0,\\"top\\":0,\\"bottom\\":0},{\\"width\\":\\"100%\\",\\"height\\":\\"100%\\"},null]},\\"children\\":null}]},{\\"type\\":\\"Text\\",\\"props\\":{\\"style\\":[{\\"position\\":\\"absolute\\",\\"top\\":60,\\"left\\":0,\\"right\\":0,\\"fontSize\\":16,\\"paddingHorizontal\\":24,\\"textAlign\\":\\"center\\",\\"backgroundColor\\":\\"transparent\\",\\"fontFamily\\":\\"System\\",\\"fontWeight\\":\\"400\\"},{\\"color\\":\\"#f9f9f9\\"}]},\\"children\\":[\\"Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industrys standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries\\"]}]}"`;
exports[`Storyshots BackgroundContainer loading 1`] = `"{\\"type\\":\\"View\\",\\"props\\":{\\"style\\":{\\"flex\\":1}},\\"children\\":[{\\"type\\":\\"View\\",\\"props\\":{\\"accessibilityIgnoresInvertColors\\":true,\\"style\\":{\\"width\\":\\"100%\\",\\"height\\":\\"100%\\",\\"position\\":\\"absolute\\"}},\\"children\\":[{\\"type\\":\\"Image\\",\\"props\\":{\\"source\\":{\\"uri\\":\\"message_empty_light\\"},\\"style\\":[{\\"position\\":\\"absolute\\",\\"left\\":0,\\"right\\":0,\\"top\\":0,\\"bottom\\":0},{\\"width\\":\\"100%\\",\\"height\\":\\"100%\\"},null]},\\"children\\":null}]},{\\"type\\":\\"ActivityIndicator\\",\\"props\\":{\\"animating\\":true,\\"color\\":\\"#6C727A\\",\\"hidesWhenStopped\\":true,\\"size\\":\\"small\\",\\"style\\":{\\"position\\":\\"absolute\\",\\"top\\":60,\\"left\\":0,\\"right\\":0,\\"fontSize\\":16,\\"paddingHorizontal\\":24,\\"textAlign\\":\\"center\\",\\"backgroundColor\\":\\"transparent\\",\\"fontFamily\\":\\"System\\",\\"fontWeight\\":\\"400\\"}},\\"children\\":null}]}"`;
exports[`Storyshots BackgroundContainer long text 1`] = `"{\\"type\\":\\"View\\",\\"props\\":{\\"style\\":{\\"flex\\":1}},\\"children\\":[{\\"type\\":\\"View\\",\\"props\\":{\\"accessibilityIgnoresInvertColors\\":true,\\"style\\":{\\"width\\":\\"100%\\",\\"height\\":\\"100%\\",\\"position\\":\\"absolute\\"}},\\"children\\":[{\\"type\\":\\"Image\\",\\"props\\":{\\"source\\":{\\"uri\\":\\"message_empty_light\\"},\\"style\\":[{\\"position\\":\\"absolute\\",\\"left\\":0,\\"right\\":0,\\"top\\":0,\\"bottom\\":0},{\\"width\\":\\"100%\\",\\"height\\":\\"100%\\"},null]},\\"children\\":null}]},{\\"type\\":\\"Text\\",\\"props\\":{\\"style\\":[{\\"position\\":\\"absolute\\",\\"top\\":60,\\"left\\":0,\\"right\\":0,\\"fontSize\\":16,\\"paddingHorizontal\\":24,\\"textAlign\\":\\"center\\",\\"backgroundColor\\":\\"transparent\\",\\"fontFamily\\":\\"System\\",\\"fontWeight\\":\\"400\\"},{\\"color\\":\\"#6C727A\\"}]},\\"children\\":[\\"Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industrys standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries\\"]}]}"`;
exports[`Storyshots BackgroundContainer text 1`] = `"{\\"type\\":\\"View\\",\\"props\\":{\\"style\\":{\\"flex\\":1}},\\"children\\":[{\\"type\\":\\"View\\",\\"props\\":{\\"accessibilityIgnoresInvertColors\\":true,\\"style\\":{\\"width\\":\\"100%\\",\\"height\\":\\"100%\\",\\"position\\":\\"absolute\\"}},\\"children\\":[{\\"type\\":\\"Image\\",\\"props\\":{\\"source\\":{\\"uri\\":\\"message_empty_light\\"},\\"style\\":[{\\"position\\":\\"absolute\\",\\"left\\":0,\\"right\\":0,\\"top\\":0,\\"bottom\\":0},{\\"width\\":\\"100%\\",\\"height\\":\\"100%\\"},null]},\\"children\\":null}]},{\\"type\\":\\"Text\\",\\"props\\":{\\"style\\":[{\\"position\\":\\"absolute\\",\\"top\\":60,\\"left\\":0,\\"right\\":0,\\"fontSize\\":16,\\"paddingHorizontal\\":24,\\"textAlign\\":\\"center\\",\\"backgroundColor\\":\\"transparent\\",\\"fontFamily\\":\\"System\\",\\"fontWeight\\":\\"400\\"},{\\"color\\":\\"#6C727A\\"}]},\\"children\\":[\\"Text here\\"]}]}"`;

View File

@ -35,7 +35,7 @@ const styles = StyleSheet.create({
const BackgroundContainer = ({ theme, text, loading }: IBackgroundContainer) => ( const BackgroundContainer = ({ theme, text, loading }: IBackgroundContainer) => (
<View style={styles.container}> <View style={styles.container}>
<ImageBackground source={{ uri: `message_empty_${theme}` }} style={styles.image} /> <ImageBackground source={{ uri: `message_empty_${theme}` }} style={styles.image} />
{text ? <Text style={[styles.text, { color: themes[theme!].auxiliaryTintColor }]}>{text}</Text> : null} {text && !loading ? <Text style={[styles.text, { color: themes[theme!].auxiliaryTintColor }]}>{text}</Text> : null}
{loading ? <ActivityIndicator style={styles.text} color={themes[theme!].auxiliaryTintColor} /> : null} {loading ? <ActivityIndicator style={styles.text} color={themes[theme!].auxiliaryTintColor} /> : null}
</View> </View>
); );

View File

@ -1,7 +1,7 @@
import React from 'react'; import React from 'react';
import FastImage from '@rocket.chat/react-native-fast-image'; import FastImage from '@rocket.chat/react-native-fast-image';
import { ICustomEmoji } from './interfaces'; import { ICustomEmoji } from '../../definitions/IEmoji';
const CustomEmoji = React.memo( const CustomEmoji = React.memo(
({ baseUrl, emoji, style }: ICustomEmoji) => ( ({ baseUrl, emoji, style }: ICustomEmoji) => (

View File

@ -5,7 +5,7 @@ import shortnameToUnicode from '../../utils/shortnameToUnicode';
import styles from './styles'; import styles from './styles';
import CustomEmoji from './CustomEmoji'; import CustomEmoji from './CustomEmoji';
import scrollPersistTaps from '../../utils/scrollPersistTaps'; import scrollPersistTaps from '../../utils/scrollPersistTaps';
import { IEmoji, IEmojiCategory } from './interfaces'; import { IEmoji, IEmojiCategory } from '../../definitions/IEmoji';
const EMOJI_SIZE = 50; const EMOJI_SIZE = 50;

View File

@ -17,7 +17,7 @@ import shortnameToUnicode from '../../utils/shortnameToUnicode';
import log from '../../utils/log'; import log from '../../utils/log';
import { themes } from '../../constants/colors'; import { themes } from '../../constants/colors';
import { withTheme } from '../../theme'; import { withTheme } from '../../theme';
import { IEmoji } from './interfaces'; import { IEmoji } from '../../definitions/IEmoji';
const scrollProps = { const scrollProps = {
keyboardShouldPersistTaps: 'always', keyboardShouldPersistTaps: 'always',
@ -36,7 +36,7 @@ interface IEmojiPickerProps {
} }
interface IEmojiPickerState { interface IEmojiPickerState {
frequentlyUsed: []; frequentlyUsed: (string | { content?: string; extension?: string; isCustom: boolean })[];
customEmojis: any; customEmojis: any;
show: boolean; show: boolean;
width: number | null; width: number | null;
@ -114,7 +114,7 @@ class EmojiPicker extends Component<IEmojiPickerProps, IEmojiPickerState> {
// Do nothing // Do nothing
} }
await db.action(async () => { await db.write(async () => {
if (freqEmojiRecord) { if (freqEmojiRecord) {
await freqEmojiRecord.update((f: any) => { await freqEmojiRecord.update((f: any) => {
f.count += 1; f.count += 1;
@ -132,8 +132,8 @@ class EmojiPicker extends Component<IEmojiPickerProps, IEmojiPickerState> {
updateFrequentlyUsed = async () => { updateFrequentlyUsed = async () => {
const db = database.active; const db = database.active;
const frequentlyUsedRecords = await db.get('frequently_used_emojis').query().fetch(); const frequentlyUsedRecords = await db.get('frequently_used_emojis').query().fetch();
let frequentlyUsed: any = orderBy(frequentlyUsedRecords, ['count'], ['desc']); const frequentlyUsedOrdered = orderBy(frequentlyUsedRecords, ['count'], ['desc']);
frequentlyUsed = frequentlyUsed.map((item: IEmoji) => { const frequentlyUsed = frequentlyUsedOrdered.map(item => {
if (item.isCustom) { if (item.isCustom) {
return { content: item.content, extension: item.extension, isCustom: item.isCustom }; return { content: item.content, extension: item.extension, isCustom: item.isCustom };
} }

View File

@ -10,7 +10,8 @@ import database from '../../lib/database';
import { Button } from '../ActionSheet'; import { Button } from '../ActionSheet';
import { useDimensions } from '../../dimensions'; import { useDimensions } from '../../dimensions';
import sharedStyles from '../../views/Styles'; import sharedStyles from '../../views/Styles';
import { IEmoji } from '../EmojiPicker/interfaces'; import { IEmoji } from '../../definitions/IEmoji';
import { TFrequentlyUsedEmojiModel } from '../../definitions/IFrequentlyUsedEmoji';
interface IHeader { interface IHeader {
handleReaction: Function; handleReaction: Function;
@ -90,14 +91,14 @@ const HeaderFooter = React.memo(({ onReaction, theme }: THeaderFooter) => (
)); ));
const Header = React.memo(({ handleReaction, server, message, isMasterDetail, theme }: IHeader) => { const Header = React.memo(({ handleReaction, server, message, isMasterDetail, theme }: IHeader) => {
const [items, setItems] = useState([]); const [items, setItems] = useState<(TFrequentlyUsedEmojiModel | string)[]>([]);
const { width, height }: any = useDimensions(); const { width, height }: any = useDimensions();
const setEmojis = async () => { const setEmojis = async () => {
try { try {
const db = database.active; const db = database.active;
const freqEmojiCollection = db.get('frequently_used_emojis'); const freqEmojiCollection = db.get('frequently_used_emojis');
let freqEmojis = await freqEmojiCollection.query().fetch(); let freqEmojis: (TFrequentlyUsedEmojiModel | string)[] = await freqEmojiCollection.query().fetch();
const isLandscape = width > height; const isLandscape = width > height;
const size = (isLandscape || isMasterDetail ? width / 2 : width) - CONTAINER_MARGIN * 2; const size = (isLandscape || isMasterDetail ? width / 2 : width) - CONTAINER_MARGIN * 2;

View File

@ -15,6 +15,7 @@ import { showConfirmationAlert } from '../../utils/info';
import { useActionSheet } from '../ActionSheet'; import { useActionSheet } from '../ActionSheet';
import Header, { HEADER_HEIGHT } from './Header'; import Header, { HEADER_HEIGHT } from './Header';
import events from '../../utils/log/events'; import events from '../../utils/log/events';
import { TMessageModel } from '../../definitions/IMessage';
interface IMessageActions { interface IMessageActions {
room: { room: {
@ -182,9 +183,9 @@ const MessageActions = React.memo(
if (result.success) { if (result.success) {
const subCollection = db.get('subscriptions'); const subCollection = db.get('subscriptions');
const subRecord = await subCollection.find(rid); const subRecord = await subCollection.find(rid);
await db.action(async () => { await db.write(async () => {
try { try {
await subRecord.update((sub: any) => (sub.lastOpen = ts)); await subRecord.update(sub => (sub.lastOpen = ts));
} catch { } catch {
// do nothing // do nothing
} }
@ -269,11 +270,11 @@ const MessageActions = React.memo(
} }
}; };
const handleToggleTranslation = async (message: any) => { const handleToggleTranslation = async (message: TMessageModel) => {
try { try {
const db = database.active; const db = database.active;
await db.action(async () => { await db.write(async () => {
await message.update((m: any) => { await message.update(m => {
m.autoTranslate = !m.autoTranslate; m.autoTranslate = !m.autoTranslate;
m._updatedAt = new Date(); m._updatedAt = new Date();
}); });
@ -282,7 +283,7 @@ const MessageActions = React.memo(
if (!translatedMessage) { if (!translatedMessage) {
const m = { const m = {
_id: message.id, _id: message.id,
rid: message.subscription.id, rid: message.subscription ? message.subscription.id : '',
u: message.u, u: message.u,
msg: message.msg msg: message.msg
}; };
@ -320,7 +321,7 @@ const MessageActions = React.memo(
}); });
}; };
const getOptions = (message: any) => { const getOptions = (message: TMessageModel) => {
let options: any = []; let options: any = [];
// Reply // Reply
@ -446,7 +447,7 @@ const MessageActions = React.memo(
return options; return options;
}; };
const showMessageActions = async (message: any) => { const showMessageActions = async (message: TMessageModel) => {
logEvent(events.ROOM_SHOW_MSG_ACTIONS); logEvent(events.ROOM_SHOW_MSG_ACTIONS);
await getPermissions(); await getPermissions();
showActionSheet({ showActionSheet({

View File

@ -2,7 +2,7 @@ import React from 'react';
import { View } from 'react-native'; import { View } from 'react-native';
import { KeyboardRegistry } from 'react-native-ui-lib/keyboard'; import { KeyboardRegistry } from 'react-native-ui-lib/keyboard';
import store from '../../lib/createStore'; import { store } from '../../lib/auxStore';
import EmojiPicker from '../EmojiPicker'; import EmojiPicker from '../EmojiPicker';
import styles from './styles'; import styles from './styles';
import { themes } from '../../constants/colors'; import { themes } from '../../constants/colors';

View File

@ -6,7 +6,7 @@ import shortnameToUnicode from '../../../utils/shortnameToUnicode';
import styles from '../styles'; import styles from '../styles';
import MessageboxContext from '../Context'; import MessageboxContext from '../Context';
import CustomEmoji from '../../EmojiPicker/CustomEmoji'; import CustomEmoji from '../../EmojiPicker/CustomEmoji';
import { IEmoji } from '../../EmojiPicker/interfaces'; import { IEmoji } from '../../../definitions/IEmoji';
interface IMessageBoxMentionEmoji { interface IMessageBoxMentionEmoji {
item: IEmoji; item: IEmoji;

View File

@ -8,7 +8,7 @@ import FixedMentionItem from './FixedMentionItem';
import MentionEmoji from './MentionEmoji'; import MentionEmoji from './MentionEmoji';
import { MENTIONS_TRACKING_TYPE_EMOJIS, MENTIONS_TRACKING_TYPE_COMMANDS, MENTIONS_TRACKING_TYPE_CANNED } from '../constants'; import { MENTIONS_TRACKING_TYPE_EMOJIS, MENTIONS_TRACKING_TYPE_COMMANDS, MENTIONS_TRACKING_TYPE_CANNED } from '../constants';
import { themes } from '../../../constants/colors'; import { themes } from '../../../constants/colors';
import { IEmoji } from '../../EmojiPicker/interfaces'; import { IEmoji } from '../../../definitions/IEmoji';
interface IMessageBoxMentionItem { interface IMessageBoxMentionItem {
item: { item: {

View File

@ -3,10 +3,11 @@ import { StyleSheet, Text, View } from 'react-native';
import moment from 'moment'; import moment from 'moment';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import Markdown from '../markdown'; import { MarkdownPreview } from '../markdown';
import { CustomIcon } from '../../lib/Icons'; import { CustomIcon } from '../../lib/Icons';
import sharedStyles from '../../views/Styles'; import sharedStyles from '../../views/Styles';
import { themes } from '../../constants/colors'; import { themes } from '../../constants/colors';
import { IMessage } from '../../definitions/IMessage';
const styles = StyleSheet.create({ const styles = StyleSheet.create({
container: { container: {
@ -42,11 +43,7 @@ const styles = StyleSheet.create({
interface IMessageBoxReplyPreview { interface IMessageBoxReplyPreview {
replying: boolean; replying: boolean;
message: { message: IMessage;
ts: Date;
msg: string;
u: any;
};
Message_TimeFormat: string; Message_TimeFormat: string;
close(): void; close(): void;
baseUrl: string; baseUrl: string;
@ -57,17 +54,7 @@ interface IMessageBoxReplyPreview {
} }
const ReplyPreview = React.memo( const ReplyPreview = React.memo(
({ ({ message, Message_TimeFormat, replying, close, theme, useRealName }: IMessageBoxReplyPreview) => {
message,
Message_TimeFormat,
baseUrl,
username,
replying,
getCustomEmoji,
close,
theme,
useRealName
}: IMessageBoxReplyPreview) => {
if (!replying) { if (!replying) {
return null; return null;
} }
@ -82,16 +69,7 @@ const ReplyPreview = React.memo(
</Text> </Text>
<Text style={[styles.time, { color: themes[theme].auxiliaryText }]}>{time}</Text> <Text style={[styles.time, { color: themes[theme].auxiliaryText }]}>{time}</Text>
</View> </View>
{/* @ts-ignore*/} <MarkdownPreview msg={message.msg} />
<Markdown
msg={message.msg}
baseUrl={baseUrl}
username={username}
getCustomEmoji={getCustomEmoji}
numberOfLines={1}
preview
theme={theme}
/>
</View> </View>
<CustomIcon name='close' color={themes[theme].auxiliaryText} size={20} style={styles.close} onPress={close} /> <CustomIcon name='close' color={themes[theme].auxiliaryText} size={20} style={styles.close} onPress={close} />
</View> </View>

View File

@ -0,0 +1,107 @@
import { Image } from 'react-native-image-crop-picker';
import { forceJpgExtension } from './forceJpgExtension';
const attachment: Image = {
exif: null,
filename: 'IMG_0040.PNG',
path: 'tmp/temp',
height: 534,
width: 223,
data: null,
modificationDate: '1643984790',
localIdentifier: 'device/L0/001',
size: 16623,
sourceURL: '',
mime: 'image/jpeg',
cropRect: null,
creationDate: '1641490665'
};
describe('forceJpgExtension for iOS', () => {
jest.mock('react-native', () => ({ Platform: { OS: 'ios' } }));
describe('with mime as image/jpeg', () => {
test('filename.jpg should be filename.jpg', () => {
const newAttachment = attachment;
newAttachment.filename = 'filename.jpg';
const file = forceJpgExtension(newAttachment);
expect(file.filename).toBe('filename.jpg');
});
test('filename.png should be filename.jpg', () => {
const newAttachment = attachment;
newAttachment.filename = 'filename.png';
const file = forceJpgExtension(newAttachment);
expect(file.filename).toBe('filename.jpg');
});
test('filename.jpeg should be filename.jpg', () => {
const newAttachment = attachment;
newAttachment.filename = 'filename.jpeg';
const file = forceJpgExtension(newAttachment);
expect(file.filename).toBe('filename.jpg');
});
test('filename.heic should be filename.jpg', () => {
const newAttachment = attachment;
newAttachment.filename = 'filename.heic';
const file = forceJpgExtension(newAttachment);
expect(file.filename).toBe('filename.jpg');
});
});
describe('with mime different', () => {
test('filename.jpg should be filename.jpg', () => {
const newAttachment = attachment;
newAttachment.filename = 'filename.png';
newAttachment.mime = 'image/png';
const file = forceJpgExtension(newAttachment);
expect(file.filename).toBe('filename.png');
});
});
});
describe('forceJpgExtension for android', () => {
jest.mock('react-native', () => ({ Platform: { OS: 'android' } }));
describe('with mime as image/jpeg', () => {
test('filename.jpg should be filename.jpg', () => {
const newAttachment = attachment;
newAttachment.filename = 'filename.jpg';
const file = forceJpgExtension(newAttachment);
expect(file.filename).toBe('filename.jpg');
});
test('filename.png should be filename.png', () => {
const newAttachment = attachment;
newAttachment.filename = 'filename.png';
const file = forceJpgExtension(newAttachment);
expect(file.filename).toBe('filename.png');
});
test('filename.jpeg should be filename.jpeg', () => {
const newAttachment = attachment;
newAttachment.filename = 'filename.jpeg';
const file = forceJpgExtension(newAttachment);
expect(file.filename).toBe('filename.jpeg');
});
test('filename.heic should be filename.heic', () => {
const newAttachment = attachment;
newAttachment.filename = 'filename.heic';
const file = forceJpgExtension(newAttachment);
expect(file.filename).toBe('filename.heic');
});
});
describe('with mime different', () => {
test('filename.jpg should be filename.jpg', () => {
const newAttachment = attachment;
newAttachment.filename = 'filename.png';
newAttachment.mime = 'image/png';
const file = forceJpgExtension(newAttachment);
expect(file.filename).toBe('filename.png');
});
});
});

View File

@ -0,0 +1,13 @@
import { ImageOrVideo } from 'react-native-image-crop-picker';
import { isIOS } from '../../utils/deviceInfo';
const regex = new RegExp(/\.[^/.]+$/); // Check from last '.' of the string
export const forceJpgExtension = (attachment: ImageOrVideo): ImageOrVideo => {
if (isIOS && attachment.mime === 'image/jpeg' && attachment.filename) {
// Replace files extension that mime type is 'image/jpeg' to .jpg;
attachment.filename = attachment.filename.replace(regex, '.jpg');
}
return attachment;
};

View File

@ -27,7 +27,7 @@ import LeftButtons from './LeftButtons';
// @ts-ignore // @ts-ignore
// eslint-disable-next-line import/extensions,import/no-unresolved // eslint-disable-next-line import/extensions,import/no-unresolved
import RightButtons from './RightButtons'; import RightButtons from './RightButtons';
import { isAndroid, isIOS, isTablet } from '../../utils/deviceInfo'; import { isAndroid, isTablet } from '../../utils/deviceInfo';
import { canUploadFile } from '../../utils/media'; import { canUploadFile } from '../../utils/media';
import EventEmiter from '../../utils/events'; import EventEmiter from '../../utils/events';
import { KEY_COMMAND, handleCommandShowUpload, handleCommandSubmit, handleCommandTyping } from '../../commands'; import { KEY_COMMAND, handleCommandShowUpload, handleCommandSubmit, handleCommandTyping } from '../../commands';
@ -47,6 +47,8 @@ import Navigation from '../../lib/Navigation';
import { withActionSheet } from '../ActionSheet'; import { withActionSheet } from '../ActionSheet';
import { sanitizeLikeString } from '../../lib/database/utils'; import { sanitizeLikeString } from '../../lib/database/utils';
import { CustomIcon } from '../../lib/Icons'; import { CustomIcon } from '../../lib/Icons';
import { IMessage } from '../../definitions/IMessage';
import { forceJpgExtension } from './forceJpgExtension';
if (isAndroid) { if (isAndroid) {
require('./EmojiKeyboard'); require('./EmojiKeyboard');
@ -73,18 +75,14 @@ const videoPickerConfig = {
interface IMessageBoxProps { interface IMessageBoxProps {
rid: string; rid: string;
baseUrl: string; baseUrl: string;
message: { message: IMessage;
u: {
username: string;
};
id: any;
};
replying: boolean; replying: boolean;
editing: boolean; editing: boolean;
threadsEnabled: boolean; threadsEnabled: boolean;
isFocused(): boolean; isFocused(): boolean;
user: { user: {
id: string; id: string;
_id: string;
username: string; username: string;
token: string; token: string;
}; };
@ -130,18 +128,6 @@ interface IMessageBoxState {
permissionToUpload: boolean; permissionToUpload: boolean;
} }
const forceJpgExtension = (attachment: ImageOrVideo) => {
if (isIOS && attachment.mime === 'image/jpeg' && attachment.filename) {
const regex = new RegExp(/.heic$/i);
if (attachment.filename.match(regex)) {
attachment.filename = attachment.filename.replace(regex, '.jpg');
} else {
attachment.filename += '.jpg';
}
}
return attachment;
};
class MessageBox extends Component<IMessageBoxProps, IMessageBoxState> { class MessageBox extends Component<IMessageBoxProps, IMessageBoxState> {
private text: string; private text: string;
@ -1083,7 +1069,6 @@ class MessageBox extends Component<IMessageBoxProps, IMessageBoxState> {
const replyPreview = !recording ? ( const replyPreview = !recording ? (
<ReplyPreview <ReplyPreview
// @ts-ignore
message={message} message={message}
close={replyCancel} close={replyCancel}
username={user.username} username={user.username}
@ -1200,5 +1185,5 @@ const mapStateToProps = (state: any) => ({
const dispatchToProps = { const dispatchToProps = {
typing: (rid: any, status: any) => userTypingAction(rid, status) typing: (rid: any, status: any) => userTypingAction(rid, status)
}; };
// @ts-ignore
export default connect(mapStateToProps, dispatchToProps, null, { forwardRef: true })(withActionSheet(MessageBox)) as any; export default connect(mapStateToProps, dispatchToProps, null, { forwardRef: true })(withActionSheet(MessageBox)) as any;

View File

@ -36,7 +36,7 @@ const MessageErrorActions = forwardRef(({ tmid }: any, ref): any => {
try { try {
// Find the thread header and update it // Find the thread header and update it
const msg = await msgCollection.find(tmid); const msg = await msgCollection.find(tmid);
if (msg.tcount <= 1) { if (msg?.tcount && msg.tcount <= 1) {
deleteBatch.push( deleteBatch.push(
msg.prepareUpdate((m: any) => { msg.prepareUpdate((m: any) => {
m.tcount = null; m.tcount = null;
@ -62,7 +62,7 @@ const MessageErrorActions = forwardRef(({ tmid }: any, ref): any => {
// Do nothing: message not found // Do nothing: message not found
} }
} }
await db.action(async () => { await db.write(async () => {
await db.batch(...deleteBatch); await db.batch(...deleteBatch);
}); });
} catch (e) { } catch (e) {

View File

@ -9,6 +9,7 @@ import { CustomIcon } from '../lib/Icons';
import sharedStyles from '../views/Styles'; import sharedStyles from '../views/Styles';
import { themes } from '../constants/colors'; import { themes } from '../constants/colors';
import { withTheme } from '../theme'; import { withTheme } from '../theme';
import { TGetCustomEmoji } from '../definitions/IEmoji';
import SafeAreaView from './SafeAreaView'; import SafeAreaView from './SafeAreaView';
const styles = StyleSheet.create({ const styles = StyleSheet.create({
@ -66,7 +67,7 @@ interface IItem {
}; };
user?: { username: any }; user?: { username: any };
baseUrl?: string; baseUrl?: string;
getCustomEmoji?: Function; getCustomEmoji?: TGetCustomEmoji;
theme?: string; theme?: string;
} }

View File

@ -4,7 +4,7 @@ import { StyleSheet, Text, TouchableOpacity, View } from 'react-native';
import I18n from '../../i18n'; import I18n from '../../i18n';
import sharedStyles from '../../views/Styles'; import sharedStyles from '../../views/Styles';
import { themes } from '../../constants/colors'; import { themes } from '../../constants/colors';
import Markdown from '../markdown'; import { MarkdownPreview } from '../markdown';
import RoomTypeIcon from '../RoomTypeIcon'; import RoomTypeIcon from '../RoomTypeIcon';
import { withTheme } from '../../theme'; import { withTheme } from '../../theme';
@ -101,16 +101,7 @@ const SubTitle = React.memo(({ usersTyping, subtitle, renderFunc, theme, scale }
// subtitle // subtitle
if (subtitle) { if (subtitle) {
return ( return <MarkdownPreview msg={subtitle} style={[styles.subtitle, { fontSize, color: themes[theme].auxiliaryText }]} />;
// @ts-ignore
<Markdown
preview
msg={subtitle}
style={[styles.subtitle, { fontSize, color: themes[theme].auxiliaryText }]}
numberOfLines={1}
theme={theme}
/>
);
} }
return null; return null;
@ -126,10 +117,7 @@ const HeaderTitle = React.memo(({ title, tmid, prid, scale, theme, testID }: TRo
); );
} }
return ( return <MarkdownPreview msg={title} style={[styles.title, titleStyle]} testID={testID} />;
// @ts-ignore
<Markdown preview msg={title} style={[styles.title, titleStyle]} numberOfLines={1} theme={theme} testID={testID} />
);
}); });
const Header = React.memo( const Header = React.memo(

File diff suppressed because one or more lines are too long

View File

@ -1,10 +1,11 @@
import { dequal } from 'dequal';
import React, { Component } from 'react'; import React, { Component } from 'react';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { dequal } from 'dequal';
import RoomHeader from './RoomHeader'; import { IApplicationState } from '../../definitions';
import { withDimensions } from '../../dimensions'; import { withDimensions } from '../../dimensions';
import I18n from '../../i18n'; import I18n from '../../i18n';
import RoomHeader from './RoomHeader';
interface IRoomHeaderContainerProps { interface IRoomHeaderContainerProps {
title: string; title: string;
@ -122,8 +123,8 @@ class RoomHeaderContainer extends Component<IRoomHeaderContainerProps, any> {
} }
} }
const mapStateToProps = (state: any, ownProps: any) => { const mapStateToProps = (state: IApplicationState, ownProps: any) => {
let statusText; let statusText = '';
let status = 'offline'; let status = 'offline';
const { roomUserId, type, visitor = {}, tmid } = ownProps; const { roomUserId, type, visitor = {}, tmid } = ownProps;

View File

@ -1,5 +1,13 @@
import React from 'react'; import React from 'react';
import { NativeSyntheticEvent, StyleSheet, Text, TextInputFocusEventData, TextInputProps, View } from 'react-native'; import {
NativeSyntheticEvent,
StyleSheet,
TextInput as RNTextInput,
Text,
TextInputFocusEventData,
TextInputProps,
View
} from 'react-native';
import Touchable from 'react-native-platform-touchable'; import Touchable from 'react-native-platform-touchable';
import TextInput from '../presentation/TextInput'; import TextInput from '../presentation/TextInput';
@ -51,7 +59,7 @@ interface ISearchBox {
hasCancel?: boolean; hasCancel?: boolean;
onCancelPress?: Function; onCancelPress?: Function;
theme?: string; theme?: string;
inputRef?: React.Ref<unknown>; inputRef?: React.Ref<RNTextInput>;
testID?: string; testID?: string;
onFocus?: (e: NativeSyntheticEvent<TextInputFocusEventData>) => void; onFocus?: (e: NativeSyntheticEvent<TextInputFocusEventData>) => void;
} }

View File

@ -1,7 +1,8 @@
import React from 'react'; import React from 'react';
import { StyleSheet, View } from 'react-native'; import { StyleSheet, View } from 'react-native';
import { withTheme } from '../theme'; import I18n from '../i18n';
import { useTheme } from '../theme';
import sharedStyles from '../views/Styles'; import sharedStyles from '../views/Styles';
import { themes } from '../constants/colors'; import { themes } from '../constants/colors';
import TextInput from '../presentation/TextInput'; import TextInput from '../presentation/TextInput';
@ -19,14 +20,13 @@ const styles = StyleSheet.create({
} }
}); });
interface ISearchHeader { interface ISearchHeaderProps {
theme?: string;
onSearchChangeText?: (text: string) => void; onSearchChangeText?: (text: string) => void;
testID?: string;
} }
// TODO: it might be useful to refactor this component for reusage const SearchHeader = ({ onSearchChangeText, testID }: ISearchHeaderProps): JSX.Element => {
const SearchHeader = ({ theme, onSearchChangeText }: ISearchHeader) => { const { theme } = useTheme();
const titleColorStyle = { color: themes[theme!].headerTitleColor };
const isLight = theme === 'light'; const isLight = theme === 'light';
const { isLandscape } = useOrientation(); const { isLandscape } = useOrientation();
const scale = isIOS && isLandscape && !isTablet ? 0.8 : 1; const scale = isIOS && isLandscape && !isTablet ? 0.8 : 1;
@ -36,14 +36,14 @@ const SearchHeader = ({ theme, onSearchChangeText }: ISearchHeader) => {
<View style={styles.container}> <View style={styles.container}>
<TextInput <TextInput
autoFocus autoFocus
style={[styles.title, isLight && titleColorStyle, { fontSize: titleFontSize }]} style={[styles.title, isLight && { color: themes[theme].headerTitleColor }, { fontSize: titleFontSize }]}
placeholder='Search' placeholder={I18n.t('Search')}
onChangeText={onSearchChangeText} onChangeText={onSearchChangeText}
theme={theme!} theme={theme}
testID='thread-messages-view-search-header' testID={testID}
/> />
</View> </View>
); );
}; };
export default withTheme(SearchHeader); export default SearchHeader;

View File

@ -1,5 +1,5 @@
import React from 'react'; import React from 'react';
import { StyleProp, StyleSheet, Text, TextInputProps, TextStyle, View, ViewStyle } from 'react-native'; import { StyleProp, StyleSheet, Text, TextInputProps, TextInput as RNTextInput, TextStyle, View, ViewStyle } from 'react-native';
import Touchable from 'react-native-platform-touchable'; import Touchable from 'react-native-platform-touchable';
import sharedStyles from '../views/Styles'; import sharedStyles from '../views/Styles';
@ -59,7 +59,7 @@ export interface IRCTextInputProps extends TextInputProps {
loading?: boolean; loading?: boolean;
containerStyle?: StyleProp<ViewStyle>; containerStyle?: StyleProp<ViewStyle>;
inputStyle?: StyleProp<TextStyle>; inputStyle?: StyleProp<TextStyle>;
inputRef?: React.Ref<unknown>; inputRef?: React.Ref<RNTextInput>;
testID?: string; testID?: string;
iconLeft?: string; iconLeft?: string;
iconRight?: string; iconRight?: string;

View File

@ -5,7 +5,7 @@ import Touchable from 'react-native-platform-touchable';
import { CustomIcon } from '../lib/Icons'; import { CustomIcon } from '../lib/Icons';
import { themes } from '../constants/colors'; import { themes } from '../constants/colors';
import sharedStyles from '../views/Styles'; import sharedStyles from '../views/Styles';
import { withTheme } from '../theme'; import { useTheme } from '../theme';
import { TThreadModel } from '../definitions/IThread'; import { TThreadModel } from '../definitions/IThread';
const styles = StyleSheet.create({ const styles = StyleSheet.create({
@ -48,12 +48,12 @@ interface IThreadDetails {
badgeColor?: string; badgeColor?: string;
toggleFollowThread: Function; toggleFollowThread: Function;
style: ViewStyle; style: ViewStyle;
theme?: string;
} }
const ThreadDetails = ({ item, user, badgeColor, toggleFollowThread, style, theme }: IThreadDetails) => { const ThreadDetails = ({ item, user, badgeColor, toggleFollowThread, style }: IThreadDetails): JSX.Element => {
const { theme } = useTheme();
let { tcount } = item; let { tcount } = item;
if (tcount! >= 1000) { if (tcount && tcount >= 1000) {
tcount = '+999'; tcount = '+999';
} }
@ -81,7 +81,6 @@ const ThreadDetails = ({ item, user, badgeColor, toggleFollowThread, style, them
</Text> </Text>
</View> </View>
</View> </View>
<View style={styles.badgeContainer}> <View style={styles.badgeContainer}>
{badgeColor ? <View style={[styles.badge, { backgroundColor: badgeColor }]} /> : null} {badgeColor ? <View style={[styles.badge, { backgroundColor: badgeColor }]} /> : null}
<Touchable onPress={() => toggleFollowThread?.(isFollowing, item.id)}> <Touchable onPress={() => toggleFollowThread?.(isFollowing, item.id)}>
@ -96,4 +95,4 @@ const ThreadDetails = ({ item, user, badgeColor, toggleFollowThread, style, them
); );
}; };
export default withTheme(ThreadDetails); export default ThreadDetails;

View File

@ -22,8 +22,8 @@ interface IMultiSelect {
context?: number; context?: number;
loading?: boolean; loading?: boolean;
multiselect?: boolean; multiselect?: boolean;
onSearch: Function; onSearch?: () => void;
onClose: Function; onClose?: () => void;
inputStyle: object; inputStyle: object;
value?: any[]; value?: any[];
disabled?: boolean | object; disabled?: boolean | object;

View File

@ -3,7 +3,7 @@ import React, { useContext } from 'react';
import { StyleSheet, Text } from 'react-native'; import { StyleSheet, Text } from 'react-native';
import { BLOCK_CONTEXT, UiKitParserMessage, UiKitParserModal, uiKitMessage, uiKitModal } from '@rocket.chat/ui-kit'; import { BLOCK_CONTEXT, UiKitParserMessage, UiKitParserModal, uiKitMessage, uiKitModal } from '@rocket.chat/ui-kit';
import Markdown from '../markdown'; import Markdown, { MarkdownPreview } from '../markdown';
import Button from '../Button'; import Button from '../Button';
import TextInput from '../TextInput'; import TextInput from '../TextInput';
import { textParser, useBlockContext } from './utils'; import { textParser, useBlockContext } from './utils';
@ -49,10 +49,10 @@ class MessageParser extends UiKitParserMessage {
} }
const isContext = context === BLOCK_CONTEXT.CONTEXT; const isContext = context === BLOCK_CONTEXT.CONTEXT;
return ( if (isContext) {
// @ts-ignore return <MarkdownPreview msg={text} style={[isContext && { color: themes[theme].auxiliaryText }]} numberOfLines={0} />;
<Markdown msg={text} theme={theme} style={[isContext && { color: themes[theme].auxiliaryText }]} preview={isContext} /> }
); return <Markdown msg={text} theme={theme} style={[isContext && { color: themes[theme].auxiliaryText }]} />;
} }
button(element: any, context: any) { button(element: any, context: any) {

View File

@ -0,0 +1,3 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Storyshots Text Input Short and Long Text 1`] = `"{\\"type\\":\\"View\\",\\"props\\":{\\"style\\":{\\"paddingHorizontal\\":14}},\\"children\\":[{\\"type\\":\\"View\\",\\"props\\":{\\"style\\":[{\\"marginBottom\\":10},null]},\\"children\\":[{\\"type\\":\\"Text\\",\\"props\\":{\\"style\\":[{\\"marginBottom\\":10,\\"fontSize\\":14,\\"textAlign\\":\\"left\\",\\"backgroundColor\\":\\"transparent\\",\\"fontFamily\\":\\"System\\",\\"fontWeight\\":\\"600\\"},{\\"color\\":\\"#0d0e12\\"},null]},\\"children\\":[\\"Short Text\\"]},{\\"type\\":\\"View\\",\\"props\\":{\\"style\\":{\\"position\\":\\"relative\\"}},\\"children\\":[{\\"type\\":\\"TextInput\\",\\"props\\":{\\"allowFontScaling\\":true,\\"rejectResponderTermination\\":true,\\"underlineColorAndroid\\":\\"transparent\\",\\"style\\":[{\\"color\\":\\"#0d0e12\\"},[{\\"textAlign\\":\\"left\\",\\"backgroundColor\\":\\"transparent\\",\\"fontFamily\\":\\"System\\",\\"fontWeight\\":\\"400\\",\\"height\\":48,\\"fontSize\\":16,\\"padding\\":14,\\"borderWidth\\":0.5,\\"borderRadius\\":2},null,null,{\\"backgroundColor\\":\\"#ffffff\\",\\"borderColor\\":\\"#cbcbcc\\",\\"color\\":\\"#0d0e12\\"},null,null],{\\"textAlign\\":\\"auto\\"}],\\"placeholderTextColor\\":\\"#9ca2a8\\",\\"keyboardAppearance\\":\\"light\\",\\"autoCorrect\\":false,\\"autoCapitalize\\":\\"none\\",\\"accessibilityLabel\\":\\"placeholder\\",\\"placeholder\\":\\"placeholder\\",\\"value\\":\\"Rocket.Chat\\"},\\"children\\":null}]}]},{\\"type\\":\\"View\\",\\"props\\":{\\"style\\":[{\\"marginBottom\\":10},null]},\\"children\\":[{\\"type\\":\\"Text\\",\\"props\\":{\\"style\\":[{\\"marginBottom\\":10,\\"fontSize\\":14,\\"textAlign\\":\\"left\\",\\"backgroundColor\\":\\"transparent\\",\\"fontFamily\\":\\"System\\",\\"fontWeight\\":\\"600\\"},{\\"color\\":\\"#0d0e12\\"},null]},\\"children\\":[\\"Long Text\\"]},{\\"type\\":\\"View\\",\\"props\\":{\\"style\\":{\\"position\\":\\"relative\\"}},\\"children\\":[{\\"type\\":\\"TextInput\\",\\"props\\":{\\"allowFontScaling\\":true,\\"rejectResponderTermination\\":true,\\"underlineColorAndroid\\":\\"transparent\\",\\"style\\":[{\\"color\\":\\"#0d0e12\\"},[{\\"textAlign\\":\\"left\\",\\"backgroundColor\\":\\"transparent\\",\\"fontFamily\\":\\"System\\",\\"fontWeight\\":\\"400\\",\\"height\\":48,\\"fontSize\\":16,\\"padding\\":14,\\"borderWidth\\":0.5,\\"borderRadius\\":2},null,null,{\\"backgroundColor\\":\\"#ffffff\\",\\"borderColor\\":\\"#cbcbcc\\",\\"color\\":\\"#0d0e12\\"},null,null],{\\"textAlign\\":\\"auto\\"}],\\"placeholderTextColor\\":\\"#9ca2a8\\",\\"keyboardAppearance\\":\\"light\\",\\"autoCorrect\\":false,\\"autoCapitalize\\":\\"none\\",\\"accessibilityLabel\\":\\"placeholder\\",\\"placeholder\\":\\"placeholder\\",\\"value\\":\\"https://open.rocket.chat/images/logo/android-chrome-512x512.png\\"},\\"children\\":null}]}]}]}"`;

View File

@ -8,10 +8,10 @@ import { events, logEvent } from '../../utils/log';
interface IAtMention { interface IAtMention {
mention: string; mention: string;
username: string; username?: string;
navToRoomInfo: Function; navToRoomInfo?: Function;
style?: any; style?: any;
useRealName: boolean; useRealName?: boolean;
mentions: any; mentions: any;
} }
@ -51,7 +51,9 @@ const AtMention = React.memo(({ mention, mentions, username, navToRoomInfo, styl
t: 'd', t: 'd',
rid: user && user._id rid: user && user._id
}; };
navToRoomInfo(navParam); if (navToRoomInfo) {
navToRoomInfo(navParam);
}
}; };
if (user) { if (user) {

View File

@ -12,8 +12,8 @@ interface IEmoji {
getCustomEmoji?: Function; getCustomEmoji?: Function;
baseUrl: string; baseUrl: string;
customEmojis?: any; customEmojis?: any;
style: object; style?: object;
theme?: string; theme: string;
onEmojiSelected?: Function; onEmojiSelected?: Function;
tabEmojiStyle?: object; tabEmojiStyle?: object;
} }
@ -32,7 +32,7 @@ const Emoji = React.memo(
); );
} }
return ( return (
<Text style={[{ color: themes[theme!].bodyText }, isMessageContainsOnlyEmoji ? styles.textBig : styles.text, style]}> <Text style={[{ color: themes[theme].bodyText }, isMessageContainsOnlyEmoji ? styles.textBig : styles.text, style]}>
{emojiUnicode} {emojiUnicode}
</Text> </Text>
); );

View File

@ -1,19 +1,16 @@
import React from 'react'; import React from 'react';
import { Text, TextStyle } from 'react-native'; import { Text, TextStyle, StyleProp } from 'react-native';
import { themes } from '../../constants/colors'; import { themes } from '../../constants/colors';
import { useTheme } from '../../theme'; import { useTheme } from '../../theme';
import { IUserChannel } from './interfaces';
import styles from './styles'; import styles from './styles';
interface IHashtag { interface IHashtag {
hashtag: string; hashtag: string;
navToRoomInfo: Function; navToRoomInfo?: Function;
style?: TextStyle[]; style?: StyleProp<TextStyle>[];
channels: { channels?: IUserChannel[];
[index: number]: string | number;
name: string;
_id: number;
}[];
} }
const Hashtag = React.memo(({ hashtag, channels, navToRoomInfo, style = [] }: IHashtag) => { const Hashtag = React.memo(({ hashtag, channels, navToRoomInfo, style = [] }: IHashtag) => {
@ -21,11 +18,13 @@ const Hashtag = React.memo(({ hashtag, channels, navToRoomInfo, style = [] }: IH
const handlePress = () => { const handlePress = () => {
const index = channels?.findIndex(channel => channel.name === hashtag); const index = channels?.findIndex(channel => channel.name === hashtag);
const navParam = { if (index && navToRoomInfo) {
t: 'c', const navParam = {
rid: channels[index]._id t: 'c',
}; rid: channels?.[index]._id
navToRoomInfo(navParam); };
navToRoomInfo(navParam);
}
}; };
if (channels && channels.length && channels.findIndex(channel => channel.name === hashtag) !== -1) { if (channels && channels.length && channels.findIndex(channel => channel.name === hashtag) !== -1) {

View File

@ -7,12 +7,13 @@ import { LISTENER } from '../Toast';
import EventEmitter from '../../utils/events'; import EventEmitter from '../../utils/events';
import I18n from '../../i18n'; import I18n from '../../i18n';
import openLink from '../../utils/openLink'; import openLink from '../../utils/openLink';
import { TOnLinkPress } from './interfaces';
interface ILink { interface ILink {
children: JSX.Element; children: JSX.Element;
link: string; link: string;
theme: string; theme: string;
onLinkPress: Function; onLinkPress?: TOnLinkPress;
} }
const Link = React.memo(({ children, link, theme, onLinkPress }: ILink) => { const Link = React.memo(({ children, link, theme, onLinkPress }: ILink) => {

View File

@ -5,7 +5,7 @@ interface IList {
ordered: boolean; ordered: boolean;
start: number; start: number;
tight: boolean; tight: boolean;
numberOfLines: number; numberOfLines?: number;
} }
const List = React.memo(({ children, ordered, tight, start = 1, numberOfLines = 0 }: IList) => { const List = React.memo(({ children, ordered, tight, start = 1, numberOfLines = 0 }: IList) => {

View File

@ -0,0 +1,44 @@
import React from 'react';
import { StyleProp, Text, TextStyle } from 'react-native';
import removeMarkdown from 'remove-markdown';
import shortnameToUnicode from '../../utils/shortnameToUnicode';
import { themes } from '../../constants/colors';
import { formatText } from './formatText';
import { useTheme } from '../../theme';
import styles from './styles';
import { formatHyperlink } from './formatHyperlink';
interface IMarkdownPreview {
msg?: string;
numberOfLines?: number;
testID?: string;
style?: StyleProp<TextStyle>[];
}
const MarkdownPreview = ({ msg, numberOfLines = 1, testID, style = [] }: IMarkdownPreview): React.ReactElement | null => {
if (!msg) {
return null;
}
const { theme } = useTheme();
let m = formatText(msg);
m = formatHyperlink(m);
m = shortnameToUnicode(m);
// Removes sequential empty spaces
m = m.replace(/\s+/g, ' ');
m = removeMarkdown(m);
m = m.replace(/\n+/g, ' ');
return (
<Text
accessibilityLabel={m}
style={[styles.text, { color: themes[theme].bodyText }, ...style]}
numberOfLines={numberOfLines}
testID={testID}>
{m}
</Text>
);
};
export default MarkdownPreview;

View File

@ -0,0 +1,26 @@
import { formatHyperlink } from './formatHyperlink';
describe('FormatText', () => {
test('empty to be empty', () => {
expect(formatHyperlink('')).toBe('');
});
test('A123 to be A123', () => {
expect(formatHyperlink('A123')).toBe('A123');
});
test('Format <http://link|Text> to be <http://link|Text>', () => {
expect(formatHyperlink('<http://link|Text>')).toBe('<http://link|Text>');
});
test('Format "[ ](https://open.rocket.chat/) Test" to be Test', () => {
expect(formatHyperlink('[ ](https://open.rocket.chat/) Test')).toBe('Test');
});
test('Format "[Open](https://open.rocket.chat/) Test" to be Test', () => {
expect(formatHyperlink('[Open](https://open.rocket.chat/) Test')).toBe('[Open](https://open.rocket.chat/) Test');
});
test('render test (arabic)', () => {
expect(formatHyperlink('[ ](https://open.rocket.chat/) اختبا')).toBe('اختبا');
});
test('render test (russian)', () => {
expect(formatHyperlink('[ ](https://open.rocket.chat/) тест123')).toBe(ест123');
});
});

View File

@ -0,0 +1,3 @@
// Ex: '[ ](https://open.rocket.chat/group/test?msg=abcdef) Test'
// Return: 'Test'
export const formatHyperlink = (text: string): string => text.replace(/^\[([\s]*)\]\(([^)]*)\)\s/, '').trim();

View File

@ -0,0 +1,20 @@
import { formatText } from './formatText';
describe('FormatText', () => {
test('empty to be empty', () => {
expect(formatText('')).toBe('');
});
test('A123 to be A123', () => {
expect(formatText('A123')).toBe('A123');
});
test('Format <http://link|Text> to be [Text](http://link)', () => {
expect(formatText('<http://link|Text>')).toBe('[Text](http://link)');
});
test('render test (arabic)', () => {
expect(formatText('اختبا <http://link|ر123>')).toBe('اختبا [ر123](http://link)');
});
test('render test (russian)', () => {
expect(formatText('<http://link|тест123>')).toBe('[тест123](http://link)');
});
});

View File

@ -0,0 +1,6 @@
// Support <http://link|Text>
export const formatText = (text: string): string =>
text.replace(
new RegExp('(?:<|<)((?:https|http):\\/\\/[^\\|]+)\\|(.+?)(?=>|>)(?:>|>)', 'gm'),
(match, url, title) => `[${title}](${url})`
);

View File

@ -1,12 +1,9 @@
import React, { PureComponent } from 'react'; import React, { PureComponent } from 'react';
import { Image, Text } from 'react-native'; import { Image, StyleProp, Text, TextStyle } from 'react-native';
import { Node, Parser } from 'commonmark'; import { Node, Parser } from 'commonmark';
import Renderer from 'commonmark-react-renderer'; import Renderer from 'commonmark-react-renderer';
import removeMarkdown from 'remove-markdown';
import { MarkdownAST } from '@rocket.chat/message-parser'; import { MarkdownAST } from '@rocket.chat/message-parser';
import { UserMention } from '../message/interfaces';
import shortnameToUnicode from '../../utils/shortnameToUnicode';
import I18n from '../../i18n'; import I18n from '../../i18n';
import { themes } from '../../constants/colors'; import { themes } from '../../constants/colors';
import MarkdownLink from './Link'; import MarkdownLink from './Link';
@ -23,43 +20,39 @@ import mergeTextNodes from './mergeTextNodes';
import styles from './styles'; import styles from './styles';
import { isValidURL } from '../../utils/url'; import { isValidURL } from '../../utils/url';
import NewMarkdown from './new'; import NewMarkdown from './new';
import { formatText } from './formatText';
import { IUserMention, IUserChannel, TOnLinkPress } from './interfaces';
import { TGetCustomEmoji } from '../../definitions/IEmoji';
import { formatHyperlink } from './formatHyperlink';
export { default as MarkdownPreview } from './Preview';
interface IMarkdownProps { interface IMarkdownProps {
msg: string; msg?: string;
md: MarkdownAST;
mentions: UserMention[];
getCustomEmoji: Function;
baseUrl: string;
username: string;
tmid: string;
isEdited: boolean;
numberOfLines: number;
customEmojis: boolean;
useRealName: boolean;
channels: {
name: string;
_id: number;
}[];
enableMessageParser: boolean;
navToRoomInfo: Function;
preview: boolean;
theme: string; theme: string;
testID: string; md?: MarkdownAST;
style: any; mentions?: IUserMention[];
onLinkPress: Function; getCustomEmoji?: TGetCustomEmoji;
baseUrl?: string;
username?: string;
tmid?: string;
isEdited?: boolean;
numberOfLines?: number;
customEmojis?: boolean;
useRealName?: boolean;
channels?: IUserChannel[];
enableMessageParser?: boolean;
// TODO: Refactor when migrate Room
navToRoomInfo?: Function;
testID?: string;
style?: StyleProp<TextStyle>[];
onLinkPress?: TOnLinkPress;
} }
type TLiteral = { type TLiteral = {
literal: string; literal: string;
}; };
// Support <http://link|Text>
const formatText = (text: string) =>
text.replace(
new RegExp('(?:<|<)((?:https|http):\\/\\/[^\\|]+)\\|(.+?)(?=>|>)(?:>|>)', 'gm'),
(match, url, title) => `[${title}](${url})`
);
const emojiRanges = [ const emojiRanges = [
'\u00a9|\u00ae|[\u2000-\u3300]|\ud83c[\ud000-\udfff]|\ud83d[\ud000-\udfff]|\ud83e[\ud000-\udfff]', // unicode emoji from https://www.regextester.com/106421 '\u00a9|\u00ae|[\u2000-\u3300]|\ud83c[\ud000-\udfff]|\ud83d[\ud000-\udfff]|\ud83e[\ud000-\udfff]', // unicode emoji from https://www.regextester.com/106421
':.{1,40}:', // custom emoji ':.{1,40}:', // custom emoji
@ -148,7 +141,7 @@ class Markdown extends PureComponent<IMarkdownProps, any> {
get isNewMarkdown(): boolean { get isNewMarkdown(): boolean {
const { md, enableMessageParser } = this.props; const { md, enableMessageParser } = this.props;
return enableMessageParser && !!md; return !!enableMessageParser && !!md;
} }
editedMessage = (ast: any) => { editedMessage = (ast: any) => {
@ -244,7 +237,7 @@ class Markdown extends PureComponent<IMarkdownProps, any> {
}; };
renderAtMention = ({ mentionName }: { mentionName: string }) => { renderAtMention = ({ mentionName }: { mentionName: string }) => {
const { username, mentions, navToRoomInfo, useRealName, style } = this.props; const { username = '', mentions, navToRoomInfo, useRealName, style } = this.props;
return ( return (
<MarkdownAtMention <MarkdownAtMention
mentions={mentions} mentions={mentions}
@ -258,7 +251,7 @@ class Markdown extends PureComponent<IMarkdownProps, any> {
}; };
renderEmoji = ({ literal }: TLiteral) => { renderEmoji = ({ literal }: TLiteral) => {
const { getCustomEmoji, baseUrl, customEmojis, style, theme } = this.props; const { getCustomEmoji, baseUrl = '', customEmojis, style, theme } = this.props;
return ( return (
<MarkdownEmoji <MarkdownEmoji
literal={literal} literal={literal}
@ -343,18 +336,13 @@ class Markdown extends PureComponent<IMarkdownProps, any> {
const { const {
msg, msg,
md, md,
numberOfLines,
preview = false,
theme,
style = [],
testID,
mentions, mentions,
channels, channels,
navToRoomInfo, navToRoomInfo,
useRealName, useRealName,
username, username = '',
getCustomEmoji, getCustomEmoji,
baseUrl, baseUrl = '',
onLinkPress onLinkPress
} = this.props; } = this.props;
@ -362,7 +350,7 @@ class Markdown extends PureComponent<IMarkdownProps, any> {
return null; return null;
} }
if (this.isNewMarkdown && !preview) { if (this.isNewMarkdown) {
return ( return (
<NewMarkdown <NewMarkdown
username={username} username={username}
@ -379,28 +367,7 @@ class Markdown extends PureComponent<IMarkdownProps, any> {
} }
let m = formatText(msg); let m = formatText(msg);
m = formatHyperlink(m);
// Ex: '[ ](https://open.rocket.chat/group/test?msg=abcdef) Test'
// Return: 'Test'
m = m.replace(/^\[([\s]*)\]\(([^)]*)\)\s/, '').trim();
if (preview) {
m = shortnameToUnicode(m);
// Removes sequential empty spaces
m = m.replace(/\s+/g, ' ');
m = removeMarkdown(m);
m = m.replace(/\n+/g, ' ');
return (
<Text
accessibilityLabel={m}
style={[styles.text, { color: themes[theme].bodyText }, ...style]}
numberOfLines={numberOfLines}
testID={testID}>
{m}
</Text>
);
}
let ast = parser.parse(m); let ast = parser.parse(m);
ast = mergeTextNodes(ast); ast = mergeTextNodes(ast);
this.isMessageContainsOnlyEmoji = isOnlyEmoji(m) && emojiCount(m) <= 3; this.isMessageContainsOnlyEmoji = isOnlyEmoji(m) && emojiCount(m) <= 3;

View File

@ -0,0 +1,12 @@
export interface IUserMention {
_id: string;
username: string;
name?: string;
}
export interface IUserChannel {
name: string;
_id: string;
}
export type TOnLinkPress = (link: string) => void;

View File

@ -1,17 +1,14 @@
import React from 'react'; import React from 'react';
import { UserMention } from '../../message/interfaces'; import { IUserMention, IUserChannel } from '../interfaces';
interface IMarkdownContext { interface IMarkdownContext {
mentions: UserMention[]; mentions?: IUserMention[];
channels: { channels?: IUserChannel[];
name: string; useRealName?: boolean;
_id: number; username?: string;
}[]; baseUrl?: string;
useRealName: boolean; navToRoomInfo?: Function;
username: string;
baseUrl: string;
navToRoomInfo: Function;
getCustomEmoji?: Function; getCustomEmoji?: Function;
onLinkPress?: Function; onLinkPress?: Function;
} }

View File

@ -1,5 +1,6 @@
import React from 'react'; import React from 'react';
import { MarkdownAST } from '@rocket.chat/message-parser'; import { MarkdownAST } from '@rocket.chat/message-parser';
import isEmpty from 'lodash/isEmpty';
import Quote from './Quote'; import Quote from './Quote';
import Paragraph from './Paragraph'; import Paragraph from './Paragraph';
@ -8,21 +9,18 @@ import Code from './Code';
import BigEmoji from './BigEmoji'; import BigEmoji from './BigEmoji';
import OrderedList from './OrderedList'; import OrderedList from './OrderedList';
import UnorderedList from './UnorderedList'; import UnorderedList from './UnorderedList';
import { UserMention } from '../../message/interfaces'; import { IUserMention, IUserChannel, TOnLinkPress } from '../interfaces';
import TaskList from './TaskList'; import TaskList from './TaskList';
import MarkdownContext from './MarkdownContext'; import MarkdownContext from './MarkdownContext';
interface IBodyProps { interface IBodyProps {
tokens: MarkdownAST; tokens?: MarkdownAST;
mentions: UserMention[]; mentions?: IUserMention[];
channels: { channels?: IUserChannel[];
name: string;
_id: number;
}[];
getCustomEmoji?: Function; getCustomEmoji?: Function;
onLinkPress?: Function; onLinkPress?: TOnLinkPress;
navToRoomInfo: Function; navToRoomInfo?: Function;
useRealName: boolean; useRealName?: boolean;
username: string; username: string;
baseUrl: string; baseUrl: string;
} }
@ -37,41 +35,47 @@ const Body = ({
getCustomEmoji, getCustomEmoji,
baseUrl, baseUrl,
onLinkPress onLinkPress
}: IBodyProps): JSX.Element => ( }: IBodyProps): React.ReactElement | null => {
<MarkdownContext.Provider if (isEmpty(tokens)) {
value={{ return null;
mentions, }
channels,
useRealName, return (
username, <MarkdownContext.Provider
navToRoomInfo, value={{
getCustomEmoji, mentions,
baseUrl, channels,
onLinkPress useRealName,
}}> username,
{tokens.map(block => { navToRoomInfo,
switch (block.type) { getCustomEmoji,
case 'BIG_EMOJI': baseUrl,
return <BigEmoji value={block.value} />; onLinkPress
case 'UNORDERED_LIST': }}>
return <UnorderedList value={block.value} />; {tokens?.map(block => {
case 'ORDERED_LIST': switch (block.type) {
return <OrderedList value={block.value} />; case 'BIG_EMOJI':
case 'TASKS': return <BigEmoji value={block.value} />;
return <TaskList value={block.value} />; case 'UNORDERED_LIST':
case 'QUOTE': return <UnorderedList value={block.value} />;
return <Quote value={block.value} />; case 'ORDERED_LIST':
case 'PARAGRAPH': return <OrderedList value={block.value} />;
return <Paragraph value={block.value} />; case 'TASKS':
case 'CODE': return <TaskList value={block.value} />;
return <Code value={block.value} />; case 'QUOTE':
case 'HEADING': return <Quote value={block.value} />;
return <Heading value={block.value} level={block.level} />; case 'PARAGRAPH':
default: return <Paragraph value={block.value} />;
return null; case 'CODE':
} return <Code value={block.value} />;
})} case 'HEADING':
</MarkdownContext.Provider> return <Heading value={block.value} level={block.level} />;
); default:
return null;
}
})}
</MarkdownContext.Provider>
);
};
export default Body; export default Body;

View File

@ -15,6 +15,7 @@ import { isAndroid, isIOS } from '../../utils/deviceInfo';
import MessageContext from './Context'; import MessageContext from './Context';
import ActivityIndicator from '../ActivityIndicator'; import ActivityIndicator from '../ActivityIndicator';
import { withDimensions } from '../../dimensions'; import { withDimensions } from '../../dimensions';
import { TGetCustomEmoji } from '../../definitions/IEmoji';
interface IButton { interface IButton {
loading: boolean; loading: boolean;
@ -29,7 +30,7 @@ interface IMessageAudioProps {
description: string; description: string;
}; };
theme: string; theme: string;
getCustomEmoji: Function; getCustomEmoji: TGetCustomEmoji;
scale?: number; scale?: number;
} }
@ -280,7 +281,6 @@ class MessageAudio extends React.Component<IMessageAudioProps, IMessageAudioStat
/> />
<Text style={[styles.duration, { color: themes[theme].auxiliaryText }]}>{this.duration}</Text> <Text style={[styles.duration, { color: themes[theme].auxiliaryText }]}>{this.duration}</Text>
</View> </View>
{/* @ts-ignore*/}
<Markdown msg={description} baseUrl={baseUrl} username={user.username} getCustomEmoji={getCustomEmoji} theme={theme} /> <Markdown msg={description} baseUrl={baseUrl} username={user.username} getCustomEmoji={getCustomEmoji} theme={theme} />
</> </>
); );

View File

@ -4,7 +4,7 @@ import { dequal } from 'dequal';
import I18n from '../../i18n'; import I18n from '../../i18n';
import styles from './styles'; import styles from './styles';
import Markdown from '../markdown'; import Markdown, { MarkdownPreview } from '../markdown';
import User from './User'; import User from './User';
import { SYSTEM_MESSAGE_TYPES_WITH_AUTHOR_NAME, getInfoMessage } from './utils'; import { SYSTEM_MESSAGE_TYPES_WITH_AUTHOR_NAME, getInfoMessage } from './utils';
import { themes } from '../../constants/colors'; import { themes } from '../../constants/colors';
@ -49,10 +49,11 @@ const Content = React.memo(
{I18n.t('Encrypted_message')} {I18n.t('Encrypted_message')}
</Text> </Text>
); );
} else if (isPreview) {
content = <MarkdownPreview msg={props.msg} />;
} else { } else {
const { baseUrl, user, onLinkPress } = useContext(MessageContext); const { baseUrl, user, onLinkPress } = useContext(MessageContext);
content = ( content = (
// @ts-ignore
<Markdown <Markdown
msg={props.msg} msg={props.msg}
md={props.md} md={props.md}
@ -61,8 +62,6 @@ const Content = React.memo(
enableMessageParser={user.enableMessageParserEarlyAdoption} enableMessageParser={user.enableMessageParserEarlyAdoption}
username={user.username} username={user.username}
isEdited={props.isEdited} isEdited={props.isEdited}
numberOfLines={isPreview ? 1 : 0}
preview={isPreview}
channels={props.channels} channels={props.channels}
mentions={props.mentions} mentions={props.mentions}
navToRoomInfo={props.navToRoomInfo} navToRoomInfo={props.navToRoomInfo}

View File

@ -11,6 +11,7 @@ import styles from './styles';
import { formatAttachmentUrl } from '../../lib/utils'; import { formatAttachmentUrl } from '../../lib/utils';
import { themes } from '../../constants/colors'; import { themes } from '../../constants/colors';
import MessageContext from './Context'; import MessageContext from './Context';
import { TGetCustomEmoji } from '../../definitions/IEmoji';
type TMessageButton = { type TMessageButton = {
children: JSX.Element; children: JSX.Element;
@ -28,7 +29,7 @@ interface IMessageImage {
imageUrl?: string; imageUrl?: string;
showAttachment: Function; showAttachment: Function;
theme: string; theme: string;
getCustomEmoji: Function; getCustomEmoji: TGetCustomEmoji;
} }
const ImageProgress = createImageProgress(FastImage); const ImageProgress = createImageProgress(FastImage);
@ -66,7 +67,6 @@ const ImageContainer = React.memo(
<Button theme={theme} onPress={onPress}> <Button theme={theme} onPress={onPress}>
<View> <View>
<MessageImage img={img} theme={theme} /> <MessageImage img={img} theme={theme} />
{/* @ts-ignore */}
<Markdown <Markdown
msg={file.description} msg={file.description}
baseUrl={baseUrl} baseUrl={baseUrl}

View File

@ -107,7 +107,7 @@ const MessageTouchable = React.memo((props: IMessageTouchable & IMessage) => {
<Touchable <Touchable
onLongPress={onLongPress} onLongPress={onLongPress}
onPress={onPress} onPress={onPress}
disabled={(props.isInfo && !props.isThreadReply) || props.archived || props.isTemp} disabled={(props.isInfo && !props.isThreadReply) || props.archived || props.isTemp || props.type === 'jitsi_call_started'}
style={{ backgroundColor: props.highlighted ? themes[props.theme].headerBackground : null }}> style={{ backgroundColor: props.highlighted ? themes[props.theme].headerBackground : null }}>
<View> <View>
<Message {...props} /> <Message {...props} />

View File

@ -9,6 +9,7 @@ import { BUTTON_HIT_SLOP } from './utils';
import { themes } from '../../constants/colors'; import { themes } from '../../constants/colors';
import { withTheme } from '../../theme'; import { withTheme } from '../../theme';
import MessageContext from './Context'; import MessageContext from './Context';
import { TGetCustomEmoji } from '../../definitions/IEmoji';
interface IMessageAddReaction { interface IMessageAddReaction {
theme: string; theme: string;
@ -19,13 +20,13 @@ interface IMessageReaction {
usernames: []; usernames: [];
emoji: object; emoji: object;
}; };
getCustomEmoji: Function; getCustomEmoji: TGetCustomEmoji;
theme: string; theme: string;
} }
interface IMessageReactions { interface IMessageReactions {
reactions?: object[]; reactions?: object[];
getCustomEmoji: Function; getCustomEmoji: TGetCustomEmoji;
theme: string; theme: string;
} }

View File

@ -5,7 +5,7 @@ import { CustomIcon } from '../../lib/Icons';
import styles from './styles'; import styles from './styles';
import { themes } from '../../constants/colors'; import { themes } from '../../constants/colors';
import I18n from '../../i18n'; import I18n from '../../i18n';
import Markdown from '../markdown'; import { MarkdownPreview } from '../markdown';
import { IMessageRepliedThread } from './interfaces'; import { IMessageRepliedThread } from './interfaces';
const RepliedThread = memo(({ tmid, tmsg, isHeader, fetchThreadName, id, isEncrypted, theme }: IMessageRepliedThread) => { const RepliedThread = memo(({ tmid, tmsg, isHeader, fetchThreadName, id, isEncrypted, theme }: IMessageRepliedThread) => {
@ -32,14 +32,7 @@ const RepliedThread = memo(({ tmid, tmsg, isHeader, fetchThreadName, id, isEncry
return ( return (
<View style={styles.repliedThread} testID={`message-thread-replied-on-${msg}`}> <View style={styles.repliedThread} testID={`message-thread-replied-on-${msg}`}>
<CustomIcon name='threads' size={20} style={styles.repliedThreadIcon} color={themes[theme].tintColor} /> <CustomIcon name='threads' size={20} style={styles.repliedThreadIcon} color={themes[theme].tintColor} />
{/* @ts-ignore*/} <MarkdownPreview msg={msg} style={[styles.repliedThreadName, { color: themes[theme].tintColor }]} />
<Markdown
msg={msg}
theme={theme}
style={[styles.repliedThreadName, { color: themes[theme].tintColor }]}
preview
numberOfLines={1}
/>
<View style={styles.repliedThreadDisclosure}> <View style={styles.repliedThreadDisclosure}>
<CustomIcon name='chevron-right' color={themes[theme].auxiliaryText} size={20} /> <CustomIcon name='chevron-right' color={themes[theme].auxiliaryText} size={20} />
</View> </View>

View File

@ -14,6 +14,7 @@ import MessageContext from './Context';
import { fileDownloadAndPreview } from '../../utils/fileDownload'; import { fileDownloadAndPreview } from '../../utils/fileDownload';
import { formatAttachmentUrl } from '../../lib/utils'; import { formatAttachmentUrl } from '../../lib/utils';
import { IAttachment } from '../../definitions/IAttachment'; import { IAttachment } from '../../definitions/IAttachment';
import { TGetCustomEmoji } from '../../definitions/IEmoji';
import RCActivityIndicator from '../ActivityIndicator'; import RCActivityIndicator from '../ActivityIndicator';
const styles = StyleSheet.create({ const styles = StyleSheet.create({
@ -99,14 +100,14 @@ interface IMessageTitle {
interface IMessageDescription { interface IMessageDescription {
attachment: IAttachment; attachment: IAttachment;
getCustomEmoji: Function; getCustomEmoji: TGetCustomEmoji;
theme: string; theme: string;
} }
interface IMessageFields { interface IMessageFields {
attachment: IAttachment; attachment: IAttachment;
theme: string; theme: string;
getCustomEmoji: Function; getCustomEmoji: TGetCustomEmoji;
} }
interface IMessageReply { interface IMessageReply {
@ -114,7 +115,7 @@ interface IMessageReply {
timeFormat: string; timeFormat: string;
index: number; index: number;
theme: string; theme: string;
getCustomEmoji: Function; getCustomEmoji: TGetCustomEmoji;
} }
const Title = React.memo(({ attachment, timeFormat, theme }: IMessageTitle) => { const Title = React.memo(({ attachment, timeFormat, theme }: IMessageTitle) => {
@ -137,10 +138,7 @@ const Description = React.memo(
return null; return null;
} }
const { baseUrl, user } = useContext(MessageContext); const { baseUrl, user } = useContext(MessageContext);
return ( return <Markdown msg={text} baseUrl={baseUrl} username={user.username} getCustomEmoji={getCustomEmoji} theme={theme} />;
// @ts-ignore
<Markdown msg={text} baseUrl={baseUrl} username={user.username} getCustomEmoji={getCustomEmoji} theme={theme} />
);
}, },
(prevProps, nextProps) => { (prevProps, nextProps) => {
if (prevProps.attachment.text !== nextProps.attachment.text) { if (prevProps.attachment.text !== nextProps.attachment.text) {
@ -180,9 +178,8 @@ const Fields = React.memo(
{attachment.fields.map(field => ( {attachment.fields.map(field => (
<View key={field.title} style={[styles.fieldContainer, { width: field.short ? '50%' : '100%' }]}> <View key={field.title} style={[styles.fieldContainer, { width: field.short ? '50%' : '100%' }]}>
<Text style={[styles.fieldTitle, { color: themes[theme].bodyText }]}>{field.title}</Text> <Text style={[styles.fieldTitle, { color: themes[theme].bodyText }]}>{field.title}</Text>
{/* @ts-ignore*/}
<Markdown <Markdown
msg={field.value!} msg={field?.value || ''}
baseUrl={baseUrl} baseUrl={baseUrl}
username={user.username} username={user.username}
getCustomEmoji={getCustomEmoji} getCustomEmoji={getCustomEmoji}
@ -214,7 +211,7 @@ const Reply = React.memo(
if (!url) { if (!url) {
return; return;
} }
if (attachment.type === 'file') { if (attachment.type === 'file' && attachment.title_link) {
setLoading(true); setLoading(true);
url = formatAttachmentUrl(attachment.title_link, user.id, user.token, baseUrl); url = formatAttachmentUrl(attachment.title_link, user.id, user.token, baseUrl);
await fileDownloadAndPreview(url, attachment); await fileDownloadAndPreview(url, attachment);
@ -266,9 +263,8 @@ const Reply = React.memo(
) : null} ) : null}
</View> </View>
</Touchable> </Touchable>
{/* @ts-ignore*/}
<Markdown <Markdown
msg={attachment.description!} msg={attachment.description}
baseUrl={baseUrl} baseUrl={baseUrl}
username={user.username} username={user.username}
getCustomEmoji={getCustomEmoji} getCustomEmoji={getCustomEmoji}

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