Compare commits

..

89 Commits

Author SHA1 Message Date
Diego Mello 891b910772
Merge branch 'develop' into chore.try-flashlist 2022-12-15 12:11:58 -03:00
Diego Mello d047cb0c49 Change estimataedItemSize to 48 (short message with avatar) 2022-12-14 15:21:57 -03:00
Diego Mello 83b5e8a98a Stop re-rendering RoomsListView when RoomView is opened on smartphone 2022-12-13 17:28:21 -03:00
Diego Mello d860991c87 Merge branch 'develop' into chore.try-flashlist
# Conflicts:
#	app/containers/MessageActions/Header.tsx
2022-12-13 15:54:15 -03:00
Diego Mello 7e849943fe Close swipeable on recycle 2022-12-13 15:53:04 -03:00
Diego Mello 2b06b01bd6 temp patch 2022-12-12 16:58:25 -03:00
Diego Mello 8678d079bc Fix keyboard on iOS 2022-12-12 16:56:25 -03:00
Diego Mello ff00ad169e ui-lib still 2022-12-09 17:20:24 -03:00
Diego Mello 4e11189f7f Patch-package fix 2022-12-08 18:35:23 -03:00
Diego Mello 428fb5dccc Fix keyboard track dismiss 2022-12-08 18:31:28 -03:00
Diego Mello ab53a5aaea Lint 2022-12-08 18:08:46 -03:00
Diego Mello 4dea3e7ce8 Fix jump to message test 2022-12-08 17:38:11 -03:00
Diego Mello 868bfb7a4c Remove login error test 2022-12-08 17:37:53 -03:00
Diego Mello e088d76724 Fix server dropdown 2022-12-08 16:08:35 -03:00
Diego Mello e6ec8b4e39 Fix emoji picker 2022-12-08 10:51:37 -03:00
Diego Mello 2683976d21 Prevent layout animation after the message has already rendered 2022-12-07 16:54:24 -03:00
Diego Mello 976753c08b Fix empty viewableItems check 2022-12-07 16:28:31 -03:00
Diego Mello fd82771438 Fix list extra data 2022-12-07 16:26:05 -03:00
Diego Mello c610b6d7ec Cleanup 2022-12-07 14:17:08 -03:00
Diego Mello f71be55140 Prevent getSubscription from running on search 2022-12-07 14:01:03 -03:00
Diego Mello 8c9f7ffa54 Bring back sCU to RoomsListView 2022-12-07 13:58:26 -03:00
Diego Mello 001f2a0c17 Cleanup 2022-12-07 13:47:58 -03:00
Diego Mello ffb31d399d Remove insets hoc from RoomsListView 2022-12-07 11:19:19 -03:00
Diego Mello 15bb21eb2f Cleanup 2022-12-07 11:14:08 -03:00
Diego Mello 9788e8a11f Remove width prop drill 2022-12-07 11:11:45 -03:00
Diego Mello 2c62d9cb46 Reenable LayoutAnimation on RoomView, but only for new messages 2022-12-07 10:32:54 -03:00
Diego Mello ae7398233a Stop calling onEndReached unnecessarily 2022-12-06 18:45:10 -03:00
Diego Mello 7b526fe700 Disable RoomView animation and fix console.count 2022-12-06 18:23:59 -03:00
Diego Mello 654feb719c fetchRoomUpdate 2022-12-06 17:10:12 -03:00
Diego Mello dbb9396042 Devtools 2022-12-06 17:06:53 -03:00
Diego Mello c877a0600b Fix setReadOnly calls on RoomView 2022-12-06 15:27:18 -03:00
Diego Mello 696a7e623d Fix RefreshControl on Android 2022-12-05 14:17:43 -03:00
Diego Mello da113ac1d0 Patch FlashList https://github.com/Shopify/flash-list/issues/679#issue-1455946016 2022-12-05 10:04:58 -03:00
Diego Mello 1af737ee71 pods for react-native-flipper-performance 2022-12-05 10:03:48 -03:00
Diego Mello c867f2928f Lint 2022-12-05 09:34:53 -03:00
Diego Mello 87a5793f24 Fix silly usage of extraData on RoomView 2022-12-02 15:14:07 -03:00
Diego Mello f5615c50f6 Export RefreshControl so we don't have to use theme hoc 2022-12-02 13:36:37 -03:00
Diego Mello b2db5500b6 Fix user presence on RoomItem 2022-12-02 11:50:56 -03:00
Diego Mello c6ecfa17a3 Fix Flipper on Android and install react-native-flipper-performance-plugin 2022-12-01 15:17:47 -03:00
Diego Mello 595f010742 Fix nonsense typing 2022-11-30 19:03:55 -03:00
Diego Mello 0c4c361446 Remove isFocused logic from sCU 2022-11-30 18:29:18 -03:00
Diego Mello 53cda7ecb1 Move renderScroll to component based approach.
Declare getItemType.
Add this.props to extraData.
2022-11-30 17:43:11 -03:00
Diego Mello 04e49563f2 Try isFocused on RoomsListView.sCU 2022-11-30 17:15:07 -03:00
Diego Mello 36f0935e55 Reenable layout animation on RoomView 2022-11-30 13:13:52 -03:00
Diego Mello 2cefe12a91 Enable layout animation on RoomsListView 2022-11-30 13:07:00 -03:00
Diego Mello 21dbc7e112 Fix edit, delete, resend and delete after error state 2022-11-29 18:55:02 -03:00
Diego Mello aaef77a58f
Merge branch 'develop' into chore.try-flashlist 2022-11-29 17:54:06 -03:00
Diego Mello cc5fe4c910 Fix pagination on RoomsListView 2022-11-29 17:24:27 -03:00
Diego Mello a593129a3c Fix thread messages grey 2022-11-29 16:43:25 -03:00
Diego Mello de6a897ec4 Cleanup 2022-11-29 16:31:33 -03:00
Diego Mello d42f623a2f Disable layout animation 2022-11-29 16:30:09 -03:00
Diego Mello 4aeda3f778 Try to fix LayoutAnimation on RoomsListView 2022-11-29 16:11:50 -03:00
Diego Mello 34a30214c2 Cleanup message 2022-11-29 15:34:16 -03:00
Diego Mello 79be07072c Merge branch 'develop' into chore.try-flashlist 2022-11-29 15:13:07 -03:00
Diego Mello a73a535221 Fix lint 2022-11-29 15:06:04 -03:00
Diego Mello 4ca5e3a0a7 Remove TAnyMessageModel completely 2022-11-29 14:57:30 -03:00
Diego Mello c0b3daa225 Remove ts-ignore 2022-11-29 14:51:46 -03:00
Diego Mello a130583ff4 Move Message interface to own file 2022-11-29 14:48:34 -03:00
Diego Mello 0b7c075560 TAnyMessageModel -> TAnyMessage on MessageActions 2022-11-29 14:36:11 -03:00
Diego Mello edc67d424b Fix quote 2022-11-29 13:26:58 -03:00
Diego Mello fad04765d4 Add asPlain to messages 2022-11-29 10:58:57 -03:00
Diego Mello 7beaa62003 Fix fav 2022-11-28 18:38:51 -03:00
Diego Mello f67a4010bc Rollback message list padding 2022-11-28 18:35:56 -03:00
Diego Mello 2ee4efd255 Merge branch 'develop' into chore.try-flashlist 2022-11-28 18:22:11 -03:00
Diego Mello 4532a6929c Fix types and remove omichannelsUpdate 2022-11-28 17:23:19 -03:00
Diego Mello 71dc0b8f50 Remove memo from RoomItem 2022-11-28 17:15:13 -03:00
Diego Mello 8ab84541f3 Fully remove sCU 2022-11-28 17:11:52 -03:00
Diego Mello 363331c953 Cleanup 2022-11-28 17:09:01 -03:00
Diego Mello 360afcdd45 Add asPlain to ISubscription 2022-11-28 17:08:53 -03:00
Diego Mello 83701d002f Fix observeRoom logic 2022-11-28 16:54:08 -03:00
Diego Mello c0d5a84ccd First "asPlain" test 2022-11-28 16:28:30 -03:00
Diego Mello 6b7b01af8d Fix toggle read 2022-11-28 15:44:14 -03:00
Diego Mello bac9eb79bb Lint 2022-11-25 17:39:04 -03:00
Diego Mello b9cafe9d31 Fixing RoomsListView 2022-11-25 17:34:49 -03:00
Diego Mello a38bc37aaa animation fix and minor cleanup 2022-11-25 16:32:25 -03:00
Diego Mello 490e567c85 Lint 2022-11-25 16:20:21 -03:00
Diego Mello 45042661c5 Reenable layout animations 2022-11-25 16:17:05 -03:00
Diego Mello e0b5030f6a Fix infinite loading indicator 2022-11-25 16:14:50 -03:00
Diego Mello 1ea3916e94 Fix load more 2022-11-25 15:56:13 -03:00
Diego Mello 537cd0032e Fix message update 2022-11-25 14:52:03 -03:00
Diego Mello e4f0fdb525 Cleanup on RoomsListView 2022-11-25 11:01:46 -03:00
Diego Mello 86a7297abc Cleanup 2022-11-25 10:57:50 -03:00
Diego Mello 34f2da40aa Add scrollViewNativeID to keyboard libs 2022-11-24 14:59:36 -03:00
Diego Mello 0317c4c27a Remove RefreshControl logic platform specific 2022-11-24 10:29:19 -03:00
Diego Mello 898a4a398b Merge branch 'develop' into chore.try-flashlist
# Conflicts:
#	app/containers/message/index.tsx
#	app/views/RoomView/List/List.tsx
2022-11-21 18:25:08 -03:00
Diego Mello 6bb7db9993 Fix lint 2022-09-19 18:09:30 -03:00
Diego Mello 86d5529cfd RoomView 2022-09-19 17:46:15 -03:00
Diego Mello 7bd4182bf7 Try it on RoomsListView 2022-09-19 17:08:25 -03:00
Diego Mello b20c9b1632 Install FlashList 2022-09-19 17:08:11 -03:00
258 changed files with 8332 additions and 5745 deletions

View File

@ -1,12 +1,9 @@
defaults: &defaults defaults: &defaults
working_directory: ~/repo working_directory: ~/repo
orbs:
android: circleci/android@2.1.2
macos: &macos macos: &macos
macos: macos:
xcode: "14.2.0" xcode: "13.3.0"
resource_class: large resource_class: large
bash-env: &bash-env bash-env: &bash-env
@ -54,14 +51,14 @@ save-gems-cache: &save-gems-cache
update-fastlane-ios: &update-fastlane-ios update-fastlane-ios: &update-fastlane-ios
name: Update Fastlane name: Update Fastlane
command: | command: |
echo "ruby-2.7.7" > ~/.ruby-version echo "ruby-2.6.4" > ~/.ruby-version
bundle install bundle install
working_directory: ios working_directory: ios
update-fastlane-android: &update-fastlane-android update-fastlane-android: &update-fastlane-android
name: Update Fastlane name: Update Fastlane
command: | command: |
echo "ruby-2.7.7" > ~/.ruby-version echo "ruby-2.6.4" > ~/.ruby-version
bundle install bundle install
working_directory: android working_directory: android
@ -121,26 +118,26 @@ commands:
if [[ $CIRCLE_JOB == "android-build-official" ]]; then if [[ $CIRCLE_JOB == "android-build-official" ]]; then
echo -e "APPLICATION_ID=chat.rocket.android" >> ./gradle.properties echo -e "APPLICATION_ID=chat.rocket.android" >> ./gradle.properties
echo -e "BugsnagAPIKey=$BUGSNAG_KEY_OFFICIAL" >> ./gradle.properties echo -e "BugsnagAPIKey=$BUGSNAG_KEY_OFFICIAL" >> ./gradle.properties
echo $KEYSTORE_OFFICIAL_BASE64 | base64 --decode > ./app/$KEYSTORE_OFFICIAL echo $CHAT_ROCKET_ANDROID_STORE_FILE_BASE64_JKS | base64 --decode > ./app/$KEYSTORE_OFFICIAL
echo -e "KEYSTORE=$KEYSTORE_OFFICIAL" >> ./gradle.properties echo -e "KEYSTORE=$KEYSTORE_OFFICIAL" >> ./gradle.properties
echo -e "KEYSTORE_PASSWORD=$KEYSTORE_OFFICIAL_PASSWORD" >> ./gradle.properties echo -e "KEYSTORE_PASSWORD=$CHAT_ROCKET_ANDROID_STORE_PASSWORD" >> ./gradle.properties
echo -e "KEY_ALIAS=$KEYSTORE_OFFICIAL_ALIAS" >> ./gradle.properties echo -e "KEY_ALIAS=$CHAT_ROCKET_ANDROID_KEY_ALIAS" >> ./gradle.properties
echo -e "KEY_PASSWORD=$KEYSTORE_OFFICIAL_PASSWORD" >> ./gradle.properties echo -e "KEY_PASSWORD=$CHAT_ROCKET_ANDROID_KEY_PASSWORD" >> ./gradle.properties
else else
echo -e "APPLICATION_ID=chat.rocket.reactnative" >> ./gradle.properties echo -e "APPLICATION_ID=chat.rocket.reactnative" >> ./gradle.properties
echo -e "BugsnagAPIKey=$BUGSNAG_KEY" >> ./gradle.properties echo -e "BugsnagAPIKey=$BUGSNAG_KEY" >> ./gradle.properties
echo $KEYSTORE_EXPERIMENTAL_BASE64 | base64 --decode > ./app/$KEYSTORE_EXPERIMENTAL echo $KEYSTORE_BASE64 | base64 --decode > ./app/$KEYSTORE
echo -e "KEYSTORE=$KEYSTORE_EXPERIMENTAL" >> ./gradle.properties echo -e "KEYSTORE=$KEYSTORE" >> ./gradle.properties
echo -e "KEYSTORE_PASSWORD=$KEYSTORE_EXPERIMENTAL_PASSWORD" >> ./gradle.properties echo -e "KEYSTORE_PASSWORD=$KEYSTORE_PASSWORD" >> ./gradle.properties
echo -e "KEY_ALIAS=$KEYSTORE_EXPERIMENTAL_ALIAS" >> ./gradle.properties echo -e "KEY_ALIAS=$KEY_ALIAS" >> ./gradle.properties
echo -e "KEY_PASSWORD=$KEYSTORE_EXPERIMENTAL_PASSWORD" >> ./gradle.properties echo -e "KEY_PASSWORD=$KEYSTORE_PASSWORD" >> ./gradle.properties
fi fi
working_directory: android working_directory: android
- run: - run:
name: Set Google Services name: Set Google Services
command: | command: |
if [[ $GOOGLE_SERVICES_ANDROID ]]; then if [[ $KEYSTORE ]]; then
echo $GOOGLE_SERVICES_ANDROID | base64 --decode > google-services.json echo $GOOGLE_SERVICES_ANDROID | base64 --decode > google-services.json
fi fi
working_directory: android/app working_directory: android/app
@ -154,7 +151,7 @@ commands:
if [[ $CIRCLE_JOB == "android-build-experimental" || "android-automatic-build-experimental" ]]; then if [[ $CIRCLE_JOB == "android-build-experimental" || "android-automatic-build-experimental" ]]; then
./gradlew bundleExperimentalPlayRelease ./gradlew bundleExperimentalPlayRelease
fi fi
if [[ ! $GOOGLE_SERVICES_ANDROID ]]; then if [[ ! $KEYSTORE ]]; then
./gradlew assembleExperimentalPlayDebug ./gradlew assembleExperimentalPlayDebug
fi fi
working_directory: android working_directory: android
@ -203,12 +200,8 @@ commands:
- run: - run:
name: Set Google Services name: Set Google Services
command: | command: |
if [[ $APP_STORE_CONNECT_API_KEY_BASE64 ]]; then if [[ $KEYSTORE ]]; then
if [[ $CIRCLE_JOB == "ios-build-official" ]]; then
echo $GOOGLE_SERVICES_IOS | base64 --decode > GoogleService-Info.plist echo $GOOGLE_SERVICES_IOS | base64 --decode > GoogleService-Info.plist
else
echo $GOOGLE_SERVICES_IOS_EXPERIMENTAL | base64 --decode > GoogleService-Info.plist
fi
fi fi
working_directory: ios working_directory: ios
- run: - run:
@ -230,12 +223,12 @@ commands:
/usr/libexec/PlistBuddy -c "Set IS_OFFICIAL NO" ./NotificationService/Info.plist /usr/libexec/PlistBuddy -c "Set IS_OFFICIAL NO" ./NotificationService/Info.plist
fi fi
if [[ $APP_STORE_CONNECT_API_KEY_BASE64 ]]; then if [[ $APP_STORE_CONNECT_API_BASE64 ]]; then
echo $APP_STORE_CONNECT_API_KEY_BASE64 | base64 --decode > ./fastlane/app_store_connect_api_key.p8 echo $APP_STORE_CONNECT_API_BASE64 | base64 --decode > ./fastlane/app_store_connect_api_key.p8
if [[ $CIRCLE_JOB == "ios-build-official" ]]; then if [[ $CIRCLE_JOB == "ios-build-official" ]]; then
bundle exec fastlane ios build_official bundle exec fastlane ios build_official
else else
if [[ $APP_STORE_CONNECT_API_KEY_BASE64 ]]; then if [[ $KEYSTORE ]]; then
bundle exec fastlane ios build_experimental bundle exec fastlane ios build_experimental
else else
bundle exec fastlane ios build_fork bundle exec fastlane ios build_fork
@ -325,19 +318,11 @@ commands:
- run: - run:
name: Fastlane Tesflight Upload name: Fastlane Tesflight Upload
command: | command: |
echo $APP_STORE_CONNECT_API_KEY_BASE64 | base64 --decode > ./fastlane/app_store_connect_api_key.p8 echo $APP_STORE_CONNECT_API_BASE64 | base64 --decode > ./fastlane/app_store_connect_api_key.p8
bundle exec fastlane ios beta official:<< parameters.official >> bundle exec fastlane ios beta official:<< parameters.official >>
working_directory: ios working_directory: ios
- save_cache: *save-gems-cache - 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 version: 2.1
# EXECUTORS # EXECUTORS
@ -449,94 +434,6 @@ jobs:
- upload-to-google-play-beta: - upload-to-google-play-beta:
official: true 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 builds
ios-build-experimental: ios-build-experimental:
executor: mac-env executor: mac-env
@ -560,89 +457,11 @@ jobs:
- upload-to-testflight: - upload-to-testflight:
official: true 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: workflows:
build-and-test: build-and-test:
jobs: jobs:
- lint-testunit - 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 Experimental
- ios-hold-build-experimental: - ios-hold-build-experimental:
type: approval type: approval

View File

@ -1,91 +0,0 @@
/** @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
View File

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

View File

@ -240,8 +240,19 @@ module.exports = {
}, },
{ {
files: ['e2e/**'], files: ['e2e/**'],
globals: {
by: true,
detox: true,
device: true,
element: true,
waitFor: true
},
rules: { rules: {
'no-await-in-loop': 0 '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
} }
} }
] ]

1
.gitignore vendored
View File

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

View File

@ -1 +1 @@
2.7.7 2.7.4

View File

@ -1,4 +1,4 @@
source 'https://rubygems.org' source 'https://rubygems.org'
# You may use http://rbenv.org/ or https://rvm.io/ to install and use this version # You may use http://rbenv.org/ or https://rvm.io/ to install and use this version
ruby '2.7.7' ruby '2.7.4'
gem 'cocoapods', '~> 1.11', '>= 1.11.2' 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 minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion
versionCode VERSIONCODE as Integer versionCode VERSIONCODE as Integer
versionName "4.37.0" versionName "4.35.0"
vectorDrawables.useSupportLibrary = true vectorDrawables.useSupportLibrary = true
if (!isFoss) { if (!isFoss) {
manifestPlaceholders = [BugsnagAPIKey: BugsnagAPIKey as String] manifestPlaceholders = [BugsnagAPIKey: BugsnagAPIKey as String]
@ -250,7 +250,6 @@ android {
release { release {
minifyEnabled enableProguardInReleaseBuilds minifyEnabled enableProguardInReleaseBuilds
setProguardFiles([getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro']) setProguardFiles([getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'])
proguardFile "${rootProject.projectDir}/../node_modules/detox/android/detox/proguard-rules-app.pro"
signingConfig signingConfigs.release signingConfig signingConfigs.release
if (!isFoss) { if (!isFoss) {
firebaseCrashlytics { firebaseCrashlytics {
@ -269,11 +268,6 @@ android {
// pickFirst '**/x86_64/libc++_shared.so' // pickFirst '**/x86_64/libc++_shared.so'
// } // }
// FIXME: Remove when we update RN
packagingOptions {
pickFirst '**/*.so'
}
// applicationVariants are e.g. debug, release // applicationVariants are e.g. debug, release
flavorDimensions "app", "type" flavorDimensions "app", "type"
@ -286,6 +280,10 @@ android {
dimension = "app" dimension = "app"
buildConfigField "boolean", "IS_OFFICIAL", "false" buildConfigField "boolean", "IS_OFFICIAL", "false"
} }
e2e {
dimension = "app"
buildConfigField "boolean", "IS_OFFICIAL", "false"
}
foss { foss {
dimension = "type" dimension = "type"
buildConfigField "boolean", "FDROID_BUILD", "true" buildConfigField "boolean", "FDROID_BUILD", "true"
@ -313,6 +311,16 @@ android {
java.srcDirs = ['src/main/java', 'src/play/java'] java.srcDirs = ['src/main/java', 'src/play/java']
manifest.srcFile 'src/play/AndroidManifest.xml' 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 -> applicationVariants.all { variant ->
@ -377,9 +385,8 @@ dependencies {
implementation "com.github.bumptech.glide:glide:4.9.0" implementation "com.github.bumptech.glide:glide:4.9.0"
annotationProcessor "com.github.bumptech.glide:compiler:4.9.0" annotationProcessor "com.github.bumptech.glide:compiler:4.9.0"
implementation "com.tencent:mmkv-static:1.2.10" implementation "com.tencent:mmkv-static:1.2.10"
androidTestImplementation('com.wix:detox:+') androidTestImplementation('com.wix:detox:+') { transitive = true }
implementation 'androidx.appcompat:appcompat:1.1.0' androidTestImplementation 'junit:junit:4.12'
implementation 'com.facebook.soloader:soloader:0.10.4'
} }
if (isNewArchitectureEnabled()) { if (isNewArchitectureEnabled()) {

View File

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

View File

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

View File

@ -0,0 +1,10 @@
<?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,9 +11,6 @@
<uses-permission android:name="android.permission.VIDEO_CAPTURE" /> <uses-permission android:name="android.permission.VIDEO_CAPTURE" />
<uses-permission android:name="android.permission.AUDIO_CAPTURE" /> <uses-permission android:name="android.permission.AUDIO_CAPTURE" />
<!-- permissions related to jitsi call -->
<uses-permission android:name="android.permission.BLUETOOTH" />
<application <application
android:name="chat.rocket.reactnative.MainApplication" android:name="chat.rocket.reactnative.MainApplication"
android:allowBackup="false" android:allowBackup="false"

View File

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

View File

@ -1,5 +1,9 @@
import org.apache.tools.ant.taskdefs.condition.Os 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. // Top-level build file where you can add configuration options common to all sub-projects/modules.
buildscript { buildscript {
def taskRequests = getGradle().getStartParameter().getTaskRequests().toString().toLowerCase() def taskRequests = getGradle().getStartParameter().getTaskRequests().toString().toLowerCase()
@ -71,38 +75,5 @@ allprojects {
google() google()
maven { url 'https://maven.google.com' } maven { url 'https://maven.google.com' }
maven { url 'https://www.jitpack.io' } 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,9 +28,7 @@ export const ROOM = createRequestTypes('ROOM', [
'DELETE', 'DELETE',
'REMOVED', 'REMOVED',
'FORWARD', 'FORWARD',
'USER_TYPING', 'USER_TYPING'
'HISTORY_REQUEST',
'HISTORY_FINISHED'
]); ]);
export const INQUIRY = createRequestTypes('INQUIRY', [ export const INQUIRY = createRequestTypes('INQUIRY', [
...defaultTypes, ...defaultTypes,
@ -40,14 +38,7 @@ export const INQUIRY = createRequestTypes('INQUIRY', [
'QUEUE_UPDATE', 'QUEUE_UPDATE',
'QUEUE_REMOVE' 'QUEUE_REMOVE'
]); ]);
export const APP = createRequestTypes('APP', [ export const APP = createRequestTypes('APP', ['START', 'READY', 'INIT', 'INIT_LOCAL_SETTINGS', 'SET_MASTER_DETAIL']);
'START',
'READY',
'INIT',
'INIT_LOCAL_SETTINGS',
'SET_MASTER_DETAIL',
'SET_NOTIFICATION_PRESENCE_CAP'
]);
export const MESSAGES = createRequestTypes('MESSAGES', ['REPLY_BROADCAST']); export const MESSAGES = createRequestTypes('MESSAGES', ['REPLY_BROADCAST']);
export const CREATE_CHANNEL = createRequestTypes('CREATE_CHANNEL', [...defaultTypes]); export const CREATE_CHANNEL = createRequestTypes('CREATE_CHANNEL', [...defaultTypes]);
export const CREATE_DISCUSSION = createRequestTypes('CREATE_DISCUSSION', [...defaultTypes]); export const CREATE_DISCUSSION = createRequestTypes('CREATE_DISCUSSION', [...defaultTypes]);

View File

@ -12,11 +12,7 @@ interface ISetMasterDetail extends Action {
isMasterDetail: boolean; isMasterDetail: boolean;
} }
interface ISetNotificationPresenceCap extends Action { export type TActionApp = IAppStart & ISetMasterDetail;
show: boolean;
}
export type TActionApp = IAppStart & ISetMasterDetail & ISetNotificationPresenceCap;
interface Params { interface Params {
root: RootEnum; root: RootEnum;
@ -55,10 +51,3 @@ export function setMasterDetail(isMasterDetail: boolean): ISetMasterDetail {
isMasterDetail 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 { Action } from 'redux';
import { ERoomType, RoomType } from '../definitions'; import { ERoomType } from '../definitions/ERoomType';
import { ROOM } from './actionsTypes'; import { ROOM } from './actionsTypes';
// TYPE RETURN RELATED // TYPE RETURN RELATED
@ -44,24 +44,7 @@ interface IUserTyping extends Action {
status: boolean; status: boolean;
} }
export interface IRoomHistoryRequest extends Action { export type TActionsRoom = TSubscribeRoom & TUnsubscribeRoom & ILeaveRoom & IDeleteRoom & IForwardRoom & IUserTyping;
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 { export function subscribeRoom(rid: string): TSubscribeRoom {
return { return {
@ -116,19 +99,3 @@ export function userTyping(rid: string, status = true): IUserTyping {
status 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,5 +1,4 @@
export const mappedIcons = { export const mappedIcons = {
'status-disabled': 59837,
'lamp-bulb': 59836, 'lamp-bulb': 59836,
'phone-in': 59835, 'phone-in': 59835,
'basketball': 59776, 'basketball': 59776,

File diff suppressed because one or more lines are too long

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,4 +1,4 @@
import React from 'react'; import React, { forwardRef, useImperativeHandle } from 'react';
import Animated, { import Animated, {
useAnimatedGestureHandler, useAnimatedGestureHandler,
useSharedValue, useSharedValue,
@ -13,21 +13,25 @@ import {
HandlerStateChangeEventPayload, HandlerStateChangeEventPayload,
PanGestureHandlerEventPayload PanGestureHandlerEventPayload
} from 'react-native-gesture-handler'; } from 'react-native-gesture-handler';
import { useWindowDimensions } from 'react-native';
import Touch from '../Touch'; import Touch from '../Touch';
import { ACTION_WIDTH, LONG_SWIPE, SMALL_SWIPE } from './styles'; import { ACTION_WIDTH, LONG_SWIPE, SMALL_SWIPE } from './styles';
import { LeftActions, RightActions } from './Actions'; import { LeftActions, RightActions } from './Actions';
import { ITouchableProps } from './interfaces'; import { ITouchableProps, ITouchableRef } from './interfaces';
import { useTheme } from '../../theme'; import { useTheme } from '../../theme';
import I18n from '../../i18n'; import I18n from '../../i18n';
import { MAX_SIDEBAR_WIDTH } from '../../lib/constants';
import { useAppSelector } from '../../lib/hooks';
const Touchable = ({ const Touchable = forwardRef<ITouchableRef, ITouchableProps>(
(
{
children, children,
type, type,
onPress, onPress,
onLongPress, onLongPress,
testID, testID,
width,
favorite, favorite,
isRead, isRead,
rid, rid,
@ -37,8 +41,13 @@ const Touchable = ({
isFocused, isFocused,
swipeEnabled, swipeEnabled,
displayMode displayMode
}: ITouchableProps): React.ReactElement => { },
ref
): React.ReactElement => {
const { colors } = useTheme(); const { colors } = useTheme();
const { width: deviceWidth } = useWindowDimensions();
const isMasterDetail = useAppSelector(state => state.app.isMasterDetail);
const width = isMasterDetail ? MAX_SIDEBAR_WIDTH : deviceWidth;
const rowOffSet = useSharedValue(0); const rowOffSet = useSharedValue(0);
const transX = useSharedValue(0); const transX = useSharedValue(0);
@ -46,11 +55,16 @@ const Touchable = ({
let _value = 0; let _value = 0;
const close = () => { const close = () => {
console.log(`${rid} close`);
rowState.value = 0; rowState.value = 0;
transX.value = withSpring(0, { overshootClamping: true }); transX.value = withSpring(0, { overshootClamping: true });
rowOffSet.value = 0; rowOffSet.value = 0;
}; };
useImperativeHandle(ref, () => ({
close
}));
const handleToggleFav = () => { const handleToggleFav = () => {
if (toggleFav) { if (toggleFav) {
toggleFav(rid, favorite); toggleFav(rid, favorite);
@ -234,6 +248,7 @@ const Touchable = ({
</Animated.View> </Animated.View>
</LongPressGestureHandler> </LongPressGestureHandler>
); );
}; }
);
export default Touchable; export default Touchable;

View File

@ -1,26 +1,21 @@
import React, { useEffect, useReducer, useRef } from 'react'; import React, { useEffect, useRef } from 'react';
import { Subscription } from 'rxjs';
import I18n from '../../i18n'; import I18n from '../../i18n';
import { useAppSelector } from '../../lib/hooks';
import { getUserPresence } from '../../lib/methods'; import { getUserPresence } from '../../lib/methods';
import { isGroupChat } from '../../lib/methods/helpers'; import { isGroupChat } from '../../lib/methods/helpers';
import { formatDate } from '../../lib/methods/helpers/room'; import { formatDate } from '../../lib/methods/helpers/room';
import { IRoomItemContainerProps } from './interfaces'; import { IRoomItemContainerProps, ITouchableRef } from './interfaces';
import RoomItem from './RoomItem'; import RoomItem from './RoomItem';
import { ROW_HEIGHT, ROW_HEIGHT_CONDENSED } from './styles'; import { ROW_HEIGHT, ROW_HEIGHT_CONDENSED } from './styles';
import { useUserStatus } from './useUserStatus';
export { ROW_HEIGHT, ROW_HEIGHT_CONDENSED }; export { ROW_HEIGHT, ROW_HEIGHT_CONDENSED };
const attrs = ['width', 'isFocused', 'showLastMessage', 'autoJoin', 'showAvatar', 'displayMode']; const RoomItemContainer = ({
const RoomItemContainer = React.memo(
({
item, item,
id, id,
onPress, onPress,
onLongPress, onLongPress,
width,
toggleFav, toggleFav,
toggleRead, toggleRead,
hideChannel, hideChannel,
@ -35,39 +30,38 @@ const RoomItemContainer = React.memo(
getRoomAvatar = () => '', getRoomAvatar = () => '',
getIsRead = () => false, getIsRead = () => false,
swipeEnabled = true swipeEnabled = true
}: IRoomItemContainerProps) => { }: IRoomItemContainerProps): React.ReactElement => {
const name = getRoomTitle(item); const name = getRoomTitle(item);
const testID = `rooms-list-view-item-${name}`; const testID = `rooms-list-view-item-${name}`;
const avatar = getRoomAvatar(item); const avatar = getRoomAvatar(item);
const isRead = getIsRead(item); const isRead = getIsRead(item);
const date = item.roomUpdatedAt && formatDate(item.roomUpdatedAt); const date = item.roomUpdatedAt && formatDate(item.roomUpdatedAt);
const alert = item.alert || item.tunread?.length; const alert = item.alert || item.tunread?.length;
const [_, forceUpdate] = useReducer(x => x + 1, 1); const connected = useAppSelector(state => state.meteor.connected);
const roomSubscription = useRef<Subscription | null>(null); const userStatus = useAppSelector(state => state.activeUsers[id || '']?.status);
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)); const isDirect = !!(item.t === 'd' && id && !isGroupChat(item));
const touchableRef = useRef<ITouchableRef>(null);
// When app reconnects, we need to fetch the rendered user's presence
useEffect(() => {
if (connected && isDirect) { if (connected && isDirect) {
getUserPresence(id); getUserPresence(id);
} }
}, [connected]); }, [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);
}
// TODO: Remove this when we have a better way to close the swipeable
touchableRef?.current?.close();
}, [item.rid]);
const handleOnPress = () => onPress(item); const handleOnPress = () => onPress(item);
const handleOnLongPress = () => onLongPress && onLongPress(item); const handleOnLongPress = () => onLongPress && onLongPress(item);
@ -85,8 +79,11 @@ const RoomItemContainer = React.memo(
accessibilityLabel = `, ${I18n.t('last_message')} ${date}`; accessibilityLabel = `, ${I18n.t('last_message')} ${date}`;
} }
const status = item.t === 'l' ? item.visitor?.status || item.v?.status : userStatus;
return ( return (
<RoomItem <RoomItem
touchableRef={touchableRef}
name={name} name={name}
avatar={avatar} avatar={avatar}
isGroupChat={isGroupChat(item)} isGroupChat={isGroupChat(item)}
@ -95,7 +92,6 @@ const RoomItemContainer = React.memo(
onLongPress={handleOnLongPress} onLongPress={handleOnLongPress}
date={date} date={date}
accessibilityLabel={accessibilityLabel} accessibilityLabel={accessibilityLabel}
width={width}
favorite={item.f} favorite={item.f}
rid={item.rid} rid={item.rid}
toggleFav={toggleFav} toggleFav={toggleFav}
@ -127,8 +123,5 @@ const RoomItemContainer = React.memo(
sourceType={item.source} sourceType={item.source}
/> />
); );
}, };
(props, nextProps) => attrs.every(key => props[key] === nextProps[key])
);
export default RoomItemContainer; export default RoomItemContainer;

View File

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

View File

@ -1,30 +0,0 @@
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, { useState } from 'react'; import React from 'react';
import { StyleProp, ViewStyle } from 'react-native'; import { StyleProp, ViewStyle } from 'react-native';
import { SvgUri } from 'react-native-svg'; import { SvgUri } from 'react-native-svg';
@ -29,12 +29,22 @@ interface IOmnichannelRoomIconProps {
} }
export const OmnichannelRoomIcon = ({ size, style, sourceType, status }: 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 baseUrl = useAppSelector(state => state.server?.server);
const connected = useAppSelector(state => state.meteor?.connected); const connected = useAppSelector(state => state.meteor?.connected);
const customIcon = ( 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 (
<CustomIcon <CustomIcon
name={iconMap[sourceType?.type || 'other']} name={iconMap[sourceType?.type || 'other']}
size={size} size={size}
@ -42,23 +52,4 @@ export const OmnichannelRoomIcon = ({ size, style, sourceType, status }: IOmnich
color={STATUS_COLORS[status || 'offline']} 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,15 +6,9 @@ import { IStatus } from './definition';
import { useAppSelector } from '../../lib/hooks'; import { useAppSelector } from '../../lib/hooks';
const StatusContainer = ({ id, style, size = 32, ...props }: Omit<IStatus, 'status'>): React.ReactElement => { const StatusContainer = ({ id, style, size = 32, ...props }: Omit<IStatus, 'status'>): React.ReactElement => {
const status = useAppSelector(state => { const status = useAppSelector(state =>
if (state.settings.Presence_broadcast_disabled) { state.meteor.connected ? state.activeUsers[id] && state.activeUsers[id].status : 'loading'
return 'disabled'; ) as TUserStatus;
}
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} />; return <Status size={size} style={style} status={status} {...props} />;
}; };

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,64 +0,0 @@
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,29 +18,13 @@ import MarkdownContext from './MarkdownContext';
interface IParagraphProps { interface IParagraphProps {
value: ParagraphProps['value']; value: ParagraphProps['value'];
forceTrim?: boolean;
} }
const Inline = ({ value, forceTrim }: IParagraphProps): React.ReactElement | null => { const Inline = ({ value }: IParagraphProps): React.ReactElement | null => {
const { useRealName, username, navToRoomInfo, mentions, channels } = useContext(MarkdownContext); const { useRealName, username, navToRoomInfo, mentions, channels } = useContext(MarkdownContext);
return ( return (
<Text style={styles.inline}> <Text style={styles.inline}>
{value.map((block, index) => { {value.map(block => {
// 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) { switch (block.type) {
case 'IMAGE': case 'IMAGE':
return <Image value={block.value} />; return <Image value={block.value} />;

View File

@ -381,6 +381,27 @@ export const BlockQuote = () => (
</View> </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 = [ const markdownLink = [
{ {
type: 'PARAGRAPH', type: 'PARAGRAPH',
@ -466,6 +487,7 @@ const markdownLinkWithEmphasis = [
export const Links = () => ( export const Links = () => (
<View style={styles.container}> <View style={styles.container}>
<NewMarkdown tokens={rocketChatLink} />
<NewMarkdown tokens={markdownLink} /> <NewMarkdown tokens={markdownLink} />
<NewMarkdown tokens={markdownLinkWithEmphasis} /> <NewMarkdown tokens={markdownLinkWithEmphasis} />
</View> </View>
@ -784,128 +806,3 @@ export const InlineKatex = () => (
<NewMarkdown tokens={inlineKatex} /> <NewMarkdown tokens={inlineKatex} />
</View> </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,28 +12,10 @@ interface IParagraphProps {
} }
const Paragraph = ({ value }: IParagraphProps) => { const Paragraph = ({ value }: IParagraphProps) => {
let forceTrim = false;
const { theme } = useTheme(); 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 ( return (
<Text style={[styles.text, { color: themes[theme].bodyText }]}> <Text style={[styles.text, { color: themes[theme].bodyText }]}>
<Inline value={value} forceTrim={forceTrim} /> <Inline value={value} />
</Text> </Text>
); );
}; };

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -10,7 +10,7 @@ const Emoji = React.memo(
const parsedContent = content.replace(/^:|:$/g, ''); const parsedContent = content.replace(/^:|:$/g, '');
const emoji = getCustomEmoji(parsedContent); const emoji = getCustomEmoji(parsedContent);
if (emoji) { if (emoji) {
return <CustomEmoji key={content} style={customEmojiStyle} emoji={emoji} />; return <CustomEmoji style={customEmojiStyle} emoji={emoji} />;
} }
return <Text style={standardEmojiStyle}>{shortnameToUnicode(content)}</Text>; 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} style={small ? styles.avatarSmall : styles.avatar}
text={avatar ? '' : author.username} text={avatar ? '' : author.username}
size={small ? 20 : 36} size={small ? 20 : 36}
borderRadius={4} borderRadius={small ? 2 : 4}
onPress={author._id === user.id ? undefined : () => navToRoomInfo(navParam)} onPress={author._id === user.id ? undefined : () => navToRoomInfo(navParam)}
getCustomEmoji={getCustomEmoji} getCustomEmoji={getCustomEmoji}
avatar={avatar} avatar={avatar}

View File

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

View File

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

View File

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

View File

@ -1,6 +1,5 @@
import React from 'react'; import React from 'react';
import { Keyboard, ViewStyle } from 'react-native'; import { Keyboard } from 'react-native';
import { Subscription } from 'rxjs';
import Message from './Message'; import Message from './Message';
import MessageContext from './Context'; import MessageContext from './Context';
@ -8,67 +7,15 @@ import { debounce } from '../../lib/methods/helpers';
import { getMessageTranslation } from './utils'; import { getMessageTranslation } from './utils';
import { TSupportedThemes, withTheme } from '../../theme'; import { TSupportedThemes, withTheme } from '../../theme';
import openLink from '../../lib/methods/helpers/openLink'; import openLink from '../../lib/methods/helpers/openLink';
import { IAttachment, TAnyMessageModel, TGetCustomEmoji } from '../../definitions'; import { IAttachment } from '../../definitions';
import { IRoomInfoParam } from '../../views/SearchMessagesView';
import { E2E_MESSAGE_TYPE, E2E_STATUS, messagesStatus } from '../../lib/constants'; import { E2E_MESSAGE_TYPE, E2E_STATUS, messagesStatus } from '../../lib/constants';
import { IMessageContainerProps, TAnyMessageContainerState } from './interfaces';
interface IMessageContainerProps { class MessageContainer extends React.Component<IMessageContainerProps, TAnyMessageContainerState> {
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 = { static defaultProps = {
getCustomEmoji: () => null, getCustomEmoji: () => null,
onLongPress: () => {}, onLongPress: () => {},
callJitsi: () => {},
blockAction: () => {}, blockAction: () => {},
archived: false, archived: false,
broadcast: false, broadcast: false,
@ -78,45 +25,6 @@ class MessageContainer extends React.Component<IMessageContainerProps, IMessageC
state = { isManualUnignored: false }; 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 = () => { onPressAction = () => {
const { closeEmojiAndAction } = this.props; const { closeEmojiAndAction } = this.props;
@ -226,11 +134,11 @@ class MessageContainer extends React.Component<IMessageContainerProps, IMessageC
try { try {
if ( if (
previousItem && previousItem &&
// @ts-ignore TODO: IMessage vs IMessageFromServer non-sense // @ts-ignore TODO: TAnyMessage vs TAnyMessageFromServer non-sense
previousItem.ts.toDateString() === item.ts.toDateString() && previousItem.ts.toDateString() === item.ts.toDateString() &&
previousItem.u.username === item.u.username && previousItem.u.username === item.u.username &&
!(previousItem.groupable === false || item.groupable === false || broadcast === true) && !(previousItem.groupable === false || item.groupable === false || broadcast === true) &&
// @ts-ignore TODO: IMessage vs IMessageFromServer non-sense // @ts-ignore TODO: TAnyMessage vs TAnyMessageFromServer non-sense
item.ts - previousItem.ts < Message_GroupingPeriod * 1000 && item.ts - previousItem.ts < Message_GroupingPeriod * 1000 &&
previousItem.tmid === item.tmid && previousItem.tmid === item.tmid &&
item.t !== 'rm' && item.t !== 'rm' &&
@ -337,7 +245,7 @@ class MessageContainer extends React.Component<IMessageContainerProps, IMessageC
navToRoomInfo, navToRoomInfo,
getCustomEmoji, getCustomEmoji,
isThreadRoom, isThreadRoom,
handleEnterCall, callJitsi,
blockAction, blockAction,
rid, rid,
threadBadgeColor, threadBadgeColor,
@ -408,7 +316,6 @@ class MessageContainer extends React.Component<IMessageContainerProps, IMessageC
replies replies
}} }}
> >
{/* @ts-ignore*/}
<Message <Message
id={id} id={id}
msg={message} msg={message}
@ -455,7 +362,7 @@ class MessageContainer extends React.Component<IMessageContainerProps, IMessageC
showAttachment={showAttachment} showAttachment={showAttachment}
getCustomEmoji={getCustomEmoji} getCustomEmoji={getCustomEmoji}
navToRoomInfo={navToRoomInfo} navToRoomInfo={navToRoomInfo}
handleEnterCall={handleEnterCall} callJitsi={callJitsi}
blockAction={blockAction} blockAction={blockAction}
highlighted={highlighted} highlighted={highlighted}
comment={comment} comment={comment}

View File

@ -1,11 +1,65 @@
import { MarkdownAST } from '@rocket.chat/message-parser'; import { MarkdownAST } from '@rocket.chat/message-parser';
import { StyleProp, TextStyle } from 'react-native'; import { StyleProp, TextStyle, ViewStyle } from 'react-native';
import { ImageStyle } from 'react-native-fast-image'; import { ImageStyle } from 'react-native-fast-image';
import { IUserChannel } from '../markdown/interfaces'; import { IUserChannel } from '../markdown/interfaces';
import { TGetCustomEmoji } from '../../definitions/IEmoji'; import { TGetCustomEmoji } from '../../definitions/IEmoji';
import { IAttachment, IThread, IUrl, IUserMention, IUserMessage, MessageType, TAnyMessageModel } from '../../definitions'; import { IAttachment, IThread, IUrl, IUserMention, IUserMessage, MessageType, TAnyMessage } from '../../definitions';
import { IRoomInfoParam } from '../../views/SearchMessagesView'; 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 { export interface IMessageAttachments {
attachments?: IAttachment[]; attachments?: IAttachment[];
@ -40,11 +94,10 @@ export interface IMessageBroadcast {
} }
export interface IMessageCallButton { export interface IMessageCallButton {
handleEnterCall?: () => void; callJitsi?: () => void;
} }
export interface IMessageContent { export interface IMessageContent {
_id: string;
isTemp: boolean; isTemp: boolean;
isInfo: string | boolean; isInfo: string | boolean;
tmid?: string; tmid?: string;
@ -119,7 +172,6 @@ export interface IMessage extends IMessageRepliedThread, IMessageInner, IMessage
hasError: boolean; hasError: boolean;
style: any; style: any;
// style: ViewStyle; // style: ViewStyle;
onLongPress?: (item: TAnyMessageModel) => void;
isReadReceiptEnabled?: boolean; isReadReceiptEnabled?: boolean;
unread?: boolean; unread?: boolean;
isIgnored: boolean; isIgnored: boolean;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -876,20 +876,5 @@
"Call": "Call", "Call": "Call",
"Reply_in_direct_message": "Reply in Direct Message", "Reply_in_direct_message": "Reply in Direct Message",
"room_archived": "archived room", "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,7 +12,6 @@
"error-could-not-change-email": "Não foi possível mudar e-mail", "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-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-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-delete-protected-role": "Não é possível remover um papel protegido",
"error-department-not-found": "Departamento não encontrado", "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", "error-direct-message-file-upload-not-allowed": "Compartilhamento de arquivos não está permitido em mensagens diretas",
@ -20,7 +19,6 @@
"error-email-domain-blacklisted": "O domínio de e-mail está na lista negra", "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-email-send-failed": "Erro ao tentar enviar e-mail: {{message}}",
"error-save-image": "Erro ao salvar imagem", "error-save-image": "Erro ao salvar imagem",
"error-save-video": "Erro ao salvar vídeo",
"error-field-unavailable": "{{field}} já está sendo usado :(", "error-field-unavailable": "{{field}} já está sendo usado :(",
"error-file-too-large": "Arquivo é muito grande", "error-file-too-large": "Arquivo é muito grande",
"error-not-permission-to-upload-file": "Você não tem permissão para enviar arquivos", "error-not-permission-to-upload-file": "Você não tem permissão para enviar arquivos",
@ -90,7 +88,6 @@
"Add_Reaction": "Reagir", "Add_Reaction": "Reagir",
"Add_Server": "Adicionar servidor", "Add_Server": "Adicionar servidor",
"Add_users": "Adicionar usuário", "Add_users": "Adicionar usuário",
"Admin_Panel": "Painel de admin",
"Agent": "Agente", "Agent": "Agente",
"Alert": "Alerta", "Alert": "Alerta",
"alert": "alerta", "alert": "alerta",
@ -99,7 +96,6 @@
"All_users_in_the_team_can_write_new_messages": "Todos usuários no canal podem enviar mensagens novas", "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", "A_meaningful_name_for_the_discussion_room": "Um nome significativo para o canal de discussão",
"All": "Todos", "All": "Todos",
"All_Messages": "Todas as mensagens",
"Allow_Reactions": "Permitir reagir", "Allow_Reactions": "Permitir reagir",
"Alphabetical": "Alfabético", "Alphabetical": "Alfabético",
"and_more": "e mais", "and_more": "e mais",
@ -167,10 +163,7 @@
"Copied_to_clipboard": "Copiado para a área de transferência!", "Copied_to_clipboard": "Copiado para a área de transferência!",
"Copy": "Copiar", "Copy": "Copiar",
"Conversation": "Conversação", "Conversation": "Conversação",
"Certificate_password": "Senha do certificado",
"Clear_cache": "Limpar cache da workspace",
"Clear_cache_loading": "Limpando cache.", "Clear_cache_loading": "Limpando cache.",
"Whats_the_password_for_your_certificate": "Qual é a senha para o seu certificado?",
"Create_account": "Criar conta", "Create_account": "Criar conta",
"Create_Channel": "Criar Canal", "Create_Channel": "Criar Canal",
"Create_Direct_Messages": "Criar Mensagens Diretas", "Create_Direct_Messages": "Criar Mensagens Diretas",
@ -178,7 +171,6 @@
"Created_snippet": "criou um snippet", "Created_snippet": "criou um snippet",
"Create_a_new_workspace": "Criar nova área de trabalho", "Create_a_new_workspace": "Criar nova área de trabalho",
"Create": "Criar", "Create": "Criar",
"Custom_Status": "Status personalizado",
"Dark": "Escuro", "Dark": "Escuro",
"Dark_level": "Nível escuro", "Dark_level": "Nível escuro",
"Default": "Padrão", "Default": "Padrão",
@ -263,7 +255,6 @@
"Has_left_the_team": "saiu da equipe", "Has_left_the_team": "saiu da equipe",
"Hide_System_Messages": "Esconder mensagens do sistema", "Hide_System_Messages": "Esconder mensagens do sistema",
"Hide_type_messages": "Esconder mensagens de \"{{type}}\"", "Hide_type_messages": "Esconder mensagens de \"{{type}}\"",
"How_It_Works": "Como funciona",
"Message_HideType_uj": "Utilizador Entrou", "Message_HideType_uj": "Utilizador Entrou",
"Message_HideType_ul": "Utilizador Saiu", "Message_HideType_ul": "Utilizador Saiu",
"Message_HideType_ru": "Utilizador Removido", "Message_HideType_ru": "Utilizador Removido",
@ -277,15 +268,11 @@
"Message_HideType_subscription_role_removed": "Papel removido", "Message_HideType_subscription_role_removed": "Papel removido",
"Message_HideType_room_archived": "Sala arquivada", "Message_HideType_room_archived": "Sala arquivada",
"Message_HideType_room_unarchived": "Sala desarquivada", "Message_HideType_room_unarchived": "Sala desarquivada",
"I_Saved_My_E2E_Password": "Salvei minha senha ponta-a-ponta",
"IP": "IP", "IP": "IP",
"In_app": "No app", "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", "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", "Invisible": "Invisível",
"Invite": "Convidar", "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", "is_typing": "está digitando",
"Invalid_or_expired_invite_token": "Token de convite inválido ou vencido", "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.", "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.",
@ -306,9 +293,7 @@
"leave": "sair", "leave": "sair",
"Legal": "Legal", "Legal": "Legal",
"Light": "Claro", "Light": "Claro",
"License": "Licença",
"Livechat": "Livechat", "Livechat": "Livechat",
"Livechat_edit": "Editar livechat",
"Livechat_transfer_return_to_the_queue": "retornou conversa para a fila", "Livechat_transfer_return_to_the_queue": "retornou conversa para a fila",
"Login": "Entrar", "Login": "Entrar",
"Login_error": "Suas credenciais foram rejeitadas. Tente novamente por favor!", "Login_error": "Suas credenciais foram rejeitadas. Tente novamente por favor!",
@ -317,7 +302,6 @@
"Logout": "Sair", "Logout": "Sair",
"Max_number_of_uses": "Número máximo de usos", "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}}", "Max_number_of_users_allowed_is_number": "Número máximo de usuários é {{maxUsers}}",
"members": "membros",
"Members": "Membros", "Members": "Membros",
"Mentioned_Messages": "Mensagens mencionadas", "Mentioned_Messages": "Mensagens mencionadas",
"mentioned": "mencionado", "mentioned": "mencionado",
@ -326,18 +310,14 @@
"Message_actions": "Ações", "Message_actions": "Ações",
"Message_pinned": "Fixou uma mensagem", "Message_pinned": "Fixou uma mensagem",
"Message_removed": "mensagem removida", "Message_removed": "mensagem removida",
"Message_starred": "Mensagem adicionada aos favoritos",
"Message_unstarred": "Mensagem removida dos favoritos",
"message": "mensagem", "message": "mensagem",
"messages": "mensagens", "messages": "mensagens",
"Message": "Mensagem", "Message": "Mensagem",
"Messages": "Mensagens", "Messages": "Mensagens",
"Message_Reported": "Mensagem reportada",
"Microphone_Permission_Message": "Rocket.Chat precisa de acesso ao seu microfone para enviar mensagens de áudio.", "Microphone_Permission_Message": "Rocket.Chat precisa de acesso ao seu microfone para enviar mensagens de áudio.",
"Microphone_Permission": "Acesso ao Microfone", "Microphone_Permission": "Acesso ao Microfone",
"Mute": "Mudo", "Mute": "Mudo",
"muted": "mudo", "muted": "mudo",
"My_servers": "Minhas workspaces",
"N_people_reacted": "{{n}} pessoas reagiram", "N_people_reacted": "{{n}} pessoas reagiram",
"N_users": "{{n}} usuários", "N_users": "{{n}} usuários",
"N_channels": "{{n}} canais", "N_channels": "{{n}} canais",
@ -346,7 +326,6 @@
"New_chat_transfer": "Nova transferência de conversa: {{agent}} retornou conversa para a fila", "New_chat_transfer": "Nova transferência de conversa: {{agent}} retornou conversa para a fila",
"New_Message": "Nova Mensagem", "New_Message": "Nova Mensagem",
"New_Password": "Nova Senha", "New_Password": "Nova Senha",
"New_Server": "Nova workspace",
"Next": "Próximo", "Next": "Próximo",
"No_files": "Não há arquivos", "No_files": "Não há arquivos",
"No_limit": "Sem limite", "No_limit": "Sem limite",
@ -360,8 +339,6 @@
"No_Message": "Não há mensagens", "No_Message": "Não há mensagens",
"No_messages_yet": "Não há mensagens ainda", "No_messages_yet": "Não há mensagens ainda",
"No_Reactions": "Sem reações", "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}}", "Not_RC_Server": "Este não é um servidor Rocket.Chat.\n{{contact}}",
"Nothing": "Nada", "Nothing": "Nada",
"Nothing_to_save": "Nada para salvar!", "Nothing_to_save": "Nada para salvar!",
@ -482,7 +459,6 @@
"Search_emoji": "Buscar emoji", "Search_emoji": "Buscar emoji",
"Search_global_users": "Busca por usuários globais", "Search_global_users": "Busca por usuários globais",
"Search_global_users_description": "Caso ativado, busca por usuários de outras empresas ou servidores.", "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", "Security_and_privacy": "Segurança e privacidade",
"Select_Avatar": "Selecionar Avatar", "Select_Avatar": "Selecionar Avatar",
"Select_Server": "Selecionar Servidor", "Select_Server": "Selecionar Servidor",
@ -497,20 +473,13 @@
"Send_message": "Enviar mensagem", "Send_message": "Enviar mensagem",
"Send_me_the_code_again": "Envie-me o código novamente", "Send_me_the_code_again": "Envie-me o código novamente",
"Send_to": "Enviar para...", "Send_to": "Enviar para...",
"Sending_to": "Envio para",
"Sent_an_attachment": "Enviou um anexo", "Sent_an_attachment": "Enviou um anexo",
"Server": "Servidor", "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_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": "Configurações",
"Settings_succesfully_changed": "Configurações salvas com sucesso!", "Settings_succesfully_changed": "Configurações salvas com sucesso!",
"Share": "Compartilhar", "Share": "Compartilhar",
"Share_Link": "Share Link", "Share_Link": "Share Link",
"Share_this_app": "Compartilhar esse app",
"Show_more": "Mostrar mais..", "Show_more": "Mostrar mais..",
"Sign_in_your_server": "Entrar no seu servidor", "Sign_in_your_server": "Entrar no seu servidor",
"Sign_Up": "Registrar", "Sign_Up": "Registrar",
@ -527,12 +496,9 @@
"Started_call": "Chamada iniciada por {{userBy}}", "Started_call": "Chamada iniciada por {{userBy}}",
"Submit": "Enviar", "Submit": "Enviar",
"Table": "Tabela", "Table": "Tabela",
"Tags": "Tags",
"Take_a_photo": "Tirar uma foto", "Take_a_photo": "Tirar uma foto",
"Take_a_video": "Gravar um vídeo", "Take_a_video": "Gravar um vídeo",
"Take_it": "Pegue!", "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 ", "Terms_of_Service": " Termos de Serviço ",
"Theme": "Tema", "Theme": "Tema",
"The_user_wont_be_able_to_type_in_roomName": "O usuário não poderá digitar em {{roomName}}", "The_user_wont_be_able_to_type_in_roomName": "O usuário não poderá digitar em {{roomName}}",
@ -577,14 +543,10 @@
"User_has_been_removed": "removeu {{userRemoved}}", "User_has_been_removed": "removeu {{userRemoved}}",
"User_sent_an_attachment": "{{user}} enviou um anexo", "User_sent_an_attachment": "{{user}} enviou um anexo",
"User_has_been_unmuted": "permitiu que {{userUnmuted}} fale na sala", "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_is_empty": "Usuário está vazio",
"Username": "Usuário", "Username": "Usuário",
"Username_or_email": "Usuário ou email", "Username_or_email": "Usuário ou email",
"Uses_server_configuration": "Usar configuração do servidor", "Uses_server_configuration": "Usar configuração do servidor",
"Validating": "Validando...",
"Registration_Succeeded": "Registrado com sucesso!",
"Verify": "Verificar", "Verify": "Verificar",
"Verify_email_title": "Registrado com sucesso!", "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.", "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.",
@ -613,9 +575,7 @@
"You_were_removed_from_channel": "Você foi removido de {{channel}}", "You_were_removed_from_channel": "Você foi removido de {{channel}}",
"you": "você", "you": "você",
"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.", "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", "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_certificate": "Seu certificado",
"Your_invite_link_will_expire_after__usesLeft__uses": "Seu link de convite irá vencer depois de {{usesLeft}} usos.", "Your_invite_link_will_expire_after__usesLeft__uses": "Seu link de convite irá vencer depois de {{usesLeft}} usos.",
@ -623,8 +583,6 @@
"Your_invite_link_will_expire_on__date__": "Seu link de convite irá vencer em {{date}}.", "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_invite_link_will_never_expire": "Seu link de convite nunca irá vencer.",
"Your_workspace": "Sua workspace", "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_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", "You_will_unset_a_certificate_for_this_server": "Você cancelará a configuração de um certificado para este servidor",
"Change_Language": "Alterar idioma", "Change_Language": "Alterar idioma",
@ -725,10 +683,6 @@
"Teams": "Times", "Teams": "Times",
"No_team_channels_found": "Nenhum canal encontrado", "No_team_channels_found": "Nenhum canal encontrado",
"Team_not_found": "Time não 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", "Add_Channel_to_Team": "Adicionar Canal ao Time",
"Left_The_Team_Successfully": "Saiu do time com sucesso", "Left_The_Team_Successfully": "Saiu do time com sucesso",
"Create_New": "Criar", "Create_New": "Criar",
@ -875,7 +829,5 @@
"Call": "Ligar", "Call": "Ligar",
"Reply_in_direct_message": "Responder por mensagem direta", "Reply_in_direct_message": "Responder por mensagem direta",
"room_archived": "{{username}} arquivou a sala", "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,8 +3,7 @@ export const STATUS_COLORS: any = {
busy: '#f5455c', busy: '#f5455c',
away: '#ffd21f', away: '#ffd21f',
offline: '#cbced1', offline: '#cbced1',
loading: '#9ea2a8', loading: '#9ea2a8'
disabled: '#F38C39'
}; };
export const SWITCH_TRACK_COLOR = { export const SWITCH_TRACK_COLOR = {

View File

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

View File

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

View File

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

View File

@ -85,4 +85,47 @@ export default class Message extends Model {
@json('md', sanitizer) md; @json('md', sanitizer) md;
@field('comment') comment; @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,8 +123,6 @@ export default class Subscription extends Model {
@field('e2e_key') E2EKey; @field('e2e_key') E2EKey;
@field('e2e_suggested_key') E2ESuggestedKey;
@field('encrypted') encrypted; @field('encrypted') encrypted;
@field('e2e_key_id') e2eKeyId; @field('e2e_key_id') e2eKeyId;
@ -138,4 +136,68 @@ export default class Subscription extends Model {
@field('on_hold') onHold; @field('on_hold') onHold;
@json('source', sanitizer) source; @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,4 +77,42 @@ export default class Thread extends Model {
@field('e2e') e2e; @field('e2e') e2e;
@field('draft_message') draftMessage; @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,4 +77,42 @@ export default class ThreadMessage extends Model {
@field('draft_message') draftMessage; @field('draft_message') draftMessage;
@field('e2e') e2e; @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,8 +16,6 @@ export default class Upload extends Model {
@field('name') name; @field('name') name;
@field('tmid') tmid;
@field('description') description; @field('description') description;
@field('size') size; @field('size') size;

View File

@ -239,24 +239,6 @@ export default schemaMigrations({
columns: [{ name: 'hide_mention_status', type: 'boolean', isOptional: true }] 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'; import { appSchema, tableSchema } from '@nozbe/watermelondb';
export default appSchema({ export default appSchema({
version: 20, version: 18,
tables: [ tables: [
tableSchema({ tableSchema({
name: 'subscriptions', name: 'subscriptions',
@ -55,7 +55,6 @@ export default appSchema({
{ name: 'livechat_data', type: 'string', isOptional: true }, { name: 'livechat_data', type: 'string', isOptional: true },
{ name: 'tags', type: 'string', isOptional: true }, { name: 'tags', type: 'string', isOptional: true },
{ name: 'e2e_key', 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: 'encrypted', type: 'boolean', isOptional: true },
{ name: 'e2e_key_id', type: 'string', isOptional: true }, { name: 'e2e_key_id', type: 'string', isOptional: true },
{ name: 'avatar_etag', type: 'string', isOptional: true }, { name: 'avatar_etag', type: 'string', isOptional: true },
@ -223,7 +222,6 @@ export default appSchema({
{ name: 'path', type: 'string', isOptional: true }, { name: 'path', type: 'string', isOptional: true },
{ name: 'rid', type: 'string', isIndexed: true }, { name: 'rid', type: 'string', isIndexed: true },
{ name: 'name', type: 'string', isOptional: true }, { name: 'name', type: 'string', isOptional: true },
{ name: 'tmid', type: 'string', isOptional: true },
{ name: 'description', type: 'string', isOptional: true }, { name: 'description', type: 'string', isOptional: true },
{ name: 'size', type: 'number' }, { name: 'size', type: 'number' },
{ name: 'type', type: 'string', isOptional: true }, { name: 'type', type: 'string', isOptional: true },

View File

@ -34,7 +34,6 @@ class Encryption {
handshake: Function; handshake: Function;
decrypt: Function; decrypt: Function;
encrypt: Function; encrypt: Function;
importRoomKey: Function;
}; };
}; };
@ -98,10 +97,6 @@ class Encryption {
}); });
}; };
stopRoom = (rid: string) => {
delete this.roomInstances[rid];
};
// When a new participant join and request a new room encryption key // When a new participant join and request a new room encryption key
provideRoomKeyToUser = async (keyId: string, rid: string) => { provideRoomKeyToUser = async (keyId: string, rid: string) => {
// If the client is not ready // If the client is not ready
@ -225,19 +220,6 @@ class Encryption {
return roomE2E; 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 // Logic to decrypt all pending messages/threads/threadMessages
// after initialize the encryption client // after initialize the encryption client
decryptPendingMessages = async (roomId?: string) => { decryptPendingMessages = async (roomId?: string) => {

View File

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

View File

@ -0,0 +1,27 @@
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

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

View File

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

View File

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

View File

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

View File

@ -9,9 +9,6 @@ import database from '../database';
import { IUser } from '../../definitions'; import { IUser } from '../../definitions';
import sdk from '../services/sdk'; import sdk from '../services/sdk';
import { compareServerVersion } from './helpers'; import { compareServerVersion } from './helpers';
import userPreferences from './userPreferences';
import { NOTIFICATION_PRESENCE_CAP } from '../constants';
import { setNotificationPresenceCap } from '../../actions/app';
export const _activeUsersSubTimeout: { activeUsersSubTimeout: boolean | ReturnType<typeof setTimeout> | number } = { export const _activeUsersSubTimeout: { activeUsersSubTimeout: boolean | ReturnType<typeof setTimeout> | number } = {
activeUsersSubTimeout: false activeUsersSubTimeout: false
@ -127,16 +124,3 @@ export function getUserPresence(uid: string) {
usersBatch.push(uid); usersBatch.push(uid);
} }
} }
export const setPresenceCap = async (enabled: boolean) => {
if (enabled) {
const notificationPresenceCap = await userPreferences.getBool(NOTIFICATION_PRESENCE_CAP);
if (notificationPresenceCap !== false) {
userPreferences.setBool(NOTIFICATION_PRESENCE_CAP, true);
reduxStore.dispatch(setNotificationPresenceCap(true));
}
} else {
userPreferences.removeItem(NOTIFICATION_PRESENCE_CAP);
reduxStore.dispatch(setNotificationPresenceCap(false));
}
};

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