Merge 4.37.0 into master (#5025)
This commit is contained in:
commit
acdfc6ddd9
|
@ -1,6 +1,9 @@
|
|||
defaults: &defaults
|
||||
working_directory: ~/repo
|
||||
|
||||
orbs:
|
||||
android: circleci/android@2.1.2
|
||||
|
||||
macos: &macos
|
||||
macos:
|
||||
xcode: "14.2.0"
|
||||
|
@ -327,6 +330,14 @@ commands:
|
|||
working_directory: ios
|
||||
- save_cache: *save-gems-cache
|
||||
|
||||
create-e2e-account-file:
|
||||
description: "Create e2e account file"
|
||||
steps:
|
||||
- run:
|
||||
command: |
|
||||
echo $E2E_ACCOUNT | base64 --decode > ./e2e_account.ts
|
||||
working_directory: e2e
|
||||
|
||||
version: 2.1
|
||||
|
||||
# EXECUTORS
|
||||
|
@ -438,6 +449,94 @@ jobs:
|
|||
- upload-to-google-play-beta:
|
||||
official: true
|
||||
|
||||
e2e-build-android:
|
||||
<<: *defaults
|
||||
executor:
|
||||
name: android/android-machine
|
||||
resource-class: xlarge
|
||||
tag: 2022.12.1
|
||||
environment:
|
||||
<<: *android-env
|
||||
steps:
|
||||
- checkout
|
||||
- restore_cache: *restore-npm-cache-linux
|
||||
- run: *install-npm-modules
|
||||
- save_cache: *save-npm-cache-linux
|
||||
- restore_cache: *restore-gradle-cache
|
||||
- run:
|
||||
name: Configure Gradle
|
||||
command: |
|
||||
echo -e "" > ./gradle.properties
|
||||
# echo -e "android.enableAapt2=false" >> ./gradle.properties
|
||||
echo -e "android.useAndroidX=true" >> ./gradle.properties
|
||||
echo -e "android.enableJetifier=true" >> ./gradle.properties
|
||||
echo -e "newArchEnabled=false" >> ./gradle.properties
|
||||
echo -e "FLIPPER_VERSION=0.125.0" >> ./gradle.properties
|
||||
echo -e "VERSIONCODE=$CIRCLE_BUILD_NUM" >> ./gradle.properties
|
||||
echo -e "APPLICATION_ID=chat.rocket.reactnative" >> ./gradle.properties
|
||||
echo -e "BugsnagAPIKey=$BUGSNAG_KEY" >> ./gradle.properties
|
||||
echo $KEYSTORE_EXPERIMENTAL_BASE64 | base64 --decode > ./app/$KEYSTORE_EXPERIMENTAL
|
||||
echo -e "KEYSTORE=$KEYSTORE_EXPERIMENTAL" >> ./gradle.properties
|
||||
echo -e "KEYSTORE_PASSWORD=$KEYSTORE_EXPERIMENTAL_PASSWORD" >> ./gradle.properties
|
||||
echo -e "KEY_ALIAS=$KEYSTORE_EXPERIMENTAL_ALIAS" >> ./gradle.properties
|
||||
echo -e "KEY_PASSWORD=$KEYSTORE_EXPERIMENTAL_PASSWORD" >> ./gradle.properties
|
||||
working_directory: android
|
||||
- run:
|
||||
name: Build Android
|
||||
command: |
|
||||
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-build-experimental:
|
||||
executor: mac-env
|
||||
|
@ -461,11 +560,89 @@ jobs:
|
|||
- upload-to-testflight:
|
||||
official: true
|
||||
|
||||
e2e-build-ios:
|
||||
executor: mac-env
|
||||
steps:
|
||||
- checkout
|
||||
- restore_cache: *restore-gems-cache
|
||||
- restore_cache: *restore-npm-cache-mac
|
||||
- run: *install-npm-modules
|
||||
- run: *update-fastlane-ios
|
||||
- save_cache: *save-npm-cache-mac
|
||||
- save_cache: *save-gems-cache
|
||||
- manage-pods
|
||||
- run:
|
||||
name: Configure Detox
|
||||
command: |
|
||||
brew tap wix/brew
|
||||
brew install applesimutils
|
||||
- run:
|
||||
name: Build
|
||||
command: |
|
||||
/usr/libexec/PlistBuddy -c "Set :bugsnag:apiKey $BUGSNAG_KEY" ./ios/RocketChatRN/Info.plist
|
||||
/usr/libexec/PlistBuddy -c "Set :bugsnag:apiKey $BUGSNAG_KEY" ./ios/ShareRocketChatRN/Info.plist
|
||||
yarn detox clean-framework-cache && yarn detox build-framework-cache
|
||||
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:
|
||||
build-and-test:
|
||||
jobs:
|
||||
- lint-testunit
|
||||
|
||||
# E2E tests
|
||||
- e2e-hold:
|
||||
type: approval
|
||||
- e2e-build-ios:
|
||||
requires:
|
||||
- e2e-hold
|
||||
- e2e-test-ios:
|
||||
requires:
|
||||
- e2e-build-ios
|
||||
- e2e-build-android:
|
||||
requires:
|
||||
- e2e-hold
|
||||
- e2e-test-android:
|
||||
requires:
|
||||
- e2e-build-android
|
||||
|
||||
# iOS Experimental
|
||||
- ios-hold-build-experimental:
|
||||
type: approval
|
||||
|
|
|
@ -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/**'],
|
||||
globals: {
|
||||
by: true,
|
||||
detox: true,
|
||||
device: true,
|
||||
element: true,
|
||||
waitFor: true
|
||||
},
|
||||
rules: {
|
||||
'import/no-extraneous-dependencies': 0,
|
||||
'no-await-in-loop': 0,
|
||||
'no-restricted-syntax': 0,
|
||||
// TODO: remove this rule when update Detox to 20 and test if the namespace Detox is available
|
||||
'no-undef': 1
|
||||
'no-await-in-loop': 0
|
||||
}
|
||||
}
|
||||
]
|
||||
|
|
|
@ -67,5 +67,6 @@ e2e/docker/rc_test_env/docker-compose.yml
|
|||
e2e/docker/data/db
|
||||
e2e/e2e_account.js
|
||||
e2e/e2e_account.ts
|
||||
junit.xml
|
||||
|
||||
*.p8
|
|
@ -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>
|
||||
</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
|
||||
|
||||
|
|
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
|
||||
targetSdkVersion rootProject.ext.targetSdkVersion
|
||||
versionCode VERSIONCODE as Integer
|
||||
versionName "4.36.0"
|
||||
versionName "4.37.0"
|
||||
vectorDrawables.useSupportLibrary = true
|
||||
if (!isFoss) {
|
||||
manifestPlaceholders = [BugsnagAPIKey: BugsnagAPIKey as String]
|
||||
|
@ -250,6 +250,7 @@ android {
|
|||
release {
|
||||
minifyEnabled enableProguardInReleaseBuilds
|
||||
setProguardFiles([getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'])
|
||||
proguardFile "${rootProject.projectDir}/../node_modules/detox/android/detox/proguard-rules-app.pro"
|
||||
signingConfig signingConfigs.release
|
||||
if (!isFoss) {
|
||||
firebaseCrashlytics {
|
||||
|
@ -268,6 +269,11 @@ android {
|
|||
// pickFirst '**/x86_64/libc++_shared.so'
|
||||
// }
|
||||
|
||||
// FIXME: Remove when we update RN
|
||||
packagingOptions {
|
||||
pickFirst '**/*.so'
|
||||
}
|
||||
|
||||
// applicationVariants are e.g. debug, release
|
||||
|
||||
flavorDimensions "app", "type"
|
||||
|
@ -280,10 +286,6 @@ android {
|
|||
dimension = "app"
|
||||
buildConfigField "boolean", "IS_OFFICIAL", "false"
|
||||
}
|
||||
e2e {
|
||||
dimension = "app"
|
||||
buildConfigField "boolean", "IS_OFFICIAL", "false"
|
||||
}
|
||||
foss {
|
||||
dimension = "type"
|
||||
buildConfigField "boolean", "FDROID_BUILD", "true"
|
||||
|
@ -311,16 +313,6 @@ android {
|
|||
java.srcDirs = ['src/main/java', 'src/play/java']
|
||||
manifest.srcFile 'src/play/AndroidManifest.xml'
|
||||
}
|
||||
e2ePlayDebug {
|
||||
java.srcDirs = ['src/main/java', 'src/play/java']
|
||||
res.srcDirs = ['src/experimental/res']
|
||||
manifest.srcFile 'src/play/AndroidManifest.xml'
|
||||
}
|
||||
e2ePlayRelease {
|
||||
java.srcDirs = ['src/main/java', 'src/play/java']
|
||||
res.srcDirs = ['src/experimental/res']
|
||||
manifest.srcFile 'src/play/AndroidManifest.xml'
|
||||
}
|
||||
}
|
||||
|
||||
applicationVariants.all { variant ->
|
||||
|
@ -385,8 +377,9 @@ dependencies {
|
|||
implementation "com.github.bumptech.glide:glide:4.9.0"
|
||||
annotationProcessor "com.github.bumptech.glide:compiler:4.9.0"
|
||||
implementation "com.tencent:mmkv-static:1.2.10"
|
||||
androidTestImplementation('com.wix:detox:+') { transitive = true }
|
||||
androidTestImplementation 'junit:junit:4.12'
|
||||
androidTestImplementation('com.wix:detox:+')
|
||||
implementation 'androidx.appcompat:appcompat:1.1.0'
|
||||
implementation 'com.facebook.soloader:soloader:0.10.4'
|
||||
}
|
||||
|
||||
if (isNewArchitectureEnabled()) {
|
||||
|
|
|
@ -18,7 +18,7 @@ public class DetoxTest {
|
|||
@Rule
|
||||
// Replace 'MainActivity' with the value of android:name entry in
|
||||
// <activity> in AndroidManifest.xml
|
||||
public ActivityTestRule<MainActivity> mActivityRule = new ActivityTestRule<>(MainActivity.class, false, false);
|
||||
public ActivityTestRule<chat.rocket.reactnative.MainActivity> mActivityRule = new ActivityTestRule<>(chat.rocket.reactnative.MainActivity.class, false, false);
|
||||
|
||||
@Test
|
||||
public void runDetoxTests() {
|
||||
|
|
|
@ -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" />
|
||||
</trust-anchors>
|
||||
</base-config>
|
||||
<domain-config cleartextTrafficPermitted="true">
|
||||
<domain includeSubdomains="true">10.0.2.2</domain>
|
||||
<domain includeSubdomains="true">localhost</domain>
|
||||
</domain-config>
|
||||
</network-security-config>
|
|
@ -1,9 +1,5 @@
|
|||
import org.apache.tools.ant.taskdefs.condition.Os
|
||||
|
||||
def safeExtGet(prop, fallback) {
|
||||
rootProject.ext.has(prop) ? rootProject.ext.get(prop) : fallback
|
||||
}
|
||||
|
||||
// Top-level build file where you can add configuration options common to all sub-projects/modules.
|
||||
buildscript {
|
||||
def taskRequests = getGradle().getStartParameter().getTaskRequests().toString().toLowerCase()
|
||||
|
@ -75,5 +71,38 @@ allprojects {
|
|||
google()
|
||||
maven { url 'https://maven.google.com' }
|
||||
maven { url 'https://www.jitpack.io' }
|
||||
|
||||
// https://stackoverflow.com/a/74333788/5447468
|
||||
// TODO: remove once we update RN
|
||||
exclusiveContent {
|
||||
// We get React Native's Android binaries exclusively through npm,
|
||||
// from a local Maven repo inside node_modules/react-native/.
|
||||
// (The use of exclusiveContent prevents looking elsewhere like Maven Central
|
||||
// and potentially getting a wrong version.)
|
||||
filter {
|
||||
includeGroup "com.facebook.react"
|
||||
}
|
||||
forRepository {
|
||||
maven {
|
||||
// NOTE: if you are in a monorepo, you may have "$rootDir/../../../node_modules/react-native/android"
|
||||
url "$rootDir/../node_modules/react-native/android"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
subprojects { subproject ->
|
||||
afterEvaluate {
|
||||
if (!project.name.equalsIgnoreCase("app") && project.hasProperty("android")) {
|
||||
android {
|
||||
compileSdkVersion 31
|
||||
buildToolsVersion "31.0.0"
|
||||
defaultConfig {
|
||||
minSdkVersion 23
|
||||
targetSdkVersion 31
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,13 +1,11 @@
|
|||
import { Q } from '@nozbe/watermelondb';
|
||||
import React, { useEffect, useRef, useState } from 'react';
|
||||
import React from 'react';
|
||||
import { shallowEqual, useSelector } from 'react-redux';
|
||||
import { Observable, Subscription } from 'rxjs';
|
||||
|
||||
import { IApplicationState, TSubscriptionModel, TUserModel } from '../../definitions';
|
||||
import database from '../../lib/database';
|
||||
import { IApplicationState } from '../../definitions';
|
||||
import { getUserSelector } from '../../selectors/login';
|
||||
import Avatar from './Avatar';
|
||||
import { IAvatar } from './interfaces';
|
||||
import { useAvatarETag } from './useAvatarETag';
|
||||
|
||||
const AvatarContainer = ({
|
||||
style,
|
||||
|
@ -23,17 +21,13 @@ const AvatarContainer = ({
|
|||
isStatic,
|
||||
rid
|
||||
}: 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 serverVersion = useSelector((state: IApplicationState) => state.share.server.version || state.server.version);
|
||||
const { id, token } = useSelector(
|
||||
const { id, token, username } = useSelector(
|
||||
(state: IApplicationState) => ({
|
||||
id: getUserSelector(state).id,
|
||||
token: getUserSelector(state).token
|
||||
token: getUserSelector(state).token,
|
||||
username: getUserSelector(state).username
|
||||
}),
|
||||
shallowEqual
|
||||
);
|
||||
|
@ -48,41 +42,7 @@ const AvatarContainer = ({
|
|||
true
|
||||
);
|
||||
|
||||
const init = async () => {
|
||||
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]);
|
||||
const { avatarETag } = useAvatarETag({ username, text, type, rid, id });
|
||||
|
||||
return (
|
||||
<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;
|
||||
color?: string;
|
||||
fontSize?: number;
|
||||
styleText?: StyleProp<TextStyle>[];
|
||||
styleText?: StyleProp<TextStyle> | StyleProp<TextStyle>[];
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
|
|
|
@ -1,34 +1,31 @@
|
|||
import React from 'react';
|
||||
import { useWindowDimensions } from 'react-native';
|
||||
import { FlatList } from 'react-native-gesture-handler';
|
||||
|
||||
import { EMOJI_BUTTON_SIZE } from './styles';
|
||||
import scrollPersistTaps from '../../lib/methods/helpers/scrollPersistTaps';
|
||||
import { IEmoji } from '../../definitions/IEmoji';
|
||||
import scrollPersistTaps from '../../lib/methods/helpers/scrollPersistTaps';
|
||||
import { PressableEmoji } from './PressableEmoji';
|
||||
import { EMOJI_BUTTON_SIZE } from './styles';
|
||||
|
||||
interface IEmojiCategoryProps {
|
||||
emojis: IEmoji[];
|
||||
onEmojiSelected: (emoji: IEmoji) => void;
|
||||
tabLabel?: string; // needed for react-native-scrollable-tab-view only
|
||||
parentWidth: number;
|
||||
}
|
||||
|
||||
const EmojiCategory = ({ onEmojiSelected, emojis }: IEmojiCategoryProps): React.ReactElement | null => {
|
||||
const { width } = useWindowDimensions();
|
||||
|
||||
const numColumns = Math.trunc(width / EMOJI_BUTTON_SIZE);
|
||||
const marginHorizontal = (width % EMOJI_BUTTON_SIZE) / 2;
|
||||
|
||||
const renderItem = ({ item }: { item: IEmoji }) => <PressableEmoji emoji={item} onPress={onEmojiSelected} />;
|
||||
|
||||
if (!width) {
|
||||
const EmojiCategory = ({ onEmojiSelected, emojis, parentWidth }: IEmojiCategoryProps): React.ReactElement | null => {
|
||||
if (!parentWidth) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const numColumns = Math.trunc(parentWidth / EMOJI_BUTTON_SIZE);
|
||||
const marginHorizontal = (parentWidth % EMOJI_BUTTON_SIZE) / 2;
|
||||
|
||||
const renderItem = ({ item }: { item: IEmoji }) => <PressableEmoji emoji={item} onPress={onEmojiSelected} />;
|
||||
|
||||
return (
|
||||
<FlatList
|
||||
// needed to update the numColumns when the width changes
|
||||
key={`emoji-category-${width}`}
|
||||
key={`emoji-category-${parentWidth}`}
|
||||
keyExtractor={item => (typeof item === 'string' ? item : item.name)}
|
||||
data={emojis}
|
||||
renderItem={renderItem}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import React from 'react';
|
||||
import React, { useState } from 'react';
|
||||
import { View } from 'react-native';
|
||||
import ScrollableTabView from 'react-native-scrollable-tab-view';
|
||||
|
||||
|
@ -20,6 +20,8 @@ const EmojiPicker = ({
|
|||
searchedEmojis = []
|
||||
}: IEmojiPickerProps): React.ReactElement | null => {
|
||||
const { colors } = useTheme();
|
||||
const [parentWidth, setParentWidth] = useState(0);
|
||||
|
||||
const { frequentlyUsed, loaded } = useFrequentlyUsedEmoji();
|
||||
|
||||
const allCustomEmojis: ICustomEmojis = useAppSelector(
|
||||
|
@ -50,7 +52,14 @@ const EmojiPicker = ({
|
|||
if (!emojis.length) {
|
||||
return null;
|
||||
}
|
||||
return <EmojiCategory emojis={emojis} onEmojiSelected={(emoji: IEmoji) => handleEmojiSelect(emoji)} tabLabel={label} />;
|
||||
return (
|
||||
<EmojiCategory
|
||||
parentWidth={parentWidth}
|
||||
emojis={emojis}
|
||||
onEmojiSelected={(emoji: IEmoji) => handleEmojiSelect(emoji)}
|
||||
tabLabel={label}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
if (!loaded) {
|
||||
|
@ -58,9 +67,13 @@ const EmojiPicker = ({
|
|||
}
|
||||
|
||||
return (
|
||||
<View style={styles.emojiPickerContainer}>
|
||||
<View style={styles.emojiPickerContainer} onLayout={e => setParentWidth(e.nativeEvent.layout.width)}>
|
||||
{searching ? (
|
||||
<EmojiCategory emojis={searchedEmojis} onEmojiSelected={(emoji: IEmoji) => handleEmojiSelect(emoji)} />
|
||||
<EmojiCategory
|
||||
emojis={searchedEmojis}
|
||||
onEmojiSelected={(emoji: IEmoji) => handleEmojiSelect(emoji)}
|
||||
parentWidth={parentWidth}
|
||||
/>
|
||||
) : (
|
||||
<ScrollableTabView
|
||||
renderTabBar={() => <TabBar />}
|
||||
|
|
|
@ -6,7 +6,7 @@ import scrollPersistTaps from '../lib/methods/helpers/scrollPersistTaps';
|
|||
interface IKeyboardViewProps extends KeyboardAwareScrollViewProps {
|
||||
keyboardVerticalOffset?: number;
|
||||
scrollEnabled?: boolean;
|
||||
children: React.ReactElement[] | React.ReactElement;
|
||||
children: React.ReactElement[] | React.ReactElement | null | (React.ReactElement | null)[];
|
||||
}
|
||||
|
||||
const KeyboardView = ({ style, contentContainerStyle, scrollEnabled, keyboardVerticalOffset, children }: IKeyboardViewProps) => (
|
||||
|
|
|
@ -487,7 +487,9 @@ class MessageBox extends Component<IMessageBoxProps, IMessageBoxState> {
|
|||
this.handleTyping(!isTextEmpty);
|
||||
const { start, end } = this.selection;
|
||||
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 result = lastWord.substring(1);
|
||||
|
||||
|
|
|
@ -24,8 +24,16 @@ const formatMsg = ({ lastMessage, type, showLastMessage, username, useRealName }
|
|||
const isLastMessageSentByMe = lastMessage.u.username === username;
|
||||
|
||||
if (!lastMessage.msg && lastMessage.attachments && Object.keys(lastMessage.attachments).length) {
|
||||
const user = isLastMessageSentByMe ? I18n.t('You') : lastMessage.u.username;
|
||||
return I18n.t('User_sent_an_attachment', { user });
|
||||
const userAttachment = () => {
|
||||
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
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
import React from 'react';
|
||||
import { fireEvent, render } from '@testing-library/react-native';
|
||||
import { TextInputProps } from 'react-native';
|
||||
|
||||
import SearchBox from '.';
|
||||
|
||||
|
@ -11,35 +10,35 @@ const testSearchInputs = {
|
|||
testID: 'search-box-text-input'
|
||||
};
|
||||
|
||||
const Render = ({ onChangeText, testID }: TextInputProps) => <SearchBox testID={testID} onChangeText={onChangeText} />;
|
||||
|
||||
describe('SearchBox', () => {
|
||||
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();
|
||||
});
|
||||
|
||||
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');
|
||||
expect(clearInput).toBeNull();
|
||||
});
|
||||
|
||||
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);
|
||||
fireEvent.changeText(component, 'new-input-value');
|
||||
expect(onChangeTextMock).toHaveBeenCalledWith('new-input-value');
|
||||
});
|
||||
|
||||
// we need skip this test for now, until discovery how handle with functions effect
|
||||
// https://github.com/callstack/react-native-testing-library/issues/978
|
||||
it.skip('should clear input when call onCancelSearch function', async () => {
|
||||
const { findByTestId } = render(<Render testID={'input-with-value'} onChangeText={onChangeTextMock} />);
|
||||
it('should clear input when clear icon is pressed', async () => {
|
||||
const { findByTestId } = render(<SearchBox onChangeText={onChangeTextMock} testID={testSearchInputs.testID} />);
|
||||
|
||||
const component = await findByTestId('clear-text-input');
|
||||
fireEvent.press(component, 'input-with-value');
|
||||
expect(onChangeTextMock).toHaveBeenCalledWith('input-with-value');
|
||||
const component = await findByTestId(testSearchInputs.testID);
|
||||
fireEvent.changeText(component, 'new-input-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 { useAppSelector } from '../../../../lib/hooks';
|
||||
import { getRoomAvatar, getUidDirectMessage } from '../../../../lib/methods/helpers';
|
||||
import { videoConfStartAndJoin } from '../../../../lib/methods/videoConf';
|
||||
import { useTheme } from '../../../../theme';
|
||||
import { useActionSheet } from '../../../ActionSheet';
|
||||
import AvatarContainer from '../../../Avatar';
|
||||
|
@ -16,12 +15,12 @@ import { BUTTON_HIT_SLOP } from '../../../message/utils';
|
|||
import StatusContainer from '../../../Status';
|
||||
import useStyle from './styles';
|
||||
|
||||
export default function CallAgainActionSheet({ rid }: { rid: string }): React.ReactElement {
|
||||
export default function StartACallActionSheet({ rid, initCall }: { rid: string; initCall: Function }): React.ReactElement {
|
||||
const style = useStyle();
|
||||
const { colors } = useTheme();
|
||||
const [user, setUser] = useState({ username: '', avatar: '', uid: '', rid: '' });
|
||||
const [phone, setPhone] = useState(true);
|
||||
const [camera, setCamera] = useState(false);
|
||||
const [user, setUser] = useState({ username: '', avatar: '', uid: '' });
|
||||
const [mic, setMic] = useState(true);
|
||||
const [cam, setCam] = useState(false);
|
||||
const username = useAppSelector(state => state.login.user.username);
|
||||
|
||||
const { hideActionSheet } = useActionSheet();
|
||||
|
@ -31,7 +30,7 @@ export default function CallAgainActionSheet({ rid }: { rid: string }): React.Re
|
|||
const room = await getSubscriptionByRoomId(rid);
|
||||
const uid = (await getUidDirectMessage(room)) as string;
|
||||
const avt = getRoomAvatar(room);
|
||||
setUser({ uid, username: room?.name || '', avatar: avt, rid: room?.id || '' });
|
||||
setUser({ uid, username: room?.name || '', avatar: avt });
|
||||
})();
|
||||
}, [rid]);
|
||||
|
||||
|
@ -43,25 +42,27 @@ export default function CallAgainActionSheet({ rid }: { rid: string }): React.Re
|
|||
<Text style={style.actionSheetHeaderTitle}>{i18n.t('Start_a_call')}</Text>
|
||||
<View style={style.actionSheetHeaderButtons}>
|
||||
<Touchable
|
||||
onPress={() => setCamera(!camera)}
|
||||
style={[style.iconCallContainer, camera && style.enabledBackground, { marginRight: 6 }]}
|
||||
onPress={() => setCam(!cam)}
|
||||
style={[style.iconCallContainer, cam && style.enabledBackground, { marginRight: 6 }]}
|
||||
hitSlop={BUTTON_HIT_SLOP}
|
||||
>
|
||||
<CustomIcon name={camera ? 'camera' : 'camera-disabled'} size={16} color={handleColor(camera)} />
|
||||
<CustomIcon name={cam ? 'camera' : 'camera-disabled'} size={20} color={handleColor(cam)} />
|
||||
</Touchable>
|
||||
<Touchable
|
||||
onPress={() => setPhone(!phone)}
|
||||
style={[style.iconCallContainer, phone && style.enabledBackground]}
|
||||
onPress={() => setMic(!mic)}
|
||||
style={[style.iconCallContainer, mic && style.enabledBackground]}
|
||||
hitSlop={BUTTON_HIT_SLOP}
|
||||
>
|
||||
<CustomIcon name={phone ? 'microphone' : 'microphone-disabled'} size={16} color={handleColor(phone)} />
|
||||
<CustomIcon name={mic ? 'microphone' : 'microphone-disabled'} size={20} color={handleColor(mic)} />
|
||||
</Touchable>
|
||||
</View>
|
||||
</View>
|
||||
<View style={style.actionSheetUsernameContainer}>
|
||||
<AvatarContainer text={user.avatar} size={36} />
|
||||
<StatusContainer size={16} id={user.uid} style={{ marginLeft: 8, marginRight: 6 }} />
|
||||
<Text style={style.actionSheetUsername}>{user.username}</Text>
|
||||
<Text style={style.actionSheetUsername} numberOfLines={1}>
|
||||
{user.username}
|
||||
</Text>
|
||||
</View>
|
||||
<View style={style.actionSheetPhotoContainer}>
|
||||
<AvatarContainer size={62} text={username} />
|
||||
|
@ -70,7 +71,7 @@ export default function CallAgainActionSheet({ rid }: { rid: string }): React.Re
|
|||
onPress={() => {
|
||||
hideActionSheet();
|
||||
setTimeout(() => {
|
||||
videoConfStartAndJoin(user.rid, camera);
|
||||
initCall({ cam, mic });
|
||||
}, 100);
|
||||
}}
|
||||
title={i18n.t('Call')}
|
|
@ -3,17 +3,16 @@ import { Text } from 'react-native';
|
|||
import Touchable from 'react-native-platform-touchable';
|
||||
|
||||
import i18n from '../../../../i18n';
|
||||
import { useVideoConf } from '../../../../lib/hooks/useVideoConf';
|
||||
import { videoConfJoin } from '../../../../lib/methods/videoConf';
|
||||
import useStyle from './styles';
|
||||
import { VideoConferenceBaseContainer } from './VideoConferenceBaseContainer';
|
||||
|
||||
const VideoConferenceDirect = React.memo(({ blockId }: { blockId: string }) => {
|
||||
const style = useStyle();
|
||||
const { joinCall } = useVideoConf();
|
||||
|
||||
return (
|
||||
<VideoConferenceBaseContainer variant='incoming'>
|
||||
<Touchable style={style.callToActionButton} onPress={() => joinCall(blockId)}>
|
||||
<Touchable style={style.callToActionButton} onPress={() => videoConfJoin(blockId)}>
|
||||
<Text style={style.callToActionButtonText}>{i18n.t('Join')}</Text>
|
||||
</Touchable>
|
||||
<Text style={style.callBack}>{i18n.t('Waiting_for_answer')}</Text>
|
||||
|
|
|
@ -6,9 +6,7 @@ import { IUser } from '../../../../definitions';
|
|||
import { VideoConferenceType } from '../../../../definitions/IVideoConference';
|
||||
import i18n from '../../../../i18n';
|
||||
import { useAppSelector } from '../../../../lib/hooks';
|
||||
import { useSnaps } from '../../../../lib/hooks/useSnaps';
|
||||
import { useActionSheet } from '../../../ActionSheet';
|
||||
import CallAgainActionSheet from './CallAgainActionSheet';
|
||||
import { useVideoConf } from '../../../../lib/hooks/useVideoConf';
|
||||
import { CallParticipants, TCallUsers } from './CallParticipants';
|
||||
import useStyle from './styles';
|
||||
import { VideoConferenceBaseContainer } from './VideoConferenceBaseContainer';
|
||||
|
@ -26,8 +24,7 @@ export default function VideoConferenceEnded({
|
|||
}): React.ReactElement {
|
||||
const style = useStyle();
|
||||
const username = useAppSelector(state => state.login.user.username);
|
||||
const { showActionSheet } = useActionSheet();
|
||||
const snaps = useSnaps([1250]);
|
||||
const { showInitCallActionSheet } = useVideoConf(rid);
|
||||
|
||||
const onlyAuthorOnCall = users.length === 1 && users.some(user => user.username === createdBy.username);
|
||||
|
||||
|
@ -35,17 +32,9 @@ export default function VideoConferenceEnded({
|
|||
<VideoConferenceBaseContainer variant='ended'>
|
||||
{type === 'direct' ? (
|
||||
<>
|
||||
<Touchable
|
||||
style={style.callToActionCallBack}
|
||||
onPress={() =>
|
||||
showActionSheet({
|
||||
children: <CallAgainActionSheet rid={rid} />,
|
||||
snaps
|
||||
})
|
||||
}
|
||||
>
|
||||
<Touchable style={style.callToActionCallBack} onPress={showInitCallActionSheet}>
|
||||
<Text style={style.callToActionCallBackText}>
|
||||
{createdBy.username === username ? i18n.t('Call_back') : i18n.t('Call_again')}
|
||||
{createdBy.username === username ? i18n.t('Call_again') : i18n.t('Call_back')}
|
||||
</Text>
|
||||
</Touchable>
|
||||
<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 i18n from '../../../../i18n';
|
||||
import { useVideoConf } from '../../../../lib/hooks/useVideoConf';
|
||||
import { videoConfJoin } from '../../../../lib/methods/videoConf';
|
||||
import { CallParticipants, TCallUsers } from './CallParticipants';
|
||||
import useStyle from './styles';
|
||||
import { VideoConferenceBaseContainer } from './VideoConferenceBaseContainer';
|
||||
|
||||
export default function VideoConferenceOutgoing({ users, blockId }: { users: TCallUsers; blockId: string }): React.ReactElement {
|
||||
const style = useStyle();
|
||||
const { joinCall } = useVideoConf();
|
||||
|
||||
return (
|
||||
<VideoConferenceBaseContainer variant='outgoing'>
|
||||
<Touchable style={style.callToActionButton} onPress={() => joinCall(blockId)}>
|
||||
<Touchable style={style.callToActionButton} onPress={() => videoConfJoin(blockId)}>
|
||||
<Text style={style.callToActionButtonText}>{i18n.t('Join')}</Text>
|
||||
</Touchable>
|
||||
<CallParticipants users={users} />
|
||||
|
|
|
@ -100,7 +100,8 @@ export default function useStyle() {
|
|||
actionSheetUsername: {
|
||||
fontSize: 16,
|
||||
...sharedStyles.textBold,
|
||||
color: colors.passcodePrimary
|
||||
color: colors.passcodePrimary,
|
||||
flexShrink: 1
|
||||
},
|
||||
enabledBackground: {
|
||||
backgroundColor: colors.conferenceCallEnabledIconBackground
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
import { BlockContext } from '@rocket.chat/ui-kit';
|
||||
import React, { useContext, useState } from 'react';
|
||||
|
||||
import { useVideoConf } from '../../lib/hooks/useVideoConf';
|
||||
import { videoConfJoin } from '../../lib/methods/videoConf';
|
||||
import { IText } from './interfaces';
|
||||
|
||||
export const textParser = ([{ text }]: IText[]) => text;
|
||||
|
@ -40,7 +40,6 @@ export const useBlockContext = ({ blockId, actionId, appId, initialValue }: IUse
|
|||
const { action, appId: appIdFromContext, viewId, state, language, errors, values = {} } = useContext(KitContext);
|
||||
const { value = initialValue } = values[actionId] || {};
|
||||
const [loading, setLoading] = useState(false);
|
||||
const { joinCall } = useVideoConf();
|
||||
|
||||
const error = errors && actionId && errors[actionId];
|
||||
|
||||
|
@ -58,7 +57,7 @@ export const useBlockContext = ({ blockId, actionId, appId, initialValue }: IUse
|
|||
try {
|
||||
if (appId === 'videoconf-core' && blockId) {
|
||||
setLoading(false);
|
||||
return joinCall(blockId);
|
||||
return videoConfJoin(blockId);
|
||||
}
|
||||
await action({
|
||||
blockId,
|
||||
|
|
|
@ -16,11 +16,13 @@ const styles = StyleSheet.create({
|
|||
},
|
||||
bottomContainerText: {
|
||||
...sharedStyles.textRegular,
|
||||
fontSize: 13
|
||||
fontSize: 13,
|
||||
textAlign: 'center'
|
||||
},
|
||||
bottomContainerTextBold: {
|
||||
...sharedStyles.textSemibold,
|
||||
fontSize: 13
|
||||
fontSize: 13,
|
||||
textAlign: 'center'
|
||||
}
|
||||
});
|
||||
|
||||
|
|
|
@ -126,6 +126,12 @@ export const Links = () => (
|
|||
<View style={styles.container}>
|
||||
<Markdown msg='[Markdown link](https://rocket.chat): `[description](url)`' 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>
|
||||
);
|
||||
|
||||
|
|
|
@ -140,10 +140,10 @@ class Markdown extends PureComponent<IMarkdownProps, any> {
|
|||
}
|
||||
|
||||
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])];
|
||||
return (
|
||||
<Text accessibilityLabel={literal} style={[styles.text, defaultStyle, ...style]} numberOfLines={numberOfLines}>
|
||||
<Text accessibilityLabel={literal} style={[styles.text, defaultStyle]} numberOfLines={numberOfLines}>
|
||||
{literal}
|
||||
</Text>
|
||||
);
|
||||
|
@ -193,12 +193,12 @@ class Markdown extends PureComponent<IMarkdownProps, any> {
|
|||
};
|
||||
|
||||
renderParagraph = ({ children }: any) => {
|
||||
const { numberOfLines, style, theme } = this.props;
|
||||
const { numberOfLines, style = [], theme } = this.props;
|
||||
if (!children || children.length === 0) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<Text style={[styles.text, style, { color: themes[theme!].bodyText }]} numberOfLines={numberOfLines}>
|
||||
<Text style={[styles.text, { color: themes[theme!].bodyText }, ...style]} numberOfLines={numberOfLines}>
|
||||
{children}
|
||||
</Text>
|
||||
);
|
||||
|
|
|
@ -17,7 +17,7 @@ const OrderedList = ({ value }: IOrderedListProps): React.ReactElement => {
|
|||
{value.map(item => (
|
||||
<View style={styles.row} key={item.number?.toString()}>
|
||||
<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} />
|
||||
</Text>
|
||||
</View>
|
||||
|
|
|
@ -18,7 +18,7 @@ const UnorderedList = ({ value }: IUnorderedListProps) => {
|
|||
{value.map(item => (
|
||||
<View style={styles.row}>
|
||||
<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} />
|
||||
</Text>
|
||||
</View>
|
||||
|
|
|
@ -10,12 +10,12 @@ import { themes } from '../../lib/constants';
|
|||
import { IMessageCallButton } from './interfaces';
|
||||
import { useTheme } from '../../theme';
|
||||
|
||||
const CallButton = React.memo(({ callJitsi }: IMessageCallButton) => {
|
||||
const CallButton = React.memo(({ handleEnterCall }: IMessageCallButton) => {
|
||||
const { theme } = useTheme();
|
||||
return (
|
||||
<View style={styles.buttonContainer}>
|
||||
<Touchable
|
||||
onPress={callJitsi}
|
||||
onPress={handleEnterCall}
|
||||
background={Touchable.Ripple(themes[theme].bannerBackground)}
|
||||
style={[styles.button, { backgroundColor: themes[theme].tintColor }]}
|
||||
hitSlop={BUTTON_HIT_SLOP}
|
||||
|
|
|
@ -53,7 +53,7 @@ const Content = React.memo(
|
|||
content = (
|
||||
<Markdown
|
||||
msg={props.msg}
|
||||
md={props.md}
|
||||
md={props.type !== 'e2e' ? props.md : undefined}
|
||||
getCustomEmoji={props.getCustomEmoji}
|
||||
enableMessageParser={user.enableMessageParserEarlyAdoption}
|
||||
username={user.username}
|
||||
|
|
|
@ -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 = () => (
|
||||
<>
|
||||
<Message
|
||||
|
|
|
@ -18,7 +18,7 @@ const MessageAvatar = React.memo(({ isHeader, avatar, author, small, navToRoomIn
|
|||
style={small ? styles.avatarSmall : styles.avatar}
|
||||
text={avatar ? '' : author.username}
|
||||
size={small ? 20 : 36}
|
||||
borderRadius={small ? 2 : 4}
|
||||
borderRadius={4}
|
||||
onPress={author._id === user.id ? undefined : () => navToRoomInfo(navParam)}
|
||||
getCustomEmoji={getCustomEmoji}
|
||||
avatar={avatar}
|
||||
|
|
|
@ -232,7 +232,9 @@ const Reply = React.memo(
|
|||
|
||||
return (
|
||||
<>
|
||||
{/* The testID is to test properly quoted messages using it as ancestor */}
|
||||
<Touchable
|
||||
testID={`reply-${attachment?.author_name}-${attachment?.text}`}
|
||||
onPress={onPress}
|
||||
style={[
|
||||
styles.button,
|
||||
|
@ -247,6 +249,8 @@ const Reply = React.memo(
|
|||
>
|
||||
<View style={styles.attachmentContainer}>
|
||||
<Title attachment={attachment} timeFormat={timeFormat} theme={theme} />
|
||||
<Description attachment={attachment} getCustomEmoji={getCustomEmoji} theme={theme} />
|
||||
<UrlImage image={attachment.thumb_url} />
|
||||
<Attachments
|
||||
attachments={attachment.attachments}
|
||||
getCustomEmoji={getCustomEmoji}
|
||||
|
@ -255,8 +259,6 @@ const Reply = React.memo(
|
|||
isReply
|
||||
id={messageId}
|
||||
/>
|
||||
<UrlImage image={attachment.thumb_url} />
|
||||
<Description attachment={attachment} getCustomEmoji={getCustomEmoji} theme={theme} />
|
||||
<Fields attachment={attachment} getCustomEmoji={getCustomEmoji} theme={theme} />
|
||||
{loading ? (
|
||||
<View style={[styles.backdrop]}>
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import React, { useContext } from 'react';
|
||||
import React, { useContext, useState } from 'react';
|
||||
import { StyleSheet, Text, View } from 'react-native';
|
||||
import Clipboard from '@react-native-clipboard/clipboard';
|
||||
import FastImage from 'react-native-fast-image';
|
||||
|
@ -48,38 +48,34 @@ const styles = StyleSheet.create({
|
|||
height: 150,
|
||||
borderTopLeftRadius: 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(
|
||||
({ title, description, theme }: { title: string; description: string; theme: TSupportedThemes }) => (
|
||||
<View style={styles.textContainer}>
|
||||
{title ? (
|
||||
<Text style={[styles.title, { color: themes[theme].tintColor }]} numberOfLines={2}>
|
||||
{title}
|
||||
</Text>
|
||||
) : null}
|
||||
{description ? (
|
||||
<Text style={[styles.description, { color: themes[theme].auxiliaryText }]} numberOfLines={2}>
|
||||
{description}
|
||||
</Text>
|
||||
) : null}
|
||||
</View>
|
||||
),
|
||||
({ title, description }: { title: string; description: string }) => {
|
||||
const { colors } = useTheme();
|
||||
return (
|
||||
<View style={styles.textContainer}>
|
||||
{title ? (
|
||||
<Text style={[styles.title, { color: colors.tintColor }]} numberOfLines={2}>
|
||||
{title}
|
||||
</Text>
|
||||
) : null}
|
||||
{description ? (
|
||||
<Text style={[styles.description, { color: colors.auxiliaryText }]} numberOfLines={2}>
|
||||
{description}
|
||||
</Text>
|
||||
) : null}
|
||||
</View>
|
||||
);
|
||||
},
|
||||
(prevProps, nextProps) => {
|
||||
if (prevProps.title !== nextProps.title) {
|
||||
return false;
|
||||
|
@ -87,16 +83,18 @@ const UrlContent = React.memo(
|
|||
if (prevProps.description !== nextProps.description) {
|
||||
return false;
|
||||
}
|
||||
if (prevProps.theme !== nextProps.theme) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
);
|
||||
|
||||
type TImageLoadedState = 'loading' | 'done' | 'error';
|
||||
|
||||
const Url = React.memo(
|
||||
({ 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;
|
||||
}
|
||||
|
||||
|
@ -107,6 +105,13 @@ const Url = React.memo(
|
|||
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 (
|
||||
<Touchable
|
||||
onPress={onPress}
|
||||
|
@ -118,13 +123,22 @@ const Url = React.memo(
|
|||
{
|
||||
backgroundColor: themes[theme].chatComponentBackground,
|
||||
borderColor: themes[theme].borderColor
|
||||
}
|
||||
},
|
||||
imageLoadedState === 'loading' && styles.loading
|
||||
]}
|
||||
background={Touchable.Ripple(themes[theme].bannerBackground)}
|
||||
>
|
||||
<>
|
||||
<UrlImage image={url.image} />
|
||||
<UrlContent title={url.title} description={url.description} theme={theme} />
|
||||
{image ? (
|
||||
<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>
|
||||
);
|
||||
|
@ -146,7 +160,6 @@ const Urls = React.memo(
|
|||
(oldProps, newProps) => dequal(oldProps.urls, newProps.urls)
|
||||
);
|
||||
|
||||
UrlImage.displayName = 'MessageUrlImage';
|
||||
UrlContent.displayName = 'MessageUrlContent';
|
||||
Url.displayName = 'MessageUrl';
|
||||
Urls.displayName = 'MessageUrls';
|
||||
|
|
|
@ -50,7 +50,7 @@ interface IMessageContainerProps {
|
|||
showAttachment: (file: IAttachment) => void;
|
||||
onReactionLongPress?: (item: TAnyMessageModel) => void;
|
||||
navToRoomInfo: (navParam: IRoomInfoParam) => void;
|
||||
callJitsi?: () => void;
|
||||
handleEnterCall?: () => void;
|
||||
blockAction?: (params: { actionId: string; appId: string; value: string; blockId: string; rid: string; mid: string }) => void;
|
||||
onAnswerButtonPress?: (message: string, tmid?: string, tshow?: boolean) => void;
|
||||
threadBadgeColor?: string;
|
||||
|
@ -69,7 +69,6 @@ class MessageContainer extends React.Component<IMessageContainerProps, IMessageC
|
|||
static defaultProps = {
|
||||
getCustomEmoji: () => null,
|
||||
onLongPress: () => {},
|
||||
callJitsi: () => {},
|
||||
blockAction: () => {},
|
||||
archived: false,
|
||||
broadcast: false,
|
||||
|
@ -338,7 +337,7 @@ class MessageContainer extends React.Component<IMessageContainerProps, IMessageC
|
|||
navToRoomInfo,
|
||||
getCustomEmoji,
|
||||
isThreadRoom,
|
||||
callJitsi,
|
||||
handleEnterCall,
|
||||
blockAction,
|
||||
rid,
|
||||
threadBadgeColor,
|
||||
|
@ -456,7 +455,7 @@ class MessageContainer extends React.Component<IMessageContainerProps, IMessageC
|
|||
showAttachment={showAttachment}
|
||||
getCustomEmoji={getCustomEmoji}
|
||||
navToRoomInfo={navToRoomInfo}
|
||||
callJitsi={callJitsi}
|
||||
handleEnterCall={handleEnterCall}
|
||||
blockAction={blockAction}
|
||||
highlighted={highlighted}
|
||||
comment={comment}
|
||||
|
|
|
@ -40,7 +40,7 @@ export interface IMessageBroadcast {
|
|||
}
|
||||
|
||||
export interface IMessageCallButton {
|
||||
callJitsi?: () => void;
|
||||
handleEnterCall?: () => void;
|
||||
}
|
||||
|
||||
export interface IMessageContent {
|
||||
|
|
|
@ -147,7 +147,10 @@ export interface IMessage extends IMessageFromServer {
|
|||
editedAt?: string | Date;
|
||||
}
|
||||
|
||||
export type TMessageModel = IMessage & Model;
|
||||
export type TMessageModel = IMessage &
|
||||
Model & {
|
||||
asPlain: () => IMessage;
|
||||
};
|
||||
|
||||
export type TAnyMessageModel = TMessageModel | TThreadModel | TThreadMessageModel;
|
||||
export type TTypeMessages = IMessageFromServer | ILoadMoreMessage | IMessage;
|
||||
|
|
|
@ -17,16 +17,14 @@ export interface IAvatarButton {
|
|||
}
|
||||
|
||||
export interface IAvatar {
|
||||
data: {} | string | null;
|
||||
data: string | null;
|
||||
url?: string;
|
||||
contentType?: string;
|
||||
service?: any;
|
||||
}
|
||||
|
||||
export interface IAvatarSuggestion {
|
||||
[service: string]: {
|
||||
url: string;
|
||||
blob: string;
|
||||
contentType: string;
|
||||
};
|
||||
url: string;
|
||||
blob: string;
|
||||
contentType: string;
|
||||
}
|
||||
|
|
|
@ -38,4 +38,7 @@ export interface IThread extends IMessage {
|
|||
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;
|
||||
}
|
||||
|
||||
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 { IMessage } from './IMessage';
|
||||
|
||||
export enum VideoConferenceStatus {
|
||||
export declare enum VideoConferenceStatus {
|
||||
CALLING = 0,
|
||||
STARTED = 1,
|
||||
EXPIRED = 2,
|
||||
ENDED = 3,
|
||||
DECLINED = 4
|
||||
}
|
||||
|
||||
export type DirectCallInstructions = {
|
||||
export declare type DirectCallInstructions = {
|
||||
type: 'direct';
|
||||
callee: IUser['_id'];
|
||||
calleeId: IUser['_id'];
|
||||
callId: string;
|
||||
};
|
||||
|
||||
export type ConferenceInstructions = {
|
||||
export declare type ConferenceInstructions = {
|
||||
type: 'videoconference';
|
||||
callId: string;
|
||||
rid: IRoom['_id'];
|
||||
};
|
||||
|
||||
export type LivechatInstructions = {
|
||||
export declare type LivechatInstructions = {
|
||||
type: 'livechat';
|
||||
callId: string;
|
||||
};
|
||||
|
||||
export type VideoConferenceType = DirectCallInstructions['type'] | ConferenceInstructions['type'] | LivechatInstructions['type'];
|
||||
|
||||
export declare type VideoConferenceType =
|
||||
| DirectCallInstructions['type']
|
||||
| ConferenceInstructions['type']
|
||||
| LivechatInstructions['type'];
|
||||
export interface IVideoConferenceUser extends Pick<Required<IUser>, '_id' | 'username' | 'name' | 'avatarETag'> {
|
||||
ts: Date;
|
||||
}
|
||||
|
||||
export interface IVideoConference extends IRocketChatRecord {
|
||||
type: VideoConferenceType;
|
||||
rid: string;
|
||||
|
@ -45,51 +42,68 @@ export interface IVideoConference extends IRocketChatRecord {
|
|||
ended?: IMessage['_id'];
|
||||
};
|
||||
url?: string;
|
||||
|
||||
createdBy: Pick<IUser, '_id' | 'username' | 'name'>;
|
||||
createdBy: Pick<Required<IUser>, '_id' | 'username' | 'name'>;
|
||||
createdAt: Date;
|
||||
|
||||
endedBy?: Pick<IUser, '_id' | 'username' | 'name'>;
|
||||
endedBy?: Pick<Required<IUser>, '_id' | 'username' | 'name'>;
|
||||
endedAt?: Date;
|
||||
|
||||
providerName: string;
|
||||
providerData?: Record<string, any>;
|
||||
|
||||
ringing?: boolean;
|
||||
}
|
||||
|
||||
export interface IDirectVideoConference extends IVideoConference {
|
||||
type: 'direct';
|
||||
}
|
||||
|
||||
export interface IGroupVideoConference extends IVideoConference {
|
||||
type: 'videoconference';
|
||||
anonymousUsers: number;
|
||||
title: string;
|
||||
}
|
||||
|
||||
export interface ILivechatVideoConference extends IVideoConference {
|
||||
type: 'livechat';
|
||||
}
|
||||
|
||||
export type VideoConference = IDirectVideoConference | IGroupVideoConference | ILivechatVideoConference;
|
||||
|
||||
export type VideoConferenceInstructions = DirectCallInstructions | ConferenceInstructions | LivechatInstructions;
|
||||
|
||||
export const isDirectVideoConference = (call: VideoConference | undefined | null): call is IDirectVideoConference =>
|
||||
call?.type === 'direct';
|
||||
|
||||
export const isGroupVideoConference = (call: VideoConference | undefined | null): call is IGroupVideoConference =>
|
||||
call?.type === 'videoconference';
|
||||
|
||||
export const isLivechatVideoConference = (call: VideoConference | undefined | null): call is ILivechatVideoConference =>
|
||||
call?.type === 'livechat';
|
||||
|
||||
type GroupVideoConferenceCreateData = Omit<IGroupVideoConference, 'createdBy'> & { createdBy: IUser['_id'] };
|
||||
type DirectVideoConferenceCreateData = Omit<IDirectVideoConference, 'createdBy'> & { createdBy: IUser['_id'] };
|
||||
type LivechatVideoConferenceCreateData = Omit<ILivechatVideoConference, 'createdBy'> & { createdBy: IUser['_id'] };
|
||||
|
||||
export type VideoConferenceCreateData = AtLeast<
|
||||
export declare type VideoConference = IDirectVideoConference | IGroupVideoConference | ILivechatVideoConference;
|
||||
export declare type VideoConferenceInstructions = DirectCallInstructions | ConferenceInstructions | LivechatInstructions;
|
||||
export declare const isDirectVideoConference: (call: VideoConference | undefined | null) => call is IDirectVideoConference;
|
||||
export declare const isGroupVideoConference: (call: VideoConference | undefined | null) => call is IGroupVideoConference;
|
||||
export declare const isLivechatVideoConference: (call: VideoConference | undefined | null) => call is ILivechatVideoConference;
|
||||
declare type GroupVideoConferenceCreateData = Omit<IGroupVideoConference, 'createdBy'> & {
|
||||
createdBy: IUser['_id'];
|
||||
};
|
||||
declare type DirectVideoConferenceCreateData = Omit<IDirectVideoConference, 'createdBy'> & {
|
||||
createdBy: IUser['_id'];
|
||||
};
|
||||
declare type LivechatVideoConferenceCreateData = Omit<ILivechatVideoConference, 'createdBy'> & {
|
||||
createdBy: IUser['_id'];
|
||||
};
|
||||
export declare type VideoConferenceCreateData = AtLeast<
|
||||
DirectVideoConferenceCreateData | GroupVideoConferenceCreateData | LivechatVideoConferenceCreateData,
|
||||
'createdBy' | 'type' | 'rid' | 'providerName' | 'providerData'
|
||||
>;
|
||||
|
||||
export type VideoConferenceCapabilities = {
|
||||
mic?: boolean;
|
||||
cam?: boolean;
|
||||
title?: boolean;
|
||||
};
|
||||
|
||||
export type VideoConfStartProps = { roomId: string; title?: string; allowRinging?: boolean };
|
||||
|
||||
export type VideoConfJoinProps = {
|
||||
callId: string;
|
||||
state?: {
|
||||
mic?: boolean;
|
||||
cam?: boolean;
|
||||
};
|
||||
};
|
||||
|
||||
export type VideoConfCancelProps = {
|
||||
callId: string;
|
||||
};
|
||||
|
||||
export type VideoConfListProps = {
|
||||
roomId: string;
|
||||
count?: number;
|
||||
offset?: number;
|
||||
};
|
||||
|
||||
export type VideoConfInfoProps = { callId: string };
|
||||
|
|
|
@ -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 }) => {
|
||||
messages: IMessageFromServer[];
|
||||
total: number;
|
||||
count: number;
|
||||
};
|
||||
};
|
||||
'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 = {
|
||||
'video-conference/jitsi.update-timeout': {
|
||||
POST: (params: { roomId: string }) => void;
|
||||
};
|
||||
'video-conference.join': {
|
||||
POST: (params: { callId: string; state: { cam: boolean } }) => { url: string; providerName: string };
|
||||
};
|
||||
'video-conference.start': {
|
||||
POST: (params: { roomId: string }) => { url: string };
|
||||
POST: (params: VideoConfStartProps) => { data: VideoConferenceInstructions & { providerName: string } };
|
||||
};
|
||||
|
||||
'video-conference.join': {
|
||||
POST: (params: VideoConfJoinProps) => { url: string; providerName: string };
|
||||
};
|
||||
|
||||
'video-conference.cancel': {
|
||||
POST: (params: { callId: string }) => void;
|
||||
POST: (params: VideoConfCancelProps) => void;
|
||||
};
|
||||
|
||||
'video-conference.info': {
|
||||
GET: (params: { callId: string }) => VideoConference & {
|
||||
capabilities: {
|
||||
mic?: boolean;
|
||||
cam?: boolean;
|
||||
title?: boolean;
|
||||
};
|
||||
};
|
||||
GET: (params: VideoConfInfoProps) => VideoConference & { capabilities: VideoConferenceCapabilities };
|
||||
};
|
||||
|
||||
'video-conference.list': {
|
||||
GET: (params: VideoConfListProps) => PaginatedResult<{ data: VideoConference[] }>;
|
||||
};
|
||||
|
||||
'video-conference.capabilities': {
|
||||
GET: () => { providerName: string; capabilities: VideoConferenceCapabilities };
|
||||
};
|
||||
|
||||
'video-conference.providers': {
|
||||
GET: () => { data: { key: string; label: string }[] };
|
||||
};
|
||||
|
||||
'video-conference/jitsi.update-timeout': {
|
||||
POST: (params: { roomId: string }) => void;
|
||||
};
|
||||
};
|
||||
|
|
|
@ -12,3 +12,6 @@ declare module 'react-native-restart';
|
|||
declare module 'react-native-jitsi-meet';
|
||||
declare module 'rn-root-view';
|
||||
declare module 'react-native-math-view';
|
||||
declare module '@env' {
|
||||
export const RUNNING_E2E_TESTS: string;
|
||||
}
|
||||
|
|
|
@ -768,7 +768,7 @@
|
|||
"Convert_to_Team_Warning": "You are converting this Channel to a Team. All Members will be kept.",
|
||||
"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_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_Newer": "Load Newer",
|
||||
"Load_Older": "Load Older",
|
||||
|
@ -877,6 +877,26 @@
|
|||
"Reply_in_direct_message": "Reply in Direct Message",
|
||||
"room_archived": "archived 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_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"
|
||||
|
|
|
@ -876,6 +876,14 @@
|
|||
"Reply_in_direct_message": "Responder por mensagem direta",
|
||||
"room_archived": "{{username}} arquivou 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_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',
|
||||
passcodeBackground: '#EEEFF1',
|
||||
passcodeButtonActive: '#E4E7EA',
|
||||
editAndUploadButtonAvatar: '#E4E7EA',
|
||||
passcodeLockIcon: '#6C727A',
|
||||
passcodePrimary: '#2F343D',
|
||||
passcodeSecondary: '#6C727A',
|
||||
|
@ -128,6 +129,7 @@ export const colors = {
|
|||
buttonText: '#ffffff',
|
||||
passcodeBackground: '#030C1B',
|
||||
passcodeButtonActive: '#0B182C',
|
||||
editAndUploadButtonAvatar: '#0B182C',
|
||||
passcodeLockIcon: '#6C727A',
|
||||
passcodePrimary: '#FFFFFF',
|
||||
passcodeSecondary: '#CBCED1',
|
||||
|
@ -197,6 +199,7 @@ export const colors = {
|
|||
buttonText: '#ffffff',
|
||||
passcodeBackground: '#000000',
|
||||
passcodeButtonActive: '#0E0D0D',
|
||||
editAndUploadButtonAvatar: '#0E0D0D',
|
||||
passcodeLockIcon: '#6C727A',
|
||||
passcodePrimary: '#FFFFFF',
|
||||
passcodeSecondary: '#CBCED1',
|
||||
|
|
|
@ -85,4 +85,47 @@ export default class Message extends Model {
|
|||
@json('md', sanitizer) md;
|
||||
|
||||
@field('comment') comment;
|
||||
|
||||
asPlain() {
|
||||
return {
|
||||
id: this.id,
|
||||
rid: this.subscription.id,
|
||||
msg: this.msg,
|
||||
t: this.t,
|
||||
ts: this.ts,
|
||||
u: this.u,
|
||||
alias: this.alias,
|
||||
parseUrls: this.parseUrls,
|
||||
groupable: this.groupable,
|
||||
avatar: this.avatar,
|
||||
emoji: this.emoji,
|
||||
attachments: this.attachments,
|
||||
urls: this.urls,
|
||||
_updatedAt: this._updatedAt,
|
||||
status: this.status,
|
||||
pinned: this.pinned,
|
||||
starred: this.starred,
|
||||
editedBy: this.editedBy,
|
||||
reactions: this.reactions,
|
||||
role: this.role,
|
||||
drid: this.drid,
|
||||
dcount: this.dcount,
|
||||
dlm: this.dlm,
|
||||
tmid: this.tmid,
|
||||
tcount: this.tcount,
|
||||
tlm: this.tlm,
|
||||
replies: this.replies,
|
||||
mentions: this.mentions,
|
||||
channels: this.channels,
|
||||
unread: this.unread,
|
||||
autoTranslate: this.autoTranslate,
|
||||
translations: this.translations,
|
||||
tmsg: this.tmsg,
|
||||
blocks: this.blocks,
|
||||
e2e: this.e2e,
|
||||
tshow: this.tshow,
|
||||
md: this.md,
|
||||
comment: this.comment
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
@ -77,4 +77,42 @@ export default class Thread extends Model {
|
|||
@field('e2e') e2e;
|
||||
|
||||
@field('draft_message') draftMessage;
|
||||
|
||||
asPlain() {
|
||||
return {
|
||||
id: this.id,
|
||||
msg: this.msg,
|
||||
t: this.t,
|
||||
ts: this.ts,
|
||||
u: this.u,
|
||||
alias: this.alias,
|
||||
parseUrls: this.parseUrls,
|
||||
groupable: this.groupable,
|
||||
avatar: this.avatar,
|
||||
emoji: this.emoji,
|
||||
attachments: this.attachments,
|
||||
urls: this.urls,
|
||||
_updatedAt: this._updatedAt,
|
||||
status: this.status,
|
||||
pinned: this.pinned,
|
||||
starred: this.starred,
|
||||
editedBy: this.editedBy,
|
||||
reactions: this.reactions,
|
||||
role: this.role,
|
||||
drid: this.drid,
|
||||
dcount: this.dcount,
|
||||
dlm: this.dlm,
|
||||
tmid: this.tmid,
|
||||
tcount: this.tcount,
|
||||
tlm: this.tlm,
|
||||
replies: this.replies,
|
||||
mentions: this.mentions,
|
||||
channels: this.channels,
|
||||
unread: this.unread,
|
||||
autoTranslate: this.autoTranslate,
|
||||
translations: this.translations,
|
||||
e2e: this.e2e,
|
||||
draftMessage: this.draftMessage
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
@ -77,4 +77,42 @@ export default class ThreadMessage extends Model {
|
|||
@field('draft_message') draftMessage;
|
||||
|
||||
@field('e2e') e2e;
|
||||
|
||||
asPlain() {
|
||||
return {
|
||||
id: this.id,
|
||||
msg: this.msg,
|
||||
t: this.t,
|
||||
ts: this.ts,
|
||||
u: this.u,
|
||||
rid: this.rid,
|
||||
alias: this.alias,
|
||||
parseUrls: this.parseUrls,
|
||||
groupable: this.groupable,
|
||||
avatar: this.avatar,
|
||||
emoji: this.emoji,
|
||||
attachments: this.attachments,
|
||||
urls: this.urls,
|
||||
_updatedAt: this._updatedAt,
|
||||
status: this.status,
|
||||
pinned: this.pinned,
|
||||
starred: this.starred,
|
||||
editedBy: this.editedBy,
|
||||
reactions: this.reactions,
|
||||
role: this.role,
|
||||
drid: this.drid,
|
||||
dcount: this.dcount,
|
||||
dlm: this.dlm,
|
||||
tcount: this.tcount,
|
||||
tlm: this.tlm,
|
||||
replies: this.replies,
|
||||
mentions: this.mentions,
|
||||
channels: this.channels,
|
||||
unread: this.unread,
|
||||
autoTranslate: this.autoTranslate,
|
||||
translations: this.translations,
|
||||
draftMessage: this.draftMessage,
|
||||
e2e: this.e2e
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
@ -13,7 +13,7 @@ export const getMessageById = async (messageId: string | null) => {
|
|||
try {
|
||||
const result = await messageCollection.find(messageId);
|
||||
return result;
|
||||
} catch (error) {
|
||||
} catch {
|
||||
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 SimpleCrypto from 'react-native-simple-crypto';
|
||||
import ByteBuffer from 'bytebuffer';
|
||||
import parse from 'url-parse';
|
||||
|
||||
import getSingleMessage from '../methods/getSingleMessage';
|
||||
import { IMessage, IUser } from '../../definitions';
|
||||
import Deferred from './helpers/deferred';
|
||||
import { debounce } from '../methods/helpers';
|
||||
|
@ -21,6 +23,11 @@ import {
|
|||
import { Encryption } from './index';
|
||||
import { E2E_MESSAGE_TYPE, E2E_STATUS } from '../constants';
|
||||
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 {
|
||||
ready: boolean;
|
||||
|
@ -268,12 +275,15 @@ export default class EncryptionRoom {
|
|||
tmsg = await this.decryptText(tmsg);
|
||||
}
|
||||
|
||||
return {
|
||||
const decryptedMessage: IMessage = {
|
||||
...message,
|
||||
tmsg,
|
||||
msg,
|
||||
e2e: E2E_STATUS.DONE
|
||||
e2e: 'done'
|
||||
};
|
||||
|
||||
const decryptedMessageWithQuote = await this.decryptQuoteAttachment(decryptedMessage);
|
||||
return decryptedMessageWithQuote;
|
||||
}
|
||||
} catch {
|
||||
// Do nothing
|
||||
|
@ -281,4 +291,37 @@ export default class EncryptionRoom {
|
|||
|
||||
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 });
|
||||
}
|
||||
|
||||
export async function callJitsi(room: ISubscription, onlyAudio = false): Promise<void> {
|
||||
logEvent(onlyAudio ? events.RA_JITSI_AUDIO : events.RA_JITSI_VIDEO);
|
||||
export async function callJitsi({ room, cam = false }: { room: ISubscription; cam?: boolean }): Promise<void> {
|
||||
logEvent(cam ? events.RA_JITSI_AUDIO : events.RA_JITSI_VIDEO);
|
||||
const url = await jitsiURL({ room });
|
||||
Navigation.navigate('JitsiMeetView', { url, onlyAudio, rid: room?.rid });
|
||||
Navigation.navigate('JitsiMeetView', { url, onlyAudio: cam, rid: room?.rid });
|
||||
}
|
||||
|
|
|
@ -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 { 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 = ({
|
||||
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 './isValidEmail';
|
||||
export * from './random';
|
||||
export * from './image';
|
||||
|
|
|
@ -5,6 +5,7 @@ import parse from 'url-parse';
|
|||
import { themes } from '../../constants';
|
||||
import { TSupportedThemes } from '../../../theme';
|
||||
import UserPreferences from '../userPreferences';
|
||||
import ensureSecureProtocol from './ensureSecureProtocol';
|
||||
|
||||
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> => {
|
||||
url = ensureSecureProtocol(url);
|
||||
try {
|
||||
const browser = UserPreferences.getString(DEFAULT_BROWSER_KEY);
|
||||
|
||||
if (browser === 'inApp') {
|
||||
await WebBrowser.openBrowserAsync(url, {
|
||||
toolbarColor: themes[theme].headerBackground,
|
||||
|
|
|
@ -86,14 +86,15 @@ class ReviewApp {
|
|||
positiveEventCount = 0;
|
||||
|
||||
pushPositiveEvent = () => {
|
||||
if (!isFDroidBuild) {
|
||||
if (this.positiveEventCount >= numberOfPositiveEvent) {
|
||||
return;
|
||||
}
|
||||
this.positiveEventCount += 1;
|
||||
if (this.positiveEventCount === numberOfPositiveEvent) {
|
||||
tryReview();
|
||||
}
|
||||
if (isFDroidBuild || process.env.RUNNING_E2E_TESTS === 'true') {
|
||||
return;
|
||||
}
|
||||
if (this.positiveEventCount >= numberOfPositiveEvent) {
|
||||
return;
|
||||
}
|
||||
this.positiveEventCount += 1;
|
||||
if (this.positiveEventCount === numberOfPositiveEvent) {
|
||||
tryReview();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { KeyboardAwareScrollViewProps } from '@codler/react-native-keyboard-aware-scroll-view';
|
||||
|
||||
const scrollPersistTaps: Partial<KeyboardAwareScrollViewProps> = {
|
||||
keyboardShouldPersistTaps: 'always',
|
||||
keyboardShouldPersistTaps: 'handled',
|
||||
keyboardDismissMode: 'interactive'
|
||||
};
|
||||
|
||||
|
|
|
@ -291,7 +291,7 @@ export default function subscribeRooms() {
|
|||
const [type, data] = ddpMessage.fields.args;
|
||||
const [, ev] = ddpMessage.fields.eventName.split('/');
|
||||
if (/userData/.test(ev)) {
|
||||
const [{ diff }] = ddpMessage.fields.args;
|
||||
const [{ diff, unset }] = ddpMessage.fields.args;
|
||||
if (diff?.statusLivechat) {
|
||||
store.dispatch(setUser({ statusLivechat: diff.statusLivechat }));
|
||||
}
|
||||
|
@ -301,6 +301,12 @@ export default function subscribeRooms() {
|
|||
if ((['settings.preferences.alsoSendThreadToChannel'] as any) in diff) {
|
||||
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 (type === 'removed') {
|
||||
|
|
|
@ -19,9 +19,9 @@ const handleBltPermission = async (): Promise<Permission[]> => {
|
|||
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 {
|
||||
const result = await Services.videoConferenceJoin(callId, cam);
|
||||
const result = await Services.videoConferenceJoin(callId, cam, mic);
|
||||
if (result.success) {
|
||||
if (isAndroid) {
|
||||
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 {
|
||||
const videoConfResponse: any = await Services.videoConferenceStart(rid);
|
||||
const videoConfResponse = await Services.videoConferenceStart(rid);
|
||||
if (videoConfResponse.success) {
|
||||
videoConfJoin(videoConfResponse.data.callId, cam);
|
||||
videoConfJoin(videoConfResponse.data.callId, cam, mic);
|
||||
}
|
||||
} catch (e) {
|
||||
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,
|
||||
params: {
|
||||
roomName?: string;
|
||||
roomAvatar?: string;
|
||||
roomAvatar?: string | null;
|
||||
roomDescription?: string;
|
||||
roomTopic?: string;
|
||||
roomAnnouncement?: string;
|
||||
|
@ -602,7 +602,7 @@ export const getRoomRoles = (
|
|||
// RC 0.65.0
|
||||
sdk.get(`${roomTypeToApiType(type)}.roles`, { roomId });
|
||||
|
||||
export const getAvatarSuggestion = (): Promise<IAvatarSuggestion> =>
|
||||
export const getAvatarSuggestion = (): Promise<{ [service: string]: IAvatarSuggestion }> =>
|
||||
// RC 0.51.0
|
||||
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 videoConferenceJoin = (callId: string, cam: boolean) =>
|
||||
sdk.post('video-conference.join', { callId, state: { cam } });
|
||||
export const videoConferenceJoin = (callId: string, cam?: boolean, mic?: boolean) =>
|
||||
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 });
|
||||
|
||||
|
|
|
@ -247,6 +247,22 @@ const handleLogout = function* handleLogout({ forcedByServer, message }) {
|
|||
};
|
||||
|
||||
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);
|
||||
|
||||
if (user?.statusLivechat && isOmnichannelModuleAvailable()) {
|
||||
|
|
|
@ -68,6 +68,7 @@ import AddChannelTeamView from '../views/AddChannelTeamView';
|
|||
import AddExistingChannelView from '../views/AddExistingChannelView';
|
||||
import SelectListView from '../views/SelectListView';
|
||||
import DiscussionsView from '../views/DiscussionsView';
|
||||
import ChangeAvatarView from '../views/ChangeAvatarView';
|
||||
import {
|
||||
AdminPanelStackParamList,
|
||||
ChatsStackParamList,
|
||||
|
@ -96,6 +97,7 @@ const ChatsStackNavigator = () => {
|
|||
<ChatsStack.Screen name='SelectListView' component={SelectListView} options={SelectListView.navigationOptions} />
|
||||
<ChatsStack.Screen name='RoomInfoView' component={RoomInfoView} options={RoomInfoView.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='DiscussionsView' component={DiscussionsView} />
|
||||
<ChatsStack.Screen
|
||||
|
@ -151,6 +153,7 @@ const ProfileStackNavigator = () => {
|
|||
>
|
||||
<ProfileStack.Screen name='ProfileView' component={ProfileView} options={ProfileView.navigationOptions} />
|
||||
<ProfileStack.Screen name='UserPreferencesView' component={UserPreferencesView} />
|
||||
<ProfileStack.Screen name='ChangeAvatarView' component={ChangeAvatarView} />
|
||||
<ProfileStack.Screen name='UserNotificationPrefView' component={UserNotificationPrefView} />
|
||||
<ProfileStack.Screen name='PickerView' component={PickerView} options={PickerView.navigationOptions} />
|
||||
</ProfileStack.Navigator>
|
||||
|
|
|
@ -17,6 +17,7 @@ import RoomsListView from '../../views/RoomsListView';
|
|||
import RoomActionsView from '../../views/RoomActionsView';
|
||||
import RoomInfoView from '../../views/RoomInfoView';
|
||||
import RoomInfoEditView from '../../views/RoomInfoEditView';
|
||||
import ChangeAvatarView from '../../views/ChangeAvatarView';
|
||||
import RoomMembersView from '../../views/RoomMembersView';
|
||||
import SearchMessagesView from '../../views/SearchMessagesView';
|
||||
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='SelectListView' component={SelectListView} />
|
||||
<ModalStack.Screen name='RoomInfoEditView' component={RoomInfoEditView} options={RoomInfoEditView.navigationOptions} />
|
||||
<ModalStack.Screen name='ChangeAvatarView' component={ChangeAvatarView} />
|
||||
<ModalStack.Screen name='RoomMembersView' component={RoomMembersView} />
|
||||
<ModalStack.Screen
|
||||
name='SearchMessagesView'
|
||||
|
|
|
@ -6,6 +6,7 @@ import { IMessage } from '../../definitions/IMessage';
|
|||
import { ISubscription, SubscriptionType, TSubscriptionModel } from '../../definitions/ISubscription';
|
||||
import { ILivechatDepartment } from '../../definitions/ILivechatDepartment';
|
||||
import { ILivechatTag } from '../../definitions/ILivechatTag';
|
||||
import { TChangeAvatarViewContext } from '../../definitions/TChangeAvatarViewContext';
|
||||
|
||||
export type MasterDetailChatsStackParamList = {
|
||||
RoomView: {
|
||||
|
@ -58,6 +59,12 @@ export type ModalStackParamList = {
|
|||
onSearch?: Function;
|
||||
isRadio?: boolean;
|
||||
};
|
||||
ChangeAvatarView: {
|
||||
context: TChangeAvatarViewContext;
|
||||
titleHeader?: string;
|
||||
room?: ISubscription;
|
||||
t?: SubscriptionType;
|
||||
};
|
||||
RoomInfoEditView: {
|
||||
rid: string;
|
||||
};
|
||||
|
|
|
@ -13,6 +13,7 @@ import { ModalStackParamList } from './MasterDetailStack/types';
|
|||
import { TThreadModel } from '../definitions';
|
||||
import { ILivechatDepartment } from '../definitions/ILivechatDepartment';
|
||||
import { ILivechatTag } from '../definitions/ILivechatTag';
|
||||
import { TChangeAvatarViewContext } from '../definitions/TChangeAvatarViewContext';
|
||||
|
||||
export type ChatsStackParamList = {
|
||||
ModalStackNavigator: NavigatorScreenParams<ModalStackParamList>;
|
||||
|
@ -181,6 +182,12 @@ export type ChatsStackParamList = {
|
|||
onlyAudio?: boolean;
|
||||
videoConf?: boolean;
|
||||
};
|
||||
ChangeAvatarView: {
|
||||
context: TChangeAvatarViewContext;
|
||||
titleHeader?: string;
|
||||
room?: ISubscription;
|
||||
t?: SubscriptionType;
|
||||
};
|
||||
};
|
||||
|
||||
export type ProfileStackParamList = {
|
||||
|
@ -195,6 +202,12 @@ export type ProfileStackParamList = {
|
|||
goBack?: Function;
|
||||
onChangeValue: Function;
|
||||
};
|
||||
ChangeAvatarView: {
|
||||
context: TChangeAvatarViewContext;
|
||||
titleHeader?: string;
|
||||
room?: ISubscription;
|
||||
t?: SubscriptionType;
|
||||
};
|
||||
};
|
||||
|
||||
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,
|
||||
onPress: () => this.onPressItem(item),
|
||||
baseUrl,
|
||||
testID: `directory-view-item-${item.name}`.toLowerCase(),
|
||||
testID: `directory-view-item-${item.name}`,
|
||||
style,
|
||||
user,
|
||||
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 { StackNavigationOptions, StackNavigationProp } from '@react-navigation/stack';
|
||||
import { HeaderBackButton } from '@react-navigation/elements';
|
||||
|
@ -46,12 +46,13 @@ const DiscussionsView = ({ navigation, route }: IDiscussionsViewProps): React.Re
|
|||
const [discussions, setDiscussions] = useState<IMessageFromServer[]>([]);
|
||||
const [search, setSearch] = useState<IMessageFromServer[]>([]);
|
||||
const [isSearching, setIsSearching] = useState(false);
|
||||
const [total, setTotal] = useState(0);
|
||||
const [searchTotal, setSearchTotal] = useState(0);
|
||||
const total = useRef(0);
|
||||
const searchText = useRef('');
|
||||
const offset = useRef(0);
|
||||
|
||||
const { colors } = useTheme();
|
||||
|
||||
const load = async (text = '') => {
|
||||
const load = async () => {
|
||||
if (loading) {
|
||||
return;
|
||||
}
|
||||
|
@ -60,18 +61,18 @@ const DiscussionsView = ({ navigation, route }: IDiscussionsViewProps): React.Re
|
|||
try {
|
||||
const result = await Services.getDiscussions({
|
||||
roomId: rid,
|
||||
offset: isSearching ? search.length : discussions.length,
|
||||
offset: offset.current,
|
||||
count: API_FETCH_COUNT,
|
||||
text
|
||||
text: searchText.current
|
||||
});
|
||||
|
||||
if (result.success) {
|
||||
offset.current += result.count;
|
||||
total.current = result.total;
|
||||
if (isSearching) {
|
||||
setSearch(result.messages);
|
||||
setSearchTotal(result.total);
|
||||
setSearch(prevState => (offset.current ? [...prevState, ...result.messages] : result.messages));
|
||||
} else {
|
||||
setDiscussions(result.messages);
|
||||
setTotal(result.total);
|
||||
}
|
||||
}
|
||||
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);
|
||||
await load(text);
|
||||
setSearch([]);
|
||||
searchText.current = text;
|
||||
offset.current = 0;
|
||||
load();
|
||||
}, 500);
|
||||
|
||||
const onCancelSearchPress = () => {
|
||||
setIsSearching(false);
|
||||
setSearch([]);
|
||||
setSearchTotal(0);
|
||||
searchText.current = '';
|
||||
offset.current = 0;
|
||||
};
|
||||
|
||||
const onSearchPress = () => {
|
||||
|
@ -181,12 +186,12 @@ const DiscussionsView = ({ navigation, route }: IDiscussionsViewProps): React.Re
|
|||
<FlatList
|
||||
data={isSearching ? search : discussions}
|
||||
renderItem={renderItem}
|
||||
keyExtractor={(item: any) => item.msg}
|
||||
keyExtractor={(item: any) => item._id}
|
||||
style={{ backgroundColor: colors.backgroundColor }}
|
||||
contentContainerStyle={styles.contentContainer}
|
||||
onEndReachedThreshold={0.5}
|
||||
removeClippedSubviews={isIOS}
|
||||
onEndReached={() => (isSearching ? searchTotal : total) > API_FETCH_COUNT ?? load()}
|
||||
onEndReached={() => isSearching && offset.current < total.current && load()}
|
||||
ItemSeparatorComponent={List.Separator}
|
||||
ListFooterComponent={loading ? <ActivityIndicator /> : null}
|
||||
scrollIndicatorInsets={{ right: 1 }}
|
||||
|
|
|
@ -1,14 +1,13 @@
|
|||
import { activateKeepAwake, deactivateKeepAwake } from 'expo-keep-awake';
|
||||
import React from 'react';
|
||||
import { BackHandler, NativeEventSubscription } from 'react-native';
|
||||
import BackgroundTimer from 'react-native-background-timer';
|
||||
import { isAppInstalled, openAppWithUri } from 'react-native-send-intent';
|
||||
import WebView from 'react-native-webview';
|
||||
import { WebViewMessage, WebViewNavigation } from 'react-native-webview/lib/WebViewTypes';
|
||||
import { activateKeepAwake, deactivateKeepAwake } from 'expo-keep-awake';
|
||||
|
||||
import { IBaseScreen } from '../definitions';
|
||||
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 { withTheme } from '../theme';
|
||||
|
||||
|
@ -20,7 +19,6 @@ class JitsiMeetView extends React.Component<TJitsiMeetViewProps> {
|
|||
private rid: string;
|
||||
private url: string;
|
||||
private videoConf: boolean;
|
||||
private jitsiTimeout: number | null;
|
||||
private backHandler!: NativeEventSubscription;
|
||||
|
||||
constructor(props: TJitsiMeetViewProps) {
|
||||
|
@ -28,7 +26,6 @@ class JitsiMeetView extends React.Component<TJitsiMeetViewProps> {
|
|||
this.rid = props.route.params?.rid;
|
||||
this.url = props.route.params?.url;
|
||||
this.videoConf = !!props.route.params?.videoConf;
|
||||
this.jitsiTimeout = null;
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
|
@ -50,10 +47,8 @@ class JitsiMeetView extends React.Component<TJitsiMeetViewProps> {
|
|||
|
||||
componentWillUnmount() {
|
||||
logEvent(this.videoConf ? events.LIVECHAT_VIDEOCONF_TERMINATE : events.JM_CONFERENCE_TERMINATE);
|
||||
if (this.jitsiTimeout && !this.videoConf) {
|
||||
BackgroundTimer.clearInterval(this.jitsiTimeout);
|
||||
this.jitsiTimeout = null;
|
||||
BackgroundTimer.stopBackgroundTimer();
|
||||
if (!this.videoConf) {
|
||||
endVideoConfTimer();
|
||||
}
|
||||
this.backHandler.remove();
|
||||
deactivateKeepAwake();
|
||||
|
@ -64,15 +59,7 @@ class JitsiMeetView extends React.Component<TJitsiMeetViewProps> {
|
|||
onConferenceJoined = () => {
|
||||
logEvent(this.videoConf ? events.LIVECHAT_VIDEOCONF_JOIN : events.JM_CONFERENCE_JOIN);
|
||||
if (this.rid && !this.videoConf) {
|
||||
Services.updateJitsiTimeout(this.rid).catch((e: unknown) => console.log(e));
|
||||
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);
|
||||
initVideoConfTimer(this.rid);
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -90,7 +77,7 @@ class JitsiMeetView extends React.Component<TJitsiMeetViewProps> {
|
|||
render() {
|
||||
return (
|
||||
<WebView
|
||||
source={{ uri: `${this.url}&config.disableDeepLinking=true` }}
|
||||
source={{ uri: `${this.url}${this.url.includes('#config') ? '&' : '#'}config.disableDeepLinking=true` }}
|
||||
onMessage={({ nativeEvent }) => this.onNavigationStateChange(nativeEvent)}
|
||||
onNavigationStateChange={this.onNavigationStateChange}
|
||||
style={{ flex: 1 }}
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue