Compare commits

..

54 Commits

Author SHA1 Message Date
Gleidson Daniel Silva 4dfc9c70f3
[FIX] Keyboard not showing all emojis or showing cut emojis (#4919)
* fix the logic based on window width and make the keyboard dynamic

* fix var naming and math

* fix alignment of emojis

* add comment

* wip
2023-03-09 17:17:20 -03:00
Piyush Gupta 513e8ee636
[FIX] Increase border radius on MessageAvatar as small (#4837)
* fixed border radius of 4 on avatar

* fixed lint

* Update tests

---------

Co-authored-by: Diego Mello <diegolmello@gmail.com>
2023-03-08 15:34:39 -03:00
Mister-H aa57237004
[FIX] Order of quoted message done according to desktop version (#4739)
Co-authored-by: Diego Mello <diegolmello@gmail.com>
2023-03-08 11:20:01 -03:00
Diego Mello 58391964cb
[FIX] Message not rendering E2EE data if md exists (#4951) 2023-03-07 10:57:57 -03:00
Diego Mello 412f62eb2a
Chore: Update Detox to 20.1.2 (#4866) 2023-03-07 09:28:51 -03:00
Reinaldo Neto 4526b7f871
[FIX] UGC Rules Text align (#4934) 2023-03-02 16:04:52 -03:00
Diego Mello 27efa89dac
Bump version to 4.37.0 (#4938) 2023-03-02 13:22:40 -03:00
Gleidson Daniel Silva 3fbb7b5720
[IMPROVE] Brings the operation of the video call closer to the web (#4883)
* rename CallAgainActionSheet to StartACallActionSheet

* remove useVideoConf and use videoConfJoin directly

* consider phone on calls

* fix text shrink

* fix mic audio

* change the behavior of call icon on header and RoomInfo

* update types

* update types and variables names

* revert old type

* fix issue on old servers

* rename to a correct naming

* fix translation

* revamp call icon

* add error handling to videoconf capabilities

* lint

* fix role logic

* change const name

* rename comp

* remove commented code

* fix types and apply correct logic

* fix naming

* correct the import

* update icon size

* create timer function for videoConf bellow 5.0

* add subscription to useVideoConf hook

---------

Co-authored-by: Diego Mello <diegolmello@gmail.com>
2023-03-01 15:26:56 -03:00
Reinaldo Neto 4d5ff8aba1
[FIX] Omnichannel's icon svg fallback when the link returns an error (#4916)
* [FIX] Omnichannel's icon svg fallback when the link returns an error

* add activity indicator

* switch between activity indicator to custom icon

* reuse the same component as const
2023-02-27 17:31:28 -03:00
Reinaldo Neto 4336f0db40
[FIX] "Allow Reaction" does not work properly on "Read Only" rooms (#4864)
* [FIX] "Allow Reaction" does not work properly on "Read Only" rooms

* fix the handle of message box

---------

Co-authored-by: Gleidson Daniel Silva <gleidson10daniel@hotmail.com>
2023-02-24 11:37:56 -03:00
Gleidson Daniel Silva 4686d4f6f8
[FIX] Text color and fit content of TaskList (#4906)
* fix regression on plain text

* update stories

* fix where to add the color

* fit content taskList

---------

Co-authored-by: Reinaldo Neto <reinaldonetof@hotmail.com>
2023-02-16 10:07:19 -03:00
Gleidson Daniel Silva 58f28fb488
[FIX] Remove some permissions and keep only basic Bluetooth permission on Android (#4912) 2023-02-15 14:19:44 -03:00
lingohub[bot] 6bb4adaecd
Language update from LingoHub 🤖 on 2023-02-14Z (#4911)
* Language update from LingoHub 🤖

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

Easy language translations with LingoHub 🚀

* Language update from LingoHub 🤖

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

Easy language translations with LingoHub 🚀

---------

Co-authored-by: Diego Mello <diego.mello@rocket.chat>
2023-02-14 18:00:41 -03:00
Diego Mello 197616b94c
Chore: Remove deprecated typing stream (#4888) 2023-02-14 15:49:16 -03:00
Diego Mello edd0333be1
Chore: Update React Native to 0.68.6 (#4881) 2023-02-14 12:06:37 -03:00
Diego Mello 8c47187f70
[NEW] Presence Cap (#4900) 2023-02-14 10:47:56 -03:00
Gleidson Daniel Silva c4a2ce20c6
Regression: Adds idempotent prop to delete audio (#4862) 2023-02-06 16:07:23 -03:00
Reinaldo Neto 68f6eb40de
Chore: Hooks app/views/InviteUsersEditView (#4670)
* Chore: Hooks app/views/InviteUsersEditView

* minor tweak

* switch value name
2023-02-02 18:41:32 -03:00
Diego Mello 59c76d3956
Bump version to 4.36.0 (#4884) 2023-02-02 16:51:16 -03:00
Reinaldo Neto 9396b08ead
[FIX] Quote rendering with leading empty space on mobile only (#4778)
* [FIX] Quote rendering with leading empty space on mobile only

* update the message storyshot

* compareServerVersion to connection string

* fix time

* fix lint

* update to servers greater or equal than 6.0

* refactor tests

* minor tweak

---------

Co-authored-by: GleidsonDaniel <gleidson10daniel@hotmail.com>
2023-02-02 00:17:09 -03:00
Reinaldo Neto a927746d7f
[FIX] Show read receipts when it isn't read yet (#4865)
* [FIX] Show read receipts when it isn't read yet

* minor tweak
2023-02-02 00:10:27 -03:00
Reinaldo Neto 21e4818af7
[FIX] Team icons on Share View and searching on RoomsListView (#4833)
* [FIX] Team icons on Share View

* fix the teams when searching
2023-02-02 00:04:17 -03:00
Diego Mello 4de7c83e80
Bump version to 4.35.1 (#4871) 2023-01-30 15:10:09 -03:00
Gleidson Daniel Silva 18cc16beb7
Regression: Add token to Jitsi call on iOS (#4863)
* add token to jitsi builder

* fix room

* fix room and remove flags

* fix video prop
2023-01-30 13:56:39 -03:00
Reinaldo Neto 3cd1a5f0a6
[FIX] Add users shouldn't have Skip option (#4828) 2023-01-25 15:28:03 -03:00
lingohub[bot] 1abff18e75
Language update from LingoHub 🤖 (#4859)
Project Name: Rocket.Chat.ReactNative
Project Link: https://translate.lingohub.com/rocketchat/dashboard/rocket-dot-chat-dot-reactnative
User: Diego Mello

Easy language translations with LingoHub 🚀

Co-authored-by: Diego Mello <diego.mello@rocket.chat>
2023-01-24 16:01:29 -03:00
Diego Mello 26c8931563
Bump version to 4.36.0 (#4854) 2023-01-24 13:25:13 -03:00
Gleidson Daniel Silva 192a6b055a
Regression: Fix RoomItem's loading status (#4835) 2023-01-24 10:03:48 -03:00
Diego Mello 7363d9d742
[FIX] Official CI build (#4850) 2023-01-24 09:50:17 -03:00
Umang Patel 21f64b08e5
Docs: Fix Rocket.Chat server guide URL (#4848) 2023-01-23 13:50:30 -03:00
Gleidson Daniel Silva 38511d2f2d
[IMPROVE] Migrate from react-native-jitsi-meet to lib react-native-jitsimeet-custom (#4823)
Co-authored-by: Diego Mello <diegolmello@gmail.com>
2023-01-20 13:55:53 -03:00
Reinaldo Neto 593d12b129
[FIX] Add User Generated Content link on login (#4827)
* [FIX] Add User Generated Content link on login

* minor tweak
2023-01-20 11:42:18 -03:00
Diego Mello ad3bfa830c
Chore: Rotate CI secrets (#4797) 2023-01-18 19:15:22 -03:00
Diego Mello af70849b5e
[FIX] Point to correct RN fork commit (#4832) 2023-01-18 14:55:54 -03:00
Diego Mello 91831c8f33
[FIX] Unwanted clear keychain and request challenges (#4826) 2023-01-18 13:43:26 -03:00
Diego Mello 447a2b9344
Regression: App not fetching messages properly after #4801 (#4819) 2023-01-16 13:43:27 -03:00
Gleidson Daniel Silva fbb05fa0d3
Regression: Add bluetooth permissions for Jitsi (#4796)
* add blt permissions

* Update AndroidManifest.xml

* add blt permissions

* remove wrong permission
2023-01-16 10:17:52 -03:00
Gleidson Daniel Silva 41b54d6d87
[FIX] Audio names not being handled properly (#4685) 2023-01-14 07:07:25 -03:00
Diego Mello 5387d31a68
[FIX] Messages not loading on some edge cases (#4801) 2023-01-13 16:32:52 -03:00
Diego Mello a1580811ed
[IMPROVE] E2EE improvements (#4763) 2023-01-12 10:32:33 -03:00
Reinaldo Neto 3a1b06b86c
[FIX] Not Login with SSO after upgrade to version 5.4.0 and 5.4.1 (#4783)
* [FIX] Not Login with SSO after upgrade to version 5.4.0 and 5.4.1

* minor tweak
2023-01-10 20:17:14 -03:00
Reinaldo Neto 9525ef25d8
[FIX] Modal freezing app after return from the background (#4795) 2023-01-09 15:32:16 -03:00
Gleidson Daniel Silva d208373b3a
Regression: Prevent screen from sleeping on Jitsi on Android (#4791) 2023-01-09 10:29:58 -03:00
Reinaldo Neto 24ad69346a
[FIX] Opening search field after server list does not hide server list (#4792)
* [FIX] Opening search field after server list does not hide server list

* minor tweak
2023-01-06 12:36:23 -03:00
Reinaldo Neto 8102bef1d0
[FIX] Quote message and reply with image (#4715)
* send msg with attachment

* send quote inside image

* minor tweak

* remove msg from return

* fix the lint and prettier

* fix visual bug for iOS

* fixing the message box input
2023-01-05 15:23:11 -03:00
Reinaldo Neto b6b5e7294f
[FIX] Member list filters for All by default (#4757)
Co-authored-by: Gleidson Daniel Silva <gleidson10daniel@hotmail.com>
2023-01-04 11:36:51 -03:00
Diego Mello 73557038a2
[FIX] Flipper on Android (#4771) 2022-12-21 17:35:45 -03:00
Diego Mello 4174aef4f8
Chore: Upgrade react-native-device-info to 10.3.0 (#4770) 2022-12-21 15:47:42 -03:00
dependabot[bot] 67f41a712c
Chore: Bump loader-utils from 1.4.1 to 1.4.2 (#4687)
Bumps [loader-utils](https://github.com/webpack/loader-utils) from 1.4.1 to 1.4.2.
- [Release notes](https://github.com/webpack/loader-utils/releases)
- [Changelog](https://github.com/webpack/loader-utils/blob/v1.4.2/CHANGELOG.md)
- [Commits](https://github.com/webpack/loader-utils/compare/v1.4.1...v1.4.2)

---
updated-dependencies:
- dependency-name: loader-utils
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Diego Mello <diegolmello@gmail.com>
2022-12-21 14:39:09 -03:00
dependabot[bot] c13e0af110
Chore: Bump underscore from 1.10.2 to 1.13.6 (#4764)
Bumps [underscore](https://github.com/jashkenas/underscore) from 1.10.2 to 1.13.6.
- [Release notes](https://github.com/jashkenas/underscore/releases)
- [Commits](https://github.com/jashkenas/underscore/compare/1.10.2...1.13.6)

---
updated-dependencies:
- dependency-name: underscore
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Diego Mello <diegolmello@gmail.com>
2022-12-21 14:38:36 -03:00
dependabot[bot] d10a77ba3d
Bump minimist from 1.2.5 to 1.2.7 (#4616)
Bumps [minimist](https://github.com/minimistjs/minimist) from 1.2.5 to 1.2.7.
- [Release notes](https://github.com/minimistjs/minimist/releases)
- [Changelog](https://github.com/minimistjs/minimist/blob/main/CHANGELOG.md)
- [Commits](https://github.com/minimistjs/minimist/compare/v1.2.5...v1.2.7)

---
updated-dependencies:
- dependency-name: minimist
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Diego Mello <diegolmello@gmail.com>
2022-12-21 14:38:18 -03:00
Reinaldo Neto 68066d487f
[FIX] Try again to resend a file on a thread (#4756) 2022-12-21 14:34:48 -03:00
Reinaldo Neto 0b5dab3ddf
[FIX] Notification preferences menu shows nothing (#4703) 2022-12-21 13:15:30 -03:00
Reinaldo Neto 23f716c280
[FIX] Disappear push notifications when open the app (#4718)
* [FIX] Disappear push notifications when open the app

* minor tweak

* test badgeCount

* refactor remove notification to remove notification and badge
2022-12-21 13:07:17 -03:00
258 changed files with 5763 additions and 8350 deletions

View File

@ -1,9 +1,12 @@
defaults: &defaults
working_directory: ~/repo
orbs:
android: circleci/android@2.1.2
macos: &macos
macos:
xcode: "13.3.0"
xcode: "14.2.0"
resource_class: large
bash-env: &bash-env
@ -51,14 +54,14 @@ save-gems-cache: &save-gems-cache
update-fastlane-ios: &update-fastlane-ios
name: Update Fastlane
command: |
echo "ruby-2.6.4" > ~/.ruby-version
echo "ruby-2.7.7" > ~/.ruby-version
bundle install
working_directory: ios
update-fastlane-android: &update-fastlane-android
name: Update Fastlane
command: |
echo "ruby-2.6.4" > ~/.ruby-version
echo "ruby-2.7.7" > ~/.ruby-version
bundle install
working_directory: android
@ -118,26 +121,26 @@ commands:
if [[ $CIRCLE_JOB == "android-build-official" ]]; then
echo -e "APPLICATION_ID=chat.rocket.android" >> ./gradle.properties
echo -e "BugsnagAPIKey=$BUGSNAG_KEY_OFFICIAL" >> ./gradle.properties
echo $CHAT_ROCKET_ANDROID_STORE_FILE_BASE64_JKS | base64 --decode > ./app/$KEYSTORE_OFFICIAL
echo $KEYSTORE_OFFICIAL_BASE64 | base64 --decode > ./app/$KEYSTORE_OFFICIAL
echo -e "KEYSTORE=$KEYSTORE_OFFICIAL" >> ./gradle.properties
echo -e "KEYSTORE_PASSWORD=$CHAT_ROCKET_ANDROID_STORE_PASSWORD" >> ./gradle.properties
echo -e "KEY_ALIAS=$CHAT_ROCKET_ANDROID_KEY_ALIAS" >> ./gradle.properties
echo -e "KEY_PASSWORD=$CHAT_ROCKET_ANDROID_KEY_PASSWORD" >> ./gradle.properties
echo -e "KEYSTORE_PASSWORD=$KEYSTORE_OFFICIAL_PASSWORD" >> ./gradle.properties
echo -e "KEY_ALIAS=$KEYSTORE_OFFICIAL_ALIAS" >> ./gradle.properties
echo -e "KEY_PASSWORD=$KEYSTORE_OFFICIAL_PASSWORD" >> ./gradle.properties
else
echo -e "APPLICATION_ID=chat.rocket.reactnative" >> ./gradle.properties
echo -e "BugsnagAPIKey=$BUGSNAG_KEY" >> ./gradle.properties
echo $KEYSTORE_BASE64 | base64 --decode > ./app/$KEYSTORE
echo -e "KEYSTORE=$KEYSTORE" >> ./gradle.properties
echo -e "KEYSTORE_PASSWORD=$KEYSTORE_PASSWORD" >> ./gradle.properties
echo -e "KEY_ALIAS=$KEY_ALIAS" >> ./gradle.properties
echo -e "KEY_PASSWORD=$KEYSTORE_PASSWORD" >> ./gradle.properties
echo $KEYSTORE_EXPERIMENTAL_BASE64 | base64 --decode > ./app/$KEYSTORE_EXPERIMENTAL
echo -e "KEYSTORE=$KEYSTORE_EXPERIMENTAL" >> ./gradle.properties
echo -e "KEYSTORE_PASSWORD=$KEYSTORE_EXPERIMENTAL_PASSWORD" >> ./gradle.properties
echo -e "KEY_ALIAS=$KEYSTORE_EXPERIMENTAL_ALIAS" >> ./gradle.properties
echo -e "KEY_PASSWORD=$KEYSTORE_EXPERIMENTAL_PASSWORD" >> ./gradle.properties
fi
working_directory: android
- run:
name: Set Google Services
command: |
if [[ $KEYSTORE ]]; then
if [[ $GOOGLE_SERVICES_ANDROID ]]; then
echo $GOOGLE_SERVICES_ANDROID | base64 --decode > google-services.json
fi
working_directory: android/app
@ -151,7 +154,7 @@ commands:
if [[ $CIRCLE_JOB == "android-build-experimental" || "android-automatic-build-experimental" ]]; then
./gradlew bundleExperimentalPlayRelease
fi
if [[ ! $KEYSTORE ]]; then
if [[ ! $GOOGLE_SERVICES_ANDROID ]]; then
./gradlew assembleExperimentalPlayDebug
fi
working_directory: android
@ -200,8 +203,12 @@ commands:
- run:
name: Set Google Services
command: |
if [[ $KEYSTORE ]]; then
echo $GOOGLE_SERVICES_IOS | base64 --decode > GoogleService-Info.plist
if [[ $APP_STORE_CONNECT_API_KEY_BASE64 ]]; then
if [[ $CIRCLE_JOB == "ios-build-official" ]]; then
echo $GOOGLE_SERVICES_IOS | base64 --decode > GoogleService-Info.plist
else
echo $GOOGLE_SERVICES_IOS_EXPERIMENTAL | base64 --decode > GoogleService-Info.plist
fi
fi
working_directory: ios
- run:
@ -223,12 +230,12 @@ commands:
/usr/libexec/PlistBuddy -c "Set IS_OFFICIAL NO" ./NotificationService/Info.plist
fi
if [[ $APP_STORE_CONNECT_API_BASE64 ]]; then
echo $APP_STORE_CONNECT_API_BASE64 | base64 --decode > ./fastlane/app_store_connect_api_key.p8
if [[ $APP_STORE_CONNECT_API_KEY_BASE64 ]]; then
echo $APP_STORE_CONNECT_API_KEY_BASE64 | base64 --decode > ./fastlane/app_store_connect_api_key.p8
if [[ $CIRCLE_JOB == "ios-build-official" ]]; then
bundle exec fastlane ios build_official
else
if [[ $KEYSTORE ]]; then
if [[ $APP_STORE_CONNECT_API_KEY_BASE64 ]]; then
bundle exec fastlane ios build_experimental
else
bundle exec fastlane ios build_fork
@ -318,11 +325,19 @@ commands:
- run:
name: Fastlane Tesflight Upload
command: |
echo $APP_STORE_CONNECT_API_BASE64 | base64 --decode > ./fastlane/app_store_connect_api_key.p8
echo $APP_STORE_CONNECT_API_KEY_BASE64 | base64 --decode > ./fastlane/app_store_connect_api_key.p8
bundle exec fastlane ios beta official:<< parameters.official >>
working_directory: ios
- save_cache: *save-gems-cache
create-e2e-account-file:
description: "Create e2e account file"
steps:
- run:
command: |
echo $E2E_ACCOUNT | base64 --decode > ./e2e_account.ts
working_directory: e2e
version: 2.1
# EXECUTORS
@ -434,6 +449,94 @@ jobs:
- upload-to-google-play-beta:
official: true
e2e-build-android:
<<: *defaults
executor:
name: android/android-machine
resource-class: xlarge
tag: 2022.12.1
environment:
<<: *android-env
steps:
- checkout
- restore_cache: *restore-npm-cache-linux
- run: *install-npm-modules
- save_cache: *save-npm-cache-linux
- restore_cache: *restore-gradle-cache
- run:
name: Configure Gradle
command: |
echo -e "" > ./gradle.properties
# echo -e "android.enableAapt2=false" >> ./gradle.properties
echo -e "android.useAndroidX=true" >> ./gradle.properties
echo -e "android.enableJetifier=true" >> ./gradle.properties
echo -e "newArchEnabled=false" >> ./gradle.properties
echo -e "FLIPPER_VERSION=0.125.0" >> ./gradle.properties
echo -e "VERSIONCODE=$CIRCLE_BUILD_NUM" >> ./gradle.properties
echo -e "APPLICATION_ID=chat.rocket.reactnative" >> ./gradle.properties
echo -e "BugsnagAPIKey=$BUGSNAG_KEY" >> ./gradle.properties
echo $KEYSTORE_EXPERIMENTAL_BASE64 | base64 --decode > ./app/$KEYSTORE_EXPERIMENTAL
echo -e "KEYSTORE=$KEYSTORE_EXPERIMENTAL" >> ./gradle.properties
echo -e "KEYSTORE_PASSWORD=$KEYSTORE_EXPERIMENTAL_PASSWORD" >> ./gradle.properties
echo -e "KEY_ALIAS=$KEYSTORE_EXPERIMENTAL_ALIAS" >> ./gradle.properties
echo -e "KEY_PASSWORD=$KEYSTORE_EXPERIMENTAL_PASSWORD" >> ./gradle.properties
working_directory: android
- run:
name: Build Android
command: |
echo "RUNNING_E2E_TESTS=true" > ./.env
yarn e2e:android-build
- save_cache: *save-gradle-cache
- store_artifacts:
path: android/app/build/outputs/apk/experimentalPlay/release/app-experimental-play-release.apk
- store_artifacts:
path: android/app/build/outputs/apk/androidTest/experimentalPlay/release/app-experimental-play-release-androidTest.apk
- persist_to_workspace:
root: /home/circleci/repo
paths:
- android/app/build/outputs/apk/
e2e-test-android:
<<: *defaults
executor:
name: android/android-machine
resource-class: xlarge
tag: 2022.12.1
parallelism: 4
steps:
- checkout
- attach_workspace:
at: /home/circleci/repo
- restore_cache: *restore-npm-cache-linux
- run: *install-npm-modules
- save_cache: *save-npm-cache-linux
- run: mkdir ~/junit
- create-e2e-account-file
- android/create-avd:
avd-name: Pixel_API_31_AOSP
install: true
system-image: system-images;android-31;default;x86_64
- run:
name: Setup emulator
command: |
echo "hw.lcd.density = 440" >> ~/.android/avd/Pixel_API_31_AOSP.avd/config.ini
echo "hw.lcd.height = 2280" >> ~/.android/avd/Pixel_API_31_AOSP.avd/config.ini
echo "hw.lcd.width = 1080" >> ~/.android/avd/Pixel_API_31_AOSP.avd/config.ini
- run:
name: Run Detox Tests
command: |
TEST=$(circleci tests glob "e2e/tests/**/*.ts" | circleci tests split --split-by=timings)
yarn e2e:android-test $TEST
- store_artifacts:
path: artifacts
- run:
command: cp junit.xml ~/junit/
when: always
- store_test_results:
path: ~/junit
- store_artifacts:
path: ~/junit
# iOS builds
ios-build-experimental:
executor: mac-env
@ -457,11 +560,89 @@ jobs:
- upload-to-testflight:
official: true
e2e-build-ios:
executor: mac-env
steps:
- checkout
- restore_cache: *restore-gems-cache
- restore_cache: *restore-npm-cache-mac
- run: *install-npm-modules
- run: *update-fastlane-ios
- save_cache: *save-npm-cache-mac
- save_cache: *save-gems-cache
- manage-pods
- run:
name: Configure Detox
command: |
brew tap wix/brew
brew install applesimutils
- run:
name: Build
command: |
/usr/libexec/PlistBuddy -c "Set :bugsnag:apiKey $BUGSNAG_KEY" ./ios/RocketChatRN/Info.plist
/usr/libexec/PlistBuddy -c "Set :bugsnag:apiKey $BUGSNAG_KEY" ./ios/ShareRocketChatRN/Info.plist
yarn detox clean-framework-cache && yarn detox build-framework-cache
echo "RUNNING_E2E_TESTS=true" > ./.env
yarn e2e:ios-build
- persist_to_workspace:
root: /Users/distiller/project
paths:
- ios/build/Build/Products/Release-iphonesimulator/Rocket.Chat Experimental.app
e2e-test-ios:
executor: mac-env
parallelism: 5
steps:
- checkout
- attach_workspace:
at: /Users/distiller/project
- restore_cache: *restore-npm-cache-mac
- run: *install-npm-modules
- save_cache: *save-npm-cache-mac
- run: mkdir ~/junit
- run:
name: Configure Detox
command: |
brew tap wix/brew
brew install applesimutils
yarn detox clean-framework-cache && yarn detox build-framework-cache
- create-e2e-account-file
- run:
name: Run tests
command: |
TEST=$(circleci tests glob "e2e/tests/**/*.ts" | circleci tests split --split-by=timings)
yarn e2e:ios-test $TEST
- store_artifacts:
path: artifacts
- run:
command: cp junit.xml ~/junit/
when: always
- store_test_results:
path: ~/junit
- store_artifacts:
path: ~/junit
workflows:
build-and-test:
jobs:
- lint-testunit
# E2E tests
- e2e-hold:
type: approval
- e2e-build-ios:
requires:
- e2e-hold
- e2e-test-ios:
requires:
- e2e-build-ios
- e2e-build-android:
requires:
- e2e-hold
- e2e-test-android:
requires:
- e2e-build-android
# iOS Experimental
- ios-hold-build-experimental:
type: approval

91
.detoxrc.js Normal file
View File

@ -0,0 +1,91 @@
/** @type {Detox.DetoxConfig} */
module.exports = {
testRunner: {
args: {
$0: 'jest',
config: 'e2e/jest.config.js'
},
retries: process.env.CI ? 3 : 0
},
artifacts: {
plugins: {
screenshot: 'failing',
video: 'failing',
uiHierarchy: 'enabled'
}
},
apps: {
'ios.debug': {
type: 'ios.app',
binaryPath: 'ios/build/Build/Products/Debug-iphonesimulator/Rocket.Chat Experimental.app',
build:
'xcodebuild -workspace ios/RocketChatRN.xcworkspace -scheme RocketChatRN -configuration Debug -sdk iphonesimulator -derivedDataPath ios/build'
},
'ios.release': {
type: 'ios.app',
binaryPath: 'ios/build/Build/Products/Release-iphonesimulator/Rocket.Chat Experimental.app',
build:
'xcodebuild -workspace ios/RocketChatRN.xcworkspace -scheme RocketChatRN -configuration Release -sdk iphonesimulator -derivedDataPath ios/build'
},
'android.debug': {
type: 'android.apk',
binaryPath: 'android/app/build/outputs/apk/experimentalPlay/debug/app-experimental-play-debug.apk',
build:
'cd android ; ./gradlew assembleExperimentalPlayDebug assembleExperimentalPlayDebugAndroidTest -DtestBuildType=debug ; cd -',
reversePorts: [8081]
},
'android.release': {
type: 'android.apk',
binaryPath: 'android/app/build/outputs/apk/experimentalPlay/release/app-experimental-play-release.apk',
build:
'cd android ; ./gradlew assembleExperimentalPlayRelease assembleExperimentalPlayReleaseAndroidTest -DtestBuildType=release ; cd -'
}
},
devices: {
simulator: {
type: 'ios.simulator',
device: {
type: 'iPhone 14'
}
},
attached: {
type: 'android.attached',
device: {
adbName: '.*'
}
},
emulator: {
type: 'android.emulator',
device: {
avdName: 'Pixel_API_31_AOSP'
},
headless: process.env.CI ? true : false
}
},
configurations: {
'ios.sim.debug': {
device: 'simulator',
app: 'ios.debug'
},
'ios.sim.release': {
device: 'simulator',
app: 'ios.release'
},
'android.att.debug': {
device: 'attached',
app: 'android.debug'
},
'android.att.release': {
device: 'attached',
app: 'android.release'
},
'android.emu.debug': {
device: 'emulator',
app: 'android.debug'
},
'android.emu.release': {
device: 'emulator',
app: 'android.release'
}
}
};

2
.env Normal file
View File

@ -0,0 +1,2 @@
# DON'T COMMIT THIS FILE
RUNNING_E2E_TESTS=

View File

@ -240,19 +240,8 @@ module.exports = {
},
{
files: ['e2e/**'],
globals: {
by: true,
detox: true,
device: true,
element: true,
waitFor: true
},
rules: {
'import/no-extraneous-dependencies': 0,
'no-await-in-loop': 0,
'no-restricted-syntax': 0,
// TODO: remove this rule when update Detox to 20 and test if the namespace Detox is available
'no-undef': 1
'no-await-in-loop': 0
}
}
]

1
.gitignore vendored
View File

@ -67,5 +67,6 @@ e2e/docker/rc_test_env/docker-compose.yml
e2e/docker/data/db
e2e/e2e_account.js
e2e/e2e_account.ts
junit.xml
*.p8

View File

@ -1 +1 @@
2.7.4
2.7.7

View File

@ -1,4 +1,4 @@
source 'https://rubygems.org'
# You may use http://rbenv.org/ or https://rvm.io/ to install and use this version
ruby '2.7.4'
ruby '2.7.7'
gem 'cocoapods', '~> 1.11', '>= 1.11.2'

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -147,7 +147,7 @@ android {
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
versionCode VERSIONCODE as Integer
versionName "4.35.0"
versionName "4.37.0"
vectorDrawables.useSupportLibrary = true
if (!isFoss) {
manifestPlaceholders = [BugsnagAPIKey: BugsnagAPIKey as String]
@ -250,6 +250,7 @@ android {
release {
minifyEnabled enableProguardInReleaseBuilds
setProguardFiles([getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'])
proguardFile "${rootProject.projectDir}/../node_modules/detox/android/detox/proguard-rules-app.pro"
signingConfig signingConfigs.release
if (!isFoss) {
firebaseCrashlytics {
@ -268,6 +269,11 @@ android {
// pickFirst '**/x86_64/libc++_shared.so'
// }
// FIXME: Remove when we update RN
packagingOptions {
pickFirst '**/*.so'
}
// applicationVariants are e.g. debug, release
flavorDimensions "app", "type"
@ -280,10 +286,6 @@ android {
dimension = "app"
buildConfigField "boolean", "IS_OFFICIAL", "false"
}
e2e {
dimension = "app"
buildConfigField "boolean", "IS_OFFICIAL", "false"
}
foss {
dimension = "type"
buildConfigField "boolean", "FDROID_BUILD", "true"
@ -311,16 +313,6 @@ android {
java.srcDirs = ['src/main/java', 'src/play/java']
manifest.srcFile 'src/play/AndroidManifest.xml'
}
e2ePlayDebug {
java.srcDirs = ['src/main/java', 'src/play/java']
res.srcDirs = ['src/experimental/res']
manifest.srcFile 'src/play/AndroidManifest.xml'
}
e2ePlayRelease {
java.srcDirs = ['src/main/java', 'src/play/java']
res.srcDirs = ['src/experimental/res']
manifest.srcFile 'src/play/AndroidManifest.xml'
}
}
applicationVariants.all { variant ->
@ -385,8 +377,9 @@ dependencies {
implementation "com.github.bumptech.glide:glide:4.9.0"
annotationProcessor "com.github.bumptech.glide:compiler:4.9.0"
implementation "com.tencent:mmkv-static:1.2.10"
androidTestImplementation('com.wix:detox:+') { transitive = true }
androidTestImplementation 'junit:junit:4.12'
androidTestImplementation('com.wix:detox:+')
implementation 'androidx.appcompat:appcompat:1.1.0'
implementation 'com.facebook.soloader:soloader:0.10.4'
}
if (isNewArchitectureEnabled()) {

View File

@ -18,7 +18,7 @@ public class DetoxTest {
@Rule
// Replace 'MainActivity' with the value of android:name entry in
// <activity> in AndroidManifest.xml
public ActivityTestRule<MainActivity> mActivityRule = new ActivityTestRule<>(MainActivity.class, false, false);
public ActivityTestRule<chat.rocket.reactnative.MainActivity> mActivityRule = new ActivityTestRule<>(chat.rocket.reactnative.MainActivity.class, false, false);
@Test
public void runDetoxTests() {

View File

@ -23,8 +23,6 @@ import com.facebook.react.ReactInstanceManager;
import com.facebook.react.bridge.ReactContext;
import com.facebook.react.modules.network.NetworkingModule;
import com.facebook.react.modules.network.CustomClientBuilder;
import tech.bam.rnperformance.flipper.RNPerfMonitorPlugin;
import okhttp3.OkHttpClient;
public class ReactNativeFlipper {
public static void initializeFlipper(Context context, ReactInstanceManager reactInstanceManager) {
@ -35,8 +33,6 @@ public class ReactNativeFlipper {
client.addPlugin(new DatabasesFlipperPlugin(context));
client.addPlugin(new SharedPreferencesFlipperPlugin(context));
client.addPlugin(CrashReporterPlugin.getInstance());
client.addPlugin(new RNPerfMonitorPlugin(reactInstanceManager));
NetworkFlipperPlugin networkFlipperPlugin = new NetworkFlipperPlugin();
NetworkingModule.setCustomClientBuilder(
new CustomClientBuilder() {

View File

@ -1,10 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<network-security-config xmlns:tools="http://schemas.android.com/tools">
<base-config cleartextTrafficPermitted="true">
<trust-anchors>
<certificates src="system" />
<certificates src="user"
tools:ignore="AcceptsUserCertificates" />
</trust-anchors>
</base-config>
</network-security-config>

View File

@ -11,6 +11,9 @@
<uses-permission android:name="android.permission.VIDEO_CAPTURE" />
<uses-permission android:name="android.permission.AUDIO_CAPTURE" />
<!-- permissions related to jitsi call -->
<uses-permission android:name="android.permission.BLUETOOTH" />
<application
android:name="chat.rocket.reactnative.MainApplication"
android:allowBackup="false"

View File

@ -7,4 +7,8 @@
tools:ignore="AcceptsUserCertificates" />
</trust-anchors>
</base-config>
<domain-config cleartextTrafficPermitted="true">
<domain includeSubdomains="true">10.0.2.2</domain>
<domain includeSubdomains="true">localhost</domain>
</domain-config>
</network-security-config>

View File

@ -1,9 +1,5 @@
import org.apache.tools.ant.taskdefs.condition.Os
def safeExtGet(prop, fallback) {
rootProject.ext.has(prop) ? rootProject.ext.get(prop) : fallback
}
// Top-level build file where you can add configuration options common to all sub-projects/modules.
buildscript {
def taskRequests = getGradle().getStartParameter().getTaskRequests().toString().toLowerCase()
@ -75,5 +71,38 @@ allprojects {
google()
maven { url 'https://maven.google.com' }
maven { url 'https://www.jitpack.io' }
// https://stackoverflow.com/a/74333788/5447468
// TODO: remove once we update RN
exclusiveContent {
// We get React Native's Android binaries exclusively through npm,
// from a local Maven repo inside node_modules/react-native/.
// (The use of exclusiveContent prevents looking elsewhere like Maven Central
// and potentially getting a wrong version.)
filter {
includeGroup "com.facebook.react"
}
forRepository {
maven {
// NOTE: if you are in a monorepo, you may have "$rootDir/../../../node_modules/react-native/android"
url "$rootDir/../node_modules/react-native/android"
}
}
}
}
}
subprojects { subproject ->
afterEvaluate {
if (!project.name.equalsIgnoreCase("app") && project.hasProperty("android")) {
android {
compileSdkVersion 31
buildToolsVersion "31.0.0"
defaultConfig {
minSdkVersion 23
targetSdkVersion 31
}
}
}
}
}

View File

@ -28,7 +28,9 @@ export const ROOM = createRequestTypes('ROOM', [
'DELETE',
'REMOVED',
'FORWARD',
'USER_TYPING'
'USER_TYPING',
'HISTORY_REQUEST',
'HISTORY_FINISHED'
]);
export const INQUIRY = createRequestTypes('INQUIRY', [
...defaultTypes,
@ -38,7 +40,14 @@ export const INQUIRY = createRequestTypes('INQUIRY', [
'QUEUE_UPDATE',
'QUEUE_REMOVE'
]);
export const APP = createRequestTypes('APP', ['START', 'READY', 'INIT', 'INIT_LOCAL_SETTINGS', 'SET_MASTER_DETAIL']);
export const APP = createRequestTypes('APP', [
'START',
'READY',
'INIT',
'INIT_LOCAL_SETTINGS',
'SET_MASTER_DETAIL',
'SET_NOTIFICATION_PRESENCE_CAP'
]);
export const MESSAGES = createRequestTypes('MESSAGES', ['REPLY_BROADCAST']);
export const CREATE_CHANNEL = createRequestTypes('CREATE_CHANNEL', [...defaultTypes]);
export const CREATE_DISCUSSION = createRequestTypes('CREATE_DISCUSSION', [...defaultTypes]);

View File

@ -12,7 +12,11 @@ interface ISetMasterDetail extends Action {
isMasterDetail: boolean;
}
export type TActionApp = IAppStart & ISetMasterDetail;
interface ISetNotificationPresenceCap extends Action {
show: boolean;
}
export type TActionApp = IAppStart & ISetMasterDetail & ISetNotificationPresenceCap;
interface Params {
root: RootEnum;
@ -51,3 +55,10 @@ export function setMasterDetail(isMasterDetail: boolean): ISetMasterDetail {
isMasterDetail
};
}
export function setNotificationPresenceCap(show: boolean): ISetNotificationPresenceCap {
return {
type: APP.SET_NOTIFICATION_PRESENCE_CAP,
show
};
}

View File

@ -1,6 +1,6 @@
import { Action } from 'redux';
import { ERoomType } from '../definitions/ERoomType';
import { ERoomType, RoomType } from '../definitions';
import { ROOM } from './actionsTypes';
// TYPE RETURN RELATED
@ -44,7 +44,24 @@ interface IUserTyping extends Action {
status: boolean;
}
export type TActionsRoom = TSubscribeRoom & TUnsubscribeRoom & ILeaveRoom & IDeleteRoom & IForwardRoom & IUserTyping;
export interface IRoomHistoryRequest extends Action {
rid: string;
t: RoomType;
loaderId: string;
}
export interface IRoomHistoryFinished extends Action {
loaderId: string;
}
export type TActionsRoom = TSubscribeRoom &
TUnsubscribeRoom &
ILeaveRoom &
IDeleteRoom &
IForwardRoom &
IUserTyping &
IRoomHistoryRequest &
IRoomHistoryFinished;
export function subscribeRoom(rid: string): TSubscribeRoom {
return {
@ -99,3 +116,19 @@ export function userTyping(rid: string, status = true): IUserTyping {
status
};
}
export function roomHistoryRequest({ rid, t, loaderId }: { rid: string; t: RoomType; loaderId: string }): IRoomHistoryRequest {
return {
type: ROOM.HISTORY_REQUEST,
rid,
t,
loaderId
};
}
export function roomHistoryFinished({ loaderId }: { loaderId: string }): IRoomHistoryFinished {
return {
type: ROOM.HISTORY_FINISHED,
loaderId
};
}

View File

@ -1,4 +1,5 @@
export const mappedIcons = {
'status-disabled': 59837,
'lamp-bulb': 59836,
'phone-in': 59835,
'basketball': 59776,

File diff suppressed because one or more lines are too long

View File

@ -1,34 +1,31 @@
import React from 'react';
import { useWindowDimensions } from 'react-native';
import { FlatList } from 'react-native-gesture-handler';
import { EMOJI_BUTTON_SIZE } from './styles';
import scrollPersistTaps from '../../lib/methods/helpers/scrollPersistTaps';
import { IEmoji } from '../../definitions/IEmoji';
import scrollPersistTaps from '../../lib/methods/helpers/scrollPersistTaps';
import { PressableEmoji } from './PressableEmoji';
import { EMOJI_BUTTON_SIZE } from './styles';
interface IEmojiCategoryProps {
emojis: IEmoji[];
onEmojiSelected: (emoji: IEmoji) => void;
tabLabel?: string; // needed for react-native-scrollable-tab-view only
parentWidth: number;
}
const EmojiCategory = ({ onEmojiSelected, emojis }: IEmojiCategoryProps): React.ReactElement | null => {
const { width } = useWindowDimensions();
const numColumns = Math.trunc(width / EMOJI_BUTTON_SIZE);
const marginHorizontal = (width % EMOJI_BUTTON_SIZE) / 2;
const renderItem = ({ item }: { item: IEmoji }) => <PressableEmoji emoji={item} onPress={onEmojiSelected} />;
if (!width) {
const EmojiCategory = ({ onEmojiSelected, emojis, parentWidth }: IEmojiCategoryProps): React.ReactElement | null => {
if (!parentWidth) {
return null;
}
const numColumns = Math.trunc(parentWidth / EMOJI_BUTTON_SIZE);
const marginHorizontal = (parentWidth % EMOJI_BUTTON_SIZE) / 2;
const renderItem = ({ item }: { item: IEmoji }) => <PressableEmoji emoji={item} onPress={onEmojiSelected} />;
return (
<FlatList
// needed to update the numColumns when the width changes
key={`emoji-category-${width}`}
key={`emoji-category-${parentWidth}`}
keyExtractor={item => (typeof item === 'string' ? item : item.name)}
data={emojis}
renderItem={renderItem}

View File

@ -1,4 +1,4 @@
import React from 'react';
import React, { useState } from 'react';
import { View } from 'react-native';
import ScrollableTabView from 'react-native-scrollable-tab-view';
@ -20,6 +20,8 @@ const EmojiPicker = ({
searchedEmojis = []
}: IEmojiPickerProps): React.ReactElement | null => {
const { colors } = useTheme();
const [parentWidth, setParentWidth] = useState(0);
const { frequentlyUsed, loaded } = useFrequentlyUsedEmoji();
const allCustomEmojis: ICustomEmojis = useAppSelector(
@ -50,7 +52,14 @@ const EmojiPicker = ({
if (!emojis.length) {
return null;
}
return <EmojiCategory emojis={emojis} onEmojiSelected={(emoji: IEmoji) => handleEmojiSelect(emoji)} tabLabel={label} />;
return (
<EmojiCategory
parentWidth={parentWidth}
emojis={emojis}
onEmojiSelected={(emoji: IEmoji) => handleEmojiSelect(emoji)}
tabLabel={label}
/>
);
};
if (!loaded) {
@ -58,9 +67,13 @@ const EmojiPicker = ({
}
return (
<View style={styles.emojiPickerContainer}>
<View style={styles.emojiPickerContainer} onLayout={e => setParentWidth(e.nativeEvent.layout.width)}>
{searching ? (
<EmojiCategory emojis={searchedEmojis} onEmojiSelected={(emoji: IEmoji) => handleEmojiSelect(emoji)} />
<EmojiCategory
emojis={searchedEmojis}
onEmojiSelected={(emoji: IEmoji) => handleEmojiSelect(emoji)}
parentWidth={parentWidth}
/>
) : (
<ScrollableTabView
renderTabBar={() => <TabBar />}

View File

@ -1,6 +1,7 @@
import React from 'react';
import { StyleSheet } from 'react-native';
import { StyleSheet, View } from 'react-native';
import { STATUS_COLORS } from '../../lib/constants';
import UnreadBadge from '../UnreadBadge';
const styles = StyleSheet.create({
@ -15,6 +16,8 @@ const styles = StyleSheet.create({
}
});
export const Badge = ({ ...props }): React.ReactElement => <UnreadBadge {...props} style={styles.badgeContainer} small />;
export const BadgeUnread = ({ ...props }): React.ReactElement => <UnreadBadge {...props} style={styles.badgeContainer} small />;
export default Badge;
export const BadgeWarn = (): React.ReactElement => (
<View style={[styles.badgeContainer, { width: 10, height: 10, backgroundColor: STATUS_COLORS.disabled }]} />
);

View File

@ -1,4 +1,5 @@
import React from 'react';
import { View } from 'react-native';
import { Header, HeaderBackground } from '@react-navigation/elements';
import { NavigationContainer } from '@react-navigation/native';
import { SafeAreaProvider } from 'react-native-safe-area-context';
@ -103,9 +104,10 @@ export const Badge = () => (
<HeaderExample
left={() => (
<HeaderButton.Container left>
<HeaderButton.Item iconName='threads' badge={() => <HeaderButton.Badge tunread={[1]} />} />
<HeaderButton.Item iconName='threads' badge={() => <HeaderButton.Badge tunread={[1]} tunreadUser={[1]} />} />
<HeaderButton.Item iconName='threads' badge={() => <HeaderButton.Badge tunread={[1]} tunreadGroup={[1]} />} />
<HeaderButton.Item iconName='threads' badge={() => <HeaderButton.BadgeUnread tunread={[1]} />} />
<HeaderButton.Item iconName='threads' badge={() => <HeaderButton.BadgeUnread tunread={[1]} tunreadUser={[1]} />} />
<HeaderButton.Item iconName='threads' badge={() => <HeaderButton.BadgeUnread tunread={[1]} tunreadGroup={[1]} />} />
<HeaderButton.Drawer badge={() => <HeaderButton.BadgeWarn />} />
</HeaderButton.Container>
)}
/>
@ -114,20 +116,23 @@ export const Badge = () => (
const ThemeStory = ({ theme }: { theme: TSupportedThemes }) => (
<ThemeContext.Provider value={{ theme, colors: colors[theme] }}>
<HeaderExample
left={() => (
<HeaderButton.Container left>
<HeaderButton.Item iconName='threads' />
</HeaderButton.Container>
)}
right={() => (
<HeaderButton.Container>
<HeaderButton.Item title='Threads' />
<HeaderButton.Item iconName='threads' badge={() => <HeaderButton.Badge tunread={[1]} />} />
</HeaderButton.Container>
)}
colors={colors[theme]}
/>
<View style={{ flexDirection: 'column' }}>
<HeaderExample
left={() => (
<HeaderButton.Container left>
<HeaderButton.Drawer badge={() => <HeaderButton.BadgeWarn />} />
<HeaderButton.Item iconName='threads' />
</HeaderButton.Container>
)}
right={() => (
<HeaderButton.Container>
<HeaderButton.Item title='Threads' />
<HeaderButton.Item iconName='threads' badge={() => <HeaderButton.BadgeUnread tunread={[1]} />} />
</HeaderButton.Container>
)}
colors={colors[theme]}
/>
</View>
</ThemeContext.Provider>
);

View File

@ -1,4 +1,4 @@
export { default as Container } from './HeaderButtonContainer';
export { default as Item } from './HeaderButtonItem';
export { default as Badge } from './HeaderButtonItemBadge';
export * from './HeaderButtonItemBadge';
export * from './Common';

View File

@ -10,12 +10,12 @@ import { useFrequentlyUsedEmoji } from '../../lib/hooks';
import CustomEmoji from '../EmojiPicker/CustomEmoji';
import { useDimensions } from '../../dimensions';
import sharedStyles from '../../views/Styles';
import { IEmoji, TAnyMessage } from '../../definitions';
import { IEmoji, TAnyMessageModel } from '../../definitions';
import Touch from '../Touch';
export interface IHeader {
handleReaction: (emoji: IEmoji | null, message: TAnyMessage) => void;
message: TAnyMessage;
handleReaction: (emoji: IEmoji | null, message: TAnyMessageModel) => void;
message: TAnyMessageModel;
isMasterDetail: boolean;
}

View File

@ -5,7 +5,6 @@ import { connect } from 'react-redux';
import moment from 'moment';
import database from '../../lib/database';
import { getMessageById } from '../../lib/database/services/Message';
import I18n from '../../i18n';
import log, { logEvent } from '../../lib/methods/helpers/log';
import Navigation from '../../lib/navigation/appNavigation';
@ -16,7 +15,7 @@ import { showConfirmationAlert } from '../../lib/methods/helpers/info';
import { TActionSheetOptionsItem, useActionSheet, ACTION_SHEET_ANIMATION_DURATION } from '../ActionSheet';
import Header, { HEADER_HEIGHT, IHeader } from './Header';
import events from '../../lib/methods/helpers/log/events';
import { IApplicationState, IEmoji, ILoggedUser, TAnyMessage, TSubscriptionModel } from '../../definitions';
import { IApplicationState, IEmoji, ILoggedUser, TAnyMessageModel, TSubscriptionModel } from '../../definitions';
import { getPermalinkMessage } from '../../lib/methods';
import { getRoomTitle, getUidDirectMessage, hasPermission } from '../../lib/methods/helpers';
import { Services } from '../../lib/services';
@ -25,10 +24,10 @@ export interface IMessageActionsProps {
room: TSubscriptionModel;
tmid?: string;
user: Pick<ILoggedUser, 'id'>;
editInit: (message: TAnyMessage) => void;
reactionInit: (message: TAnyMessage) => void;
editInit: (message: TAnyMessageModel) => void;
reactionInit: (message: TAnyMessageModel) => void;
onReactionPress: (shortname: IEmoji, messageId: string) => void;
replyInit: (message: TAnyMessage, mention: boolean) => void;
replyInit: (message: TAnyMessageModel, mention: boolean) => void;
isMasterDetail: boolean;
isReadOnly: boolean;
Message_AllowDeleting?: boolean;
@ -47,7 +46,7 @@ export interface IMessageActionsProps {
}
export interface IMessageActions {
showMessageActions: (message: TAnyMessage) => Promise<void>;
showMessageActions: (message: TAnyMessageModel) => Promise<void>;
}
const MessageActions = React.memo(
@ -110,9 +109,9 @@ const MessageActions = React.memo(
}
};
const isOwn = (message: TAnyMessage) => message.u && message.u._id === user.id;
const isOwn = (message: TAnyMessageModel) => message.u && message.u._id === user.id;
const allowEdit = (message: TAnyMessage) => {
const allowEdit = (message: TAnyMessageModel) => {
if (isReadOnly) {
return false;
}
@ -136,7 +135,7 @@ const MessageActions = React.memo(
return true;
};
const allowDelete = (message: TAnyMessage) => {
const allowDelete = (message: TAnyMessageModel) => {
if (isReadOnly) {
return false;
}
@ -167,19 +166,19 @@ const MessageActions = React.memo(
return true;
};
const getPermalink = (message: TAnyMessage) => getPermalinkMessage(message);
const getPermalink = (message: TAnyMessageModel) => getPermalinkMessage(message);
const handleReply = (message: TAnyMessage) => {
const handleReply = (message: TAnyMessageModel) => {
logEvent(events.ROOM_MSG_ACTION_REPLY);
replyInit(message, true);
};
const handleEdit = (message: TAnyMessage) => {
const handleEdit = (message: TAnyMessageModel) => {
logEvent(events.ROOM_MSG_ACTION_EDIT);
editInit(message);
};
const handleCreateDiscussion = (message: TAnyMessage) => {
const handleCreateDiscussion = (message: TAnyMessageModel) => {
logEvent(events.ROOM_MSG_ACTION_DISCUSSION);
const params = { message, channel: room, showCloseModal: true };
if (isMasterDetail) {
@ -189,7 +188,7 @@ const MessageActions = React.memo(
}
};
const handleUnread = async (message: TAnyMessage) => {
const handleUnread = async (message: TAnyMessageModel) => {
logEvent(events.ROOM_MSG_ACTION_UNREAD);
const { id: messageId, ts } = message;
const { rid } = room;
@ -214,7 +213,7 @@ const MessageActions = React.memo(
}
};
const handlePermalink = async (message: TAnyMessage) => {
const handlePermalink = async (message: TAnyMessageModel) => {
logEvent(events.ROOM_MSG_ACTION_PERMALINK);
try {
const permalink = await getPermalink(message);
@ -225,13 +224,13 @@ const MessageActions = React.memo(
}
};
const handleCopy = async (message: TAnyMessage) => {
const handleCopy = async (message: TAnyMessageModel) => {
logEvent(events.ROOM_MSG_ACTION_COPY);
await Clipboard.setString((message?.attachments?.[0]?.description || message.msg) ?? '');
EventEmitter.emit(LISTENER, { message: I18n.t('Copied_to_clipboard') });
};
const handleShare = async (message: TAnyMessage) => {
const handleShare = async (message: TAnyMessageModel) => {
logEvent(events.ROOM_MSG_ACTION_SHARE);
try {
const permalink = await getPermalink(message);
@ -243,12 +242,12 @@ const MessageActions = React.memo(
}
};
const handleQuote = (message: TAnyMessage) => {
const handleQuote = (message: TAnyMessageModel) => {
logEvent(events.ROOM_MSG_ACTION_QUOTE);
replyInit(message, false);
};
const handleReplyInDM = async (message: TAnyMessage) => {
const handleReplyInDM = async (message: TAnyMessageModel) => {
if (message?.u?.username) {
const result = await Services.createDirectMessage(message.u.username);
if (result.success) {
@ -265,7 +264,7 @@ const MessageActions = React.memo(
}
};
const handleStar = async (message: TAnyMessage) => {
const handleStar = async (message: TAnyMessageModel) => {
logEvent(message.starred ? events.ROOM_MSG_ACTION_UNSTAR : events.ROOM_MSG_ACTION_STAR);
try {
await Services.toggleStarMessage(message.id, message.starred as boolean); // TODO: reevaluate `message.starred` type on IMessage
@ -276,7 +275,7 @@ const MessageActions = React.memo(
}
};
const handlePin = async (message: TAnyMessage) => {
const handlePin = async (message: TAnyMessageModel) => {
logEvent(events.ROOM_MSG_ACTION_PIN);
try {
await Services.togglePinMessage(message.id, message.pinned as boolean); // TODO: reevaluate `message.pinned` type on IMessage
@ -296,7 +295,7 @@ const MessageActions = React.memo(
hideActionSheet();
};
const handleReadReceipt = (message: TAnyMessage) => {
const handleReadReceipt = (message: TAnyMessageModel) => {
if (isMasterDetail) {
Navigation.navigate('ModalStackNavigator', { screen: 'ReadReceiptsView', params: { messageId: message.id } });
} else {
@ -304,18 +303,14 @@ const MessageActions = React.memo(
}
};
const handleToggleTranslation = async (message: TAnyMessage) => {
const handleToggleTranslation = async (message: TAnyMessageModel) => {
try {
if (!room.autoTranslateLanguage) {
return;
}
const db = database.active;
const messageRecord = await getMessageById(message.id);
if (!messageRecord) {
return;
}
await db.write(async () => {
await messageRecord.update(m => {
await message.update(m => {
m.autoTranslate = !m.autoTranslate;
m._updatedAt = new Date();
});
@ -329,7 +324,7 @@ const MessageActions = React.memo(
}
};
const handleReport = async (message: TAnyMessage) => {
const handleReport = async (message: TAnyMessageModel) => {
logEvent(events.ROOM_MSG_ACTION_REPORT);
try {
await Services.reportMessage(message.id);
@ -340,14 +335,14 @@ const MessageActions = React.memo(
}
};
const handleDelete = (message: TAnyMessage) => {
const handleDelete = (message: TAnyMessageModel) => {
showConfirmationAlert({
message: I18n.t('You_will_not_be_able_to_recover_this_message'),
confirmationText: I18n.t('Delete'),
onPress: async () => {
try {
logEvent(events.ROOM_MSG_ACTION_DELETE);
await Services.deleteMessage(message.id, message.rid);
await Services.deleteMessage(message.id, message.subscription ? message.subscription.id : '');
} catch (e) {
logEvent(events.ROOM_MSG_ACTION_DELETE_F);
log(e);
@ -356,7 +351,7 @@ const MessageActions = React.memo(
});
};
const getOptions = (message: TAnyMessage) => {
const getOptions = (message: TAnyMessageModel) => {
const options: TActionSheetOptionsItem[] = [];
const videoConfBlock = message.t === 'videoconf';
@ -492,7 +487,7 @@ const MessageActions = React.memo(
return options;
};
const showMessageActions = async (message: TAnyMessage) => {
const showMessageActions = async (message: TAnyMessageModel) => {
logEvent(events.ROOM_SHOW_MSG_ACTIONS);
await getPermissions();
showActionSheet({

View File

@ -1,7 +1,6 @@
import React, { Component } from 'react';
import { Alert, Keyboard, NativeModules, Text, View, BackHandler } from 'react-native';
import { connect } from 'react-redux';
// import { KeyboardTrackingView } from 'react-native-keyboard-tracking-view';
import { KeyboardAccessoryView } from 'react-native-ui-lib/keyboard';
import ImagePicker, { Image, ImageOrVideo, Options } from 'react-native-image-crop-picker';
import { dequal } from 'dequal';
@ -170,7 +169,7 @@ class MessageBox extends Component<IMessageBoxProps, IMessageBoxState> {
id: ''
},
sharing: false,
iOSScrollBehavior: NativeModules.KeyboardTrackingViewTempManager?.KeyboardTrackingScrollBehaviorScrollToBottomInvertedOnly,
iOSScrollBehavior: NativeModules.KeyboardTrackingViewTempManager?.KeyboardTrackingScrollBehaviorFixedOffset,
isActionsEnabled: true,
getCustomEmoji: () => {}
};
@ -303,7 +302,7 @@ class MessageBox extends Component<IMessageBoxProps, IMessageBoxState> {
if (usedCannedResponse !== nextProps.usedCannedResponse) {
this.onChangeText(nextProps.usedCannedResponse ?? '');
}
if (sharing) {
if (sharing && !replying) {
this.setInput(nextProps.message.msg ?? '');
return;
}
@ -858,14 +857,21 @@ class MessageBox extends Component<IMessageBoxProps, IMessageBoxState> {
};
openShareView = (attachments: any) => {
const { message, replyCancel, replyWithMention } = this.props;
const { message, replyCancel, replyWithMention, replying } = this.props;
// Start a thread with an attachment
let value: TThreadModel | IMessage = this.thread;
if (replyWithMention) {
value = message;
replyCancel();
}
Navigation.navigate('ShareView', { room: this.room, thread: value, attachments });
Navigation.navigate('ShareView', {
room: this.room,
thread: value,
attachments,
replying,
replyingMessage: message,
closeReply: replyCancel
});
};
createDiscussion = () => {
@ -1043,16 +1049,7 @@ class MessageBox extends Component<IMessageBoxProps, IMessageBoxState> {
// Legacy reply or quote (quote is a reply without mention)
} else {
const { user, roomType } = this.props;
const permalink = await this.getPermalink(replyingMessage);
let msg = `[ ](${permalink}) `;
// if original message wasn't sent by current user and neither from a direct room
if (user.username !== replyingMessage?.u?.username && roomType !== 'd' && replyWithMention) {
msg += `@${replyingMessage?.u?.username} `;
}
msg = `${msg} ${message}`;
const msg = await this.formatReplyMessage(replyingMessage, message);
onSubmit(msg);
}
replyCancel();
@ -1064,6 +1061,20 @@ class MessageBox extends Component<IMessageBoxProps, IMessageBoxState> {
}
};
formatReplyMessage = async (replyingMessage: IMessage, message = '') => {
const { user, roomType, replyWithMention, serverVersion } = this.props;
const permalink = await this.getPermalink(replyingMessage);
let msg = `[ ](${permalink}) `;
// if original message wasn't sent by current user and neither from a direct room
if (user.username !== replyingMessage?.u?.username && roomType !== 'd' && replyWithMention) {
msg += `@${replyingMessage?.u?.username} `;
}
const connectionString = compareServerVersion(serverVersion, 'lowerThan', '5.0.0') ? ' ' : '\n';
return `${msg}${connectionString}${message}`;
};
updateMentions = (keyword: any, type: string) => {
if (type === MENTIONS_TRACKING_TYPE_USERS) {
this.getUsers(keyword);
@ -1291,7 +1302,7 @@ class MessageBox extends Component<IMessageBoxProps, IMessageBoxState> {
render() {
console.count(`${this.constructor.name}.render calls`);
const { showEmojiKeyboard } = this.state;
const { user, baseUrl, theme, iOSScrollBehavior, tmid, rid } = this.props;
const { user, baseUrl, theme, iOSScrollBehavior } = this.props;
return (
<MessageboxContext.Provider
value={{
@ -1310,10 +1321,11 @@ class MessageBox extends Component<IMessageBoxProps, IMessageBoxState> {
kbInitialProps={{ theme }}
onKeyboardResigned={this.onKeyboardResigned}
onItemSelected={this.onKeyboardItemSelected}
trackInteractive
requiresSameParentToManageScrollView
addBottomView
bottomViewColor={themes[theme].messageboxBackground}
iOSScrollBehavior={iOSScrollBehavior}
scrollViewNativeID={tmid || rid}
/>
</MessageboxContext.Provider>
);

View File

@ -1,8 +1,6 @@
import { forwardRef, useImperativeHandle } from 'react';
import Model from '@nozbe/watermelondb/Model';
import { getMessageById } from '../lib/database/services/Message';
import { getThreadMessageById } from '../lib/database/services/ThreadMessage';
import database from '../lib/database';
import protectedFunction from '../lib/methods/helpers/protectedFunction';
import { useActionSheet } from './ActionSheet';
@ -29,16 +27,16 @@ const MessageErrorActions = forwardRef<IMessageErrorActions, { tmid?: string }>(
const msgCollection = db.get('messages');
const threadCollection = db.get('threads');
const msg = await getMessageById(message.id);
if (msg) {
deleteBatch.push(msg.prepareDestroyPermanently());
}
// Delete the object (it can be Message or ThreadMessage instance)
deleteBatch.push(message.prepareDestroyPermanently());
// If it's a thread, we find and delete the whole tree, if necessary
if (tmid) {
const msg = await getThreadMessageById(message.id);
if (msg) {
try {
const msg = await msgCollection.find(message.id);
deleteBatch.push(msg.prepareDestroyPermanently());
} catch {
// Do nothing: message not found
}
try {

View File

@ -46,6 +46,7 @@ const RoomHeaderContainer = React.memo(
const connecting = useSelector((state: IApplicationState) => state.meteor.connecting || state.server.loading);
const usersTyping = useSelector((state: IApplicationState) => state.usersTyping, shallowEqual);
const connected = useSelector((state: IApplicationState) => state.meteor.connected);
const presenceDisabled = useSelector((state: IApplicationState) => state.settings.Presence_broadcast_disabled);
const activeUser = useSelector(
(state: IApplicationState) => (roomUserId ? state.activeUsers?.[roomUserId] : undefined),
shallowEqual
@ -61,9 +62,13 @@ const RoomHeaderContainer = React.memo(
if (connected) {
if ((type === 'd' || (tmid && roomUserId)) && activeUser) {
const { status: statusActiveUser, statusText: statusTextActiveUser } = activeUser;
status = statusActiveUser;
statusText = statusTextActiveUser;
if (presenceDisabled) {
status = 'disabled';
} else {
const { status: statusActiveUser, statusText: statusTextActiveUser } = activeUser;
status = statusActiveUser;
statusText = statusTextActiveUser;
}
} else if (type === 'l' && visitor?.status) {
const { status: statusVisitor } = visitor;
status = statusVisitor;

View File

@ -1,4 +1,5 @@
import React from 'react';
import { dequal } from 'dequal';
import I18n from '../../i18n';
import styles from './styles';
@ -44,7 +45,9 @@ const formatMsg = ({ lastMessage, type, showLastMessage, username, useRealName }
return `${prefix}${lastMessage.msg}`;
};
const LastMessage = ({ lastMessage, type, showLastMessage, username, alert, useRealName }: ILastMessageProps) => {
const arePropsEqual = (oldProps: any, newProps: any) => dequal(oldProps, newProps);
const LastMessage = React.memo(({ lastMessage, type, showLastMessage, username, alert, useRealName }: ILastMessageProps) => {
const { colors } = useTheme();
return (
<MarkdownPreview
@ -60,6 +63,6 @@ const LastMessage = ({ lastMessage, type, showLastMessage, username, alert, useR
testID='room-item-last-message'
/>
);
};
}, arePropsEqual);
export default LastMessage;

View File

@ -65,6 +65,7 @@ export const UserStatus = () => (
<RoomItem status='busy' />
<RoomItem status='offline' />
<RoomItem status='loading' />
<RoomItem status='disabled' />
<RoomItem status='wrong' />
</>
);

View File

@ -20,6 +20,7 @@ const RoomItem = ({
prid,
name,
avatar,
width,
username,
showLastMessage,
status = 'offline',
@ -51,13 +52,12 @@ const RoomItem = ({
showAvatar,
displayMode,
sourceType,
hideMentionStatus,
touchableRef
hideMentionStatus
}: IRoomItemProps) => (
<Touchable
ref={touchableRef}
onPress={onPress}
onLongPress={onLongPress}
width={width}
favorite={favorite}
toggleFav={toggleFav}
isRead={isRead}

View File

@ -1,4 +1,4 @@
import React, { forwardRef, useImperativeHandle } from 'react';
import React from 'react';
import Animated, {
useAnimatedGestureHandler,
useSharedValue,
@ -13,242 +13,227 @@ import {
HandlerStateChangeEventPayload,
PanGestureHandlerEventPayload
} from 'react-native-gesture-handler';
import { useWindowDimensions } from 'react-native';
import Touch from '../Touch';
import { ACTION_WIDTH, LONG_SWIPE, SMALL_SWIPE } from './styles';
import { LeftActions, RightActions } from './Actions';
import { ITouchableProps, ITouchableRef } from './interfaces';
import { ITouchableProps } from './interfaces';
import { useTheme } from '../../theme';
import I18n from '../../i18n';
import { MAX_SIDEBAR_WIDTH } from '../../lib/constants';
import { useAppSelector } from '../../lib/hooks';
const Touchable = forwardRef<ITouchableRef, ITouchableProps>(
(
{
children,
type,
onPress,
onLongPress,
testID,
favorite,
isRead,
rid,
toggleFav,
toggleRead,
hideChannel,
isFocused,
swipeEnabled,
displayMode
},
ref
): React.ReactElement => {
const { colors } = useTheme();
const { width: deviceWidth } = useWindowDimensions();
const isMasterDetail = useAppSelector(state => state.app.isMasterDetail);
const width = isMasterDetail ? MAX_SIDEBAR_WIDTH : deviceWidth;
const Touchable = ({
children,
type,
onPress,
onLongPress,
testID,
width,
favorite,
isRead,
rid,
toggleFav,
toggleRead,
hideChannel,
isFocused,
swipeEnabled,
displayMode
}: ITouchableProps): React.ReactElement => {
const { colors } = useTheme();
const rowOffSet = useSharedValue(0);
const transX = useSharedValue(0);
const rowState = useSharedValue(0); // 0: closed, 1: right opened, -1: left opened
let _value = 0;
const rowOffSet = useSharedValue(0);
const transX = useSharedValue(0);
const rowState = useSharedValue(0); // 0: closed, 1: right opened, -1: left opened
let _value = 0;
const close = () => {
console.log(`${rid} close`);
rowState.value = 0;
transX.value = withSpring(0, { overshootClamping: true });
rowOffSet.value = 0;
};
const close = () => {
rowState.value = 0;
transX.value = withSpring(0, { overshootClamping: true });
rowOffSet.value = 0;
};
useImperativeHandle(ref, () => ({
close
}));
const handleToggleFav = () => {
if (toggleFav) {
toggleFav(rid, favorite);
}
close();
};
const handleToggleFav = () => {
if (toggleFav) {
toggleFav(rid, favorite);
}
const handleToggleRead = () => {
if (toggleRead) {
toggleRead(rid, isRead);
}
};
const handleHideChannel = () => {
if (hideChannel) {
hideChannel(rid, type);
}
};
const onToggleReadPress = () => {
handleToggleRead();
close();
};
const onHidePress = () => {
handleHideChannel();
close();
};
const handlePress = () => {
if (rowState.value !== 0) {
close();
};
return;
}
if (onPress) {
onPress();
}
};
const handleToggleRead = () => {
if (toggleRead) {
toggleRead(rid, isRead);
}
};
const handleHideChannel = () => {
if (hideChannel) {
hideChannel(rid, type);
}
};
const onToggleReadPress = () => {
handleToggleRead();
const handleLongPress = () => {
if (rowState.value !== 0) {
close();
};
return;
}
const onHidePress = () => {
handleHideChannel();
close();
};
if (onLongPress) {
onLongPress();
}
};
const handlePress = () => {
if (rowState.value !== 0) {
close();
return;
}
if (onPress) {
onPress();
}
};
const onLongPressHandlerStateChange = ({ nativeEvent }: { nativeEvent: HandlerStateChangeEventPayload }) => {
if (nativeEvent.state === State.ACTIVE) {
handleLongPress();
}
};
const handleLongPress = () => {
if (rowState.value !== 0) {
close();
return;
}
if (onLongPress) {
onLongPress();
}
};
const onLongPressHandlerStateChange = ({ nativeEvent }: { nativeEvent: HandlerStateChangeEventPayload }) => {
if (nativeEvent.state === State.ACTIVE) {
handleLongPress();
}
};
const handleRelease = (event: PanGestureHandlerEventPayload) => {
const { translationX } = event;
_value += translationX;
let toValue = 0;
if (rowState.value === 0) {
// if no option is opened
if (translationX > 0 && translationX < LONG_SWIPE) {
if (I18n.isRTL) {
toValue = 2 * ACTION_WIDTH;
} else {
toValue = ACTION_WIDTH;
}
rowState.value = -1;
} else if (translationX >= LONG_SWIPE) {
toValue = 0;
if (I18n.isRTL) {
handleHideChannel();
} else {
handleToggleRead();
}
} else if (translationX < 0 && translationX > -LONG_SWIPE) {
// open trailing option if he swipe left
if (I18n.isRTL) {
toValue = -ACTION_WIDTH;
} else {
toValue = -2 * ACTION_WIDTH;
}
rowState.value = 1;
} else if (translationX <= -LONG_SWIPE) {
toValue = 0;
rowState.value = 1;
if (I18n.isRTL) {
handleToggleRead();
} else {
handleHideChannel();
}
} else {
toValue = 0;
}
} else if (rowState.value === -1) {
// if left option is opened
if (_value < SMALL_SWIPE) {
toValue = 0;
rowState.value = 0;
} else if (_value > LONG_SWIPE) {
toValue = 0;
rowState.value = 0;
if (I18n.isRTL) {
handleHideChannel();
} else {
handleToggleRead();
}
} else if (I18n.isRTL) {
const handleRelease = (event: PanGestureHandlerEventPayload) => {
const { translationX } = event;
_value += translationX;
let toValue = 0;
if (rowState.value === 0) {
// if no option is opened
if (translationX > 0 && translationX < LONG_SWIPE) {
if (I18n.isRTL) {
toValue = 2 * ACTION_WIDTH;
} else {
toValue = ACTION_WIDTH;
}
} else if (rowState.value === 1) {
// if right option is opened
if (_value > -2 * SMALL_SWIPE) {
toValue = 0;
rowState.value = 0;
} else if (_value < -LONG_SWIPE) {
if (I18n.isRTL) {
handleToggleRead();
} else {
handleHideChannel();
}
} else if (I18n.isRTL) {
rowState.value = -1;
} else if (translationX >= LONG_SWIPE) {
toValue = 0;
if (I18n.isRTL) {
handleHideChannel();
} else {
handleToggleRead();
}
} else if (translationX < 0 && translationX > -LONG_SWIPE) {
// open trailing option if he swipe left
if (I18n.isRTL) {
toValue = -ACTION_WIDTH;
} else {
toValue = -2 * ACTION_WIDTH;
}
rowState.value = 1;
} else if (translationX <= -LONG_SWIPE) {
toValue = 0;
rowState.value = 1;
if (I18n.isRTL) {
handleToggleRead();
} else {
handleHideChannel();
}
} else {
toValue = 0;
}
transX.value = withSpring(toValue, { overshootClamping: true });
rowOffSet.value = toValue;
_value = toValue;
};
const onGestureEvent = useAnimatedGestureHandler({
onActive: event => {
transX.value = event.translationX + rowOffSet.value;
if (transX.value > 2 * width) transX.value = 2 * width;
},
onEnd: event => {
runOnJS(handleRelease)(event);
} else if (rowState.value === -1) {
// if left option is opened
if (_value < SMALL_SWIPE) {
toValue = 0;
rowState.value = 0;
} else if (_value > LONG_SWIPE) {
toValue = 0;
rowState.value = 0;
if (I18n.isRTL) {
handleHideChannel();
} else {
handleToggleRead();
}
} else if (I18n.isRTL) {
toValue = 2 * ACTION_WIDTH;
} else {
toValue = ACTION_WIDTH;
}
});
} else if (rowState.value === 1) {
// if right option is opened
if (_value > -2 * SMALL_SWIPE) {
toValue = 0;
rowState.value = 0;
} else if (_value < -LONG_SWIPE) {
if (I18n.isRTL) {
handleToggleRead();
} else {
handleHideChannel();
}
} else if (I18n.isRTL) {
toValue = -ACTION_WIDTH;
} else {
toValue = -2 * ACTION_WIDTH;
}
}
transX.value = withSpring(toValue, { overshootClamping: true });
rowOffSet.value = toValue;
_value = toValue;
};
const animatedStyles = useAnimatedStyle(() => ({ transform: [{ translateX: transX.value }] }));
const onGestureEvent = useAnimatedGestureHandler({
onActive: event => {
transX.value = event.translationX + rowOffSet.value;
if (transX.value > 2 * width) transX.value = 2 * width;
},
onEnd: event => {
runOnJS(handleRelease)(event);
}
});
return (
<LongPressGestureHandler onHandlerStateChange={onLongPressHandlerStateChange}>
<Animated.View>
<PanGestureHandler activeOffsetX={[-20, 20]} onGestureEvent={onGestureEvent} enabled={swipeEnabled}>
<Animated.View>
<LeftActions
transX={transX}
isRead={isRead}
width={width}
onToggleReadPress={onToggleReadPress}
displayMode={displayMode}
/>
<RightActions
transX={transX}
favorite={favorite}
width={width}
toggleFav={handleToggleFav}
onHidePress={onHidePress}
displayMode={displayMode}
/>
<Animated.View style={animatedStyles}>
<Touch
onPress={handlePress}
testID={testID}
style={{
backgroundColor: isFocused ? colors.chatComponentBackground : colors.backgroundColor
}}
>
{children}
</Touch>
</Animated.View>
const animatedStyles = useAnimatedStyle(() => ({ transform: [{ translateX: transX.value }] }));
return (
<LongPressGestureHandler onHandlerStateChange={onLongPressHandlerStateChange}>
<Animated.View>
<PanGestureHandler activeOffsetX={[-20, 20]} onGestureEvent={onGestureEvent} enabled={swipeEnabled}>
<Animated.View>
<LeftActions
transX={transX}
isRead={isRead}
width={width}
onToggleReadPress={onToggleReadPress}
displayMode={displayMode}
/>
<RightActions
transX={transX}
favorite={favorite}
width={width}
toggleFav={handleToggleFav}
onHidePress={onHidePress}
displayMode={displayMode}
/>
<Animated.View style={animatedStyles}>
<Touch
onPress={handlePress}
testID={testID}
style={{
backgroundColor: isFocused ? colors.chatComponentBackground : colors.backgroundColor
}}
>
{children}
</Touch>
</Animated.View>
</PanGestureHandler>
</Animated.View>
</LongPressGestureHandler>
);
}
);
</Animated.View>
</PanGestureHandler>
</Animated.View>
</LongPressGestureHandler>
);
};
export default Touchable;

View File

@ -1,127 +1,134 @@
import React, { useEffect, useRef } from 'react';
import React, { useEffect, useReducer, useRef } from 'react';
import { Subscription } from 'rxjs';
import I18n from '../../i18n';
import { useAppSelector } from '../../lib/hooks';
import { getUserPresence } from '../../lib/methods';
import { isGroupChat } from '../../lib/methods/helpers';
import { formatDate } from '../../lib/methods/helpers/room';
import { IRoomItemContainerProps, ITouchableRef } from './interfaces';
import { IRoomItemContainerProps } from './interfaces';
import RoomItem from './RoomItem';
import { ROW_HEIGHT, ROW_HEIGHT_CONDENSED } from './styles';
import { useUserStatus } from './useUserStatus';
export { ROW_HEIGHT, ROW_HEIGHT_CONDENSED };
const RoomItemContainer = ({
item,
id,
onPress,
onLongPress,
toggleFav,
toggleRead,
hideChannel,
isFocused,
showLastMessage,
username,
useRealName,
autoJoin,
showAvatar,
displayMode,
getRoomTitle = () => 'title',
getRoomAvatar = () => '',
getIsRead = () => false,
swipeEnabled = true
}: IRoomItemContainerProps): React.ReactElement => {
const name = getRoomTitle(item);
const testID = `rooms-list-view-item-${name}`;
const avatar = getRoomAvatar(item);
const isRead = getIsRead(item);
const date = item.roomUpdatedAt && formatDate(item.roomUpdatedAt);
const alert = item.alert || item.tunread?.length;
const connected = useAppSelector(state => state.meteor.connected);
const userStatus = useAppSelector(state => state.activeUsers[id || '']?.status);
const isDirect = !!(item.t === 'd' && id && !isGroupChat(item));
const touchableRef = useRef<ITouchableRef>(null);
const attrs = ['width', 'isFocused', 'showLastMessage', 'autoJoin', 'showAvatar', 'displayMode'];
// When app reconnects, we need to fetch the rendered user's presence
useEffect(() => {
if (connected && isDirect) {
getUserPresence(id);
const RoomItemContainer = React.memo(
({
item,
id,
onPress,
onLongPress,
width,
toggleFav,
toggleRead,
hideChannel,
isFocused,
showLastMessage,
username,
useRealName,
autoJoin,
showAvatar,
displayMode,
getRoomTitle = () => 'title',
getRoomAvatar = () => '',
getIsRead = () => false,
swipeEnabled = true
}: IRoomItemContainerProps) => {
const name = getRoomTitle(item);
const testID = `rooms-list-view-item-${name}`;
const avatar = getRoomAvatar(item);
const isRead = getIsRead(item);
const date = item.roomUpdatedAt && formatDate(item.roomUpdatedAt);
const alert = item.alert || item.tunread?.length;
const [_, forceUpdate] = useReducer(x => x + 1, 1);
const roomSubscription = useRef<Subscription | null>(null);
const { connected, status } = useUserStatus(item.t, item?.visitor?.status, id);
useEffect(() => {
const init = () => {
if (item?.observe) {
const observable = item.observe();
roomSubscription.current = observable?.subscribe?.(() => {
if (_) forceUpdate();
});
}
};
init();
return () => roomSubscription.current?.unsubscribe();
}, []);
useEffect(() => {
const isDirect = !!(item.t === 'd' && id && !isGroupChat(item));
if (connected && isDirect) {
getUserPresence(id);
}
}, [connected]);
const handleOnPress = () => onPress(item);
const handleOnLongPress = () => onLongPress && onLongPress(item);
let accessibilityLabel = '';
if (item.unread === 1) {
accessibilityLabel = `, ${item.unread} ${I18n.t('alert')}`;
} else if (item.unread > 1) {
accessibilityLabel = `, ${item.unread} ${I18n.t('alerts')}`;
}
}, [connected]);
/**
* The component can be recycled by FlashList.
* When rid changes and there's no user's status, we need to fetch it
*/
useEffect(() => {
if (!userStatus && isDirect) {
getUserPresence(id);
if (item.userMentions > 0) {
accessibilityLabel = `, ${I18n.t('you_were_mentioned')}`;
}
if (date) {
accessibilityLabel = `, ${I18n.t('last_message')} ${date}`;
}
// TODO: Remove this when we have a better way to close the swipeable
touchableRef?.current?.close();
}, [item.rid]);
return (
<RoomItem
name={name}
avatar={avatar}
isGroupChat={isGroupChat(item)}
isRead={isRead}
onPress={handleOnPress}
onLongPress={handleOnLongPress}
date={date}
accessibilityLabel={accessibilityLabel}
width={width}
favorite={item.f}
rid={item.rid}
toggleFav={toggleFav}
toggleRead={toggleRead}
hideChannel={hideChannel}
testID={testID}
type={item.t}
isFocused={isFocused}
prid={item.prid}
status={status}
hideUnreadStatus={item.hideUnreadStatus}
hideMentionStatus={item.hideMentionStatus}
alert={alert}
lastMessage={item.lastMessage}
showLastMessage={showLastMessage}
username={username}
useRealName={useRealName}
unread={item.unread}
userMentions={item.userMentions}
groupMentions={item.groupMentions}
tunread={item.tunread}
tunreadUser={item.tunreadUser}
tunreadGroup={item.tunreadGroup}
swipeEnabled={swipeEnabled}
teamMain={item.teamMain}
autoJoin={autoJoin}
showAvatar={showAvatar}
displayMode={displayMode}
sourceType={item.source}
/>
);
},
(props, nextProps) => attrs.every(key => props[key] === nextProps[key])
);
const handleOnPress = () => onPress(item);
const handleOnLongPress = () => onLongPress && onLongPress(item);
let accessibilityLabel = '';
if (item.unread === 1) {
accessibilityLabel = `, ${item.unread} ${I18n.t('alert')}`;
} else if (item.unread > 1) {
accessibilityLabel = `, ${item.unread} ${I18n.t('alerts')}`;
}
if (item.userMentions > 0) {
accessibilityLabel = `, ${I18n.t('you_were_mentioned')}`;
}
if (date) {
accessibilityLabel = `, ${I18n.t('last_message')} ${date}`;
}
const status = item.t === 'l' ? item.visitor?.status || item.v?.status : userStatus;
return (
<RoomItem
touchableRef={touchableRef}
name={name}
avatar={avatar}
isGroupChat={isGroupChat(item)}
isRead={isRead}
onPress={handleOnPress}
onLongPress={handleOnLongPress}
date={date}
accessibilityLabel={accessibilityLabel}
favorite={item.f}
rid={item.rid}
toggleFav={toggleFav}
toggleRead={toggleRead}
hideChannel={hideChannel}
testID={testID}
type={item.t}
isFocused={isFocused}
prid={item.prid}
status={status}
hideUnreadStatus={item.hideUnreadStatus}
hideMentionStatus={item.hideMentionStatus}
alert={alert}
lastMessage={item.lastMessage}
showLastMessage={showLastMessage}
username={username}
useRealName={useRealName}
unread={item.unread}
userMentions={item.userMentions}
groupMentions={item.groupMentions}
tunread={item.tunread}
tunreadUser={item.tunreadUser}
tunreadGroup={item.tunreadGroup}
swipeEnabled={swipeEnabled}
teamMain={item.teamMain}
autoJoin={autoJoin}
showAvatar={showAvatar}
displayMode={displayMode}
sourceType={item.source}
/>
);
};
export default RoomItemContainer;

View File

@ -78,6 +78,7 @@ interface IBaseRoomItem extends IRoomItemTouchables {
showAvatar: boolean;
swipeEnabled: boolean;
autoJoin?: boolean;
width: number;
username?: string;
}
@ -115,7 +116,6 @@ export interface IRoomItemProps extends IBaseRoomItem {
size?: number;
sourceType: IOmnichannelSource;
hideMentionStatus?: boolean;
touchableRef: React.RefObject<ITouchableRef>;
}
export interface ILastMessageProps {
@ -127,14 +127,11 @@ export interface ILastMessageProps {
alert: boolean;
}
export interface ITouchableRef {
close: () => void;
}
export interface ITouchableProps extends IRoomItemTouchables {
children: JSX.Element;
type: SubscriptionType;
testID: string;
width: number;
favorite: boolean;
isRead: boolean;
rid: string;

View File

@ -0,0 +1,30 @@
import { TUserStatus } from '../../definitions';
import { useAppSelector } from '../../lib/hooks';
import { RoomTypes } from '../../lib/methods';
export const useUserStatus = (
type: RoomTypes,
liveChatStatus?: TUserStatus,
id?: string
): { connected: boolean; status: TUserStatus } => {
const connected = useAppSelector(state => state.meteor.connected);
const presenceDisabled = useAppSelector(state => state.settings.Presence_broadcast_disabled);
const userStatus = useAppSelector(state => state.activeUsers[id || '']?.status);
let status = 'loading';
if (connected) {
if (type === 'd') {
if (presenceDisabled) {
status = 'disabled';
} else {
status = userStatus || 'loading';
}
} else if (type === 'l' && liveChatStatus) {
status = liveChatStatus;
}
}
return {
connected,
status: status as TUserStatus
};
};

View File

@ -1,4 +1,4 @@
import React from 'react';
import React, { useState } from 'react';
import { StyleProp, ViewStyle } from 'react-native';
import { SvgUri } from 'react-native-svg';
@ -29,22 +29,12 @@ interface IOmnichannelRoomIconProps {
}
export const OmnichannelRoomIcon = ({ size, style, sourceType, status }: IOmnichannelRoomIconProps) => {
const [loading, setLoading] = useState(true);
const [svgError, setSvgError] = useState(false);
const baseUrl = useAppSelector(state => state.server?.server);
const connected = useAppSelector(state => state.meteor?.connected);
if (sourceType?.type === OmnichannelSourceType.APP && sourceType.id && sourceType.sidebarIcon && connected) {
return (
<SvgUri
height={size}
width={size}
color={STATUS_COLORS[status || 'offline']}
uri={`${baseUrl}/api/apps/public/${sourceType.id}/get-sidebar-icon?icon=${sourceType.sidebarIcon}`}
style={style}
/>
);
}
return (
const customIcon = (
<CustomIcon
name={iconMap[sourceType?.type || 'other']}
size={size}
@ -52,4 +42,23 @@ export const OmnichannelRoomIcon = ({ size, style, sourceType, status }: IOmnich
color={STATUS_COLORS[status || 'offline']}
/>
);
if (!svgError && sourceType?.type === OmnichannelSourceType.APP && sourceType.id && sourceType.sidebarIcon && connected) {
return (
<>
<SvgUri
height={size}
width={size}
color={STATUS_COLORS[status || 'offline']}
uri={`${baseUrl}/api/apps/public/${sourceType.id}/get-sidebar-icon?icon=${sourceType.sidebarIcon}`}
style={style}
onError={() => setSvgError(true)}
onLoad={() => setLoading(false)}
/>
{loading ? customIcon : null}
</>
);
}
return customIcon;
};

View File

@ -6,9 +6,15 @@ import { IStatus } from './definition';
import { useAppSelector } from '../../lib/hooks';
const StatusContainer = ({ id, style, size = 32, ...props }: Omit<IStatus, 'status'>): React.ReactElement => {
const status = useAppSelector(state =>
state.meteor.connected ? state.activeUsers[id] && state.activeUsers[id].status : 'loading'
) as TUserStatus;
const status = useAppSelector(state => {
if (state.settings.Presence_broadcast_disabled) {
return 'disabled';
}
if (state.meteor.connected) {
return state.activeUsers[id] && state.activeUsers[id].status;
}
return 'loading';
}) as TUserStatus;
return <Status size={size} style={style} status={status} {...props} />;
};

View File

@ -6,7 +6,6 @@ import i18n from '../../../../i18n';
import { getSubscriptionByRoomId } from '../../../../lib/database/services/Subscription';
import { useAppSelector } from '../../../../lib/hooks';
import { getRoomAvatar, getUidDirectMessage } from '../../../../lib/methods/helpers';
import { videoConfStartAndJoin } from '../../../../lib/methods/videoConf';
import { useTheme } from '../../../../theme';
import { useActionSheet } from '../../../ActionSheet';
import AvatarContainer from '../../../Avatar';
@ -16,12 +15,12 @@ import { BUTTON_HIT_SLOP } from '../../../message/utils';
import StatusContainer from '../../../Status';
import useStyle from './styles';
export default function CallAgainActionSheet({ rid }: { rid: string }): React.ReactElement {
export default function StartACallActionSheet({ rid, initCall }: { rid: string; initCall: Function }): React.ReactElement {
const style = useStyle();
const { colors } = useTheme();
const [user, setUser] = useState({ username: '', avatar: '', uid: '', rid: '' });
const [phone, setPhone] = useState(true);
const [camera, setCamera] = useState(false);
const [user, setUser] = useState({ username: '', avatar: '', uid: '' });
const [mic, setMic] = useState(true);
const [cam, setCam] = useState(false);
const username = useAppSelector(state => state.login.user.username);
const { hideActionSheet } = useActionSheet();
@ -31,7 +30,7 @@ export default function CallAgainActionSheet({ rid }: { rid: string }): React.Re
const room = await getSubscriptionByRoomId(rid);
const uid = (await getUidDirectMessage(room)) as string;
const avt = getRoomAvatar(room);
setUser({ uid, username: room?.name || '', avatar: avt, rid: room?.id || '' });
setUser({ uid, username: room?.name || '', avatar: avt });
})();
}, [rid]);
@ -43,25 +42,27 @@ export default function CallAgainActionSheet({ rid }: { rid: string }): React.Re
<Text style={style.actionSheetHeaderTitle}>{i18n.t('Start_a_call')}</Text>
<View style={style.actionSheetHeaderButtons}>
<Touchable
onPress={() => setCamera(!camera)}
style={[style.iconCallContainer, camera && style.enabledBackground, { marginRight: 6 }]}
onPress={() => setCam(!cam)}
style={[style.iconCallContainer, cam && style.enabledBackground, { marginRight: 6 }]}
hitSlop={BUTTON_HIT_SLOP}
>
<CustomIcon name={camera ? 'camera' : 'camera-disabled'} size={16} color={handleColor(camera)} />
<CustomIcon name={cam ? 'camera' : 'camera-disabled'} size={20} color={handleColor(cam)} />
</Touchable>
<Touchable
onPress={() => setPhone(!phone)}
style={[style.iconCallContainer, phone && style.enabledBackground]}
onPress={() => setMic(!mic)}
style={[style.iconCallContainer, mic && style.enabledBackground]}
hitSlop={BUTTON_HIT_SLOP}
>
<CustomIcon name={phone ? 'microphone' : 'microphone-disabled'} size={16} color={handleColor(phone)} />
<CustomIcon name={mic ? 'microphone' : 'microphone-disabled'} size={20} color={handleColor(mic)} />
</Touchable>
</View>
</View>
<View style={style.actionSheetUsernameContainer}>
<AvatarContainer text={user.avatar} size={36} />
<StatusContainer size={16} id={user.uid} style={{ marginLeft: 8, marginRight: 6 }} />
<Text style={style.actionSheetUsername}>{user.username}</Text>
<Text style={style.actionSheetUsername} numberOfLines={1}>
{user.username}
</Text>
</View>
<View style={style.actionSheetPhotoContainer}>
<AvatarContainer size={62} text={username} />
@ -70,7 +71,7 @@ export default function CallAgainActionSheet({ rid }: { rid: string }): React.Re
onPress={() => {
hideActionSheet();
setTimeout(() => {
videoConfStartAndJoin(user.rid, camera);
initCall({ cam, mic });
}, 100);
}}
title={i18n.t('Call')}

View File

@ -3,17 +3,16 @@ import { Text } from 'react-native';
import Touchable from 'react-native-platform-touchable';
import i18n from '../../../../i18n';
import { useVideoConf } from '../../../../lib/hooks/useVideoConf';
import { videoConfJoin } from '../../../../lib/methods/videoConf';
import useStyle from './styles';
import { VideoConferenceBaseContainer } from './VideoConferenceBaseContainer';
const VideoConferenceDirect = React.memo(({ blockId }: { blockId: string }) => {
const style = useStyle();
const { joinCall } = useVideoConf();
return (
<VideoConferenceBaseContainer variant='incoming'>
<Touchable style={style.callToActionButton} onPress={() => joinCall(blockId)}>
<Touchable style={style.callToActionButton} onPress={() => videoConfJoin(blockId)}>
<Text style={style.callToActionButtonText}>{i18n.t('Join')}</Text>
</Touchable>
<Text style={style.callBack}>{i18n.t('Waiting_for_answer')}</Text>

View File

@ -6,9 +6,7 @@ import { IUser } from '../../../../definitions';
import { VideoConferenceType } from '../../../../definitions/IVideoConference';
import i18n from '../../../../i18n';
import { useAppSelector } from '../../../../lib/hooks';
import { useSnaps } from '../../../../lib/hooks/useSnaps';
import { useActionSheet } from '../../../ActionSheet';
import CallAgainActionSheet from './CallAgainActionSheet';
import { useVideoConf } from '../../../../lib/hooks/useVideoConf';
import { CallParticipants, TCallUsers } from './CallParticipants';
import useStyle from './styles';
import { VideoConferenceBaseContainer } from './VideoConferenceBaseContainer';
@ -26,8 +24,7 @@ export default function VideoConferenceEnded({
}): React.ReactElement {
const style = useStyle();
const username = useAppSelector(state => state.login.user.username);
const { showActionSheet } = useActionSheet();
const snaps = useSnaps([1250]);
const { showInitCallActionSheet } = useVideoConf(rid);
const onlyAuthorOnCall = users.length === 1 && users.some(user => user.username === createdBy.username);
@ -35,15 +32,7 @@ export default function VideoConferenceEnded({
<VideoConferenceBaseContainer variant='ended'>
{type === 'direct' ? (
<>
<Touchable
style={style.callToActionCallBack}
onPress={() =>
showActionSheet({
children: <CallAgainActionSheet rid={rid} />,
snaps
})
}
>
<Touchable style={style.callToActionCallBack} onPress={showInitCallActionSheet}>
<Text style={style.callToActionCallBackText}>
{createdBy.username === username ? i18n.t('Call_back') : i18n.t('Call_again')}
</Text>

View File

@ -3,18 +3,17 @@ import { Text } from 'react-native';
import Touchable from 'react-native-platform-touchable';
import i18n from '../../../../i18n';
import { useVideoConf } from '../../../../lib/hooks/useVideoConf';
import { videoConfJoin } from '../../../../lib/methods/videoConf';
import { CallParticipants, TCallUsers } from './CallParticipants';
import useStyle from './styles';
import { VideoConferenceBaseContainer } from './VideoConferenceBaseContainer';
export default function VideoConferenceOutgoing({ users, blockId }: { users: TCallUsers; blockId: string }): React.ReactElement {
const style = useStyle();
const { joinCall } = useVideoConf();
return (
<VideoConferenceBaseContainer variant='outgoing'>
<Touchable style={style.callToActionButton} onPress={() => joinCall(blockId)}>
<Touchable style={style.callToActionButton} onPress={() => videoConfJoin(blockId)}>
<Text style={style.callToActionButtonText}>{i18n.t('Join')}</Text>
</Touchable>
<CallParticipants users={users} />

View File

@ -100,7 +100,8 @@ export default function useStyle() {
actionSheetUsername: {
fontSize: 16,
...sharedStyles.textBold,
color: colors.passcodePrimary
color: colors.passcodePrimary,
flexShrink: 1
},
enabledBackground: {
backgroundColor: colors.conferenceCallEnabledIconBackground

View File

@ -2,7 +2,7 @@
import { BlockContext } from '@rocket.chat/ui-kit';
import React, { useContext, useState } from 'react';
import { useVideoConf } from '../../lib/hooks/useVideoConf';
import { videoConfJoin } from '../../lib/methods/videoConf';
import { IText } from './interfaces';
export const textParser = ([{ text }]: IText[]) => text;
@ -40,7 +40,6 @@ export const useBlockContext = ({ blockId, actionId, appId, initialValue }: IUse
const { action, appId: appIdFromContext, viewId, state, language, errors, values = {} } = useContext(KitContext);
const { value = initialValue } = values[actionId] || {};
const [loading, setLoading] = useState(false);
const { joinCall } = useVideoConf();
const error = errors && actionId && errors[actionId];
@ -58,7 +57,7 @@ export const useBlockContext = ({ blockId, actionId, appId, initialValue }: IUse
try {
if (appId === 'videoconf-core' && blockId) {
setLoading(false);
return joinCall(blockId);
return videoConfJoin(blockId);
}
await action({
blockId,

View File

@ -0,0 +1,64 @@
import React from 'react';
import { View, StyleSheet, Text, ViewStyle } from 'react-native';
import sharedStyles from '../views/Styles';
import { useTheme } from '../theme';
import openLink from '../lib/methods/helpers/openLink';
import { useAppSelector } from '../lib/hooks';
import I18n from '../i18n';
const styles = StyleSheet.create({
bottomContainer: {
flexDirection: 'column',
alignItems: 'center',
marginBottom: 32,
marginHorizontal: 30
},
bottomContainerText: {
...sharedStyles.textRegular,
fontSize: 13,
textAlign: 'center'
},
bottomContainerTextBold: {
...sharedStyles.textSemibold,
fontSize: 13,
textAlign: 'center'
}
});
const UGCRules = ({ styleContainer }: { styleContainer?: ViewStyle }) => {
const { colors, theme } = useTheme();
const { server } = useAppSelector(state => ({
server: state.server.server
}));
const openContract = (route: string) => {
if (!server) {
return;
}
openLink(`${server}/${route}`, theme);
};
return (
<View style={[styles.bottomContainer, styleContainer]}>
<Text style={[styles.bottomContainerText, { color: colors.auxiliaryText }]}>
{`${I18n.t('Onboarding_agree_terms')}\n`}
<Text
style={[styles.bottomContainerTextBold, { color: colors.actionTintColor }]}
onPress={() => openContract('terms-of-service')}
>
{I18n.t('Terms_of_Service')}
</Text>{' '}
{I18n.t('and')}
<Text
style={[styles.bottomContainerTextBold, { color: colors.actionTintColor }]}
onPress={() => openContract('privacy-policy')}
>
{' '}
{I18n.t('Privacy_Policy')}
</Text>
</Text>
</View>
);
};
export default UGCRules;

View File

@ -18,13 +18,29 @@ import MarkdownContext from './MarkdownContext';
interface IParagraphProps {
value: ParagraphProps['value'];
forceTrim?: boolean;
}
const Inline = ({ value }: IParagraphProps): React.ReactElement | null => {
const Inline = ({ value, forceTrim }: IParagraphProps): React.ReactElement | null => {
const { useRealName, username, navToRoomInfo, mentions, channels } = useContext(MarkdownContext);
return (
<Text style={styles.inline}>
{value.map(block => {
{value.map((block, index) => {
// We are forcing trim when is a `[ ](https://https://open.rocket.chat/) plain_text`
// to clean the empty spaces
if (forceTrim) {
if (index === 0 && block.type === 'LINK') {
block.value.label.value =
// Need to update the @rocket.chat/message-parser to understand that the label can be a Markup | Markup[]
// https://github.com/RocketChat/fuselage/blob/461ecf661d9ff4a46390957c915e4352fa942a7c/packages/message-parser/src/definitions.ts#L141
// @ts-ignore
block.value?.label?.value?.toString().trimLeft() || block?.value?.label?.[0]?.value?.toString().trimLeft();
}
if (index === 1 && block.type !== 'LINK') {
block.value = block.value?.toString().trimLeft();
}
}
switch (block.type) {
case 'IMAGE':
return <Image value={block.value} />;

View File

@ -381,27 +381,6 @@ export const BlockQuote = () => (
</View>
);
const rocketChatLink = [
{
type: 'PARAGRAPH',
value: [
{
type: 'LINK',
value: {
src: {
type: 'PLAIN_TEXT',
value: 'https://rocket.chat'
},
label: {
type: 'PLAIN_TEXT',
value: 'https://rocket.chat'
}
}
}
]
}
];
const markdownLink = [
{
type: 'PARAGRAPH',
@ -487,7 +466,6 @@ const markdownLinkWithEmphasis = [
export const Links = () => (
<View style={styles.container}>
<NewMarkdown tokens={rocketChatLink} />
<NewMarkdown tokens={markdownLink} />
<NewMarkdown tokens={markdownLinkWithEmphasis} />
</View>
@ -806,3 +784,128 @@ export const InlineKatex = () => (
<NewMarkdown tokens={inlineKatex} />
</View>
);
const messageQuote = {
/**
# Hello head 1
[ ](https://google.com)
*/
headAndLink: [
{ type: 'HEADING', level: 1, value: [{ type: 'PLAIN_TEXT', value: 'Hello head 1' }] },
{ type: 'LINE_BREAK' },
{
type: 'PARAGRAPH',
value: [
{
type: 'LINK',
value: { src: { type: 'PLAIN_TEXT', value: 'https://google.com' }, label: { type: 'PLAIN_TEXT', value: ' ' } }
}
]
}
],
/**
# Head 1 as the first line then line break and after paragraph
bla bla bla bla bla bla
bla bla bla bla bla bla
[ ](https://google.com)
*/
headTextAndLink: [
{
type: 'HEADING',
level: 1,
value: [{ type: 'PLAIN_TEXT', value: 'Head 1 as the first line then line break and after paragraph' }]
},
{ type: 'LINE_BREAK' },
{ type: 'PARAGRAPH', value: [{ type: 'PLAIN_TEXT', value: 'bla bla bla bla bla bla ' }] },
{ type: 'PARAGRAPH', value: [{ type: 'PLAIN_TEXT', value: 'bla bla bla bla bla bla ' }] },
{
type: 'PARAGRAPH',
value: [
{
type: 'LINK',
value: { src: { type: 'PLAIN_TEXT', value: 'https://google.com' }, label: { type: 'PLAIN_TEXT', value: ' ' } }
}
]
}
],
/**
[ ](permalink from message)\n# Head 1 after a forced line break
asdas asd asd asd
*/
headTextAndQuote: [
{
type: 'PARAGRAPH',
value: [
{
type: 'LINK',
value: {
src: { type: 'PLAIN_TEXT', value: 'https://open.rocket.chat/direct/subaru123?msg=QB42gWcaO6BgqtLTo' },
label: { type: 'PLAIN_TEXT', value: ' ' }
}
},
{ type: 'PLAIN_TEXT', value: ' ' }
]
},
{ type: 'HEADING', level: 1, value: [{ type: 'PLAIN_TEXT', value: 'Head 1 after a forced line break' }] },
{ type: 'LINE_BREAK' },
{ type: 'PARAGRAPH', value: [{ type: 'PLAIN_TEXT', value: 'Description' }] }
],
/**
[ ](https://google.com) *There is a link before this bold separated by single space*
*/
linkAndBoldText: [
{
type: 'PARAGRAPH',
value: [
{
type: 'LINK',
value: { src: { type: 'PLAIN_TEXT', value: 'https://google.com' }, label: { type: 'PLAIN_TEXT', value: ' ' } }
},
{ type: 'PLAIN_TEXT', value: ' ' },
{ type: 'BOLD', value: [{ type: 'PLAIN_TEXT', value: 'There is a link before this bold separated by single space' }] }
]
}
],
simpleQuote: [
{
type: 'PARAGRAPH',
value: [
{
type: 'LINK',
value: {
src: {
type: 'PLAIN_TEXT',
value: 'https://open.rocket.chat/group/quoteeee9798789?msg=ZZp6t2dCRX4TqExht'
},
// format of label for servers greater or equal than 6.0
label: [
{
type: 'PLAIN_TEXT',
value: ' '
}
]
}
}
]
},
{
type: 'PARAGRAPH',
value: [
{
type: 'PLAIN_TEXT',
value: 'Quoting a message wrote before'
}
]
}
]
};
export const MessageQuote = () => (
<View style={styles.container}>
<NewMarkdown tokens={messageQuote.headAndLink} />
<NewMarkdown tokens={messageQuote.headTextAndLink} />
<NewMarkdown tokens={messageQuote.headTextAndQuote} />
<NewMarkdown tokens={messageQuote.linkAndBoldText} />
<NewMarkdown tokens={messageQuote.simpleQuote} />
</View>
);

View File

@ -12,10 +12,28 @@ interface IParagraphProps {
}
const Paragraph = ({ value }: IParagraphProps) => {
let forceTrim = false;
const { theme } = useTheme();
if (
value?.[0]?.type === 'LINK' &&
// Need to update the @rocket.chat/message-parser to understand that the label can be a Markup | Markup[]
// https://github.com/RocketChat/fuselage/blob/461ecf661d9ff4a46390957c915e4352fa942a7c/packages/message-parser/src/definitions.ts#L141
// @ts-ignore
(value?.[0]?.value?.label?.value?.toString().trim() === '' || value?.[0]?.value?.label?.[0]?.value?.toString().trim() === '')
) {
// We are returning null when we receive a message like this: `[ ](https://open.rocket.chat/)\nplain_text`
// to avoid render a line empty above the the message
if (value.length === 1) {
return null;
}
if (value.length === 2 && value?.[1]?.type === 'PLAIN_TEXT' && value?.[1]?.value?.toString().trim() === '') {
return null;
}
forceTrim = true;
}
return (
<Text style={[styles.text, { color: themes[theme].bodyText }]}>
<Inline value={value} />
<Inline value={value} forceTrim={forceTrim} />
</Text>
);
};

View File

@ -8,7 +8,7 @@ interface IPlainProps {
value: PlainProps['value'];
}
const Plain = ({ value }: IPlainProps) => (
const Plain = ({ value }: IPlainProps): React.ReactElement => (
<Text accessibilityLabel={value} style={styles.plainText}>
{value}
</Text>

View File

@ -4,7 +4,6 @@ import { Tasks as TasksProps } from '@rocket.chat/message-parser';
import Inline from './Inline';
import styles from '../styles';
import { themes } from '../../../lib/constants';
import { useTheme } from '../../../theme';
interface ITasksProps {
@ -12,13 +11,15 @@ interface ITasksProps {
}
const TaskList = ({ value = [] }: ITasksProps) => {
const { theme } = useTheme();
const { colors } = useTheme();
return (
<View>
{value.map(item => (
<View style={styles.row}>
<Text style={[styles.text, { color: themes[theme].bodyText }]}>{item.status ? '- [x] ' : '- [ ] '}</Text>
<Inline value={item.value} />
<Text style={[styles.text, { color: colors.bodyText }]}>{item.status ? '- [x] ' : '- [ ] '}</Text>
<Text style={[styles.inline, { color: colors.bodyText }]}>
<Inline value={item.value} />
</Text>
</View>
))}
</View>

View File

@ -68,7 +68,7 @@ const Attachments: React.FC<IMessageAttachments> = React.memo(
if (file && file.image_url) {
return (
<Image
key={index}
key={file.image_url}
file={file}
showAttachment={showAttachment}
getCustomEmoji={getCustomEmoji}
@ -81,7 +81,7 @@ const Attachments: React.FC<IMessageAttachments> = React.memo(
if (file && file.audio_url) {
return (
<Audio
key={index}
key={file.audio_url}
file={file}
getCustomEmoji={getCustomEmoji}
isReply={isReply}
@ -95,7 +95,7 @@ const Attachments: React.FC<IMessageAttachments> = React.memo(
if (file.video_url) {
return (
<Video
key={index}
key={file.video_url}
file={file}
showAttachment={showAttachment}
getCustomEmoji={getCustomEmoji}

View File

@ -10,12 +10,12 @@ import { themes } from '../../lib/constants';
import { IMessageCallButton } from './interfaces';
import { useTheme } from '../../theme';
const CallButton = React.memo(({ callJitsi }: IMessageCallButton) => {
const CallButton = React.memo(({ handleEnterCall }: IMessageCallButton) => {
const { theme } = useTheme();
return (
<View style={styles.buttonContainer}>
<Touchable
onPress={callJitsi}
onPress={handleEnterCall}
background={Touchable.Ripple(themes[theme].bannerBackground)}
style={[styles.button, { backgroundColor: themes[theme].tintColor }]}
hitSlop={BUTTON_HIT_SLOP}

View File

@ -90,8 +90,8 @@ const Fields = React.memo(
return (
<>
{attachment.fields.map((field, index) => (
<View key={index} style={[styles.fieldContainer, { width: field.short ? '50%' : '100%' }]}>
{attachment.fields.map(field => (
<View key={field.title} style={[styles.fieldContainer, { width: field.short ? '50%' : '100%' }]}>
<Text testID='collapsibleQuoteTouchableFieldTitle' style={[styles.fieldTitle, { color: themes[theme].bodyText }]}>
{field.title}
</Text>

View File

@ -1,14 +1,20 @@
import React from 'react';
import { themes } from '../../../../lib/constants';
import { CustomIcon } from '../../../CustomIcon';
import styles from '../../styles';
import { useTheme } from '../../../../theme';
const ReadReceipt = React.memo(({ isReadReceiptEnabled, unread }: { isReadReceiptEnabled?: boolean; unread?: boolean }) => {
const { theme } = useTheme();
if (isReadReceiptEnabled && !unread && unread !== null) {
return <CustomIcon name='check' color={themes[theme].tintColor} size={16} style={styles.rightIcons} />;
const { colors } = useTheme();
if (isReadReceiptEnabled) {
return (
<CustomIcon
name='check'
color={!unread && unread !== null ? colors.tintColor : colors.auxiliaryTintColor}
size={16}
style={styles.rightIcons}
/>
);
}
return null;
});

View File

@ -53,7 +53,7 @@ const Content = React.memo(
content = (
<Markdown
msg={props.msg}
md={props.md}
md={props.type !== 'e2e' ? props.md : undefined}
getCustomEmoji={props.getCustomEmoji}
enableMessageParser={user.enableMessageParserEarlyAdoption}
username={user.username}

View File

@ -10,7 +10,7 @@ const Emoji = React.memo(
const parsedContent = content.replace(/^:|:$/g, '');
const emoji = getCustomEmoji(parsedContent);
if (emoji) {
return <CustomEmoji style={customEmojiStyle} emoji={emoji} />;
return <CustomEmoji key={content} style={customEmojiStyle} emoji={emoji} />;
}
return <Text style={standardEmojiStyle}>{shortnameToUnicode(content)}</Text>;
},

View File

@ -18,7 +18,7 @@ const MessageAvatar = React.memo(({ isHeader, avatar, author, small, navToRoomIn
style={small ? styles.avatarSmall : styles.avatar}
text={avatar ? '' : author.username}
size={small ? 20 : 36}
borderRadius={small ? 2 : 4}
borderRadius={4}
onPress={author._id === user.id ? undefined : () => navToRoomInfo(navParam)}
getCustomEmoji={getCustomEmoji}
avatar={avatar}

View File

@ -53,6 +53,7 @@ const Reaction = React.memo(({ reaction, getCustomEmoji, theme }: IMessageReacti
<Touchable
onPress={() => onReactionPress(reaction.emoji)}
onLongPress={onReactionLongPress}
key={reaction.emoji}
testID={`message-reaction-${reaction.emoji}`}
style={[
styles.reactionButton,
@ -82,8 +83,8 @@ const Reactions = React.memo(({ reactions, getCustomEmoji }: IMessageReactions)
}
return (
<View style={styles.reactionsContainer}>
{reactions.map((reaction, index) => (
<Reaction key={index} reaction={reaction} getCustomEmoji={getCustomEmoji} theme={theme} />
{reactions.map(reaction => (
<Reaction key={reaction.emoji} reaction={reaction} getCustomEmoji={getCustomEmoji} theme={theme} />
))}
<AddReaction theme={theme} />
</View>

View File

@ -184,8 +184,8 @@ const Fields = React.memo(
return (
<View style={styles.fieldsContainer}>
{attachment.fields.map((field, index) => (
<View key={index} style={[styles.fieldContainer, { width: field.short ? '50%' : '100%' }]}>
{attachment.fields.map(field => (
<View key={field.title} style={[styles.fieldContainer, { width: field.short ? '50%' : '100%' }]}>
<Text style={[styles.fieldTitle, { color: themes[theme].bodyText }]}>{field.title}</Text>
<Markdown msg={field?.value || ''} username={user.username} getCustomEmoji={getCustomEmoji} theme={theme} />
</View>
@ -247,6 +247,8 @@ const Reply = React.memo(
>
<View style={styles.attachmentContainer}>
<Title attachment={attachment} timeFormat={timeFormat} theme={theme} />
<Description attachment={attachment} getCustomEmoji={getCustomEmoji} theme={theme} />
<UrlImage image={attachment.thumb_url} />
<Attachments
attachments={attachment.attachments}
getCustomEmoji={getCustomEmoji}
@ -255,8 +257,6 @@ const Reply = React.memo(
isReply
id={messageId}
/>
<UrlImage image={attachment.thumb_url} />
<Description attachment={attachment} getCustomEmoji={getCustomEmoji} theme={theme} />
<Fields attachment={attachment} getCustomEmoji={getCustomEmoji} theme={theme} />
{loading ? (
<View style={[styles.backdrop]}>

View File

@ -141,7 +141,7 @@ const Urls = React.memo(
return null;
}
return urls.map((url: IUrl, index: number) => <Url url={url} key={index} index={index} theme={theme} />);
return urls.map((url: IUrl, index: number) => <Url url={url} key={url.url} index={index} theme={theme} />);
},
(oldProps, newProps) => dequal(oldProps.urls, newProps.urls)
);

View File

@ -1,5 +1,6 @@
import React from 'react';
import { Keyboard } from 'react-native';
import { Keyboard, ViewStyle } from 'react-native';
import { Subscription } from 'rxjs';
import Message from './Message';
import MessageContext from './Context';
@ -7,15 +8,67 @@ import { debounce } from '../../lib/methods/helpers';
import { getMessageTranslation } from './utils';
import { TSupportedThemes, withTheme } from '../../theme';
import openLink from '../../lib/methods/helpers/openLink';
import { IAttachment } from '../../definitions';
import { IAttachment, TAnyMessageModel, TGetCustomEmoji } from '../../definitions';
import { IRoomInfoParam } from '../../views/SearchMessagesView';
import { E2E_MESSAGE_TYPE, E2E_STATUS, messagesStatus } from '../../lib/constants';
import { IMessageContainerProps, TAnyMessageContainerState } from './interfaces';
class MessageContainer extends React.Component<IMessageContainerProps, TAnyMessageContainerState> {
interface IMessageContainerProps {
item: TAnyMessageModel;
user: {
id: string;
username: string;
token: string;
};
msg?: string;
rid: string;
timeFormat?: string;
style?: ViewStyle;
archived?: boolean;
broadcast?: boolean;
previousItem?: TAnyMessageModel;
baseUrl: string;
Message_GroupingPeriod?: number;
isReadReceiptEnabled?: boolean;
isThreadRoom: boolean;
isSystemMessage?: boolean;
useRealName?: boolean;
autoTranslateRoom?: boolean;
autoTranslateLanguage?: string;
status?: number;
isIgnored?: boolean;
highlighted?: boolean;
getCustomEmoji: TGetCustomEmoji;
onLongPress?: (item: TAnyMessageModel) => void;
onReactionPress?: (emoji: string, id: string) => void;
onEncryptedPress?: () => void;
onDiscussionPress?: (item: TAnyMessageModel) => void;
onThreadPress?: (item: TAnyMessageModel) => void;
errorActionsShow?: (item: TAnyMessageModel) => void;
replyBroadcast?: (item: TAnyMessageModel) => void;
reactionInit?: (item: TAnyMessageModel) => void;
fetchThreadName?: (tmid: string, id: string) => Promise<string | undefined>;
showAttachment: (file: IAttachment) => void;
onReactionLongPress?: (item: TAnyMessageModel) => void;
navToRoomInfo: (navParam: IRoomInfoParam) => void;
handleEnterCall?: () => void;
blockAction?: (params: { actionId: string; appId: string; value: string; blockId: string; rid: string; mid: string }) => void;
onAnswerButtonPress?: (message: string, tmid?: string, tshow?: boolean) => void;
threadBadgeColor?: string;
toggleFollowThread?: (isFollowingThread: boolean, tmid?: string) => Promise<void>;
jumpToMessage?: (link: string) => void;
onPress?: () => void;
theme: TSupportedThemes;
closeEmojiAndAction?: (action?: Function, params?: any) => void;
}
interface IMessageContainerState {
isManualUnignored: boolean;
}
class MessageContainer extends React.Component<IMessageContainerProps, IMessageContainerState> {
static defaultProps = {
getCustomEmoji: () => null,
onLongPress: () => {},
callJitsi: () => {},
blockAction: () => {},
archived: false,
broadcast: false,
@ -25,6 +78,45 @@ class MessageContainer extends React.Component<IMessageContainerProps, TAnyMessa
state = { isManualUnignored: false };
private subscription?: Subscription;
componentDidMount() {
const { item } = this.props;
if (item && item.observe) {
const observable = item.observe();
this.subscription = observable.subscribe(() => {
this.forceUpdate();
});
}
}
shouldComponentUpdate(nextProps: IMessageContainerProps, nextState: IMessageContainerState) {
const { isManualUnignored } = this.state;
const { threadBadgeColor, isIgnored, highlighted, previousItem } = this.props;
if (nextProps.highlighted !== highlighted) {
return true;
}
if (nextProps.threadBadgeColor !== threadBadgeColor) {
return true;
}
if (nextProps.isIgnored !== isIgnored) {
return true;
}
if (nextState.isManualUnignored !== isManualUnignored) {
return true;
}
if (nextProps.previousItem?._id !== previousItem?._id) {
return true;
}
return false;
}
componentWillUnmount() {
if (this.subscription && this.subscription.unsubscribe) {
this.subscription.unsubscribe();
}
}
onPressAction = () => {
const { closeEmojiAndAction } = this.props;
@ -134,11 +226,11 @@ class MessageContainer extends React.Component<IMessageContainerProps, TAnyMessa
try {
if (
previousItem &&
// @ts-ignore TODO: TAnyMessage vs TAnyMessageFromServer non-sense
// @ts-ignore TODO: IMessage vs IMessageFromServer non-sense
previousItem.ts.toDateString() === item.ts.toDateString() &&
previousItem.u.username === item.u.username &&
!(previousItem.groupable === false || item.groupable === false || broadcast === true) &&
// @ts-ignore TODO: TAnyMessage vs TAnyMessageFromServer non-sense
// @ts-ignore TODO: IMessage vs IMessageFromServer non-sense
item.ts - previousItem.ts < Message_GroupingPeriod * 1000 &&
previousItem.tmid === item.tmid &&
item.t !== 'rm' &&
@ -245,7 +337,7 @@ class MessageContainer extends React.Component<IMessageContainerProps, TAnyMessa
navToRoomInfo,
getCustomEmoji,
isThreadRoom,
callJitsi,
handleEnterCall,
blockAction,
rid,
threadBadgeColor,
@ -316,6 +408,7 @@ class MessageContainer extends React.Component<IMessageContainerProps, TAnyMessa
replies
}}
>
{/* @ts-ignore*/}
<Message
id={id}
msg={message}
@ -362,7 +455,7 @@ class MessageContainer extends React.Component<IMessageContainerProps, TAnyMessa
showAttachment={showAttachment}
getCustomEmoji={getCustomEmoji}
navToRoomInfo={navToRoomInfo}
callJitsi={callJitsi}
handleEnterCall={handleEnterCall}
blockAction={blockAction}
highlighted={highlighted}
comment={comment}

View File

@ -1,65 +1,11 @@
import { MarkdownAST } from '@rocket.chat/message-parser';
import { StyleProp, TextStyle, ViewStyle } from 'react-native';
import { StyleProp, TextStyle } from 'react-native';
import { ImageStyle } from 'react-native-fast-image';
import { IUserChannel } from '../markdown/interfaces';
import { TGetCustomEmoji } from '../../definitions/IEmoji';
import { IAttachment, IThread, IUrl, IUserMention, IUserMessage, MessageType, TAnyMessage } from '../../definitions';
import { IAttachment, IThread, IUrl, IUserMention, IUserMessage, MessageType, TAnyMessageModel } from '../../definitions';
import { IRoomInfoParam } from '../../views/SearchMessagesView';
import { TSupportedThemes } from '../../theme';
export interface IMessageContainerProps {
item: TAnyMessage;
user: {
id: string;
username: string;
token: string;
};
msg?: string;
rid: string;
timeFormat?: string;
style?: ViewStyle;
archived?: boolean;
broadcast?: boolean;
previousItem?: TAnyMessage;
baseUrl: string;
Message_GroupingPeriod?: number;
isReadReceiptEnabled?: boolean;
isThreadRoom: boolean;
isSystemMessage?: boolean;
useRealName?: boolean;
autoTranslateRoom?: boolean;
autoTranslateLanguage?: string;
status?: number;
isIgnored?: boolean;
highlighted?: boolean;
getCustomEmoji: TGetCustomEmoji;
onLongPress?: (item: TAnyMessage) => void;
onReactionPress?: (emoji: string, id: string) => void;
onEncryptedPress?: () => void;
onDiscussionPress?: (item: TAnyMessage) => void;
onThreadPress?: (item: TAnyMessage) => void;
errorActionsShow?: (item: TAnyMessage) => void;
replyBroadcast?: (item: TAnyMessage) => void;
reactionInit?: (item: TAnyMessage) => void;
fetchThreadName?: (tmid: string, id: string) => Promise<string | undefined>;
showAttachment: (file: IAttachment) => void;
onReactionLongPress?: (item: TAnyMessage) => void;
navToRoomInfo: (navParam: IRoomInfoParam) => void;
callJitsi?: () => void;
blockAction?: (params: { actionId: string; appId: string; value: string; blockId: string; rid: string; mid: string }) => void;
onAnswerButtonPress?: (message: string, tmid?: string, tshow?: boolean) => void;
threadBadgeColor?: string;
toggleFollowThread?: (isFollowingThread: boolean, tmid?: string) => Promise<void>;
jumpToMessage?: (link: string) => void;
onPress?: () => void;
theme: TSupportedThemes;
closeEmojiAndAction?: (action?: Function, params?: any) => void;
}
export interface TAnyMessageContainerState {
isManualUnignored: boolean;
}
export interface IMessageAttachments {
attachments?: IAttachment[];
@ -94,10 +40,11 @@ export interface IMessageBroadcast {
}
export interface IMessageCallButton {
callJitsi?: () => void;
handleEnterCall?: () => void;
}
export interface IMessageContent {
_id: string;
isTemp: boolean;
isInfo: string | boolean;
tmid?: string;
@ -172,6 +119,7 @@ export interface IMessage extends IMessageRepliedThread, IMessageInner, IMessage
hasError: boolean;
style: any;
// style: ViewStyle;
onLongPress?: (item: TAnyMessageModel) => void;
isReadReceiptEnabled?: boolean;
unread?: boolean;
isIgnored: boolean;

View File

@ -1,5 +1,5 @@
/* eslint-disable complexity */
import { MessageTypesValues, TAnyMessage } from '../../definitions/IMessage';
import { MessageTypesValues, TMessageModel } from '../../definitions/IMessage';
import I18n from '../../i18n';
import { DISCUSSION } from './constants';
@ -183,7 +183,7 @@ export const getInfoMessage = ({ type, role, msg, author, comment }: TInfoMessag
}
};
export const getMessageTranslation = (message: TAnyMessage, autoTranslateLanguage: string): string | null => {
export const getMessageTranslation = (message: TMessageModel, autoTranslateLanguage: string): string | null => {
if (!autoTranslateLanguage) {
return null;
}

View File

@ -4,14 +4,15 @@ import { MarkdownAST } from '@rocket.chat/message-parser';
import { MessageTypeLoad } from '../lib/constants';
import { IAttachment } from './IAttachment';
import { IReaction } from './IReaction';
import { IThreadMessage } from './IThreadMessage';
import { IThread } from './IThread';
import { TThreadMessageModel } from './IThreadMessage';
import { TThreadModel } from './IThread';
import { IUrl, IUrlFromServer } from './IUrl';
export type MessageType =
| 'jitsi_call_started'
| 'discussion-created'
| 'e2e'
| 'load_more'
| 'rm'
| 'uj'
| MessageTypeLoad
@ -146,12 +147,9 @@ export interface IMessage extends IMessageFromServer {
editedAt?: string | Date;
}
export type TMessageModel = IMessage &
Model & {
asPlain: () => IMessage;
};
export type TMessageModel = IMessage & Model;
export type TAnyMessage = IMessage | IThread | IThreadMessage;
export type TAnyMessageModel = TMessageModel | TThreadModel | TThreadMessageModel;
export type TTypeMessages = IMessageFromServer | ILoadMoreMessage | IMessage;
// Read receipts to ReadReceiptView and chat.getMessageReadReceipts

View File

@ -91,11 +91,13 @@ export interface ISubscription {
livechatData?: any;
tags?: string[];
E2EKey?: string;
E2ESuggestedKey?: string;
encrypted?: boolean;
e2eKeyId?: string;
avatarETag?: string;
teamId?: string;
teamMain?: boolean;
unsubscribe: () => Promise<any>;
separator?: boolean;
onHold?: boolean;
source?: IOmnichannelSource;
@ -107,10 +109,7 @@ export interface ISubscription {
uploads: RelationModified<TUploadModel>;
}
export type TSubscriptionModel = ISubscription &
Model & {
asPlain: () => ISubscription;
};
export type TSubscriptionModel = ISubscription & Model;
export type TSubscription = TSubscriptionModel | ISubscription;
// https://github.com/RocketChat/Rocket.Chat/blob/a88a96fcadd925b678ff27ada37075e029f78b5e/definition/ISubscription.ts#L8
@ -147,6 +146,7 @@ export interface IServerSubscription extends IRocketChatRecord {
onHold?: boolean;
encrypted?: boolean;
E2EKey?: string;
E2ESuggestedKey?: string;
unreadAlert?: 'default' | 'all' | 'mentions' | 'nothing';
fname?: unknown;

View File

@ -38,7 +38,4 @@ export interface IThread extends IMessage {
draftMessage?: string;
}
export type TThreadModel = IThread &
Model & {
asPlain: () => IThread;
};
export type TThreadModel = IThread & Model;

View File

@ -6,7 +6,4 @@ export interface IThreadMessage extends IMessage {
tmsg?: string;
}
export type TThreadMessageModel = IThreadMessage &
Model & {
asPlain: () => IThreadMessage;
};
export type TThreadMessageModel = IThreadMessage & Model;

View File

@ -5,6 +5,7 @@ export interface IUpload {
rid?: string;
path: string;
name?: string;
tmid?: string;
description?: string;
size: number;
type?: string;
@ -12,6 +13,7 @@ export interface IUpload {
progress?: number;
error?: boolean;
subscription?: { id: string };
msg?: string;
}
export type TUploadModel = IUpload & Model;

View File

@ -4,37 +4,34 @@ import type { IRoom } from './IRoom';
import type { IUser } from './IUser';
import type { IMessage } from './IMessage';
export enum VideoConferenceStatus {
export declare enum VideoConferenceStatus {
CALLING = 0,
STARTED = 1,
EXPIRED = 2,
ENDED = 3,
DECLINED = 4
}
export type DirectCallInstructions = {
export declare type DirectCallInstructions = {
type: 'direct';
callee: IUser['_id'];
calleeId: IUser['_id'];
callId: string;
};
export type ConferenceInstructions = {
export declare type ConferenceInstructions = {
type: 'videoconference';
callId: string;
rid: IRoom['_id'];
};
export type LivechatInstructions = {
export declare type LivechatInstructions = {
type: 'livechat';
callId: string;
};
export type VideoConferenceType = DirectCallInstructions['type'] | ConferenceInstructions['type'] | LivechatInstructions['type'];
export declare type VideoConferenceType =
| DirectCallInstructions['type']
| ConferenceInstructions['type']
| LivechatInstructions['type'];
export interface IVideoConferenceUser extends Pick<Required<IUser>, '_id' | 'username' | 'name' | 'avatarETag'> {
ts: Date;
}
export interface IVideoConference extends IRocketChatRecord {
type: VideoConferenceType;
rid: string;
@ -45,51 +42,68 @@ export interface IVideoConference extends IRocketChatRecord {
ended?: IMessage['_id'];
};
url?: string;
createdBy: Pick<IUser, '_id' | 'username' | 'name'>;
createdBy: Pick<Required<IUser>, '_id' | 'username' | 'name'>;
createdAt: Date;
endedBy?: Pick<IUser, '_id' | 'username' | 'name'>;
endedBy?: Pick<Required<IUser>, '_id' | 'username' | 'name'>;
endedAt?: Date;
providerName: string;
providerData?: Record<string, any>;
ringing?: boolean;
}
export interface IDirectVideoConference extends IVideoConference {
type: 'direct';
}
export interface IGroupVideoConference extends IVideoConference {
type: 'videoconference';
anonymousUsers: number;
title: string;
}
export interface ILivechatVideoConference extends IVideoConference {
type: 'livechat';
}
export type VideoConference = IDirectVideoConference | IGroupVideoConference | ILivechatVideoConference;
export type VideoConferenceInstructions = DirectCallInstructions | ConferenceInstructions | LivechatInstructions;
export const isDirectVideoConference = (call: VideoConference | undefined | null): call is IDirectVideoConference =>
call?.type === 'direct';
export const isGroupVideoConference = (call: VideoConference | undefined | null): call is IGroupVideoConference =>
call?.type === 'videoconference';
export const isLivechatVideoConference = (call: VideoConference | undefined | null): call is ILivechatVideoConference =>
call?.type === 'livechat';
type GroupVideoConferenceCreateData = Omit<IGroupVideoConference, 'createdBy'> & { createdBy: IUser['_id'] };
type DirectVideoConferenceCreateData = Omit<IDirectVideoConference, 'createdBy'> & { createdBy: IUser['_id'] };
type LivechatVideoConferenceCreateData = Omit<ILivechatVideoConference, 'createdBy'> & { createdBy: IUser['_id'] };
export type VideoConferenceCreateData = AtLeast<
export declare type VideoConference = IDirectVideoConference | IGroupVideoConference | ILivechatVideoConference;
export declare type VideoConferenceInstructions = DirectCallInstructions | ConferenceInstructions | LivechatInstructions;
export declare const isDirectVideoConference: (call: VideoConference | undefined | null) => call is IDirectVideoConference;
export declare const isGroupVideoConference: (call: VideoConference | undefined | null) => call is IGroupVideoConference;
export declare const isLivechatVideoConference: (call: VideoConference | undefined | null) => call is ILivechatVideoConference;
declare type GroupVideoConferenceCreateData = Omit<IGroupVideoConference, 'createdBy'> & {
createdBy: IUser['_id'];
};
declare type DirectVideoConferenceCreateData = Omit<IDirectVideoConference, 'createdBy'> & {
createdBy: IUser['_id'];
};
declare type LivechatVideoConferenceCreateData = Omit<ILivechatVideoConference, 'createdBy'> & {
createdBy: IUser['_id'];
};
export declare type VideoConferenceCreateData = AtLeast<
DirectVideoConferenceCreateData | GroupVideoConferenceCreateData | LivechatVideoConferenceCreateData,
'createdBy' | 'type' | 'rid' | 'providerName' | 'providerData'
>;
export type VideoConferenceCapabilities = {
mic?: boolean;
cam?: boolean;
title?: boolean;
};
export type VideoConfStartProps = { roomId: string; title?: string; allowRinging?: boolean };
export type VideoConfJoinProps = {
callId: string;
state?: {
mic?: boolean;
cam?: boolean;
};
};
export type VideoConfCancelProps = {
callId: string;
};
export type VideoConfListProps = {
roomId: string;
count?: number;
offset?: number;
};
export type VideoConfInfoProps = { callId: string };

View File

@ -1,3 +1,3 @@
export const STATUSES = ['offline', 'online', 'away', 'busy'] as const;
export const STATUSES = ['offline', 'online', 'away', 'busy', 'disabled'] as const;
export type TUserStatus = typeof STATUSES[number];

View File

@ -29,6 +29,7 @@ export * from './ISearch';
export * from './TUserStatus';
export * from './IProfile';
export * from './IReaction';
export * from './ERoomType';
export interface IBaseScreen<T extends Record<string, object | undefined>, S extends string> {
navigation: StackNavigationProp<T, S>;

View File

@ -12,6 +12,12 @@ export type E2eEndpoints = {
'e2e.updateGroupKey': {
POST: (params: { uid: string; rid: string; key: string }) => {};
};
'e2e.acceptSuggestedGroupKey': {
POST: (params: { rid: string }) => {};
};
'e2e.rejectSuggestedGroupKey': {
POST: (params: { rid: string }) => {};
};
'e2e.setRoomKeyID': {
POST: (params: { rid: string; keyID: string }) => {};
};

View File

@ -1,27 +1,45 @@
import { VideoConference } from '../../IVideoConference';
import {
VideoConfCancelProps,
VideoConference,
VideoConferenceCapabilities,
VideoConferenceInstructions,
VideoConfInfoProps,
VideoConfJoinProps,
VideoConfListProps,
VideoConfStartProps
} from '../../IVideoConference';
import { PaginatedResult } from '../helpers/PaginatedResult';
export type VideoConferenceEndpoints = {
'video-conference/jitsi.update-timeout': {
POST: (params: { roomId: string }) => void;
};
'video-conference.join': {
POST: (params: { callId: string; state: { cam: boolean } }) => { url: string; providerName: string };
};
'video-conference.start': {
POST: (params: { roomId: string }) => { url: string };
POST: (params: VideoConfStartProps) => { data: VideoConferenceInstructions & { providerName: string } };
};
'video-conference.join': {
POST: (params: VideoConfJoinProps) => { url: string; providerName: string };
};
'video-conference.cancel': {
POST: (params: { callId: string }) => void;
POST: (params: VideoConfCancelProps) => void;
};
'video-conference.info': {
GET: (params: { callId: string }) => VideoConference & {
capabilities: {
mic?: boolean;
cam?: boolean;
title?: boolean;
};
};
GET: (params: VideoConfInfoProps) => VideoConference & { capabilities: VideoConferenceCapabilities };
};
'video-conference.list': {
GET: (params: VideoConfListProps) => PaginatedResult<{ data: VideoConference[] }>;
};
'video-conference.capabilities': {
GET: () => { providerName: string; capabilities: VideoConferenceCapabilities };
};
'video-conference.providers': {
GET: () => { data: { key: string; label: string }[] };
};
'video-conference/jitsi.update-timeout': {
POST: (params: { roomId: string }) => void;
};
};

View File

@ -12,3 +12,6 @@ declare module 'react-native-restart';
declare module 'react-native-jitsi-meet';
declare module 'rn-root-view';
declare module 'react-native-math-view';
declare module '@env' {
export const RUNNING_E2E_TESTS: string;
}

View File

@ -876,5 +876,20 @@
"Call": "Call",
"Reply_in_direct_message": "Reply in Direct Message",
"room_archived": "archived room",
"room_unarchived": "unarchived room"
"room_unarchived": "unarchived room",
"no-videoconf-provider-app-header": "Conference call not available",
"no-videoconf-provider-app-body": "Conference call apps can be installed in the Rocket.Chat marketplace by a workspace admin.",
"admin-no-videoconf-provider-app-header": "Conference call not enabled",
"admin-no-videoconf-provider-app-body": "Conference call apps are available in the Rocket.Chat marketplace.",
"no-active-video-conf-provider-header": "Conference call not enabled",
"no-active-video-conf-provider-body": "A workspace admin needs to enable the conference call feature first.",
"admin-no-active-video-conf-provider-header": "Conference call not enabled",
"admin-no-active-video-conf-provider-body": "Configure conference calls in order to make it available on this workspace.",
"video-conf-provider-not-configured-header": "Conference call not enabled",
"video-conf-provider-not-configured-body": "A workspace admin needs to enable the conference calls feature first.",
"admin-video-conf-provider-not-configured-header": "Conference call not enabled",
"admin-video-conf-provider-not-configured-body": "Configure conference calls in order to make it available on this workspace.",
"Presence_Cap_Warning_Title": "User status temporarily disabled",
"Presence_Cap_Warning_Description": "Active connections have reached the limit for the workspace, thus the service that handles user status is disabled. It can be re-enabled manually in workspace settings.",
"Learn_more": "Learn more"
}

View File

@ -12,6 +12,7 @@
"error-could-not-change-email": "Não foi possível mudar e-mail",
"error-could-not-change-name": "Não foi possível mudar o nome",
"error-could-not-change-username": "Não foi possível alterar o nome de usuário",
"error-could-not-change-status": "Não foi possível alterar o status",
"error-delete-protected-role": "Não é possível remover um papel protegido",
"error-department-not-found": "Departamento não encontrado",
"error-direct-message-file-upload-not-allowed": "Compartilhamento de arquivos não está permitido em mensagens diretas",
@ -19,6 +20,7 @@
"error-email-domain-blacklisted": "O domínio de e-mail está na lista negra",
"error-email-send-failed": "Erro ao tentar enviar e-mail: {{message}}",
"error-save-image": "Erro ao salvar imagem",
"error-save-video": "Erro ao salvar vídeo",
"error-field-unavailable": "{{field}} já está sendo usado :(",
"error-file-too-large": "Arquivo é muito grande",
"error-not-permission-to-upload-file": "Você não tem permissão para enviar arquivos",
@ -88,6 +90,7 @@
"Add_Reaction": "Reagir",
"Add_Server": "Adicionar servidor",
"Add_users": "Adicionar usuário",
"Admin_Panel": "Painel de admin",
"Agent": "Agente",
"Alert": "Alerta",
"alert": "alerta",
@ -96,6 +99,7 @@
"All_users_in_the_team_can_write_new_messages": "Todos usuários no canal podem enviar mensagens novas",
"A_meaningful_name_for_the_discussion_room": "Um nome significativo para o canal de discussão",
"All": "Todos",
"All_Messages": "Todas as mensagens",
"Allow_Reactions": "Permitir reagir",
"Alphabetical": "Alfabético",
"and_more": "e mais",
@ -163,7 +167,10 @@
"Copied_to_clipboard": "Copiado para a área de transferência!",
"Copy": "Copiar",
"Conversation": "Conversação",
"Certificate_password": "Senha do certificado",
"Clear_cache": "Limpar cache da workspace",
"Clear_cache_loading": "Limpando cache.",
"Whats_the_password_for_your_certificate": "Qual é a senha para o seu certificado?",
"Create_account": "Criar conta",
"Create_Channel": "Criar Canal",
"Create_Direct_Messages": "Criar Mensagens Diretas",
@ -171,6 +178,7 @@
"Created_snippet": "criou um snippet",
"Create_a_new_workspace": "Criar nova área de trabalho",
"Create": "Criar",
"Custom_Status": "Status personalizado",
"Dark": "Escuro",
"Dark_level": "Nível escuro",
"Default": "Padrão",
@ -255,6 +263,7 @@
"Has_left_the_team": "saiu da equipe",
"Hide_System_Messages": "Esconder mensagens do sistema",
"Hide_type_messages": "Esconder mensagens de \"{{type}}\"",
"How_It_Works": "Como funciona",
"Message_HideType_uj": "Utilizador Entrou",
"Message_HideType_ul": "Utilizador Saiu",
"Message_HideType_ru": "Utilizador Removido",
@ -268,11 +277,15 @@
"Message_HideType_subscription_role_removed": "Papel removido",
"Message_HideType_room_archived": "Sala arquivada",
"Message_HideType_room_unarchived": "Sala desarquivada",
"I_Saved_My_E2E_Password": "Salvei minha senha ponta-a-ponta",
"IP": "IP",
"In_app": "No app",
"In_App_And_Desktop": "In-app e área de trabalho",
"In_App_and_Desktop_Alert_info": "Exibe um banner na parte superior da tela quando o aplicativo é aberto e exibe uma notificação na área de trabalho",
"Invisible": "Invisível",
"Invite": "Convidar",
"is_a_valid_RocketChat_instance": "é uma instância Rocket.Chat",
"is_not_a_valid_RocketChat_instance": "não é uma instância Rocket.Chat",
"is_typing": "está digitando",
"Invalid_or_expired_invite_token": "Token de convite inválido ou vencido",
"Invalid_server_version": "O servidor que você está conectando não é suportado mais por esta versão do aplicativo: {{currentVersion}}.\n\nEsta versão do aplicativo requer a versão {{minVersion}} do servidor para funcionar corretamente.",
@ -293,7 +306,9 @@
"leave": "sair",
"Legal": "Legal",
"Light": "Claro",
"License": "Licença",
"Livechat": "Livechat",
"Livechat_edit": "Editar livechat",
"Livechat_transfer_return_to_the_queue": "retornou conversa para a fila",
"Login": "Entrar",
"Login_error": "Suas credenciais foram rejeitadas. Tente novamente por favor!",
@ -302,6 +317,7 @@
"Logout": "Sair",
"Max_number_of_uses": "Número máximo de usos",
"Max_number_of_users_allowed_is_number": "Número máximo de usuários é {{maxUsers}}",
"members": "membros",
"Members": "Membros",
"Mentioned_Messages": "Mensagens mencionadas",
"mentioned": "mencionado",
@ -310,14 +326,18 @@
"Message_actions": "Ações",
"Message_pinned": "Fixou uma mensagem",
"Message_removed": "mensagem removida",
"Message_starred": "Mensagem adicionada aos favoritos",
"Message_unstarred": "Mensagem removida dos favoritos",
"message": "mensagem",
"messages": "mensagens",
"Message": "Mensagem",
"Messages": "Mensagens",
"Message_Reported": "Mensagem reportada",
"Microphone_Permission_Message": "Rocket.Chat precisa de acesso ao seu microfone para enviar mensagens de áudio.",
"Microphone_Permission": "Acesso ao Microfone",
"Mute": "Mudo",
"muted": "mudo",
"My_servers": "Minhas workspaces",
"N_people_reacted": "{{n}} pessoas reagiram",
"N_users": "{{n}} usuários",
"N_channels": "{{n}} canais",
@ -326,6 +346,7 @@
"New_chat_transfer": "Nova transferência de conversa: {{agent}} retornou conversa para a fila",
"New_Message": "Nova Mensagem",
"New_Password": "Nova Senha",
"New_Server": "Nova workspace",
"Next": "Próximo",
"No_files": "Não há arquivos",
"No_limit": "Sem limite",
@ -339,6 +360,8 @@
"No_Message": "Não há mensagens",
"No_messages_yet": "Não há mensagens ainda",
"No_Reactions": "Sem reações",
"No_Read_Receipts": "Não lida",
"Not_logged": "Desconectado",
"Not_RC_Server": "Este não é um servidor Rocket.Chat.\n{{contact}}",
"Nothing": "Nada",
"Nothing_to_save": "Nada para salvar!",
@ -459,6 +482,7 @@
"Search_emoji": "Buscar emoji",
"Search_global_users": "Busca por usuários globais",
"Search_global_users_description": "Caso ativado, busca por usuários de outras empresas ou servidores.",
"Seconds": "{{second}} segundos",
"Security_and_privacy": "Segurança e privacidade",
"Select_Avatar": "Selecionar Avatar",
"Select_Server": "Selecionar Servidor",
@ -473,13 +497,20 @@
"Send_message": "Enviar mensagem",
"Send_me_the_code_again": "Envie-me o código novamente",
"Send_to": "Enviar para...",
"Sending_to": "Envio para",
"Sent_an_attachment": "Enviou um anexo",
"Server": "Servidor",
"Servers": "Workspaces",
"Server_version": "Versão da workspace: {{version}}",
"Set_username_subtitle": "O usuário é utilizado para permitir que você seja mencionado em mensagens",
"Set_custom_status": "Definir status personalizado",
"Set_status": "Definir status",
"Status_saved_successfully": "Status salvo com sucesso!",
"Settings": "Configurações",
"Settings_succesfully_changed": "Configurações salvas com sucesso!",
"Share": "Compartilhar",
"Share_Link": "Share Link",
"Share_this_app": "Compartilhar esse app",
"Show_more": "Mostrar mais..",
"Sign_in_your_server": "Entrar no seu servidor",
"Sign_Up": "Registrar",
@ -496,9 +527,12 @@
"Started_call": "Chamada iniciada por {{userBy}}",
"Submit": "Enviar",
"Table": "Tabela",
"Tags": "Tags",
"Take_a_photo": "Tirar uma foto",
"Take_a_video": "Gravar um vídeo",
"Take_it": "Pegue!",
"tap_to_change_status": "toque para mudar de status",
"Tap_to_view_servers_list": "Toque para visualizar as workspaces",
"Terms_of_Service": " Termos de Serviço ",
"Theme": "Tema",
"The_user_wont_be_able_to_type_in_roomName": "O usuário não poderá digitar em {{roomName}}",
@ -543,10 +577,14 @@
"User_has_been_removed": "removeu {{userRemoved}}",
"User_sent_an_attachment": "{{user}} enviou um anexo",
"User_has_been_unmuted": "permitiu que {{userUnmuted}} fale na sala",
"Defined_user_as_role": "definiu {{user}} como {{role}}",
"Removed_user_as_role": "removeu {{user}} como {{role}}",
"Username_is_empty": "Usuário está vazio",
"Username": "Usuário",
"Username_or_email": "Usuário ou email",
"Uses_server_configuration": "Usar configuração do servidor",
"Validating": "Validando...",
"Registration_Succeeded": "Registrado com sucesso!",
"Verify": "Verificar",
"Verify_email_title": "Registrado com sucesso!",
"Verify_email_desc": "Nós lhe enviamos um e-mail para confirmar o seu registro. Se você não receber um e-mail em breve, por favor retorne e tente novamente.",
@ -575,7 +613,9 @@
"You_were_removed_from_channel": "Você foi removido de {{channel}}",
"you": "você",
"You": "Você",
"Logged_out_by_server": "Você foi desconectado pela workspace. Por favor entre novamente.",
"Token_expired": "Sua sessão expirou. Por favor entre novamente.",
"You_need_to_access_at_least_one_RocketChat_server_to_share_something": "Você precisa acessar pelo menos uma workspace Rocket.Chat para compartilhar.",
"You_need_to_verifiy_your_email_address_to_get_notications": "Você precisa confirmar seu endereço de e-mail para obter notificações",
"Your_certificate": "Seu certificado",
"Your_invite_link_will_expire_after__usesLeft__uses": "Seu link de convite irá vencer depois de {{usesLeft}} usos.",
@ -583,6 +623,8 @@
"Your_invite_link_will_expire_on__date__": "Seu link de convite irá vencer em {{date}}.",
"Your_invite_link_will_never_expire": "Seu link de convite nunca irá vencer.",
"Your_workspace": "Sua workspace",
"Your_password_is": "Sua senha é",
"Version_no": "Versão: {{version}}",
"You_will_not_be_able_to_recover_this_message": "Você não será capaz de recuperar essa mensagem!",
"You_will_unset_a_certificate_for_this_server": "Você cancelará a configuração de um certificado para este servidor",
"Change_Language": "Alterar idioma",
@ -683,6 +725,10 @@
"Teams": "Times",
"No_team_channels_found": "Nenhum canal encontrado",
"Team_not_found": "Time não encontrado",
"Create_Team": "Criar time",
"Team_Name": "Nome do time",
"creating_team": "criando time",
"team-name-already-exists": "Um time com esse nome já existe",
"Add_Channel_to_Team": "Adicionar Canal ao Time",
"Left_The_Team_Successfully": "Saiu do time com sucesso",
"Create_New": "Criar",
@ -829,5 +875,7 @@
"Call": "Ligar",
"Reply_in_direct_message": "Responder por mensagem direta",
"room_archived": "{{username}} arquivou a sala",
"room_unarchived": "{{username}} desarquivou a sala"
"room_unarchived": "{{username}} desarquivou a sala",
"Presence_Cap_Warning_Title": "Status do usuário desabilitado temporariamente",
"Presence_Cap_Warning_Description": "O limite de conexões ativas para a workspace foi atingido, por isso o serviço responsável pela presença dos usuários está temporariamente desabilitado. Ele pode ser reabilitado manualmente nas configurações da workspace."
}

View File

@ -3,7 +3,8 @@ export const STATUS_COLORS: any = {
busy: '#f5455c',
away: '#ffd21f',
offline: '#cbced1',
loading: '#9ea2a8'
loading: '#9ea2a8',
disabled: '#F38C39'
};
export const SWITCH_TRACK_COLOR = {

View File

@ -1,3 +1,4 @@
// 🚨🚨 48 settings after login. Pay attention not to reach 50 as that's the limit per request.
export const defaultSettings = {
Accounts_AllowEmailChange: {
type: 'valueAsBoolean'
@ -229,5 +230,8 @@ export const defaultSettings = {
},
Number_of_users_autocomplete_suggestions: {
type: 'valueAsNumber'
},
Presence_broadcast_disabled: {
type: 'valueAsBoolean'
}
} as const;

View File

@ -8,5 +8,6 @@ export * from './localAuthentication';
export * from './localPath';
export * from './messagesStatus';
export * from './messageTypeLoad';
export * from './notifications';
export * from './defaultSettings';
export * from './tablet';

View File

@ -0,0 +1 @@
export const NOTIFICATION_PRESENCE_CAP = 'NOTIFICATION_PRESENCE_CAP';

View File

@ -85,47 +85,4 @@ export default class Message extends Model {
@json('md', sanitizer) md;
@field('comment') comment;
asPlain() {
return {
id: this.id,
rid: this.subscription.id,
msg: this.msg,
t: this.t,
ts: this.ts,
u: this.u,
alias: this.alias,
parseUrls: this.parseUrls,
groupable: this.groupable,
avatar: this.avatar,
emoji: this.emoji,
attachments: this.attachments,
urls: this.urls,
_updatedAt: this._updatedAt,
status: this.status,
pinned: this.pinned,
starred: this.starred,
editedBy: this.editedBy,
reactions: this.reactions,
role: this.role,
drid: this.drid,
dcount: this.dcount,
dlm: this.dlm,
tmid: this.tmid,
tcount: this.tcount,
tlm: this.tlm,
replies: this.replies,
mentions: this.mentions,
channels: this.channels,
unread: this.unread,
autoTranslate: this.autoTranslate,
translations: this.translations,
tmsg: this.tmsg,
blocks: this.blocks,
e2e: this.e2e,
tshow: this.tshow,
md: this.md,
comment: this.comment
};
}
}

View File

@ -123,6 +123,8 @@ export default class Subscription extends Model {
@field('e2e_key') E2EKey;
@field('e2e_suggested_key') E2ESuggestedKey;
@field('encrypted') encrypted;
@field('e2e_key_id') e2eKeyId;
@ -136,68 +138,4 @@ export default class Subscription extends Model {
@field('on_hold') onHold;
@json('source', sanitizer) source;
// TODO: if this is proven to be the best way to do it, we should use TS to map through the properties
asPlain() {
return {
_id: this._id,
f: this.f,
t: this.t,
ts: this.ts,
ls: this.ls,
name: this.name,
fname: this.fname,
rid: this.rid,
open: this.open,
alert: this.alert,
unread: this.unread,
userMentions: this.userMentions,
groupMentions: this.groupMentions,
roomUpdatedAt: this.roomUpdatedAt,
ro: this.ro,
lastOpen: this.lastOpen,
description: this.description,
announcement: this.announcement,
bannerClosed: this.bannerClosed,
topic: this.topic,
blocked: this.blocked,
blocker: this.blocker,
reactWhenReadOnly: this.reactWhenReadOnly,
archived: this.archived,
joinCodeRequired: this.joinCodeRequired,
notifications: this.notifications,
broadcast: this.broadcast,
prid: this.prid,
draftMessage: this.draftMessage,
lastThreadSync: this.lastThreadSync,
jitsiTimeout: this.jitsiTimeout,
autoTranslate: this.autoTranslate,
autoTranslateLanguage: this.autoTranslateLanguage,
hideUnreadStatus: this.hideUnreadStatus,
hideMentionStatus: this.hideMentionStatus,
departmentId: this.departmentId,
E2EKey: this.E2EKey,
encrypted: this.encrypted,
e2eKeyId: this.e2eKeyId,
avatarETag: this.avatarETag,
teamId: this.teamId,
teamMain: this.teamMain,
onHold: this.onHold,
roles: this.roles,
tunread: this.tunread,
tunreadUser: this.tunreadUser,
tunreadGroup: this.tunreadGroup,
muted: this.muted,
ignored: this.ignored,
lastMessage: this.lastMessage,
sysMes: this.sysMes,
uids: this.uids,
usernames: this.usernames,
visitor: this.visitor,
servedBy: this.servedBy,
livechatData: this.livechatData,
tags: this.tags,
source: this.source
};
}
}

View File

@ -77,42 +77,4 @@ export default class Thread extends Model {
@field('e2e') e2e;
@field('draft_message') draftMessage;
asPlain() {
return {
id: this.id,
msg: this.msg,
t: this.t,
ts: this.ts,
u: this.u,
alias: this.alias,
parseUrls: this.parseUrls,
groupable: this.groupable,
avatar: this.avatar,
emoji: this.emoji,
attachments: this.attachments,
urls: this.urls,
_updatedAt: this._updatedAt,
status: this.status,
pinned: this.pinned,
starred: this.starred,
editedBy: this.editedBy,
reactions: this.reactions,
role: this.role,
drid: this.drid,
dcount: this.dcount,
dlm: this.dlm,
tmid: this.tmid,
tcount: this.tcount,
tlm: this.tlm,
replies: this.replies,
mentions: this.mentions,
channels: this.channels,
unread: this.unread,
autoTranslate: this.autoTranslate,
translations: this.translations,
e2e: this.e2e,
draftMessage: this.draftMessage
};
}
}

View File

@ -77,42 +77,4 @@ export default class ThreadMessage extends Model {
@field('draft_message') draftMessage;
@field('e2e') e2e;
asPlain() {
return {
id: this.id,
msg: this.msg,
t: this.t,
ts: this.ts,
u: this.u,
rid: this.rid,
alias: this.alias,
parseUrls: this.parseUrls,
groupable: this.groupable,
avatar: this.avatar,
emoji: this.emoji,
attachments: this.attachments,
urls: this.urls,
_updatedAt: this._updatedAt,
status: this.status,
pinned: this.pinned,
starred: this.starred,
editedBy: this.editedBy,
reactions: this.reactions,
role: this.role,
drid: this.drid,
dcount: this.dcount,
dlm: this.dlm,
tcount: this.tcount,
tlm: this.tlm,
replies: this.replies,
mentions: this.mentions,
channels: this.channels,
unread: this.unread,
autoTranslate: this.autoTranslate,
translations: this.translations,
draftMessage: this.draftMessage,
e2e: this.e2e
};
}
}

View File

@ -16,6 +16,8 @@ export default class Upload extends Model {
@field('name') name;
@field('tmid') tmid;
@field('description') description;
@field('size') size;

View File

@ -239,6 +239,24 @@ export default schemaMigrations({
columns: [{ name: 'hide_mention_status', type: 'boolean', isOptional: true }]
})
]
},
{
toVersion: 19,
steps: [
addColumns({
table: 'uploads',
columns: [{ name: 'tmid', type: 'string', isOptional: true }]
})
]
},
{
toVersion: 20,
steps: [
addColumns({
table: 'subscriptions',
columns: [{ name: 'e2e_suggested_key', type: 'string', isOptional: true }]
})
]
}
]
});

View File

@ -1,7 +1,7 @@
import { appSchema, tableSchema } from '@nozbe/watermelondb';
export default appSchema({
version: 18,
version: 20,
tables: [
tableSchema({
name: 'subscriptions',
@ -55,6 +55,7 @@ export default appSchema({
{ name: 'livechat_data', type: 'string', isOptional: true },
{ name: 'tags', type: 'string', isOptional: true },
{ name: 'e2e_key', type: 'string', isOptional: true },
{ name: 'e2e_suggested_key', type: 'string', isOptional: true },
{ name: 'encrypted', type: 'boolean', isOptional: true },
{ name: 'e2e_key_id', type: 'string', isOptional: true },
{ name: 'avatar_etag', type: 'string', isOptional: true },
@ -222,6 +223,7 @@ export default appSchema({
{ name: 'path', type: 'string', isOptional: true },
{ name: 'rid', type: 'string', isIndexed: true },
{ name: 'name', type: 'string', isOptional: true },
{ name: 'tmid', type: 'string', isOptional: true },
{ name: 'description', type: 'string', isOptional: true },
{ name: 'size', type: 'number' },
{ name: 'type', type: 'string', isOptional: true },

View File

@ -34,6 +34,7 @@ class Encryption {
handshake: Function;
decrypt: Function;
encrypt: Function;
importRoomKey: Function;
};
};
@ -97,6 +98,10 @@ class Encryption {
});
};
stopRoom = (rid: string) => {
delete this.roomInstances[rid];
};
// When a new participant join and request a new room encryption key
provideRoomKeyToUser = async (keyId: string, rid: string) => {
// If the client is not ready
@ -220,6 +225,19 @@ class Encryption {
return roomE2E;
};
evaluateSuggestedKey = async (rid: string, E2ESuggestedKey: string) => {
try {
if (this.privateKey) {
const roomE2E = await this.getRoomInstance(rid);
await roomE2E.importRoomKey(E2ESuggestedKey, this.privateKey);
delete this.roomInstances[rid];
await Services.e2eAcceptSuggestedGroupKey(rid);
}
} catch (e) {
await Services.e2eRejectSuggestedGroupKey(rid);
}
};
// Logic to decrypt all pending messages/threads/threadMessages
// after initialize the encryption client
decryptPendingMessages = async (roomId?: string) => {

View File

@ -74,7 +74,10 @@ export default class EncryptionRoom {
if (E2EKey && Encryption.privateKey) {
// We're establishing a new room encryption client
this.establishing = true;
await this.importRoomKey(E2EKey, Encryption.privateKey);
const { keyID, roomKey, sessionKeyExportedString } = await this.importRoomKey(E2EKey, Encryption.privateKey);
this.keyID = keyID;
this.roomKey = roomKey;
this.sessionKeyExportedString = sessionKeyExportedString;
this.readyPromise.resolve();
return;
}
@ -96,20 +99,33 @@ export default class EncryptionRoom {
};
// Import roomKey as an AES Decrypt key
importRoomKey = async (E2EKey: string, privateKey: string) => {
const roomE2EKey = E2EKey.slice(12);
importRoomKey = async (
E2EKey: string,
privateKey: string
): Promise<{ sessionKeyExportedString: string | ByteBuffer; roomKey: ArrayBuffer; keyID: string }> => {
try {
const roomE2EKey = E2EKey.slice(12);
const decryptedKey = await SimpleCrypto.RSA.decrypt(roomE2EKey, privateKey);
this.sessionKeyExportedString = toString(decryptedKey);
const decryptedKey = await SimpleCrypto.RSA.decrypt(roomE2EKey, privateKey);
const sessionKeyExportedString = toString(decryptedKey);
this.keyID = Base64.encode(this.sessionKeyExportedString as string).slice(0, 12);
const keyID = Base64.encode(sessionKeyExportedString as string).slice(0, 12);
// Extract K from Web Crypto Secret Key
// K is a base64URL encoded array of bytes
// Web Crypto API uses this as a private key to decrypt/encrypt things
// Reference: https://www.javadoc.io/doc/com.nimbusds/nimbus-jose-jwt/5.1/com/nimbusds/jose/jwk/OctetSequenceKey.html
const { k } = EJSON.parse(this.sessionKeyExportedString as string);
this.roomKey = b64ToBuffer(k);
// Extract K from Web Crypto Secret Key
// K is a base64URL encoded array of bytes
// Web Crypto API uses this as a private key to decrypt/encrypt things
// Reference: https://www.javadoc.io/doc/com.nimbusds/nimbus-jose-jwt/5.1/com/nimbusds/jose/jwk/OctetSequenceKey.html
const { k } = EJSON.parse(sessionKeyExportedString as string);
const roomKey = b64ToBuffer(k);
return {
sessionKeyExportedString,
roomKey,
keyID
};
} catch (e: any) {
throw new Error(e);
}
};
// Create a key to a room

View File

@ -1,27 +0,0 @@
import { useCallback } from 'react';
import { TActionSheetOptionsItem, useActionSheet } from '../../containers/ActionSheet';
import i18n from '../../i18n';
import { videoConfJoin } from '../methods/videoConf';
export const useVideoConf = (): { joinCall: (blockId: string) => void } => {
const { showActionSheet } = useActionSheet();
const joinCall = useCallback(blockId => {
const options: TActionSheetOptionsItem[] = [
{
title: i18n.t('Video_call'),
icon: 'camera',
onPress: () => videoConfJoin(blockId, true)
},
{
title: i18n.t('Voice_call'),
icon: 'microphone',
onPress: () => videoConfJoin(blockId, false)
}
];
showActionSheet({ options });
}, []);
return { joinCall };
};

View File

@ -0,0 +1,113 @@
import React, { useEffect, useState } from 'react';
import { Q } from '@nozbe/watermelondb';
import { useActionSheet } from '../../containers/ActionSheet';
import StartACallActionSheet from '../../containers/UIKit/VideoConferenceBlock/components/StartACallActionSheet';
import { ISubscription, SubscriptionType, TSubscriptionModel } from '../../definitions';
import i18n from '../../i18n';
import { getUserSelector } from '../../selectors/login';
import database from '../database';
import { getSubscriptionByRoomId } from '../database/services/Subscription';
import { callJitsi } from '../methods';
import { compareServerVersion, showErrorAlert } from '../methods/helpers';
import { videoConfStartAndJoin } from '../methods/videoConf';
import { Services } from '../services';
import { useAppSelector } from './useAppSelector';
import { useSnaps } from './useSnaps';
const availabilityErrors = {
NOT_CONFIGURED: 'video-conf-provider-not-configured',
NOT_ACTIVE: 'no-active-video-conf-provider',
NO_APP: 'no-videoconf-provider-app'
} as const;
const handleErrors = (isAdmin: boolean, error: typeof availabilityErrors[keyof typeof availabilityErrors]) => {
if (isAdmin) return showErrorAlert(i18n.t(`admin-${error}-body`), i18n.t(`admin-${error}-header`));
return showErrorAlert(i18n.t(`${error}-body`), i18n.t(`${error}-header`));
};
export const useVideoConf = (rid: string): { showInitCallActionSheet: () => Promise<void>; showCallOption: boolean } => {
const [showCallOption, setShowCallOption] = useState(false);
const serverVersion = useAppSelector(state => state.server.version);
const jitsiEnabled = useAppSelector(state => state.settings.Jitsi_Enabled);
const jitsiEnableTeams = useAppSelector(state => state.settings.Jitsi_Enable_Teams);
const jitsiEnableChannels = useAppSelector(state => state.settings.Jitsi_Enable_Channels);
const user = useAppSelector(state => getUserSelector(state));
const isServer5OrNewer = compareServerVersion(serverVersion, 'greaterThanOrEqualTo', '5.0.0');
const { showActionSheet } = useActionSheet();
const snaps = useSnaps([1250]);
const handleShowCallOption = (room: TSubscriptionModel) => {
if (isServer5OrNewer) return setShowCallOption(true);
const isJitsiDisabledForTeams = room.teamMain && !jitsiEnableTeams;
const isJitsiDisabledForChannels = !room.teamMain && (room.t === 'p' || room.t === 'c') && !jitsiEnableChannels;
if (room.t === SubscriptionType.DIRECT) return setShowCallOption(!!jitsiEnabled);
if (room.t === SubscriptionType.CHANNEL) return setShowCallOption(!isJitsiDisabledForChannels);
if (room.t === SubscriptionType.GROUP) return setShowCallOption(!isJitsiDisabledForTeams);
return setShowCallOption(false);
};
const canInitAnCall = async () => {
if (isServer5OrNewer) {
try {
await Services.videoConferenceGetCapabilities();
return true;
} catch (error: any) {
const isAdmin = !!['admin'].find(role => user.roles?.includes(role));
switch (error?.error) {
case availabilityErrors.NOT_CONFIGURED:
return handleErrors(isAdmin, availabilityErrors.NOT_CONFIGURED);
case availabilityErrors.NOT_ACTIVE:
return handleErrors(isAdmin, availabilityErrors.NOT_ACTIVE);
case availabilityErrors.NO_APP:
return handleErrors(isAdmin, availabilityErrors.NO_APP);
default:
return handleErrors(isAdmin, availabilityErrors.NOT_CONFIGURED);
}
}
}
return true;
};
const initCall = async ({ cam, mic }: { cam: boolean; mic: boolean }) => {
if (isServer5OrNewer) return videoConfStartAndJoin({ rid, cam, mic });
const room = (await getSubscriptionByRoomId(rid)) as ISubscription;
callJitsi({ room, cam });
};
const showInitCallActionSheet = async () => {
const canInit = await canInitAnCall();
if (canInit) {
showActionSheet({
children: <StartACallActionSheet rid={rid} initCall={initCall} />,
snaps
});
}
};
const initSubscription = () => {
try {
const db = database.active;
const observeSubCollection = db.get('subscriptions').query(Q.where('rid', rid)).observe();
const subObserveQuery = observeSubCollection.subscribe(data => {
if (data[0]) {
handleShowCallOption(data[0]);
subObserveQuery.unsubscribe();
}
});
} catch (e) {
console.log("observeSubscriptions: Can't find subscription to observe");
}
};
useEffect(() => {
initSubscription();
}, []);
return { showInitCallActionSheet, showCallOption };
};

View File

@ -4,12 +4,17 @@ import { sanitizeLikeString } from '../database/utils';
import { store } from '../store/auxStore';
import log from './helpers/log';
const DEFAULT_EXTENSION = 'mp3';
const sanitizeString = (value: string) => sanitizeLikeString(value.substring(value.lastIndexOf('/') + 1));
const parseFilename = (value: string) => {
const extension = value.substring(value.lastIndexOf('.') + 1);
const filename = sanitizeString(value.substring(value.lastIndexOf('/') + 1).split('.')[0]);
return `${filename}.${extension}`;
const getExtension = (value: string) => {
let extension = DEFAULT_EXTENSION;
const filename = value.split('/').pop();
if (filename?.includes('.')) {
extension = value.substring(value.lastIndexOf('.') + 1);
}
return extension;
};
const ensureDirAsync = async (dir: string, intermediates = true): Promise<void> => {
@ -27,7 +32,7 @@ export const downloadAudioFile = async (url: string, fileUrl: string, messageId:
const serverUrl = store.getState().server.server;
const serverUrlParsed = sanitizeString(serverUrl);
const folderPath = `${FileSystem.documentDirectory}audios/${serverUrlParsed}`;
const filename = `${messageId}_${parseFilename(fileUrl)}`;
const filename = `${messageId}.${getExtension(fileUrl)}`;
const filePath = `${folderPath}/${filename}`;
await ensureDirAsync(folderPath);
const file = await FileSystem.getInfoAsync(filePath);
@ -47,7 +52,7 @@ export const deleteAllAudioFiles = async (serverUrl: string): Promise<void> => {
try {
const serverUrlParsed = sanitizeString(serverUrl);
const path = `${FileSystem.documentDirectory}audios/${serverUrlParsed}`;
await FileSystem.deleteAsync(path);
await FileSystem.deleteAsync(path, { idempotent: true });
} catch (error) {
log(error);
}

View File

@ -46,8 +46,8 @@ export function callJitsiWithoutServer(path: string): void {
Navigation.navigate('JitsiMeetView', { url, onlyAudio: false });
}
export async function callJitsi(room: ISubscription, onlyAudio = false): Promise<void> {
logEvent(onlyAudio ? events.RA_JITSI_AUDIO : events.RA_JITSI_VIDEO);
export async function callJitsi({ room, cam = false }: { room: ISubscription; cam?: boolean }): Promise<void> {
logEvent(cam ? events.RA_JITSI_AUDIO : events.RA_JITSI_VIDEO);
const url = await jitsiURL({ room });
Navigation.navigate('JitsiMeetView', { url, onlyAudio, rid: room?.rid });
Navigation.navigate('JitsiMeetView', { url, onlyAudio: cam, rid: room?.rid });
}

View File

@ -1,17 +1,15 @@
import { SubscriptionType, TMessageModel } from '../../../definitions';
import { loadNextMessages, loadMessagesForRoom } from '../../../lib/methods';
import { MessageTypeLoad } from '../../../lib/constants';
import { SubscriptionType, TAnyMessageModel } from '../../definitions';
import { loadNextMessages, loadMessagesForRoom } from '.';
import { MessageTypeLoad } from '../constants';
const getMoreMessages = ({
rid,
t,
tmid,
loaderItem
}: {
rid: string;
t: SubscriptionType;
tmid?: string;
loaderItem: TMessageModel;
loaderItem: TAnyMessageModel;
}): Promise<void> => {
if ([MessageTypeLoad.MORE, MessageTypeLoad.PREVIOUS_CHUNK].includes(loaderItem.t as MessageTypeLoad)) {
return loadMessagesForRoom({
@ -25,7 +23,6 @@ const getMoreMessages = ({
if (loaderItem.t === MessageTypeLoad.NEXT_CHUNK) {
return loadNextMessages({
rid,
tmid,
ts: loaderItem.ts as Date,
loaderItem
});

View File

@ -1,15 +1,16 @@
import log from './helpers/log';
import { IMessage, TSubscriptionModel } from '../../definitions';
import { TMessageModel, TSubscriptionModel } from '../../definitions';
import { store } from '../store/auxStore';
import { isGroupChat } from './helpers';
import { getRoom } from './getRoom';
type TRoomType = 'p' | 'c' | 'd';
export async function getPermalinkMessage(message: IMessage): Promise<string | null> {
export async function getPermalinkMessage(message: TMessageModel): Promise<string | null> {
if (!message.subscription) return null;
let room: TSubscriptionModel;
try {
room = await getRoom(message.rid);
room = await getRoom(message.subscription.id);
} catch (e) {
log(e);
return null;

View File

@ -11,6 +11,7 @@ import database from '../database';
import sdk from '../services/sdk';
import protectedFunction from './helpers/protectedFunction';
import { parseSettings, _prepareSettings } from './parseSettings';
import { setPresenceCap } from './getUsersPresence';
const serverInfoKeys = [
'Site_Name',
@ -157,8 +158,11 @@ export async function getSettings(): Promise<void> {
const data: IData[] = result.settings || [];
const filteredSettings: IPreparedSettings[] = _prepareSettings(data);
const filteredSettingsIds = filteredSettings.map(s => s._id);
const parsedSettings = parseSettings(filteredSettings);
reduxStore.dispatch(addSettings(parseSettings(filteredSettings)));
reduxStore.dispatch(addSettings(parsedSettings));
setPresenceCap(parsedSettings.Presence_broadcast_disabled);
// filter server info
const serverInfo = filteredSettings.filter(i1 => serverInfoKeys.includes(i1._id));

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