Merge 4.37.0 into master (#5025)
This commit is contained in:
commit
acdfc6ddd9
|
@ -1,6 +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: "14.2.0"
|
||||||
|
@ -327,6 +330,14 @@ commands:
|
||||||
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
|
||||||
|
@ -438,6 +449,94 @@ 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: |
|
||||||
|
export RUNNING_E2E_TESTS=true
|
||||||
|
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
|
||||||
|
@ -461,11 +560,89 @@ 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
|
||||||
|
export RUNNING_E2E_TESTS=true
|
||||||
|
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
|
||||||
|
|
|
@ -0,0 +1,91 @@
|
||||||
|
/** @type {Detox.DetoxConfig} */
|
||||||
|
module.exports = {
|
||||||
|
testRunner: {
|
||||||
|
args: {
|
||||||
|
$0: 'jest',
|
||||||
|
config: 'e2e/jest.config.js'
|
||||||
|
},
|
||||||
|
retries: process.env.CI ? 3 : 0
|
||||||
|
},
|
||||||
|
artifacts: {
|
||||||
|
plugins: {
|
||||||
|
screenshot: 'failing',
|
||||||
|
video: 'failing',
|
||||||
|
uiHierarchy: 'enabled'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
apps: {
|
||||||
|
'ios.debug': {
|
||||||
|
type: 'ios.app',
|
||||||
|
binaryPath: 'ios/build/Build/Products/Debug-iphonesimulator/Rocket.Chat Experimental.app',
|
||||||
|
build:
|
||||||
|
'xcodebuild -workspace ios/RocketChatRN.xcworkspace -scheme RocketChatRN -configuration Debug -sdk iphonesimulator -derivedDataPath ios/build'
|
||||||
|
},
|
||||||
|
'ios.release': {
|
||||||
|
type: 'ios.app',
|
||||||
|
binaryPath: 'ios/build/Build/Products/Release-iphonesimulator/Rocket.Chat Experimental.app',
|
||||||
|
build:
|
||||||
|
'xcodebuild -workspace ios/RocketChatRN.xcworkspace -scheme RocketChatRN -configuration Release -sdk iphonesimulator -derivedDataPath ios/build'
|
||||||
|
},
|
||||||
|
'android.debug': {
|
||||||
|
type: 'android.apk',
|
||||||
|
binaryPath: 'android/app/build/outputs/apk/experimentalPlay/debug/app-experimental-play-debug.apk',
|
||||||
|
build:
|
||||||
|
'cd android ; ./gradlew assembleExperimentalPlayDebug assembleExperimentalPlayDebugAndroidTest -DtestBuildType=debug ; cd -',
|
||||||
|
reversePorts: [8081]
|
||||||
|
},
|
||||||
|
'android.release': {
|
||||||
|
type: 'android.apk',
|
||||||
|
binaryPath: 'android/app/build/outputs/apk/experimentalPlay/release/app-experimental-play-release.apk',
|
||||||
|
build:
|
||||||
|
'cd android ; ./gradlew assembleExperimentalPlayRelease assembleExperimentalPlayReleaseAndroidTest -DtestBuildType=release ; cd -'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
devices: {
|
||||||
|
simulator: {
|
||||||
|
type: 'ios.simulator',
|
||||||
|
device: {
|
||||||
|
type: 'iPhone 14'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
attached: {
|
||||||
|
type: 'android.attached',
|
||||||
|
device: {
|
||||||
|
adbName: '.*'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
emulator: {
|
||||||
|
type: 'android.emulator',
|
||||||
|
device: {
|
||||||
|
avdName: 'Pixel_API_31_AOSP'
|
||||||
|
},
|
||||||
|
headless: process.env.CI ? true : false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
configurations: {
|
||||||
|
'ios.sim.debug': {
|
||||||
|
device: 'simulator',
|
||||||
|
app: 'ios.debug'
|
||||||
|
},
|
||||||
|
'ios.sim.release': {
|
||||||
|
device: 'simulator',
|
||||||
|
app: 'ios.release'
|
||||||
|
},
|
||||||
|
'android.att.debug': {
|
||||||
|
device: 'attached',
|
||||||
|
app: 'android.debug'
|
||||||
|
},
|
||||||
|
'android.att.release': {
|
||||||
|
device: 'attached',
|
||||||
|
app: 'android.release'
|
||||||
|
},
|
||||||
|
'android.emu.debug': {
|
||||||
|
device: 'emulator',
|
||||||
|
app: 'android.debug'
|
||||||
|
},
|
||||||
|
'android.emu.release': {
|
||||||
|
device: 'emulator',
|
||||||
|
app: 'android.release'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
13
.eslintrc.js
13
.eslintrc.js
|
@ -240,19 +240,8 @@ module.exports = {
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
files: ['e2e/**'],
|
files: ['e2e/**'],
|
||||||
globals: {
|
|
||||||
by: true,
|
|
||||||
detox: true,
|
|
||||||
device: true,
|
|
||||||
element: true,
|
|
||||||
waitFor: true
|
|
||||||
},
|
|
||||||
rules: {
|
rules: {
|
||||||
'import/no-extraneous-dependencies': 0,
|
'no-await-in-loop': 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
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
|
@ -67,5 +67,6 @@ 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
|
|
@ -18,7 +18,7 @@
|
||||||
<img alt="Download on App Store" src="https://user-images.githubusercontent.com/7317008/43209852-4ca39622-904b-11e8-8ce1-cdc3aee76ae9.png" height=43>
|
<img alt="Download on App Store" src="https://user-images.githubusercontent.com/7317008/43209852-4ca39622-904b-11e8-8ce1-cdc3aee76ae9.png" height=43>
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
Check [our docs](https://docs.rocket.chat/installation/mobile-and-desktop-apps#mobile-apps) for beta and Experimental versions.
|
Check [our docs](https://docs.rocket.chat/use-rocket.chat/rocket.chat-mobile) for beta and Experimental versions.
|
||||||
|
|
||||||
## Reporting an Issue
|
## Reporting an Issue
|
||||||
|
|
||||||
|
|
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
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
@ -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.36.0"
|
versionName "4.37.0"
|
||||||
vectorDrawables.useSupportLibrary = true
|
vectorDrawables.useSupportLibrary = true
|
||||||
if (!isFoss) {
|
if (!isFoss) {
|
||||||
manifestPlaceholders = [BugsnagAPIKey: BugsnagAPIKey as String]
|
manifestPlaceholders = [BugsnagAPIKey: BugsnagAPIKey as String]
|
||||||
|
@ -250,6 +250,7 @@ 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 {
|
||||||
|
@ -268,6 +269,11 @@ 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"
|
||||||
|
@ -280,10 +286,6 @@ 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"
|
||||||
|
@ -311,16 +313,6 @@ 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 ->
|
||||||
|
@ -385,8 +377,9 @@ 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:+') { transitive = true }
|
androidTestImplementation('com.wix:detox:+')
|
||||||
androidTestImplementation 'junit:junit:4.12'
|
implementation 'androidx.appcompat:appcompat:1.1.0'
|
||||||
|
implementation 'com.facebook.soloader:soloader:0.10.4'
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isNewArchitectureEnabled()) {
|
if (isNewArchitectureEnabled()) {
|
||||||
|
|
|
@ -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<MainActivity> mActivityRule = new ActivityTestRule<>(MainActivity.class, false, false);
|
public ActivityTestRule<chat.rocket.reactnative.MainActivity> mActivityRule = new ActivityTestRule<>(chat.rocket.reactnative.MainActivity.class, false, false);
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void runDetoxTests() {
|
public void runDetoxTests() {
|
||||||
|
|
|
@ -1,10 +0,0 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<network-security-config xmlns:tools="http://schemas.android.com/tools">
|
|
||||||
<base-config cleartextTrafficPermitted="true">
|
|
||||||
<trust-anchors>
|
|
||||||
<certificates src="system" />
|
|
||||||
<certificates src="user"
|
|
||||||
tools:ignore="AcceptsUserCertificates" />
|
|
||||||
</trust-anchors>
|
|
||||||
</base-config>
|
|
||||||
</network-security-config>
|
|
|
@ -7,4 +7,8 @@
|
||||||
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>
|
|
@ -1,9 +1,5 @@
|
||||||
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()
|
||||||
|
@ -75,5 +71,38 @@ 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -1,13 +1,11 @@
|
||||||
import { Q } from '@nozbe/watermelondb';
|
import React from 'react';
|
||||||
import React, { useEffect, useRef, useState } from 'react';
|
|
||||||
import { shallowEqual, useSelector } from 'react-redux';
|
import { shallowEqual, useSelector } from 'react-redux';
|
||||||
import { Observable, Subscription } from 'rxjs';
|
|
||||||
|
|
||||||
import { IApplicationState, TSubscriptionModel, TUserModel } from '../../definitions';
|
import { IApplicationState } from '../../definitions';
|
||||||
import database from '../../lib/database';
|
|
||||||
import { getUserSelector } from '../../selectors/login';
|
import { getUserSelector } from '../../selectors/login';
|
||||||
import Avatar from './Avatar';
|
import Avatar from './Avatar';
|
||||||
import { IAvatar } from './interfaces';
|
import { IAvatar } from './interfaces';
|
||||||
|
import { useAvatarETag } from './useAvatarETag';
|
||||||
|
|
||||||
const AvatarContainer = ({
|
const AvatarContainer = ({
|
||||||
style,
|
style,
|
||||||
|
@ -23,17 +21,13 @@ const AvatarContainer = ({
|
||||||
isStatic,
|
isStatic,
|
||||||
rid
|
rid
|
||||||
}: IAvatar): React.ReactElement => {
|
}: IAvatar): React.ReactElement => {
|
||||||
const subscription = useRef<Subscription>();
|
|
||||||
const [avatarETag, setAvatarETag] = useState<string | undefined>('');
|
|
||||||
|
|
||||||
const isDirect = () => type === 'd';
|
|
||||||
|
|
||||||
const server = useSelector((state: IApplicationState) => state.share.server.server || state.server.server);
|
const server = useSelector((state: IApplicationState) => state.share.server.server || state.server.server);
|
||||||
const serverVersion = useSelector((state: IApplicationState) => state.share.server.version || state.server.version);
|
const serverVersion = useSelector((state: IApplicationState) => state.share.server.version || state.server.version);
|
||||||
const { id, token } = useSelector(
|
const { id, token, username } = useSelector(
|
||||||
(state: IApplicationState) => ({
|
(state: IApplicationState) => ({
|
||||||
id: getUserSelector(state).id,
|
id: getUserSelector(state).id,
|
||||||
token: getUserSelector(state).token
|
token: getUserSelector(state).token,
|
||||||
|
username: getUserSelector(state).username
|
||||||
}),
|
}),
|
||||||
shallowEqual
|
shallowEqual
|
||||||
);
|
);
|
||||||
|
@ -48,41 +42,7 @@ const AvatarContainer = ({
|
||||||
true
|
true
|
||||||
);
|
);
|
||||||
|
|
||||||
const init = async () => {
|
const { avatarETag } = useAvatarETag({ username, text, type, rid, id });
|
||||||
const db = database.active;
|
|
||||||
const usersCollection = db.get('users');
|
|
||||||
const subsCollection = db.get('subscriptions');
|
|
||||||
|
|
||||||
let record;
|
|
||||||
try {
|
|
||||||
if (isDirect()) {
|
|
||||||
const [user] = await usersCollection.query(Q.where('username', text)).fetch();
|
|
||||||
record = user;
|
|
||||||
} else if (rid) {
|
|
||||||
record = await subsCollection.find(rid);
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
// Record not found
|
|
||||||
}
|
|
||||||
|
|
||||||
if (record) {
|
|
||||||
const observable = record.observe() as Observable<TSubscriptionModel | TUserModel>;
|
|
||||||
subscription.current = observable.subscribe(r => {
|
|
||||||
setAvatarETag(r.avatarETag);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!avatarETag) {
|
|
||||||
init();
|
|
||||||
}
|
|
||||||
return () => {
|
|
||||||
if (subscription?.current?.unsubscribe) {
|
|
||||||
subscription.current.unsubscribe();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}, [text, type, size, avatarETag, externalProviderUrl]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Avatar
|
<Avatar
|
|
@ -0,0 +1,85 @@
|
||||||
|
import React from 'react';
|
||||||
|
import { StyleSheet } from 'react-native';
|
||||||
|
|
||||||
|
import Button from '../Button';
|
||||||
|
import AvatarContainer from './AvatarContainer';
|
||||||
|
import { IAvatar } from './interfaces';
|
||||||
|
import I18n from '../../i18n';
|
||||||
|
import { useTheme } from '../../theme';
|
||||||
|
import { BUTTON_HIT_SLOP } from '../message/utils';
|
||||||
|
import { useAppSelector } from '../../lib/hooks';
|
||||||
|
import { compareServerVersion } from '../../lib/methods/helpers';
|
||||||
|
import sharedStyles from '../../views/Styles';
|
||||||
|
|
||||||
|
const styles = StyleSheet.create({
|
||||||
|
editAvatarButton: {
|
||||||
|
marginTop: 8,
|
||||||
|
paddingVertical: 8,
|
||||||
|
paddingHorizontal: 12,
|
||||||
|
marginBottom: 0,
|
||||||
|
height: undefined
|
||||||
|
},
|
||||||
|
textButton: {
|
||||||
|
fontSize: 12,
|
||||||
|
...sharedStyles.textSemibold
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
interface IAvatarContainer extends Omit<IAvatar, 'size'> {
|
||||||
|
handleEdit?: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const AvatarWithEdit = ({
|
||||||
|
style,
|
||||||
|
text = '',
|
||||||
|
avatar,
|
||||||
|
emoji,
|
||||||
|
borderRadius,
|
||||||
|
type,
|
||||||
|
children,
|
||||||
|
onPress,
|
||||||
|
getCustomEmoji,
|
||||||
|
isStatic,
|
||||||
|
rid,
|
||||||
|
handleEdit
|
||||||
|
}: IAvatarContainer): React.ReactElement => {
|
||||||
|
const { colors } = useTheme();
|
||||||
|
|
||||||
|
const { serverVersion } = useAppSelector(state => ({
|
||||||
|
serverVersion: state.server.version
|
||||||
|
}));
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<AvatarContainer
|
||||||
|
style={style}
|
||||||
|
text={text}
|
||||||
|
avatar={avatar}
|
||||||
|
emoji={emoji}
|
||||||
|
size={120}
|
||||||
|
borderRadius={borderRadius}
|
||||||
|
type={type}
|
||||||
|
children={children}
|
||||||
|
onPress={onPress}
|
||||||
|
getCustomEmoji={getCustomEmoji}
|
||||||
|
isStatic={isStatic}
|
||||||
|
rid={rid}
|
||||||
|
/>
|
||||||
|
{handleEdit && serverVersion && compareServerVersion(serverVersion, 'greaterThanOrEqualTo', '3.6.0') ? (
|
||||||
|
<Button
|
||||||
|
title={I18n.t('Edit')}
|
||||||
|
type='secondary'
|
||||||
|
backgroundColor={colors.editAndUploadButtonAvatar}
|
||||||
|
onPress={handleEdit}
|
||||||
|
testID='avatar-edit-button'
|
||||||
|
style={styles.editAvatarButton}
|
||||||
|
styleText={styles.textButton}
|
||||||
|
color={colors.titleText}
|
||||||
|
hitSlop={BUTTON_HIT_SLOP}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default AvatarWithEdit;
|
|
@ -0,0 +1,5 @@
|
||||||
|
import Avatar from './AvatarContainer';
|
||||||
|
|
||||||
|
export { default as AvatarWithEdit } from './AvatarWithEdit';
|
||||||
|
|
||||||
|
export default Avatar;
|
|
@ -0,0 +1,67 @@
|
||||||
|
import { Q } from '@nozbe/watermelondb';
|
||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
import { Observable, Subscription } from 'rxjs';
|
||||||
|
|
||||||
|
import { TLoggedUserModel, TSubscriptionModel, TUserModel } from '../../definitions';
|
||||||
|
import database from '../../lib/database';
|
||||||
|
|
||||||
|
export const useAvatarETag = ({
|
||||||
|
username,
|
||||||
|
text,
|
||||||
|
type = '',
|
||||||
|
rid,
|
||||||
|
id
|
||||||
|
}: {
|
||||||
|
type?: string;
|
||||||
|
username: string;
|
||||||
|
text: string;
|
||||||
|
rid?: string;
|
||||||
|
id: string;
|
||||||
|
}) => {
|
||||||
|
const [avatarETag, setAvatarETag] = useState<string | undefined>('');
|
||||||
|
|
||||||
|
const isDirect = () => type === 'd';
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
let subscription: Subscription;
|
||||||
|
if (!avatarETag) {
|
||||||
|
const observeAvatarETag = async () => {
|
||||||
|
const db = database.active;
|
||||||
|
const usersCollection = db.get('users');
|
||||||
|
const subsCollection = db.get('subscriptions');
|
||||||
|
|
||||||
|
let record;
|
||||||
|
try {
|
||||||
|
if (username === text) {
|
||||||
|
const serversDB = database.servers;
|
||||||
|
const userCollections = serversDB.get('users');
|
||||||
|
const user = await userCollections.find(id);
|
||||||
|
record = user;
|
||||||
|
} else if (isDirect()) {
|
||||||
|
const [user] = await usersCollection.query(Q.where('username', text)).fetch();
|
||||||
|
record = user;
|
||||||
|
} else if (rid) {
|
||||||
|
record = await subsCollection.find(rid);
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// Record not found
|
||||||
|
}
|
||||||
|
|
||||||
|
if (record) {
|
||||||
|
const observable = record.observe() as Observable<TSubscriptionModel | TUserModel | TLoggedUserModel>;
|
||||||
|
subscription = observable.subscribe(r => {
|
||||||
|
setAvatarETag(r.avatarETag);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
observeAvatarETag();
|
||||||
|
return () => {
|
||||||
|
if (subscription?.unsubscribe) {
|
||||||
|
subscription.unsubscribe();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}, [text]);
|
||||||
|
|
||||||
|
return { avatarETag };
|
||||||
|
};
|
|
@ -14,7 +14,7 @@ interface IButtonProps extends PlatformTouchableProps {
|
||||||
loading?: boolean;
|
loading?: boolean;
|
||||||
color?: string;
|
color?: string;
|
||||||
fontSize?: number;
|
fontSize?: number;
|
||||||
styleText?: StyleProp<TextStyle>[];
|
styleText?: StyleProp<TextStyle> | StyleProp<TextStyle>[];
|
||||||
}
|
}
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
const styles = StyleSheet.create({
|
||||||
|
|
|
@ -1,34 +1,31 @@
|
||||||
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 { EMOJI_BUTTON_SIZE } from './styles';
|
|
||||||
import scrollPersistTaps from '../../lib/methods/helpers/scrollPersistTaps';
|
|
||||||
import { IEmoji } from '../../definitions/IEmoji';
|
import { IEmoji } from '../../definitions/IEmoji';
|
||||||
|
import scrollPersistTaps from '../../lib/methods/helpers/scrollPersistTaps';
|
||||||
import { PressableEmoji } from './PressableEmoji';
|
import { PressableEmoji } from './PressableEmoji';
|
||||||
|
import { EMOJI_BUTTON_SIZE } from './styles';
|
||||||
|
|
||||||
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 }: IEmojiCategoryProps): React.ReactElement | null => {
|
const EmojiCategory = ({ onEmojiSelected, emojis, parentWidth }: IEmojiCategoryProps): React.ReactElement | null => {
|
||||||
const { width } = useWindowDimensions();
|
if (!parentWidth) {
|
||||||
|
|
||||||
const numColumns = Math.trunc(width / EMOJI_BUTTON_SIZE);
|
|
||||||
const marginHorizontal = (width % EMOJI_BUTTON_SIZE) / 2;
|
|
||||||
|
|
||||||
const renderItem = ({ item }: { item: IEmoji }) => <PressableEmoji emoji={item} onPress={onEmojiSelected} />;
|
|
||||||
|
|
||||||
if (!width) {
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const numColumns = Math.trunc(parentWidth / EMOJI_BUTTON_SIZE);
|
||||||
|
const marginHorizontal = (parentWidth % EMOJI_BUTTON_SIZE) / 2;
|
||||||
|
|
||||||
|
const renderItem = ({ item }: { item: IEmoji }) => <PressableEmoji emoji={item} onPress={onEmojiSelected} />;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<FlatList
|
<FlatList
|
||||||
// needed to update the numColumns when the width changes
|
key={`emoji-category-${parentWidth}`}
|
||||||
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}
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import React from 'react';
|
import React, { useState } 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,6 +20,8 @@ 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(
|
||||||
|
@ -50,7 +52,14 @@ const EmojiPicker = ({
|
||||||
if (!emojis.length) {
|
if (!emojis.length) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
return <EmojiCategory emojis={emojis} onEmojiSelected={(emoji: IEmoji) => handleEmojiSelect(emoji)} tabLabel={label} />;
|
return (
|
||||||
|
<EmojiCategory
|
||||||
|
parentWidth={parentWidth}
|
||||||
|
emojis={emojis}
|
||||||
|
onEmojiSelected={(emoji: IEmoji) => handleEmojiSelect(emoji)}
|
||||||
|
tabLabel={label}
|
||||||
|
/>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
if (!loaded) {
|
if (!loaded) {
|
||||||
|
@ -58,9 +67,13 @@ const EmojiPicker = ({
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View style={styles.emojiPickerContainer}>
|
<View style={styles.emojiPickerContainer} onLayout={e => setParentWidth(e.nativeEvent.layout.width)}>
|
||||||
{searching ? (
|
{searching ? (
|
||||||
<EmojiCategory emojis={searchedEmojis} onEmojiSelected={(emoji: IEmoji) => handleEmojiSelect(emoji)} />
|
<EmojiCategory
|
||||||
|
emojis={searchedEmojis}
|
||||||
|
onEmojiSelected={(emoji: IEmoji) => handleEmojiSelect(emoji)}
|
||||||
|
parentWidth={parentWidth}
|
||||||
|
/>
|
||||||
) : (
|
) : (
|
||||||
<ScrollableTabView
|
<ScrollableTabView
|
||||||
renderTabBar={() => <TabBar />}
|
renderTabBar={() => <TabBar />}
|
||||||
|
|
|
@ -6,7 +6,7 @@ import scrollPersistTaps from '../lib/methods/helpers/scrollPersistTaps';
|
||||||
interface IKeyboardViewProps extends KeyboardAwareScrollViewProps {
|
interface IKeyboardViewProps extends KeyboardAwareScrollViewProps {
|
||||||
keyboardVerticalOffset?: number;
|
keyboardVerticalOffset?: number;
|
||||||
scrollEnabled?: boolean;
|
scrollEnabled?: boolean;
|
||||||
children: React.ReactElement[] | React.ReactElement;
|
children: React.ReactElement[] | React.ReactElement | null | (React.ReactElement | null)[];
|
||||||
}
|
}
|
||||||
|
|
||||||
const KeyboardView = ({ style, contentContainerStyle, scrollEnabled, keyboardVerticalOffset, children }: IKeyboardViewProps) => (
|
const KeyboardView = ({ style, contentContainerStyle, scrollEnabled, keyboardVerticalOffset, children }: IKeyboardViewProps) => (
|
||||||
|
|
|
@ -487,7 +487,9 @@ class MessageBox extends Component<IMessageBoxProps, IMessageBoxState> {
|
||||||
this.handleTyping(!isTextEmpty);
|
this.handleTyping(!isTextEmpty);
|
||||||
const { start, end } = this.selection;
|
const { start, end } = this.selection;
|
||||||
const cursor = Math.max(start, end);
|
const cursor = Math.max(start, end);
|
||||||
const txt = cursor < text.length ? text.substr(0, cursor).split(' ') : text.split(' ');
|
const whiteSpaceOrBreakLineRegex = /[\s\n]+/;
|
||||||
|
const txt =
|
||||||
|
cursor < text.length ? text.substr(0, cursor).split(whiteSpaceOrBreakLineRegex) : text.split(whiteSpaceOrBreakLineRegex);
|
||||||
const lastWord = txt[txt.length - 1];
|
const lastWord = txt[txt.length - 1];
|
||||||
const result = lastWord.substring(1);
|
const result = lastWord.substring(1);
|
||||||
|
|
||||||
|
|
|
@ -24,8 +24,16 @@ const formatMsg = ({ lastMessage, type, showLastMessage, username, useRealName }
|
||||||
const isLastMessageSentByMe = lastMessage.u.username === username;
|
const isLastMessageSentByMe = lastMessage.u.username === username;
|
||||||
|
|
||||||
if (!lastMessage.msg && lastMessage.attachments && Object.keys(lastMessage.attachments).length) {
|
if (!lastMessage.msg && lastMessage.attachments && Object.keys(lastMessage.attachments).length) {
|
||||||
const user = isLastMessageSentByMe ? I18n.t('You') : lastMessage.u.username;
|
const userAttachment = () => {
|
||||||
return I18n.t('User_sent_an_attachment', { user });
|
if (isLastMessageSentByMe) {
|
||||||
|
return I18n.t('You');
|
||||||
|
}
|
||||||
|
if (useRealName && lastMessage.u.name) {
|
||||||
|
return lastMessage.u.name;
|
||||||
|
}
|
||||||
|
return lastMessage.u.username;
|
||||||
|
};
|
||||||
|
return I18n.t('User_sent_an_attachment', { user: userAttachment() });
|
||||||
}
|
}
|
||||||
|
|
||||||
// Encrypted message pending decrypt
|
// Encrypted message pending decrypt
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { fireEvent, render } from '@testing-library/react-native';
|
import { fireEvent, render } from '@testing-library/react-native';
|
||||||
import { TextInputProps } from 'react-native';
|
|
||||||
|
|
||||||
import SearchBox from '.';
|
import SearchBox from '.';
|
||||||
|
|
||||||
|
@ -11,35 +10,35 @@ const testSearchInputs = {
|
||||||
testID: 'search-box-text-input'
|
testID: 'search-box-text-input'
|
||||||
};
|
};
|
||||||
|
|
||||||
const Render = ({ onChangeText, testID }: TextInputProps) => <SearchBox testID={testID} onChangeText={onChangeText} />;
|
|
||||||
|
|
||||||
describe('SearchBox', () => {
|
describe('SearchBox', () => {
|
||||||
it('should render the searchbox component', () => {
|
it('should render the searchbox component', () => {
|
||||||
const { findByTestId } = render(<Render onChangeText={testSearchInputs.onChangeText} testID={testSearchInputs.testID} />);
|
const { findByTestId } = render(<SearchBox onChangeText={testSearchInputs.onChangeText} testID={testSearchInputs.testID} />);
|
||||||
|
|
||||||
expect(findByTestId('searchbox')).toBeTruthy();
|
expect(findByTestId('searchbox')).toBeTruthy();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should not render clear-input icon', async () => {
|
it('should not render clear-input icon', async () => {
|
||||||
const { queryByTestId } = render(<Render onChangeText={testSearchInputs.onChangeText} testID={testSearchInputs.testID} />);
|
const { queryByTestId } = render(<SearchBox onChangeText={testSearchInputs.onChangeText} testID={testSearchInputs.testID} />);
|
||||||
const clearInput = await queryByTestId('clear-text-input');
|
const clearInput = await queryByTestId('clear-text-input');
|
||||||
expect(clearInput).toBeNull();
|
expect(clearInput).toBeNull();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should input new value with onChangeText function', async () => {
|
it('should input new value with onChangeText function', async () => {
|
||||||
const { findByTestId } = render(<Render onChangeText={onChangeTextMock} testID={testSearchInputs.testID} />);
|
const { findByTestId } = render(<SearchBox onChangeText={onChangeTextMock} testID={testSearchInputs.testID} />);
|
||||||
|
|
||||||
const component = await findByTestId(testSearchInputs.testID);
|
const component = await findByTestId(testSearchInputs.testID);
|
||||||
fireEvent.changeText(component, 'new-input-value');
|
fireEvent.changeText(component, 'new-input-value');
|
||||||
expect(onChangeTextMock).toHaveBeenCalledWith('new-input-value');
|
expect(onChangeTextMock).toHaveBeenCalledWith('new-input-value');
|
||||||
});
|
});
|
||||||
|
|
||||||
// we need skip this test for now, until discovery how handle with functions effect
|
it('should clear input when clear icon is pressed', async () => {
|
||||||
// https://github.com/callstack/react-native-testing-library/issues/978
|
const { findByTestId } = render(<SearchBox onChangeText={onChangeTextMock} testID={testSearchInputs.testID} />);
|
||||||
it.skip('should clear input when call onCancelSearch function', async () => {
|
|
||||||
const { findByTestId } = render(<Render testID={'input-with-value'} onChangeText={onChangeTextMock} />);
|
|
||||||
|
|
||||||
const component = await findByTestId('clear-text-input');
|
const component = await findByTestId(testSearchInputs.testID);
|
||||||
fireEvent.press(component, 'input-with-value');
|
fireEvent.changeText(component, 'new-input-value');
|
||||||
expect(onChangeTextMock).toHaveBeenCalledWith('input-with-value');
|
|
||||||
|
const clearTextInput = await findByTestId('clear-text-input');
|
||||||
|
fireEvent.press(clearTextInput);
|
||||||
|
expect(onChangeTextMock).toHaveBeenCalledWith('');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -6,7 +6,6 @@ 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';
|
||||||
|
@ -16,12 +15,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 CallAgainActionSheet({ rid }: { rid: string }): React.ReactElement {
|
export default function StartACallActionSheet({ rid, initCall }: { rid: string; initCall: Function }): React.ReactElement {
|
||||||
const style = useStyle();
|
const style = useStyle();
|
||||||
const { colors } = useTheme();
|
const { colors } = useTheme();
|
||||||
const [user, setUser] = useState({ username: '', avatar: '', uid: '', rid: '' });
|
const [user, setUser] = useState({ username: '', avatar: '', uid: '' });
|
||||||
const [phone, setPhone] = useState(true);
|
const [mic, setMic] = useState(true);
|
||||||
const [camera, setCamera] = useState(false);
|
const [cam, setCam] = useState(false);
|
||||||
const username = useAppSelector(state => state.login.user.username);
|
const username = useAppSelector(state => state.login.user.username);
|
||||||
|
|
||||||
const { hideActionSheet } = useActionSheet();
|
const { hideActionSheet } = useActionSheet();
|
||||||
|
@ -31,7 +30,7 @@ export default function CallAgainActionSheet({ rid }: { rid: string }): React.Re
|
||||||
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, rid: room?.id || '' });
|
setUser({ uid, username: room?.name || '', avatar: avt });
|
||||||
})();
|
})();
|
||||||
}, [rid]);
|
}, [rid]);
|
||||||
|
|
||||||
|
@ -43,25 +42,27 @@ export default function CallAgainActionSheet({ rid }: { rid: string }): React.Re
|
||||||
<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={() => setCamera(!camera)}
|
onPress={() => setCam(!cam)}
|
||||||
style={[style.iconCallContainer, camera && style.enabledBackground, { marginRight: 6 }]}
|
style={[style.iconCallContainer, cam && style.enabledBackground, { marginRight: 6 }]}
|
||||||
hitSlop={BUTTON_HIT_SLOP}
|
hitSlop={BUTTON_HIT_SLOP}
|
||||||
>
|
>
|
||||||
<CustomIcon name={camera ? 'camera' : 'camera-disabled'} size={16} color={handleColor(camera)} />
|
<CustomIcon name={cam ? 'camera' : 'camera-disabled'} size={20} color={handleColor(cam)} />
|
||||||
</Touchable>
|
</Touchable>
|
||||||
<Touchable
|
<Touchable
|
||||||
onPress={() => setPhone(!phone)}
|
onPress={() => setMic(!mic)}
|
||||||
style={[style.iconCallContainer, phone && style.enabledBackground]}
|
style={[style.iconCallContainer, mic && style.enabledBackground]}
|
||||||
hitSlop={BUTTON_HIT_SLOP}
|
hitSlop={BUTTON_HIT_SLOP}
|
||||||
>
|
>
|
||||||
<CustomIcon name={phone ? 'microphone' : 'microphone-disabled'} size={16} color={handleColor(phone)} />
|
<CustomIcon name={mic ? 'microphone' : 'microphone-disabled'} size={20} color={handleColor(mic)} />
|
||||||
</Touchable>
|
</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}>{user.username}</Text>
|
<Text style={style.actionSheetUsername} numberOfLines={1}>
|
||||||
|
{user.username}
|
||||||
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
<View style={style.actionSheetPhotoContainer}>
|
<View style={style.actionSheetPhotoContainer}>
|
||||||
<AvatarContainer size={62} text={username} />
|
<AvatarContainer size={62} text={username} />
|
||||||
|
@ -70,7 +71,7 @@ export default function CallAgainActionSheet({ rid }: { rid: string }): React.Re
|
||||||
onPress={() => {
|
onPress={() => {
|
||||||
hideActionSheet();
|
hideActionSheet();
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
videoConfStartAndJoin(user.rid, camera);
|
initCall({ cam, mic });
|
||||||
}, 100);
|
}, 100);
|
||||||
}}
|
}}
|
||||||
title={i18n.t('Call')}
|
title={i18n.t('Call')}
|
|
@ -3,17 +3,16 @@ 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 { useVideoConf } from '../../../../lib/hooks/useVideoConf';
|
import { videoConfJoin } from '../../../../lib/methods/videoConf';
|
||||||
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={() => joinCall(blockId)}>
|
<Touchable style={style.callToActionButton} onPress={() => videoConfJoin(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>
|
||||||
|
|
|
@ -6,9 +6,7 @@ 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 { useSnaps } from '../../../../lib/hooks/useSnaps';
|
import { useVideoConf } from '../../../../lib/hooks/useVideoConf';
|
||||||
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';
|
||||||
|
@ -26,8 +24,7 @@ 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 { showActionSheet } = useActionSheet();
|
const { showInitCallActionSheet } = useVideoConf(rid);
|
||||||
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);
|
||||||
|
|
||||||
|
@ -35,17 +32,9 @@ export default function VideoConferenceEnded({
|
||||||
<VideoConferenceBaseContainer variant='ended'>
|
<VideoConferenceBaseContainer variant='ended'>
|
||||||
{type === 'direct' ? (
|
{type === 'direct' ? (
|
||||||
<>
|
<>
|
||||||
<Touchable
|
<Touchable style={style.callToActionCallBack} onPress={showInitCallActionSheet}>
|
||||||
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_again') : i18n.t('Call_back')}
|
||||||
</Text>
|
</Text>
|
||||||
</Touchable>
|
</Touchable>
|
||||||
<Text style={style.callBack}>{i18n.t('Call_was_not_answered')}</Text>
|
<Text style={style.callBack}>{i18n.t('Call_was_not_answered')}</Text>
|
||||||
|
|
|
@ -3,18 +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 { useVideoConf } from '../../../../lib/hooks/useVideoConf';
|
import { videoConfJoin } from '../../../../lib/methods/videoConf';
|
||||||
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={() => joinCall(blockId)}>
|
<Touchable style={style.callToActionButton} onPress={() => videoConfJoin(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} />
|
||||||
|
|
|
@ -100,7 +100,8 @@ 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
|
||||||
|
|
|
@ -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 { useVideoConf } from '../../lib/hooks/useVideoConf';
|
import { videoConfJoin } from '../../lib/methods/videoConf';
|
||||||
import { IText } from './interfaces';
|
import { IText } from './interfaces';
|
||||||
|
|
||||||
export const textParser = ([{ text }]: IText[]) => text;
|
export const textParser = ([{ text }]: IText[]) => text;
|
||||||
|
@ -40,7 +40,6 @@ export const useBlockContext = ({ blockId, actionId, appId, initialValue }: IUse
|
||||||
const { action, appId: appIdFromContext, viewId, state, language, errors, values = {} } = useContext(KitContext);
|
const { 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];
|
||||||
|
|
||||||
|
@ -58,7 +57,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 joinCall(blockId);
|
return videoConfJoin(blockId);
|
||||||
}
|
}
|
||||||
await action({
|
await action({
|
||||||
blockId,
|
blockId,
|
||||||
|
|
|
@ -16,11 +16,13 @@ const styles = StyleSheet.create({
|
||||||
},
|
},
|
||||||
bottomContainerText: {
|
bottomContainerText: {
|
||||||
...sharedStyles.textRegular,
|
...sharedStyles.textRegular,
|
||||||
fontSize: 13
|
fontSize: 13,
|
||||||
|
textAlign: 'center'
|
||||||
},
|
},
|
||||||
bottomContainerTextBold: {
|
bottomContainerTextBold: {
|
||||||
...sharedStyles.textSemibold,
|
...sharedStyles.textSemibold,
|
||||||
fontSize: 13
|
fontSize: 13,
|
||||||
|
textAlign: 'center'
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -126,6 +126,12 @@ export const Links = () => (
|
||||||
<View style={styles.container}>
|
<View style={styles.container}>
|
||||||
<Markdown msg='[Markdown link](https://rocket.chat): `[description](url)`' theme={theme} />
|
<Markdown msg='[Markdown link](https://rocket.chat): `[description](url)`' theme={theme} />
|
||||||
<Markdown msg='<https://rocket.chat|Formatted Link>: `<url|description>`' theme={theme} />
|
<Markdown msg='<https://rocket.chat|Formatted Link>: `<url|description>`' theme={theme} />
|
||||||
|
<Markdown msg='[Markdown link](https://rocket.chat) and the text with default style' theme={theme} />
|
||||||
|
<Markdown
|
||||||
|
msg='[Markdown link](https://rocket.chat) and the text with a color specific as auxiliaryText'
|
||||||
|
theme={theme}
|
||||||
|
style={[{ color: themes[theme].auxiliaryText }]}
|
||||||
|
/>
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
@ -140,10 +140,10 @@ class Markdown extends PureComponent<IMarkdownProps, any> {
|
||||||
}
|
}
|
||||||
|
|
||||||
renderText = ({ context, literal }: { context: []; literal: string }) => {
|
renderText = ({ context, literal }: { context: []; literal: string }) => {
|
||||||
const { numberOfLines, style = [] } = this.props;
|
const { numberOfLines } = this.props;
|
||||||
const defaultStyle = [this.isMessageContainsOnlyEmoji ? styles.textBig : {}, ...context.map(type => styles[type])];
|
const defaultStyle = [this.isMessageContainsOnlyEmoji ? styles.textBig : {}, ...context.map(type => styles[type])];
|
||||||
return (
|
return (
|
||||||
<Text accessibilityLabel={literal} style={[styles.text, defaultStyle, ...style]} numberOfLines={numberOfLines}>
|
<Text accessibilityLabel={literal} style={[styles.text, defaultStyle]} numberOfLines={numberOfLines}>
|
||||||
{literal}
|
{literal}
|
||||||
</Text>
|
</Text>
|
||||||
);
|
);
|
||||||
|
@ -193,12 +193,12 @@ class Markdown extends PureComponent<IMarkdownProps, any> {
|
||||||
};
|
};
|
||||||
|
|
||||||
renderParagraph = ({ children }: any) => {
|
renderParagraph = ({ children }: any) => {
|
||||||
const { numberOfLines, style, theme } = this.props;
|
const { numberOfLines, style = [], theme } = this.props;
|
||||||
if (!children || children.length === 0) {
|
if (!children || children.length === 0) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<Text style={[styles.text, style, { color: themes[theme!].bodyText }]} numberOfLines={numberOfLines}>
|
<Text style={[styles.text, { color: themes[theme!].bodyText }, ...style]} numberOfLines={numberOfLines}>
|
||||||
{children}
|
{children}
|
||||||
</Text>
|
</Text>
|
||||||
);
|
);
|
||||||
|
|
|
@ -17,7 +17,7 @@ const OrderedList = ({ value }: IOrderedListProps): React.ReactElement => {
|
||||||
{value.map(item => (
|
{value.map(item => (
|
||||||
<View style={styles.row} key={item.number?.toString()}>
|
<View style={styles.row} key={item.number?.toString()}>
|
||||||
<Text style={[styles.text, { color: colors.bodyText }]}>{item.number}. </Text>
|
<Text style={[styles.text, { color: colors.bodyText }]}>{item.number}. </Text>
|
||||||
<Text style={{ color: colors.bodyText }}>
|
<Text style={[styles.inline, { color: colors.bodyText }]}>
|
||||||
<Inline value={item.value} />
|
<Inline value={item.value} />
|
||||||
</Text>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
|
|
|
@ -18,7 +18,7 @@ const UnorderedList = ({ value }: IUnorderedListProps) => {
|
||||||
{value.map(item => (
|
{value.map(item => (
|
||||||
<View style={styles.row}>
|
<View style={styles.row}>
|
||||||
<Text style={[styles.text, { color: themes[theme].bodyText }]}>- </Text>
|
<Text style={[styles.text, { color: themes[theme].bodyText }]}>- </Text>
|
||||||
<Text style={{ color: themes[theme].bodyText }}>
|
<Text style={[styles.inline, { color: themes[theme].bodyText }]}>
|
||||||
<Inline value={item.value} />
|
<Inline value={item.value} />
|
||||||
</Text>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
|
|
|
@ -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(({ callJitsi }: IMessageCallButton) => {
|
const CallButton = React.memo(({ handleEnterCall }: IMessageCallButton) => {
|
||||||
const { theme } = useTheme();
|
const { theme } = useTheme();
|
||||||
return (
|
return (
|
||||||
<View style={styles.buttonContainer}>
|
<View style={styles.buttonContainer}>
|
||||||
<Touchable
|
<Touchable
|
||||||
onPress={callJitsi}
|
onPress={handleEnterCall}
|
||||||
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}
|
||||||
|
|
|
@ -53,7 +53,7 @@ const Content = React.memo(
|
||||||
content = (
|
content = (
|
||||||
<Markdown
|
<Markdown
|
||||||
msg={props.msg}
|
msg={props.msg}
|
||||||
md={props.md}
|
md={props.type !== 'e2e' ? props.md : undefined}
|
||||||
getCustomEmoji={props.getCustomEmoji}
|
getCustomEmoji={props.getCustomEmoji}
|
||||||
enableMessageParser={user.enableMessageParserEarlyAdoption}
|
enableMessageParser={user.enableMessageParserEarlyAdoption}
|
||||||
username={user.username}
|
username={user.username}
|
||||||
|
|
|
@ -634,6 +634,28 @@ export const URL = () => (
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
export const URLImagePreview = () => (
|
||||||
|
<>
|
||||||
|
<Message
|
||||||
|
urls={[
|
||||||
|
{
|
||||||
|
url: 'https://www.google.com/logos/doodles/2022/seasonal-holidays-2022-6753651837109831.4-law.gif',
|
||||||
|
title: 'Google',
|
||||||
|
description:
|
||||||
|
"Search the world's information, including webpages, images, videos and more. Google has many special features to help you find exactly what you're looking for."
|
||||||
|
}
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
<Message
|
||||||
|
urls={[
|
||||||
|
{
|
||||||
|
url: 'https://www.google.com/logos/doodles/2022/seasonal-holidays-2022-6753651837109831.4-law.gif'
|
||||||
|
}
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
|
||||||
export const CustomFields = () => (
|
export const CustomFields = () => (
|
||||||
<>
|
<>
|
||||||
<Message
|
<Message
|
||||||
|
|
|
@ -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={small ? 2 : 4}
|
borderRadius={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}
|
||||||
|
|
|
@ -232,7 +232,9 @@ const Reply = React.memo(
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
{/* The testID is to test properly quoted messages using it as ancestor */}
|
||||||
<Touchable
|
<Touchable
|
||||||
|
testID={`reply-${attachment?.author_name}-${attachment?.text}`}
|
||||||
onPress={onPress}
|
onPress={onPress}
|
||||||
style={[
|
style={[
|
||||||
styles.button,
|
styles.button,
|
||||||
|
@ -247,6 +249,8 @@ 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}
|
||||||
|
@ -255,8 +259,6 @@ 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]}>
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import React, { useContext } from 'react';
|
import React, { useContext, useState } from 'react';
|
||||||
import { StyleSheet, Text, View } from 'react-native';
|
import { StyleSheet, Text, View } from 'react-native';
|
||||||
import Clipboard from '@react-native-clipboard/clipboard';
|
import Clipboard from '@react-native-clipboard/clipboard';
|
||||||
import FastImage from 'react-native-fast-image';
|
import FastImage from 'react-native-fast-image';
|
||||||
|
@ -48,38 +48,34 @@ const styles = StyleSheet.create({
|
||||||
height: 150,
|
height: 150,
|
||||||
borderTopLeftRadius: 4,
|
borderTopLeftRadius: 4,
|
||||||
borderTopRightRadius: 4
|
borderTopRightRadius: 4
|
||||||
|
},
|
||||||
|
imageWithoutContent: {
|
||||||
|
borderRadius: 4
|
||||||
|
},
|
||||||
|
loading: {
|
||||||
|
height: 0,
|
||||||
|
borderWidth: 0
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const UrlImage = React.memo(
|
|
||||||
({ image }: { image: string }) => {
|
|
||||||
const { baseUrl, user } = useContext(MessageContext);
|
|
||||||
|
|
||||||
if (!image) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
image = image.includes('http') ? image : `${baseUrl}/${image}?rc_uid=${user.id}&rc_token=${user.token}`;
|
|
||||||
return <FastImage source={{ uri: image }} style={styles.image} resizeMode={FastImage.resizeMode.cover} />;
|
|
||||||
},
|
|
||||||
(prevProps, nextProps) => prevProps.image === nextProps.image
|
|
||||||
);
|
|
||||||
|
|
||||||
const UrlContent = React.memo(
|
const UrlContent = React.memo(
|
||||||
({ title, description, theme }: { title: string; description: string; theme: TSupportedThemes }) => (
|
({ title, description }: { title: string; description: string }) => {
|
||||||
<View style={styles.textContainer}>
|
const { colors } = useTheme();
|
||||||
{title ? (
|
return (
|
||||||
<Text style={[styles.title, { color: themes[theme].tintColor }]} numberOfLines={2}>
|
<View style={styles.textContainer}>
|
||||||
{title}
|
{title ? (
|
||||||
</Text>
|
<Text style={[styles.title, { color: colors.tintColor }]} numberOfLines={2}>
|
||||||
) : null}
|
{title}
|
||||||
{description ? (
|
</Text>
|
||||||
<Text style={[styles.description, { color: themes[theme].auxiliaryText }]} numberOfLines={2}>
|
) : null}
|
||||||
{description}
|
{description ? (
|
||||||
</Text>
|
<Text style={[styles.description, { color: colors.auxiliaryText }]} numberOfLines={2}>
|
||||||
) : null}
|
{description}
|
||||||
</View>
|
</Text>
|
||||||
),
|
) : null}
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
},
|
||||||
(prevProps, nextProps) => {
|
(prevProps, nextProps) => {
|
||||||
if (prevProps.title !== nextProps.title) {
|
if (prevProps.title !== nextProps.title) {
|
||||||
return false;
|
return false;
|
||||||
|
@ -87,16 +83,18 @@ const UrlContent = React.memo(
|
||||||
if (prevProps.description !== nextProps.description) {
|
if (prevProps.description !== nextProps.description) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (prevProps.theme !== nextProps.theme) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
type TImageLoadedState = 'loading' | 'done' | 'error';
|
||||||
|
|
||||||
const Url = React.memo(
|
const Url = React.memo(
|
||||||
({ url, index, theme }: { url: IUrl; index: number; theme: TSupportedThemes }) => {
|
({ url, index, theme }: { url: IUrl; index: number; theme: TSupportedThemes }) => {
|
||||||
if (!url || url?.ignoreParse) {
|
const [imageLoadedState, setImageLoadedState] = useState<TImageLoadedState>('loading');
|
||||||
|
const { baseUrl, user } = useContext(MessageContext);
|
||||||
|
|
||||||
|
if (!url || url?.ignoreParse || imageLoadedState === 'error') {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -107,6 +105,13 @@ const Url = React.memo(
|
||||||
EventEmitter.emit(LISTENER, { message: I18n.t('Copied_to_clipboard') });
|
EventEmitter.emit(LISTENER, { message: I18n.t('Copied_to_clipboard') });
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const hasContent = url.title || url.description;
|
||||||
|
|
||||||
|
let image = url.image || url.url;
|
||||||
|
if (image) {
|
||||||
|
image = image.includes('http') ? image : `${baseUrl}/${image}?rc_uid=${user.id}&rc_token=${user.token}`;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Touchable
|
<Touchable
|
||||||
onPress={onPress}
|
onPress={onPress}
|
||||||
|
@ -118,13 +123,22 @@ const Url = React.memo(
|
||||||
{
|
{
|
||||||
backgroundColor: themes[theme].chatComponentBackground,
|
backgroundColor: themes[theme].chatComponentBackground,
|
||||||
borderColor: themes[theme].borderColor
|
borderColor: themes[theme].borderColor
|
||||||
}
|
},
|
||||||
|
imageLoadedState === 'loading' && styles.loading
|
||||||
]}
|
]}
|
||||||
background={Touchable.Ripple(themes[theme].bannerBackground)}
|
background={Touchable.Ripple(themes[theme].bannerBackground)}
|
||||||
>
|
>
|
||||||
<>
|
<>
|
||||||
<UrlImage image={url.image} />
|
{image ? (
|
||||||
<UrlContent title={url.title} description={url.description} theme={theme} />
|
<FastImage
|
||||||
|
source={{ uri: image }}
|
||||||
|
style={[styles.image, !hasContent && styles.imageWithoutContent, imageLoadedState === 'loading' && styles.loading]}
|
||||||
|
resizeMode={FastImage.resizeMode.cover}
|
||||||
|
onError={() => setImageLoadedState('error')}
|
||||||
|
onLoad={() => setImageLoadedState('done')}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
{hasContent ? <UrlContent title={url.title} description={url.description} /> : null}
|
||||||
</>
|
</>
|
||||||
</Touchable>
|
</Touchable>
|
||||||
);
|
);
|
||||||
|
@ -146,7 +160,6 @@ const Urls = React.memo(
|
||||||
(oldProps, newProps) => dequal(oldProps.urls, newProps.urls)
|
(oldProps, newProps) => dequal(oldProps.urls, newProps.urls)
|
||||||
);
|
);
|
||||||
|
|
||||||
UrlImage.displayName = 'MessageUrlImage';
|
|
||||||
UrlContent.displayName = 'MessageUrlContent';
|
UrlContent.displayName = 'MessageUrlContent';
|
||||||
Url.displayName = 'MessageUrl';
|
Url.displayName = 'MessageUrl';
|
||||||
Urls.displayName = 'MessageUrls';
|
Urls.displayName = 'MessageUrls';
|
||||||
|
|
|
@ -50,7 +50,7 @@ interface IMessageContainerProps {
|
||||||
showAttachment: (file: IAttachment) => void;
|
showAttachment: (file: IAttachment) => void;
|
||||||
onReactionLongPress?: (item: TAnyMessageModel) => void;
|
onReactionLongPress?: (item: TAnyMessageModel) => void;
|
||||||
navToRoomInfo: (navParam: IRoomInfoParam) => void;
|
navToRoomInfo: (navParam: IRoomInfoParam) => void;
|
||||||
callJitsi?: () => void;
|
handleEnterCall?: () => void;
|
||||||
blockAction?: (params: { actionId: string; appId: string; value: string; blockId: string; rid: string; mid: string }) => void;
|
blockAction?: (params: { actionId: string; appId: string; value: string; blockId: string; rid: string; mid: string }) => void;
|
||||||
onAnswerButtonPress?: (message: string, tmid?: string, tshow?: boolean) => void;
|
onAnswerButtonPress?: (message: string, tmid?: string, tshow?: boolean) => void;
|
||||||
threadBadgeColor?: string;
|
threadBadgeColor?: string;
|
||||||
|
@ -69,7 +69,6 @@ class MessageContainer extends React.Component<IMessageContainerProps, IMessageC
|
||||||
static defaultProps = {
|
static defaultProps = {
|
||||||
getCustomEmoji: () => null,
|
getCustomEmoji: () => null,
|
||||||
onLongPress: () => {},
|
onLongPress: () => {},
|
||||||
callJitsi: () => {},
|
|
||||||
blockAction: () => {},
|
blockAction: () => {},
|
||||||
archived: false,
|
archived: false,
|
||||||
broadcast: false,
|
broadcast: false,
|
||||||
|
@ -338,7 +337,7 @@ class MessageContainer extends React.Component<IMessageContainerProps, IMessageC
|
||||||
navToRoomInfo,
|
navToRoomInfo,
|
||||||
getCustomEmoji,
|
getCustomEmoji,
|
||||||
isThreadRoom,
|
isThreadRoom,
|
||||||
callJitsi,
|
handleEnterCall,
|
||||||
blockAction,
|
blockAction,
|
||||||
rid,
|
rid,
|
||||||
threadBadgeColor,
|
threadBadgeColor,
|
||||||
|
@ -456,7 +455,7 @@ class MessageContainer extends React.Component<IMessageContainerProps, IMessageC
|
||||||
showAttachment={showAttachment}
|
showAttachment={showAttachment}
|
||||||
getCustomEmoji={getCustomEmoji}
|
getCustomEmoji={getCustomEmoji}
|
||||||
navToRoomInfo={navToRoomInfo}
|
navToRoomInfo={navToRoomInfo}
|
||||||
callJitsi={callJitsi}
|
handleEnterCall={handleEnterCall}
|
||||||
blockAction={blockAction}
|
blockAction={blockAction}
|
||||||
highlighted={highlighted}
|
highlighted={highlighted}
|
||||||
comment={comment}
|
comment={comment}
|
||||||
|
|
|
@ -40,7 +40,7 @@ export interface IMessageBroadcast {
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IMessageCallButton {
|
export interface IMessageCallButton {
|
||||||
callJitsi?: () => void;
|
handleEnterCall?: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IMessageContent {
|
export interface IMessageContent {
|
||||||
|
|
|
@ -147,7 +147,10 @@ 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 TAnyMessageModel = TMessageModel | TThreadModel | TThreadMessageModel;
|
||||||
export type TTypeMessages = IMessageFromServer | ILoadMoreMessage | IMessage;
|
export type TTypeMessages = IMessageFromServer | ILoadMoreMessage | IMessage;
|
||||||
|
|
|
@ -17,16 +17,14 @@ export interface IAvatarButton {
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IAvatar {
|
export interface IAvatar {
|
||||||
data: {} | string | null;
|
data: string | null;
|
||||||
url?: string;
|
url?: string;
|
||||||
contentType?: string;
|
contentType?: string;
|
||||||
service?: any;
|
service?: any;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IAvatarSuggestion {
|
export interface IAvatarSuggestion {
|
||||||
[service: string]: {
|
url: string;
|
||||||
url: string;
|
blob: string;
|
||||||
blob: string;
|
contentType: string;
|
||||||
contentType: string;
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -38,4 +38,7 @@ export interface IThread extends IMessage {
|
||||||
draftMessage?: string;
|
draftMessage?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type TThreadModel = IThread & Model;
|
export type TThreadModel = IThread &
|
||||||
|
Model & {
|
||||||
|
asPlain: () => IMessage;
|
||||||
|
};
|
||||||
|
|
|
@ -6,4 +6,7 @@ export interface IThreadMessage extends IMessage {
|
||||||
tmsg?: string;
|
tmsg?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type TThreadMessageModel = IThreadMessage & Model;
|
export type TThreadMessageModel = IThreadMessage &
|
||||||
|
Model & {
|
||||||
|
asPlain: () => IMessage;
|
||||||
|
};
|
||||||
|
|
|
@ -4,37 +4,34 @@ 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 enum VideoConferenceStatus {
|
export declare 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';
|
||||||
callee: IUser['_id'];
|
calleeId: 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 =
|
||||||
export type VideoConferenceType = DirectCallInstructions['type'] | ConferenceInstructions['type'] | LivechatInstructions['type'];
|
| DirectCallInstructions['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;
|
||||||
|
@ -45,51 +42,68 @@ 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 type VideoConference = IDirectVideoConference | IGroupVideoConference | ILivechatVideoConference;
|
export declare type VideoConferenceInstructions = DirectCallInstructions | ConferenceInstructions | LivechatInstructions;
|
||||||
|
export declare const isDirectVideoConference: (call: VideoConference | undefined | null) => call is IDirectVideoConference;
|
||||||
export type VideoConferenceInstructions = DirectCallInstructions | ConferenceInstructions | LivechatInstructions;
|
export declare const isGroupVideoConference: (call: VideoConference | undefined | null) => call is IGroupVideoConference;
|
||||||
|
export declare const isLivechatVideoConference: (call: VideoConference | undefined | null) => call is ILivechatVideoConference;
|
||||||
export const isDirectVideoConference = (call: VideoConference | undefined | null): call is IDirectVideoConference =>
|
declare type GroupVideoConferenceCreateData = Omit<IGroupVideoConference, 'createdBy'> & {
|
||||||
call?.type === 'direct';
|
createdBy: IUser['_id'];
|
||||||
|
};
|
||||||
export const isGroupVideoConference = (call: VideoConference | undefined | null): call is IGroupVideoConference =>
|
declare type DirectVideoConferenceCreateData = Omit<IDirectVideoConference, 'createdBy'> & {
|
||||||
call?.type === 'videoconference';
|
createdBy: IUser['_id'];
|
||||||
|
};
|
||||||
export const isLivechatVideoConference = (call: VideoConference | undefined | null): call is ILivechatVideoConference =>
|
declare type LivechatVideoConferenceCreateData = Omit<ILivechatVideoConference, 'createdBy'> & {
|
||||||
call?.type === 'livechat';
|
createdBy: IUser['_id'];
|
||||||
|
};
|
||||||
type GroupVideoConferenceCreateData = Omit<IGroupVideoConference, 'createdBy'> & { createdBy: IUser['_id'] };
|
export declare type VideoConferenceCreateData = AtLeast<
|
||||||
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 };
|
||||||
|
|
|
@ -0,0 +1 @@
|
||||||
|
export type TChangeAvatarViewContext = 'profile' | 'room';
|
|
@ -33,6 +33,7 @@ export type ChatEndpoints = {
|
||||||
GET: (params: { roomId: IServerRoom['_id']; text?: string; offset: number; count: number }) => {
|
GET: (params: { roomId: IServerRoom['_id']; text?: string; offset: number; count: number }) => {
|
||||||
messages: IMessageFromServer[];
|
messages: IMessageFromServer[];
|
||||||
total: number;
|
total: number;
|
||||||
|
count: number;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
'chat.getThreadsList': {
|
'chat.getThreadsList': {
|
||||||
|
|
|
@ -1,27 +1,45 @@
|
||||||
import { VideoConference } from '../../IVideoConference';
|
import {
|
||||||
|
VideoConfCancelProps,
|
||||||
|
VideoConference,
|
||||||
|
VideoConferenceCapabilities,
|
||||||
|
VideoConferenceInstructions,
|
||||||
|
VideoConfInfoProps,
|
||||||
|
VideoConfJoinProps,
|
||||||
|
VideoConfListProps,
|
||||||
|
VideoConfStartProps
|
||||||
|
} from '../../IVideoConference';
|
||||||
|
import { PaginatedResult } from '../helpers/PaginatedResult';
|
||||||
|
|
||||||
export type VideoConferenceEndpoints = {
|
export type VideoConferenceEndpoints = {
|
||||||
'video-conference/jitsi.update-timeout': {
|
|
||||||
POST: (params: { roomId: string }) => void;
|
|
||||||
};
|
|
||||||
'video-conference.join': {
|
|
||||||
POST: (params: { callId: string; state: { cam: boolean } }) => { url: string; providerName: string };
|
|
||||||
};
|
|
||||||
'video-conference.start': {
|
'video-conference.start': {
|
||||||
POST: (params: { roomId: string }) => { url: string };
|
POST: (params: VideoConfStartProps) => { data: VideoConferenceInstructions & { providerName: string } };
|
||||||
|
};
|
||||||
|
|
||||||
|
'video-conference.join': {
|
||||||
|
POST: (params: VideoConfJoinProps) => { url: string; providerName: string };
|
||||||
};
|
};
|
||||||
|
|
||||||
'video-conference.cancel': {
|
'video-conference.cancel': {
|
||||||
POST: (params: { callId: string }) => void;
|
POST: (params: VideoConfCancelProps) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
'video-conference.info': {
|
'video-conference.info': {
|
||||||
GET: (params: { callId: string }) => VideoConference & {
|
GET: (params: VideoConfInfoProps) => VideoConference & { capabilities: VideoConferenceCapabilities };
|
||||||
capabilities: {
|
};
|
||||||
mic?: boolean;
|
|
||||||
cam?: boolean;
|
'video-conference.list': {
|
||||||
title?: boolean;
|
GET: (params: VideoConfListProps) => PaginatedResult<{ data: VideoConference[] }>;
|
||||||
};
|
};
|
||||||
};
|
|
||||||
|
'video-conference.capabilities': {
|
||||||
|
GET: () => { providerName: string; capabilities: VideoConferenceCapabilities };
|
||||||
|
};
|
||||||
|
|
||||||
|
'video-conference.providers': {
|
||||||
|
GET: () => { data: { key: string; label: string }[] };
|
||||||
|
};
|
||||||
|
|
||||||
|
'video-conference/jitsi.update-timeout': {
|
||||||
|
POST: (params: { roomId: string }) => void;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
|
@ -12,3 +12,6 @@ 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;
|
||||||
|
}
|
||||||
|
|
|
@ -768,7 +768,7 @@
|
||||||
"Convert_to_Team_Warning": "You are converting this Channel to a Team. All Members will be kept.",
|
"Convert_to_Team_Warning": "You are converting this Channel to a Team. All Members will be kept.",
|
||||||
"Move_to_Team": "Move to Team",
|
"Move_to_Team": "Move to Team",
|
||||||
"Move_Channel_Paragraph": "Moving a channel inside a team means that this channel will be added in the team’s context, however, all channel’s members, which are not members of the respective team, will still have access to this channel, but will not be added as team’s members. \n\nAll channel’s management will still be made by the owners of this channel.\n\nTeam’s members and even team’s owners, if not a member of this channel, can not have access to the channel’s content. \n\nPlease notice that the Team’s owner will be able remove members from the Channel.",
|
"Move_Channel_Paragraph": "Moving a channel inside a team means that this channel will be added in the team’s context, however, all channel’s members, which are not members of the respective team, will still have access to this channel, but will not be added as team’s members. \n\nAll channel’s management will still be made by the owners of this channel.\n\nTeam’s members and even team’s owners, if not a member of this channel, can not have access to the channel’s content. \n\nPlease notice that the Team’s owner will be able remove members from the Channel.",
|
||||||
"Move_to_Team_Warning": "After reading the previous intructions about this behavior, do you still want to move this channel to the selected team?",
|
"Move_to_Team_Warning": "After reading the previous instructions about this behavior, do you still want to move this channel to the selected team?",
|
||||||
"Load_More": "Load More",
|
"Load_More": "Load More",
|
||||||
"Load_Newer": "Load Newer",
|
"Load_Newer": "Load Newer",
|
||||||
"Load_Older": "Load Older",
|
"Load_Older": "Load Older",
|
||||||
|
@ -877,6 +877,26 @@
|
||||||
"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",
|
||||||
|
"Upload_image": "Upload image",
|
||||||
|
"Delete_image": "Delete image",
|
||||||
|
"Images_uploaded": "Images uploaded",
|
||||||
|
"Avatar": "Avatar",
|
||||||
|
"insert_Avatar_URL": "insert image URL here",
|
||||||
|
"Discard_changes":"Discard changes?",
|
||||||
|
"Discard":"Discard",
|
||||||
|
"Discard_changes_description":"All changes will be lost if you go back without saving.",
|
||||||
|
"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_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.",
|
"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"
|
"Learn_more": "Learn more"
|
||||||
|
|
|
@ -876,6 +876,14 @@
|
||||||
"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",
|
||||||
|
"Upload_image": "Carregar imagem",
|
||||||
|
"Delete_image": "Deletar imagem",
|
||||||
|
"Images_uploaded": "Imagens carregadas",
|
||||||
|
"Avatar": "Avatar",
|
||||||
|
"insert_Avatar_URL": "insira o URL da imagem aqui",
|
||||||
|
"Discard_changes":"Descartar alterações?",
|
||||||
|
"Discard":"Descartar",
|
||||||
|
"Discard_changes_description":"Todas as alterações serão perdidas, se você sair sem salvar.",
|
||||||
"Presence_Cap_Warning_Title": "Status do usuário desabilitado temporariamente",
|
"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."
|
"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."
|
||||||
}
|
}
|
|
@ -59,6 +59,7 @@ export const colors = {
|
||||||
buttonText: '#ffffff',
|
buttonText: '#ffffff',
|
||||||
passcodeBackground: '#EEEFF1',
|
passcodeBackground: '#EEEFF1',
|
||||||
passcodeButtonActive: '#E4E7EA',
|
passcodeButtonActive: '#E4E7EA',
|
||||||
|
editAndUploadButtonAvatar: '#E4E7EA',
|
||||||
passcodeLockIcon: '#6C727A',
|
passcodeLockIcon: '#6C727A',
|
||||||
passcodePrimary: '#2F343D',
|
passcodePrimary: '#2F343D',
|
||||||
passcodeSecondary: '#6C727A',
|
passcodeSecondary: '#6C727A',
|
||||||
|
@ -128,6 +129,7 @@ export const colors = {
|
||||||
buttonText: '#ffffff',
|
buttonText: '#ffffff',
|
||||||
passcodeBackground: '#030C1B',
|
passcodeBackground: '#030C1B',
|
||||||
passcodeButtonActive: '#0B182C',
|
passcodeButtonActive: '#0B182C',
|
||||||
|
editAndUploadButtonAvatar: '#0B182C',
|
||||||
passcodeLockIcon: '#6C727A',
|
passcodeLockIcon: '#6C727A',
|
||||||
passcodePrimary: '#FFFFFF',
|
passcodePrimary: '#FFFFFF',
|
||||||
passcodeSecondary: '#CBCED1',
|
passcodeSecondary: '#CBCED1',
|
||||||
|
@ -197,6 +199,7 @@ export const colors = {
|
||||||
buttonText: '#ffffff',
|
buttonText: '#ffffff',
|
||||||
passcodeBackground: '#000000',
|
passcodeBackground: '#000000',
|
||||||
passcodeButtonActive: '#0E0D0D',
|
passcodeButtonActive: '#0E0D0D',
|
||||||
|
editAndUploadButtonAvatar: '#0E0D0D',
|
||||||
passcodeLockIcon: '#6C727A',
|
passcodeLockIcon: '#6C727A',
|
||||||
passcodePrimary: '#FFFFFF',
|
passcodePrimary: '#FFFFFF',
|
||||||
passcodeSecondary: '#CBCED1',
|
passcodeSecondary: '#CBCED1',
|
||||||
|
|
|
@ -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
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -13,7 +13,7 @@ export const getMessageById = async (messageId: string | null) => {
|
||||||
try {
|
try {
|
||||||
const result = await messageCollection.find(messageId);
|
const result = await messageCollection.find(messageId);
|
||||||
return result;
|
return result;
|
||||||
} catch (error) {
|
} catch {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
|
@ -0,0 +1,25 @@
|
||||||
|
import { store } from '../../store/auxStore';
|
||||||
|
import { IAttachment, IMessage } from '../../../definitions';
|
||||||
|
import { getAvatarURL } from '../../methods/helpers';
|
||||||
|
|
||||||
|
export function createQuoteAttachment(message: IMessage, messageLink: string): IAttachment {
|
||||||
|
const { server, version: serverVersion } = store.getState().server;
|
||||||
|
const externalProviderUrl = (store.getState().settings?.Accounts_AvatarExternalProviderUrl as string) || '';
|
||||||
|
|
||||||
|
return {
|
||||||
|
text: message.msg,
|
||||||
|
...('translations' in message && { translations: message?.translations }),
|
||||||
|
message_link: messageLink,
|
||||||
|
author_name: message.alias || message.u.username,
|
||||||
|
author_icon: getAvatarURL({
|
||||||
|
avatar: message.u?.username && `/avatar/${message.u?.username}`,
|
||||||
|
type: message.t,
|
||||||
|
userId: message.u?._id,
|
||||||
|
server,
|
||||||
|
serverVersion,
|
||||||
|
externalProviderUrl
|
||||||
|
}),
|
||||||
|
attachments: message.attachments || [],
|
||||||
|
ts: message.ts
|
||||||
|
};
|
||||||
|
}
|
|
@ -0,0 +1,12 @@
|
||||||
|
import { getMessageUrlRegex } from './getMessageUrlRegex';
|
||||||
|
|
||||||
|
describe('Should regex', () => {
|
||||||
|
test('a common quote separated by space', () => {
|
||||||
|
const quote = '[ ](https://open.rocket.chat/group/room?msg=rid) test';
|
||||||
|
expect(quote.match(getMessageUrlRegex())).toStrictEqual(['https://open.rocket.chat/group/room?msg=rid']);
|
||||||
|
});
|
||||||
|
test('a quote separated by break line', () => {
|
||||||
|
const quote = '[ ](https://open.rocket.chat/group/room?msg=rid)\ntest';
|
||||||
|
expect(quote.match(getMessageUrlRegex())).toStrictEqual(['https://open.rocket.chat/group/room?msg=rid']);
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,3 @@
|
||||||
|
// https://github.com/RocketChat/Rocket.Chat/blob/0226236b871d12c62338111c70b65d5d406447a3/apps/meteor/lib/getMessageUrlRegex.ts#L1-L2
|
||||||
|
export const getMessageUrlRegex = (): RegExp =>
|
||||||
|
/([A-Za-z]{3,9}):\/\/([-;:&=\+\$,\w]+@{1})?([-A-Za-z0-9\.]+)+:?(\d+)?((\/[-\+=!:~%\/\.@\,\w]*)?\??([-\+=&!:;%@\/\.\,\w]+)?(?:#([^\s\)]+))?)?/g;
|
|
@ -0,0 +1,14 @@
|
||||||
|
import { IMessage } from '../../../definitions';
|
||||||
|
|
||||||
|
export const mapMessageFromAPI = ({ attachments, tlm, ts, _updatedAt, ...message }: IMessage) => ({
|
||||||
|
...message,
|
||||||
|
ts: new Date(ts),
|
||||||
|
...(tlm && { tlm: new Date(tlm) }),
|
||||||
|
_updatedAt: new Date(_updatedAt),
|
||||||
|
...(attachments && {
|
||||||
|
attachments: attachments.map(({ ts, ...attachment }) => ({
|
||||||
|
...(ts && { ts: new Date(ts) }),
|
||||||
|
...(attachment as any)
|
||||||
|
}))
|
||||||
|
})
|
||||||
|
});
|
|
@ -0,0 +1,17 @@
|
||||||
|
import { TMessageModel } from '../../../definitions';
|
||||||
|
|
||||||
|
export const mapMessageFromDB = (messageModel: TMessageModel) => {
|
||||||
|
const parsedMessage = messageModel.asPlain();
|
||||||
|
return {
|
||||||
|
...parsedMessage,
|
||||||
|
ts: new Date(parsedMessage.ts),
|
||||||
|
...(parsedMessage.tlm && { tlm: new Date(parsedMessage.tlm) }),
|
||||||
|
_updatedAt: new Date(parsedMessage._updatedAt),
|
||||||
|
...(parsedMessage.attachments && {
|
||||||
|
attachments: parsedMessage.attachments.map(({ ts, ...attachment }) => ({
|
||||||
|
...(ts && { ts: new Date(ts) }),
|
||||||
|
...(attachment as any)
|
||||||
|
}))
|
||||||
|
})
|
||||||
|
};
|
||||||
|
};
|
|
@ -2,7 +2,9 @@ import EJSON from 'ejson';
|
||||||
import { Base64 } from 'js-base64';
|
import { Base64 } from 'js-base64';
|
||||||
import SimpleCrypto from 'react-native-simple-crypto';
|
import SimpleCrypto from 'react-native-simple-crypto';
|
||||||
import ByteBuffer from 'bytebuffer';
|
import ByteBuffer from 'bytebuffer';
|
||||||
|
import parse from 'url-parse';
|
||||||
|
|
||||||
|
import getSingleMessage from '../methods/getSingleMessage';
|
||||||
import { IMessage, IUser } from '../../definitions';
|
import { IMessage, IUser } from '../../definitions';
|
||||||
import Deferred from './helpers/deferred';
|
import Deferred from './helpers/deferred';
|
||||||
import { debounce } from '../methods/helpers';
|
import { debounce } from '../methods/helpers';
|
||||||
|
@ -21,6 +23,11 @@ import {
|
||||||
import { Encryption } from './index';
|
import { Encryption } from './index';
|
||||||
import { E2E_MESSAGE_TYPE, E2E_STATUS } from '../constants';
|
import { E2E_MESSAGE_TYPE, E2E_STATUS } from '../constants';
|
||||||
import { Services } from '../services';
|
import { Services } from '../services';
|
||||||
|
import { getMessageUrlRegex } from './helpers/getMessageUrlRegex';
|
||||||
|
import { mapMessageFromAPI } from './helpers/mapMessageFromApi';
|
||||||
|
import { mapMessageFromDB } from './helpers/mapMessageFromDB';
|
||||||
|
import { createQuoteAttachment } from './helpers/createQuoteAttachment';
|
||||||
|
import { getMessageById } from '../database/services/Message';
|
||||||
|
|
||||||
export default class EncryptionRoom {
|
export default class EncryptionRoom {
|
||||||
ready: boolean;
|
ready: boolean;
|
||||||
|
@ -268,12 +275,15 @@ export default class EncryptionRoom {
|
||||||
tmsg = await this.decryptText(tmsg);
|
tmsg = await this.decryptText(tmsg);
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
const decryptedMessage: IMessage = {
|
||||||
...message,
|
...message,
|
||||||
tmsg,
|
tmsg,
|
||||||
msg,
|
msg,
|
||||||
e2e: E2E_STATUS.DONE
|
e2e: 'done'
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const decryptedMessageWithQuote = await this.decryptQuoteAttachment(decryptedMessage);
|
||||||
|
return decryptedMessageWithQuote;
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
// Do nothing
|
// Do nothing
|
||||||
|
@ -281,4 +291,37 @@ export default class EncryptionRoom {
|
||||||
|
|
||||||
return message;
|
return message;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
async decryptQuoteAttachment(message: IMessage) {
|
||||||
|
const urls = message?.msg?.match(getMessageUrlRegex()) || [];
|
||||||
|
await Promise.all(
|
||||||
|
urls.map(async (url: string) => {
|
||||||
|
const parsedUrl = parse(url, true);
|
||||||
|
const messageId = parsedUrl.query?.msg;
|
||||||
|
if (!messageId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// From local db
|
||||||
|
const messageFromDB = await getMessageById(messageId);
|
||||||
|
if (messageFromDB && messageFromDB.e2e === 'done') {
|
||||||
|
const decryptedQuoteMessage = mapMessageFromDB(messageFromDB);
|
||||||
|
message.attachments = message.attachments || [];
|
||||||
|
const quoteAttachment = createQuoteAttachment(decryptedQuoteMessage, url);
|
||||||
|
return message.attachments.push(quoteAttachment);
|
||||||
|
}
|
||||||
|
|
||||||
|
// From API
|
||||||
|
const quotedMessageObject = await getSingleMessage(messageId);
|
||||||
|
if (!quotedMessageObject) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const decryptedQuoteMessage = await this.decrypt(mapMessageFromAPI(quotedMessageObject));
|
||||||
|
message.attachments = message.attachments || [];
|
||||||
|
const quoteAttachment = createQuoteAttachment(decryptedQuoteMessage, url);
|
||||||
|
return message.attachments.push(quoteAttachment);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
return message;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,27 +0,0 @@
|
||||||
import { useCallback } from 'react';
|
|
||||||
|
|
||||||
import { TActionSheetOptionsItem, useActionSheet } from '../../containers/ActionSheet';
|
|
||||||
import i18n from '../../i18n';
|
|
||||||
import { videoConfJoin } from '../methods/videoConf';
|
|
||||||
|
|
||||||
export const useVideoConf = (): { joinCall: (blockId: string) => void } => {
|
|
||||||
const { showActionSheet } = useActionSheet();
|
|
||||||
|
|
||||||
const joinCall = useCallback(blockId => {
|
|
||||||
const options: TActionSheetOptionsItem[] = [
|
|
||||||
{
|
|
||||||
title: i18n.t('Video_call'),
|
|
||||||
icon: 'camera',
|
|
||||||
onPress: () => videoConfJoin(blockId, true)
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: i18n.t('Voice_call'),
|
|
||||||
icon: 'microphone',
|
|
||||||
onPress: () => videoConfJoin(blockId, false)
|
|
||||||
}
|
|
||||||
];
|
|
||||||
showActionSheet({ options });
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return { joinCall };
|
|
||||||
};
|
|
|
@ -0,0 +1,100 @@
|
||||||
|
import React, { useEffect, useState } from 'react';
|
||||||
|
|
||||||
|
import { useActionSheet } from '../../containers/ActionSheet';
|
||||||
|
import StartACallActionSheet from '../../containers/UIKit/VideoConferenceBlock/components/StartACallActionSheet';
|
||||||
|
import { ISubscription, SubscriptionType } from '../../definitions';
|
||||||
|
import i18n from '../../i18n';
|
||||||
|
import { getUserSelector } from '../../selectors/login';
|
||||||
|
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 = async () => {
|
||||||
|
if (isServer5OrNewer) return setShowCallOption(true);
|
||||||
|
const room = await getSubscriptionByRoomId(rid);
|
||||||
|
|
||||||
|
if (room) {
|
||||||
|
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
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
handleShowCallOption();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return { showInitCallActionSheet, showCallOption };
|
||||||
|
};
|
|
@ -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: ISubscription, onlyAudio = false): Promise<void> {
|
export async function callJitsi({ room, cam = false }: { room: ISubscription; cam?: boolean }): Promise<void> {
|
||||||
logEvent(onlyAudio ? events.RA_JITSI_AUDIO : events.RA_JITSI_VIDEO);
|
logEvent(cam ? events.RA_JITSI_AUDIO : events.RA_JITSI_VIDEO);
|
||||||
const url = await jitsiURL({ room });
|
const url = await jitsiURL({ room });
|
||||||
Navigation.navigate('JitsiMeetView', { url, onlyAudio, rid: room?.rid });
|
Navigation.navigate('JitsiMeetView', { url, onlyAudio: cam, rid: room?.rid });
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,20 @@
|
||||||
|
import ensureSecureProtocol from './ensureSecureProtocol';
|
||||||
|
|
||||||
|
describe('Add the protocol https at the begin of the URL', () => {
|
||||||
|
it('return the link as original when sent with https at the begin', () => {
|
||||||
|
const linkHttps = 'https://www.google.com';
|
||||||
|
expect(ensureSecureProtocol(linkHttps)).toBe(linkHttps);
|
||||||
|
});
|
||||||
|
it('return the link as original when sent with http at the begin', () => {
|
||||||
|
const linkHttp = 'http://www.google.com';
|
||||||
|
expect(ensureSecureProtocol(linkHttp)).toBe(linkHttp);
|
||||||
|
});
|
||||||
|
it("return a new link with protocol at the begin when there isn't the protocol at the begin", () => {
|
||||||
|
const linkWithoutProtocol = 'www.google.com';
|
||||||
|
expect(ensureSecureProtocol(linkWithoutProtocol)).toBe('https://www.google.com');
|
||||||
|
});
|
||||||
|
it('return the link correctly when the original starts with double slash, because the server is returning that', () => {
|
||||||
|
const linkWithDoubleSlashAtBegin = '//www.google.com';
|
||||||
|
expect(ensureSecureProtocol(linkWithDoubleSlashAtBegin)).toBe('https://www.google.com');
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,10 @@
|
||||||
|
// If the link does not have the protocol at the beginning, we are inserting https as the default,
|
||||||
|
// since by convention the most used is the secure protocol, with the same behavior as the web.
|
||||||
|
const ensureSecureProtocol = (url: string): string => {
|
||||||
|
if (!url.toLowerCase().startsWith('http')) {
|
||||||
|
return `https://${url.replace('//', '')}`;
|
||||||
|
}
|
||||||
|
return url;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ensureSecureProtocol;
|
|
@ -0,0 +1,32 @@
|
||||||
|
import { formatUrl } from './getAvatarUrl';
|
||||||
|
|
||||||
|
jest.mock('react-native', () => ({ PixelRatio: { get: () => 1 } }));
|
||||||
|
|
||||||
|
describe('formatUrl function', () => {
|
||||||
|
test('formats the default URL to get the user avatar', () => {
|
||||||
|
const url = 'https://mobile.rocket.chat/avatar/reinaldoneto';
|
||||||
|
const size = 30;
|
||||||
|
const query = '&extraparam=true';
|
||||||
|
const expected = 'https://mobile.rocket.chat/avatar/reinaldoneto?format=png&size=30&extraparam=true';
|
||||||
|
const result = formatUrl(url, size, query);
|
||||||
|
expect(result).toEqual(expected);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('formats an external provider URI to get the user avatar', () => {
|
||||||
|
const url = 'https://open.rocket.chat/avatar/reinaldoneto';
|
||||||
|
const size = 30;
|
||||||
|
const query = undefined;
|
||||||
|
const expected = 'https://open.rocket.chat/avatar/reinaldoneto?format=png&size=30';
|
||||||
|
const result = formatUrl(url, size, query);
|
||||||
|
expect(result).toEqual(expected);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('formats an external provider URI that already includes a query to get the user avatar', () => {
|
||||||
|
const url = 'https://open.rocket.chat/avatar?rcusername=reinaldoneto';
|
||||||
|
const size = 30;
|
||||||
|
const query = undefined;
|
||||||
|
const expected = 'https://open.rocket.chat/avatar?rcusername=reinaldoneto&format=png&size=30';
|
||||||
|
const result = formatUrl(url, size, query);
|
||||||
|
expect(result).toEqual(expected);
|
||||||
|
});
|
||||||
|
});
|
|
@ -4,7 +4,10 @@ import { SubscriptionType } from '../../../definitions';
|
||||||
import { IAvatar } from '../../../containers/Avatar/interfaces';
|
import { IAvatar } from '../../../containers/Avatar/interfaces';
|
||||||
import { compareServerVersion } from './compareServerVersion';
|
import { compareServerVersion } from './compareServerVersion';
|
||||||
|
|
||||||
const formatUrl = (url: string, size: number, query?: string) => `${url}?format=png&size=${PixelRatio.get() * size}${query}`;
|
export const formatUrl = (url: string, size: number, query?: string) => {
|
||||||
|
const hasQuestionMark = /\/[^\/?]+\?/.test(url);
|
||||||
|
return `${url}${hasQuestionMark ? '&' : '?'}format=png&size=${PixelRatio.get() * size}${query || ''}`;
|
||||||
|
};
|
||||||
|
|
||||||
export const getAvatarURL = ({
|
export const getAvatarURL = ({
|
||||||
type,
|
type,
|
||||||
|
|
|
@ -0,0 +1,10 @@
|
||||||
|
import { Image } from 'react-native';
|
||||||
|
|
||||||
|
export const isImageURL = async (url: string): Promise<boolean> => {
|
||||||
|
try {
|
||||||
|
const result = await Image.prefetch(url);
|
||||||
|
return result;
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
};
|
|
@ -14,3 +14,4 @@ export * from './server';
|
||||||
export * from './url';
|
export * from './url';
|
||||||
export * from './isValidEmail';
|
export * from './isValidEmail';
|
||||||
export * from './random';
|
export * from './random';
|
||||||
|
export * from './image';
|
||||||
|
|
|
@ -5,6 +5,7 @@ import parse from 'url-parse';
|
||||||
import { themes } from '../../constants';
|
import { themes } from '../../constants';
|
||||||
import { TSupportedThemes } from '../../../theme';
|
import { TSupportedThemes } from '../../../theme';
|
||||||
import UserPreferences from '../userPreferences';
|
import UserPreferences from '../userPreferences';
|
||||||
|
import ensureSecureProtocol from './ensureSecureProtocol';
|
||||||
|
|
||||||
export const DEFAULT_BROWSER_KEY = 'DEFAULT_BROWSER_KEY';
|
export const DEFAULT_BROWSER_KEY = 'DEFAULT_BROWSER_KEY';
|
||||||
|
|
||||||
|
@ -37,9 +38,9 @@ const appSchemeURL = (url: string, browser: string): string => {
|
||||||
};
|
};
|
||||||
|
|
||||||
const openLink = async (url: string, theme: TSupportedThemes = 'light'): Promise<void> => {
|
const openLink = async (url: string, theme: TSupportedThemes = 'light'): Promise<void> => {
|
||||||
|
url = ensureSecureProtocol(url);
|
||||||
try {
|
try {
|
||||||
const browser = UserPreferences.getString(DEFAULT_BROWSER_KEY);
|
const browser = UserPreferences.getString(DEFAULT_BROWSER_KEY);
|
||||||
|
|
||||||
if (browser === 'inApp') {
|
if (browser === 'inApp') {
|
||||||
await WebBrowser.openBrowserAsync(url, {
|
await WebBrowser.openBrowserAsync(url, {
|
||||||
toolbarColor: themes[theme].headerBackground,
|
toolbarColor: themes[theme].headerBackground,
|
||||||
|
|
|
@ -86,14 +86,15 @@ class ReviewApp {
|
||||||
positiveEventCount = 0;
|
positiveEventCount = 0;
|
||||||
|
|
||||||
pushPositiveEvent = () => {
|
pushPositiveEvent = () => {
|
||||||
if (!isFDroidBuild) {
|
if (isFDroidBuild || process.env.RUNNING_E2E_TESTS === 'true') {
|
||||||
if (this.positiveEventCount >= numberOfPositiveEvent) {
|
return;
|
||||||
return;
|
}
|
||||||
}
|
if (this.positiveEventCount >= numberOfPositiveEvent) {
|
||||||
this.positiveEventCount += 1;
|
return;
|
||||||
if (this.positiveEventCount === numberOfPositiveEvent) {
|
}
|
||||||
tryReview();
|
this.positiveEventCount += 1;
|
||||||
}
|
if (this.positiveEventCount === numberOfPositiveEvent) {
|
||||||
|
tryReview();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import { KeyboardAwareScrollViewProps } from '@codler/react-native-keyboard-aware-scroll-view';
|
import { KeyboardAwareScrollViewProps } from '@codler/react-native-keyboard-aware-scroll-view';
|
||||||
|
|
||||||
const scrollPersistTaps: Partial<KeyboardAwareScrollViewProps> = {
|
const scrollPersistTaps: Partial<KeyboardAwareScrollViewProps> = {
|
||||||
keyboardShouldPersistTaps: 'always',
|
keyboardShouldPersistTaps: 'handled',
|
||||||
keyboardDismissMode: 'interactive'
|
keyboardDismissMode: 'interactive'
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -291,7 +291,7 @@ export default function subscribeRooms() {
|
||||||
const [type, data] = ddpMessage.fields.args;
|
const [type, data] = ddpMessage.fields.args;
|
||||||
const [, ev] = ddpMessage.fields.eventName.split('/');
|
const [, ev] = ddpMessage.fields.eventName.split('/');
|
||||||
if (/userData/.test(ev)) {
|
if (/userData/.test(ev)) {
|
||||||
const [{ diff }] = ddpMessage.fields.args;
|
const [{ diff, unset }] = ddpMessage.fields.args;
|
||||||
if (diff?.statusLivechat) {
|
if (diff?.statusLivechat) {
|
||||||
store.dispatch(setUser({ statusLivechat: diff.statusLivechat }));
|
store.dispatch(setUser({ statusLivechat: diff.statusLivechat }));
|
||||||
}
|
}
|
||||||
|
@ -301,6 +301,12 @@ export default function subscribeRooms() {
|
||||||
if ((['settings.preferences.alsoSendThreadToChannel'] as any) in diff) {
|
if ((['settings.preferences.alsoSendThreadToChannel'] as any) in diff) {
|
||||||
store.dispatch(setUser({ alsoSendThreadToChannel: diff['settings.preferences.alsoSendThreadToChannel'] }));
|
store.dispatch(setUser({ alsoSendThreadToChannel: diff['settings.preferences.alsoSendThreadToChannel'] }));
|
||||||
}
|
}
|
||||||
|
if (diff?.avatarETag) {
|
||||||
|
store.dispatch(setUser({ avatarETag: diff.avatarETag }));
|
||||||
|
}
|
||||||
|
if (unset?.avatarETag) {
|
||||||
|
store.dispatch(setUser({ avatarETag: '' }));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (/subscriptions/.test(ev)) {
|
if (/subscriptions/.test(ev)) {
|
||||||
if (type === 'removed') {
|
if (type === 'removed') {
|
||||||
|
|
|
@ -19,9 +19,9 @@ const handleBltPermission = async (): Promise<Permission[]> => {
|
||||||
return [PermissionsAndroid.PERMISSIONS.ACCESS_COARSE_LOCATION];
|
return [PermissionsAndroid.PERMISSIONS.ACCESS_COARSE_LOCATION];
|
||||||
};
|
};
|
||||||
|
|
||||||
export const videoConfJoin = async (callId: string, cam: boolean) => {
|
export const videoConfJoin = async (callId: string, cam?: boolean, mic?: boolean): Promise<void> => {
|
||||||
try {
|
try {
|
||||||
const result = await Services.videoConferenceJoin(callId, cam);
|
const result = await Services.videoConferenceJoin(callId, cam, mic);
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
if (isAndroid) {
|
if (isAndroid) {
|
||||||
const bltPermission = await handleBltPermission();
|
const bltPermission = await handleBltPermission();
|
||||||
|
@ -44,11 +44,11 @@ export const videoConfJoin = async (callId: string, cam: boolean) => {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const videoConfStartAndJoin = async (rid: string, cam: boolean) => {
|
export const videoConfStartAndJoin = async ({ rid, cam, mic }: { rid: string; cam?: boolean; mic?: boolean }): Promise<void> => {
|
||||||
try {
|
try {
|
||||||
const videoConfResponse: any = await Services.videoConferenceStart(rid);
|
const videoConfResponse = await Services.videoConferenceStart(rid);
|
||||||
if (videoConfResponse.success) {
|
if (videoConfResponse.success) {
|
||||||
videoConfJoin(videoConfResponse.data.callId, cam);
|
videoConfJoin(videoConfResponse.data.callId, cam, mic);
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
showErrorAlert(i18n.t('error-init-video-conf'));
|
showErrorAlert(i18n.t('error-init-video-conf'));
|
||||||
|
|
|
@ -0,0 +1,27 @@
|
||||||
|
import BackgroundTimer from 'react-native-background-timer';
|
||||||
|
|
||||||
|
import { Services } from '../services';
|
||||||
|
|
||||||
|
let interval: number | null = null;
|
||||||
|
|
||||||
|
export const initVideoConfTimer = (rid: string): void => {
|
||||||
|
if (rid) {
|
||||||
|
Services.updateJitsiTimeout(rid).catch((e: unknown) => console.log(e));
|
||||||
|
if (interval) {
|
||||||
|
BackgroundTimer.clearInterval(interval);
|
||||||
|
BackgroundTimer.stopBackgroundTimer();
|
||||||
|
interval = null;
|
||||||
|
}
|
||||||
|
interval = BackgroundTimer.setInterval(() => {
|
||||||
|
Services.updateJitsiTimeout(rid).catch((e: unknown) => console.log(e));
|
||||||
|
}, 10000);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const endVideoConfTimer = (): void => {
|
||||||
|
if (interval) {
|
||||||
|
BackgroundTimer.clearInterval(interval);
|
||||||
|
interval = null;
|
||||||
|
BackgroundTimer.stopBackgroundTimer();
|
||||||
|
}
|
||||||
|
};
|
|
@ -561,7 +561,7 @@ export const saveRoomSettings = (
|
||||||
rid: string,
|
rid: string,
|
||||||
params: {
|
params: {
|
||||||
roomName?: string;
|
roomName?: string;
|
||||||
roomAvatar?: string;
|
roomAvatar?: string | null;
|
||||||
roomDescription?: string;
|
roomDescription?: string;
|
||||||
roomTopic?: string;
|
roomTopic?: string;
|
||||||
roomAnnouncement?: string;
|
roomAnnouncement?: string;
|
||||||
|
@ -602,7 +602,7 @@ export const getRoomRoles = (
|
||||||
// RC 0.65.0
|
// RC 0.65.0
|
||||||
sdk.get(`${roomTypeToApiType(type)}.roles`, { roomId });
|
sdk.get(`${roomTypeToApiType(type)}.roles`, { roomId });
|
||||||
|
|
||||||
export const getAvatarSuggestion = (): Promise<IAvatarSuggestion> =>
|
export const getAvatarSuggestion = (): Promise<{ [service: string]: IAvatarSuggestion }> =>
|
||||||
// RC 0.51.0
|
// RC 0.51.0
|
||||||
sdk.methodCallWrapper('getAvatarSuggestion');
|
sdk.methodCallWrapper('getAvatarSuggestion');
|
||||||
|
|
||||||
|
@ -936,8 +936,10 @@ export function getUserInfo(userId: string) {
|
||||||
|
|
||||||
export const toggleFavorite = (roomId: string, favorite: boolean) => sdk.post('rooms.favorite', { roomId, favorite });
|
export const toggleFavorite = (roomId: string, favorite: boolean) => sdk.post('rooms.favorite', { roomId, favorite });
|
||||||
|
|
||||||
export const videoConferenceJoin = (callId: string, cam: boolean) =>
|
export const videoConferenceJoin = (callId: string, cam?: boolean, mic?: boolean) =>
|
||||||
sdk.post('video-conference.join', { callId, state: { cam } });
|
sdk.post('video-conference.join', { callId, state: { cam: !!cam, mic: mic === undefined ? true : mic } });
|
||||||
|
|
||||||
|
export const videoConferenceGetCapabilities = () => sdk.get('video-conference.capabilities');
|
||||||
|
|
||||||
export const videoConferenceStart = (roomId: string) => sdk.post('video-conference.start', { roomId });
|
export const videoConferenceStart = (roomId: string) => sdk.post('video-conference.start', { roomId });
|
||||||
|
|
||||||
|
|
|
@ -247,6 +247,22 @@ const handleLogout = function* handleLogout({ forcedByServer, message }) {
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSetUser = function* handleSetUser({ user }) {
|
const handleSetUser = function* handleSetUser({ user }) {
|
||||||
|
if ('avatarETag' in user) {
|
||||||
|
const userId = yield select(state => state.login.user.id);
|
||||||
|
const serversDB = database.servers;
|
||||||
|
const userCollections = serversDB.get('users');
|
||||||
|
yield serversDB.write(async () => {
|
||||||
|
try {
|
||||||
|
const userRecord = await userCollections.find(userId);
|
||||||
|
await userRecord.update(record => {
|
||||||
|
record.avatarETag = user.avatarETag;
|
||||||
|
});
|
||||||
|
} catch {
|
||||||
|
//
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
setLanguage(user?.language);
|
setLanguage(user?.language);
|
||||||
|
|
||||||
if (user?.statusLivechat && isOmnichannelModuleAvailable()) {
|
if (user?.statusLivechat && isOmnichannelModuleAvailable()) {
|
||||||
|
|
|
@ -68,6 +68,7 @@ import AddChannelTeamView from '../views/AddChannelTeamView';
|
||||||
import AddExistingChannelView from '../views/AddExistingChannelView';
|
import AddExistingChannelView from '../views/AddExistingChannelView';
|
||||||
import SelectListView from '../views/SelectListView';
|
import SelectListView from '../views/SelectListView';
|
||||||
import DiscussionsView from '../views/DiscussionsView';
|
import DiscussionsView from '../views/DiscussionsView';
|
||||||
|
import ChangeAvatarView from '../views/ChangeAvatarView';
|
||||||
import {
|
import {
|
||||||
AdminPanelStackParamList,
|
AdminPanelStackParamList,
|
||||||
ChatsStackParamList,
|
ChatsStackParamList,
|
||||||
|
@ -96,6 +97,7 @@ const ChatsStackNavigator = () => {
|
||||||
<ChatsStack.Screen name='SelectListView' component={SelectListView} options={SelectListView.navigationOptions} />
|
<ChatsStack.Screen name='SelectListView' component={SelectListView} options={SelectListView.navigationOptions} />
|
||||||
<ChatsStack.Screen name='RoomInfoView' component={RoomInfoView} options={RoomInfoView.navigationOptions} />
|
<ChatsStack.Screen name='RoomInfoView' component={RoomInfoView} options={RoomInfoView.navigationOptions} />
|
||||||
<ChatsStack.Screen name='RoomInfoEditView' component={RoomInfoEditView} options={RoomInfoEditView.navigationOptions} />
|
<ChatsStack.Screen name='RoomInfoEditView' component={RoomInfoEditView} options={RoomInfoEditView.navigationOptions} />
|
||||||
|
<ChatsStack.Screen name='ChangeAvatarView' component={ChangeAvatarView} />
|
||||||
<ChatsStack.Screen name='RoomMembersView' component={RoomMembersView} />
|
<ChatsStack.Screen name='RoomMembersView' component={RoomMembersView} />
|
||||||
<ChatsStack.Screen name='DiscussionsView' component={DiscussionsView} />
|
<ChatsStack.Screen name='DiscussionsView' component={DiscussionsView} />
|
||||||
<ChatsStack.Screen
|
<ChatsStack.Screen
|
||||||
|
@ -151,6 +153,7 @@ const ProfileStackNavigator = () => {
|
||||||
>
|
>
|
||||||
<ProfileStack.Screen name='ProfileView' component={ProfileView} options={ProfileView.navigationOptions} />
|
<ProfileStack.Screen name='ProfileView' component={ProfileView} options={ProfileView.navigationOptions} />
|
||||||
<ProfileStack.Screen name='UserPreferencesView' component={UserPreferencesView} />
|
<ProfileStack.Screen name='UserPreferencesView' component={UserPreferencesView} />
|
||||||
|
<ProfileStack.Screen name='ChangeAvatarView' component={ChangeAvatarView} />
|
||||||
<ProfileStack.Screen name='UserNotificationPrefView' component={UserNotificationPrefView} />
|
<ProfileStack.Screen name='UserNotificationPrefView' component={UserNotificationPrefView} />
|
||||||
<ProfileStack.Screen name='PickerView' component={PickerView} options={PickerView.navigationOptions} />
|
<ProfileStack.Screen name='PickerView' component={PickerView} options={PickerView.navigationOptions} />
|
||||||
</ProfileStack.Navigator>
|
</ProfileStack.Navigator>
|
||||||
|
|
|
@ -17,6 +17,7 @@ import RoomsListView from '../../views/RoomsListView';
|
||||||
import RoomActionsView from '../../views/RoomActionsView';
|
import RoomActionsView from '../../views/RoomActionsView';
|
||||||
import RoomInfoView from '../../views/RoomInfoView';
|
import RoomInfoView from '../../views/RoomInfoView';
|
||||||
import RoomInfoEditView from '../../views/RoomInfoEditView';
|
import RoomInfoEditView from '../../views/RoomInfoEditView';
|
||||||
|
import ChangeAvatarView from '../../views/ChangeAvatarView';
|
||||||
import RoomMembersView from '../../views/RoomMembersView';
|
import RoomMembersView from '../../views/RoomMembersView';
|
||||||
import SearchMessagesView from '../../views/SearchMessagesView';
|
import SearchMessagesView from '../../views/SearchMessagesView';
|
||||||
import SelectedUsersView from '../../views/SelectedUsersView';
|
import SelectedUsersView from '../../views/SelectedUsersView';
|
||||||
|
@ -128,6 +129,7 @@ const ModalStackNavigator = React.memo(({ navigation }: INavigation) => {
|
||||||
<ModalStack.Screen name='RoomInfoView' component={RoomInfoView} options={RoomInfoView.navigationOptions} />
|
<ModalStack.Screen name='RoomInfoView' component={RoomInfoView} options={RoomInfoView.navigationOptions} />
|
||||||
<ModalStack.Screen name='SelectListView' component={SelectListView} />
|
<ModalStack.Screen name='SelectListView' component={SelectListView} />
|
||||||
<ModalStack.Screen name='RoomInfoEditView' component={RoomInfoEditView} options={RoomInfoEditView.navigationOptions} />
|
<ModalStack.Screen name='RoomInfoEditView' component={RoomInfoEditView} options={RoomInfoEditView.navigationOptions} />
|
||||||
|
<ModalStack.Screen name='ChangeAvatarView' component={ChangeAvatarView} />
|
||||||
<ModalStack.Screen name='RoomMembersView' component={RoomMembersView} />
|
<ModalStack.Screen name='RoomMembersView' component={RoomMembersView} />
|
||||||
<ModalStack.Screen
|
<ModalStack.Screen
|
||||||
name='SearchMessagesView'
|
name='SearchMessagesView'
|
||||||
|
|
|
@ -6,6 +6,7 @@ import { IMessage } from '../../definitions/IMessage';
|
||||||
import { ISubscription, SubscriptionType, TSubscriptionModel } from '../../definitions/ISubscription';
|
import { ISubscription, SubscriptionType, TSubscriptionModel } from '../../definitions/ISubscription';
|
||||||
import { ILivechatDepartment } from '../../definitions/ILivechatDepartment';
|
import { ILivechatDepartment } from '../../definitions/ILivechatDepartment';
|
||||||
import { ILivechatTag } from '../../definitions/ILivechatTag';
|
import { ILivechatTag } from '../../definitions/ILivechatTag';
|
||||||
|
import { TChangeAvatarViewContext } from '../../definitions/TChangeAvatarViewContext';
|
||||||
|
|
||||||
export type MasterDetailChatsStackParamList = {
|
export type MasterDetailChatsStackParamList = {
|
||||||
RoomView: {
|
RoomView: {
|
||||||
|
@ -58,6 +59,12 @@ export type ModalStackParamList = {
|
||||||
onSearch?: Function;
|
onSearch?: Function;
|
||||||
isRadio?: boolean;
|
isRadio?: boolean;
|
||||||
};
|
};
|
||||||
|
ChangeAvatarView: {
|
||||||
|
context: TChangeAvatarViewContext;
|
||||||
|
titleHeader?: string;
|
||||||
|
room?: ISubscription;
|
||||||
|
t?: SubscriptionType;
|
||||||
|
};
|
||||||
RoomInfoEditView: {
|
RoomInfoEditView: {
|
||||||
rid: string;
|
rid: string;
|
||||||
};
|
};
|
||||||
|
|
|
@ -13,6 +13,7 @@ import { ModalStackParamList } from './MasterDetailStack/types';
|
||||||
import { TThreadModel } from '../definitions';
|
import { TThreadModel } from '../definitions';
|
||||||
import { ILivechatDepartment } from '../definitions/ILivechatDepartment';
|
import { ILivechatDepartment } from '../definitions/ILivechatDepartment';
|
||||||
import { ILivechatTag } from '../definitions/ILivechatTag';
|
import { ILivechatTag } from '../definitions/ILivechatTag';
|
||||||
|
import { TChangeAvatarViewContext } from '../definitions/TChangeAvatarViewContext';
|
||||||
|
|
||||||
export type ChatsStackParamList = {
|
export type ChatsStackParamList = {
|
||||||
ModalStackNavigator: NavigatorScreenParams<ModalStackParamList>;
|
ModalStackNavigator: NavigatorScreenParams<ModalStackParamList>;
|
||||||
|
@ -181,6 +182,12 @@ export type ChatsStackParamList = {
|
||||||
onlyAudio?: boolean;
|
onlyAudio?: boolean;
|
||||||
videoConf?: boolean;
|
videoConf?: boolean;
|
||||||
};
|
};
|
||||||
|
ChangeAvatarView: {
|
||||||
|
context: TChangeAvatarViewContext;
|
||||||
|
titleHeader?: string;
|
||||||
|
room?: ISubscription;
|
||||||
|
t?: SubscriptionType;
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export type ProfileStackParamList = {
|
export type ProfileStackParamList = {
|
||||||
|
@ -195,6 +202,12 @@ export type ProfileStackParamList = {
|
||||||
goBack?: Function;
|
goBack?: Function;
|
||||||
onChangeValue: Function;
|
onChangeValue: Function;
|
||||||
};
|
};
|
||||||
|
ChangeAvatarView: {
|
||||||
|
context: TChangeAvatarViewContext;
|
||||||
|
titleHeader?: string;
|
||||||
|
room?: ISubscription;
|
||||||
|
t?: SubscriptionType;
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export type SettingsStackParamList = {
|
export type SettingsStackParamList = {
|
||||||
|
|
|
@ -0,0 +1,56 @@
|
||||||
|
import React, { useState, useEffect } from 'react';
|
||||||
|
import { Text, View } from 'react-native';
|
||||||
|
|
||||||
|
import { IAvatar } from '../../definitions';
|
||||||
|
import { Services } from '../../lib/services';
|
||||||
|
import I18n from '../../i18n';
|
||||||
|
import styles from './styles';
|
||||||
|
import { useTheme } from '../../theme';
|
||||||
|
import AvatarSuggestionItem from './AvatarSuggestionItem';
|
||||||
|
|
||||||
|
const AvatarSuggestion = ({
|
||||||
|
onPress,
|
||||||
|
username,
|
||||||
|
resetAvatar
|
||||||
|
}: {
|
||||||
|
onPress: (value: IAvatar) => void;
|
||||||
|
username?: string;
|
||||||
|
resetAvatar?: () => void;
|
||||||
|
}) => {
|
||||||
|
const [avatarSuggestions, setAvatarSuggestions] = useState<IAvatar[]>([]);
|
||||||
|
|
||||||
|
const { colors } = useTheme();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const getAvatarSuggestion = async () => {
|
||||||
|
const result = await Services.getAvatarSuggestion();
|
||||||
|
const suggestions = Object.keys(result).map(service => {
|
||||||
|
const { url, blob, contentType } = result[service];
|
||||||
|
return {
|
||||||
|
url,
|
||||||
|
data: blob,
|
||||||
|
service,
|
||||||
|
contentType
|
||||||
|
};
|
||||||
|
});
|
||||||
|
setAvatarSuggestions(suggestions);
|
||||||
|
};
|
||||||
|
getAvatarSuggestion();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View style={styles.containerImagesUploaded}>
|
||||||
|
<Text style={[styles.itemLabel, { color: colors.titleText }]}>{I18n.t('Images_uploaded')}</Text>
|
||||||
|
<View style={styles.containerAvatarSuggestion}>
|
||||||
|
{username && resetAvatar ? (
|
||||||
|
<AvatarSuggestionItem text={`@${username}`} testID={`reset-avatar-suggestion`} onPress={resetAvatar} />
|
||||||
|
) : null}
|
||||||
|
{avatarSuggestions.slice(0, 7).map(item => (
|
||||||
|
<AvatarSuggestionItem item={item} testID={`${item?.service}-avatar-suggestion`} onPress={onPress} />
|
||||||
|
))}
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default AvatarSuggestion;
|
|
@ -0,0 +1,40 @@
|
||||||
|
import React from 'react';
|
||||||
|
import { View, StyleSheet } from 'react-native';
|
||||||
|
|
||||||
|
import { IAvatar } from '../../definitions';
|
||||||
|
import Avatar from '../../containers/Avatar';
|
||||||
|
import { useTheme } from '../../theme';
|
||||||
|
|
||||||
|
const styles = StyleSheet.create({
|
||||||
|
container: {
|
||||||
|
width: 64,
|
||||||
|
height: 64,
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
marginRight: 20,
|
||||||
|
marginBottom: 12,
|
||||||
|
borderRadius: 4
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const AvatarSuggestionItem = ({
|
||||||
|
item,
|
||||||
|
onPress,
|
||||||
|
text,
|
||||||
|
testID
|
||||||
|
}: {
|
||||||
|
item?: IAvatar;
|
||||||
|
testID?: string;
|
||||||
|
onPress: Function;
|
||||||
|
text?: string;
|
||||||
|
}) => {
|
||||||
|
const { colors } = useTheme();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View key={item?.service} testID={testID} style={[styles.container, { backgroundColor: colors.borderColor }]}>
|
||||||
|
<Avatar avatar={item?.url} text={text} size={64} onPress={() => onPress(item)} />
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default AvatarSuggestionItem;
|
|
@ -0,0 +1,29 @@
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import I18n from '../../i18n';
|
||||||
|
import { FormTextInput } from '../../containers/TextInput';
|
||||||
|
import { useDebounce, isImageURL } from '../../lib/methods/helpers';
|
||||||
|
|
||||||
|
const AvatarUrl = ({ submit }: { submit: (value: string) => void }) => {
|
||||||
|
const handleChangeText = useDebounce(async (value: string) => {
|
||||||
|
if (value) {
|
||||||
|
const result = await isImageURL(value);
|
||||||
|
if (result) {
|
||||||
|
return submit(value);
|
||||||
|
}
|
||||||
|
return submit('');
|
||||||
|
}
|
||||||
|
}, 500);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FormTextInput
|
||||||
|
label={I18n.t('Avatar_Url')}
|
||||||
|
placeholder={I18n.t('insert_Avatar_URL')}
|
||||||
|
onChangeText={handleChangeText}
|
||||||
|
testID='change-avatar-view-avatar-url'
|
||||||
|
containerStyle={{ marginBottom: 0 }}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default AvatarUrl;
|
File diff suppressed because one or more lines are too long
|
@ -0,0 +1,4 @@
|
||||||
|
import ImagePicker, { Image as ImageInterface } from 'react-native-image-crop-picker';
|
||||||
|
|
||||||
|
export type Image = ImageInterface;
|
||||||
|
export default ImagePicker;
|
|
@ -0,0 +1,250 @@
|
||||||
|
import React, { useEffect, useLayoutEffect, useReducer, useRef, useState } from 'react';
|
||||||
|
import { ScrollView, View } from 'react-native';
|
||||||
|
import { RouteProp, useNavigation, useRoute } from '@react-navigation/native';
|
||||||
|
import { StackNavigationProp } from '@react-navigation/stack';
|
||||||
|
import { shallowEqual } from 'react-redux';
|
||||||
|
|
||||||
|
import KeyboardView from '../../containers/KeyboardView';
|
||||||
|
import sharedStyles from '../Styles';
|
||||||
|
import scrollPersistTaps from '../../lib/methods/helpers/scrollPersistTaps';
|
||||||
|
import { showConfirmationAlert, showErrorAlert } from '../../lib/methods/helpers/info';
|
||||||
|
import StatusBar from '../../containers/StatusBar';
|
||||||
|
import { useTheme } from '../../theme';
|
||||||
|
import SafeAreaView from '../../containers/SafeAreaView';
|
||||||
|
import * as List from '../../containers/List';
|
||||||
|
import styles from './styles';
|
||||||
|
import { useAppSelector } from '../../lib/hooks';
|
||||||
|
import { getUserSelector } from '../../selectors/login';
|
||||||
|
import Avatar from '../../containers/Avatar';
|
||||||
|
import AvatarPresentational from '../../containers/Avatar/Avatar';
|
||||||
|
import AvatarUrl from './AvatarUrl';
|
||||||
|
import Button from '../../containers/Button';
|
||||||
|
import I18n from '../../i18n';
|
||||||
|
import { ChatsStackParamList } from '../../stacks/types';
|
||||||
|
import { IAvatar } from '../../definitions';
|
||||||
|
import AvatarSuggestion from './AvatarSuggestion';
|
||||||
|
import log from '../../lib/methods/helpers/log';
|
||||||
|
import { changeRoomsAvatar, changeUserAvatar, resetUserAvatar } from './submitServices';
|
||||||
|
import ImagePicker, { Image } from './ImagePicker';
|
||||||
|
|
||||||
|
enum AvatarStateActions {
|
||||||
|
CHANGE_AVATAR = 'CHANGE_AVATAR',
|
||||||
|
RESET_USER_AVATAR = 'RESET_USER_AVATAR',
|
||||||
|
RESET_ROOM_AVATAR = 'RESET_ROOM_AVATAR'
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IReducerAction {
|
||||||
|
type: AvatarStateActions;
|
||||||
|
payload?: Partial<IState>;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IState extends IAvatar {
|
||||||
|
resetUserAvatar: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const initialState = {
|
||||||
|
data: '',
|
||||||
|
url: '',
|
||||||
|
contentType: '',
|
||||||
|
service: '',
|
||||||
|
resetUserAvatar: ''
|
||||||
|
};
|
||||||
|
|
||||||
|
function reducer(state: IState, action: IReducerAction) {
|
||||||
|
const { type, payload } = action;
|
||||||
|
if (type in AvatarStateActions) {
|
||||||
|
return {
|
||||||
|
...initialState,
|
||||||
|
...payload
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ChangeAvatarView = () => {
|
||||||
|
const [state, dispatch] = useReducer(reducer, initialState);
|
||||||
|
const [saving, setSaving] = useState(false);
|
||||||
|
const { colors } = useTheme();
|
||||||
|
const { userId, username, server } = useAppSelector(
|
||||||
|
state => ({
|
||||||
|
userId: getUserSelector(state).id,
|
||||||
|
username: getUserSelector(state).username,
|
||||||
|
server: state.server.server
|
||||||
|
}),
|
||||||
|
shallowEqual
|
||||||
|
);
|
||||||
|
const isDirty = useRef<boolean>(false);
|
||||||
|
const navigation = useNavigation<StackNavigationProp<ChatsStackParamList, 'ChangeAvatarView'>>();
|
||||||
|
const { context, titleHeader, room, t } = useRoute<RouteProp<ChatsStackParamList, 'ChangeAvatarView'>>().params;
|
||||||
|
|
||||||
|
useLayoutEffect(() => {
|
||||||
|
navigation.setOptions({
|
||||||
|
title: titleHeader || I18n.t('Avatar')
|
||||||
|
});
|
||||||
|
}, [titleHeader, navigation]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
navigation.addListener('beforeRemove', e => {
|
||||||
|
if (!isDirty.current) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
showConfirmationAlert({
|
||||||
|
title: I18n.t('Discard_changes'),
|
||||||
|
message: I18n.t('Discard_changes_description'),
|
||||||
|
confirmationText: I18n.t('Discard'),
|
||||||
|
onPress: () => {
|
||||||
|
navigation.dispatch(e.data.action);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}, [navigation]);
|
||||||
|
|
||||||
|
const dispatchAvatar = (action: IReducerAction) => {
|
||||||
|
isDirty.current = true;
|
||||||
|
dispatch(action);
|
||||||
|
};
|
||||||
|
|
||||||
|
const submit = async () => {
|
||||||
|
try {
|
||||||
|
setSaving(true);
|
||||||
|
if (context === 'room' && room?.rid) {
|
||||||
|
// Change Rooms Avatar
|
||||||
|
await changeRoomsAvatar(room.rid, state?.data);
|
||||||
|
} else if (state?.url) {
|
||||||
|
// Change User's Avatar
|
||||||
|
await changeUserAvatar(state);
|
||||||
|
} else if (state.resetUserAvatar) {
|
||||||
|
// Change User's Avatar
|
||||||
|
await resetUserAvatar(userId);
|
||||||
|
}
|
||||||
|
isDirty.current = false;
|
||||||
|
} catch (e: any) {
|
||||||
|
log(e);
|
||||||
|
return showErrorAlert(e.message, I18n.t('Oops'));
|
||||||
|
} finally {
|
||||||
|
setSaving(false);
|
||||||
|
}
|
||||||
|
return navigation.goBack();
|
||||||
|
};
|
||||||
|
|
||||||
|
const pickImage = async () => {
|
||||||
|
const options = {
|
||||||
|
cropping: true,
|
||||||
|
compressImageQuality: 0.8,
|
||||||
|
freeStyleCropEnabled: true,
|
||||||
|
cropperAvoidEmptySpaceAroundImage: false,
|
||||||
|
cropperChooseText: I18n.t('Choose'),
|
||||||
|
cropperCancelText: I18n.t('Cancel'),
|
||||||
|
includeBase64: true
|
||||||
|
};
|
||||||
|
try {
|
||||||
|
const response: Image = await ImagePicker.openPicker(options);
|
||||||
|
dispatchAvatar({
|
||||||
|
type: AvatarStateActions.CHANGE_AVATAR,
|
||||||
|
payload: { url: response.path, data: `data:image/jpeg;base64,${response.data}`, service: 'upload' }
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
log(error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const deletingRoomAvatar = context === 'room' && state.data === null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<KeyboardView
|
||||||
|
style={{ backgroundColor: colors.auxiliaryBackground }}
|
||||||
|
contentContainerStyle={sharedStyles.container}
|
||||||
|
keyboardVerticalOffset={128}
|
||||||
|
>
|
||||||
|
<StatusBar />
|
||||||
|
<SafeAreaView testID='change-avatar-view'>
|
||||||
|
<ScrollView
|
||||||
|
contentContainerStyle={sharedStyles.containerScrollView}
|
||||||
|
testID='change-avatar-view-list'
|
||||||
|
{...scrollPersistTaps}
|
||||||
|
>
|
||||||
|
<View style={styles.avatarContainer} testID='change-avatar-view-avatar'>
|
||||||
|
{deletingRoomAvatar ? (
|
||||||
|
<AvatarPresentational
|
||||||
|
text={room?.name || state.resetUserAvatar || username}
|
||||||
|
avatar={state?.url}
|
||||||
|
isStatic={state?.url}
|
||||||
|
size={120}
|
||||||
|
type={t}
|
||||||
|
server={server}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<Avatar
|
||||||
|
text={room?.name || state.resetUserAvatar || username}
|
||||||
|
avatar={state?.url}
|
||||||
|
isStatic={state?.url}
|
||||||
|
size={120}
|
||||||
|
type={t}
|
||||||
|
rid={room?.rid}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
{context === 'profile' ? (
|
||||||
|
<AvatarUrl
|
||||||
|
submit={value =>
|
||||||
|
dispatchAvatar({
|
||||||
|
type: AvatarStateActions.CHANGE_AVATAR,
|
||||||
|
payload: { url: value, data: value, service: 'url' }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
<List.Separator style={styles.separator} />
|
||||||
|
{context === 'profile' ? (
|
||||||
|
<AvatarSuggestion
|
||||||
|
resetAvatar={() =>
|
||||||
|
dispatchAvatar({
|
||||||
|
type: AvatarStateActions.RESET_USER_AVATAR,
|
||||||
|
payload: { resetUserAvatar: `@${username}` }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
username={username}
|
||||||
|
onPress={value =>
|
||||||
|
dispatchAvatar({
|
||||||
|
type: AvatarStateActions.CHANGE_AVATAR,
|
||||||
|
payload: value
|
||||||
|
})
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
<Button
|
||||||
|
title={I18n.t('Upload_image')}
|
||||||
|
type='secondary'
|
||||||
|
disabled={saving}
|
||||||
|
backgroundColor={colors.editAndUploadButtonAvatar}
|
||||||
|
onPress={pickImage}
|
||||||
|
testID='change-avatar-view-upload-image'
|
||||||
|
/>
|
||||||
|
{context === 'room' ? (
|
||||||
|
<Button
|
||||||
|
title={I18n.t('Delete_image')}
|
||||||
|
type='primary'
|
||||||
|
disabled={saving}
|
||||||
|
backgroundColor={colors.dangerColor}
|
||||||
|
onPress={() => dispatchAvatar({ type: AvatarStateActions.RESET_ROOM_AVATAR, payload: { data: null } })}
|
||||||
|
testID='change-avatar-view-delete-my-account'
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
<Button
|
||||||
|
title={I18n.t('Save')}
|
||||||
|
disabled={!isDirty.current || saving}
|
||||||
|
type='primary'
|
||||||
|
loading={saving}
|
||||||
|
onPress={submit}
|
||||||
|
testID='change-avatar-view-submit'
|
||||||
|
/>
|
||||||
|
</ScrollView>
|
||||||
|
</SafeAreaView>
|
||||||
|
</KeyboardView>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ChangeAvatarView;
|
|
@ -0,0 +1,27 @@
|
||||||
|
import { StyleSheet } from 'react-native';
|
||||||
|
|
||||||
|
import sharedStyles from '../Styles';
|
||||||
|
|
||||||
|
export default StyleSheet.create({
|
||||||
|
avatarContainer: {
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
marginBottom: 24
|
||||||
|
},
|
||||||
|
separator: {
|
||||||
|
marginVertical: 16
|
||||||
|
},
|
||||||
|
itemLabel: {
|
||||||
|
marginBottom: 12,
|
||||||
|
fontSize: 14,
|
||||||
|
...sharedStyles.textSemibold
|
||||||
|
},
|
||||||
|
containerImagesUploaded: {
|
||||||
|
flex: 1
|
||||||
|
},
|
||||||
|
containerAvatarSuggestion: {
|
||||||
|
flex: 1,
|
||||||
|
flexWrap: 'wrap',
|
||||||
|
flexDirection: 'row'
|
||||||
|
}
|
||||||
|
});
|
|
@ -0,0 +1,14 @@
|
||||||
|
import I18n from '../../i18n';
|
||||||
|
|
||||||
|
export const handleError = (e: any, action: string) => {
|
||||||
|
if (e.data && e.data.error.includes('[error-too-many-requests]')) {
|
||||||
|
throw new Error(e.data.error);
|
||||||
|
}
|
||||||
|
if (e.error && e.error === 'error-avatar-invalid-url') {
|
||||||
|
throw new Error(I18n.t(e.error, { url: e.details.url }));
|
||||||
|
}
|
||||||
|
if (I18n.isTranslated(e.error)) {
|
||||||
|
throw new Error(I18n.t(e.error));
|
||||||
|
}
|
||||||
|
throw new Error(I18n.t('There_was_an_error_while_action', { action: I18n.t(action) }));
|
||||||
|
};
|
|
@ -0,0 +1,29 @@
|
||||||
|
import { Services } from '../../lib/services';
|
||||||
|
import log from '../../lib/methods/helpers/log';
|
||||||
|
import { IAvatar } from '../../definitions';
|
||||||
|
import { handleError } from './submitHelpers';
|
||||||
|
|
||||||
|
export const changeRoomsAvatar = async (rid: string, roomAvatar: string | null) => {
|
||||||
|
try {
|
||||||
|
await Services.saveRoomSettings(rid, { roomAvatar });
|
||||||
|
} catch (e) {
|
||||||
|
log(e);
|
||||||
|
return handleError(e, 'changing_avatar');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const changeUserAvatar = async (avatarUpload: IAvatar) => {
|
||||||
|
try {
|
||||||
|
await Services.setAvatarFromService(avatarUpload);
|
||||||
|
} catch (e) {
|
||||||
|
return handleError(e, 'changing_avatar');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const resetUserAvatar = async (userId: string) => {
|
||||||
|
try {
|
||||||
|
await Services.resetAvatar(userId);
|
||||||
|
} catch (e) {
|
||||||
|
return handleError(e, 'changing_avatar');
|
||||||
|
}
|
||||||
|
};
|
|
@ -243,7 +243,7 @@ class DirectoryView extends React.Component<IDirectoryViewProps, IDirectoryViewS
|
||||||
title: item.name as string,
|
title: item.name as string,
|
||||||
onPress: () => this.onPressItem(item),
|
onPress: () => this.onPressItem(item),
|
||||||
baseUrl,
|
baseUrl,
|
||||||
testID: `directory-view-item-${item.name}`.toLowerCase(),
|
testID: `directory-view-item-${item.name}`,
|
||||||
style,
|
style,
|
||||||
user,
|
user,
|
||||||
theme,
|
theme,
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import React, { useEffect, useLayoutEffect, useState } from 'react';
|
import React, { useEffect, useLayoutEffect, useRef, useState } from 'react';
|
||||||
import { FlatList, StyleSheet } from 'react-native';
|
import { FlatList, StyleSheet } from 'react-native';
|
||||||
import { StackNavigationOptions, StackNavigationProp } from '@react-navigation/stack';
|
import { StackNavigationOptions, StackNavigationProp } from '@react-navigation/stack';
|
||||||
import { HeaderBackButton } from '@react-navigation/elements';
|
import { HeaderBackButton } from '@react-navigation/elements';
|
||||||
|
@ -46,12 +46,13 @@ const DiscussionsView = ({ navigation, route }: IDiscussionsViewProps): React.Re
|
||||||
const [discussions, setDiscussions] = useState<IMessageFromServer[]>([]);
|
const [discussions, setDiscussions] = useState<IMessageFromServer[]>([]);
|
||||||
const [search, setSearch] = useState<IMessageFromServer[]>([]);
|
const [search, setSearch] = useState<IMessageFromServer[]>([]);
|
||||||
const [isSearching, setIsSearching] = useState(false);
|
const [isSearching, setIsSearching] = useState(false);
|
||||||
const [total, setTotal] = useState(0);
|
const total = useRef(0);
|
||||||
const [searchTotal, setSearchTotal] = useState(0);
|
const searchText = useRef('');
|
||||||
|
const offset = useRef(0);
|
||||||
|
|
||||||
const { colors } = useTheme();
|
const { colors } = useTheme();
|
||||||
|
|
||||||
const load = async (text = '') => {
|
const load = async () => {
|
||||||
if (loading) {
|
if (loading) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -60,18 +61,18 @@ const DiscussionsView = ({ navigation, route }: IDiscussionsViewProps): React.Re
|
||||||
try {
|
try {
|
||||||
const result = await Services.getDiscussions({
|
const result = await Services.getDiscussions({
|
||||||
roomId: rid,
|
roomId: rid,
|
||||||
offset: isSearching ? search.length : discussions.length,
|
offset: offset.current,
|
||||||
count: API_FETCH_COUNT,
|
count: API_FETCH_COUNT,
|
||||||
text
|
text: searchText.current
|
||||||
});
|
});
|
||||||
|
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
|
offset.current += result.count;
|
||||||
|
total.current = result.total;
|
||||||
if (isSearching) {
|
if (isSearching) {
|
||||||
setSearch(result.messages);
|
setSearch(prevState => (offset.current ? [...prevState, ...result.messages] : result.messages));
|
||||||
setSearchTotal(result.total);
|
|
||||||
} else {
|
} else {
|
||||||
setDiscussions(result.messages);
|
setDiscussions(result.messages);
|
||||||
setTotal(result.total);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
|
@ -81,15 +82,19 @@ const DiscussionsView = ({ navigation, route }: IDiscussionsViewProps): React.Re
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const onSearchChangeText = useDebounce(async (text: string) => {
|
const onSearchChangeText = useDebounce((text: string) => {
|
||||||
setIsSearching(true);
|
setIsSearching(true);
|
||||||
await load(text);
|
setSearch([]);
|
||||||
|
searchText.current = text;
|
||||||
|
offset.current = 0;
|
||||||
|
load();
|
||||||
}, 500);
|
}, 500);
|
||||||
|
|
||||||
const onCancelSearchPress = () => {
|
const onCancelSearchPress = () => {
|
||||||
setIsSearching(false);
|
setIsSearching(false);
|
||||||
setSearch([]);
|
setSearch([]);
|
||||||
setSearchTotal(0);
|
searchText.current = '';
|
||||||
|
offset.current = 0;
|
||||||
};
|
};
|
||||||
|
|
||||||
const onSearchPress = () => {
|
const onSearchPress = () => {
|
||||||
|
@ -181,12 +186,12 @@ const DiscussionsView = ({ navigation, route }: IDiscussionsViewProps): React.Re
|
||||||
<FlatList
|
<FlatList
|
||||||
data={isSearching ? search : discussions}
|
data={isSearching ? search : discussions}
|
||||||
renderItem={renderItem}
|
renderItem={renderItem}
|
||||||
keyExtractor={(item: any) => item.msg}
|
keyExtractor={(item: any) => item._id}
|
||||||
style={{ backgroundColor: colors.backgroundColor }}
|
style={{ backgroundColor: colors.backgroundColor }}
|
||||||
contentContainerStyle={styles.contentContainer}
|
contentContainerStyle={styles.contentContainer}
|
||||||
onEndReachedThreshold={0.5}
|
onEndReachedThreshold={0.5}
|
||||||
removeClippedSubviews={isIOS}
|
removeClippedSubviews={isIOS}
|
||||||
onEndReached={() => (isSearching ? searchTotal : total) > API_FETCH_COUNT ?? load()}
|
onEndReached={() => isSearching && offset.current < total.current && load()}
|
||||||
ItemSeparatorComponent={List.Separator}
|
ItemSeparatorComponent={List.Separator}
|
||||||
ListFooterComponent={loading ? <ActivityIndicator /> : null}
|
ListFooterComponent={loading ? <ActivityIndicator /> : null}
|
||||||
scrollIndicatorInsets={{ right: 1 }}
|
scrollIndicatorInsets={{ right: 1 }}
|
||||||
|
|
|
@ -1,14 +1,13 @@
|
||||||
|
import { activateKeepAwake, deactivateKeepAwake } from 'expo-keep-awake';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { BackHandler, NativeEventSubscription } from 'react-native';
|
import { BackHandler, NativeEventSubscription } from 'react-native';
|
||||||
import BackgroundTimer from 'react-native-background-timer';
|
|
||||||
import { isAppInstalled, openAppWithUri } from 'react-native-send-intent';
|
import { isAppInstalled, openAppWithUri } from 'react-native-send-intent';
|
||||||
import WebView from 'react-native-webview';
|
import WebView from 'react-native-webview';
|
||||||
import { WebViewMessage, WebViewNavigation } from 'react-native-webview/lib/WebViewTypes';
|
import { WebViewMessage, WebViewNavigation } from 'react-native-webview/lib/WebViewTypes';
|
||||||
import { activateKeepAwake, deactivateKeepAwake } from 'expo-keep-awake';
|
|
||||||
|
|
||||||
import { IBaseScreen } from '../definitions';
|
import { IBaseScreen } from '../definitions';
|
||||||
import { events, logEvent } from '../lib/methods/helpers/log';
|
import { events, logEvent } from '../lib/methods/helpers/log';
|
||||||
import { Services } from '../lib/services';
|
import { endVideoConfTimer, initVideoConfTimer } from '../lib/methods/videoConfTimer';
|
||||||
import { ChatsStackParamList } from '../stacks/types';
|
import { ChatsStackParamList } from '../stacks/types';
|
||||||
import { withTheme } from '../theme';
|
import { withTheme } from '../theme';
|
||||||
|
|
||||||
|
@ -20,7 +19,6 @@ class JitsiMeetView extends React.Component<TJitsiMeetViewProps> {
|
||||||
private rid: string;
|
private rid: string;
|
||||||
private url: string;
|
private url: string;
|
||||||
private videoConf: boolean;
|
private videoConf: boolean;
|
||||||
private jitsiTimeout: number | null;
|
|
||||||
private backHandler!: NativeEventSubscription;
|
private backHandler!: NativeEventSubscription;
|
||||||
|
|
||||||
constructor(props: TJitsiMeetViewProps) {
|
constructor(props: TJitsiMeetViewProps) {
|
||||||
|
@ -28,7 +26,6 @@ class JitsiMeetView extends React.Component<TJitsiMeetViewProps> {
|
||||||
this.rid = props.route.params?.rid;
|
this.rid = props.route.params?.rid;
|
||||||
this.url = props.route.params?.url;
|
this.url = props.route.params?.url;
|
||||||
this.videoConf = !!props.route.params?.videoConf;
|
this.videoConf = !!props.route.params?.videoConf;
|
||||||
this.jitsiTimeout = null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
|
@ -50,10 +47,8 @@ class JitsiMeetView extends React.Component<TJitsiMeetViewProps> {
|
||||||
|
|
||||||
componentWillUnmount() {
|
componentWillUnmount() {
|
||||||
logEvent(this.videoConf ? events.LIVECHAT_VIDEOCONF_TERMINATE : events.JM_CONFERENCE_TERMINATE);
|
logEvent(this.videoConf ? events.LIVECHAT_VIDEOCONF_TERMINATE : events.JM_CONFERENCE_TERMINATE);
|
||||||
if (this.jitsiTimeout && !this.videoConf) {
|
if (!this.videoConf) {
|
||||||
BackgroundTimer.clearInterval(this.jitsiTimeout);
|
endVideoConfTimer();
|
||||||
this.jitsiTimeout = null;
|
|
||||||
BackgroundTimer.stopBackgroundTimer();
|
|
||||||
}
|
}
|
||||||
this.backHandler.remove();
|
this.backHandler.remove();
|
||||||
deactivateKeepAwake();
|
deactivateKeepAwake();
|
||||||
|
@ -64,15 +59,7 @@ class JitsiMeetView extends React.Component<TJitsiMeetViewProps> {
|
||||||
onConferenceJoined = () => {
|
onConferenceJoined = () => {
|
||||||
logEvent(this.videoConf ? events.LIVECHAT_VIDEOCONF_JOIN : events.JM_CONFERENCE_JOIN);
|
logEvent(this.videoConf ? events.LIVECHAT_VIDEOCONF_JOIN : events.JM_CONFERENCE_JOIN);
|
||||||
if (this.rid && !this.videoConf) {
|
if (this.rid && !this.videoConf) {
|
||||||
Services.updateJitsiTimeout(this.rid).catch((e: unknown) => console.log(e));
|
initVideoConfTimer(this.rid);
|
||||||
if (this.jitsiTimeout) {
|
|
||||||
BackgroundTimer.clearInterval(this.jitsiTimeout);
|
|
||||||
BackgroundTimer.stopBackgroundTimer();
|
|
||||||
this.jitsiTimeout = null;
|
|
||||||
}
|
|
||||||
this.jitsiTimeout = BackgroundTimer.setInterval(() => {
|
|
||||||
Services.updateJitsiTimeout(this.rid).catch((e: unknown) => console.log(e));
|
|
||||||
}, 10000);
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -90,7 +77,7 @@ class JitsiMeetView extends React.Component<TJitsiMeetViewProps> {
|
||||||
render() {
|
render() {
|
||||||
return (
|
return (
|
||||||
<WebView
|
<WebView
|
||||||
source={{ uri: `${this.url}&config.disableDeepLinking=true` }}
|
source={{ uri: `${this.url}${this.url.includes('#config') ? '&' : '#'}config.disableDeepLinking=true` }}
|
||||||
onMessage={({ nativeEvent }) => this.onNavigationStateChange(nativeEvent)}
|
onMessage={({ nativeEvent }) => this.onNavigationStateChange(nativeEvent)}
|
||||||
onNavigationStateChange={this.onNavigationStateChange}
|
onNavigationStateChange={this.onNavigationStateChange}
|
||||||
style={{ flex: 1 }}
|
style={{ flex: 1 }}
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue