mirror of
https://gitcode.com/GitHub_Trending/ji/jitsi-meet.git
synced 2026-01-07 07:12:28 +00:00
Compare commits
73 Commits
saghul-pat
...
8740
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cb621f8e32 | ||
|
|
3c80cfddd7 | ||
|
|
557f6defb8 | ||
|
|
52fa36f930 | ||
|
|
b050e5f5e8 | ||
|
|
bf8d83953b | ||
|
|
f16bf466eb | ||
|
|
29ea811527 | ||
|
|
435d034fdb | ||
|
|
419baa7ab7 | ||
|
|
9eb7b7bb01 | ||
|
|
19ee989cda | ||
|
|
ab1dcc5375 | ||
|
|
3047b4c8c4 | ||
|
|
2afce3d151 | ||
|
|
1cea9b1786 | ||
|
|
2b7299ae05 | ||
|
|
4b50f13e96 | ||
|
|
c639acebcf | ||
|
|
1a34ed9a2d | ||
|
|
0939e207eb | ||
|
|
8c3ea05ae6 | ||
|
|
daf8a929b1 | ||
|
|
2f3df2c66f | ||
|
|
d8d1f8331e | ||
|
|
0e69336f94 | ||
|
|
ede8ae6cb9 | ||
|
|
7e57156d2a | ||
|
|
6742435487 | ||
|
|
99f34aaef4 | ||
|
|
69f9838c03 | ||
|
|
dbfd24261d | ||
|
|
2305ae85a0 | ||
|
|
31a30f1118 | ||
|
|
eacf7addb2 | ||
|
|
2cf788ebee | ||
|
|
6bd3ed5ae4 | ||
|
|
b511f4b8df | ||
|
|
ead019f71b | ||
|
|
7a97d15e89 | ||
|
|
1acb99d763 | ||
|
|
adbe990867 | ||
|
|
a4367567ab | ||
|
|
7f56cbc4ce | ||
|
|
d636d084c8 | ||
|
|
298567be48 | ||
|
|
c233629e51 | ||
|
|
75b5702a7e | ||
|
|
540f01d47e | ||
|
|
5c7ed6a8b3 | ||
|
|
3c5d33fefa | ||
|
|
be04236834 | ||
|
|
ec1bfe73b3 | ||
|
|
d2ed9ffef6 | ||
|
|
6141ff78f8 | ||
|
|
c6a75fb9ed | ||
|
|
3438438219 | ||
|
|
7cedea6740 | ||
|
|
69f26c8a38 | ||
|
|
92a4750d0e | ||
|
|
370a884765 | ||
|
|
877fc98eef | ||
|
|
7bed0b36bd | ||
|
|
cd5aed37e9 | ||
|
|
b8dad082df | ||
|
|
f84f98e8e5 | ||
|
|
d1328d68f2 | ||
|
|
43d5c1e3ba | ||
|
|
22ed00724d | ||
|
|
0b095f36eb | ||
|
|
327376d85e | ||
|
|
f28bd67ff4 | ||
|
|
3a54c3418b |
2
.github/workflows/ci.yml
vendored
2
.github/workflows/ci.yml
vendored
@@ -103,7 +103,7 @@ jobs:
|
||||
android-sdk-build:
|
||||
name: Build mobile SDK (Android)
|
||||
runs-on: ubuntu-latest
|
||||
container: reactnativecommunity/react-native-android:v13.0
|
||||
container: reactnativecommunity/react-native-android:v18.0
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v4
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
apply plugin: 'com.android.application'
|
||||
apply plugin: 'kotlin-android'
|
||||
|
||||
// Crashlytics integration is done as part of Firebase now, so it gets
|
||||
// automagically activated with google-services.json
|
||||
@@ -66,9 +67,18 @@ android {
|
||||
}
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility JavaVersion.VERSION_1_8
|
||||
targetCompatibility JavaVersion.VERSION_1_8
|
||||
sourceCompatibility rootProject.ext.javaVersion
|
||||
targetCompatibility rootProject.ext.javaVersion
|
||||
}
|
||||
|
||||
kotlinOptions {
|
||||
jvmTarget = rootProject.ext.jvmTargetVersion
|
||||
}
|
||||
|
||||
kotlin {
|
||||
jvmToolchain(rootProject.ext.jvmToolchainVersion)
|
||||
}
|
||||
|
||||
namespace 'org.jitsi.meet'
|
||||
}
|
||||
|
||||
|
||||
@@ -4,3 +4,8 @@
|
||||
-keepattributes *Annotation*
|
||||
-keepattributes SourceFile,LineNumberTable
|
||||
-keep public class * extends java.lang.Exception
|
||||
|
||||
# R8 missing classes - suppress warnings
|
||||
-dontwarn com.facebook.fresco.ui.common.LoggingListener
|
||||
-dontwarn com.facebook.memory.config.MemorySpikeConfig
|
||||
-dontwarn kotlinx.parcelize.Parcelize
|
||||
|
||||
@@ -5,46 +5,52 @@ import org.gradle.util.VersionNumber
|
||||
// sub-projects/modules.
|
||||
|
||||
buildscript {
|
||||
ext {
|
||||
kotlinVersion = "2.0.21"
|
||||
gradlePluginVersion = "8.4.2"
|
||||
buildToolsVersion = "34.0.0"
|
||||
compileSdkVersion = 34
|
||||
minSdkVersion = 26
|
||||
targetSdkVersion = 34
|
||||
supportLibVersion = "28.0.0"
|
||||
ndkVersion = "27.1.12297006"
|
||||
|
||||
// The Maven artifact groupId of the third-party react-native modules which
|
||||
// Jitsi Meet SDK for Android depends on and which are not available in
|
||||
// third-party Maven repositories so we have to deploy to a Maven repository
|
||||
// of ours.
|
||||
moduleGroupId = 'com.facebook.react'
|
||||
|
||||
// Maven repo where artifacts will be published
|
||||
mavenRepo = System.env.MVN_REPO ?: ""
|
||||
mavenUser = System.env.MVN_USER ?: ""
|
||||
mavenPassword = System.env.MVN_PASSWORD ?: ""
|
||||
|
||||
// Libre build
|
||||
libreBuild = (System.env.LIBRE_BUILD ?: "false").toBoolean()
|
||||
|
||||
googleServicesEnabled = project.file('app/google-services.json').exists() && !libreBuild
|
||||
|
||||
//React Native and Hermes Version
|
||||
rnVersion = "0.77.2"
|
||||
|
||||
// Java dependencies
|
||||
javaVersion = JavaVersion.VERSION_17
|
||||
jvmToolchainVersion = 17
|
||||
jvmTargetVersion = '17'
|
||||
}
|
||||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
}
|
||||
dependencies {
|
||||
classpath 'com.android.tools.build:gradle:7.4.2'
|
||||
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$rootProject.ext.kotlinVersion"
|
||||
classpath "com.android.tools.build:gradle:$rootProject.ext.gradlePluginVersion"
|
||||
classpath 'com.google.gms:google-services:4.4.0'
|
||||
classpath 'com.google.firebase:firebase-crashlytics-gradle:2.9.9'
|
||||
}
|
||||
}
|
||||
|
||||
ext {
|
||||
kotlinVersion = "1.9.24"
|
||||
buildToolsVersion = "34.0.0"
|
||||
compileSdkVersion = 34
|
||||
minSdkVersion = 26
|
||||
targetSdkVersion = 34
|
||||
supportLibVersion = "28.0.0"
|
||||
ndkVersion = "26.1.10909125"
|
||||
|
||||
// The Maven artifact groupId of the third-party react-native modules which
|
||||
// Jitsi Meet SDK for Android depends on and which are not available in
|
||||
// third-party Maven repositories so we have to deploy to a Maven repository
|
||||
// of ours.
|
||||
moduleGroupId = 'com.facebook.react'
|
||||
|
||||
// Maven repo where artifacts will be published
|
||||
mavenRepo = System.env.MVN_REPO ?: ""
|
||||
mavenUser = System.env.MVN_USER ?: ""
|
||||
mavenPassword = System.env.MVN_PASSWORD ?: ""
|
||||
|
||||
// Libre build
|
||||
libreBuild = (System.env.LIBRE_BUILD ?: "false").toBoolean()
|
||||
|
||||
googleServicesEnabled = project.file('app/google-services.json').exists() && !libreBuild
|
||||
|
||||
//React Native and Hermes Version
|
||||
rnVersion = "0.75.5"
|
||||
}
|
||||
|
||||
allprojects {
|
||||
repositories {
|
||||
mavenCentral()
|
||||
@@ -134,7 +140,7 @@ allprojects {
|
||||
project.version = "${json.version}-jitsi-${versionQualifierNumber}"
|
||||
|
||||
task jitsiAndroidSourcesJar(type: Jar) {
|
||||
classifier = 'sources'
|
||||
archiveClassifier = 'sources'
|
||||
from android.sourceSets.main.java.source
|
||||
}
|
||||
|
||||
@@ -185,16 +191,46 @@ allprojects {
|
||||
}
|
||||
}
|
||||
|
||||
// Force the version of the Android build tools we have chosen on all
|
||||
// subprojects. The forcing was introduced for react-native and the third-party
|
||||
// modules that we utilize such as react-native-background-timer.
|
||||
// Force the version of the Android build tools we have chosen on all subprojects.
|
||||
subprojects { subproject ->
|
||||
afterEvaluate{
|
||||
if ((subproject.plugins.hasPlugin('android')
|
||||
|| subproject.plugins.hasPlugin('android-library'))
|
||||
&& rootProject.ext.has('buildToolsVersion')) {
|
||||
|
||||
android {
|
||||
buildToolsVersion rootProject.ext.buildToolsVersion
|
||||
|
||||
buildFeatures {
|
||||
buildConfig true
|
||||
}
|
||||
|
||||
// Set JVM target across all subprojects
|
||||
compileOptions {
|
||||
sourceCompatibility rootProject.ext.javaVersion
|
||||
targetCompatibility rootProject.ext.javaVersion
|
||||
}
|
||||
|
||||
// Disable lint errors for problematic third-party modules
|
||||
// react-native-background-timer
|
||||
// react-native-calendar-events
|
||||
lint {
|
||||
abortOnError = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Add Kotlin configuration for subprojects that use Kotlin
|
||||
if (subproject.plugins.hasPlugin('kotlin-android')) {
|
||||
subproject.kotlin {
|
||||
jvmToolchain(rootProject.ext.jvmToolchainVersion)
|
||||
}
|
||||
|
||||
// Set Kotlin JVM target
|
||||
subproject.android {
|
||||
kotlinOptions {
|
||||
jvmTarget = rootProject.ext.jvmTargetVersion
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,20 +11,26 @@
|
||||
# The setting is particularly useful for tweaking memory settings.
|
||||
# Default value: -Xmx1024m -XX:MaxPermSize=256m
|
||||
|
||||
org.gradle.jvmargs=-Xmx2048m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8
|
||||
org.gradle.jvmargs=-Xmx4048m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8
|
||||
|
||||
# When configured, Gradle will run in incubating parallel mode.
|
||||
# This option should only be used with decoupled projects. More details, visit
|
||||
# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
|
||||
# org.gradle.parallel=true
|
||||
|
||||
# This one fixes a weird WebRTC runtime problem on some devices.
|
||||
# https://github.com/jitsi/jitsi-meet/issues/7911#issuecomment-714323255
|
||||
android.enableDexingArtifactTransform.desugaring=false
|
||||
|
||||
android.useAndroidX=true
|
||||
|
||||
android.enableJetifier=true
|
||||
android.bundle.enableUncompressedNativeLibs=false
|
||||
|
||||
# Use this property to enable support to the new architecture.
|
||||
# This will allow you to use TurboModules and the Fabric render in
|
||||
# your application. You should enable this flag either if you want
|
||||
# to write custom TurboModules/Fabric components OR use libraries that
|
||||
# are providing them.
|
||||
newArchEnabled=false
|
||||
|
||||
# Use this property to enable or disable the Hermes JS engine.
|
||||
hermesEnabled=true
|
||||
|
||||
appVersion=99.0.0
|
||||
sdkVersion=0.0.0
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-7.5.1-all.zip
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.14-bin.zip
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
|
||||
@@ -50,6 +50,7 @@ dependencies {
|
||||
implementation 'com.squareup.duktape:duktape-android:1.3.0'
|
||||
implementation 'com.google.code.gson:gson:2.8.6'
|
||||
implementation 'androidx.startup:startup-runtime:1.1.0'
|
||||
implementation 'com.google.j2objc:j2objc-annotations:3.0.0'
|
||||
|
||||
// Only add these packages if we are NOT doing a LIBRE_BUILD
|
||||
if (!rootProject.ext.libreBuild) {
|
||||
@@ -83,7 +84,7 @@ dependencies {
|
||||
implementation project(':react-native-screens')
|
||||
implementation project(':react-native-slider')
|
||||
implementation project(':react-native-sound')
|
||||
implementation project(':react-native-splash-screen')
|
||||
implementation project(':react-native-splash-view')
|
||||
implementation project(':react-native-svg')
|
||||
implementation project(':react-native-video')
|
||||
implementation project(':react-native-webview')
|
||||
@@ -137,8 +138,16 @@ android.libraryVariants.all { def variant ->
|
||||
def devEnabled = !targetName.toLowerCase().contains("release")
|
||||
|
||||
// Run the bundler
|
||||
// Use full path to node to avoid PATH issues in Gradle
|
||||
def nodePath = System.getenv('NVM_BIN') ? "${System.getenv('NVM_BIN')}/node" : "node"
|
||||
|
||||
// Debug: Print the node path and environment
|
||||
println "Using node path: ${nodePath}"
|
||||
println "NVM_BIN: ${System.getenv('NVM_BIN')}"
|
||||
println "Working directory: ${reactRoot}"
|
||||
|
||||
commandLine(
|
||||
"node",
|
||||
nodePath,
|
||||
"node_modules/react-native/scripts/bundle.js",
|
||||
"--platform", "android",
|
||||
"--dev", "${devEnabled}",
|
||||
@@ -151,6 +160,70 @@ android.libraryVariants.all { def variant ->
|
||||
enabled !devEnabled
|
||||
}
|
||||
|
||||
// GRADLE REQUIREMENTS (Gradle 8.7+ / AGP 8.5.0+):
|
||||
|
||||
// This task requires explicit dependencies on resource tasks from all React Native modules
|
||||
// due to Gradle's strict validation of task dependencies.
|
||||
|
||||
// Without these dependencies,
|
||||
// builds will fail with errors like:
|
||||
// "Task ':sdk:bundleReleaseJsAndAssets' uses the output of task ':react-native-amplitude:packageReleaseResources'
|
||||
// without declaring a dependency on it."
|
||||
|
||||
// The automatic dependency resolution below ensures all required resource tasks are properly
|
||||
// declared as dependencies before this task executes.
|
||||
|
||||
if (variant.name.toLowerCase().contains("release")) {
|
||||
rootProject.subprojects.each { subproject ->
|
||||
if (
|
||||
subproject.name.startsWith("react-native-") ||
|
||||
subproject.name.startsWith("@react-native-") ||
|
||||
subproject.name.startsWith("@giphy/")
|
||||
) {
|
||||
[
|
||||
"packageReleaseResources",
|
||||
"generateReleaseResValues",
|
||||
"generateReleaseResources",
|
||||
"generateReleaseBuildConfig",
|
||||
"processReleaseManifest",
|
||||
"writeReleaseAarMetadata",
|
||||
"generateReleaseRFile",
|
||||
"compileReleaseLibraryResources",
|
||||
"compileReleaseJavaWithJavac",
|
||||
"javaPreCompileRelease",
|
||||
"bundleLibCompileToJarRelease",
|
||||
"exportReleaseConsumerProguardFiles",
|
||||
"mergeReleaseGeneratedProguardFiles",
|
||||
"mergeReleaseJniLibFolders",
|
||||
"mergeReleaseShaders",
|
||||
"packageReleaseAssets",
|
||||
"processReleaseJavaRes",
|
||||
"prepareReleaseArtProfile",
|
||||
"copyReleaseJniLibsProjectOnly",
|
||||
"extractDeepLinksRelease",
|
||||
"createFullJarRelease",
|
||||
"generateReleaseLintModel",
|
||||
"writeReleaseLintModelMetadata",
|
||||
"generateReleaseLintVitalModel",
|
||||
"lintVitalAnalyzeRelease",
|
||||
"lintReportRelease",
|
||||
"lintAnalyzeRelease",
|
||||
"lintReportDebug",
|
||||
"lintAnalyzeDebug"
|
||||
].each { taskName ->
|
||||
if (subproject.tasks.findByName(taskName)) {
|
||||
currentBundleTask.dependsOn(subproject.tasks.named(taskName))
|
||||
}
|
||||
}
|
||||
|
||||
// Also depend on the main build task to ensure all sub-tasks are completed
|
||||
if (subproject.tasks.findByName("build")) {
|
||||
currentBundleTask.dependsOn(subproject.tasks.named("build"))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
currentBundleTask.ext.generatedResFolders = files(resourcesDir).builtBy(currentBundleTask)
|
||||
currentBundleTask.ext.generatedAssetsFolders = files(jsBundleDir).builtBy(currentBundleTask)
|
||||
variant.registerGeneratedResFolders(currentBundleTask.generatedResFolders)
|
||||
|
||||
@@ -23,8 +23,10 @@ import androidx.annotation.NonNull;
|
||||
import androidx.startup.Initializer;
|
||||
|
||||
import com.facebook.soloader.SoLoader;
|
||||
import com.facebook.react.soloader.OpenSourceMergedSoMapping;
|
||||
import org.wonday.orientation.OrientationActivityLifecycle;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
@@ -35,7 +37,11 @@ public class JitsiInitializer implements Initializer<Boolean> {
|
||||
public Boolean create(@NonNull Context context) {
|
||||
Log.d(this.getClass().getCanonicalName(), "create");
|
||||
|
||||
SoLoader.init(context, /* native exopackage */ false);
|
||||
try {
|
||||
SoLoader.init(context, OpenSourceMergedSoMapping.INSTANCE);
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
|
||||
// Register our uncaught exception handler.
|
||||
JitsiMeetUncaughtExceptionHandler.register();
|
||||
@@ -43,6 +49,10 @@ public class JitsiInitializer implements Initializer<Boolean> {
|
||||
// Register activity lifecycle handler for the orientation locker module.
|
||||
((Application) context).registerActivityLifecycleCallbacks(OrientationActivityLifecycle.getInstance());
|
||||
|
||||
// Initialize ReactInstanceManager during application startup
|
||||
// This ensures it's ready before any Activity onCreate is called
|
||||
ReactInstanceManagerHolder.initReactInstanceManager((Application) context);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@@ -22,7 +22,7 @@ import android.os.Bundle;
|
||||
|
||||
import com.facebook.react.ReactInstanceManager;
|
||||
|
||||
import org.devio.rn.splashscreen.SplashScreen;
|
||||
import com.splashview.SplashView;
|
||||
import org.jitsi.meet.sdk.log.JitsiMeetLogger;
|
||||
|
||||
public class JitsiMeet {
|
||||
@@ -92,7 +92,7 @@ public class JitsiMeet {
|
||||
*/
|
||||
public static void showSplashScreen(Activity activity) {
|
||||
try {
|
||||
SplashScreen.show(activity);
|
||||
SplashView.INSTANCE.showSplashView(activity);
|
||||
} catch (Exception e) {
|
||||
JitsiMeetLogger.e(e, "Failed to show splash screen");
|
||||
}
|
||||
|
||||
@@ -102,6 +102,10 @@ public class JitsiMeetActivity extends AppCompatActivity
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
// ReactInstanceManager is now initialized by JitsiInitializer during application startup
|
||||
// Just call onHostResume since the manager is already ready
|
||||
JitsiMeetActivityDelegate.onHostResume(this);
|
||||
|
||||
setContentView(R.layout.activity_jitsi_meet);
|
||||
this.jitsiView = findViewById(R.id.jitsiView);
|
||||
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
package org.jitsi.meet.sdk;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.app.Application;
|
||||
import android.content.Context;
|
||||
import android.os.Bundle;
|
||||
import android.util.AttributeSet;
|
||||
@@ -196,8 +197,6 @@ public class JitsiMeetView extends FrameLayout {
|
||||
}
|
||||
|
||||
setBackgroundColor(BACKGROUND_COLOR);
|
||||
|
||||
ReactInstanceManagerHolder.initReactInstanceManager((Activity) context);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -18,6 +18,7 @@ package org.jitsi.meet.sdk;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.app.Activity;
|
||||
import android.app.Application;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
@@ -33,7 +34,6 @@ import com.facebook.react.uimanager.ViewManager;
|
||||
import com.oney.WebRTCModule.EglUtils;
|
||||
import com.oney.WebRTCModule.WebRTCModuleOptions;
|
||||
|
||||
import org.devio.rn.splashscreen.SplashScreenModule;
|
||||
import org.jitsi.meet.sdk.log.JitsiMeetLogger;
|
||||
import org.webrtc.EglBase;
|
||||
|
||||
@@ -68,7 +68,6 @@ class ReactInstanceManagerHolder {
|
||||
new JavaScriptSandboxModule(reactContext),
|
||||
new LocaleDetector(reactContext),
|
||||
new LogBridgeModule(reactContext),
|
||||
new SplashScreenModule(reactContext),
|
||||
new PictureInPictureModule(reactContext),
|
||||
new ProximityModule(reactContext),
|
||||
new org.jitsi.meet.sdk.net.NAT64AddrInfoModule(reactContext)));
|
||||
@@ -90,7 +89,7 @@ class ReactInstanceManagerHolder {
|
||||
new com.reactnativecommunity.asyncstorage.AsyncStoragePackage(),
|
||||
new com.ocetnik.timer.BackgroundTimerPackage(),
|
||||
new com.calendarevents.RNCalendarEventsPackage(),
|
||||
new com.corbt.keepawake.KCKeepAwakePackage(),
|
||||
new com.sayem.keepawake.KCKeepAwakePackage(),
|
||||
new com.facebook.react.shell.MainReactPackage(),
|
||||
new com.reactnativecommunity.clipboard.ClipboardPackage(),
|
||||
new com.reactnativecommunity.netinfo.NetInfoPackage(),
|
||||
@@ -110,6 +109,7 @@ class ReactInstanceManagerHolder {
|
||||
new com.th3rdwave.safeareacontext.SafeAreaContextPackage(),
|
||||
new com.horcrux.svg.SvgPackage(),
|
||||
new org.wonday.orientation.OrientationPackage(),
|
||||
new com.splashview.SplashViewPackage(),
|
||||
new ReactPackageAdapter() {
|
||||
@Override
|
||||
public List<NativeModule> createNativeModules(ReactApplicationContext reactContext) {
|
||||
@@ -133,7 +133,7 @@ class ReactInstanceManagerHolder {
|
||||
|
||||
// GiphyReactNativeSdkPackage
|
||||
try {
|
||||
Class<?> giphyPackageClass = Class.forName("com.giphyreactnativesdk.GiphyReactNativeSdkPackage");
|
||||
Class<?> giphyPackageClass = Class.forName("com.giphyreactnativesdk.RTNGiphySdkPackage");
|
||||
Constructor<?> constructor = giphyPackageClass.getConstructor();
|
||||
packages.add((ReactPackage)constructor.newInstance());
|
||||
} catch (Exception e) {
|
||||
@@ -208,9 +208,9 @@ class ReactInstanceManagerHolder {
|
||||
* time. All {@code ReactRootView} instances will be tied to the one and
|
||||
* only {@code ReactInstanceManager}.
|
||||
*
|
||||
* @param activity {@code Activity} current running Activity.
|
||||
* @param app {@code Application}
|
||||
*/
|
||||
static void initReactInstanceManager(Activity activity) {
|
||||
static void initReactInstanceManager(Application app) {
|
||||
if (reactInstanceManager != null) {
|
||||
return;
|
||||
}
|
||||
@@ -232,14 +232,14 @@ class ReactInstanceManagerHolder {
|
||||
|
||||
reactInstanceManager
|
||||
= ReactInstanceManager.builder()
|
||||
.setApplication(activity.getApplication())
|
||||
.setCurrentActivity(activity)
|
||||
.setApplication(app)
|
||||
.setCurrentActivity(null)
|
||||
.setBundleAssetName("index.android.bundle")
|
||||
.setJSMainModulePath("index.android")
|
||||
.setJavaScriptExecutorFactory(new HermesExecutorFactory())
|
||||
.addPackages(getReactNativePackages())
|
||||
.setUseDeveloperSupport(BuildConfig.DEBUG)
|
||||
.setInitialLifecycleState(LifecycleState.RESUMED)
|
||||
.setInitialLifecycleState(LifecycleState.BEFORE_CREATE)
|
||||
.build();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
rootProject.name = 'jitsi-meet'
|
||||
|
||||
include ':app', ':sdk'
|
||||
|
||||
include ':react-native-amplitude'
|
||||
@@ -29,7 +27,7 @@ project(':react-native-google-signin').projectDir = new File(rootProject.project
|
||||
include ':react-native-immersive-mode'
|
||||
project(':react-native-immersive-mode').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-immersive-mode/android')
|
||||
include ':react-native-keep-awake'
|
||||
project(':react-native-keep-awake').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-keep-awake/android')
|
||||
project(':react-native-keep-awake').projectDir = new File(rootProject.projectDir, '../node_modules/@sayem314/react-native-keep-awake/android')
|
||||
include ':react-native-orientation-locker'
|
||||
project(':react-native-orientation-locker').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-orientation-locker/android')
|
||||
include ':react-native-pager-view'
|
||||
@@ -44,8 +42,8 @@ include ':react-native-slider'
|
||||
project(':react-native-slider').projectDir = new File(rootProject.projectDir, '../node_modules/@react-native-community/slider/android')
|
||||
include ':react-native-sound'
|
||||
project(':react-native-sound').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-sound/android')
|
||||
include ':react-native-splash-screen'
|
||||
project(':react-native-splash-screen').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-splash-screen/android')
|
||||
include ':react-native-splash-view'
|
||||
project(':react-native-splash-view').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-splash-view/android')
|
||||
include ':react-native-svg'
|
||||
project(':react-native-svg').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-svg/android')
|
||||
include ':react-native-video'
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
module.exports = {
|
||||
presets: [ 'module:metro-react-native-babel-preset' ],
|
||||
presets: [ 'module:@react-native/babel-preset' ],
|
||||
env: {
|
||||
production: {
|
||||
plugins: [ 'react-native-paper/babel' ]
|
||||
|
||||
@@ -18,8 +18,6 @@ import {
|
||||
maybeRedirectToWelcomePage,
|
||||
reloadWithStoredParams
|
||||
} from './react/features/app/actions';
|
||||
import { showModeratedNotification } from './react/features/av-moderation/actions';
|
||||
import { shouldShowModeratedNotification } from './react/features/av-moderation/functions';
|
||||
import {
|
||||
_conferenceWillJoin,
|
||||
authStatusChanged,
|
||||
@@ -52,7 +50,8 @@ import {
|
||||
commonUserJoinedHandling,
|
||||
commonUserLeftHandling,
|
||||
getConferenceOptions,
|
||||
sendLocalParticipant
|
||||
sendLocalParticipant,
|
||||
updateTrackMuteState
|
||||
} from './react/features/base/conference/functions';
|
||||
import { getReplaceParticipant, getSsrcRewritingFeatureFlag } from './react/features/base/config/functions';
|
||||
import { connect } from './react/features/base/connection/actions.web';
|
||||
@@ -153,7 +152,6 @@ import {
|
||||
DATA_CHANNEL_CLOSED_NOTIFICATION_ID,
|
||||
NOTIFICATION_TIMEOUT_TYPE
|
||||
} from './react/features/notifications/constants';
|
||||
import { isModerationNotificationDisplayed } from './react/features/notifications/functions';
|
||||
import { suspendDetected } from './react/features/power-monitor/actions';
|
||||
import { initPrejoin, isPrejoinPageVisible } from './react/features/prejoin/functions';
|
||||
import { disableReceiver, stopReceiver } from './react/features/remote-control/actions';
|
||||
@@ -704,15 +702,6 @@ export default {
|
||||
return;
|
||||
}
|
||||
|
||||
// check for A/V Moderation when trying to unmute
|
||||
if (!mute && shouldShowModeratedNotification(MEDIA_TYPE.AUDIO, state)) {
|
||||
if (!isModerationNotificationDisplayed(MEDIA_TYPE.AUDIO, state)) {
|
||||
APP.store.dispatch(showModeratedNotification(MEDIA_TYPE.AUDIO));
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
await APP.store.dispatch(setAudioMuted(mute, true));
|
||||
},
|
||||
|
||||
@@ -746,12 +735,6 @@ export default {
|
||||
* dialogs in case of media permissions error.
|
||||
*/
|
||||
muteVideo(mute) {
|
||||
if (this.videoSwitchInProgress) {
|
||||
logger.warn('muteVideo - unable to perform operations while video switch is in progress');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const state = APP.store.getState();
|
||||
|
||||
if (!mute
|
||||
@@ -761,11 +744,6 @@ export default {
|
||||
return;
|
||||
}
|
||||
|
||||
// check for A/V Moderation when trying to unmute and return early
|
||||
if (!mute && shouldShowModeratedNotification(MEDIA_TYPE.VIDEO, state)) {
|
||||
return;
|
||||
}
|
||||
|
||||
APP.store.dispatch(setVideoMuted(mute, VIDEO_MUTISM_AUTHORITY.USER, true));
|
||||
},
|
||||
|
||||
@@ -1019,7 +997,6 @@ export default {
|
||||
|
||||
// Restore initial state.
|
||||
this._localTracksInitialized = false;
|
||||
this.isSharingScreen = false;
|
||||
this.roomName = roomName;
|
||||
|
||||
const { tryCreateLocalTracks, errors } = this.createInitialLocalTracks(options);
|
||||
@@ -1198,8 +1175,6 @@ export default {
|
||||
return Boolean(APP.store.getState()['features/base/audio-only'].enabled);
|
||||
},
|
||||
|
||||
videoSwitchInProgress: false,
|
||||
|
||||
/**
|
||||
* This fields stores a handler which will create a Promise which turns off
|
||||
* the screen sharing and restores the previous video state (was there
|
||||
@@ -1228,7 +1203,6 @@ export default {
|
||||
*/
|
||||
async _turnScreenSharingOff(didHaveVideo, ignoreDidHaveVideo) {
|
||||
this._untoggleScreenSharing = null;
|
||||
this.videoSwitchInProgress = true;
|
||||
|
||||
APP.store.dispatch(stopReceiver());
|
||||
|
||||
@@ -1280,13 +1254,11 @@ export default {
|
||||
|
||||
return promise.then(
|
||||
() => {
|
||||
this.videoSwitchInProgress = false;
|
||||
sendAnalytics(createScreenSharingEvent('stopped',
|
||||
duration === 0 ? null : duration));
|
||||
logger.info('Screen sharing stopped.');
|
||||
},
|
||||
error => {
|
||||
this.videoSwitchInProgress = false;
|
||||
logger.error(`_turnScreenSharingOff failed: ${error}`);
|
||||
|
||||
throw error;
|
||||
@@ -1316,14 +1288,13 @@ export default {
|
||||
this._untoggleScreenSharing
|
||||
= this._turnScreenSharingOff.bind(this, didHaveVideo);
|
||||
|
||||
const desktopVideoStream = desktopStreams.find(stream => stream.getType() === MEDIA_TYPE.VIDEO);
|
||||
const desktopAudioStream = desktopStreams.find(stream => stream.getType() === MEDIA_TYPE.AUDIO);
|
||||
|
||||
if (desktopAudioStream) {
|
||||
desktopAudioStream.on(
|
||||
JitsiTrackEvents.LOCAL_TRACK_STOPPED,
|
||||
() => {
|
||||
logger.debug(`Local screensharing audio track stopped. ${this.isSharingScreen}`);
|
||||
logger.debug('Local screensharing audio track stopped.');
|
||||
|
||||
// Handle case where screen share was stopped from the browsers 'screen share in progress'
|
||||
// window. If audio screen sharing is stopped via the normal UX flow this point shouldn't
|
||||
@@ -1335,21 +1306,6 @@ export default {
|
||||
);
|
||||
}
|
||||
|
||||
if (desktopVideoStream) {
|
||||
desktopVideoStream.on(
|
||||
JitsiTrackEvents.LOCAL_TRACK_STOPPED,
|
||||
() => {
|
||||
logger.debug(`Local screensharing track stopped. ${this.isSharingScreen}`);
|
||||
|
||||
// If the stream was stopped during screen sharing
|
||||
// session then we should switch back to video.
|
||||
this.isSharingScreen
|
||||
&& this._untoggleScreenSharing
|
||||
&& this._untoggleScreenSharing();
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
return desktopStreams;
|
||||
}, error => {
|
||||
throw error;
|
||||
@@ -1497,10 +1453,6 @@ export default {
|
||||
room.on(JitsiConferenceEvents.TRACK_MUTE_CHANGED, (track, participantThatMutedUs) => {
|
||||
if (participantThatMutedUs) {
|
||||
APP.store.dispatch(participantMutedUs(participantThatMutedUs, track));
|
||||
if (this.isSharingScreen && track.isVideoTrack()) {
|
||||
logger.debug('TRACK_MUTE_CHANGED while screen sharing');
|
||||
this._turnScreenSharingOff(false);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1712,8 +1664,12 @@ export default {
|
||||
room.on(
|
||||
JitsiConferenceEvents.START_MUTED_POLICY_CHANGED,
|
||||
({ audio, video }) => {
|
||||
APP.store.dispatch(
|
||||
onStartMutedPolicyChanged(audio, video));
|
||||
APP.store.dispatch(onStartMutedPolicyChanged(audio, video));
|
||||
|
||||
const state = APP.store.getState();
|
||||
|
||||
updateTrackMuteState(state, APP.store.dispatch, true);
|
||||
updateTrackMuteState(state, APP.store.dispatch, false);
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
16
config.js
16
config.js
@@ -117,6 +117,11 @@ var config = {
|
||||
|
||||
// Will replace ice candidates IPs with invalid ones in order to fail ice.
|
||||
// failICE: true,
|
||||
|
||||
// When running on Spot TV, this controls whether to show the recording consent dialog.
|
||||
// If false (default), Spot instances will not show the recording consent dialog.
|
||||
// If true, Spot instances will show the recording consent dialog like regular clients.
|
||||
// showSpotConsentDialog: false,
|
||||
},
|
||||
|
||||
// Disables moderator indicators.
|
||||
@@ -1126,10 +1131,6 @@ var config = {
|
||||
// The Amplitude APP Key:
|
||||
// amplitudeAPPKey: '<APP_KEY>',
|
||||
|
||||
// Enables Amplitude UTM tracking:
|
||||
// Default value is false.
|
||||
// amplitudeIncludeUTM: false,
|
||||
|
||||
// Obfuscates room name sent to analytics (amplitude, rtcstats)
|
||||
// Default value is false.
|
||||
// obfuscateRoomName: false,
|
||||
@@ -1779,6 +1780,13 @@ var config = {
|
||||
// // The minimum number of participants that must be in the call for
|
||||
// // the top panel layout to be used.
|
||||
// minParticipantCountForTopPanel: 50,
|
||||
|
||||
// // The width of the filmstrip on joining meeting. Can be resized afterwards.
|
||||
// initialWidth: 400,
|
||||
|
||||
// // Whether the draggable resize bar of the filmstrip is always visible. Setting this to true will make
|
||||
// // the filmstrip always visible in case `disableResizable` is false.
|
||||
// alwaysShowResizeBar: true,
|
||||
// },
|
||||
|
||||
// Tile view related config options.
|
||||
|
||||
@@ -33,6 +33,7 @@ target 'JitsiMeetSDK' do
|
||||
:path => config[:reactNativePath],
|
||||
:hermes_enabled => true,
|
||||
:fabric_enabled => false,
|
||||
:new_arch_enabled => false,
|
||||
# An absolute path to your application root.
|
||||
:app_path => "#{Pod::Config.instance.installation_root}/.."
|
||||
)
|
||||
@@ -66,6 +67,7 @@ target 'JitsiMeetSDKLite' do
|
||||
:path => config[:reactNativePath],
|
||||
:hermes_enabled => true,
|
||||
:fabric_enabled => false,
|
||||
:new_arch_enabled => false,
|
||||
# An absolute path to your application root.
|
||||
:app_path => "#{Pod::Config.instance.installation_root}/.."
|
||||
)
|
||||
@@ -77,10 +79,12 @@ target 'JitsiMeetSDKLite' do
|
||||
end
|
||||
|
||||
post_install do |installer|
|
||||
|
||||
react_native_post_install(
|
||||
installer,
|
||||
use_native_modules![:reactNativePath],
|
||||
:mac_catalyst_enabled => false
|
||||
:mac_catalyst_enabled => false,
|
||||
# :ccache_enabled => true
|
||||
)
|
||||
installer.pods_project.targets.each do |target|
|
||||
# https://github.com/CocoaPods/CocoaPods/issues/11402
|
||||
@@ -99,4 +103,5 @@ post_install do |installer|
|
||||
|
||||
# Patch SocketRocket to support TLS 1.3
|
||||
%x(patch Pods/SocketRocket/SocketRocket/SRSecurityPolicy.m -N < patches/ws-tls13.diff)
|
||||
|
||||
end
|
||||
|
||||
1254
ios/Podfile.lock
1254
ios/Podfile.lock
File diff suppressed because it is too large
Load Diff
@@ -17,7 +17,7 @@
|
||||
0BEA5C3B1F7B8F73000D0AB4 /* ComplicationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0BEA5C3A1F7B8F73000D0AB4 /* ComplicationController.swift */; };
|
||||
0BEA5C3D1F7B8F73000D0AB4 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 0BEA5C3C1F7B8F73000D0AB4 /* Assets.xcassets */; };
|
||||
0BEA5C411F7B8F73000D0AB4 /* JitsiMeetCompanion.app in Embed Watch Content */ = {isa = PBXBuildFile; fileRef = 0BEA5C251F7B8F73000D0AB4 /* JitsiMeetCompanion.app */; };
|
||||
13B07FBD1A68108700A75B9A /* LaunchScreen.xib in Resources */ = {isa = PBXBuildFile; fileRef = 13B07FB11A68108700A75B9A /* LaunchScreen.xib */; };
|
||||
13B07FBD1A68108700A75B9A /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 13B07FB11A68108700A75B9A /* LaunchScreen.storyboard */; };
|
||||
13B07FBF1A68108700A75B9A /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 13B07FB51A68108700A75B9A /* Images.xcassets */; };
|
||||
2681BB562C7A0B42CFBA6719 /* libPods-JitsiMeet.a in Frameworks */ = {isa = PBXBuildFile; fileRef = D6152FF9E9F7B0E86F70A21D /* libPods-JitsiMeet.a */; };
|
||||
361974E2A13624D7735D619D /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = 5C1BE20ECD5DEEB48FED90B5 /* PrivacyInfo.xcprivacy */; };
|
||||
@@ -132,7 +132,7 @@
|
||||
0BEA5C3C1F7B8F73000D0AB4 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
|
||||
0BEA5C3E1F7B8F73000D0AB4 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
||||
13B07F961A680F5B00A75B9A /* jitsi-meet.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "jitsi-meet.app"; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
13B07FB21A68108700A75B9A /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/LaunchScreen.xib; sourceTree = "<group>"; };
|
||||
13B07FB21A68108700A75B9A /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = src/Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; };
|
||||
13B07FB51A68108700A75B9A /* Images.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Images.xcassets; sourceTree = "<group>"; };
|
||||
13B07FB61A68108700A75B9A /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
||||
3E0F4ED943C0B12BE77F6B45 /* Pods-JitsiMeet.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-JitsiMeet.release.xcconfig"; path = "Target Support Files/Pods-JitsiMeet/Pods-JitsiMeet.release.xcconfig"; sourceTree = "<group>"; };
|
||||
@@ -250,7 +250,6 @@
|
||||
DEA0B7132D7EF7590062A9F6 /* AppDelegate.swift */,
|
||||
13B07FB51A68108700A75B9A /* Images.xcassets */,
|
||||
13B07FB61A68108700A75B9A /* Info.plist */,
|
||||
13B07FB11A68108700A75B9A /* LaunchScreen.xib */,
|
||||
DEA0B7112D7EF16E0062A9F6 /* ViewController.swift */,
|
||||
);
|
||||
path = src;
|
||||
@@ -475,7 +474,7 @@
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
13B07FBF1A68108700A75B9A /* Images.xcassets in Resources */,
|
||||
13B07FBD1A68108700A75B9A /* LaunchScreen.xib in Resources */,
|
||||
13B07FBD1A68108700A75B9A /* LaunchScreen.storyboard in Resources */,
|
||||
361974E2A13624D7735D619D /* PrivacyInfo.xcprivacy in Resources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
@@ -687,12 +686,12 @@
|
||||
name = Interface.storyboard;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
13B07FB11A68108700A75B9A /* LaunchScreen.xib */ = {
|
||||
13B07FB11A68108700A75B9A /* LaunchScreen.storyboard */ = {
|
||||
isa = PBXVariantGroup;
|
||||
children = (
|
||||
13B07FB21A68108700A75B9A /* Base */,
|
||||
);
|
||||
name = LaunchScreen.xib;
|
||||
name = LaunchScreen.storyboard;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
/* End PBXVariantGroup section */
|
||||
|
||||
@@ -37,7 +37,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
|
||||
|
||||
let vc = ViewController()
|
||||
self.window?.rootViewController = vc
|
||||
jitsiMeet.showSplashScreen(vc.view)
|
||||
jitsiMeet.showSplashScreen()
|
||||
|
||||
self.window?.makeKeyAndVisible()
|
||||
|
||||
|
||||
42
ios/app/src/Base.lproj/LaunchScreen.storyboard
Normal file
42
ios/app/src/Base.lproj/LaunchScreen.storyboard
Normal file
@@ -0,0 +1,42 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="23504" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" launchScreen="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="01J-lp-oVM">
|
||||
<device id="retina4_7" orientation="portrait" appearance="light"/>
|
||||
<dependencies>
|
||||
<deployment identifier="iOS"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="23506"/>
|
||||
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
|
||||
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
||||
</dependencies>
|
||||
<scenes>
|
||||
<!--View Controller-->
|
||||
<scene sceneID="EHf-IW-A2E">
|
||||
<objects>
|
||||
<viewController id="01J-lp-oVM" sceneMemberID="viewController">
|
||||
<view key="view" contentMode="scaleToFill" id="iN0-l3-epB">
|
||||
<rect key="frame" x="0.0" y="0.0" width="480" height="480"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
<subviews>
|
||||
<imageView opaque="NO" userInteractionEnabled="NO" contentMode="center" image="LaunchScreen" translatesAutoresizingMaskIntoConstraints="NO" id="4B8-Xf-NDE">
|
||||
<rect key="frame" x="0.0" y="0.0" width="480" height="480"/>
|
||||
</imageView>
|
||||
</subviews>
|
||||
<color key="backgroundColor" red="0.090196078431372548" green="0.62745098039215685" blue="0.85882352941176465" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="bottom" secondItem="4B8-Xf-NDE" secondAttribute="bottom" id="aFF-BR-glX"/>
|
||||
<constraint firstItem="4B8-Xf-NDE" firstAttribute="leading" secondItem="iN0-l3-epB" secondAttribute="leading" id="glR-YN-1GF"/>
|
||||
<constraint firstAttribute="trailing" secondItem="4B8-Xf-NDE" secondAttribute="trailing" id="tva-gl-jRX"/>
|
||||
<constraint firstItem="4B8-Xf-NDE" firstAttribute="top" secondItem="iN0-l3-epB" secondAttribute="top" id="yaV-1V-oEh"/>
|
||||
</constraints>
|
||||
<nil key="simulatedStatusBarMetrics"/>
|
||||
<freeformSimulatedSizeMetrics key="simulatedDestinationMetrics"/>
|
||||
<point key="canvasLocation" x="548" y="455"/>
|
||||
</view>
|
||||
</viewController>
|
||||
<placeholder placeholderIdentifier="IBFirstResponder" id="iYj-Kq-Ea1" userLabel="First Responder" sceneMemberID="firstResponder"/>
|
||||
</objects>
|
||||
<resources>
|
||||
<image name="LaunchScreen" width="480" height="480"/>
|
||||
</resources>
|
||||
</scene>
|
||||
</scenes>
|
||||
</document>
|
||||
@@ -1,35 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="23504" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" launchScreen="YES" useTraitCollections="YES" colorMatched="YES">
|
||||
<device id="retina6_12" orientation="portrait" appearance="light"/>
|
||||
<dependencies>
|
||||
<deployment identifier="iOS"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="23506"/>
|
||||
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
||||
</dependencies>
|
||||
<objects>
|
||||
<placeholder placeholderIdentifier="IBFilesOwner" id="-1" userLabel="File's Owner"/>
|
||||
<placeholder placeholderIdentifier="IBFirstResponder" id="-2" customClass="UIResponder"/>
|
||||
<view contentMode="scaleToFill" id="iN0-l3-epB">
|
||||
<rect key="frame" x="0.0" y="0.0" width="480" height="480"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
<subviews>
|
||||
<imageView opaque="NO" userInteractionEnabled="NO" contentMode="center" image="LaunchScreen" translatesAutoresizingMaskIntoConstraints="NO" id="4B8-Xf-NDE">
|
||||
<rect key="frame" x="0.0" y="0.0" width="480" height="480"/>
|
||||
</imageView>
|
||||
</subviews>
|
||||
<color key="backgroundColor" red="0.090196078431372548" green="0.62745098039215685" blue="0.85882352941176465" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="bottom" secondItem="4B8-Xf-NDE" secondAttribute="bottom" id="aFF-BR-glX"/>
|
||||
<constraint firstItem="4B8-Xf-NDE" firstAttribute="leading" secondItem="iN0-l3-epB" secondAttribute="leading" id="glR-YN-1GF"/>
|
||||
<constraint firstAttribute="trailing" secondItem="4B8-Xf-NDE" secondAttribute="trailing" id="tva-gl-jRX"/>
|
||||
<constraint firstItem="4B8-Xf-NDE" firstAttribute="top" secondItem="iN0-l3-epB" secondAttribute="top" id="yaV-1V-oEh"/>
|
||||
</constraints>
|
||||
<nil key="simulatedStatusBarMetrics"/>
|
||||
<freeformSimulatedSizeMetrics key="simulatedDestinationMetrics"/>
|
||||
<point key="canvasLocation" x="548" y="455"/>
|
||||
</view>
|
||||
</objects>
|
||||
<resources>
|
||||
<image name="LaunchScreen" width="480" height="480"/>
|
||||
</resources>
|
||||
</document>
|
||||
@@ -90,7 +90,7 @@
|
||||
<string>LaunchScreen</string>
|
||||
<key>UIRequiredDeviceCapabilities</key>
|
||||
<array>
|
||||
<string>armv7</string>
|
||||
<string>arm64</string>
|
||||
</array>
|
||||
<key>UIStatusBarStyle</key>
|
||||
<string>UIStatusBarStyleLightContent</string>
|
||||
|
||||
@@ -100,6 +100,9 @@ typedef NS_ENUM(NSInteger, WebRTCLoggingSeverity) {
|
||||
|
||||
- (BOOL)isCrashReportingDisabled;
|
||||
|
||||
- (void)showSplashScreen:(UIView * _Nonnull) rootView;
|
||||
/**
|
||||
* Shows the splash screen.
|
||||
*/
|
||||
- (void)showSplashScreen;
|
||||
|
||||
@end
|
||||
|
||||
@@ -23,7 +23,6 @@
|
||||
#import "JitsiMeetView+Private.h"
|
||||
#import "RCTBridgeWrapper.h"
|
||||
#import "ReactUtils.h"
|
||||
#import "RNSplashScreen.h"
|
||||
#import "ScheenshareEventEmiter.h"
|
||||
|
||||
#import <react-native-webrtc/WebRTCModuleOptions.h>
|
||||
@@ -221,8 +220,17 @@
|
||||
return nil;
|
||||
}
|
||||
|
||||
- (void)showSplashScreen:(UIView*)rootView {
|
||||
[RNSplashScreen showSplash:@"LaunchScreen" inRootView:rootView];
|
||||
- (void)showSplashScreen {
|
||||
Class splashClass = NSClassFromString(@"SplashView");
|
||||
if (splashClass && [splashClass respondsToSelector:@selector(sharedInstance)]) {
|
||||
id splashInstance = [splashClass performSelector:@selector(sharedInstance)];
|
||||
if (splashInstance && [splashInstance respondsToSelector:@selector(showSplash)]) {
|
||||
[splashInstance performSelector:@selector(showSplash)];
|
||||
NSLog(@"✅ Splash Screen Shown Successfully");
|
||||
}
|
||||
} else {
|
||||
NSLog(@"⚠️ SplashView module not found");
|
||||
}
|
||||
}
|
||||
|
||||
#pragma mark - Property getter / setters
|
||||
|
||||
@@ -109,6 +109,7 @@
|
||||
}
|
||||
},
|
||||
"chat": {
|
||||
"disabled": "L'invio di messaggi in chat è disabilitato.",
|
||||
"enter": "Entra nella conversazione",
|
||||
"error": "Errore: il tuo messaggio non è stato inviato. Motivo: {{error}}",
|
||||
"fieldPlaceHolder": "Scrivi qui il tuo messaggio",
|
||||
@@ -546,8 +547,10 @@
|
||||
"downloadFailedDescription": "Si prega di riprovare.",
|
||||
"downloadFailedTitle": "Download non riuscito",
|
||||
"downloadFile": "Download",
|
||||
"dragAndDrop": "Trascina e rilascia i file qui",
|
||||
"fileAlreadyUploaded": "Questo file è già stato caricato nella riunione",
|
||||
"dragAndDrop": "Trascina e rilascia i file qui o da qualsiasi altra parte nella schermata",
|
||||
"fileAlreadyUploaded": "Questo file è già stato caricato nella riunione.",
|
||||
"fileTooLargeDescription": "Assicurati che il file non superi {{ maxFileSize }}.",
|
||||
"fileTooLargeTitle": "Il file selezionato è troppo grande",
|
||||
"removeFile": "Rimuovi",
|
||||
"uploadFailedDescription": "Si prega di riprovare.",
|
||||
"uploadFailedTitle": "Caricamento non riuscito",
|
||||
@@ -906,6 +909,7 @@
|
||||
"visitorInQueue": " ({{count}} in attesa)",
|
||||
"visitorRequests": " ({{count}} richiesta/e)",
|
||||
"visitors": "Spettatori {{count}}",
|
||||
"visitorsList": "Spettatori ({{count}})",
|
||||
"waitingLobby": "({{count}}) in attesa"
|
||||
},
|
||||
"search": "Cerca partecipanti",
|
||||
@@ -946,7 +950,7 @@
|
||||
},
|
||||
"results": {
|
||||
"changeVote": "Cambia voto",
|
||||
"empty": "Non ci sono ancora sondaggi in questa riunione. Crea un sondaggio qui!",
|
||||
"empty": "Non ci sono ancora sondaggi in questa riunione.",
|
||||
"hideDetailedResults": "Nascondi dettagli",
|
||||
"showDetailedResults": "Mostra dettagli",
|
||||
"vote": "Voti"
|
||||
|
||||
@@ -300,6 +300,12 @@
|
||||
"alreadySharedVideoTitle": "Only one shared video is allowed at a time",
|
||||
"applicationWindow": "Application window",
|
||||
"authenticationRequired": "Authentication required",
|
||||
"cameraCaptureDialog": {
|
||||
"description": "Take and send a picture using your mobile camera",
|
||||
"ok": "Open camera",
|
||||
"reject": "Not now",
|
||||
"title": "Take a picture"
|
||||
},
|
||||
"cameraConstraintFailedError": "Your camera does not satisfy some of the required constraints.",
|
||||
"cameraNotFoundError": "Camera was not found.",
|
||||
"cameraNotSendingData": "We are unable to access your camera. Please check if another application is using this device, select another device from the settings menu or try to reload the application.",
|
||||
@@ -375,22 +381,34 @@
|
||||
"micTimeoutError": "Could not start audio source. Timeout occurred!",
|
||||
"micUnknownError": "Cannot use microphone for an unknown reason.",
|
||||
"moderationAudioLabel": "Allow attendees to unmute themselves",
|
||||
"moderationDesktopLabel": "Allow non-moderators to share their screen",
|
||||
"moderationVideoLabel": "Allow non-moderators to start their video",
|
||||
"muteEveryoneDialog": "The participants can unmute themselves at any time.",
|
||||
"muteEveryoneDialogModerationOn": "The participants can send a request to speak at any time.",
|
||||
"muteEveryoneElseDialog": "Once muted, you won't be able to unmute them, but they can unmute themselves at any time.",
|
||||
"muteEveryoneElseTitle": "Mute everyone except {{whom}}?",
|
||||
"muteEveryoneElsesDesktopDialog": "Once the share is stopped, you won't be able to restart it, but they can do so at any time.",
|
||||
"muteEveryoneElsesDesktopTitle": "Stop everyone's screen-share except {{whom}}?",
|
||||
"muteEveryoneElsesVideoDialog": "Once the camera is disabled, you won't be able to turn it back on, but they can turn it back on at any time.",
|
||||
"muteEveryoneElsesVideoTitle": "Stop everyone's video except {{whom}}?",
|
||||
"muteEveryoneSelf": "yourself",
|
||||
"muteEveryoneStartMuted": "Everyone starts muted from now on",
|
||||
"muteEveryoneTitle": "Mute everyone?",
|
||||
"muteEveryonesDesktopDialog": "The participants can share their screen at any time.",
|
||||
"muteEveryonesDesktopDialogModerationOn": "The participants can send a request to share their screen at any time.",
|
||||
"muteEveryonesDesktopTitle": "Stop everyone's screen share?",
|
||||
"muteEveryonesVideoDialog": "The participants can turn on their video at any time.",
|
||||
"muteEveryonesVideoDialogModerationOn": "The participants can send a request to turn on their video at any time.",
|
||||
"muteEveryonesVideoDialogOk": "Disable",
|
||||
"muteEveryonesVideoTitle": "Stop everyone's video?",
|
||||
"muteParticipantBody": "You won't be able to unmute them, but they can unmute themselves at any time.",
|
||||
"muteParticipantButton": "Mute",
|
||||
"muteParticipantsDesktopBody": "You won't be able to start their screen-share, but they can do so at any time.",
|
||||
"muteParticipantsDesktopBodyModerationOn": "You won't be able to start their screen-share and neither will they.",
|
||||
"muteParticipantsDesktopButton": "Stop screen sharing",
|
||||
"muteParticipantsDesktopDialog": "Are you sure you want to turn off this participant's screen-share? You won't be able to restart it, but they can do so at any time.",
|
||||
"muteParticipantsDesktopDialogModerationOn": "Are you sure you want to turn off this participant's screen-share? You won't be able to turn the screen back on and neither will they.",
|
||||
"muteParticipantsDesktopTitle": "Disable screen-share of this participant?",
|
||||
"muteParticipantsVideoBody": "You won't be able to turn the camera back on, but they can turn it back on at any time.",
|
||||
"muteParticipantsVideoBodyModerationOn": "You won't be able to turn the camera back on and neither will they.",
|
||||
"muteParticipantsVideoButton": "Stop video",
|
||||
@@ -767,8 +785,9 @@
|
||||
"me": "me",
|
||||
"notify": {
|
||||
"OldElectronAPPTitle": "Security vulnerability!",
|
||||
"allowAll": "Allow All",
|
||||
"allowAudio": "Allow Audio",
|
||||
"allowBoth": "Both",
|
||||
"allowDesktop": "Allow screen sharing",
|
||||
"allowVideo": "Allow Video",
|
||||
"allowedUnmute": "You can unmute your microphone, start your camera or share your screen.",
|
||||
"audioUnmuteBlockedDescription": "Mic unmute operation has been temporarily blocked because of system limits.",
|
||||
@@ -782,6 +801,7 @@
|
||||
"dataChannelClosedDescription": "The bridge channel is down and thus video quality may be limited to its lowest setting.",
|
||||
"dataChannelClosedDescriptionWithAudio": "The bridge channel is down and thus disruptions to audio and video may occur.",
|
||||
"dataChannelClosedWithAudio": "Audio and video quality may be impaired",
|
||||
"desktopMutedRemotelyTitle": "Your screen sharing has been stopped by {{participantDisplayName}}",
|
||||
"disabledIframe": "Embedding is only meant for demo purposes, so this call will disconnect in {{timeout}} minutes.",
|
||||
"disabledIframeSecondaryNative": "Embedding {{domain}} is only meant for demo purposes, so this call will disconnect in {{timeout}} minutes.",
|
||||
"disabledIframeSecondaryWeb": "Embedding {{domain}} is only meant for demo purposes, so this call will disconnect in {{timeout}} minutes. Please use <a href='{{jaasDomain}}' rel='noopener noreferrer' target='_blank'>Jitsi as a Service</a> for production embedding!",
|
||||
@@ -862,6 +882,7 @@
|
||||
"suggestRecordingDescription": "Would you like to start a recording?",
|
||||
"suggestRecordingTitle": "Record this meeting",
|
||||
"unmute": "Unmute Audio",
|
||||
"unmuteScreen": "Start screen sharing",
|
||||
"unmuteVideo": "Unmute Video",
|
||||
"videoMutedRemotelyDescription": "You can always turn it on again.",
|
||||
"videoMutedRemotelyTitle": "Your video has been turned off by {{participantDisplayName}}",
|
||||
@@ -881,11 +902,14 @@
|
||||
"admit": "Admit",
|
||||
"admitAll": "Admit all",
|
||||
"allow": "Allow non-moderators to:",
|
||||
"allowDesktop": "Allow screen sharing",
|
||||
"allowVideo": "Allow video",
|
||||
"askDesktop": "Ask to share screen",
|
||||
"askUnmute": "Ask to unmute",
|
||||
"audioModeration": "Unmute themselves",
|
||||
"blockEveryoneMicCamera": "Block everyone's mic and camera",
|
||||
"breakoutRooms": "Breakout rooms",
|
||||
"desktopModeration": "Start screen sharing",
|
||||
"goLive": "Go live",
|
||||
"invite": "Invite someone",
|
||||
"lowerAllHands": "Lower all hands",
|
||||
@@ -897,6 +921,8 @@
|
||||
"muteAll": "Mute all",
|
||||
"muteEveryoneElse": "Mute everyone else",
|
||||
"reject": "Reject",
|
||||
"stopDesktop": "Stop screen sharing",
|
||||
"stopEveryonesDesktop": "Stop everyone's screen-share",
|
||||
"stopEveryonesVideo": "Stop everyone's video",
|
||||
"stopVideo": "Stop video",
|
||||
"unblockEveryoneMicCamera": "Unblock everyone's mic and camera",
|
||||
@@ -906,9 +932,11 @@
|
||||
"headings": {
|
||||
"lobby": "Lobby ({{count}})",
|
||||
"participantsList": "Meeting participants ({{count}})",
|
||||
"viewerRequests": "Viewers requests {{count}}",
|
||||
"visitorInQueue": " (waiting {{count}})",
|
||||
"visitorRequests": " (requests {{count}})",
|
||||
"visitors": "Viewers {{count}}",
|
||||
"visitorsList": "Viewers ({{count}})",
|
||||
"waitingLobby": "Waiting in lobby ({{count}})"
|
||||
},
|
||||
"search": "Search participants",
|
||||
@@ -1501,6 +1529,8 @@
|
||||
"connectionInfo": "Connection Info",
|
||||
"demote": "Move to viewer",
|
||||
"domute": "Mute",
|
||||
"domuteDesktop": "Stop screen-sharing",
|
||||
"domuteDesktopOfOthers": "Stop screen-sharing for everyone else",
|
||||
"domuteOthers": "Mute everyone else",
|
||||
"domuteVideo": "Disable camera",
|
||||
"domuteVideoOfOthers": "Disable camera of everyone else",
|
||||
|
||||
@@ -13,7 +13,7 @@ import {
|
||||
requestEnableAudioModeration,
|
||||
requestEnableVideoModeration
|
||||
} from '../../react/features/av-moderation/actions';
|
||||
import { isEnabledFromState } from '../../react/features/av-moderation/functions';
|
||||
import { isEnabledFromState, isForceMuted } from '../../react/features/av-moderation/functions';
|
||||
import { setAudioOnly } from '../../react/features/base/audio-only/actions';
|
||||
import {
|
||||
endConference,
|
||||
@@ -30,6 +30,7 @@ import { overwriteConfig } from '../../react/features/base/config/actions';
|
||||
import { getWhitelistedJSON } from '../../react/features/base/config/functions.any';
|
||||
import { toggleDialog } from '../../react/features/base/dialog/actions';
|
||||
import { isSupportedBrowser } from '../../react/features/base/environment/environment';
|
||||
import { isMobileBrowser } from '../../react/features/base/environment/utils';
|
||||
import { parseJWTFromURLParams } from '../../react/features/base/jwt/functions';
|
||||
import JitsiMeetJS, { JitsiRecordingConstants } from '../../react/features/base/lib-jitsi-meet';
|
||||
import { MEDIA_TYPE, VIDEO_TYPE } from '../../react/features/base/media/constants';
|
||||
@@ -106,14 +107,17 @@ import {
|
||||
close as closeParticipantsPane,
|
||||
open as openParticipantsPane
|
||||
} from '../../react/features/participants-pane/actions';
|
||||
import { getParticipantsPaneOpen, isForceMuted } from '../../react/features/participants-pane/functions';
|
||||
import { getParticipantsPaneOpen } from '../../react/features/participants-pane/functions';
|
||||
import { startLocalVideoRecording, stopLocalVideoRecording } from '../../react/features/recording/actions.any';
|
||||
import { grantRecordingConsent, grantRecordingConsentAndUnmute } from '../../react/features/recording/actions.web';
|
||||
import { RECORDING_METADATA_ID, RECORDING_TYPES } from '../../react/features/recording/constants';
|
||||
import { getActiveSession, supportsLocalRecording } from '../../react/features/recording/functions';
|
||||
import { startAudioScreenShareFlow, startScreenShareFlow } from '../../react/features/screen-share/actions';
|
||||
import { isScreenAudioSupported } from '../../react/features/screen-share/functions';
|
||||
import { toggleScreenshotCaptureSummary } from '../../react/features/screenshot-capture/actions';
|
||||
import {
|
||||
openCameraCaptureDialog,
|
||||
toggleScreenshotCaptureSummary
|
||||
} from '../../react/features/screenshot-capture/actions';
|
||||
import { isScreenshotCaptureEnabled } from '../../react/features/screenshot-capture/functions';
|
||||
import SettingsDialog from '../../react/features/settings/components/web/SettingsDialog';
|
||||
import { SETTINGS_TABS } from '../../react/features/settings/constants';
|
||||
@@ -940,6 +944,20 @@ function initCommands() {
|
||||
});
|
||||
});
|
||||
break;
|
||||
case 'capture-camera-picture' : {
|
||||
const { cameraFacingMode, descriptionText, titleText } = request;
|
||||
|
||||
if (!isMobileBrowser()) {
|
||||
logger.error('This feature is only supported on mobile');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
APP.store.dispatch(openCameraCaptureDialog(callback, { cameraFacingMode,
|
||||
descriptionText,
|
||||
titleText }));
|
||||
break;
|
||||
}
|
||||
case 'deployment-info':
|
||||
callback(APP.store.getState()['features/base/config'].deploymentInfo);
|
||||
break;
|
||||
|
||||
21
modules/API/external/external_api.js
vendored
21
modules/API/external/external_api.js
vendored
@@ -820,6 +820,27 @@ export default class JitsiMeetExternalAPI extends EventEmitter {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Captures a picture through OS camera.
|
||||
*
|
||||
* @param {string} cameraFacingMode - The OS camera facing mode (environment/user).
|
||||
* @param {string} descriptionText - The OS camera facing mode (environment/user).
|
||||
* @param {string} titleText - The OS camera facing mode (environment/user).
|
||||
* @returns {Promise<string>} - Resolves with a base64 encoded image data of the screenshot.
|
||||
*/
|
||||
captureCameraPicture(
|
||||
cameraFacingMode,
|
||||
descriptionText,
|
||||
titleText
|
||||
) {
|
||||
return this._transport.sendRequest({
|
||||
name: 'capture-camera-picture',
|
||||
cameraFacingMode,
|
||||
descriptionText,
|
||||
titleText
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes the listeners and removes the Jitsi Meet frame.
|
||||
*
|
||||
|
||||
6441
package-lock.json
generated
6441
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
46
package.json
46
package.json
@@ -23,7 +23,7 @@
|
||||
"@emotion/styled": "11.10.6",
|
||||
"@giphy/js-fetch-api": "4.9.3",
|
||||
"@giphy/react-components": "6.9.4",
|
||||
"@giphy/react-native-sdk": "2.3.0",
|
||||
"@giphy/react-native-sdk": "3.3.1",
|
||||
"@jitsi/excalidraw": "https://github.com/jitsi/excalidraw/releases/download/v0.0.19/jitsi-excalidraw-0.0.19.tgz",
|
||||
"@jitsi/js-utils": "2.2.1",
|
||||
"@jitsi/logger": "2.0.2",
|
||||
@@ -34,13 +34,14 @@
|
||||
"@react-native-async-storage/async-storage": "1.23.1",
|
||||
"@react-native-clipboard/clipboard": "1.14.3",
|
||||
"@react-native-community/netinfo": "11.1.0",
|
||||
"@react-native-community/slider": "4.4.3",
|
||||
"@react-native-community/slider": "4.5.6",
|
||||
"@react-native-google-signin/google-signin": "10.1.0",
|
||||
"@react-navigation/bottom-tabs": "6.6.0",
|
||||
"@react-navigation/elements": "1.3.30",
|
||||
"@react-navigation/material-top-tabs": "6.6.13",
|
||||
"@react-navigation/native": "6.1.17",
|
||||
"@react-navigation/stack": "6.4.0",
|
||||
"@sayem314/react-native-keep-awake": "1.3.1",
|
||||
"@stomp/stompjs": "7.0.0",
|
||||
"@svgr/webpack": "6.3.1",
|
||||
"@tensorflow/tfjs-backend-wasm": "3.13.0",
|
||||
@@ -69,7 +70,7 @@
|
||||
"js-md5": "0.6.1",
|
||||
"js-sha512": "0.8.0",
|
||||
"jwt-decode": "2.2.0",
|
||||
"lib-jitsi-meet": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v2018.0.0+1773bcff/lib-jitsi-meet.tgz",
|
||||
"lib-jitsi-meet": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v2033.0.0+bf3e3a8e/lib-jitsi-meet.tgz",
|
||||
"lodash-es": "4.17.21",
|
||||
"null-loader": "4.0.1",
|
||||
"optional-require": "1.0.3",
|
||||
@@ -82,35 +83,35 @@
|
||||
"react-focus-on": "3.8.1",
|
||||
"react-i18next": "10.11.4",
|
||||
"react-linkify": "1.0.0-alpha",
|
||||
"react-native": "0.75.5",
|
||||
"react-native-background-timer": "2.4.1",
|
||||
"react-native-calendar-events": "2.2.0",
|
||||
"react-native-default-preference": "1.4.4",
|
||||
"react-native": "0.77.2",
|
||||
"react-native-background-timer": "https://github.com/jitsi/react-native-background-timer.git#d180dfaa4486ae3ee17d01242db92cb3195f4718",
|
||||
"react-native-calendar-events": "https://github.com/jitsi/react-native-calendar-events.git#47f068dedfed7c0f72042e093f688eb11624eb7b",
|
||||
"react-native-default-preference": "https://github.com/jitsi/react-native-default-preference.git#c9bf63bdc058e3fa2aa0b87b1ee1af240f44ed02",
|
||||
"react-native-device-info": "10.9.0",
|
||||
"react-native-dialog": "https://github.com/jitsi/react-native-dialog/releases/download/v9.2.2-jitsi.1/react-native-dialog-9.2.2.tgz",
|
||||
"react-native-gesture-handler": "2.18.1",
|
||||
"react-native-get-random-values": "1.9.0",
|
||||
"react-native-immersive-mode": "2.0.2",
|
||||
"react-native-keep-awake": "4.0.0",
|
||||
"react-native-gesture-handler": "2.24.0",
|
||||
"react-native-get-random-values": "1.11.0",
|
||||
"react-native-immersive-mode": "https://github.com/jitsi/react-native-immersive-mode.git#38cc9001db24618bc0c61800f81e889bcfb6ff2c",
|
||||
"react-native-orientation-locker": "1.6.0",
|
||||
"react-native-pager-view": "6.4.1",
|
||||
"react-native-paper": "5.10.3",
|
||||
"react-native-performance": "5.0.0",
|
||||
"react-native-safe-area-context": "4.12.0",
|
||||
"react-native-screens": "3.35.0",
|
||||
"react-native-sound": "0.11.2",
|
||||
"react-native-splash-screen": "3.3.0",
|
||||
"react-native-svg": "13.13.0",
|
||||
"react-native-performance": "5.1.2",
|
||||
"react-native-safe-area-context": "5.4.0",
|
||||
"react-native-screens": "4.11.1",
|
||||
"react-native-sound": "https://github.com/jitsi/react-native-sound.git#ea13c97b5c2a4ff5e0d9bacbd9ff5e4457fe2c3c",
|
||||
"react-native-splash-view": "0.0.18",
|
||||
"react-native-svg": "15.11.2",
|
||||
"react-native-svg-transformer": "1.2.0",
|
||||
"react-native-tab-view": "3.5.2",
|
||||
"react-native-url-polyfill": "2.0.0",
|
||||
"react-native-video": "6.0.0-alpha.11",
|
||||
"react-native-video": "6.13.0",
|
||||
"react-native-watch-connectivity": "1.1.0",
|
||||
"react-native-webrtc": "124.0.4",
|
||||
"react-native-webview": "13.8.7",
|
||||
"react-native-webview": "13.13.5",
|
||||
"react-native-youtube-iframe": "2.3.0",
|
||||
"react-redux": "7.2.9",
|
||||
"react-textarea-autosize": "8.3.0",
|
||||
"react-virtualized-auto-sizer": "^1.0.26",
|
||||
"react-window": "1.8.6",
|
||||
"react-youtube": "10.1.0",
|
||||
"redux": "4.0.4",
|
||||
@@ -132,7 +133,11 @@
|
||||
"@babel/preset-env": "7.25.9",
|
||||
"@babel/preset-react": "7.25.9",
|
||||
"@jitsi/eslint-config": "6.0.4",
|
||||
"@react-native/metro-config": "0.75.5",
|
||||
"@react-native-community/cli": "15.0.1",
|
||||
"@react-native-community/cli-platform-android": "15.0.1",
|
||||
"@react-native-community/cli-platform-ios": "15.0.1",
|
||||
"@react-native/babel-preset": "0.77.2",
|
||||
"@react-native/metro-config": "0.77.2",
|
||||
"@types/amplitude-js": "8.16.5",
|
||||
"@types/audioworklet": "0.0.29",
|
||||
"@types/dom-screen-wake-lock": "1.0.1",
|
||||
@@ -176,7 +181,6 @@
|
||||
"eslint-plugin-typescript-sort-keys": "3.3.0",
|
||||
"jetifier": "1.6.4",
|
||||
"jsonwebtoken": "9.0.2",
|
||||
"metro-react-native-babel-preset": "0.77.0",
|
||||
"patch-package": "6.4.7",
|
||||
"pretty": "2.0.0",
|
||||
"process": "0.11.10",
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
diff --git a/node_modules/@giphy/react-native-sdk/giphy-react-native-sdk.podspec b/node_modules/@giphy/react-native-sdk/giphy-react-native-sdk.podspec
|
||||
index ddd41ac..a7b143e 100644
|
||||
--- a/node_modules/@giphy/react-native-sdk/giphy-react-native-sdk.podspec
|
||||
+++ b/node_modules/@giphy/react-native-sdk/giphy-react-native-sdk.podspec
|
||||
@@ -16,5 +16,5 @@ Pod::Spec.new do |s|
|
||||
s.source_files = "ios/**/*.{h,m,mm,swift}"
|
||||
|
||||
s.dependency "React-Core"
|
||||
- s.dependency "Giphy", "2.2.4"
|
||||
+ s.dependency "Giphy", "2.2.12"
|
||||
end
|
||||
@@ -5,8 +5,9 @@ buildscript {
|
||||
}
|
||||
|
||||
dependencies {
|
||||
classpath 'com.android.tools.build:gradle:3.5.3'
|
||||
classpath "com.android.tools.build:gradle:$rootProject.ext.gradlePluginVersion"
|
||||
}
|
||||
namespace 'org.jitsi.meet.reactnativesdk'
|
||||
}
|
||||
|
||||
def isNewArchitectureEnabled() {
|
||||
@@ -46,8 +47,8 @@ android {
|
||||
}
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility JavaVersion.VERSION_1_8
|
||||
targetCompatibility JavaVersion.VERSION_1_8
|
||||
sourceCompatibility JavaVersion.VERSION_17
|
||||
targetCompatibility JavaVersion.VERSION_17
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
JitsiMeetReactNative_kotlinVersion=1.7.0
|
||||
JitsiMeetReactNative_minSdkVersion=21
|
||||
JitsiMeetReactNative_targetSdkVersion=31
|
||||
JitsiMeetReactNative_compileSdkVersion=31
|
||||
JitsiMeetReactNative_ndkversion=21.4.7075529
|
||||
JitsiMeetReactNative_kotlinVersion=2.0.21
|
||||
JitsiMeetReactNative_minSdkVersion=26
|
||||
JitsiMeetReactNative_targetSdkVersion=34
|
||||
JitsiMeetReactNative_compileSdkVersion=34
|
||||
JitsiMeetReactNative_ndkversion=27.1.12297006
|
||||
|
||||
@@ -64,7 +64,8 @@
|
||||
"@react-native-community/netinfo": "0.0.0",
|
||||
"@react-native-community/slider": "0.0.0",
|
||||
"@react-native-google-signin/google-signin": "0.0.0",
|
||||
"react-native": "*",
|
||||
"@sayem314/react-native-keep-awake": "0.0.0",
|
||||
"react-native": "0.0.0",
|
||||
"react": "*",
|
||||
"react-native-background-timer": "0.0.0",
|
||||
"react-native-calendar-events": "0.0.0",
|
||||
@@ -73,14 +74,13 @@
|
||||
"react-native-get-random-values": "0.0.0",
|
||||
"react-native-gesture-handler": "0.0.0",
|
||||
"react-native-immersive-mode": "0.0.0",
|
||||
"react-native-keep-awake": "0.0.0",
|
||||
"react-native-pager-view": "0.0.0",
|
||||
"react-native-performance": "0.0.0",
|
||||
"react-native-orientation-locker": "0.0.0",
|
||||
"react-native-safe-area-context": "0.0.0",
|
||||
"react-native-screens": "0.0.0",
|
||||
"react-native-sound": "0.0.0",
|
||||
"react-native-splash-screen": "0.0.0",
|
||||
"react-native-splash-view": "0.0.0",
|
||||
"react-native-svg": "0.0.0",
|
||||
"react-native-video": "0.0.0",
|
||||
"react-native-watch-connectivity": "0.0.0",
|
||||
|
||||
@@ -63,6 +63,11 @@ export const ACTION_SHORTCUT_TRIGGERED = 'triggered';
|
||||
*/
|
||||
export const AUDIO_MUTE = 'audio.mute';
|
||||
|
||||
/**
|
||||
* The name of the keyboard shortcut or toolbar button for muting desktop sharing.
|
||||
*/
|
||||
export const DESKTOP_MUTE = 'desktop.mute';
|
||||
|
||||
/**
|
||||
* The name of the keyboard shortcut or toolbar button for muting video.
|
||||
*/
|
||||
|
||||
@@ -84,7 +84,6 @@ export async function createHandlers({ getState }: IStore) {
|
||||
} = config;
|
||||
const {
|
||||
amplitudeAPPKey,
|
||||
amplitudeIncludeUTM,
|
||||
blackListedEvents,
|
||||
scriptURLs,
|
||||
matomoEndpoint,
|
||||
@@ -94,7 +93,6 @@ export async function createHandlers({ getState }: IStore) {
|
||||
const { group, user } = state['features/base/jwt'];
|
||||
const handlerConstructorOptions = {
|
||||
amplitudeAPPKey,
|
||||
amplitudeIncludeUTM,
|
||||
blackListedEvents,
|
||||
envType: deploymentInfo?.envType || 'dev',
|
||||
matomoEndpoint,
|
||||
|
||||
@@ -11,7 +11,6 @@ export interface IEvent {
|
||||
|
||||
interface IOptions {
|
||||
amplitudeAPPKey?: string;
|
||||
amplitudeIncludeUTM?: boolean;
|
||||
blackListedEvents?: string[];
|
||||
envType?: string;
|
||||
group?: string;
|
||||
|
||||
@@ -4,21 +4,18 @@ import logger from '../logger';
|
||||
|
||||
import AbstractHandler, { IEvent } from './AbstractHandler';
|
||||
import { fixDeviceID } from './amplitude/fixDeviceID';
|
||||
import amplitude from './amplitude/lib';
|
||||
import amplitude, { initAmplitude } from './amplitude/lib';
|
||||
|
||||
/**
|
||||
* Analytics handler for Amplitude.
|
||||
*/
|
||||
export default class AmplitudeHandler extends AbstractHandler {
|
||||
_deviceId: string;
|
||||
_userId: Object;
|
||||
|
||||
/**
|
||||
* Creates new instance of the Amplitude analytics handler.
|
||||
*
|
||||
* @param {Object} options - The amplitude options.
|
||||
* @param {string} options.amplitudeAPPKey - The Amplitude app key required by the Amplitude API.
|
||||
* @param {boolean} options.amplitudeIncludeUTM - Whether to include UTM parameters
|
||||
* @param {string} options.amplitudeAPPKey - The Amplitude app key required by the Amplitude API
|
||||
* in the Amplitude events.
|
||||
*/
|
||||
constructor(options: any) {
|
||||
@@ -26,54 +23,26 @@ export default class AmplitudeHandler extends AbstractHandler {
|
||||
|
||||
const {
|
||||
amplitudeAPPKey,
|
||||
amplitudeIncludeUTM: includeUtm = true,
|
||||
user
|
||||
} = options;
|
||||
|
||||
this._enabled = true;
|
||||
|
||||
const onError = (e: Error) => {
|
||||
logger.error('Error initializing Amplitude', e);
|
||||
this._enabled = false;
|
||||
};
|
||||
|
||||
// Forces sending all events on exit (flushing) via sendBeacon
|
||||
const onExitPage = () => {
|
||||
amplitude.flush();
|
||||
};
|
||||
|
||||
if (navigator.product === 'ReactNative') {
|
||||
amplitude.init(amplitudeAPPKey);
|
||||
fixDeviceID(amplitude).then(() => {
|
||||
const deviceId = amplitude.getDeviceId();
|
||||
|
||||
if (deviceId) {
|
||||
this._deviceId = deviceId;
|
||||
}
|
||||
initAmplitude(amplitudeAPPKey, user)
|
||||
.then(() => {
|
||||
logger.info('Amplitude initialized');
|
||||
fixDeviceID(amplitude);
|
||||
})
|
||||
.catch(e => {
|
||||
logger.error('Error initializing Amplitude', e);
|
||||
this._enabled = false;
|
||||
});
|
||||
} else {
|
||||
const amplitudeOptions: any = {
|
||||
includeReferrer: true,
|
||||
includeUtm,
|
||||
saveParamsReferrerOncePerSession: false,
|
||||
onError,
|
||||
onExitPage
|
||||
};
|
||||
|
||||
amplitude.init(amplitudeAPPKey, undefined, amplitudeOptions);
|
||||
fixDeviceID(amplitude);
|
||||
}
|
||||
|
||||
if (user) {
|
||||
this._userId = user;
|
||||
amplitude.setUserId(user);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the Amplitude user properties.
|
||||
*
|
||||
* @param {Object} userProps - The user portperties.
|
||||
* @param {Object} userProps - The user properties.
|
||||
* @returns {void}
|
||||
*/
|
||||
setUserProperties(userProps: any) {
|
||||
@@ -104,7 +73,7 @@ export default class AmplitudeHandler extends AbstractHandler {
|
||||
|
||||
const eventName = this._extractName(event) ?? '';
|
||||
|
||||
amplitude.logEvent(eventName, event);
|
||||
amplitude.track(eventName, event);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -113,13 +82,6 @@ export default class AmplitudeHandler extends AbstractHandler {
|
||||
* @returns {Object}
|
||||
*/
|
||||
getIdentityProps() {
|
||||
if (navigator.product === 'ReactNative') {
|
||||
return {
|
||||
deviceId: this._deviceId,
|
||||
userId: this._userId
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
sessionId: amplitude.getSessionId(),
|
||||
deviceId: amplitude.getDeviceId(),
|
||||
|
||||
@@ -17,7 +17,7 @@ export async function fixDeviceID(amplitude: Types.ReactNativeClient) {
|
||||
const current = await DefaultPreference.get('amplitudeDeviceId');
|
||||
|
||||
if (current) {
|
||||
await amplitude.setDeviceId(current);
|
||||
amplitude.setDeviceId(current);
|
||||
} else {
|
||||
const uid = await getUniqueId();
|
||||
|
||||
@@ -27,7 +27,7 @@ export async function fixDeviceID(amplitude: Types.ReactNativeClient) {
|
||||
return;
|
||||
}
|
||||
|
||||
await amplitude.setDeviceId(uid as string);
|
||||
amplitude.setDeviceId(uid as string);
|
||||
await DefaultPreference.set('amplitudeDeviceId', uid as string);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,46 @@
|
||||
import { Types } from '@amplitude/analytics-browser';
|
||||
// @ts-ignore
|
||||
import { jitsiLocalStorage } from '@jitsi/js-utils';
|
||||
|
||||
import logger from '../../logger';
|
||||
|
||||
/**
|
||||
* Key used to store the device id in local storage.
|
||||
*/
|
||||
const DEVICE_ID_KEY = '__AMDID';
|
||||
|
||||
/**
|
||||
* Custom logic for setting the correct device id.
|
||||
*
|
||||
* @param {Types.BrowserClient} _amplitude - The amplitude instance.
|
||||
* @param {Types.BrowserClient} amplitude - The amplitude instance.
|
||||
* @returns {void}
|
||||
*/
|
||||
export function fixDeviceID(_amplitude: Types.BrowserClient): Promise<any> {
|
||||
return new Promise(resolve => resolve(true));
|
||||
export function fixDeviceID(amplitude: Types.BrowserClient) {
|
||||
const deviceId = jitsiLocalStorage.getItem(DEVICE_ID_KEY);
|
||||
|
||||
if (deviceId) {
|
||||
// Set the device id in Amplitude.
|
||||
try {
|
||||
amplitude.setDeviceId(JSON.parse(deviceId));
|
||||
} catch (error) {
|
||||
logger.error('Failed to set device ID in Amplitude', error);
|
||||
|
||||
return Promise.resolve(false);
|
||||
}
|
||||
} else {
|
||||
const newDeviceId = amplitude.getDeviceId();
|
||||
|
||||
if (newDeviceId) {
|
||||
jitsiLocalStorage.setItem(DEVICE_ID_KEY, JSON.stringify(newDeviceId));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the amplitude shared deviceId.
|
||||
*
|
||||
* @returns {string} - The amplitude deviceId.
|
||||
*/
|
||||
export function getDeviceID() {
|
||||
return jitsiLocalStorage.getItem(DEVICE_ID_KEY);
|
||||
}
|
||||
|
||||
@@ -1,3 +1,15 @@
|
||||
import { createInstance } from '@amplitude/analytics-react-native';
|
||||
import amplitude from '@amplitude/analytics-react-native';
|
||||
|
||||
export default createInstance();
|
||||
export default amplitude;
|
||||
|
||||
/**
|
||||
* Initializes the Amplitude instance.
|
||||
*
|
||||
* @param {string} amplitudeAPPKey - The Amplitude app key.
|
||||
* @param {string | undefined} user - The user ID.
|
||||
* @returns {Promise} The initialized Amplitude instance.
|
||||
*/
|
||||
export function initAmplitude(
|
||||
amplitudeAPPKey: string, user: string | undefined): Promise<unknown> {
|
||||
return amplitude.init(amplitudeAPPKey, user, {}).promise;
|
||||
}
|
||||
|
||||
@@ -1,3 +1,38 @@
|
||||
import { createInstance } from '@amplitude/analytics-browser';
|
||||
|
||||
export default createInstance();
|
||||
const amplitude = createInstance();
|
||||
|
||||
export default amplitude;
|
||||
|
||||
/**
|
||||
* Initializes the Amplitude instance.
|
||||
*
|
||||
* @param {string} amplitudeAPPKey - The Amplitude app key.
|
||||
* @param {string | undefined} user - The user ID.
|
||||
* @returns {Promise} The initialized Amplitude instance.
|
||||
*/
|
||||
export function initAmplitude(
|
||||
amplitudeAPPKey: string, user: string | undefined): Promise<unknown> {
|
||||
|
||||
// Forces sending all events on exit (flushing) via sendBeacon.
|
||||
window.addEventListener('pagehide', () => {
|
||||
// Set https transport to use sendBeacon API.
|
||||
amplitude.setTransport('beacon');
|
||||
// Send all pending events to server.
|
||||
amplitude.flush();
|
||||
});
|
||||
|
||||
const options = {
|
||||
autocapture: {
|
||||
attribution: true,
|
||||
pageViews: true,
|
||||
sessions: false,
|
||||
fileDownloads: false,
|
||||
formInteractions: false,
|
||||
elementInteractions: false
|
||||
},
|
||||
defaultTracking: false
|
||||
};
|
||||
|
||||
return amplitude.init(amplitudeAPPKey, user, options).promise;
|
||||
}
|
||||
|
||||
@@ -2,7 +2,8 @@ import React, { ComponentType } from 'react';
|
||||
import { NativeModules, Platform, StyleSheet, View } from 'react-native';
|
||||
import DeviceInfo from 'react-native-device-info';
|
||||
import { SafeAreaProvider } from 'react-native-safe-area-context';
|
||||
import SplashScreen from 'react-native-splash-screen';
|
||||
// @ts-ignore
|
||||
import { hideSplash } from 'react-native-splash-view';
|
||||
|
||||
import BottomSheetContainer from '../../base/dialog/components/native/BottomSheetContainer';
|
||||
import DialogContainer from '../../base/dialog/components/native/DialogContainer';
|
||||
@@ -84,7 +85,7 @@ export class App extends AbstractApp<IProps> {
|
||||
override async componentDidMount() {
|
||||
await super.componentDidMount();
|
||||
|
||||
SplashScreen.hide();
|
||||
hideSplash();
|
||||
|
||||
const liteTxt = AppInfo.isLiteSDK ? ' (lite)' : '';
|
||||
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import { IStateful } from '../base/app/types';
|
||||
import { toState } from '../base/redux/functions';
|
||||
import { getServerURL } from '../base/settings/functions.web';
|
||||
import { getJitsiMeetGlobalNS } from '../base/util/helpers';
|
||||
|
||||
export * from './functions.any';
|
||||
import logger from './logger';
|
||||
|
||||
/**
|
||||
* Retrieves the default URL for the app. This can either come from a prop to
|
||||
@@ -31,3 +33,27 @@ export function getDefaultURL(stateful: IStateful) {
|
||||
export function getName() {
|
||||
return interfaceConfig.APP_NAME;
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes a handler function after the window load event has been received.
|
||||
* If the app has already loaded, the handler is executed immediately.
|
||||
* Otherwise, the handler is registered as a 'load' event listener.
|
||||
*
|
||||
* @param {Function} handler - The callback function to execute.
|
||||
* @returns {void}
|
||||
*/
|
||||
export function executeAfterLoad(handler: () => void) {
|
||||
const safeHandler = () => {
|
||||
try {
|
||||
handler();
|
||||
} catch (error) {
|
||||
logger.error('Error executing handler after load:', error);
|
||||
}
|
||||
};
|
||||
|
||||
if (getJitsiMeetGlobalNS()?.hasLoaded) {
|
||||
safeHandler();
|
||||
} else {
|
||||
window.addEventListener('load', safeHandler);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,7 +24,6 @@ import '../calendar-sync/middleware';
|
||||
import '../chat/middleware';
|
||||
import '../conference/middleware';
|
||||
import '../connection-indicator/middleware';
|
||||
import '../deep-linking/middleware';
|
||||
import '../device-selection/middleware';
|
||||
import '../display-name/middleware';
|
||||
import '../dynamic-branding/middleware';
|
||||
|
||||
@@ -2,6 +2,7 @@ import '../base/app/middleware';
|
||||
import '../base/connection/middleware';
|
||||
import '../base/devices/middleware';
|
||||
import '../base/media/middleware';
|
||||
import '../deep-linking/middleware.web';
|
||||
import '../dynamic-branding/middleware';
|
||||
import '../e2ee/middleware';
|
||||
import '../external-api/middleware';
|
||||
|
||||
@@ -37,6 +37,15 @@ export const ENABLE_MODERATION = 'ENABLE_MODERATION';
|
||||
*/
|
||||
export const REQUEST_DISABLE_AUDIO_MODERATION = 'REQUEST_DISABLE_AUDIO_MODERATION';
|
||||
|
||||
/**
|
||||
* The type of (redux) action which signals that Desktop Moderation disable has been requested.
|
||||
*
|
||||
* {
|
||||
* type: REQUEST_DISABLE_DESKTOP_MODERATION
|
||||
* }
|
||||
*/
|
||||
export const REQUEST_DISABLE_DESKTOP_MODERATION = 'REQUEST_DISABLE_DESKTOP_MODERATION';
|
||||
|
||||
/**
|
||||
* The type of (redux) action which signals that Video Moderation disable has been requested.
|
||||
*
|
||||
@@ -55,6 +64,15 @@ export const REQUEST_DISABLE_VIDEO_MODERATION = 'REQUEST_DISABLE_VIDEO_MODERATIO
|
||||
*/
|
||||
export const REQUEST_ENABLE_AUDIO_MODERATION = 'REQUEST_ENABLE_AUDIO_MODERATION';
|
||||
|
||||
/**
|
||||
* The type of (redux) action which signals that Desktop Moderation enable has been requested.
|
||||
*
|
||||
* {
|
||||
* type: REQUEST_ENABLE_DESKTOP_MODERATION
|
||||
* }
|
||||
*/
|
||||
export const REQUEST_ENABLE_DESKTOP_MODERATION = 'REQUEST_ENABLE_DESKTOP_MODERATION';
|
||||
|
||||
/**
|
||||
* The type of (redux) action which signals that Video Moderation enable has been requested.
|
||||
*
|
||||
@@ -117,7 +135,7 @@ export const PARTICIPANT_REJECTED = 'PARTICIPANT_REJECTED';
|
||||
|
||||
|
||||
/**
|
||||
* The type of (redux) action which signals that a participant asked to have its audio umuted.
|
||||
* The type of (redux) action which signals that a participant asked to have its audio unmuted.
|
||||
*
|
||||
* {
|
||||
* type: PARTICIPANT_PENDING_AUDIO
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { batch } from 'react-redux';
|
||||
|
||||
import { IStore } from '../app/types';
|
||||
import { getConferenceState } from '../base/conference/functions';
|
||||
import { MEDIA_TYPE, type MediaType } from '../base/media/constants';
|
||||
import { getParticipantById, isParticipantModerator } from '../base/participants/functions';
|
||||
import { IParticipant } from '../base/participants/types';
|
||||
import { isForceMuted } from '../participants-pane/functions';
|
||||
|
||||
import {
|
||||
DISABLE_MODERATION,
|
||||
@@ -16,11 +16,14 @@ import {
|
||||
PARTICIPANT_PENDING_AUDIO,
|
||||
PARTICIPANT_REJECTED,
|
||||
REQUEST_DISABLE_AUDIO_MODERATION,
|
||||
REQUEST_DISABLE_DESKTOP_MODERATION,
|
||||
REQUEST_DISABLE_VIDEO_MODERATION,
|
||||
REQUEST_ENABLE_AUDIO_MODERATION,
|
||||
REQUEST_ENABLE_DESKTOP_MODERATION,
|
||||
REQUEST_ENABLE_VIDEO_MODERATION
|
||||
} from './actionTypes';
|
||||
import { isEnabledFromState } from './functions';
|
||||
import { MEDIA_TYPE, type MediaType } from './constants';
|
||||
import { isEnabledFromState, isForceMuted } from './functions';
|
||||
|
||||
/**
|
||||
* Action used by moderator to approve audio for a participant.
|
||||
@@ -42,6 +45,25 @@ export const approveParticipantAudio = (id: string) => (dispatch: IStore['dispat
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Action used by moderator to approve desktop for a participant.
|
||||
*
|
||||
* @param {staring} id - The id of the participant to be approved.
|
||||
* @returns {void}
|
||||
*/
|
||||
export const approveParticipantDesktop = (id: string) => (dispatch: IStore['dispatch'], getState: IStore['getState']) => {
|
||||
const state = getState();
|
||||
const { conference } = getConferenceState(state);
|
||||
const participant = getParticipantById(state, id);
|
||||
|
||||
const isDesktopForceMuted = isForceMuted(participant, MEDIA_TYPE.DESKTOP, state);
|
||||
const isDesktopModerationOn = isEnabledFromState(MEDIA_TYPE.DESKTOP, state);
|
||||
|
||||
if (isDesktopModerationOn && isDesktopForceMuted) {
|
||||
conference?.avModerationApprove(MEDIA_TYPE.DESKTOP, id);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Action used by moderator to approve video for a participant.
|
||||
*
|
||||
@@ -68,8 +90,11 @@ export const approveParticipantVideo = (id: string) => (dispatch: IStore['dispat
|
||||
* @returns {void}
|
||||
*/
|
||||
export const approveParticipant = (id: string) => (dispatch: IStore['dispatch']) => {
|
||||
dispatch(approveParticipantAudio(id));
|
||||
dispatch(approveParticipantVideo(id));
|
||||
batch(() => {
|
||||
dispatch(approveParticipantAudio(id));
|
||||
dispatch(approveParticipantDesktop(id));
|
||||
dispatch(approveParticipantVideo(id));
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -92,6 +117,26 @@ export const rejectParticipantAudio = (id: string) => (dispatch: IStore['dispatc
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Action used by moderator to reject desktop for a participant.
|
||||
*
|
||||
* @param {staring} id - The id of the participant to be rejected.
|
||||
* @returns {void}
|
||||
*/
|
||||
export const rejectParticipantDesktop = (id: string) => (dispatch: IStore['dispatch'], getState: IStore['getState']) => {
|
||||
const state = getState();
|
||||
const { conference } = getConferenceState(state);
|
||||
const desktopModeration = isEnabledFromState(MEDIA_TYPE.DESKTOP, state);
|
||||
|
||||
const participant = getParticipantById(state, id);
|
||||
const isDesktopForceMuted = isForceMuted(participant, MEDIA_TYPE.DESKTOP, state);
|
||||
const isModerator = isParticipantModerator(participant);
|
||||
|
||||
if (desktopModeration && !isDesktopForceMuted && !isModerator) {
|
||||
conference?.avModerationReject(MEDIA_TYPE.DESKTOP, id);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Action used by moderator to reject video for a participant.
|
||||
*
|
||||
@@ -185,6 +230,19 @@ export const requestDisableAudioModeration = () => {
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Requests disable of video moderation.
|
||||
*
|
||||
* @returns {{
|
||||
* type: REQUEST_DISABLE_DESKTOP_MODERATION
|
||||
* }}
|
||||
*/
|
||||
export const requestDisableDesktopModeration = () => {
|
||||
return {
|
||||
type: REQUEST_DISABLE_DESKTOP_MODERATION
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Requests disable of video moderation.
|
||||
*
|
||||
@@ -211,6 +269,19 @@ export const requestEnableAudioModeration = () => {
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Requests enable of video moderation.
|
||||
*
|
||||
* @returns {{
|
||||
* type: REQUEST_ENABLE_DESKTOP_MODERATION
|
||||
* }}
|
||||
*/
|
||||
export const requestEnableDesktopModeration = () => {
|
||||
return {
|
||||
type: REQUEST_ENABLE_DESKTOP_MODERATION
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Requests enable of video moderation.
|
||||
*
|
||||
@@ -313,4 +384,3 @@ export function participantRejected(id: string, mediaType: MediaType) {
|
||||
mediaType
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -1,18 +1,35 @@
|
||||
import { MEDIA_TYPE } from '../base/media/constants';
|
||||
export type MediaType = 'audio' | 'video' | 'desktop';
|
||||
|
||||
/**
|
||||
* Mapping between a media type and the witelist reducer key.
|
||||
* The set of media types for AV moderation.
|
||||
*
|
||||
* @enum {string}
|
||||
*/
|
||||
export const MEDIA_TYPE: {
|
||||
AUDIO: MediaType;
|
||||
DESKTOP: MediaType;
|
||||
VIDEO: MediaType;
|
||||
} = {
|
||||
AUDIO: 'audio',
|
||||
DESKTOP: 'desktop',
|
||||
VIDEO: 'video'
|
||||
};
|
||||
|
||||
/**
|
||||
* Mapping between a media type and the whitelist reducer key.
|
||||
*/
|
||||
export const MEDIA_TYPE_TO_WHITELIST_STORE_KEY: { [key: string]: string; } = {
|
||||
[MEDIA_TYPE.AUDIO]: 'audioWhitelist',
|
||||
[MEDIA_TYPE.DESKTOP]: 'desktopWhitelist',
|
||||
[MEDIA_TYPE.VIDEO]: 'videoWhitelist'
|
||||
};
|
||||
|
||||
/**
|
||||
* Mapping between a media type and the pending reducer key.
|
||||
*/
|
||||
export const MEDIA_TYPE_TO_PENDING_STORE_KEY: { [key: string]: 'pendingAudio' | 'pendingVideo'; } = {
|
||||
export const MEDIA_TYPE_TO_PENDING_STORE_KEY: { [key: string]: 'pendingAudio' | 'pendingDesktop' | 'pendingVideo'; } = {
|
||||
[MEDIA_TYPE.AUDIO]: 'pendingAudio',
|
||||
[MEDIA_TYPE.DESKTOP]: 'pendingDesktop',
|
||||
[MEDIA_TYPE.VIDEO]: 'pendingVideo'
|
||||
};
|
||||
|
||||
@@ -20,11 +37,15 @@ export const ASKED_TO_UNMUTE_NOTIFICATION_ID = 'asked-to-unmute';
|
||||
export const ASKED_TO_UNMUTE_SOUND_ID = 'ASKED_TO_UNMUTE_SOUND';
|
||||
|
||||
export const AUDIO_MODERATION_NOTIFICATION_ID = 'audio-moderation';
|
||||
export const DESKTOP_MODERATION_NOTIFICATION_ID = 'desktop-moderation';
|
||||
export const VIDEO_MODERATION_NOTIFICATION_ID = 'video-moderation';
|
||||
export const CS_MODERATION_NOTIFICATION_ID = 'screensharing-moderation';
|
||||
|
||||
export const AUDIO_RAISED_HAND_NOTIFICATION_ID = 'raise-hand-audio';
|
||||
export const DESKTOP_RAISED_HAND_NOTIFICATION_ID = 'raise-hand-desktop';
|
||||
export const VIDEO_RAISED_HAND_NOTIFICATION_ID = 'raise-hand-video';
|
||||
|
||||
export const MODERATION_NOTIFICATIONS = {
|
||||
[MEDIA_TYPE.AUDIO]: AUDIO_MODERATION_NOTIFICATION_ID,
|
||||
[MEDIA_TYPE.SCREENSHARE]: CS_MODERATION_NOTIFICATION_ID,
|
||||
[MEDIA_TYPE.DESKTOP]: DESKTOP_MODERATION_NOTIFICATION_ID,
|
||||
[MEDIA_TYPE.VIDEO]: VIDEO_MODERATION_NOTIFICATION_ID
|
||||
};
|
||||
|
||||
@@ -1,10 +1,14 @@
|
||||
import { IReduxState } from '../app/types';
|
||||
import { MEDIA_TYPE, type MediaType } from '../base/media/constants';
|
||||
import { isLocalParticipantModerator } from '../base/participants/functions';
|
||||
import { isLocalParticipantModerator, isParticipantModerator } from '../base/participants/functions';
|
||||
import { IParticipant } from '../base/participants/types';
|
||||
import { isInBreakoutRoom } from '../breakout-rooms/functions';
|
||||
|
||||
import { MEDIA_TYPE_TO_PENDING_STORE_KEY, MEDIA_TYPE_TO_WHITELIST_STORE_KEY } from './constants';
|
||||
import {
|
||||
MEDIA_TYPE,
|
||||
MEDIA_TYPE_TO_PENDING_STORE_KEY,
|
||||
MEDIA_TYPE_TO_WHITELIST_STORE_KEY,
|
||||
MediaType
|
||||
} from './constants';
|
||||
|
||||
/**
|
||||
* Returns this feature's root state.
|
||||
@@ -29,10 +33,18 @@ const EMPTY_ARRAY: any[] = [];
|
||||
* @param {IReduxState} state - Global state.
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export const isEnabledFromState = (mediaType: MediaType, state: IReduxState) =>
|
||||
(mediaType === MEDIA_TYPE.AUDIO
|
||||
? getState(state)?.audioModerationEnabled
|
||||
: getState(state)?.videoModerationEnabled) === true;
|
||||
export const isEnabledFromState = (mediaType: MediaType, state: IReduxState) => {
|
||||
switch (mediaType) {
|
||||
case MEDIA_TYPE.AUDIO:
|
||||
return getState(state)?.audioModerationEnabled === true;
|
||||
case MEDIA_TYPE.DESKTOP:
|
||||
return getState(state)?.desktopModerationEnabled === true;
|
||||
case MEDIA_TYPE.VIDEO:
|
||||
return getState(state)?.videoModerationEnabled === true;
|
||||
default:
|
||||
throw new Error(`Unknown media type: ${mediaType}`);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns whether moderation is enabled per media type.
|
||||
@@ -61,11 +73,20 @@ export const isSupported = () => (state: IReduxState) => {
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export const isLocalParticipantApprovedFromState = (mediaType: MediaType, state: IReduxState) => {
|
||||
const approved = (mediaType === MEDIA_TYPE.AUDIO
|
||||
? getState(state).audioUnmuteApproved
|
||||
: getState(state).videoUnmuteApproved) === true;
|
||||
if (isLocalParticipantModerator(state)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return approved || isLocalParticipantModerator(state);
|
||||
switch (mediaType) {
|
||||
case MEDIA_TYPE.AUDIO:
|
||||
return getState(state).audioUnmuteApproved === true;
|
||||
case MEDIA_TYPE.DESKTOP:
|
||||
return getState(state).desktopUnmuteApproved === true;
|
||||
case MEDIA_TYPE.VIDEO:
|
||||
return getState(state).videoUnmuteApproved === true;
|
||||
default:
|
||||
throw new Error(`Unknown media type: ${mediaType}`);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -134,3 +155,28 @@ export const getParticipantsAskingToAudioUnmute = (state: IReduxState) => {
|
||||
export const shouldShowModeratedNotification = (mediaType: MediaType, state: IReduxState) =>
|
||||
isEnabledFromState(mediaType, state)
|
||||
&& !isLocalParticipantApprovedFromState(mediaType, state);
|
||||
|
||||
/**
|
||||
* Checks if a participant is force muted.
|
||||
*
|
||||
* @param {IParticipant|undefined} participant - The participant.
|
||||
* @param {MediaType} mediaType - The media type.
|
||||
* @param {IReduxState} state - The redux state.
|
||||
* @returns {MediaState}
|
||||
*/
|
||||
export function isForceMuted(participant: IParticipant | undefined, mediaType: MediaType, state: IReduxState) {
|
||||
if (isEnabledFromState(mediaType, state)) {
|
||||
if (participant?.local) {
|
||||
return !isLocalParticipantApprovedFromState(mediaType, state);
|
||||
}
|
||||
|
||||
// moderators cannot be force muted
|
||||
if (isParticipantModerator(participant)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return !isParticipantApproved(participant?.id ?? '', mediaType)(state);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -3,8 +3,12 @@ import { batch } from 'react-redux';
|
||||
import { APP_WILL_MOUNT, APP_WILL_UNMOUNT } from '../base/app/actionTypes';
|
||||
import { getConferenceState } from '../base/conference/functions';
|
||||
import { JitsiConferenceEvents } from '../base/lib-jitsi-meet';
|
||||
import { MEDIA_TYPE, MediaType } from '../base/media/constants';
|
||||
import { isAudioMuted, isVideoMuted } from '../base/media/functions';
|
||||
import { MEDIA_TYPE as TRACK_MEDIA_TYPE } from '../base/media/constants';
|
||||
import {
|
||||
isAudioMuted,
|
||||
isScreenshareMuted,
|
||||
isVideoMuted
|
||||
} from '../base/media/functions';
|
||||
import { PARTICIPANT_UPDATED } from '../base/participants/actionTypes';
|
||||
import { raiseHand } from '../base/participants/actions';
|
||||
import {
|
||||
@@ -30,8 +34,10 @@ import {
|
||||
PARTICIPANT_APPROVED,
|
||||
PARTICIPANT_REJECTED,
|
||||
REQUEST_DISABLE_AUDIO_MODERATION,
|
||||
REQUEST_DISABLE_DESKTOP_MODERATION,
|
||||
REQUEST_DISABLE_VIDEO_MODERATION,
|
||||
REQUEST_ENABLE_AUDIO_MODERATION,
|
||||
REQUEST_ENABLE_DESKTOP_MODERATION,
|
||||
REQUEST_ENABLE_VIDEO_MODERATION
|
||||
} from './actionTypes';
|
||||
import {
|
||||
@@ -49,8 +55,10 @@ import {
|
||||
ASKED_TO_UNMUTE_NOTIFICATION_ID,
|
||||
ASKED_TO_UNMUTE_SOUND_ID,
|
||||
AUDIO_MODERATION_NOTIFICATION_ID,
|
||||
CS_MODERATION_NOTIFICATION_ID,
|
||||
VIDEO_MODERATION_NOTIFICATION_ID
|
||||
DESKTOP_MODERATION_NOTIFICATION_ID,
|
||||
MEDIA_TYPE,
|
||||
MediaType,
|
||||
VIDEO_MODERATION_NOTIFICATION_ID,
|
||||
} from './constants';
|
||||
import {
|
||||
isEnabledFromState,
|
||||
@@ -90,9 +98,9 @@ MiddlewareRegistry.register(({ dispatch, getState }) => next => action => {
|
||||
uid = VIDEO_MODERATION_NOTIFICATION_ID;
|
||||
break;
|
||||
}
|
||||
case MEDIA_TYPE.SCREENSHARE: {
|
||||
case MEDIA_TYPE.DESKTOP: {
|
||||
titleKey = 'notify.moderationInEffectCSTitle';
|
||||
uid = CS_MODERATION_NOTIFICATION_ID;
|
||||
uid = DESKTOP_MODERATION_NOTIFICATION_ID;
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -115,6 +123,10 @@ MiddlewareRegistry.register(({ dispatch, getState }) => next => action => {
|
||||
conference?.disableAVModeration(MEDIA_TYPE.AUDIO);
|
||||
break;
|
||||
}
|
||||
case REQUEST_DISABLE_DESKTOP_MODERATION: {
|
||||
conference?.disableAVModeration(MEDIA_TYPE.DESKTOP);
|
||||
break;
|
||||
}
|
||||
case REQUEST_DISABLE_VIDEO_MODERATION: {
|
||||
conference?.disableAVModeration(MEDIA_TYPE.VIDEO);
|
||||
break;
|
||||
@@ -123,6 +135,10 @@ MiddlewareRegistry.register(({ dispatch, getState }) => next => action => {
|
||||
conference?.enableAVModeration(MEDIA_TYPE.AUDIO);
|
||||
break;
|
||||
}
|
||||
case REQUEST_ENABLE_DESKTOP_MODERATION: {
|
||||
conference?.enableAVModeration(MEDIA_TYPE.DESKTOP);
|
||||
break;
|
||||
}
|
||||
case REQUEST_ENABLE_VIDEO_MODERATION: {
|
||||
conference?.enableAVModeration(MEDIA_TYPE.VIDEO);
|
||||
break;
|
||||
@@ -219,24 +235,37 @@ StateListenerRegistry.register(
|
||||
const customActionHandler = [];
|
||||
|
||||
if ((mediaType === MEDIA_TYPE.AUDIO || getState()['features/av-moderation'].audioUnmuteApproved)
|
||||
&& isAudioMuted(getState())) {
|
||||
&& isAudioMuted(getState())) {
|
||||
customActionNameKey.push('notify.unmute');
|
||||
customActionHandler.push(() => {
|
||||
dispatch(muteLocal(false, MEDIA_TYPE.AUDIO));
|
||||
dispatch(muteLocal(false, TRACK_MEDIA_TYPE.AUDIO));
|
||||
dispatch(hideNotification(ASKED_TO_UNMUTE_NOTIFICATION_ID));
|
||||
});
|
||||
}
|
||||
|
||||
if ((mediaType === MEDIA_TYPE.VIDEO || getState()['features/av-moderation'].videoUnmuteApproved)
|
||||
&& isVideoMuted(getState())) {
|
||||
customActionNameKey.push('notify.unmuteVideo');
|
||||
if ((mediaType === MEDIA_TYPE.DESKTOP || getState()['features/av-moderation'].desktopUnmuteApproved)
|
||||
&& isScreenshareMuted(getState())) {
|
||||
customActionNameKey.push('notify.unmuteScreen');
|
||||
customActionHandler.push(() => {
|
||||
dispatch(muteLocal(false, MEDIA_TYPE.VIDEO));
|
||||
dispatch(muteLocal(false, TRACK_MEDIA_TYPE.SCREENSHARE));
|
||||
dispatch(hideNotification(ASKED_TO_UNMUTE_NOTIFICATION_ID));
|
||||
|
||||
// lower hand as there will be no audio and change in dominant speaker to clear it
|
||||
// Since permission is requested by raising the hand, lower it not to rely on dominant speaker detection
|
||||
// to clear the hand.
|
||||
dispatch(raiseHand(false));
|
||||
});
|
||||
}
|
||||
|
||||
if ((mediaType === MEDIA_TYPE.VIDEO || getState()['features/av-moderation'].videoUnmuteApproved)
|
||||
&& isVideoMuted(getState())) {
|
||||
customActionNameKey.push('notify.unmuteVideo');
|
||||
customActionHandler.push(() => {
|
||||
dispatch(muteLocal(false, TRACK_MEDIA_TYPE.VIDEO));
|
||||
dispatch(hideNotification(ASKED_TO_UNMUTE_NOTIFICATION_ID));
|
||||
|
||||
// Since permission is requested by raising the hand, lower it not to rely on dominant speaker detection
|
||||
// to clear the hand.
|
||||
dispatch(raiseHand(false));
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { MEDIA_TYPE, type MediaType } from '../base/media/constants';
|
||||
import {
|
||||
PARTICIPANT_LEFT,
|
||||
PARTICIPANT_UPDATED
|
||||
@@ -16,14 +15,21 @@ import {
|
||||
PARTICIPANT_PENDING_AUDIO,
|
||||
PARTICIPANT_REJECTED
|
||||
} from './actionTypes';
|
||||
import { MEDIA_TYPE_TO_PENDING_STORE_KEY } from './constants';
|
||||
import {
|
||||
MEDIA_TYPE,
|
||||
MEDIA_TYPE_TO_PENDING_STORE_KEY,
|
||||
type MediaType
|
||||
} from './constants';
|
||||
|
||||
const initialState = {
|
||||
audioModerationEnabled: false,
|
||||
desktopModerationEnabled: false,
|
||||
videoModerationEnabled: false,
|
||||
audioWhitelist: {},
|
||||
desktopWhitelist: {},
|
||||
videoWhitelist: {},
|
||||
pendingAudio: [],
|
||||
pendingDesktop: [],
|
||||
pendingVideo: []
|
||||
};
|
||||
|
||||
@@ -31,7 +37,11 @@ export interface IAVModerationState {
|
||||
audioModerationEnabled: boolean;
|
||||
audioUnmuteApproved?: boolean | undefined;
|
||||
audioWhitelist: { [id: string]: boolean; };
|
||||
desktopModerationEnabled: boolean;
|
||||
desktopUnmuteApproved?: boolean | undefined;
|
||||
desktopWhitelist: { [id: string]: boolean; };
|
||||
pendingAudio: Array<{ id: string; }>;
|
||||
pendingDesktop: Array<{ id: string; }>;
|
||||
pendingVideo: Array<{ id: string; }>;
|
||||
videoModerationEnabled: boolean;
|
||||
videoUnmuteApproved?: boolean | undefined;
|
||||
@@ -77,28 +87,61 @@ ReducerRegistry.register<IAVModerationState>('features/av-moderation',
|
||||
(state = initialState, action): IAVModerationState => {
|
||||
switch (action.type) {
|
||||
case DISABLE_MODERATION: {
|
||||
const newState = action.mediaType === MEDIA_TYPE.AUDIO
|
||||
? {
|
||||
let newState = {};
|
||||
|
||||
switch (action.mediaType) {
|
||||
case MEDIA_TYPE.AUDIO:
|
||||
newState = {
|
||||
audioModerationEnabled: false,
|
||||
audioUnmuteApproved: undefined
|
||||
} : {
|
||||
};
|
||||
break;
|
||||
case MEDIA_TYPE.DESKTOP:
|
||||
newState = {
|
||||
desktopModerationEnabled: false,
|
||||
desktopUnmuteApproved: undefined
|
||||
};
|
||||
break;
|
||||
case MEDIA_TYPE.VIDEO:
|
||||
newState = {
|
||||
videoModerationEnabled: false,
|
||||
videoUnmuteApproved: undefined
|
||||
};
|
||||
break;
|
||||
}
|
||||
|
||||
return {
|
||||
...state,
|
||||
...newState,
|
||||
audioWhitelist: {},
|
||||
desktopWhitelist: {},
|
||||
videoWhitelist: {},
|
||||
pendingAudio: [],
|
||||
pendingDesktop: [],
|
||||
pendingVideo: []
|
||||
};
|
||||
}
|
||||
|
||||
case ENABLE_MODERATION: {
|
||||
const newState = action.mediaType === MEDIA_TYPE.AUDIO
|
||||
? { audioModerationEnabled: true } : { videoModerationEnabled: true };
|
||||
let newState = {};
|
||||
|
||||
switch (action.mediaType) {
|
||||
case MEDIA_TYPE.AUDIO:
|
||||
newState = {
|
||||
audioModerationEnabled: true,
|
||||
};
|
||||
break;
|
||||
case MEDIA_TYPE.DESKTOP:
|
||||
newState = {
|
||||
desktopModerationEnabled: true,
|
||||
};
|
||||
break;
|
||||
case MEDIA_TYPE.VIDEO:
|
||||
newState = {
|
||||
videoModerationEnabled: true,
|
||||
};
|
||||
break;
|
||||
}
|
||||
|
||||
return {
|
||||
...state,
|
||||
@@ -107,8 +150,25 @@ ReducerRegistry.register<IAVModerationState>('features/av-moderation',
|
||||
}
|
||||
|
||||
case LOCAL_PARTICIPANT_APPROVED: {
|
||||
const newState = action.mediaType === MEDIA_TYPE.AUDIO
|
||||
? { audioUnmuteApproved: true } : { videoUnmuteApproved: true };
|
||||
let newState = {};
|
||||
|
||||
switch (action.mediaType) {
|
||||
case MEDIA_TYPE.AUDIO:
|
||||
newState = {
|
||||
audioUnmuteApproved: true
|
||||
};
|
||||
break;
|
||||
case MEDIA_TYPE.DESKTOP:
|
||||
newState = {
|
||||
desktopUnmuteApproved: true
|
||||
};
|
||||
break;
|
||||
case MEDIA_TYPE.VIDEO:
|
||||
newState = {
|
||||
videoUnmuteApproved: true
|
||||
};
|
||||
break;
|
||||
}
|
||||
|
||||
return {
|
||||
...state,
|
||||
@@ -117,8 +177,25 @@ ReducerRegistry.register<IAVModerationState>('features/av-moderation',
|
||||
}
|
||||
|
||||
case LOCAL_PARTICIPANT_REJECTED: {
|
||||
const newState = action.mediaType === MEDIA_TYPE.AUDIO
|
||||
? { audioUnmuteApproved: false } : { videoUnmuteApproved: false };
|
||||
let newState = {};
|
||||
|
||||
switch (action.mediaType) {
|
||||
case MEDIA_TYPE.AUDIO:
|
||||
newState = {
|
||||
audioUnmuteApproved: false
|
||||
};
|
||||
break;
|
||||
case MEDIA_TYPE.DESKTOP:
|
||||
newState = {
|
||||
desktopUnmuteApproved: false
|
||||
};
|
||||
break;
|
||||
case MEDIA_TYPE.VIDEO:
|
||||
newState = {
|
||||
videoUnmuteApproved: false
|
||||
};
|
||||
break;
|
||||
}
|
||||
|
||||
return {
|
||||
...state,
|
||||
@@ -146,7 +223,7 @@ ReducerRegistry.register<IAVModerationState>('features/av-moderation',
|
||||
|
||||
case PARTICIPANT_UPDATED: {
|
||||
const participant = action.participant;
|
||||
const { audioModerationEnabled, videoModerationEnabled } = state;
|
||||
const { audioModerationEnabled, desktopModerationEnabled, videoModerationEnabled } = state;
|
||||
let hasStateChanged = false;
|
||||
|
||||
// skips changing the reference of pendingAudio or pendingVideo,
|
||||
@@ -155,6 +232,10 @@ ReducerRegistry.register<IAVModerationState>('features/av-moderation',
|
||||
hasStateChanged = _updatePendingParticipant(MEDIA_TYPE.AUDIO, participant, state);
|
||||
}
|
||||
|
||||
if (desktopModerationEnabled) {
|
||||
hasStateChanged = hasStateChanged || _updatePendingParticipant(MEDIA_TYPE.DESKTOP, participant, state);
|
||||
}
|
||||
|
||||
if (videoModerationEnabled) {
|
||||
hasStateChanged = hasStateChanged || _updatePendingParticipant(MEDIA_TYPE.VIDEO, participant, state);
|
||||
}
|
||||
@@ -168,9 +249,10 @@ ReducerRegistry.register<IAVModerationState>('features/av-moderation',
|
||||
|
||||
return state;
|
||||
}
|
||||
|
||||
case PARTICIPANT_LEFT: {
|
||||
const participant = action.participant;
|
||||
const { audioModerationEnabled, videoModerationEnabled } = state;
|
||||
const { audioModerationEnabled, desktopModerationEnabled, videoModerationEnabled } = state;
|
||||
let hasStateChanged = false;
|
||||
|
||||
// skips changing the reference of pendingAudio or pendingVideo,
|
||||
@@ -184,6 +266,15 @@ ReducerRegistry.register<IAVModerationState>('features/av-moderation',
|
||||
}
|
||||
}
|
||||
|
||||
if (desktopModerationEnabled) {
|
||||
const newPendingDesktop = state.pendingDesktop.filter(pending => pending.id !== participant.id);
|
||||
|
||||
if (state.pendingDesktop.length !== newPendingDesktop.length) {
|
||||
state.pendingDesktop = newPendingDesktop;
|
||||
hasStateChanged = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (videoModerationEnabled) {
|
||||
const newPendingVideo = state.pendingVideo.filter(pending => pending.id !== participant.id);
|
||||
|
||||
@@ -213,6 +304,13 @@ ReducerRegistry.register<IAVModerationState>('features/av-moderation',
|
||||
};
|
||||
}
|
||||
|
||||
if (mediaType === MEDIA_TYPE.DESKTOP) {
|
||||
return {
|
||||
...state,
|
||||
pendingDesktop: state.pendingDesktop.filter(pending => pending.id !== id)
|
||||
};
|
||||
}
|
||||
|
||||
if (mediaType === MEDIA_TYPE.VIDEO) {
|
||||
return {
|
||||
...state,
|
||||
@@ -236,6 +334,16 @@ ReducerRegistry.register<IAVModerationState>('features/av-moderation',
|
||||
};
|
||||
}
|
||||
|
||||
if (mediaType === MEDIA_TYPE.DESKTOP) {
|
||||
return {
|
||||
...state,
|
||||
desktopWhitelist: {
|
||||
...state.desktopWhitelist,
|
||||
[id]: true
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
if (mediaType === MEDIA_TYPE.VIDEO) {
|
||||
return {
|
||||
...state,
|
||||
@@ -262,6 +370,16 @@ ReducerRegistry.register<IAVModerationState>('features/av-moderation',
|
||||
};
|
||||
}
|
||||
|
||||
if (mediaType === MEDIA_TYPE.DESKTOP) {
|
||||
return {
|
||||
...state,
|
||||
desktopWhitelist: {
|
||||
...state.desktopWhitelist,
|
||||
[id]: false
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
if (mediaType === MEDIA_TYPE.VIDEO) {
|
||||
return {
|
||||
...state,
|
||||
|
||||
@@ -24,20 +24,17 @@ MiddlewareRegistry.register(() => (next: Function) => (action: AnyAction) => {
|
||||
case APP_WILL_MOUNT: {
|
||||
// Disable it inside an iframe until Google fixes the origin trial for 3rd party sources:
|
||||
// https://bugs.chromium.org/p/chromium/issues/detail?id=1504167
|
||||
if (!isEmbedded() && 'PressureObserver' in globalThis) {
|
||||
if (!isEmbedded() && 'PressureObserver' in window) {
|
||||
pressureObserver = new window.PressureObserver(
|
||||
(records: typeof window.PressureRecord) => {
|
||||
logger.info('Compute pressure state changed:', JSON.stringify(records));
|
||||
if (typeof APP !== 'undefined') {
|
||||
APP.API.notifyComputePressureChanged(records);
|
||||
}
|
||||
},
|
||||
{ sampleRate: 1 }
|
||||
APP.API.notifyComputePressureChanged(records);
|
||||
}
|
||||
);
|
||||
|
||||
try {
|
||||
pressureObserver
|
||||
.observe('cpu')
|
||||
.observe('cpu', { sampleInterval: 30_000 })
|
||||
.catch((e: any) => logger.error('CPU pressure observer failed to start', e));
|
||||
} catch (e: any) {
|
||||
logger.error('CPU pressure observer failed to start', e);
|
||||
|
||||
@@ -83,7 +83,8 @@ import {
|
||||
getConferenceState,
|
||||
getCurrentConference,
|
||||
getVisitorOptions,
|
||||
sendLocalParticipant
|
||||
sendLocalParticipant,
|
||||
updateTrackMuteState
|
||||
} from './functions';
|
||||
import logger from './logger';
|
||||
import { IConferenceMetadata, IJitsiConference } from './reducer';
|
||||
@@ -186,6 +187,15 @@ function _addConferenceListeners(conference: IJitsiConference, dispatch: IStore[
|
||||
(disableVideoMuteChange: boolean) => {
|
||||
dispatch(setVideoUnmutePermissions(disableVideoMuteChange));
|
||||
});
|
||||
conference.on(
|
||||
JitsiConferenceEvents.START_MUTED_POLICY_CHANGED,
|
||||
({ audio, video }: { audio: boolean; video: boolean; }) => {
|
||||
dispatch(onStartMutedPolicyChanged(audio, video));
|
||||
|
||||
updateTrackMuteState(state, dispatch, true);
|
||||
updateTrackMuteState(state, dispatch, false);
|
||||
}
|
||||
);
|
||||
|
||||
// Dispatches into features/base/tracks follow:
|
||||
|
||||
@@ -1013,6 +1023,8 @@ export function setStartMutedPolicy(
|
||||
audio: startAudioMuted,
|
||||
video: startVideoMuted
|
||||
});
|
||||
|
||||
dispatch(onStartMutedPolicyChanged(startAudioMuted, startVideoMuted));
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -40,3 +40,8 @@ export const CONFERENCE_LEAVE_REASONS = {
|
||||
SWITCH_ROOM: 'switch_room',
|
||||
UNRECOVERABLE_ERROR: 'unrecoverable_error'
|
||||
};
|
||||
|
||||
/**
|
||||
* The ID of the notification that is shown when the user is muted by focus.
|
||||
*/
|
||||
export const START_MUTED_NOTIFICATION_ID = 'start-muted';
|
||||
|
||||
@@ -3,9 +3,13 @@ import { upperFirst, words } from 'lodash-es';
|
||||
|
||||
import { getName } from '../../app/functions';
|
||||
import { IReduxState, IStore } from '../../app/types';
|
||||
import { showNotification } from '../../notifications/actions';
|
||||
import { NOTIFICATION_TIMEOUT_TYPE } from '../../notifications/constants';
|
||||
import { determineTranscriptionLanguage } from '../../transcribing/functions';
|
||||
import { IStateful } from '../app/types';
|
||||
import { JitsiTrackErrors } from '../lib-jitsi-meet';
|
||||
import { setAudioMuted, setVideoMuted } from '../media/actions';
|
||||
import { VIDEO_MUTISM_AUTHORITY } from '../media/constants';
|
||||
import {
|
||||
participantJoined,
|
||||
participantLeft
|
||||
@@ -22,7 +26,8 @@ import { setObfuscatedRoom } from './actions';
|
||||
import {
|
||||
AVATAR_URL_COMMAND,
|
||||
EMAIL_COMMAND,
|
||||
JITSI_CONFERENCE_URL_KEY
|
||||
JITSI_CONFERENCE_URL_KEY,
|
||||
START_MUTED_NOTIFICATION_ID
|
||||
} from './constants';
|
||||
import logger from './logger';
|
||||
import { IJitsiConference } from './reducer';
|
||||
@@ -574,3 +579,42 @@ function safeStartCase(s = '') {
|
||||
(result, word, index) => result + (index ? ' ' : '') + upperFirst(word)
|
||||
, '');
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the mute state of the track based on the start muted policy.
|
||||
*
|
||||
* @param {Object|Function} stateful - Either the whole Redux state object or the Redux store's {@code getState} method.
|
||||
* @param {Function} dispatch - Redux dispatch function.
|
||||
* @param {boolean} isAudio - Whether the track is audio or video.
|
||||
* @returns {void}
|
||||
*/
|
||||
export function updateTrackMuteState(stateful: IStateful, dispatch: IStore['dispatch'], isAudio: boolean) {
|
||||
const state = toState(stateful);
|
||||
const mutedPolicyKey = isAudio ? 'startAudioMutedPolicy' : 'startVideoMutedPolicy';
|
||||
const mutedPolicyValue = state['features/base/conference'][mutedPolicyKey];
|
||||
|
||||
// Currently, the policy only supports force muting others, not unmuting them.
|
||||
if (!mutedPolicyValue) {
|
||||
return;
|
||||
}
|
||||
|
||||
let muteStateUpdated = false;
|
||||
const { muted } = isAudio ? state['features/base/media'].audio : state['features/base/media'].video;
|
||||
|
||||
if (isAudio && !Boolean(muted)) {
|
||||
dispatch(setAudioMuted(mutedPolicyValue, true));
|
||||
muteStateUpdated = true;
|
||||
} else if (!isAudio && !Boolean(muted)) {
|
||||
// TODO: Add a new authority for video mutism for the moderator case.
|
||||
dispatch(setVideoMuted(mutedPolicyValue, VIDEO_MUTISM_AUTHORITY.USER, true));
|
||||
muteStateUpdated = true;
|
||||
}
|
||||
|
||||
if (muteStateUpdated) {
|
||||
dispatch(showNotification({
|
||||
titleKey: 'notify.mutedTitle',
|
||||
descriptionKey: 'notify.muted',
|
||||
uid: START_MUTED_NOTIFICATION_ID // use the same id, to make sure we show one notification
|
||||
}, NOTIFICATION_TIMEOUT_TYPE.SHORT));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@ import { IStore } from '../../app/types';
|
||||
import { removeLobbyChatParticipant } from '../../chat/actions.any';
|
||||
import { openDisplayNamePrompt } from '../../display-name/actions';
|
||||
import { isVpaasMeeting } from '../../jaas/functions';
|
||||
import { showErrorNotification, showNotification } from '../../notifications/actions';
|
||||
import { clearNotifications, showErrorNotification, showNotification } from '../../notifications/actions';
|
||||
import { NOTIFICATION_TIMEOUT_TYPE } from '../../notifications/constants';
|
||||
import { INotificationProps } from '../../notifications/types';
|
||||
import { hasDisplayName } from '../../prejoin/utils';
|
||||
@@ -25,7 +25,7 @@ import LocalRecordingManager from '../../recording/components/Recording/LocalRec
|
||||
import { AudioMixerEffect } from '../../stream-effects/audio-mixer/AudioMixerEffect';
|
||||
import { iAmVisitor } from '../../visitors/functions';
|
||||
import { overwriteConfig } from '../config/actions';
|
||||
import { CONNECTION_ESTABLISHED, CONNECTION_FAILED } from '../connection/actionTypes';
|
||||
import { CONNECTION_ESTABLISHED, CONNECTION_FAILED, CONNECTION_WILL_CONNECT } from '../connection/actionTypes';
|
||||
import { connectionDisconnected, disconnect } from '../connection/actions';
|
||||
import { validateJwt } from '../jwt/functions';
|
||||
import { JitsiConferenceErrors, JitsiConferenceEvents, JitsiConnectionErrors } from '../lib-jitsi-meet';
|
||||
@@ -72,7 +72,6 @@ import {
|
||||
} from './functions';
|
||||
import logger from './logger';
|
||||
import { IConferenceMetadata } from './reducer';
|
||||
import './subscriber';
|
||||
|
||||
/**
|
||||
* Handler for before unload event.
|
||||
@@ -99,6 +98,11 @@ MiddlewareRegistry.register(store => next => action => {
|
||||
case CONNECTION_FAILED:
|
||||
return _connectionFailed(store, next, action);
|
||||
|
||||
case CONNECTION_WILL_CONNECT:
|
||||
// we are starting a new join process, let's clear the error notifications if any from any previous attempt
|
||||
store.dispatch(clearNotifications());
|
||||
break;
|
||||
|
||||
case CONFERENCE_SUBJECT_CHANGED:
|
||||
return _conferenceSubjectChanged(store, next, action);
|
||||
|
||||
|
||||
@@ -1,61 +0,0 @@
|
||||
import { IStore } from '../../app/types';
|
||||
import { showNotification } from '../../notifications/actions';
|
||||
import { NOTIFICATION_TIMEOUT_TYPE } from '../../notifications/constants';
|
||||
import { setAudioMuted, setVideoMuted } from '../media/actions';
|
||||
import { VIDEO_MUTISM_AUTHORITY } from '../media/constants';
|
||||
import StateListenerRegistry from '../redux/StateListenerRegistry';
|
||||
|
||||
let hasShownNotification = false;
|
||||
|
||||
/**
|
||||
* Handles changes in the start muted policy for audio and video tracks in the meta data set for the conference.
|
||||
*/
|
||||
StateListenerRegistry.register(
|
||||
/* selector */ state => state['features/base/conference'].startAudioMutedPolicy,
|
||||
/* listener */ (startAudioMutedPolicy, store) => {
|
||||
_updateTrackMuteState(store, true);
|
||||
});
|
||||
|
||||
StateListenerRegistry.register(
|
||||
/* selector */ state => state['features/base/conference'].startVideoMutedPolicy,
|
||||
/* listener */(startVideoMutedPolicy, store) => {
|
||||
_updateTrackMuteState(store, false);
|
||||
});
|
||||
|
||||
/**
|
||||
* Updates the mute state of the track based on the start muted policy.
|
||||
*
|
||||
* @param {IStore} store - The redux store.
|
||||
* @param {boolean} isAudio - Whether the track is audio or video.
|
||||
* @returns {void}
|
||||
*/
|
||||
function _updateTrackMuteState(store: IStore, isAudio: boolean) {
|
||||
const { dispatch, getState } = store;
|
||||
const mutedPolicyKey = isAudio ? 'startAudioMutedPolicy' : 'startVideoMutedPolicy';
|
||||
const mutedPolicyValue = getState()['features/base/conference'][mutedPolicyKey];
|
||||
|
||||
// Currently, the policy only supports force muting others, not unmuting them.
|
||||
if (!mutedPolicyValue) {
|
||||
return;
|
||||
}
|
||||
|
||||
let muteStateUpdated = false;
|
||||
const { muted } = isAudio ? getState()['features/base/media'].audio : getState()['features/base/media'].video;
|
||||
|
||||
if (isAudio && !Boolean(muted)) {
|
||||
dispatch(setAudioMuted(mutedPolicyValue, true));
|
||||
muteStateUpdated = true;
|
||||
} else if (!isAudio && !Boolean(muted)) {
|
||||
// TODO: Add a new authority for video mutism for the moderator case.
|
||||
dispatch(setVideoMuted(mutedPolicyValue, VIDEO_MUTISM_AUTHORITY.USER, true));
|
||||
muteStateUpdated = true;
|
||||
}
|
||||
|
||||
if (!hasShownNotification && muteStateUpdated) {
|
||||
hasShownNotification = true;
|
||||
dispatch(showNotification({
|
||||
titleKey: 'notify.mutedTitle',
|
||||
descriptionKey: 'notify.muted'
|
||||
}, NOTIFICATION_TIMEOUT_TYPE.SHORT));
|
||||
}
|
||||
}
|
||||
@@ -172,7 +172,6 @@ export interface IConfig {
|
||||
_screenshotHistoryRegionUrl?: number;
|
||||
analytics?: {
|
||||
amplitudeAPPKey?: string;
|
||||
amplitudeIncludeUTM?: boolean;
|
||||
blackListedEvents?: string[];
|
||||
disabled?: boolean;
|
||||
matomoEndpoint?: string;
|
||||
@@ -384,10 +383,12 @@ export interface IConfig {
|
||||
maxFileSize?: number;
|
||||
};
|
||||
filmstrip?: {
|
||||
alwaysShowResizeBar?: boolean;
|
||||
disableResizable?: boolean;
|
||||
disableStageFilmstrip?: boolean;
|
||||
disableTopPanel?: boolean;
|
||||
disabled?: boolean;
|
||||
initialWidth?: number;
|
||||
minParticipantCountForTopPanel?: number;
|
||||
};
|
||||
flags?: {
|
||||
@@ -552,7 +553,7 @@ export interface IConfig {
|
||||
disableDemote?: boolean;
|
||||
disableGrantModerator?: boolean;
|
||||
disableKick?: boolean;
|
||||
disablePrivateChat?: 'all' | 'allow-moderator-chat';
|
||||
disablePrivateChat?: 'all' | 'allow-moderator-chat' | 'disable-visitor-chat';
|
||||
disabled?: boolean;
|
||||
};
|
||||
replaceParticipant?: string;
|
||||
@@ -594,6 +595,7 @@ export interface IConfig {
|
||||
failICE?: boolean;
|
||||
noAutoPlayVideo?: boolean;
|
||||
p2pTestMode?: boolean;
|
||||
showSpotConsentDialog?: boolean;
|
||||
skipInterimTranscriptions?: boolean;
|
||||
testMode?: boolean;
|
||||
};
|
||||
|
||||
@@ -2,6 +2,7 @@ import { AnyAction } from 'redux';
|
||||
|
||||
import { IStore } from '../../app/types';
|
||||
import { SET_DYNAMIC_BRANDING_DATA } from '../../dynamic-branding/actionTypes';
|
||||
import { setUserFilmstripWidth } from '../../filmstrip/actions.web';
|
||||
import { getFeatureFlag } from '../flags/functions';
|
||||
import MiddlewareRegistry from '../redux/MiddlewareRegistry';
|
||||
import { updateSettings } from '../settings/actions';
|
||||
@@ -79,12 +80,18 @@ function _setConfig({ dispatch, getState }: IStore, next: Function, action: AnyA
|
||||
}));
|
||||
}
|
||||
|
||||
if (action.config.filmstrip?.stageFilmstripParticipants !== undefined) {
|
||||
const { initialWidth, stageFilmstripParticipants } = action.config.filmstrip || {};
|
||||
|
||||
if (stageFilmstripParticipants !== undefined) {
|
||||
dispatch(updateSettings({
|
||||
maxStageParticipants: action.config.filmstrip.stageFilmstripParticipants
|
||||
maxStageParticipants: stageFilmstripParticipants
|
||||
}));
|
||||
}
|
||||
|
||||
if (initialWidth) {
|
||||
dispatch(setUserFilmstripWidth(initialWidth));
|
||||
}
|
||||
|
||||
dispatch(updateConfig(config));
|
||||
|
||||
// FIXME On Web we rely on the global 'config' variable which gets altered
|
||||
|
||||
@@ -185,6 +185,15 @@ function _setConfig(state: IConfig, { config }: { config: IConfig; }) {
|
||||
});
|
||||
}
|
||||
|
||||
const { alwaysShowResizeBar, disableResizable } = config.filmstrip || {};
|
||||
|
||||
if (alwaysShowResizeBar && disableResizable) {
|
||||
config.filmstrip = {
|
||||
...config.filmstrip,
|
||||
alwaysShowResizeBar: false
|
||||
};
|
||||
}
|
||||
|
||||
const newState = merge(
|
||||
{},
|
||||
config,
|
||||
|
||||
@@ -18,6 +18,7 @@ export const MEET_FEATURES: Record<string, ParticipantFeaturesKey> = {
|
||||
ROOM: 'room',
|
||||
SCREEN_SHARING: 'screen-sharing',
|
||||
SEND_GROUPCHAT: 'send-groupchat',
|
||||
LIST_VISITORS: 'list-visitors',
|
||||
SIP_INBOUND_CALL: 'sip-inbound-call',
|
||||
SIP_OUTBOUND_CALL: 'sip-outbound-call',
|
||||
TRANSCRIPTION: 'transcription'
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { IStore } from '../../app/types';
|
||||
import { showModeratedNotification } from '../../av-moderation/actions';
|
||||
import { MEDIA_TYPE as AVM_MEDIA_TYPE } from '../../av-moderation/constants';
|
||||
import { shouldShowModeratedNotification } from '../../av-moderation/functions';
|
||||
import { isModerationNotificationDisplayed } from '../../notifications/functions';
|
||||
|
||||
@@ -18,7 +19,6 @@ import {
|
||||
TOGGLE_CAMERA_FACING_MODE
|
||||
} from './actionTypes';
|
||||
import {
|
||||
MEDIA_TYPE,
|
||||
MediaType,
|
||||
SCREENSHARE_MUTISM_AUTHORITY,
|
||||
VIDEO_MUTISM_AUTHORITY
|
||||
@@ -56,10 +56,23 @@ export function setAudioAvailable(available: boolean) {
|
||||
* }}
|
||||
*/
|
||||
export function setAudioMuted(muted: boolean, ensureTrack = false) {
|
||||
return {
|
||||
type: SET_AUDIO_MUTED,
|
||||
ensureTrack,
|
||||
muted
|
||||
return (dispatch: IStore['dispatch'], getState: IStore['getState']) => {
|
||||
const state = getState();
|
||||
|
||||
// check for A/V Moderation when trying to unmute
|
||||
if (!muted && shouldShowModeratedNotification(AVM_MEDIA_TYPE.AUDIO, state)) {
|
||||
if (!isModerationNotificationDisplayed(AVM_MEDIA_TYPE.AUDIO, state)) {
|
||||
ensureTrack && dispatch(showModeratedNotification(AVM_MEDIA_TYPE.AUDIO));
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
dispatch({
|
||||
type: SET_AUDIO_MUTED,
|
||||
ensureTrack,
|
||||
muted
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
@@ -126,9 +139,9 @@ export function setScreenshareMuted(
|
||||
const state = getState();
|
||||
|
||||
// check for A/V Moderation when trying to unmute
|
||||
if (!muted && shouldShowModeratedNotification(MEDIA_TYPE.SCREENSHARE, state)) {
|
||||
if (!isModerationNotificationDisplayed(MEDIA_TYPE.SCREENSHARE, state)) {
|
||||
ensureTrack && dispatch(showModeratedNotification(MEDIA_TYPE.SCREENSHARE));
|
||||
if (!muted && shouldShowModeratedNotification(AVM_MEDIA_TYPE.DESKTOP, state)) {
|
||||
if (!isModerationNotificationDisplayed(AVM_MEDIA_TYPE.DESKTOP, state)) {
|
||||
ensureTrack && dispatch(showModeratedNotification(AVM_MEDIA_TYPE.DESKTOP));
|
||||
}
|
||||
|
||||
return;
|
||||
@@ -184,9 +197,9 @@ export function setVideoMuted(
|
||||
const state = getState();
|
||||
|
||||
// check for A/V Moderation when trying to unmute
|
||||
if (!muted && shouldShowModeratedNotification(MEDIA_TYPE.VIDEO, state)) {
|
||||
if (!isModerationNotificationDisplayed(MEDIA_TYPE.VIDEO, state)) {
|
||||
ensureTrack && dispatch(showModeratedNotification(MEDIA_TYPE.VIDEO));
|
||||
if (!muted && shouldShowModeratedNotification(AVM_MEDIA_TYPE.VIDEO, state)) {
|
||||
if (!isModerationNotificationDisplayed(AVM_MEDIA_TYPE.VIDEO, state)) {
|
||||
ensureTrack && dispatch(showModeratedNotification(AVM_MEDIA_TYPE.VIDEO));
|
||||
}
|
||||
|
||||
return;
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
*
|
||||
* @enum {string}
|
||||
*/
|
||||
export const CAMERA_FACING_MODE = {
|
||||
export const CAMERA_FACING_MODE: Record<string, string> = {
|
||||
ENVIRONMENT: 'environment',
|
||||
USER: 'user'
|
||||
};
|
||||
|
||||
@@ -88,6 +88,16 @@ export function getStartWithVideoMuted(stateful: IStateful) {
|
||||
return Boolean(getPropertyValue(stateful, 'startWithVideoMuted', START_WITH_AUDIO_VIDEO_MUTED_SOURCES));
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines whether screen-share is currently muted.
|
||||
*
|
||||
* @param {Function|Object} stateful - The redux store, state, or {@code getState} function.
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export function isScreenshareMuted(stateful: IStateful) {
|
||||
return Boolean(toState(stateful)['features/base/media'].screenshare.muted);
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines whether video is currently muted.
|
||||
*
|
||||
|
||||
@@ -8,15 +8,17 @@ import {
|
||||
} from '../../analytics/AnalyticsEvents';
|
||||
import { sendAnalytics } from '../../analytics/functions';
|
||||
import { IStore } from '../../app/types';
|
||||
import { MEDIA_TYPE as AVM_MEDIA_TYPE } from '../../av-moderation/constants';
|
||||
import { isForceMuted } from '../../av-moderation/functions';
|
||||
import { APP_STATE_CHANGED } from '../../mobile/background/actionTypes';
|
||||
import { showWarningNotification } from '../../notifications/actions';
|
||||
import { NOTIFICATION_TIMEOUT_TYPE } from '../../notifications/constants';
|
||||
import { isForceMuted } from '../../participants-pane/functions';
|
||||
import { isScreenMediaShared } from '../../screen-share/functions';
|
||||
import { SET_AUDIO_ONLY } from '../audio-only/actionTypes';
|
||||
import { setAudioOnly } from '../audio-only/actions';
|
||||
import { SET_ROOM } from '../conference/actionTypes';
|
||||
import { isRoomValid } from '../conference/functions';
|
||||
import { PARTICIPANT_MUTED_US } from '../participants/actionTypes';
|
||||
import { getLocalParticipant } from '../participants/functions';
|
||||
import MiddlewareRegistry from '../redux/MiddlewareRegistry';
|
||||
import { getPropertyValue } from '../settings/functions.any';
|
||||
@@ -46,7 +48,8 @@ import {
|
||||
import {
|
||||
MEDIA_TYPE,
|
||||
SCREENSHARE_MUTISM_AUTHORITY,
|
||||
VIDEO_MUTISM_AUTHORITY
|
||||
VIDEO_MUTISM_AUTHORITY,
|
||||
VIDEO_TYPE
|
||||
} from './constants';
|
||||
import { getStartWithAudioMuted, getStartWithVideoMuted } from './functions';
|
||||
import logger from './logger';
|
||||
@@ -66,6 +69,24 @@ MiddlewareRegistry.register(store => next => action => {
|
||||
case APP_STATE_CHANGED:
|
||||
return _appStateChanged(store, next, action);
|
||||
|
||||
case PARTICIPANT_MUTED_US: {
|
||||
const { dispatch } = store;
|
||||
const { track } = action;
|
||||
|
||||
// Sync the media muted state with the track muted state.
|
||||
if (track.isAudioTrack()) {
|
||||
dispatch(setAudioMuted(true, /* ensureTrack */ false));
|
||||
} else if (track.isVideoTrack()) {
|
||||
if (track.getVideoType() === VIDEO_TYPE.DESKTOP) {
|
||||
dispatch(setScreenshareMuted(true, SCREENSHARE_MUTISM_AUTHORITY.USER, /* ensureTrack */ false));
|
||||
} else {
|
||||
dispatch(setVideoMuted(true, VIDEO_MUTISM_AUTHORITY.USER, /* ensureTrack */ false));
|
||||
}
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case SET_AUDIO_ONLY:
|
||||
return _setAudioOnly(store, next, action);
|
||||
|
||||
@@ -88,7 +109,7 @@ MiddlewareRegistry.register(store => next => action => {
|
||||
const state = store.getState();
|
||||
const participant = getLocalParticipant(state);
|
||||
|
||||
if (!action.muted && isForceMuted(participant, MEDIA_TYPE.AUDIO, state)) {
|
||||
if (!action.muted && isForceMuted(participant, AVM_MEDIA_TYPE.AUDIO, state)) {
|
||||
return;
|
||||
}
|
||||
break;
|
||||
@@ -113,7 +134,7 @@ MiddlewareRegistry.register(store => next => action => {
|
||||
const state = store.getState();
|
||||
const participant = getLocalParticipant(state);
|
||||
|
||||
if (!action.muted && isForceMuted(participant, MEDIA_TYPE.SCREENSHARE, state)) {
|
||||
if (!action.muted && isForceMuted(participant, AVM_MEDIA_TYPE.DESKTOP, state)) {
|
||||
return;
|
||||
}
|
||||
break;
|
||||
@@ -122,7 +143,7 @@ MiddlewareRegistry.register(store => next => action => {
|
||||
const state = store.getState();
|
||||
const participant = getLocalParticipant(state);
|
||||
|
||||
if (!action.muted && isForceMuted(participant, MEDIA_TYPE.VIDEO, state)) {
|
||||
if (!action.muted && isForceMuted(participant, AVM_MEDIA_TYPE.VIDEO, state)) {
|
||||
return;
|
||||
}
|
||||
break;
|
||||
|
||||
@@ -112,6 +112,17 @@ export const PARTICIPANT_KICKED = 'PARTICIPANT_KICKED';
|
||||
*/
|
||||
export const PARTICIPANT_LEFT = 'PARTICIPANT_LEFT';
|
||||
|
||||
/**
|
||||
* Action to handle case when the remote participant mutes the local participant.
|
||||
*
|
||||
* {
|
||||
* type: PARTICIPANT_MUTED_US,
|
||||
* participant: Participant,
|
||||
* track: JitsiLocalTrack
|
||||
* }
|
||||
*/
|
||||
export const PARTICIPANT_MUTED_US = 'PARTICIPANT_MUTED_US';
|
||||
|
||||
/**
|
||||
* Action to handle case when the sources attached to a participant are updated.
|
||||
*
|
||||
|
||||
@@ -17,6 +17,7 @@ import {
|
||||
PARTICIPANT_JOINED,
|
||||
PARTICIPANT_KICKED,
|
||||
PARTICIPANT_LEFT,
|
||||
PARTICIPANT_MUTED_US,
|
||||
PARTICIPANT_ROLE_CHANGED,
|
||||
PARTICIPANT_SOURCES_UPDATED,
|
||||
PARTICIPANT_UPDATED,
|
||||
@@ -467,19 +468,10 @@ export function participantUpdated(participant: IParticipant = { id: '' }) {
|
||||
* @returns {Promise}
|
||||
*/
|
||||
export function participantMutedUs(participant: any, track: any) {
|
||||
return (dispatch: IStore['dispatch'], getState: IStore['getState']) => {
|
||||
if (!participant) {
|
||||
return;
|
||||
}
|
||||
|
||||
const isAudio = track.isAudioTrack();
|
||||
|
||||
dispatch(showNotification({
|
||||
titleKey: isAudio ? 'notify.mutedRemotelyTitle' : 'notify.videoMutedRemotelyTitle',
|
||||
titleArguments: {
|
||||
participantDisplayName: getParticipantDisplayName(getState, participant.getId())
|
||||
}
|
||||
}, NOTIFICATION_TIMEOUT_TYPE.MEDIUM));
|
||||
return {
|
||||
type: PARTICIPANT_MUTED_US,
|
||||
participant,
|
||||
track
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -2,10 +2,12 @@
|
||||
import { getGravatarURL } from '@jitsi/js-utils/avatar';
|
||||
|
||||
import { IReduxState, IStore } from '../../app/types';
|
||||
import { isVisitorChatParticipant } from '../../chat/functions';
|
||||
import { isStageFilmstripAvailable } from '../../filmstrip/functions';
|
||||
import { isAddPeopleEnabled, isDialOutEnabled } from '../../invite/functions';
|
||||
import { toggleShareDialog } from '../../share-room/actions';
|
||||
import { iAmVisitor } from '../../visitors/functions';
|
||||
import { IVisitorChatParticipant } from '../../visitors/types';
|
||||
import { IStateful } from '../app/types';
|
||||
import { GRAVATAR_BASE_URL } from '../avatar/constants';
|
||||
import { isCORSAvatarURL } from '../avatar/functions';
|
||||
@@ -392,7 +394,7 @@ export function getMutedStateByParticipantAndMediaType(
|
||||
if (mediaType === MEDIA_TYPE.AUDIO) {
|
||||
return Array.from(sources.values())[0].muted;
|
||||
}
|
||||
const videoType = mediaType === MEDIA_TYPE.VIDEO ? VIDEO_TYPE.CAMERA : VIDEO_TYPE.SCREENSHARE;
|
||||
const videoType = mediaType === MEDIA_TYPE.VIDEO ? VIDEO_TYPE.CAMERA : VIDEO_TYPE.DESKTOP;
|
||||
const source = Array.from(sources.values()).find(src => src.videoType === videoType);
|
||||
|
||||
return source?.muted ?? true;
|
||||
@@ -827,18 +829,28 @@ export const setShareDialogVisiblity = (addPeopleFeatureEnabled: boolean, dispat
|
||||
/**
|
||||
* Checks if private chat is enabled for the given participant.
|
||||
*
|
||||
* @param {IParticipant|undefined} participant - The participant to check.
|
||||
* @param {IParticipant|IVisitorChatParticipant|undefined} participant - The participant to check.
|
||||
* @param {IReduxState} state - The Redux state.
|
||||
* @returns {boolean} - True if private chat is enabled, false otherwise.
|
||||
*/
|
||||
export function isPrivateChatEnabled(participant: IParticipant | undefined, state: IReduxState) {
|
||||
export function isPrivateChatEnabled(participant: IParticipant | IVisitorChatParticipant | undefined, state: IReduxState) {
|
||||
const { remoteVideoMenu = {} } = state['features/base/config'];
|
||||
const { disablePrivateChat } = remoteVideoMenu;
|
||||
|
||||
if (participant?.local || state['features/visitors'].iAmVisitor || disablePrivateChat === 'all') {
|
||||
if ((!isVisitorChatParticipant(participant) && participant?.local) || disablePrivateChat === 'all') {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (disablePrivateChat === 'disable-visitor-chat') {
|
||||
// Block if the participant we're trying to message is a visitor
|
||||
// OR if the local user is a visitor
|
||||
if (isVisitorChatParticipant(participant) || iAmVisitor(state)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true; // should allow private chat for other participants
|
||||
}
|
||||
|
||||
if (disablePrivateChat === 'allow-moderator-chat') {
|
||||
return isLocalParticipantModerator(state) || isParticipantModerator(participant);
|
||||
}
|
||||
|
||||
@@ -3,7 +3,19 @@ import { batch } from 'react-redux';
|
||||
import { AnyAction } from 'redux';
|
||||
|
||||
import { IStore } from '../../app/types';
|
||||
import { approveParticipant, approveParticipantAudio, approveParticipantVideo } from '../../av-moderation/actions';
|
||||
import {
|
||||
approveParticipant,
|
||||
approveParticipantAudio,
|
||||
approveParticipantDesktop,
|
||||
approveParticipantVideo
|
||||
} from '../../av-moderation/actions';
|
||||
import {
|
||||
AUDIO_RAISED_HAND_NOTIFICATION_ID,
|
||||
DESKTOP_RAISED_HAND_NOTIFICATION_ID,
|
||||
MEDIA_TYPE,
|
||||
VIDEO_RAISED_HAND_NOTIFICATION_ID
|
||||
} from '../../av-moderation/constants';
|
||||
import { isForceMuted } from '../../av-moderation/functions';
|
||||
import { UPDATE_BREAKOUT_ROOMS } from '../../breakout-rooms/actionTypes';
|
||||
import { getBreakoutRooms } from '../../breakout-rooms/functions';
|
||||
import { toggleE2EE } from '../../e2ee/actions';
|
||||
@@ -15,7 +27,6 @@ import {
|
||||
RAISE_HAND_NOTIFICATION_ID
|
||||
} from '../../notifications/constants';
|
||||
import { open as openParticipantsPane } from '../../participants-pane/actions';
|
||||
import { isForceMuted } from '../../participants-pane/functions';
|
||||
import { CALLING, INVITED } from '../../presence-status/constants';
|
||||
import { RAISE_HAND_SOUND_ID } from '../../reactions/constants';
|
||||
import { RECORDING_OFF_SOUND_ID, RECORDING_ON_SOUND_ID } from '../../recording/constants';
|
||||
@@ -26,7 +37,7 @@ import { IJitsiConference } from '../conference/reducer';
|
||||
import { SET_CONFIG } from '../config/actionTypes';
|
||||
import { getDisableRemoveRaisedHandOnFocus } from '../config/functions.any';
|
||||
import { JitsiConferenceEvents } from '../lib-jitsi-meet';
|
||||
import { MEDIA_TYPE } from '../media/constants';
|
||||
import { VIDEO_TYPE } from '../media/constants';
|
||||
import MiddlewareRegistry from '../redux/MiddlewareRegistry';
|
||||
import StateListenerRegistry from '../redux/StateListenerRegistry';
|
||||
import { playSound, registerSound, unregisterSound } from '../sounds/actions';
|
||||
@@ -43,6 +54,7 @@ import {
|
||||
OVERWRITE_PARTICIPANT_NAME,
|
||||
PARTICIPANT_JOINED,
|
||||
PARTICIPANT_LEFT,
|
||||
PARTICIPANT_MUTED_US,
|
||||
PARTICIPANT_UPDATED,
|
||||
RAISE_HAND_UPDATED,
|
||||
SET_LOCAL_PARTICIPANT_RECORDING_STATUS
|
||||
@@ -292,6 +304,31 @@ MiddlewareRegistry.register(store => next => action => {
|
||||
break;
|
||||
}
|
||||
|
||||
case PARTICIPANT_MUTED_US: {
|
||||
const { dispatch, getState } = store;
|
||||
const { participant, track } = action;
|
||||
let titleKey;
|
||||
|
||||
if (track.isAudioTrack()) {
|
||||
titleKey = 'notify.mutedRemotelyTitle';
|
||||
} else if (track.isVideoTrack()) {
|
||||
if (track.getVideoType() === VIDEO_TYPE.DESKTOP) {
|
||||
titleKey = 'notify.desktopMutedRemotelyTitle';
|
||||
} else {
|
||||
titleKey = 'notify.videoMutedRemotelyTitle';
|
||||
}
|
||||
}
|
||||
|
||||
dispatch(showNotification({
|
||||
titleKey,
|
||||
titleArguments: {
|
||||
participantDisplayName: getParticipantDisplayName(getState, participant.getId())
|
||||
}
|
||||
}, NOTIFICATION_TIMEOUT_TYPE.MEDIUM));
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case PARTICIPANT_UPDATED:
|
||||
return _participantJoinedOrUpdated(store, next, action);
|
||||
|
||||
@@ -786,77 +823,124 @@ function _raiseHandUpdated({ dispatch, getState }: IStore, conference: IJitsiCon
|
||||
APP.API.notifyRaiseHandUpdated(participantId, raisedHandTimestamp);
|
||||
}
|
||||
|
||||
if (!raisedHandTimestamp) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Display notifications about raised hands.
|
||||
|
||||
const isModerator = isLocalParticipantModerator(state);
|
||||
const participant = getParticipantById(state, participantId);
|
||||
const participantName = getParticipantDisplayName(state, participantId);
|
||||
|
||||
let shouldDisplayAllowAudio = false;
|
||||
let shouldDisplayAllowVideo = false;
|
||||
let shouldDisplayAllowDesktop = false;
|
||||
|
||||
if (isModerator) {
|
||||
shouldDisplayAllowAudio = isForceMuted(participant, MEDIA_TYPE.AUDIO, state);
|
||||
shouldDisplayAllowVideo = isForceMuted(participant, MEDIA_TYPE.VIDEO, state);
|
||||
shouldDisplayAllowDesktop = isForceMuted(participant, MEDIA_TYPE.DESKTOP, state);
|
||||
}
|
||||
|
||||
let action;
|
||||
|
||||
if (shouldDisplayAllowAudio || shouldDisplayAllowVideo) {
|
||||
action = {
|
||||
customActionNameKey: [] as string[],
|
||||
customActionHandler: [] as Function[]
|
||||
if (shouldDisplayAllowAudio || shouldDisplayAllowVideo || shouldDisplayAllowDesktop) {
|
||||
const action: {
|
||||
customActionHandler: Array<() => void>;
|
||||
customActionNameKey: string[];
|
||||
} = {
|
||||
customActionHandler: [],
|
||||
customActionNameKey: [],
|
||||
};
|
||||
|
||||
// Always add a "allow all" at the end of the list.
|
||||
action.customActionNameKey.push('notify.allowAll');
|
||||
action.customActionHandler.push(() => {
|
||||
dispatch(approveParticipant(participantId));
|
||||
dispatch(hideNotification(AUDIO_RAISED_HAND_NOTIFICATION_ID));
|
||||
dispatch(hideNotification(DESKTOP_RAISED_HAND_NOTIFICATION_ID));
|
||||
dispatch(hideNotification(VIDEO_RAISED_HAND_NOTIFICATION_ID));
|
||||
});
|
||||
|
||||
if (shouldDisplayAllowAudio) {
|
||||
action.customActionNameKey.push('notify.allowAudio');
|
||||
action.customActionHandler.push(() => {
|
||||
const customActionNameKey = action.customActionNameKey.slice();
|
||||
const customActionHandler = action.customActionHandler.slice();
|
||||
|
||||
customActionNameKey.unshift('notify.allowAudio');
|
||||
customActionHandler.unshift(() => {
|
||||
dispatch(approveParticipantAudio(participantId));
|
||||
dispatch(hideNotification(RAISE_HAND_NOTIFICATION_ID));
|
||||
dispatch(hideNotification(AUDIO_RAISED_HAND_NOTIFICATION_ID));
|
||||
});
|
||||
|
||||
dispatch(showNotification({
|
||||
title: participantName,
|
||||
descriptionKey: 'notify.raisedHand',
|
||||
uid: AUDIO_RAISED_HAND_NOTIFICATION_ID,
|
||||
customActionNameKey,
|
||||
customActionHandler,
|
||||
}, NOTIFICATION_TIMEOUT_TYPE.EXTRA_LONG));
|
||||
}
|
||||
if (shouldDisplayAllowVideo) {
|
||||
action.customActionNameKey.push('notify.allowVideo');
|
||||
action.customActionHandler.push(() => {
|
||||
const customActionNameKey = action.customActionNameKey.slice();
|
||||
const customActionHandler = action.customActionHandler.slice();
|
||||
|
||||
customActionNameKey.unshift('notify.allowVideo');
|
||||
customActionHandler.unshift(() => {
|
||||
dispatch(approveParticipantVideo(participantId));
|
||||
dispatch(hideNotification(RAISE_HAND_NOTIFICATION_ID));
|
||||
dispatch(hideNotification(VIDEO_RAISED_HAND_NOTIFICATION_ID));
|
||||
});
|
||||
|
||||
dispatch(showNotification({
|
||||
title: participantName,
|
||||
descriptionKey: 'notify.raisedHand',
|
||||
uid: VIDEO_RAISED_HAND_NOTIFICATION_ID,
|
||||
customActionNameKey,
|
||||
customActionHandler,
|
||||
}, NOTIFICATION_TIMEOUT_TYPE.EXTRA_LONG));
|
||||
}
|
||||
if (shouldDisplayAllowAudio && shouldDisplayAllowVideo) {
|
||||
action.customActionNameKey.push('notify.allowBoth');
|
||||
action.customActionHandler.push(() => {
|
||||
dispatch(approveParticipant(participantId));
|
||||
dispatch(hideNotification(RAISE_HAND_NOTIFICATION_ID));
|
||||
if (shouldDisplayAllowDesktop) {
|
||||
const customActionNameKey = action.customActionNameKey.slice();
|
||||
const customActionHandler = action.customActionHandler.slice();
|
||||
|
||||
customActionNameKey.unshift('notify.allowDesktop');
|
||||
customActionHandler.unshift(() => {
|
||||
dispatch(approveParticipantDesktop(participantId));
|
||||
dispatch(hideNotification(DESKTOP_RAISED_HAND_NOTIFICATION_ID));
|
||||
});
|
||||
|
||||
dispatch(showNotification({
|
||||
title: participantName,
|
||||
descriptionKey: 'notify.raisedHand',
|
||||
uid: DESKTOP_RAISED_HAND_NOTIFICATION_ID,
|
||||
customActionNameKey,
|
||||
customActionHandler
|
||||
}, NOTIFICATION_TIMEOUT_TYPE.EXTRA_LONG));
|
||||
}
|
||||
} else {
|
||||
action = {
|
||||
customActionNameKey: [ 'notify.viewParticipants' ],
|
||||
customActionHandler: [ () => dispatch(openParticipantsPane()) ]
|
||||
};
|
||||
}
|
||||
|
||||
if (raisedHandTimestamp) {
|
||||
let notificationTitle;
|
||||
const participantName = getParticipantDisplayName(state, participantId);
|
||||
const { raisedHandsQueue } = state['features/base/participants'];
|
||||
|
||||
if (raisedHandsQueue.length > 1) {
|
||||
const raisedHands = raisedHandsQueue.length - 1;
|
||||
|
||||
notificationTitle = i18n.t('notify.raisedHands', {
|
||||
participantName,
|
||||
raisedHands
|
||||
raisedHands: raisedHandsQueue.length - 1
|
||||
});
|
||||
} else {
|
||||
notificationTitle = participantName;
|
||||
}
|
||||
|
||||
dispatch(showNotification({
|
||||
titleKey: 'notify.somebody',
|
||||
title: notificationTitle,
|
||||
descriptionKey: 'notify.raisedHand',
|
||||
concatText: true,
|
||||
uid: RAISE_HAND_NOTIFICATION_ID,
|
||||
...action
|
||||
}, NOTIFICATION_TIMEOUT_TYPE.MEDIUM));
|
||||
dispatch(playSound(RAISE_HAND_SOUND_ID));
|
||||
customActionNameKey: [ 'notify.viewParticipants' ],
|
||||
customActionHandler: [ () => dispatch(openParticipantsPane()) ]
|
||||
}, NOTIFICATION_TIMEOUT_TYPE.LONG));
|
||||
}
|
||||
|
||||
dispatch(playSound(RAISE_HAND_SOUND_ID));
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -61,6 +61,7 @@ export interface IParticipantFeatures {
|
||||
'file-upload'?: boolean | string;
|
||||
'flip'?: boolean | string;
|
||||
'inbound-call'?: boolean | string;
|
||||
'list-visitors'?: boolean | string;
|
||||
'livestreaming'?: boolean | string;
|
||||
'lobby'?: boolean | string;
|
||||
'moderation'?: boolean | string;
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
import { AUDIO_ONLY_SCREEN_SHARE_NO_TRACK } from '../../../../modules/UI/UIErrors';
|
||||
import { IReduxState, IStore } from '../../app/types';
|
||||
import { showModeratedNotification } from '../../av-moderation/actions';
|
||||
import { MEDIA_TYPE as AVM_MEDIA_TYPE } from '../../av-moderation/constants';
|
||||
import { shouldShowModeratedNotification } from '../../av-moderation/functions';
|
||||
import { setNoiseSuppressionEnabled } from '../../noise-suppression/actions';
|
||||
import { showErrorNotification, showNotification } from '../../notifications/actions';
|
||||
@@ -55,10 +56,10 @@ export function toggleScreensharing(
|
||||
shareOptions: IShareOptions = {}) {
|
||||
return (dispatch: IStore['dispatch'], getState: IStore['getState']) => {
|
||||
// check for A/V Moderation when trying to start screen sharing
|
||||
if ((enabled || enabled === undefined) && shouldShowModeratedNotification(MEDIA_TYPE.VIDEO, getState())) {
|
||||
dispatch(showModeratedNotification(MEDIA_TYPE.SCREENSHARE));
|
||||
if ((enabled || enabled === undefined) && shouldShowModeratedNotification(AVM_MEDIA_TYPE.DESKTOP, getState())) {
|
||||
dispatch(showModeratedNotification(AVM_MEDIA_TYPE.DESKTOP));
|
||||
|
||||
return Promise.reject();
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
return _toggleScreenSharing({
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import { IReduxState, IStore } from '../../app/types';
|
||||
import { getSsrcRewritingFeatureFlag } from '../config/functions.any';
|
||||
import { JitsiTrackErrors, browser } from '../lib-jitsi-meet';
|
||||
import { gumPending } from '../media/actions';
|
||||
import { CAMERA_FACING_MODE, MEDIA_TYPE, MediaType, VIDEO_TYPE } from '../media/constants';
|
||||
import { IMediaState } from '../media/reducer';
|
||||
import { IGUMPendingState } from '../media/types';
|
||||
import {
|
||||
getMutedStateByParticipantAndMediaType,
|
||||
getVirtualScreenshareParticipantOwnerId,
|
||||
isScreenShareParticipant
|
||||
} from '../participants/functions';
|
||||
@@ -35,6 +37,10 @@ export function isParticipantMediaMuted(participant: IParticipant | undefined,
|
||||
return false;
|
||||
}
|
||||
|
||||
if (getSsrcRewritingFeatureFlag(state)) {
|
||||
return getMutedStateByParticipantAndMediaType(state, participant, mediaType);
|
||||
}
|
||||
|
||||
const tracks = getTrackState(state);
|
||||
|
||||
if (participant?.local) {
|
||||
@@ -53,10 +59,21 @@ export function isParticipantMediaMuted(participant: IParticipant | undefined,
|
||||
* @param {IReduxState} state - Global state.
|
||||
* @returns {boolean} - Is audio muted for the participant.
|
||||
*/
|
||||
export function isParticipantAudioMuted(participant: IParticipant, state: IReduxState) {
|
||||
export function isParticipantAudioMuted(participant: IParticipant | undefined, state: IReduxState) {
|
||||
return isParticipantMediaMuted(participant, MEDIA_TYPE.AUDIO, state);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the participant is screen-share muted.
|
||||
*
|
||||
* @param {IParticipant} participant - Participant reference.
|
||||
* @param {IReduxState} state - Global state.
|
||||
* @returns {boolean} - Is screen-share muted for the participant.
|
||||
*/
|
||||
export function isParticipantScreenShareMuted(participant: IParticipant | undefined, state: IReduxState) {
|
||||
return isParticipantMediaMuted(participant, MEDIA_TYPE.SCREENSHARE, state);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the participant is video muted.
|
||||
*
|
||||
@@ -118,6 +135,10 @@ export function getLocalJitsiDesktopTrack(state: IReduxState) {
|
||||
* @returns {(Track|undefined)}
|
||||
*/
|
||||
export function getLocalTrack(tracks: ITrack[], mediaType: MediaType, includePending = false) {
|
||||
if (mediaType === MEDIA_TYPE.SCREENSHARE) {
|
||||
return getLocalDesktopTrack(tracks, includePending);
|
||||
}
|
||||
|
||||
return (
|
||||
getLocalTracks(tracks, includePending)
|
||||
.find(t => t.mediaType === mediaType));
|
||||
@@ -216,6 +237,14 @@ export function getTrackByMediaTypeAndParticipant(
|
||||
tracks: ITrack[],
|
||||
mediaType: MediaType,
|
||||
participantId?: string) {
|
||||
if (!participantId) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (mediaType === MEDIA_TYPE.SCREENSHARE) {
|
||||
return getScreenShareTrack(tracks, participantId);
|
||||
}
|
||||
|
||||
return tracks.find(
|
||||
t => Boolean(t.jitsiTrack) && t.participantId === participantId && t.mediaType === mediaType
|
||||
);
|
||||
|
||||
@@ -188,10 +188,6 @@ function _setMuted(store: IStore, { ensureTrack, muted }: {
|
||||
const localTrack = _getLocalTrack(store, mediaType, /* includePending */ true);
|
||||
const state = getState();
|
||||
|
||||
if (mediaType === MEDIA_TYPE.SCREENSHARE && !muted) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (localTrack) {
|
||||
// The `jitsiTrack` property will have a value only for a localTrack for which `getUserMedia` has already
|
||||
// completed. If there's no `jitsiTrack`, then the `muted` state will be applied once the `jitsiTrack` is
|
||||
@@ -203,8 +199,11 @@ function _setMuted(store: IStore, { ensureTrack, muted }: {
|
||||
.catch(() => dispatch(trackMuteUnmuteFailed(localTrack, muted)));
|
||||
}
|
||||
} else if (!muted && ensureTrack) {
|
||||
// TODO(saghul): reconcile these 2 types.
|
||||
const createMediaType = mediaType === MEDIA_TYPE.SCREENSHARE ? 'desktop' : mediaType;
|
||||
|
||||
typeof APP !== 'undefined' && dispatch(gumPending([ mediaType ], IGUMPendingState.PENDING_UNMUTE));
|
||||
dispatch(createLocalTracksA({ devices: [ mediaType ] })).then(() => {
|
||||
dispatch(createLocalTracksA({ devices: [ createMediaType ] })).then(() => {
|
||||
typeof APP !== 'undefined' && dispatch(gumPending([ mediaType ], IGUMPendingState.NONE));
|
||||
});
|
||||
}
|
||||
|
||||
@@ -58,7 +58,7 @@ const IconButton: React.FC<IIconButtonProps> = ({
|
||||
style = { [
|
||||
iconButtonContainerStyles,
|
||||
style
|
||||
] as ViewStyle }
|
||||
] as ViewStyle[] }
|
||||
underlayColor = { underlayColor }>
|
||||
<Icon
|
||||
color = { color }
|
||||
|
||||
@@ -14,7 +14,7 @@ export interface IProps {
|
||||
/**
|
||||
* Label used for accessibility.
|
||||
*/
|
||||
accessibilityLabel: string;
|
||||
accessibilityLabel?: string;
|
||||
|
||||
/**
|
||||
* The context menu item background color.
|
||||
@@ -232,7 +232,7 @@ const ContextMenuItem = ({
|
||||
<div
|
||||
aria-controls = { controls }
|
||||
aria-disabled = { disabled }
|
||||
aria-label = { accessibilityLabel }
|
||||
aria-label = { accessibilityLabel || undefined }
|
||||
aria-selected = { role === 'tab' ? selected : undefined }
|
||||
className = { cx(styles.contextMenuItem,
|
||||
_overflowDrawer && styles.contextMenuItemDrawer,
|
||||
|
||||
@@ -12,13 +12,7 @@ const JITSI_MEET_APPS = [
|
||||
'org.jitsi.meet',
|
||||
|
||||
// Android debug app.
|
||||
'org.jitsi.meet.debug',
|
||||
|
||||
// 8x8 Work (Android).
|
||||
'org.vom8x8.sipua',
|
||||
|
||||
// 8x8 Work (iOS).
|
||||
'com.yourcompany.Virtual-Office'
|
||||
'org.jitsi.meet.debug'
|
||||
];
|
||||
|
||||
/**
|
||||
|
||||
@@ -4,6 +4,7 @@ import { WithTranslation } from 'react-i18next';
|
||||
import { IReduxState, IStore } from '../../app/types';
|
||||
import { getParticipantById } from '../../base/participants/functions';
|
||||
import { IParticipant } from '../../base/participants/types';
|
||||
import { IVisitorChatParticipant } from '../../visitors/types';
|
||||
import { sendMessage, setPrivateMessageRecipient } from '../actions';
|
||||
|
||||
interface IProps extends WithTranslation {
|
||||
@@ -23,6 +24,16 @@ interface IProps extends WithTranslation {
|
||||
*/
|
||||
_participant?: IParticipant;
|
||||
|
||||
/**
|
||||
* The display name of the visitor (if applicable).
|
||||
*/
|
||||
displayName?: string;
|
||||
|
||||
/**
|
||||
* Whether the message is from a visitor.
|
||||
*/
|
||||
isFromVisitor?: boolean;
|
||||
|
||||
/**
|
||||
* The message that is about to be sent.
|
||||
*/
|
||||
@@ -67,9 +78,21 @@ export class AbstractChatPrivacyDialog extends PureComponent<IProps> {
|
||||
* @returns {void}
|
||||
*/
|
||||
_onSendPrivateMessage() {
|
||||
const { message, _onSendMessage, _onSetMessageRecipient, _participant } = this.props;
|
||||
const { message, _onSendMessage, _onSetMessageRecipient, _participant, isFromVisitor, displayName, participantID } = this.props;
|
||||
|
||||
if (isFromVisitor) {
|
||||
// For visitors, create a participant object since they don't exist in the main participant list
|
||||
const visitorParticipant = {
|
||||
id: participantID,
|
||||
name: displayName,
|
||||
isVisitor: true
|
||||
};
|
||||
|
||||
_onSetMessageRecipient(visitorParticipant);
|
||||
} else {
|
||||
_onSetMessageRecipient(_participant);
|
||||
}
|
||||
|
||||
_onSetMessageRecipient(_participant);
|
||||
_onSendMessage(message);
|
||||
|
||||
return true;
|
||||
@@ -88,7 +111,7 @@ export function _mapDispatchToProps(dispatch: IStore['dispatch']) {
|
||||
dispatch(sendMessage(message, true));
|
||||
},
|
||||
|
||||
_onSetMessageRecipient: (participant: IParticipant) => {
|
||||
_onSetMessageRecipient: (participant: IParticipant | IVisitorChatParticipant) => {
|
||||
dispatch(setPrivateMessageRecipient(participant));
|
||||
}
|
||||
};
|
||||
@@ -103,6 +126,6 @@ export function _mapDispatchToProps(dispatch: IStore['dispatch']) {
|
||||
*/
|
||||
export function _mapStateToProps(state: IReduxState, ownProps: IProps) {
|
||||
return {
|
||||
_participant: getParticipantById(state, ownProps.participantID)
|
||||
_participant: ownProps.isFromVisitor ? undefined : getParticipantById(state, ownProps.participantID)
|
||||
};
|
||||
}
|
||||
|
||||
@@ -3,7 +3,9 @@ import { WithTranslation } from 'react-i18next';
|
||||
|
||||
import { IReduxState, IStore } from '../../app/types';
|
||||
import { getParticipantDisplayName, isLocalParticipantModerator } from '../../base/participants/functions';
|
||||
import { getVisitorDisplayName } from '../../visitors/functions';
|
||||
import { setLobbyChatActiveState, setPrivateMessageRecipient } from '../actions.any';
|
||||
import { isVisitorChatParticipant } from '../functions';
|
||||
|
||||
|
||||
export interface IProps extends WithTranslation {
|
||||
@@ -13,6 +15,11 @@ export interface IProps extends WithTranslation {
|
||||
*/
|
||||
_isLobbyChatActive: boolean;
|
||||
|
||||
/**
|
||||
* Whether the private message recipient is a visitor.
|
||||
*/
|
||||
_isVisitor?: boolean;
|
||||
|
||||
/**
|
||||
* The name of the lobby message recipient, if any.
|
||||
*/
|
||||
@@ -72,10 +79,18 @@ export function _mapDispatchToProps(dispatch: IStore['dispatch']) {
|
||||
*/
|
||||
export function _mapStateToProps(state: IReduxState, _ownProps: any) {
|
||||
const { privateMessageRecipient, lobbyMessageRecipient, isLobbyChatActive } = state['features/chat'];
|
||||
let _privateMessageRecipient;
|
||||
const _isVisitor = isVisitorChatParticipant(privateMessageRecipient);
|
||||
|
||||
if (privateMessageRecipient) {
|
||||
_privateMessageRecipient = _isVisitor
|
||||
? getVisitorDisplayName(state, privateMessageRecipient.name)
|
||||
: getParticipantDisplayName(state, privateMessageRecipient.id);
|
||||
}
|
||||
|
||||
return {
|
||||
_privateMessageRecipient:
|
||||
privateMessageRecipient ? getParticipantDisplayName(state, privateMessageRecipient.id) : undefined,
|
||||
_privateMessageRecipient,
|
||||
_isVisitor,
|
||||
_isLobbyChatActive: isLobbyChatActive,
|
||||
_lobbyMessageRecipient:
|
||||
isLobbyChatActive && lobbyMessageRecipient ? lobbyMessageRecipient.name : undefined,
|
||||
|
||||
@@ -122,15 +122,17 @@ class ChatMessage extends Component<IChatMessageProps> {
|
||||
* @returns {React.ReactElement<*> | null}
|
||||
*/
|
||||
_renderDisplayName() {
|
||||
const { message, showDisplayName } = this.props;
|
||||
const { message, showDisplayName, t } = this.props;
|
||||
|
||||
if (!showDisplayName) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const { displayName, isFromVisitor } = message;
|
||||
|
||||
return (
|
||||
<Text style = { styles.senderDisplayName }>
|
||||
{ message.displayName }
|
||||
{ `${displayName}${isFromVisitor ? ` ${t('visitors.chatIndicator')}` : ''}` }
|
||||
</Text>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -12,7 +12,8 @@ import {
|
||||
} from '../../../mobile/navigation/components/conference/ConferenceNavigationContainerRef';
|
||||
import { setLobbyChatActiveState, setPrivateMessageRecipient } from '../../actions.any';
|
||||
import AbstractMessageRecipient, {
|
||||
IProps as AbstractProps
|
||||
IProps as AbstractProps,
|
||||
_mapStateToProps as _mapStateToPropsAbstract
|
||||
} from '../AbstractMessageRecipient';
|
||||
|
||||
import styles from './styles';
|
||||
@@ -36,11 +37,6 @@ interface IProps extends AbstractProps {
|
||||
id: string;
|
||||
name: string;
|
||||
} | ILocalParticipant;
|
||||
|
||||
/**
|
||||
* The participant object set for private messaging.
|
||||
*/
|
||||
privateMessageRecipient: { name: string; };
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -96,7 +92,8 @@ class MessageRecipient extends AbstractMessageRecipient<IProps> {
|
||||
const {
|
||||
isLobbyChatActive,
|
||||
lobbyMessageRecipient,
|
||||
privateMessageRecipient,
|
||||
_privateMessageRecipient,
|
||||
_isVisitor,
|
||||
t
|
||||
} = this.props;
|
||||
|
||||
@@ -120,7 +117,7 @@ class MessageRecipient extends AbstractMessageRecipient<IProps> {
|
||||
);
|
||||
}
|
||||
|
||||
if (!privateMessageRecipient) {
|
||||
if (!_privateMessageRecipient) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -130,7 +127,7 @@ class MessageRecipient extends AbstractMessageRecipient<IProps> {
|
||||
style = { styles.messageRecipientContainer as ViewStyle }>
|
||||
<Text style = { styles.messageRecipientText }>
|
||||
{ t('chat.messageTo', {
|
||||
recipient: privateMessageRecipient.name
|
||||
recipient: `${_privateMessageRecipient}${_isVisitor ? ` ${t('visitors.chatIndicator')}` : ''}`
|
||||
}) }
|
||||
</Text>
|
||||
<TouchableHighlight
|
||||
@@ -157,6 +154,7 @@ function _mapStateToProps(state: IReduxState, _ownProps: any) {
|
||||
const { lobbyMessageRecipient, isLobbyChatActive } = state['features/chat'];
|
||||
|
||||
return {
|
||||
..._mapStateToPropsAbstract(state, _ownProps),
|
||||
isLobbyChatActive,
|
||||
lobbyMessageRecipient
|
||||
};
|
||||
|
||||
@@ -224,11 +224,13 @@ const ChatMessage = ({
|
||||
* @returns {React$Element<*>}
|
||||
*/
|
||||
function _renderDisplayName() {
|
||||
const { displayName, isFromVisitor = false } = message;
|
||||
|
||||
return (
|
||||
<div
|
||||
aria-hidden = { true }
|
||||
className = { cx('display-name', classes.displayName) }>
|
||||
{message.displayName}
|
||||
{`${displayName}${isFromVisitor ? ` ${t('visitors.chatIndicator')}` : ''}`}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -338,6 +340,8 @@ const ChatMessage = ({
|
||||
{!shouldDisplayChatMessageMenu && (
|
||||
<div className = { classes.optionsButtonContainer }>
|
||||
{isHovered && <MessageMenu
|
||||
displayName = { message.displayName }
|
||||
isFromVisitor = { message.isFromVisitor }
|
||||
isLobbyMessage = { message.lobbyChat }
|
||||
message = { message.message }
|
||||
participantId = { message.participantId }
|
||||
@@ -391,6 +395,8 @@ const ChatMessage = ({
|
||||
<div>
|
||||
<div className = { classes.optionsButtonContainer }>
|
||||
{isHovered && <MessageMenu
|
||||
displayName = { message.displayName }
|
||||
isFromVisitor = { message.isFromVisitor }
|
||||
isLobbyMessage = { message.lobbyChat }
|
||||
message = { message.message }
|
||||
participantId = { message.participantId }
|
||||
@@ -414,7 +420,15 @@ function _mapStateToProps(state: IReduxState, { message }: IProps) {
|
||||
const { knocking } = state['features/lobby'];
|
||||
|
||||
const participant = getParticipantById(state, message.participantId);
|
||||
const enablePrivateChat = isPrivateChatEnabled(participant, state);
|
||||
|
||||
// For visitor private messages, participant will be undefined but we should still allow private chat
|
||||
// Create a visitor participant object for visitor messages to pass to isPrivateChatEnabled
|
||||
const participantForCheck = message.isFromVisitor
|
||||
? { id: message.participantId, name: message.displayName, isVisitor: true as const }
|
||||
: participant;
|
||||
|
||||
const enablePrivateChat = (!message.isFromVisitor || message.privateMessage)
|
||||
&& isPrivateChatEnabled(participantForCheck, state);
|
||||
|
||||
return {
|
||||
shouldDisplayChatMessageMenu: enablePrivateChat,
|
||||
|
||||
@@ -15,6 +15,8 @@ import { handleLobbyChatInitialized, openChat } from '../../actions.web';
|
||||
|
||||
export interface IProps {
|
||||
className?: string;
|
||||
displayName?: string;
|
||||
isFromVisitor?: boolean;
|
||||
isLobbyMessage: boolean;
|
||||
message: string;
|
||||
participantId: string;
|
||||
@@ -58,7 +60,7 @@ const useStyles = makeStyles()(theme => {
|
||||
};
|
||||
});
|
||||
|
||||
const MessageMenu = ({ message, participantId, isLobbyMessage, shouldDisplayChatMessageMenu }: IProps) => {
|
||||
const MessageMenu = ({ message, participantId, isFromVisitor, isLobbyMessage, shouldDisplayChatMessageMenu, displayName }: IProps) => {
|
||||
const dispatch = useDispatch();
|
||||
const { classes, cx } = useStyles();
|
||||
const { t } = useTranslation();
|
||||
@@ -82,10 +84,23 @@ const MessageMenu = ({ message, participantId, isLobbyMessage, shouldDisplayChat
|
||||
if (isLobbyMessage) {
|
||||
dispatch(handleLobbyChatInitialized(participantId));
|
||||
} else {
|
||||
dispatch(openChat(participant));
|
||||
// For visitor messages, participant will be undefined but we can still open chat
|
||||
// using the participantId which contains the visitor's original JID
|
||||
if (isFromVisitor) {
|
||||
// Handle visitor participant that doesn't exist in main participant list
|
||||
const visitorParticipant = {
|
||||
id: participantId,
|
||||
name: displayName,
|
||||
isVisitor: true
|
||||
};
|
||||
|
||||
dispatch(openChat(visitorParticipant));
|
||||
} else {
|
||||
dispatch(openChat(participant));
|
||||
}
|
||||
}
|
||||
handleClose();
|
||||
}, [ dispatch, isLobbyMessage, participant, participantId ]);
|
||||
}, [ dispatch, isLobbyMessage, participant, participantId, displayName ]);
|
||||
|
||||
const handleCopyClick = useCallback(() => {
|
||||
copyText(message)
|
||||
|
||||
@@ -48,6 +48,7 @@ const useStyles = makeStyles()(theme => {
|
||||
const MessageRecipient = ({
|
||||
_privateMessageRecipient,
|
||||
_isLobbyChatActive,
|
||||
_isVisitor,
|
||||
_lobbyMessageRecipient,
|
||||
_onRemovePrivateMessageRecipient,
|
||||
_onHideLobbyChatRecipient,
|
||||
@@ -80,9 +81,9 @@ const MessageRecipient = ({
|
||||
id = 'chat-recipient'
|
||||
role = 'alert'>
|
||||
<span className = { classes.text }>
|
||||
{t(_isLobbyChatActive ? 'chat.lobbyChatMessageTo' : 'chat.messageTo', {
|
||||
recipient: _isLobbyChatActive ? _lobbyMessageRecipient : _privateMessageRecipient
|
||||
})}
|
||||
{ _isLobbyChatActive
|
||||
? t('chat.lobbyChatMessageTo', { recipient: _lobbyMessageRecipient })
|
||||
: t('chat.messageTo', { recipient: `${_privateMessageRecipient}${_isVisitor ? ` ${t('visitors.chatIndicator')}` : ''}` }) }
|
||||
</span>
|
||||
<Button
|
||||
accessibilityLabel = { t('dialog.close') }
|
||||
|
||||
@@ -9,10 +9,12 @@ import { getLocalizedDateFormatter } from '../base/i18n/dateUtil';
|
||||
import i18next from '../base/i18n/i18next';
|
||||
import { MEET_FEATURES } from '../base/jwt/constants';
|
||||
import { isJwtFeatureEnabled } from '../base/jwt/functions';
|
||||
import { getParticipantById } from '../base/participants/functions';
|
||||
import { getParticipantById, isPrivateChatEnabled } from '../base/participants/functions';
|
||||
import { IParticipant } from '../base/participants/types';
|
||||
import { escapeRegexp } from '../base/util/helpers';
|
||||
import { getParticipantsPaneWidth } from '../participants-pane/functions';
|
||||
import { VIDEO_SPACE_MIN_SIZE } from '../video-layout/constants';
|
||||
import { IVisitorChatParticipant } from '../visitors/types';
|
||||
|
||||
import { MESSAGE_TYPE_ERROR, MESSAGE_TYPE_LOCAL, TIMESTAMP_FORMAT } from './constants';
|
||||
import { IMessage } from './types';
|
||||
@@ -178,9 +180,24 @@ export function getCanReplyToMessage(state: IReduxState, message: IMessage) {
|
||||
const { knocking } = state['features/lobby'];
|
||||
const participant = getParticipantById(state, message.participantId);
|
||||
|
||||
return Boolean(participant)
|
||||
// Check if basic reply conditions are met
|
||||
const basicCanReply = (Boolean(participant) || message.isFromVisitor)
|
||||
&& (message.privateMessage || (message.lobbyChat && !knocking))
|
||||
&& message.messageType !== MESSAGE_TYPE_LOCAL;
|
||||
|
||||
if (!basicCanReply) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check private chat configuration for visitor messages
|
||||
if (message.isFromVisitor) {
|
||||
const visitorParticipant = { id: message.participantId, name: message.displayName, isVisitor: true as const };
|
||||
|
||||
return isPrivateChatEnabled(visitorParticipant, state);
|
||||
}
|
||||
|
||||
// For non-visitor messages, use the regular participant
|
||||
return isPrivateChatEnabled(participant, state);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -190,8 +207,19 @@ export function getCanReplyToMessage(state: IReduxState, message: IMessage) {
|
||||
* @returns {string}
|
||||
*/
|
||||
export function getPrivateNoticeMessage(message: IMessage) {
|
||||
let recipient;
|
||||
|
||||
if (message.messageType === MESSAGE_TYPE_LOCAL) {
|
||||
// For messages sent by local user, show the recipient name
|
||||
// For visitor messages, use the visitor's display name with indicator
|
||||
recipient = message.sentToVisitor ? `${message.recipient} ${i18next.t('visitors.chatIndicator')}` : message.recipient;
|
||||
} else {
|
||||
// For messages received from others, show "you"
|
||||
recipient = i18next.t('chat.you');
|
||||
}
|
||||
|
||||
return i18next.t('chat.privateNotice', {
|
||||
recipient: message.messageType === MESSAGE_TYPE_LOCAL ? message.recipient : i18next.t('chat.you')
|
||||
recipient
|
||||
});
|
||||
}
|
||||
|
||||
@@ -225,3 +253,15 @@ export function getChatMaxSize(state: IReduxState) {
|
||||
|
||||
return Math.max(clientWidth - getParticipantsPaneWidth(state) - VIDEO_SPACE_MIN_SIZE, 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Type guard to check if a participant is a visitor chat participant.
|
||||
*
|
||||
* @param {IParticipant | IVisitorChatParticipant | undefined} participant - The participant to check.
|
||||
* @returns {boolean} - True if the participant is a visitor chat participant.
|
||||
*/
|
||||
export function isVisitorChatParticipant(
|
||||
participant?: IParticipant | IVisitorChatParticipant
|
||||
): participant is IVisitorChatParticipant {
|
||||
return Boolean(participant && 'isVisitor' in participant && participant.isVisitor === true);
|
||||
}
|
||||
|
||||
@@ -33,8 +33,7 @@ import { pushReactions } from '../reactions/actions.any';
|
||||
import { ENDPOINT_REACTION_NAME } from '../reactions/constants';
|
||||
import { getReactionMessageFromBuffer, isReactionsEnabled } from '../reactions/functions.any';
|
||||
import { showToolbox } from '../toolbox/actions';
|
||||
|
||||
import './subscriber';
|
||||
import { getVisitorDisplayName } from '../visitors/functions';
|
||||
|
||||
import {
|
||||
ADD_MESSAGE,
|
||||
@@ -55,8 +54,9 @@ import {
|
||||
MESSAGE_TYPE_REMOTE,
|
||||
MESSAGE_TYPE_SYSTEM
|
||||
} from './constants';
|
||||
import { getUnreadCount, isSendGroupChatDisabled } from './functions';
|
||||
import { getUnreadCount, isSendGroupChatDisabled, isVisitorChatParticipant } from './functions';
|
||||
import { INCOMING_MSG_SOUND_FILE } from './sounds';
|
||||
import './subscriber';
|
||||
|
||||
/**
|
||||
* Timeout for when to show the privacy notice after a private message was received.
|
||||
@@ -186,6 +186,9 @@ MiddlewareRegistry.register(store => next => action => {
|
||||
|
||||
if (participant) {
|
||||
action.participant = participant;
|
||||
} else if (isVisitorChatParticipant(privateMessageRecipient)) {
|
||||
// Handle visitor participants that don't exist in the main participant list
|
||||
action.participant = privateMessageRecipient;
|
||||
}
|
||||
}
|
||||
} else if (focusedTab === ChatTabs.POLLS) {
|
||||
@@ -204,14 +207,17 @@ MiddlewareRegistry.register(store => next => action => {
|
||||
// recipient. This logic tries to mitigate this risk.
|
||||
const shouldSendPrivateMessageTo = _shouldSendPrivateMessageTo(state, action);
|
||||
|
||||
const participantExists = shouldSendPrivateMessageTo
|
||||
&& getParticipantById(state, shouldSendPrivateMessageTo);
|
||||
if (shouldSendPrivateMessageTo) {
|
||||
const participantExists = getParticipantById(state, shouldSendPrivateMessageTo.id);
|
||||
|
||||
if (shouldSendPrivateMessageTo && participantExists) {
|
||||
dispatch(openDialog(ChatPrivacyDialog, {
|
||||
message: action.message,
|
||||
participantID: shouldSendPrivateMessageTo
|
||||
}));
|
||||
if (participantExists || shouldSendPrivateMessageTo.isFromVisitor) {
|
||||
dispatch(openDialog(ChatPrivacyDialog, {
|
||||
message: action.message,
|
||||
participantID: shouldSendPrivateMessageTo.id,
|
||||
isFromVisitor: shouldSendPrivateMessageTo.isFromVisitor,
|
||||
displayName: shouldSendPrivateMessageTo.name
|
||||
}));
|
||||
}
|
||||
} else {
|
||||
// Sending the message if privacy notice doesn't need to be shown.
|
||||
|
||||
@@ -227,10 +233,10 @@ MiddlewareRegistry.register(store => next => action => {
|
||||
type: LOBBY_CHAT_MESSAGE,
|
||||
message: action.message
|
||||
}, lobbyMessageRecipient.id);
|
||||
_persistSentPrivateMessage(store, lobbyMessageRecipient.id, action.message, true);
|
||||
_persistSentPrivateMessage(store, lobbyMessageRecipient, action.message, true);
|
||||
} else if (privateMessageRecipient) {
|
||||
conference.sendPrivateTextMessage(privateMessageRecipient.id, action.message);
|
||||
_persistSentPrivateMessage(store, privateMessageRecipient.id, action.message);
|
||||
conference.sendPrivateTextMessage(privateMessageRecipient.id, action.message, 'body', isVisitorChatParticipant(privateMessageRecipient));
|
||||
_persistSentPrivateMessage(store, privateMessageRecipient, action.message);
|
||||
} else {
|
||||
conference.sendTextMessage(action.message);
|
||||
}
|
||||
@@ -317,7 +323,7 @@ function _addChatMsgListener(conference: IJitsiConference, store: IStore) {
|
||||
JitsiConferenceEvents.MESSAGE_RECEIVED,
|
||||
/* eslint-disable max-params */
|
||||
(participantId: string, message: string, timestamp: number,
|
||||
displayName: string, isGuest: boolean, messageId: string) => {
|
||||
displayName: string, isFromVisitor: boolean, messageId: string) => {
|
||||
/* eslint-enable max-params */
|
||||
_onConferenceMessageReceived(store, {
|
||||
// in case of messages coming from visitors we can have unknown id
|
||||
@@ -325,7 +331,7 @@ function _addChatMsgListener(conference: IJitsiConference, store: IStore) {
|
||||
message,
|
||||
timestamp,
|
||||
displayName,
|
||||
isGuest,
|
||||
isFromVisitor,
|
||||
messageId,
|
||||
privateMessage: false });
|
||||
|
||||
@@ -350,13 +356,15 @@ function _addChatMsgListener(conference: IJitsiConference, store: IStore) {
|
||||
|
||||
conference.on(
|
||||
JitsiConferenceEvents.PRIVATE_MESSAGE_RECEIVED,
|
||||
(participantId: string, message: string, timestamp: number, messageId: string) => {
|
||||
(participantId: string, message: string, timestamp: number, messageId: string, displayName?: string, isFromVisitor?: boolean) => {
|
||||
_onConferenceMessageReceived(store, {
|
||||
participantId,
|
||||
message,
|
||||
timestamp,
|
||||
displayName,
|
||||
messageId,
|
||||
privateMessage: true
|
||||
privateMessage: true,
|
||||
isFromVisitor
|
||||
});
|
||||
}
|
||||
);
|
||||
@@ -375,8 +383,8 @@ function _addChatMsgListener(conference: IJitsiConference, store: IStore) {
|
||||
* @returns {void}
|
||||
*/
|
||||
function _onConferenceMessageReceived(store: IStore,
|
||||
{ displayName, isGuest, message, messageId, participantId, privateMessage, timestamp }: {
|
||||
displayName?: string; isGuest?: boolean; message: string; messageId?: string;
|
||||
{ displayName, isFromVisitor, message, messageId, participantId, privateMessage, timestamp }: {
|
||||
displayName?: string; isFromVisitor?: boolean; message: string; messageId?: string;
|
||||
participantId: string; privateMessage: boolean; timestamp: number; }
|
||||
) {
|
||||
|
||||
@@ -390,7 +398,7 @@ function _onConferenceMessageReceived(store: IStore,
|
||||
}
|
||||
_handleReceivedMessage(store, {
|
||||
displayName,
|
||||
isGuest,
|
||||
isFromVisitor,
|
||||
participantId,
|
||||
message,
|
||||
privateMessage,
|
||||
@@ -505,8 +513,8 @@ function getLobbyChatDisplayName(state: IReduxState, participantId: string) {
|
||||
* @returns {void}
|
||||
*/
|
||||
function _handleReceivedMessage({ dispatch, getState }: IStore,
|
||||
{ displayName, isGuest, lobbyChat, message, messageId, participantId, privateMessage, timestamp }: {
|
||||
displayName?: string; isGuest?: boolean; lobbyChat: boolean; message: string;
|
||||
{ displayName, isFromVisitor, lobbyChat, message, messageId, participantId, privateMessage, timestamp }: {
|
||||
displayName?: string; isFromVisitor?: boolean; lobbyChat: boolean; message: string;
|
||||
messageId?: string; participantId: string; privateMessage: boolean; timestamp: number; },
|
||||
shouldPlaySound = true,
|
||||
isReaction = false
|
||||
@@ -525,9 +533,17 @@ function _handleReceivedMessage({ dispatch, getState }: IStore,
|
||||
const participant = getParticipantById(state, participantId) || { local: undefined };
|
||||
|
||||
const localParticipant = getLocalParticipant(getState);
|
||||
let displayNameToShow = lobbyChat
|
||||
? getLobbyChatDisplayName(state, participantId)
|
||||
: displayName || getParticipantDisplayName(state, participantId);
|
||||
let _displayName, displayNameToShow;
|
||||
|
||||
if (lobbyChat) {
|
||||
displayNameToShow = _displayName = getLobbyChatDisplayName(state, participantId);
|
||||
} else if (isFromVisitor) {
|
||||
_displayName = getVisitorDisplayName(state, displayName);
|
||||
displayNameToShow = `${_displayName} ${i18next.t('visitors.chatIndicator')}`;
|
||||
} else {
|
||||
displayNameToShow = _displayName = getParticipantDisplayName(state, participantId);
|
||||
}
|
||||
|
||||
const hasRead = participant.local || isChatOpen;
|
||||
const timestampToDate = timestamp ? new Date(timestamp) : new Date();
|
||||
const millisecondsTimestamp = timestampToDate.getTime();
|
||||
@@ -536,12 +552,8 @@ function _handleReceivedMessage({ dispatch, getState }: IStore,
|
||||
const shouldShowNotification = userSelectedNotifications?.['notify.chatMessages']
|
||||
&& !hasRead && !isReaction && (!timestamp || lobbyChat);
|
||||
|
||||
if (isGuest) {
|
||||
displayNameToShow = `${displayNameToShow} ${i18next.t('visitors.chatIndicator')}`;
|
||||
}
|
||||
|
||||
dispatch(addMessage({
|
||||
displayName: displayNameToShow,
|
||||
displayName: _displayName,
|
||||
hasRead,
|
||||
participantId,
|
||||
messageType: participant.local ? MESSAGE_TYPE_LOCAL : MESSAGE_TYPE_REMOTE,
|
||||
@@ -551,7 +563,8 @@ function _handleReceivedMessage({ dispatch, getState }: IStore,
|
||||
recipient: getParticipantDisplayName(state, localParticipant?.id ?? ''),
|
||||
timestamp: millisecondsTimestamp,
|
||||
messageId,
|
||||
isReaction
|
||||
isReaction,
|
||||
isFromVisitor
|
||||
}));
|
||||
|
||||
if (shouldShowNotification) {
|
||||
@@ -574,6 +587,15 @@ function _handleReceivedMessage({ dispatch, getState }: IStore,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Interface for recipient objects used in private messaging.
|
||||
*/
|
||||
interface IRecipient {
|
||||
id: string;
|
||||
isVisitor?: boolean;
|
||||
name?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Persists the sent private messages as if they were received over the muc.
|
||||
*
|
||||
@@ -582,12 +604,12 @@ function _handleReceivedMessage({ dispatch, getState }: IStore,
|
||||
* But those messages should be in the store as well, otherwise they don't appear in the chat window.
|
||||
*
|
||||
* @param {Store} store - The Redux store.
|
||||
* @param {string} recipientID - The ID of the recipient the private message was sent to.
|
||||
* @param {IRecipient} recipient - The recipient the private message was sent to.
|
||||
* @param {string} message - The sent message.
|
||||
* @param {boolean} isLobbyPrivateMessage - Is a lobby message.
|
||||
* @returns {void}
|
||||
*/
|
||||
function _persistSentPrivateMessage({ dispatch, getState }: IStore, recipientID: string,
|
||||
function _persistSentPrivateMessage({ dispatch, getState }: IStore, recipient: IRecipient,
|
||||
message: string, isLobbyPrivateMessage = false) {
|
||||
const state = getState();
|
||||
const localParticipant = getLocalParticipant(state);
|
||||
@@ -598,6 +620,13 @@ function _persistSentPrivateMessage({ dispatch, getState }: IStore, recipientID:
|
||||
const displayName = getParticipantDisplayName(state, localParticipant.id);
|
||||
const { lobbyMessageRecipient } = state['features/chat'];
|
||||
|
||||
const recipientName
|
||||
= recipient.isVisitor
|
||||
? getVisitorDisplayName(state, recipient.name)
|
||||
: (isLobbyPrivateMessage
|
||||
? lobbyMessageRecipient?.name
|
||||
: getParticipantDisplayName(getState, recipient?.id));
|
||||
|
||||
dispatch(addMessage({
|
||||
displayName,
|
||||
hasRead: true,
|
||||
@@ -606,20 +635,19 @@ function _persistSentPrivateMessage({ dispatch, getState }: IStore, recipientID:
|
||||
message,
|
||||
privateMessage: !isLobbyPrivateMessage,
|
||||
lobbyChat: isLobbyPrivateMessage,
|
||||
recipient: isLobbyPrivateMessage
|
||||
? lobbyMessageRecipient?.name
|
||||
: getParticipantDisplayName(getState, recipientID),
|
||||
recipient: recipientName,
|
||||
sentToVisitor: recipient.isVisitor,
|
||||
timestamp: Date.now()
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the ID of the participant who we may have wanted to send the message
|
||||
* Returns the participant info for who we may have wanted to send the message
|
||||
* that we're about to send.
|
||||
*
|
||||
* @param {Object} state - The Redux state.
|
||||
* @param {Object} action - The action being dispatched now.
|
||||
* @returns {string?}
|
||||
* @returns {IRecipient?} - The recipient info or undefined if no notice should be shown.
|
||||
*/
|
||||
function _shouldSendPrivateMessageTo(state: IReduxState, action: AnyAction) {
|
||||
if (action.ignorePrivacy) {
|
||||
@@ -651,7 +679,11 @@ function _shouldSendPrivateMessageTo(state: IReduxState, action: AnyAction) {
|
||||
|
||||
if (lastMessage.privateMessage) {
|
||||
// We show the notice if the last received message was private.
|
||||
return lastMessage.participantId;
|
||||
return {
|
||||
id: lastMessage.participantId,
|
||||
isFromVisitor: Boolean(lastMessage.isFromVisitor),
|
||||
name: lastMessage.displayName
|
||||
};
|
||||
}
|
||||
|
||||
// But messages may come rapidly, we want to protect our users from mis-sending a message
|
||||
@@ -666,7 +698,11 @@ function _shouldSendPrivateMessageTo(state: IReduxState, action: AnyAction) {
|
||||
? recentPrivateMessages[0] : recentPrivateMessages[recentPrivateMessages.length - 1];
|
||||
|
||||
if (recentPrivateMessage) {
|
||||
return recentPrivateMessage.participantId;
|
||||
return {
|
||||
id: recentPrivateMessage.participantId,
|
||||
isFromVisitor: Boolean(recentPrivateMessage.isFromVisitor),
|
||||
name: recentPrivateMessage.displayName
|
||||
};
|
||||
}
|
||||
|
||||
return undefined;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { UPDATE_CONFERENCE_METADATA } from '../base/conference/actionTypes';
|
||||
import { ILocalParticipant, IParticipant } from '../base/participants/types';
|
||||
import ReducerRegistry from '../base/redux/ReducerRegistry';
|
||||
import { IVisitorChatParticipant } from '../visitors/types';
|
||||
|
||||
import {
|
||||
ADD_MESSAGE,
|
||||
@@ -51,7 +52,7 @@ export interface IChatState {
|
||||
} | ILocalParticipant;
|
||||
messages: IMessage[];
|
||||
nbUnreadMessages: number;
|
||||
privateMessageRecipient?: IParticipant;
|
||||
privateMessageRecipient?: IParticipant | IVisitorChatParticipant;
|
||||
width: {
|
||||
current: number;
|
||||
userSet: number | null;
|
||||
@@ -64,6 +65,7 @@ ReducerRegistry.register<IChatState>('features/chat', (state = DEFAULT_STATE, ac
|
||||
const newMessage: IMessage = {
|
||||
displayName: action.displayName,
|
||||
error: action.error,
|
||||
isFromVisitor: Boolean(action.isFromVisitor),
|
||||
participantId: action.participantId,
|
||||
isReaction: action.isReaction,
|
||||
messageId: action.messageId,
|
||||
@@ -73,6 +75,7 @@ ReducerRegistry.register<IChatState>('features/chat', (state = DEFAULT_STATE, ac
|
||||
privateMessage: action.privateMessage,
|
||||
lobbyChat: action.lobbyChat,
|
||||
recipient: action.recipient,
|
||||
sentToVisitor: Boolean(action.sentToVisitor),
|
||||
timestamp: action.timestamp
|
||||
};
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ import { IStore } from '../app/types';
|
||||
export interface IMessage {
|
||||
displayName: string;
|
||||
error?: Object;
|
||||
isFromVisitor?: boolean;
|
||||
isReaction: boolean;
|
||||
lobbyChat: boolean;
|
||||
message: string;
|
||||
@@ -14,6 +15,7 @@ export interface IMessage {
|
||||
privateMessage: boolean;
|
||||
reactions: Map<string, Set<string>>;
|
||||
recipient: string;
|
||||
sentToVisitor?: boolean;
|
||||
timestamp: number;
|
||||
}
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ import { isVpaasMeeting } from '../jaas/functions';
|
||||
import DeepLinkingDesktopPage from './components/DeepLinkingDesktopPage';
|
||||
import DeepLinkingMobilePage from './components/DeepLinkingMobilePage';
|
||||
import NoMobileApp from './components/NoMobileApp';
|
||||
import { _openDesktopApp } from './openDesktopApp';
|
||||
import { _openDesktopApp } from './openDesktopApp.web';
|
||||
|
||||
/**
|
||||
* Generates a deep linking URL based on the current window URL.
|
||||
@@ -1,7 +1,7 @@
|
||||
import MiddlewareRegistry from '../base/redux/MiddlewareRegistry';
|
||||
|
||||
import { OPEN_DESKTOP_APP } from './actionTypes';
|
||||
import { openDesktopApp } from './functions';
|
||||
import { openDesktopApp } from './functions.web';
|
||||
|
||||
/**
|
||||
* Implements the middleware of the deep linking feature.
|
||||
@@ -1,3 +1,4 @@
|
||||
import { executeAfterLoad } from '../app/functions.web';
|
||||
import { IReduxState } from '../app/types';
|
||||
import { URI_PROTOCOL_PATTERN } from '../base/util/uri';
|
||||
|
||||
@@ -16,7 +17,10 @@ export function _openDesktopApp(_state: Object) {
|
||||
const { appScheme } = deeplinkingDesktop;
|
||||
const regex = new RegExp(URI_PROTOCOL_PATTERN, 'gi');
|
||||
|
||||
window.location.href = window.location.href.replace(regex, `${appScheme}:`);
|
||||
// This is needed to workaround https://issues.chromium.org/issues/41398687
|
||||
executeAfterLoad(() => {
|
||||
window.location.href = window.location.href.replace(regex, `${appScheme}:`);
|
||||
});
|
||||
|
||||
return Promise.resolve(true);
|
||||
}
|
||||
@@ -234,6 +234,7 @@ const FileSharing = () => {
|
||||
const handleFileSelect = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
if (e.target.files) {
|
||||
processFiles(e.target.files as FileList, store);
|
||||
e.target.value = ''; // Reset the input value to allow re-uploading the same file
|
||||
}
|
||||
}, [ processFiles ]);
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ import { MEET_FEATURES } from '../base/jwt/constants';
|
||||
import { isJwtFeatureEnabled } from '../base/jwt/functions';
|
||||
import { showErrorNotification } from '../notifications/actions';
|
||||
import { NOTIFICATION_TIMEOUT_TYPE, NOTIFICATION_TYPE } from '../notifications/constants';
|
||||
import { iAmVisitor } from '../visitors/functions';
|
||||
|
||||
import { uploadFiles } from './actions';
|
||||
import { MAX_FILE_SIZE } from './constants';
|
||||
@@ -150,5 +151,7 @@ export const processFiles = (fileList: FileList | File[], store: IStore) => {
|
||||
* @returns {boolean} Indication of whether local user can upload files.
|
||||
*/
|
||||
export function isFileUploadingEnabled(state: IReduxState): boolean {
|
||||
return isJwtFeatureEnabled(state, MEET_FEATURES.FILE_UPLOAD, false) && isFileSharingEnabled(state);
|
||||
return !iAmVisitor(state)
|
||||
&& isJwtFeatureEnabled(state, MEET_FEATURES.FILE_UPLOAD, false)
|
||||
&& isFileSharingEnabled(state);
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ import { addFile, removeFile, updateFileProgress } from './actions';
|
||||
import { getFileExtension } from './functions.any';
|
||||
import logger from './logger';
|
||||
import { IFileMetadata } from './types';
|
||||
import { downloadFile } from './utils';
|
||||
|
||||
|
||||
/**
|
||||
@@ -122,14 +123,14 @@ MiddlewareRegistry.register(store => next => action => {
|
||||
}
|
||||
}))
|
||||
.then((response: any) => response.json())
|
||||
.then((data: { presignedUrl: any; }) => {
|
||||
const url = data.presignedUrl;
|
||||
.then((data: { fileName: string; presignedUrl: string; }) => {
|
||||
const { presignedUrl, fileName } = data;
|
||||
|
||||
if (!url) {
|
||||
if (!presignedUrl) {
|
||||
throw new Error('No presigned URL found in the response.');
|
||||
}
|
||||
|
||||
window.open(url, '_blank', 'noreferrer,noopener');
|
||||
return downloadFile(presignedUrl, fileName);
|
||||
})
|
||||
.catch((error: any) => {
|
||||
logger.warn('Could not download file:', error);
|
||||
|
||||
27
react/features/file-sharing/utils.ts
Normal file
27
react/features/file-sharing/utils.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
const generateDownloadUrl = async (url: string) => {
|
||||
const resp = await fetch(url);
|
||||
const respBlob = await resp.blob();
|
||||
|
||||
const blob = new Blob([ respBlob ]);
|
||||
|
||||
return URL.createObjectURL(blob);
|
||||
};
|
||||
|
||||
export const downloadFile = async (url: string, fileName: string) => {
|
||||
const dowloadUrl = await generateDownloadUrl(url);
|
||||
const link = document.createElement('a');
|
||||
|
||||
if (fileName) {
|
||||
link.download = fileName;
|
||||
}
|
||||
link.href = dowloadUrl;
|
||||
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
link.remove();
|
||||
|
||||
// fix for certain browsers
|
||||
setTimeout(() => {
|
||||
URL.revokeObjectURL(dowloadUrl);
|
||||
}, 0);
|
||||
};
|
||||
@@ -179,6 +179,9 @@ function styles(theme: Theme, props: IProps) {
|
||||
|
||||
'&.top-panel-filmstrip': {
|
||||
flexDirection: 'column' as const
|
||||
},
|
||||
'&.always-show-resize-bar': {
|
||||
backgroundColor: BACKGROUND_COLOR
|
||||
}
|
||||
},
|
||||
|
||||
@@ -245,6 +248,10 @@ function styles(theme: Theme, props: IProps) {
|
||||
* The type of the React {@code Component} props of {@link Filmstrip}.
|
||||
*/
|
||||
export interface IProps extends WithTranslation {
|
||||
/**
|
||||
* Whether to always show the resize bar on filmstrip. This will make the filmstrip always visible.
|
||||
*/
|
||||
_alwaysShowResizeBar?: boolean;
|
||||
|
||||
/**
|
||||
* Additional CSS class names top add to the root.
|
||||
@@ -533,6 +540,7 @@ class Filmstrip extends PureComponent <IProps, IState> {
|
||||
override render() {
|
||||
const filmstripStyle: any = { };
|
||||
const {
|
||||
_alwaysShowResizeBar,
|
||||
_currentLayout,
|
||||
_disableSelfView,
|
||||
_filmstripDisabled,
|
||||
@@ -649,11 +657,12 @@ class Filmstrip extends PureComponent <IProps, IState> {
|
||||
{_resizableFilmstrip
|
||||
? <div
|
||||
className = { clsx('resizable-filmstrip', classes.resizableFilmstripContainer,
|
||||
_topPanelFilmstrip && 'top-panel-filmstrip') }>
|
||||
_topPanelFilmstrip && 'top-panel-filmstrip',
|
||||
_alwaysShowResizeBar && 'always-show-resize-bar') }>
|
||||
<div
|
||||
className = { clsx('dragHandleContainer',
|
||||
classes.dragHandleContainer,
|
||||
isMouseDown && 'visible',
|
||||
(isMouseDown || _alwaysShowResizeBar) && 'visible',
|
||||
_topPanelFilmstrip && 'top-panel')
|
||||
}
|
||||
onMouseDown = { this._onDragHandleMouseDown }>
|
||||
@@ -1081,7 +1090,7 @@ class Filmstrip extends PureComponent <IProps, IState> {
|
||||
function _mapStateToProps(state: IReduxState, ownProps: any) {
|
||||
const { _hasScroll = false, filmstripType, _topPanelFilmstrip, _remoteParticipants } = ownProps;
|
||||
const { toolbarButtons } = state['features/toolbox'];
|
||||
const { iAmRecorder } = state['features/base/config'];
|
||||
const { iAmRecorder, filmstrip: { alwaysShowResizeBar } = {} } = state['features/base/config'];
|
||||
const { topPanelHeight, topPanelVisible, visible, width: verticalFilmstripWidth } = state['features/filmstrip'];
|
||||
const { localScreenShare } = state['features/base/participants'];
|
||||
const reduceHeight = state['features/toolbox'].visible && toolbarButtons?.length;
|
||||
@@ -1134,7 +1143,8 @@ function _mapStateToProps(state: IReduxState, ownProps: any) {
|
||||
_topPanelVisible,
|
||||
_verticalFilmstripWidth: verticalFilmstripWidth.current,
|
||||
_verticalViewMaxWidth: getVerticalViewMaxWidth(state),
|
||||
_videosClassName: videosClassName
|
||||
_videosClassName: videosClassName,
|
||||
_alwaysShowResizeBar: alwaysShowResizeBar
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -81,13 +81,13 @@ export function shouldRemoteVideosBeVisible(state: IReduxState) {
|
||||
// in the filmstrip.
|
||||
const participantCount = getParticipantCountWithFake(state);
|
||||
let pinnedParticipant;
|
||||
const { disable1On1Mode } = state['features/base/config'];
|
||||
const { disable1On1Mode, filmstrip: { alwaysShowResizeBar } = {} } = state['features/base/config'];
|
||||
const { contextMenuOpened } = state['features/base/responsive-ui'];
|
||||
|
||||
return Boolean(
|
||||
contextMenuOpened
|
||||
|| participantCount > 2
|
||||
|
||||
|| alwaysShowResizeBar
|
||||
// Always show the filmstrip when there is another participant to
|
||||
// show and the local video is pinned, or the toolbar is displayed.
|
||||
|| (participantCount > 1
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import KeepAwake from 'react-native-keep-awake';
|
||||
import { activateKeepAwake, deactivateKeepAwake } from '@sayem314/react-native-keep-awake';
|
||||
|
||||
import { getCurrentConference } from '../../base/conference/functions';
|
||||
import StateListenerRegistry from '../../base/redux/StateListenerRegistry';
|
||||
@@ -28,8 +28,8 @@ StateListenerRegistry.register(
|
||||
*/
|
||||
function _setWakeLock(wakeLock: boolean) {
|
||||
if (wakeLock) {
|
||||
KeepAwake.activate();
|
||||
activateKeepAwake();
|
||||
} else {
|
||||
KeepAwake.deactivate();
|
||||
deactivateKeepAwake();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { MODERATION_NOTIFICATIONS } from '../av-moderation/constants';
|
||||
import { MODERATION_NOTIFICATIONS, MediaType } from '../av-moderation/constants';
|
||||
import { IStateful } from '../base/app/types';
|
||||
import { MediaType } from '../base/media/constants';
|
||||
import { toState } from '../base/redux/functions';
|
||||
|
||||
/**
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user