Compare commits

..

1 Commits

Author SHA1 Message Date
damencho
a24ddf205c feat(visitors): Adds enabled flag in the metadata. 2025-07-09 15:51:21 +03:00
141 changed files with 5193 additions and 7968 deletions

View File

@@ -103,7 +103,7 @@ jobs:
android-sdk-build:
name: Build mobile SDK (Android)
runs-on: ubuntu-latest
container: reactnativecommunity/react-native-android:v18.0
container: reactnativecommunity/react-native-android:v13.0
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4

View File

@@ -1,5 +1,4 @@
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
@@ -67,18 +66,9 @@ android {
}
compileOptions {
sourceCompatibility rootProject.ext.javaVersion
targetCompatibility rootProject.ext.javaVersion
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
kotlinOptions {
jvmTarget = rootProject.ext.jvmTargetVersion
}
kotlin {
jvmToolchain(rootProject.ext.jvmToolchainVersion)
}
namespace 'org.jitsi.meet'
}

View File

@@ -4,8 +4,3 @@
-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

View File

@@ -5,52 +5,46 @@ 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 "org.jetbrains.kotlin:kotlin-gradle-plugin:$rootProject.ext.kotlinVersion"
classpath "com.android.tools.build:gradle:$rootProject.ext.gradlePluginVersion"
classpath 'com.android.tools.build:gradle:7.4.2'
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()
@@ -140,7 +134,7 @@ allprojects {
project.version = "${json.version}-jitsi-${versionQualifierNumber}"
task jitsiAndroidSourcesJar(type: Jar) {
archiveClassifier = 'sources'
classifier = 'sources'
from android.sourceSets.main.java.source
}
@@ -191,46 +185,16 @@ allprojects {
}
}
// Force the version of the Android build tools we have chosen on all subprojects.
// 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.
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
}
}
}
}

View File

@@ -11,26 +11,20 @@
# The setting is particularly useful for tweaking memory settings.
# Default value: -Xmx1024m -XX:MaxPermSize=256m
org.gradle.jvmargs=-Xmx4048m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8
org.gradle.jvmargs=-Xmx2048m -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
# 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
android.bundle.enableUncompressedNativeLibs=false
appVersion=99.0.0
sdkVersion=0.0.0

View File

@@ -1,5 +1,5 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.14-bin.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-7.5.1-all.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists

View File

@@ -50,7 +50,6 @@ 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) {
@@ -84,7 +83,7 @@ dependencies {
implementation project(':react-native-screens')
implementation project(':react-native-slider')
implementation project(':react-native-sound')
implementation project(':react-native-splash-view')
implementation project(':react-native-splash-screen')
implementation project(':react-native-svg')
implementation project(':react-native-video')
implementation project(':react-native-webview')
@@ -138,16 +137,8 @@ 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(
nodePath,
"node",
"node_modules/react-native/scripts/bundle.js",
"--platform", "android",
"--dev", "${devEnabled}",
@@ -160,70 +151,6 @@ 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)

View File

@@ -23,10 +23,8 @@ 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;
@@ -37,11 +35,7 @@ public class JitsiInitializer implements Initializer<Boolean> {
public Boolean create(@NonNull Context context) {
Log.d(this.getClass().getCanonicalName(), "create");
try {
SoLoader.init(context, OpenSourceMergedSoMapping.INSTANCE);
} catch (IOException e) {
throw new RuntimeException(e);
}
SoLoader.init(context, /* native exopackage */ false);
// Register our uncaught exception handler.
JitsiMeetUncaughtExceptionHandler.register();
@@ -49,10 +43,6 @@ 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;
}

View File

@@ -22,7 +22,7 @@ import android.os.Bundle;
import com.facebook.react.ReactInstanceManager;
import com.splashview.SplashView;
import org.devio.rn.splashscreen.SplashScreen;
import org.jitsi.meet.sdk.log.JitsiMeetLogger;
public class JitsiMeet {
@@ -92,7 +92,7 @@ public class JitsiMeet {
*/
public static void showSplashScreen(Activity activity) {
try {
SplashView.INSTANCE.showSplashView(activity);
SplashScreen.show(activity);
} catch (Exception e) {
JitsiMeetLogger.e(e, "Failed to show splash screen");
}

View File

@@ -102,10 +102,6 @@ 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);

View File

@@ -17,7 +17,6 @@
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;
@@ -197,6 +196,8 @@ public class JitsiMeetView extends FrameLayout {
}
setBackgroundColor(BACKGROUND_COLOR);
ReactInstanceManagerHolder.initReactInstanceManager((Activity) context);
}
/**

View File

@@ -18,7 +18,6 @@ package org.jitsi.meet.sdk;
import android.annotation.SuppressLint;
import android.app.Activity;
import android.app.Application;
import androidx.annotation.Nullable;
@@ -34,6 +33,7 @@ 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,6 +68,7 @@ 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)));
@@ -89,7 +90,7 @@ class ReactInstanceManagerHolder {
new com.reactnativecommunity.asyncstorage.AsyncStoragePackage(),
new com.ocetnik.timer.BackgroundTimerPackage(),
new com.calendarevents.RNCalendarEventsPackage(),
new com.sayem.keepawake.KCKeepAwakePackage(),
new com.corbt.keepawake.KCKeepAwakePackage(),
new com.facebook.react.shell.MainReactPackage(),
new com.reactnativecommunity.clipboard.ClipboardPackage(),
new com.reactnativecommunity.netinfo.NetInfoPackage(),
@@ -109,7 +110,6 @@ 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.RTNGiphySdkPackage");
Class<?> giphyPackageClass = Class.forName("com.giphyreactnativesdk.GiphyReactNativeSdkPackage");
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 app {@code Application}
* @param activity {@code Activity} current running Activity.
*/
static void initReactInstanceManager(Application app) {
static void initReactInstanceManager(Activity activity) {
if (reactInstanceManager != null) {
return;
}
@@ -232,14 +232,14 @@ class ReactInstanceManagerHolder {
reactInstanceManager
= ReactInstanceManager.builder()
.setApplication(app)
.setCurrentActivity(null)
.setApplication(activity.getApplication())
.setCurrentActivity(activity)
.setBundleAssetName("index.android.bundle")
.setJSMainModulePath("index.android")
.setJavaScriptExecutorFactory(new HermesExecutorFactory())
.addPackages(getReactNativePackages())
.setUseDeveloperSupport(BuildConfig.DEBUG)
.setInitialLifecycleState(LifecycleState.BEFORE_CREATE)
.setInitialLifecycleState(LifecycleState.RESUMED)
.build();
}
}

View File

@@ -1,3 +1,5 @@
rootProject.name = 'jitsi-meet'
include ':app', ':sdk'
include ':react-native-amplitude'
@@ -27,7 +29,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/@sayem314/react-native-keep-awake/android')
project(':react-native-keep-awake').projectDir = new File(rootProject.projectDir, '../node_modules/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'
@@ -42,8 +44,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-view'
project(':react-native-splash-view').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-splash-view/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-svg'
project(':react-native-svg').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-svg/android')
include ':react-native-video'

View File

@@ -1,5 +1,5 @@
module.exports = {
presets: [ 'module:@react-native/babel-preset' ],
presets: [ 'module:metro-react-native-babel-preset' ],
env: {
production: {
plugins: [ 'react-native-paper/babel' ]

View File

@@ -18,6 +18,8 @@ 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,
@@ -50,8 +52,7 @@ import {
commonUserJoinedHandling,
commonUserLeftHandling,
getConferenceOptions,
sendLocalParticipant,
updateTrackMuteState
sendLocalParticipant
} from './react/features/base/conference/functions';
import { getReplaceParticipant, getSsrcRewritingFeatureFlag } from './react/features/base/config/functions';
import { connect } from './react/features/base/connection/actions.web';
@@ -152,6 +153,7 @@ 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';
@@ -702,6 +704,15 @@ 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));
},
@@ -735,6 +746,12 @@ 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
@@ -744,6 +761,11 @@ 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));
},
@@ -997,6 +1019,7 @@ export default {
// Restore initial state.
this._localTracksInitialized = false;
this.isSharingScreen = false;
this.roomName = roomName;
const { tryCreateLocalTracks, errors } = this.createInitialLocalTracks(options);
@@ -1175,6 +1198,8 @@ 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
@@ -1203,6 +1228,7 @@ export default {
*/
async _turnScreenSharingOff(didHaveVideo, ignoreDidHaveVideo) {
this._untoggleScreenSharing = null;
this.videoSwitchInProgress = true;
APP.store.dispatch(stopReceiver());
@@ -1254,11 +1280,13 @@ 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;
@@ -1288,13 +1316,14 @@ 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.');
logger.debug(`Local screensharing audio track stopped. ${this.isSharingScreen}`);
// 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
@@ -1306,6 +1335,21 @@ 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;
@@ -1453,6 +1497,10 @@ 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);
}
}
});
@@ -1664,12 +1712,8 @@ export default {
room.on(
JitsiConferenceEvents.START_MUTED_POLICY_CHANGED,
({ 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);
APP.store.dispatch(
onStartMutedPolicyChanged(audio, video));
}
);

View File

@@ -33,7 +33,6 @@ 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}/.."
)
@@ -67,7 +66,6 @@ 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}/.."
)
@@ -79,12 +77,10 @@ target 'JitsiMeetSDKLite' do
end
post_install do |installer|
react_native_post_install(
installer,
use_native_modules![:reactNativePath],
:mac_catalyst_enabled => false,
# :ccache_enabled => true
:mac_catalyst_enabled => false
)
installer.pods_project.targets.each do |target|
# https://github.com/CocoaPods/CocoaPods/issues/11402
@@ -103,5 +99,4 @@ post_install do |installer|
# Patch SocketRocket to support TLS 1.3
%x(patch Pods/SocketRocket/SocketRocket/SRSecurityPolicy.m -N < patches/ws-tls13.diff)
end

File diff suppressed because it is too large Load Diff

View File

@@ -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.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 13B07FB11A68108700A75B9A /* LaunchScreen.storyboard */; };
13B07FBD1A68108700A75B9A /* LaunchScreen.xib in Resources */ = {isa = PBXBuildFile; fileRef = 13B07FB11A68108700A75B9A /* LaunchScreen.xib */; };
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.storyboard; name = Base; path = src/Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; };
13B07FB21A68108700A75B9A /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/LaunchScreen.xib; 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,6 +250,7 @@
DEA0B7132D7EF7590062A9F6 /* AppDelegate.swift */,
13B07FB51A68108700A75B9A /* Images.xcassets */,
13B07FB61A68108700A75B9A /* Info.plist */,
13B07FB11A68108700A75B9A /* LaunchScreen.xib */,
DEA0B7112D7EF16E0062A9F6 /* ViewController.swift */,
);
path = src;
@@ -474,7 +475,7 @@
buildActionMask = 2147483647;
files = (
13B07FBF1A68108700A75B9A /* Images.xcassets in Resources */,
13B07FBD1A68108700A75B9A /* LaunchScreen.storyboard in Resources */,
13B07FBD1A68108700A75B9A /* LaunchScreen.xib in Resources */,
361974E2A13624D7735D619D /* PrivacyInfo.xcprivacy in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
@@ -686,12 +687,12 @@
name = Interface.storyboard;
sourceTree = "<group>";
};
13B07FB11A68108700A75B9A /* LaunchScreen.storyboard */ = {
13B07FB11A68108700A75B9A /* LaunchScreen.xib */ = {
isa = PBXVariantGroup;
children = (
13B07FB21A68108700A75B9A /* Base */,
);
name = LaunchScreen.storyboard;
name = LaunchScreen.xib;
sourceTree = "<group>";
};
/* End PBXVariantGroup section */

View File

@@ -37,7 +37,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
let vc = ViewController()
self.window?.rootViewController = vc
jitsiMeet.showSplashScreen()
jitsiMeet.showSplashScreen(vc.view)
self.window?.makeKeyAndVisible()

View File

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

View File

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

View File

@@ -90,7 +90,7 @@
<string>LaunchScreen</string>
<key>UIRequiredDeviceCapabilities</key>
<array>
<string>arm64</string>
<string>armv7</string>
</array>
<key>UIStatusBarStyle</key>
<string>UIStatusBarStyleLightContent</string>

View File

@@ -100,9 +100,6 @@ typedef NS_ENUM(NSInteger, WebRTCLoggingSeverity) {
- (BOOL)isCrashReportingDisabled;
/**
* Shows the splash screen.
*/
- (void)showSplashScreen;
- (void)showSplashScreen:(UIView * _Nonnull) rootView;
@end

View File

@@ -23,6 +23,7 @@
#import "JitsiMeetView+Private.h"
#import "RCTBridgeWrapper.h"
#import "ReactUtils.h"
#import "RNSplashScreen.h"
#import "ScheenshareEventEmiter.h"
#import <react-native-webrtc/WebRTCModuleOptions.h>
@@ -220,17 +221,8 @@
return nil;
}
- (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");
}
- (void)showSplashScreen:(UIView*)rootView {
[RNSplashScreen showSplash:@"LaunchScreen" inRootView:rootView];
}
#pragma mark - Property getter / setters

View File

@@ -300,12 +300,6 @@
"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.",
@@ -381,34 +375,22 @@
"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",
@@ -785,9 +767,8 @@
"me": "me",
"notify": {
"OldElectronAPPTitle": "Security vulnerability!",
"allowAll": "Allow All",
"allowAudio": "Allow Audio",
"allowDesktop": "Allow screen sharing",
"allowBoth": "Both",
"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.",
@@ -801,7 +782,6 @@
"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!",
@@ -882,7 +862,6 @@
"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}}",
@@ -902,14 +881,11 @@
"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",
@@ -921,8 +897,6 @@
"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",
@@ -1529,8 +1503,6 @@
"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",

View File

@@ -13,7 +13,7 @@ import {
requestEnableAudioModeration,
requestEnableVideoModeration
} from '../../react/features/av-moderation/actions';
import { isEnabledFromState, isForceMuted } from '../../react/features/av-moderation/functions';
import { isEnabledFromState } from '../../react/features/av-moderation/functions';
import { setAudioOnly } from '../../react/features/base/audio-only/actions';
import {
endConference,
@@ -30,7 +30,6 @@ 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';
@@ -107,17 +106,14 @@ import {
close as closeParticipantsPane,
open as openParticipantsPane
} from '../../react/features/participants-pane/actions';
import { getParticipantsPaneOpen } from '../../react/features/participants-pane/functions';
import { getParticipantsPaneOpen, isForceMuted } 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 {
openCameraCaptureDialog,
toggleScreenshotCaptureSummary
} from '../../react/features/screenshot-capture/actions';
import { 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';
@@ -944,20 +940,6 @@ 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;

View File

@@ -820,27 +820,6 @@ 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.
*

7671
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -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": "3.3.1",
"@giphy/react-native-sdk": "2.3.0",
"@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,14 +34,13 @@
"@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.5.6",
"@react-native-community/slider": "4.4.3",
"@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",
@@ -70,7 +69,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/v2030.0.0+b225c920/lib-jitsi-meet.tgz",
"lib-jitsi-meet": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v2025.0.0+49eb29a8/lib-jitsi-meet.tgz",
"lodash-es": "4.17.21",
"null-loader": "4.0.1",
"optional-require": "1.0.3",
@@ -83,31 +82,32 @@
"react-focus-on": "3.8.1",
"react-i18next": "10.11.4",
"react-linkify": "1.0.0-alpha",
"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": "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-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.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-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-orientation-locker": "1.6.0",
"react-native-pager-view": "6.4.1",
"react-native-paper": "5.10.3",
"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-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-svg-transformer": "1.2.0",
"react-native-tab-view": "3.5.2",
"react-native-url-polyfill": "2.0.0",
"react-native-video": "6.13.0",
"react-native-video": "6.0.0-alpha.11",
"react-native-watch-connectivity": "1.1.0",
"react-native-webrtc": "124.0.4",
"react-native-webview": "13.13.5",
"react-native-webview": "13.8.7",
"react-native-youtube-iframe": "2.3.0",
"react-redux": "7.2.9",
"react-textarea-autosize": "8.3.0",
@@ -133,11 +133,7 @@
"@babel/preset-env": "7.25.9",
"@babel/preset-react": "7.25.9",
"@jitsi/eslint-config": "6.0.4",
"@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",
"@react-native/metro-config": "0.75.5",
"@types/amplitude-js": "8.16.5",
"@types/audioworklet": "0.0.29",
"@types/dom-screen-wake-lock": "1.0.1",
@@ -181,6 +177,7 @@
"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",

View File

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

View File

@@ -5,9 +5,8 @@ buildscript {
}
dependencies {
classpath "com.android.tools.build:gradle:$rootProject.ext.gradlePluginVersion"
classpath 'com.android.tools.build:gradle:3.5.3'
}
namespace 'org.jitsi.meet.reactnativesdk'
}
def isNewArchitectureEnabled() {
@@ -47,8 +46,8 @@ android {
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_17
targetCompatibility JavaVersion.VERSION_17
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
}

View File

@@ -1,5 +1,5 @@
JitsiMeetReactNative_kotlinVersion=2.0.21
JitsiMeetReactNative_minSdkVersion=26
JitsiMeetReactNative_targetSdkVersion=34
JitsiMeetReactNative_compileSdkVersion=34
JitsiMeetReactNative_ndkversion=27.1.12297006
JitsiMeetReactNative_kotlinVersion=1.7.0
JitsiMeetReactNative_minSdkVersion=21
JitsiMeetReactNative_targetSdkVersion=31
JitsiMeetReactNative_compileSdkVersion=31
JitsiMeetReactNative_ndkversion=21.4.7075529

View File

@@ -64,8 +64,7 @@
"@react-native-community/netinfo": "0.0.0",
"@react-native-community/slider": "0.0.0",
"@react-native-google-signin/google-signin": "0.0.0",
"@sayem314/react-native-keep-awake": "0.0.0",
"react-native": "0.0.0",
"react-native": "*",
"react": "*",
"react-native-background-timer": "0.0.0",
"react-native-calendar-events": "0.0.0",
@@ -74,13 +73,14 @@
"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-view": "0.0.0",
"react-native-splash-screen": "0.0.0",
"react-native-svg": "0.0.0",
"react-native-video": "0.0.0",
"react-native-watch-connectivity": "0.0.0",

View File

@@ -63,11 +63,6 @@ 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.
*/

View File

@@ -2,8 +2,7 @@ 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';
// @ts-ignore
import { hideSplash } from 'react-native-splash-view';
import SplashScreen from 'react-native-splash-screen';
import BottomSheetContainer from '../../base/dialog/components/native/BottomSheetContainer';
import DialogContainer from '../../base/dialog/components/native/DialogContainer';
@@ -85,7 +84,7 @@ export class App extends AbstractApp<IProps> {
override async componentDidMount() {
await super.componentDidMount();
hideSplash();
SplashScreen.hide();
const liteTxt = AppInfo.isLiteSDK ? ' (lite)' : '';

View File

@@ -1,10 +1,8 @@
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
@@ -33,27 +31,3 @@ 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);
}
}

View File

@@ -24,6 +24,7 @@ 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';

View File

@@ -2,7 +2,6 @@ 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';

View File

@@ -37,15 +37,6 @@ 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.
*
@@ -64,15 +55,6 @@ 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.
*
@@ -135,7 +117,7 @@ export const PARTICIPANT_REJECTED = 'PARTICIPANT_REJECTED';
/**
* The type of (redux) action which signals that a participant asked to have its audio unmuted.
* The type of (redux) action which signals that a participant asked to have its audio umuted.
*
* {
* type: PARTICIPANT_PENDING_AUDIO

View File

@@ -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,14 +16,11 @@ 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 { MEDIA_TYPE, type MediaType } from './constants';
import { isEnabledFromState, isForceMuted } from './functions';
import { isEnabledFromState } from './functions';
/**
* Action used by moderator to approve audio for a participant.
@@ -45,25 +42,6 @@ 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.
*
@@ -90,11 +68,8 @@ export const approveParticipantVideo = (id: string) => (dispatch: IStore['dispat
* @returns {void}
*/
export const approveParticipant = (id: string) => (dispatch: IStore['dispatch']) => {
batch(() => {
dispatch(approveParticipantAudio(id));
dispatch(approveParticipantDesktop(id));
dispatch(approveParticipantVideo(id));
});
dispatch(approveParticipantAudio(id));
dispatch(approveParticipantVideo(id));
};
/**
@@ -117,26 +92,6 @@ 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.
*
@@ -230,19 +185,6 @@ 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.
*
@@ -269,19 +211,6 @@ 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.
*
@@ -384,3 +313,4 @@ export function participantRejected(id: string, mediaType: MediaType) {
mediaType
};
}

View File

@@ -1,35 +1,18 @@
export type MediaType = 'audio' | 'video' | 'desktop';
import { MEDIA_TYPE } from '../base/media/constants';
/**
* 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.
* Mapping between a media type and the witelist 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' | 'pendingDesktop' | 'pendingVideo'; } = {
export const MEDIA_TYPE_TO_PENDING_STORE_KEY: { [key: string]: 'pendingAudio' | 'pendingVideo'; } = {
[MEDIA_TYPE.AUDIO]: 'pendingAudio',
[MEDIA_TYPE.DESKTOP]: 'pendingDesktop',
[MEDIA_TYPE.VIDEO]: 'pendingVideo'
};
@@ -37,15 +20,11 @@ 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 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 CS_MODERATION_NOTIFICATION_ID = 'screensharing-moderation';
export const MODERATION_NOTIFICATIONS = {
[MEDIA_TYPE.AUDIO]: AUDIO_MODERATION_NOTIFICATION_ID,
[MEDIA_TYPE.DESKTOP]: DESKTOP_MODERATION_NOTIFICATION_ID,
[MEDIA_TYPE.SCREENSHARE]: CS_MODERATION_NOTIFICATION_ID,
[MEDIA_TYPE.VIDEO]: VIDEO_MODERATION_NOTIFICATION_ID
};

View File

@@ -1,14 +1,10 @@
import { IReduxState } from '../app/types';
import { isLocalParticipantModerator, isParticipantModerator } from '../base/participants/functions';
import { MEDIA_TYPE, type MediaType } from '../base/media/constants';
import { isLocalParticipantModerator } from '../base/participants/functions';
import { IParticipant } from '../base/participants/types';
import { isInBreakoutRoom } from '../breakout-rooms/functions';
import {
MEDIA_TYPE,
MEDIA_TYPE_TO_PENDING_STORE_KEY,
MEDIA_TYPE_TO_WHITELIST_STORE_KEY,
MediaType
} from './constants';
import { MEDIA_TYPE_TO_PENDING_STORE_KEY, MEDIA_TYPE_TO_WHITELIST_STORE_KEY } from './constants';
/**
* Returns this feature's root state.
@@ -33,18 +29,10 @@ const EMPTY_ARRAY: any[] = [];
* @param {IReduxState} state - Global state.
* @returns {boolean}
*/
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}`);
}
};
export const isEnabledFromState = (mediaType: MediaType, state: IReduxState) =>
(mediaType === MEDIA_TYPE.AUDIO
? getState(state)?.audioModerationEnabled
: getState(state)?.videoModerationEnabled) === true;
/**
* Returns whether moderation is enabled per media type.
@@ -73,20 +61,11 @@ export const isSupported = () => (state: IReduxState) => {
* @returns {boolean}
*/
export const isLocalParticipantApprovedFromState = (mediaType: MediaType, state: IReduxState) => {
if (isLocalParticipantModerator(state)) {
return true;
}
const approved = (mediaType === MEDIA_TYPE.AUDIO
? getState(state).audioUnmuteApproved
: getState(state).videoUnmuteApproved) === true;
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}`);
}
return approved || isLocalParticipantModerator(state);
};
/**
@@ -155,28 +134,3 @@ 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;
}

View File

@@ -3,12 +3,8 @@ 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 as TRACK_MEDIA_TYPE } from '../base/media/constants';
import {
isAudioMuted,
isScreenshareMuted,
isVideoMuted
} from '../base/media/functions';
import { MEDIA_TYPE, MediaType } from '../base/media/constants';
import { isAudioMuted, isVideoMuted } from '../base/media/functions';
import { PARTICIPANT_UPDATED } from '../base/participants/actionTypes';
import { raiseHand } from '../base/participants/actions';
import {
@@ -34,10 +30,8 @@ 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 {
@@ -55,10 +49,8 @@ import {
ASKED_TO_UNMUTE_NOTIFICATION_ID,
ASKED_TO_UNMUTE_SOUND_ID,
AUDIO_MODERATION_NOTIFICATION_ID,
DESKTOP_MODERATION_NOTIFICATION_ID,
MEDIA_TYPE,
MediaType,
VIDEO_MODERATION_NOTIFICATION_ID,
CS_MODERATION_NOTIFICATION_ID,
VIDEO_MODERATION_NOTIFICATION_ID
} from './constants';
import {
isEnabledFromState,
@@ -98,9 +90,9 @@ MiddlewareRegistry.register(({ dispatch, getState }) => next => action => {
uid = VIDEO_MODERATION_NOTIFICATION_ID;
break;
}
case MEDIA_TYPE.DESKTOP: {
case MEDIA_TYPE.SCREENSHARE: {
titleKey = 'notify.moderationInEffectCSTitle';
uid = DESKTOP_MODERATION_NOTIFICATION_ID;
uid = CS_MODERATION_NOTIFICATION_ID;
break;
}
}
@@ -123,10 +115,6 @@ 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;
@@ -135,10 +123,6 @@ 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;
@@ -235,37 +219,24 @@ 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, TRACK_MEDIA_TYPE.AUDIO));
dispatch(muteLocal(false, MEDIA_TYPE.AUDIO));
dispatch(hideNotification(ASKED_TO_UNMUTE_NOTIFICATION_ID));
});
}
if ((mediaType === MEDIA_TYPE.DESKTOP || getState()['features/av-moderation'].desktopUnmuteApproved)
&& isScreenshareMuted(getState())) {
customActionNameKey.push('notify.unmuteScreen');
customActionHandler.push(() => {
dispatch(muteLocal(false, TRACK_MEDIA_TYPE.SCREENSHARE));
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));
});
}
if ((mediaType === MEDIA_TYPE.VIDEO || getState()['features/av-moderation'].videoUnmuteApproved)
&& isVideoMuted(getState())) {
&& isVideoMuted(getState())) {
customActionNameKey.push('notify.unmuteVideo');
customActionHandler.push(() => {
dispatch(muteLocal(false, TRACK_MEDIA_TYPE.VIDEO));
dispatch(muteLocal(false, 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.
// lower hand as there will be no audio and change in dominant speaker to clear it
dispatch(raiseHand(false));
});
}

View File

@@ -1,3 +1,4 @@
import { MEDIA_TYPE, type MediaType } from '../base/media/constants';
import {
PARTICIPANT_LEFT,
PARTICIPANT_UPDATED
@@ -15,21 +16,14 @@ import {
PARTICIPANT_PENDING_AUDIO,
PARTICIPANT_REJECTED
} from './actionTypes';
import {
MEDIA_TYPE,
MEDIA_TYPE_TO_PENDING_STORE_KEY,
type MediaType
} from './constants';
import { MEDIA_TYPE_TO_PENDING_STORE_KEY } from './constants';
const initialState = {
audioModerationEnabled: false,
desktopModerationEnabled: false,
videoModerationEnabled: false,
audioWhitelist: {},
desktopWhitelist: {},
videoWhitelist: {},
pendingAudio: [],
pendingDesktop: [],
pendingVideo: []
};
@@ -37,11 +31,7 @@ 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;
@@ -87,61 +77,28 @@ ReducerRegistry.register<IAVModerationState>('features/av-moderation',
(state = initialState, action): IAVModerationState => {
switch (action.type) {
case DISABLE_MODERATION: {
let newState = {};
switch (action.mediaType) {
case MEDIA_TYPE.AUDIO:
newState = {
const newState = action.mediaType === MEDIA_TYPE.AUDIO
? {
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: {
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;
}
const newState = action.mediaType === MEDIA_TYPE.AUDIO
? { audioModerationEnabled: true } : { videoModerationEnabled: true };
return {
...state,
@@ -150,25 +107,8 @@ ReducerRegistry.register<IAVModerationState>('features/av-moderation',
}
case LOCAL_PARTICIPANT_APPROVED: {
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;
}
const newState = action.mediaType === MEDIA_TYPE.AUDIO
? { audioUnmuteApproved: true } : { videoUnmuteApproved: true };
return {
...state,
@@ -177,25 +117,8 @@ ReducerRegistry.register<IAVModerationState>('features/av-moderation',
}
case LOCAL_PARTICIPANT_REJECTED: {
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;
}
const newState = action.mediaType === MEDIA_TYPE.AUDIO
? { audioUnmuteApproved: false } : { videoUnmuteApproved: false };
return {
...state,
@@ -223,7 +146,7 @@ ReducerRegistry.register<IAVModerationState>('features/av-moderation',
case PARTICIPANT_UPDATED: {
const participant = action.participant;
const { audioModerationEnabled, desktopModerationEnabled, videoModerationEnabled } = state;
const { audioModerationEnabled, videoModerationEnabled } = state;
let hasStateChanged = false;
// skips changing the reference of pendingAudio or pendingVideo,
@@ -232,10 +155,6 @@ 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);
}
@@ -249,10 +168,9 @@ ReducerRegistry.register<IAVModerationState>('features/av-moderation',
return state;
}
case PARTICIPANT_LEFT: {
const participant = action.participant;
const { audioModerationEnabled, desktopModerationEnabled, videoModerationEnabled } = state;
const { audioModerationEnabled, videoModerationEnabled } = state;
let hasStateChanged = false;
// skips changing the reference of pendingAudio or pendingVideo,
@@ -266,15 +184,6 @@ 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);
@@ -304,13 +213,6 @@ 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,
@@ -334,16 +236,6 @@ 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,
@@ -370,16 +262,6 @@ 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,

View File

@@ -83,8 +83,7 @@ import {
getConferenceState,
getCurrentConference,
getVisitorOptions,
sendLocalParticipant,
updateTrackMuteState
sendLocalParticipant
} from './functions';
import logger from './logger';
import { IConferenceMetadata, IJitsiConference } from './reducer';
@@ -187,15 +186,6 @@ 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:
@@ -1023,8 +1013,6 @@ export function setStartMutedPolicy(
audio: startAudioMuted,
video: startVideoMuted
});
dispatch(onStartMutedPolicyChanged(startAudioMuted, startVideoMuted));
};
}

View File

@@ -40,8 +40,3 @@ 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';

View File

@@ -3,13 +3,9 @@ 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
@@ -26,8 +22,7 @@ import { setObfuscatedRoom } from './actions';
import {
AVATAR_URL_COMMAND,
EMAIL_COMMAND,
JITSI_CONFERENCE_URL_KEY,
START_MUTED_NOTIFICATION_ID
JITSI_CONFERENCE_URL_KEY
} from './constants';
import logger from './logger';
import { IJitsiConference } from './reducer';
@@ -579,42 +574,3 @@ 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));
}
}

View File

@@ -72,6 +72,7 @@ import {
} from './functions';
import logger from './logger';
import { IConferenceMetadata } from './reducer';
import './subscriber';
/**
* Handler for before unload event.

View File

@@ -0,0 +1,61 @@
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));
}
}

View File

@@ -1,6 +1,5 @@
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';
@@ -19,6 +18,7 @@ import {
TOGGLE_CAMERA_FACING_MODE
} from './actionTypes';
import {
MEDIA_TYPE,
MediaType,
SCREENSHARE_MUTISM_AUTHORITY,
VIDEO_MUTISM_AUTHORITY
@@ -56,23 +56,10 @@ export function setAudioAvailable(available: boolean) {
* }}
*/
export function setAudioMuted(muted: boolean, ensureTrack = false) {
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
});
return {
type: SET_AUDIO_MUTED,
ensureTrack,
muted
};
}
@@ -139,9 +126,9 @@ export function setScreenshareMuted(
const state = getState();
// check for A/V Moderation when trying to unmute
if (!muted && shouldShowModeratedNotification(AVM_MEDIA_TYPE.DESKTOP, state)) {
if (!isModerationNotificationDisplayed(AVM_MEDIA_TYPE.DESKTOP, state)) {
ensureTrack && dispatch(showModeratedNotification(AVM_MEDIA_TYPE.DESKTOP));
if (!muted && shouldShowModeratedNotification(MEDIA_TYPE.SCREENSHARE, state)) {
if (!isModerationNotificationDisplayed(MEDIA_TYPE.SCREENSHARE, state)) {
ensureTrack && dispatch(showModeratedNotification(MEDIA_TYPE.SCREENSHARE));
}
return;
@@ -197,9 +184,9 @@ export function setVideoMuted(
const state = getState();
// check for A/V Moderation when trying to unmute
if (!muted && shouldShowModeratedNotification(AVM_MEDIA_TYPE.VIDEO, state)) {
if (!isModerationNotificationDisplayed(AVM_MEDIA_TYPE.VIDEO, state)) {
ensureTrack && dispatch(showModeratedNotification(AVM_MEDIA_TYPE.VIDEO));
if (!muted && shouldShowModeratedNotification(MEDIA_TYPE.VIDEO, state)) {
if (!isModerationNotificationDisplayed(MEDIA_TYPE.VIDEO, state)) {
ensureTrack && dispatch(showModeratedNotification(MEDIA_TYPE.VIDEO));
}
return;

View File

@@ -3,7 +3,7 @@
*
* @enum {string}
*/
export const CAMERA_FACING_MODE: Record<string, string> = {
export const CAMERA_FACING_MODE = {
ENVIRONMENT: 'environment',
USER: 'user'
};

View File

@@ -88,16 +88,6 @@ 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.
*

View File

@@ -8,17 +8,15 @@ 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';
@@ -48,8 +46,7 @@ import {
import {
MEDIA_TYPE,
SCREENSHARE_MUTISM_AUTHORITY,
VIDEO_MUTISM_AUTHORITY,
VIDEO_TYPE
VIDEO_MUTISM_AUTHORITY
} from './constants';
import { getStartWithAudioMuted, getStartWithVideoMuted } from './functions';
import logger from './logger';
@@ -69,24 +66,6 @@ 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);
@@ -109,7 +88,7 @@ MiddlewareRegistry.register(store => next => action => {
const state = store.getState();
const participant = getLocalParticipant(state);
if (!action.muted && isForceMuted(participant, AVM_MEDIA_TYPE.AUDIO, state)) {
if (!action.muted && isForceMuted(participant, MEDIA_TYPE.AUDIO, state)) {
return;
}
break;
@@ -134,7 +113,7 @@ MiddlewareRegistry.register(store => next => action => {
const state = store.getState();
const participant = getLocalParticipant(state);
if (!action.muted && isForceMuted(participant, AVM_MEDIA_TYPE.DESKTOP, state)) {
if (!action.muted && isForceMuted(participant, MEDIA_TYPE.SCREENSHARE, state)) {
return;
}
break;
@@ -143,7 +122,7 @@ MiddlewareRegistry.register(store => next => action => {
const state = store.getState();
const participant = getLocalParticipant(state);
if (!action.muted && isForceMuted(participant, AVM_MEDIA_TYPE.VIDEO, state)) {
if (!action.muted && isForceMuted(participant, MEDIA_TYPE.VIDEO, state)) {
return;
}
break;

View File

@@ -112,17 +112,6 @@ 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.
*

View File

@@ -17,7 +17,6 @@ import {
PARTICIPANT_JOINED,
PARTICIPANT_KICKED,
PARTICIPANT_LEFT,
PARTICIPANT_MUTED_US,
PARTICIPANT_ROLE_CHANGED,
PARTICIPANT_SOURCES_UPDATED,
PARTICIPANT_UPDATED,
@@ -468,10 +467,19 @@ export function participantUpdated(participant: IParticipant = { id: '' }) {
* @returns {Promise}
*/
export function participantMutedUs(participant: any, track: any) {
return {
type: PARTICIPANT_MUTED_US,
participant,
track
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));
};
}

View File

@@ -392,7 +392,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.DESKTOP;
const videoType = mediaType === MEDIA_TYPE.VIDEO ? VIDEO_TYPE.CAMERA : VIDEO_TYPE.SCREENSHARE;
const source = Array.from(sources.values()).find(src => src.videoType === videoType);
return source?.muted ?? true;

View File

@@ -3,19 +3,7 @@ import { batch } from 'react-redux';
import { AnyAction } from 'redux';
import { IStore } from '../../app/types';
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 { approveParticipant, approveParticipantAudio, approveParticipantVideo } from '../../av-moderation/actions';
import { UPDATE_BREAKOUT_ROOMS } from '../../breakout-rooms/actionTypes';
import { getBreakoutRooms } from '../../breakout-rooms/functions';
import { toggleE2EE } from '../../e2ee/actions';
@@ -27,6 +15,7 @@ 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';
@@ -37,7 +26,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 { VIDEO_TYPE } from '../media/constants';
import { MEDIA_TYPE } from '../media/constants';
import MiddlewareRegistry from '../redux/MiddlewareRegistry';
import StateListenerRegistry from '../redux/StateListenerRegistry';
import { playSound, registerSound, unregisterSound } from '../sounds/actions';
@@ -54,7 +43,6 @@ import {
OVERWRITE_PARTICIPANT_NAME,
PARTICIPANT_JOINED,
PARTICIPANT_LEFT,
PARTICIPANT_MUTED_US,
PARTICIPANT_UPDATED,
RAISE_HAND_UPDATED,
SET_LOCAL_PARTICIPANT_RECORDING_STATUS
@@ -304,31 +292,6 @@ 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);
@@ -823,124 +786,77 @@ 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);
}
if (shouldDisplayAllowAudio || shouldDisplayAllowVideo || shouldDisplayAllowDesktop) {
const action: {
customActionHandler: Array<() => void>;
customActionNameKey: string[];
} = {
customActionHandler: [],
customActionNameKey: [],
let action;
if (shouldDisplayAllowAudio || shouldDisplayAllowVideo) {
action = {
customActionNameKey: [] as string[],
customActionHandler: [] as Function[]
};
// 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) {
const customActionNameKey = action.customActionNameKey.slice();
const customActionHandler = action.customActionHandler.slice();
customActionNameKey.unshift('notify.allowAudio');
customActionHandler.unshift(() => {
action.customActionNameKey.push('notify.allowAudio');
action.customActionHandler.push(() => {
dispatch(approveParticipantAudio(participantId));
dispatch(hideNotification(AUDIO_RAISED_HAND_NOTIFICATION_ID));
dispatch(hideNotification(RAISE_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) {
const customActionNameKey = action.customActionNameKey.slice();
const customActionHandler = action.customActionHandler.slice();
customActionNameKey.unshift('notify.allowVideo');
customActionHandler.unshift(() => {
action.customActionNameKey.push('notify.allowVideo');
action.customActionHandler.push(() => {
dispatch(approveParticipantVideo(participantId));
dispatch(hideNotification(VIDEO_RAISED_HAND_NOTIFICATION_ID));
dispatch(hideNotification(RAISE_HAND_NOTIFICATION_ID));
});
dispatch(showNotification({
title: participantName,
descriptionKey: 'notify.raisedHand',
uid: VIDEO_RAISED_HAND_NOTIFICATION_ID,
customActionNameKey,
customActionHandler,
}, NOTIFICATION_TIMEOUT_TYPE.EXTRA_LONG));
}
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));
if (shouldDisplayAllowAudio && shouldDisplayAllowVideo) {
action.customActionNameKey.push('notify.allowBoth');
action.customActionHandler.push(() => {
dispatch(approveParticipant(participantId));
dispatch(hideNotification(RAISE_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: raisedHandsQueue.length - 1
raisedHands
});
} else {
notificationTitle = participantName;
}
dispatch(showNotification({
titleKey: 'notify.somebody',
title: notificationTitle,
descriptionKey: 'notify.raisedHand',
concatText: true,
uid: RAISE_HAND_NOTIFICATION_ID,
customActionNameKey: [ 'notify.viewParticipants' ],
customActionHandler: [ () => dispatch(openParticipantsPane()) ]
}, NOTIFICATION_TIMEOUT_TYPE.LONG));
...action
}, NOTIFICATION_TIMEOUT_TYPE.MEDIUM));
dispatch(playSound(RAISE_HAND_SOUND_ID));
}
dispatch(playSound(RAISE_HAND_SOUND_ID));
}
/**

View File

@@ -2,7 +2,6 @@
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';
@@ -56,10 +55,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(AVM_MEDIA_TYPE.DESKTOP, getState())) {
dispatch(showModeratedNotification(AVM_MEDIA_TYPE.DESKTOP));
if ((enabled || enabled === undefined) && shouldShowModeratedNotification(MEDIA_TYPE.VIDEO, getState())) {
dispatch(showModeratedNotification(MEDIA_TYPE.SCREENSHARE));
return Promise.resolve();
return Promise.reject();
}
return _toggleScreenSharing({

View File

@@ -1,12 +1,10 @@
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';
@@ -37,10 +35,6 @@ export function isParticipantMediaMuted(participant: IParticipant | undefined,
return false;
}
if (getSsrcRewritingFeatureFlag(state)) {
return getMutedStateByParticipantAndMediaType(state, participant, mediaType);
}
const tracks = getTrackState(state);
if (participant?.local) {
@@ -59,21 +53,10 @@ export function isParticipantMediaMuted(participant: IParticipant | undefined,
* @param {IReduxState} state - Global state.
* @returns {boolean} - Is audio muted for the participant.
*/
export function isParticipantAudioMuted(participant: IParticipant | undefined, state: IReduxState) {
export function isParticipantAudioMuted(participant: IParticipant, 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.
*
@@ -135,10 +118,6 @@ 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));
@@ -237,14 +216,6 @@ 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
);

View File

@@ -188,6 +188,10 @@ 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
@@ -199,11 +203,8 @@ 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: [ createMediaType ] })).then(() => {
dispatch(createLocalTracksA({ devices: [ mediaType ] })).then(() => {
typeof APP !== 'undefined' && dispatch(gumPending([ mediaType ], IGUMPendingState.NONE));
});
}

View File

@@ -58,7 +58,7 @@ const IconButton: React.FC<IIconButtonProps> = ({
style = { [
iconButtonContainerStyles,
style
] as ViewStyle[] }
] as ViewStyle }
underlayColor = { underlayColor }>
<Icon
color = { color }

View File

@@ -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.web';
import { _openDesktopApp } from './openDesktopApp';
/**
* Generates a deep linking URL based on the current window URL.

View File

@@ -1,7 +1,7 @@
import MiddlewareRegistry from '../base/redux/MiddlewareRegistry';
import { OPEN_DESKTOP_APP } from './actionTypes';
import { openDesktopApp } from './functions.web';
import { openDesktopApp } from './functions';
/**
* Implements the middleware of the deep linking feature.

View File

@@ -1,4 +1,3 @@
import { executeAfterLoad } from '../app/functions.web';
import { IReduxState } from '../app/types';
import { URI_PROTOCOL_PATTERN } from '../base/util/uri';
@@ -17,10 +16,7 @@ export function _openDesktopApp(_state: Object) {
const { appScheme } = deeplinkingDesktop;
const regex = new RegExp(URI_PROTOCOL_PATTERN, 'gi');
// This is needed to workaround https://issues.chromium.org/issues/41398687
executeAfterLoad(() => {
window.location.href = window.location.href.replace(regex, `${appScheme}:`);
});
window.location.href = window.location.href.replace(regex, `${appScheme}:`);
return Promise.resolve(true);
}

View File

@@ -1,4 +1,4 @@
import { activateKeepAwake, deactivateKeepAwake } from '@sayem314/react-native-keep-awake';
import KeepAwake from '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) {
activateKeepAwake();
KeepAwake.activate();
} else {
deactivateKeepAwake();
KeepAwake.deactivate();
}
}

View File

@@ -1,5 +1,6 @@
import { MODERATION_NOTIFICATIONS, MediaType } from '../av-moderation/constants';
import { MODERATION_NOTIFICATIONS } from '../av-moderation/constants';
import { IStateful } from '../base/app/types';
import { MediaType } from '../base/media/constants';
import { toState } from '../base/redux/functions';
/**

View File

@@ -11,7 +11,6 @@ import {
requestEnableAudioModeration,
requestEnableVideoModeration
} from '../../../av-moderation/actions';
import { MEDIA_TYPE } from '../../../av-moderation/constants';
import {
isEnabled as isAvModerationEnabled,
isSupported as isAvModerationSupported
@@ -21,6 +20,7 @@ import { hideSheet, openDialog } from '../../../base/dialog/actions';
import BottomSheet from '../../../base/dialog/components/native/BottomSheet';
import Icon from '../../../base/icons/components/Icon';
import { IconCheck, IconRaiseHand, IconVideoOff } from '../../../base/icons/svg';
import { MEDIA_TYPE } from '../../../base/media/constants';
import { raiseHand } from '../../../base/participants/actions';
import { getRaiseHandsQueue, isLocalParticipantModerator } from '../../../base/participants/functions';
import { LOWER_HAND_MESSAGE } from '../../../base/tracks/constants';

View File

@@ -2,9 +2,12 @@ import React, { PureComponent } from 'react';
import { connect } from 'react-redux';
import { IReduxState, IStore } from '../../../app/types';
import { getSsrcRewritingFeatureFlag } from '../../../base/config/functions.any';
import { translate } from '../../../base/i18n/functions';
import { MEDIA_TYPE } from '../../../base/media/constants';
import {
getLocalParticipant,
getMutedStateByParticipantAndMediaType,
getParticipantById,
getParticipantDisplayName,
hasRaisedHand,
@@ -166,8 +169,12 @@ function mapStateToProps(state: IReduxState, ownProps: any) {
const { participant } = ownProps;
const { ownerId } = state['features/shared-video'];
const localParticipantId = getLocalParticipant(state)?.id;
const _isAudioMuted = isParticipantAudioMuted(participant, state);
const _isVideoMuted = isParticipantVideoMuted(participant, state);
const _isAudioMuted = getSsrcRewritingFeatureFlag(state)
? Boolean(participant && getMutedStateByParticipantAndMediaType(state, participant, MEDIA_TYPE.AUDIO))
: Boolean(participant && isParticipantAudioMuted(participant, state));
const _isVideoMuted = getSsrcRewritingFeatureFlag(state)
? Boolean(participant && getMutedStateByParticipantAndMediaType(state, participant, MEDIA_TYPE.VIDEO))
: isParticipantVideoMuted(participant, state);
const audioMediaState = getParticipantAudioMediaState(participant, _isAudioMuted, state);
const videoMediaState = getParticipantVideoMediaState(participant, _isVideoMuted, state);
const { disableModeratorIndicator } = state['features/base/config'];

View File

@@ -6,7 +6,6 @@ import AutoSizer from 'react-virtualized-auto-sizer';
import { FixedSizeList } from 'react-window';
import { makeStyles } from 'tss-react/mui';
import { IReduxState } from '../../../app/types';
import Icon from '../../../base/icons/components/Icon';
import { IconArrowDown, IconArrowUp } from '../../../base/icons/svg';
import { withPixelLineHeight } from '../../../base/styles/functions.web';
@@ -79,7 +78,6 @@ export default function CurrentVisitorsList({ searchString }: IProps) {
const visitors = useSelector(getVisitorsList);
const featureEnabled = useSelector(isVisitorsListEnabled);
const shouldDisplayList = useSelector(shouldDisplayCurrentVisitorsList);
const { defaultRemoteDisplayName } = useSelector((state: IReduxState) => state['features/base/config']);
const { t } = useTranslation();
const { classes } = useStyles();
const dispatch = useDispatch();
@@ -111,11 +109,9 @@ export default function CurrentVisitorsList({ searchString }: IProps) {
return null;
}
const filtered = visitors.filter(v => {
const displayName = v.name || defaultRemoteDisplayName || 'Fellow Jitster';
return normalizeAccents(displayName).toLowerCase().includes(normalizeAccents(searchString).toLowerCase());
});
const filtered = visitors.filter(v =>
normalizeAccents(v.name).toLowerCase().includes(normalizeAccents(searchString).toLowerCase())
);
// ListItem height is 56px including padding so the item size
// for virtualization needs to match it exactly to avoid clipping.
@@ -129,7 +125,7 @@ export default function CurrentVisitorsList({ searchString }: IProps) {
<ParticipantItem
actionsTrigger = { ACTION_TRIGGER.HOVER }
audioMediaState = { MEDIA_STATE.NONE }
displayName = { v.name || defaultRemoteDisplayName || 'Fellow Jitster' }
displayName = { v.name }
participantID = { v.id }
videoMediaState = { MEDIA_STATE.NONE } />
</div>

View File

@@ -6,13 +6,10 @@ import { makeStyles } from 'tss-react/mui';
import { IReduxState } from '../../../app/types';
import {
requestDisableAudioModeration,
requestDisableDesktopModeration,
requestDisableVideoModeration,
requestEnableAudioModeration,
requestEnableDesktopModeration,
requestEnableVideoModeration
} from '../../../av-moderation/actions';
import { MEDIA_TYPE } from '../../../av-moderation/constants';
import {
isEnabled as isAvModerationEnabled,
isSupported as isAvModerationSupported
@@ -20,11 +17,10 @@ import {
import { openDialog } from '../../../base/dialog/actions';
import {
IconCheck,
IconCloseLarge,
IconDotsHorizontal,
IconScreenshare,
IconVideoOff
} from '../../../base/icons/svg';
import { MEDIA_TYPE } from '../../../base/media/constants';
import { getRaiseHandsQueue } from '../../../base/participants/functions';
import { withPixelLineHeight } from '../../../base/styles/functions.web';
import ContextMenu from '../../../base/ui/components/web/ContextMenu';
@@ -34,7 +30,6 @@ import { openSettingsDialog } from '../../../settings/actions.web';
import { SETTINGS_TABS } from '../../../settings/constants';
import { shouldShowModeratorSettings } from '../../../settings/functions.web';
import LowerHandButton from '../../../video-menu/components/web/LowerHandButton';
import MuteEveryonesDesktopDialog from '../../../video-menu/components/web/MuteEveryonesDesktopDialog';
import MuteEveryonesVideoDialog from '../../../video-menu/components/web/MuteEveryonesVideoDialog';
const useStyles = makeStyles()(theme => {
@@ -91,16 +86,17 @@ export const FooterContextMenu = ({ isOpen, onDrawerClose, onMouseLeave }: IProp
const raisedHandsQueue = useSelector(getRaiseHandsQueue);
const isModeratorSettingsTabEnabled = useSelector(shouldShowModeratorSettings);
const isAudioModerationEnabled = useSelector(isAvModerationEnabled(MEDIA_TYPE.AUDIO));
const isDesktopModerationEnabled = useSelector(isAvModerationEnabled(MEDIA_TYPE.DESKTOP));
const isVideoModerationEnabled = useSelector(isAvModerationEnabled(MEDIA_TYPE.VIDEO));
const isBreakoutRoom = useSelector(isInBreakoutRoom);
const { t } = useTranslation();
const disableAudioModeration = useCallback(() => dispatch(requestDisableAudioModeration()), [ dispatch ]);
const disableDesktopModeration = useCallback(() => dispatch(requestDisableDesktopModeration()), [ dispatch ]);
const disableVideoModeration = useCallback(() => dispatch(requestDisableVideoModeration()), [ dispatch ]);
const enableAudioModeration = useCallback(() => dispatch(requestEnableAudioModeration()), [ dispatch ]);
const enableDesktopModeration = useCallback(() => dispatch(requestEnableDesktopModeration()), [ dispatch ]);
const enableVideoModeration = useCallback(() => dispatch(requestEnableVideoModeration()), [ dispatch ]);
const { classes } = useStyles();
@@ -108,9 +104,6 @@ export const FooterContextMenu = ({ isOpen, onDrawerClose, onMouseLeave }: IProp
const muteAllVideo = useCallback(
() => dispatch(openDialog(MuteEveryonesVideoDialog)), [ dispatch ]);
const muteAllDesktop = useCallback(
() => dispatch(openDialog(MuteEveryonesDesktopDialog)), [ dispatch ]);
const openModeratorSettings = () => dispatch(openSettingsDialog(SETTINGS_TABS.MODERATOR));
const actions = [
@@ -120,7 +113,7 @@ export const FooterContextMenu = ({ isOpen, onDrawerClose, onMouseLeave }: IProp
id: isAudioModerationEnabled
? 'participants-pane-context-menu-stop-audio-moderation'
: 'participants-pane-context-menu-start-audio-moderation',
icon: isAudioModerationEnabled ? IconCloseLarge : IconCheck,
icon: !isAudioModerationEnabled && IconCheck,
onClick: isAudioModerationEnabled ? disableAudioModeration : enableAudioModeration,
text: t('participantsPane.actions.audioModeration')
}, {
@@ -129,18 +122,9 @@ export const FooterContextMenu = ({ isOpen, onDrawerClose, onMouseLeave }: IProp
id: isVideoModerationEnabled
? 'participants-pane-context-menu-stop-video-moderation'
: 'participants-pane-context-menu-start-video-moderation',
icon: isVideoModerationEnabled ? IconCloseLarge : IconCheck,
icon: !isVideoModerationEnabled && IconCheck,
onClick: isVideoModerationEnabled ? disableVideoModeration : enableVideoModeration,
text: t('participantsPane.actions.videoModeration')
}, {
accessibilityLabel: t('participantsPane.actions.desktopModeration'),
className: isDesktopModerationEnabled ? classes.indentedLabel : '',
id: isDesktopModerationEnabled
? 'participants-pane-context-menu-stop-desktop-moderation'
: 'participants-pane-context-menu-start-desktop-moderation',
icon: isDesktopModerationEnabled ? IconCloseLarge : IconCheck,
onClick: isDesktopModerationEnabled ? disableDesktopModeration : enableDesktopModeration,
text: t('participantsPane.actions.desktopModeration')
}
];
@@ -153,22 +137,13 @@ export const FooterContextMenu = ({ isOpen, onDrawerClose, onMouseLeave }: IProp
onDrawerClose = { onDrawerClose }
onMouseLeave = { onMouseLeave }>
<ContextMenuItemGroup
actions = { [
{
accessibilityLabel: t('participantsPane.actions.stopEveryonesVideo'),
id: 'participants-pane-context-menu-stop-video',
icon: IconVideoOff,
onClick: muteAllVideo,
text: t('participantsPane.actions.stopEveryonesVideo')
},
{
accessibilityLabel: t('participantsPane.actions.stopEveryonesDesktop'),
id: 'participants-pane-context-menu-stop-desktop',
icon: IconScreenshare,
onClick: muteAllDesktop,
text: t('participantsPane.actions.stopEveryonesDesktop')
}
] } />
actions = { [ {
accessibilityLabel: t('participantsPane.actions.stopEveryonesVideo'),
id: 'participants-pane-context-menu-stop-video',
icon: IconVideoOff,
onClick: muteAllVideo,
text: t('participantsPane.actions.stopEveryonesVideo')
} ] } />
{raisedHandsQueue.length !== 0 && <LowerHandButton />}
{!isBreakoutRoom && isModerationSupported && (
<ContextMenuItemGroup actions = { actions }>

View File

@@ -2,10 +2,12 @@ import React, { useCallback, useEffect, useState } from 'react';
import { connect } from 'react-redux';
import { IReduxState } from '../../../app/types';
import { getSsrcRewritingFeatureFlag } from '../../../base/config/functions.any';
import { JitsiTrackEvents } from '../../../base/lib-jitsi-meet';
import { MEDIA_TYPE } from '../../../base/media/constants';
import {
getLocalParticipant,
getMutedStateByParticipantAndMediaType,
getParticipantByIdOrUndefined,
getParticipantDisplayName,
hasRaisedHand,
@@ -121,6 +123,11 @@ interface IProps {
*/
isInBreakoutRoom: boolean;
/**
* Callback used to open a confirmation dialog for audio muting.
*/
muteAudio: Function;
/**
* The translated text for the mute participant button.
*/
@@ -146,6 +153,7 @@ interface IProps {
*/
overflowDrawer: boolean;
/**
* The aria-label for the ellipsis action.
*/
@@ -156,6 +164,11 @@ interface IProps {
*/
participantID?: string;
/**
* Callback used to stop a participant's video.
*/
stopVideo: Function;
/**
* The translated "you" text.
*/
@@ -183,11 +196,13 @@ function MeetingParticipantItem({
_videoMediaState,
isHighlighted,
isInBreakoutRoom,
muteAudio,
onContextMenu,
onLeave,
openDrawerForParticipant,
overflowDrawer,
participantActionEllipsisLabel,
stopVideo,
youText
}: IProps) {
@@ -253,8 +268,10 @@ function MeetingParticipantItem({
{!isInBreakoutRoom && (
<ParticipantQuickAction
buttonType = { _quickActionButtonType }
muteAudio = { muteAudio }
participantID = { _participantID }
participantName = { _displayName } />
participantName = { _displayName }
stopVideo = { stopVideo } />
)}
<ParticipantActionEllipsis
accessibilityLabel = { participantActionEllipsisLabel }
@@ -287,11 +304,15 @@ function _mapStateToProps(state: IReduxState, ownProps: any) {
const participant = getParticipantByIdOrUndefined(state, participantID);
const _displayName = getParticipantDisplayName(state, participant?.id ?? '');
const _matchesSearch = participantMatchesSearch(participant, searchString);
const _isAudioMuted = isParticipantAudioMuted(participant, state);
const _isVideoMuted = isParticipantVideoMuted(participant, state);
const _isAudioMuted = getSsrcRewritingFeatureFlag(state)
? Boolean(participant && getMutedStateByParticipantAndMediaType(state, participant, MEDIA_TYPE.AUDIO))
: Boolean(participant && isParticipantAudioMuted(participant, state));
const _isVideoMuted = getSsrcRewritingFeatureFlag(state)
? Boolean(participant && getMutedStateByParticipantAndMediaType(state, participant, MEDIA_TYPE.VIDEO))
: isParticipantVideoMuted(participant, state);
const _audioMediaState = getParticipantAudioMediaState(participant, _isAudioMuted, state);
const _videoMediaState = getParticipantVideoMediaState(participant, _isVideoMuted, state);
const _quickActionButtonType = getQuickActionButtonType(participant, state);
const _quickActionButtonType = getQuickActionButtonType(participant, _isAudioMuted, _isVideoMuted, state);
const tracks = state['features/base/tracks'];
const _audioTrack = participantID === localParticipantId

View File

@@ -19,6 +19,11 @@ interface IProps {
*/
lowerMenu: Function;
/**
* Callback used to open a confirmation dialog for audio muting.
*/
muteAudio: Function;
/**
* The translated text for the mute participant button.
*/
@@ -54,6 +59,11 @@ interface IProps {
*/
searchString?: string;
/**
* Callback used to stop a participant's video.
*/
stopVideo: Function;
/**
* Callback for the activation of this item's context menu.
*/
@@ -74,12 +84,14 @@ function MeetingParticipantItems({
isInBreakoutRoom,
lowerMenu,
toggleMenu,
muteAudio,
participantIds,
openDrawerForParticipant,
overflowDrawer,
raiseContextId,
participantActionEllipsisLabel,
searchString,
stopVideo,
youText
}: IProps) {
const renderParticipant = (id: string) => (
@@ -87,6 +99,7 @@ function MeetingParticipantItems({
isHighlighted = { raiseContextId === id }
isInBreakoutRoom = { isInBreakoutRoom }
key = { id }
muteAudio = { muteAudio }
onContextMenu = { toggleMenu(id) }
onLeave = { lowerMenu }
openDrawerForParticipant = { openDrawerForParticipant }
@@ -94,6 +107,7 @@ function MeetingParticipantItems({
participantActionEllipsisLabel = { participantActionEllipsisLabel }
participantID = { id }
searchString = { searchString }
stopVideo = { stopVideo }
youText = { youText } />
);

View File

@@ -1,10 +1,12 @@
import React from 'react';
import React, { useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { connect, useSelector } from 'react-redux';
import { connect, useDispatch, useSelector } from 'react-redux';
import { makeStyles } from 'tss-react/mui';
import { IReduxState } from '../../../app/types';
import { rejectParticipantAudio, rejectParticipantVideo } from '../../../av-moderation/actions';
import participantsPaneTheme from '../../../base/components/themes/participantsPaneTheme.json';
import { MEDIA_TYPE } from '../../../base/media/constants';
import { getParticipantById, isScreenShareParticipant } from '../../../base/participants/functions';
import { withPixelLineHeight } from '../../../base/styles/functions.web';
import Input from '../../../base/ui/components/web/Input';
@@ -12,6 +14,7 @@ import useContextMenu from '../../../base/ui/hooks/useContextMenu.web';
import { normalizeAccents } from '../../../base/util/strings.web';
import { getBreakoutRooms, getCurrentRoomId, isInBreakoutRoom } from '../../../breakout-rooms/functions';
import { isButtonEnabled, showOverflowDrawer } from '../../../toolbox/functions.web';
import { muteRemote } from '../../../video-menu/actions.web';
import { iAmVisitor } from '../../../visitors/functions';
import { getSortedParticipantIds, isCurrentRoomRenamable, shouldRenderInviteButton } from '../../functions';
import { useParticipantDrawer } from '../../hooks';
@@ -79,9 +82,18 @@ function MeetingParticipants({
showInviteButton,
sortedParticipantIds = []
}: IProps) {
const dispatch = useDispatch();
const { t } = useTranslation();
const [ lowerMenu, , toggleMenu, menuEnter, menuLeave, raiseContext ] = useContextMenu<string>();
const muteAudio = useCallback(id => () => {
dispatch(muteRemote(id, MEDIA_TYPE.AUDIO));
dispatch(rejectParticipantAudio(id));
}, [ dispatch ]);
const stopVideo = useCallback(id => () => {
dispatch(muteRemote(id, MEDIA_TYPE.VIDEO));
dispatch(rejectParticipantVideo(id));
}, [ dispatch ]);
const [ drawerParticipant, closeDrawer, openDrawerForParticipant ] = useParticipantDrawer();
// FIXME:
@@ -128,18 +140,21 @@ function MeetingParticipants({
<MeetingParticipantItems
isInBreakoutRoom = { isBreakoutRoom }
lowerMenu = { lowerMenu }
muteAudio = { muteAudio }
openDrawerForParticipant = { openDrawerForParticipant }
overflowDrawer = { overflowDrawer }
participantActionEllipsisLabel = { participantActionEllipsisLabel }
participantIds = { sortedParticipantIds }
raiseContextId = { raiseContext.entity }
searchString = { normalizeAccents(searchString) }
stopVideo = { stopVideo }
toggleMenu = { toggleMenu }
youText = { youText } />
</div>
<MeetingParticipantContextMenu
closeDrawer = { closeDrawer }
drawerParticipant = { drawerParticipant }
muteAudio = { muteAudio }
offsetTarget = { raiseContext?.offsetTarget }
onEnter = { menuEnter }
onLeave = { menuLeave }

View File

@@ -3,17 +3,8 @@ import { useTranslation } from 'react-i18next';
import { useDispatch } from 'react-redux';
import { makeStyles } from 'tss-react/mui';
import {
approveParticipantAudio,
approveParticipantDesktop,
approveParticipantVideo,
rejectParticipantAudio,
rejectParticipantDesktop,
rejectParticipantVideo
} from '../../../av-moderation/actions';
import { MEDIA_TYPE } from '../../../base/media/constants';
import { approveParticipantAudio, approveParticipantVideo } from '../../../av-moderation/actions';
import Button from '../../../base/ui/components/web/Button';
import { muteRemote } from '../../../video-menu/actions.web';
import { QUICK_ACTION_BUTTON } from '../../constants';
interface IProps {
@@ -33,6 +24,11 @@ interface IProps {
*/
buttonType: string;
/**
* Callback used to open a confirmation dialog for audio muting.
*/
muteAudio: Function;
/**
* Label for mute participant button.
*/
@@ -48,6 +44,11 @@ interface IProps {
*/
participantName: string;
/**
* Callback used to stop a participant's video.
*/
stopVideo: Function;
}
const useStyles = makeStyles()(theme => {
@@ -60,8 +61,10 @@ const useStyles = makeStyles()(theme => {
const ParticipantQuickAction = ({
buttonType,
muteAudio,
participantID,
participantName
participantName,
stopVideo
}: IProps) => {
const { classes: styles } = useStyles();
const dispatch = useDispatch();
@@ -71,29 +74,10 @@ const ParticipantQuickAction = ({
dispatch(approveParticipantAudio(participantID));
}, [ dispatch, participantID ]);
const allowDesktop = useCallback(() => {
dispatch(approveParticipantDesktop(participantID));
}, [ dispatch, participantID ]);
const allowVideo = useCallback(() => {
dispatch(approveParticipantVideo(participantID));
}, [ dispatch, participantID ]);
const muteAudio = useCallback(() => {
dispatch(muteRemote(participantID, MEDIA_TYPE.AUDIO));
dispatch(rejectParticipantAudio(participantID));
}, [ dispatch, participantID ]);
const stopDesktop = useCallback(() => {
dispatch(muteRemote(participantID, MEDIA_TYPE.SCREENSHARE));
dispatch(rejectParticipantDesktop(participantID));
}, [ dispatch, participantID ]);
const stopVideo = useCallback(() => {
dispatch(muteRemote(participantID, MEDIA_TYPE.VIDEO));
dispatch(rejectParticipantVideo(participantID));
}, [ dispatch, participantID ]);
switch (buttonType) {
case QUICK_ACTION_BUTTON.MUTE: {
return (
@@ -101,7 +85,7 @@ const ParticipantQuickAction = ({
accessibilityLabel = { `${t('participantsPane.actions.mute')} ${participantName}` }
className = { styles.button }
label = { t('participantsPane.actions.mute') }
onClick = { muteAudio }
onClick = { muteAudio(participantID) }
size = 'small'
testId = { `mute-audio-${participantID}` } />
);
@@ -117,17 +101,6 @@ const ParticipantQuickAction = ({
testId = { `unmute-audio-${participantID}` } />
);
}
case QUICK_ACTION_BUTTON.ALLOW_DESKTOP: {
return (
<Button
accessibilityLabel = { `${t('participantsPane.actions.askDesktop')} ${participantName}` }
className = { styles.button }
label = { t('participantsPane.actions.allowDesktop') }
onClick = { allowDesktop }
size = 'small'
testId = { `unmute-desktop-${participantID}` } />
);
}
case QUICK_ACTION_BUTTON.ALLOW_VIDEO: {
return (
<Button
@@ -139,24 +112,13 @@ const ParticipantQuickAction = ({
testId = { `unmute-video-${participantID}` } />
);
}
case QUICK_ACTION_BUTTON.STOP_DESKTOP: {
return (
<Button
accessibilityLabel = { `${t('participantsPane.actions.stopDesktop')} ${participantName}` }
className = { styles.button }
label = { t('participantsPane.actions.stopDesktop') }
onClick = { stopDesktop }
size = 'small'
testId = { `mute-desktop-${participantID}` } />
);
}
case QUICK_ACTION_BUTTON.STOP_VIDEO: {
return (
<Button
accessibilityLabel = { `${t('participantsPane.actions.mute')} ${participantName}` }
className = { styles.button }
label = { t('participantsPane.actions.stopVideo') }
onClick = { stopVideo }
onClick = { stopVideo(participantID) }
size = 'small'
testId = { `mute-video-${participantID}` } />
);

View File

@@ -45,6 +45,7 @@ const useStyles = makeStyles<IStylesProps>()((theme, { isChatOpen }) => {
participantsPane: {
backgroundColor: theme.palette.ui01,
flexShrink: 0,
overflow: 'hidden',
position: 'relative',
transition: 'width .16s ease-in-out',
width: '315px',
@@ -71,6 +72,7 @@ const useStyles = makeStyles<IStylesProps>()((theme, { isChatOpen }) => {
container: {
boxSizing: 'border-box',
flex: 1,
overflowY: 'auto',
position: 'relative',
padding: `0 ${participantsPaneTheme.panePadding}px`,
display: 'flex',

View File

@@ -36,27 +36,22 @@ export const MEDIA_STATE: { [key: string]: MediaState; } = {
NONE: 'None'
};
export type QuickActionButtonType
= 'Mute' | 'AskToUnmute' | 'AllowVideo' | 'StopVideo' | 'AllowDesktop' | 'StopDesktop' | 'None';
export type QuickActionButtonType = 'Mute' | 'AskToUnmute' | 'AllowVideo' | 'StopVideo' | 'None';
/**
* Enum of possible participant mute button states.
*/
export const QUICK_ACTION_BUTTON: {
ALLOW_DESKTOP: QuickActionButtonType;
ALLOW_VIDEO: QuickActionButtonType;
ASK_TO_UNMUTE: QuickActionButtonType;
MUTE: QuickActionButtonType;
NONE: QuickActionButtonType;
STOP_DESKTOP: QuickActionButtonType;
STOP_VIDEO: QuickActionButtonType;
} = {
ALLOW_DESKTOP: 'AllowDesktop',
ALLOW_VIDEO: 'AllowVideo',
MUTE: 'Mute',
ASK_TO_UNMUTE: 'AskToUnmute',
NONE: 'None',
STOP_DESKTOP: 'StopDesktop',
STOP_VIDEO: 'StopVideo'
};

View File

@@ -1,7 +1,8 @@
import { IReduxState } from '../app/types';
import { MEDIA_TYPE as AVM_MEDIA_TYPE } from '../av-moderation/constants';
import {
isForceMuted,
isEnabledFromState,
isLocalParticipantApprovedFromState,
isParticipantApproved,
isSupported
} from '../av-moderation/functions';
import { IStateful } from '../base/app/types';
@@ -9,26 +10,48 @@ import theme from '../base/components/themes/participantsPaneTheme.json';
import { getCurrentConference } from '../base/conference/functions';
import { INVITE_ENABLED, PARTICIPANTS_ENABLED } from '../base/flags/constants';
import { getFeatureFlag } from '../base/flags/functions';
import { MEDIA_TYPE, type MediaType } from '../base/media/constants';
import {
getDominantSpeakerParticipant,
getLocalParticipant,
getRaiseHandsQueue,
getRemoteParticipantsSorted,
isLocalParticipantModerator
isLocalParticipantModerator,
isParticipantModerator
} from '../base/participants/functions';
import { IParticipant } from '../base/participants/types';
import { toState } from '../base/redux/functions';
import {
isParticipantAudioMuted,
isParticipantScreenShareMuted,
isParticipantVideoMuted
} from '../base/tracks/functions.any';
import { normalizeAccents } from '../base/util/strings';
import { BREAKOUT_ROOMS_RENAME_FEATURE } from '../breakout-rooms/constants';
import { isInBreakoutRoom } from '../breakout-rooms/functions';
import { MEDIA_STATE, QUICK_ACTION_BUTTON, REDUCER_KEY } from './constants';
/**
* 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;
}
/**
* Determines the audio media state (the mic icon) for a participant.
*
@@ -46,7 +69,7 @@ export function getParticipantAudioMediaState(participant: IParticipant | undefi
}
if (muted) {
if (isForceMuted(participant, AVM_MEDIA_TYPE.AUDIO, state)) {
if (isForceMuted(participant, MEDIA_TYPE.AUDIO, state)) {
return MEDIA_STATE.FORCE_MUTED;
}
@@ -71,7 +94,7 @@ export function getParticipantAudioMediaState(participant: IParticipant | undefi
export function getParticipantVideoMediaState(participant: IParticipant | undefined,
muted: Boolean, state: IReduxState) {
if (muted) {
if (isForceMuted(participant, AVM_MEDIA_TYPE.VIDEO, state)) {
if (isForceMuted(participant, MEDIA_TYPE.VIDEO, state)) {
return MEDIA_STATE.FORCE_MUTED;
}
@@ -116,44 +139,33 @@ export const getParticipantsPaneOpen = (state: IReduxState) => Boolean(getState(
* The button is displayed when hovering a participant from the participant list.
*
* @param {IParticipant} participant - The participant.
* @param {boolean} isAudioMuted - If audio is muted for the participant.
* @param {boolean} isVideoMuted - If audio is muted for the participant.
* @param {IReduxState} state - The redux state.
* @returns {string} - The type of the quick action button.
*/
export function getQuickActionButtonType(participant: IParticipant | undefined, state: IReduxState) {
if (!isLocalParticipantModerator(state)) {
return QUICK_ACTION_BUTTON.NONE;
}
export function getQuickActionButtonType(
participant: IParticipant | undefined,
isAudioMuted: Boolean,
isVideoMuted: Boolean,
state: IReduxState) {
// handled only by moderators
const isVideoForceMuted = isForceMuted(participant, MEDIA_TYPE.VIDEO, state);
const isParticipantSilent = participant?.isSilent || false;
// Handled only by moderators.
const isAudioMuted = isParticipantAudioMuted(participant, state);
const isScreenShareMuted = isParticipantScreenShareMuted(participant, state);
const isVideoMuted = isParticipantVideoMuted(participant, state);
const isDesktopForceMuted = isForceMuted(participant, AVM_MEDIA_TYPE.DESKTOP, state);
const isVideoForceMuted = isForceMuted(participant, AVM_MEDIA_TYPE.VIDEO, state);
const isParticipantSilent = participant?.isSilent ?? false;
if (!isAudioMuted && !isParticipantSilent) {
return QUICK_ACTION_BUTTON.MUTE;
}
if (!isVideoMuted) {
return QUICK_ACTION_BUTTON.STOP_VIDEO;
}
if (!isScreenShareMuted) {
return QUICK_ACTION_BUTTON.STOP_DESKTOP;
}
if (isSupported()(state) && !isParticipantSilent) {
return QUICK_ACTION_BUTTON.ASK_TO_UNMUTE;
}
if (isVideoForceMuted) {
return QUICK_ACTION_BUTTON.ALLOW_VIDEO;
}
if (isDesktopForceMuted) {
return QUICK_ACTION_BUTTON.ALLOW_DESKTOP;
if (isLocalParticipantModerator(state)) {
if (!isAudioMuted && !isParticipantSilent) {
return QUICK_ACTION_BUTTON.MUTE;
}
if (!isVideoMuted) {
return QUICK_ACTION_BUTTON.STOP_VIDEO;
}
if (isSupported()(state) && !isParticipantSilent) {
return QUICK_ACTION_BUTTON.ASK_TO_UNMUTE;
}
if (isVideoForceMuted) {
return QUICK_ACTION_BUTTON.ALLOW_VIDEO;
}
}
return QUICK_ACTION_BUTTON.NONE;

View File

@@ -1,3 +0,0 @@
import { getLogger } from '../base/logging/functions';
export default getLogger('features/polls');

View File

@@ -16,7 +16,6 @@ import {
COMMAND_NEW_POLL,
COMMAND_OLD_POLLS
} from './constants';
import logger from './logger';
import { IAnswer, IPoll, IPollData } from './types';
/**
@@ -44,16 +43,7 @@ const parsePollData = (pollData: Partial<IPollData>): IPoll | null => {
const { id, senderId, question, answers } = pollData;
if (typeof id !== 'string' || typeof senderId !== 'string'
|| typeof question !== 'string' || !(answers instanceof Array)) {
logger.error('Malformed poll data received:', pollData);
return null;
}
// Validate answers.
if (answers.some(answer => typeof answer !== 'string')) {
logger.error('Malformed answers data received:', answers);
|| typeof question !== 'string' || !(answers instanceof Array)) {
return null;
}
@@ -183,7 +173,7 @@ function _handleReceivePollsMessage(data: any, dispatch: IStore['dispatch'], get
const receivedAnswer: IAnswer = {
voterId,
pollId,
answers: answers.slice(0, MAX_ANSWERS).map(Boolean)
answers: answers.slice(0, MAX_ANSWERS)
};
dispatch(receiveAnswer(pollId, receivedAnswer));
@@ -198,7 +188,7 @@ function _handleReceivePollsMessage(data: any, dispatch: IStore['dispatch'], get
const poll = parsePollData(pollData);
if (poll === null) {
logger.warn('Malformed old poll data', pollData);
console.warn('[features/polls] Invalid old poll data');
} else {
dispatch(receivePoll(pollData.id, poll, false));
}

View File

@@ -1,154 +0,0 @@
import { Theme } from '@mui/material';
import React, { useCallback, useRef } from 'react';
import { WithTranslation } from 'react-i18next';
import { connect, useDispatch } from 'react-redux';
import { makeStyles } from 'tss-react/mui';
import { hideDialog } from '../base/dialog/actions';
import { translate } from '../base/i18n/functions';
import Label from '../base/label/components/web/Label';
import { CAMERA_FACING_MODE } from '../base/media/constants';
import Button from '../base/ui/components/web/Button';
import Dialog from '../base/ui/components/web/Dialog';
import { BUTTON_TYPES } from '../base/ui/constants.any';
import { ICameraCapturePayload } from './actionTypes';
const useStyles = makeStyles()((theme: Theme) => ({
container: {
display: 'flex',
height: '100%',
width: '100%',
justifyContent: 'center',
alignItems: 'center',
flexDirection: 'column',
gap: theme.spacing(3),
textAlign: 'center'
},
buttonsContainer: {
display: 'flex',
flexDirection: 'column',
gap: theme.spacing(3),
width: '100%',
maxWidth: '100%'
},
hidden: {
display: 'none'
},
label: {
background: 'transparent',
margin: `${theme.spacing(3)} 0 ${theme.spacing(6)}`,
},
button: {
width: '100%',
height: '48px',
maxWidth: '400px'
}
}));
/**
* The type of {@link CameraCaptureDialog}'s React {@code Component} props.
*/
interface IProps extends WithTranslation {
/**
* Callback function on file input changed.
*/
callback: ({ error, dataURL }: { dataURL?: string; error?: string; }) => void;
/**
* The camera capture payload.
*/
componentProps: ICameraCapturePayload;
}
/**
* Implements the Camera capture dialog.
*
* @param {Object} props - The props of the component.
* @returns {React$Element}
*/
const CameraCaptureDialog = ({
callback,
componentProps,
t,
}: IProps) => {
const { cameraFacingMode = CAMERA_FACING_MODE.ENVIRONMENT,
descriptionText,
titleText } = componentProps;
const dispatch = useDispatch();
const { classes } = useStyles();
const inputRef = useRef<HTMLInputElement>(null);
const onCancel = useCallback(() => {
callback({
error: 'User canceled!'
});
dispatch(hideDialog());
}, []);
const onSubmit = useCallback(() => {
inputRef.current?.click();
}, []);
const onInputChange = useCallback(event => {
const reader = new FileReader();
const files = event.target.files;
if (!files?.[0]) {
callback({
error: 'No picture selected!'
});
return;
}
reader.onload = () => {
callback({ dataURL: reader.result as string });
dispatch(hideDialog());
};
reader.onerror = () => {
callback({ error: 'Failed generating base64 URL!' });
dispatch(hideDialog());
};
reader.readAsDataURL(files[0]);
}, []);
return (
<Dialog
cancel = {{ hidden: true }}
disableAutoHideOnSubmit = { true }
ok = {{ hidden: true }}
onCancel = { onCancel }
titleKey = { titleText || t('dialog.cameraCaptureDialog.title') }>
<div className = { classes.container }>
<Label
aria-label = { descriptionText || t('dialog.cameraCaptureDialog.description') }
className = { classes.label }
text = { descriptionText || t('dialog.cameraCaptureDialog.description') } />
<div className = { classes.buttonsContainer } >
<Button
accessibilityLabel = { t('dialog.cameraCaptureDialog.ok') }
className = { classes.button }
labelKey = { 'dialog.cameraCaptureDialog.ok' }
onClick = { onSubmit } />
<Button
accessibilityLabel = { t('dialog.cameraCaptureDialog.reject') }
className = { classes.button }
labelKey = { 'dialog.cameraCaptureDialog.reject' }
onClick = { onCancel }
type = { BUTTON_TYPES.TERTIARY } />
<input
accept = 'image/*'
capture = { cameraFacingMode }
className = { classes.hidden }
onChange = { onInputChange }
ref = { inputRef }
type = 'file' />
</div>
</div>
</Dialog>
);
};
export default translate(connect()(CameraCaptureDialog));

View File

@@ -7,23 +7,3 @@
*/
export const SET_SCREENSHOT_CAPTURE = 'SET_SCREENSHOT_CAPTURE';
/**
* The camera capture payload.
*/
export interface ICameraCapturePayload {
/**
* Selected camera on open.
*/
cameraFacingMode?: string;
/**
* Custom explanatory text to show.
*/
descriptionText?: string,
/**
* Custom dialog title text.
*/
titleText?: string
};

View File

@@ -1,10 +1,7 @@
import { IStore } from '../app/types';
import { openDialog } from '../base/dialog/actions';
import { isMobileBrowser } from '../base/environment/utils';
import { getLocalJitsiDesktopTrack } from '../base/tracks/functions';
import CameraCaptureDialog from './CameraCaptureDialog';
import { ICameraCapturePayload, SET_SCREENSHOT_CAPTURE } from './actionTypes';
import { SET_SCREENSHOT_CAPTURE } from './actionTypes';
import { createScreenshotCaptureSummary } from './functions';
import logger from './logger';
@@ -64,23 +61,3 @@ export function toggleScreenshotCaptureSummary(enabled: boolean) {
return Promise.resolve();
};
}
/**
* Opens {@code CameraCaptureDialog}.
*
* @param {Function} callback - The callback to execute on picture taken.
* @param {ICameraCapturePayload} componentProps - The camera capture payload.
* @returns {Function}
*/
export function openCameraCaptureDialog(callback: Function, componentProps: ICameraCapturePayload) {
return (dispatch: IStore['dispatch']) => {
if (!isMobileBrowser()) {
return;
}
dispatch(openDialog(CameraCaptureDialog, {
callback,
componentProps
}));
};
}

View File

@@ -1,11 +1,11 @@
import { IReduxState } from '../app/types';
import { MEDIA_TYPE } from '../av-moderation/constants';
import { isEnabledFromState } from '../av-moderation/functions';
import { IStateful } from '../base/app/types';
import { isNameReadOnly } from '../base/config/functions.any';
import { SERVER_URL_CHANGE_ENABLED } from '../base/flags/constants';
import { getFeatureFlag } from '../base/flags/functions';
import i18next, { DEFAULT_LANGUAGE, LANGUAGES } from '../base/i18n/i18next';
import { MEDIA_TYPE } from '../base/media/constants';
import { getLocalParticipant } from '../base/participants/functions';
import { toState } from '../base/redux/functions';
import { getHideSelfView } from '../base/settings/functions.any';
@@ -91,23 +91,6 @@ export function getNotificationsMap(stateful: IStateful): { [key: string]: boole
}, {});
}
function normalizeCurrentLanguage(language: string) {
if (!language) {
return;
}
const [ country, lang ] = language.split('-');
const jitsiNormalized = `${country}${lang ?? ''}`;
if (LANGUAGES.includes(jitsiNormalized)) {
return jitsiNormalized;
}
if (LANGUAGES.includes(country)) {
return country;
}
}
/**
* Returns the properties for the "More" tab from settings dialog from Redux
* state.
@@ -119,7 +102,7 @@ function normalizeCurrentLanguage(language: string) {
export function getMoreTabProps(stateful: IStateful) {
const state = toState(stateful);
const stageFilmstripEnabled = isStageFilmstripEnabled(state);
const language = normalizeCurrentLanguage(i18next.language) || DEFAULT_LANGUAGE;
const language = i18next.language || DEFAULT_LANGUAGE;
const configuredTabs: string[] = interfaceConfig.SETTINGS_SECTIONS || [];
// when self view is controlled by the config we hide the settings

View File

@@ -1,15 +1,15 @@
import { MEDIA_TYPE } from '../av-moderation/constants';
import { isForceMuted } from '../av-moderation/functions';
import { APP_WILL_MOUNT, APP_WILL_UNMOUNT } from '../base/app/actionTypes';
import { CONFERENCE_JOINED } from '../base/conference/actionTypes';
import { JitsiConferenceEvents } from '../base/lib-jitsi-meet';
import { setAudioMuted } from '../base/media/actions';
import { MEDIA_TYPE } from '../base/media/constants';
import { raiseHand } from '../base/participants/actions';
import { getLocalParticipant } from '../base/participants/functions';
import MiddlewareRegistry from '../base/redux/MiddlewareRegistry';
import { playSound, registerSound, unregisterSound } from '../base/sounds/actions';
import { hideNotification, showNotification } from '../notifications/actions';
import { NOTIFICATION_TIMEOUT_TYPE } from '../notifications/constants';
import { isForceMuted } from '../participants-pane/functions';
import { isAudioMuteButtonDisabled } from '../toolbox/functions.any';
import { setCurrentNotificationUid } from './actions';

View File

@@ -1,6 +1,6 @@
import React from 'react';
import { Image, View, ViewStyle } from 'react-native';
import { SvgCssUri } from 'react-native-svg/css';
import { SvgCssUri } from 'react-native-svg';
import { connect } from 'react-redux';
import { translate } from '../../../base/i18n/functions';
@@ -44,7 +44,6 @@ class CustomOptionButton extends AbstractButton<ICustomOptionButton> {
if (this.iconSrc?.includes('svg')) {
iconComponent = (
<SvgCssUri
// @ts-ignore
height = { BaseTheme.spacing[4] }
uri = { this.iconSrc }
width = { BaseTheme.spacing[4] } />
@@ -63,7 +62,7 @@ class CustomOptionButton extends AbstractButton<ICustomOptionButton> {
<View
style = { this.props.isToolboxButton && [
styles.toolboxButtonIconContainer,
{ backgroundColor: this.backgroundColor } ] as ViewStyle[] }>
{ backgroundColor: this.backgroundColor } ] as ViewStyle }>
{ iconComponent }
</View>
);

View File

@@ -1,26 +1,19 @@
import {
AUDIO_MUTE,
DESKTOP_MUTE,
VIDEO_MUTE,
createRemoteMuteConfirmedEvent,
createToolbarEvent
} from '../analytics/AnalyticsEvents';
import { sendAnalytics } from '../analytics/functions';
import { IStore } from '../app/types';
import {
rejectParticipantAudio,
rejectParticipantDesktop,
rejectParticipantVideo
} from '../av-moderation/actions';
import { setAudioMuted, setScreenshareMuted, setVideoMuted } from '../base/media/actions';
import {
MEDIA_TYPE,
MediaType,
SCREENSHARE_MUTISM_AUTHORITY,
VIDEO_MUTISM_AUTHORITY
} from '../base/media/constants';
import { rejectParticipantAudio, rejectParticipantVideo, showModeratedNotification } from '../av-moderation/actions';
import { shouldShowModeratedNotification } from '../av-moderation/functions';
import { setAudioMuted, setVideoMuted } from '../base/media/actions';
import { MEDIA_TYPE, MediaType, VIDEO_MUTISM_AUTHORITY } from '../base/media/constants';
import { muteRemoteParticipant } from '../base/participants/actions';
import { getRemoteParticipants } from '../base/participants/functions';
import { toggleScreensharing } from '../base/tracks/actions';
import { isModerationNotificationDisplayed } from '../notifications/functions';
import logger from './logger';
@@ -29,31 +22,39 @@ import logger from './logger';
*
* @param {boolean} enable - Whether to mute or unmute.
* @param {MEDIA_TYPE} mediaType - The type of the media channel to mute.
* @param {boolean} stopScreenSharing - Whether or not to stop the screensharing.
* @returns {Function}
*/
export function muteLocal(enable: boolean, mediaType: MediaType) {
return (dispatch: IStore['dispatch']) => {
switch (mediaType) {
case MEDIA_TYPE.AUDIO: {
sendAnalytics(createToolbarEvent(AUDIO_MUTE, { enable }));
dispatch(setAudioMuted(enable, /* ensureTrack */ true));
break;
}
case MEDIA_TYPE.SCREENSHARE: {
sendAnalytics(createToolbarEvent(DESKTOP_MUTE, { enable }));
dispatch(setScreenshareMuted(enable, SCREENSHARE_MUTISM_AUTHORITY.USER, /* ensureTrack */ true));
break;
}
case MEDIA_TYPE.VIDEO: {
sendAnalytics(createToolbarEvent(VIDEO_MUTE, { enable }));
dispatch(setVideoMuted(enable, VIDEO_MUTISM_AUTHORITY.USER, /* ensureTrack */ true));
break;
}
default: {
export function muteLocal(enable: boolean, mediaType: MediaType, stopScreenSharing = false) {
return (dispatch: IStore['dispatch'], getState: IStore['getState']) => {
const isAudio = mediaType === MEDIA_TYPE.AUDIO;
if (!isAudio && mediaType !== MEDIA_TYPE.VIDEO) {
logger.error(`Unsupported media type: ${mediaType}`);
return;
}
// check for A/V Moderation when trying to unmute
if (isAudio && !enable && shouldShowModeratedNotification(MEDIA_TYPE.AUDIO, getState())) {
if (!isModerationNotificationDisplayed(MEDIA_TYPE.AUDIO, getState())) {
dispatch(showModeratedNotification(MEDIA_TYPE.AUDIO));
}
return;
}
if (enable && stopScreenSharing) {
dispatch(toggleScreensharing(false, false));
}
sendAnalytics(createToolbarEvent(isAudio ? AUDIO_MUTE : VIDEO_MUTE, { enable }));
dispatch(isAudio ? setAudioMuted(enable, /* ensureTrack */ true)
: setVideoMuted(enable, VIDEO_MUTISM_AUTHORITY.USER, /* ensureTrack */ true));
// FIXME: The old conference logic still relies on this event being emitted.
if (typeof APP !== 'undefined') {
isAudio ? APP.conference.muteAudio(enable) : APP.conference.muteVideo(enable, false);
}
};
}
@@ -67,12 +68,13 @@ export function muteLocal(enable: boolean, mediaType: MediaType) {
*/
export function muteRemote(participantId: string, mediaType: MediaType) {
return (dispatch: IStore['dispatch']) => {
if (mediaType !== MEDIA_TYPE.AUDIO && mediaType !== MEDIA_TYPE.VIDEO) {
logger.error(`Unsupported media type: ${mediaType}`);
return;
}
sendAnalytics(createRemoteMuteConfirmedEvent(participantId, mediaType));
// TODO(saghul): reconcile these 2 types.
const muteMediaType = mediaType === MEDIA_TYPE.SCREENSHARE ? 'desktop' : mediaType;
dispatch(muteRemoteParticipant(participantId, muteMediaType));
dispatch(muteRemoteParticipant(participantId, mediaType));
};
}
@@ -95,10 +97,8 @@ export function muteAllParticipants(exclude: Array<string>, mediaType: MediaType
dispatch(muteRemote(id, mediaType));
if (mediaType === MEDIA_TYPE.AUDIO) {
dispatch(rejectParticipantAudio(id));
} else if (mediaType === MEDIA_TYPE.VIDEO) {
} else {
dispatch(rejectParticipantVideo(id));
} else if (mediaType === MEDIA_TYPE.SCREENSHARE) {
dispatch(rejectParticipantDesktop(id));
}
});
};

View File

@@ -1,26 +1,24 @@
import { Component } from 'react';
import { WithTranslation } from 'react-i18next';
import { IReduxState, IStore } from '../../app/types';
import { IReduxState } from '../../app/types';
import { requestDisableAudioModeration, requestEnableAudioModeration } from '../../av-moderation/actions';
import { MEDIA_TYPE as AVM_MEDIA_TYPE } from '../../av-moderation/constants';
import { isEnabledFromState, isSupported } from '../../av-moderation/functions';
import { MEDIA_TYPE } from '../../base/media/constants';
import { getLocalParticipant, getParticipantDisplayName, isEveryoneModerator } from '../../base/participants/functions';
import { muteAllParticipants } from '../actions';
import AbstractMuteRemoteParticipantDialog, {
type IProps as AbstractProps
} from './AbstractMuteRemoteParticipantDialog';
/**
* The type of the React {@code Component} props of
* {@link AbstractMuteEveryoneDialog}.
*/
export interface IProps extends WithTranslation {
export interface IProps extends AbstractProps {
content?: string;
dispatch: IStore['dispatch'];
exclude: Array<string>;
isAudioModerationEnabled?: boolean;
isEveryoneModerator: boolean;
isModerationSupported?: boolean;
participantID: string;
showAdvancedModerationToggle: boolean;
title: string;
}
@@ -35,15 +33,17 @@ interface IState {
* An abstract Component with the contents for a dialog that asks for confirmation
* from the user before muting all remote participants.
*
* @augments AbstractMuteRemoteParticipantDialog
*/
export default class AbstractMuteEveryoneDialog<P extends IProps> extends Component<P, IState> {
export default class AbstractMuteEveryoneDialog<P extends IProps> extends
AbstractMuteRemoteParticipantDialog<P, IState> {
static defaultProps = {
exclude: [],
muteLocal: false
};
/**
* Initializes a new {@code AbstractMuteEveryoneDialog} instance.
* Initializes a new {@code AbstractMuteRemoteParticipantDialog} instance.
*
* @param {Object} props - The read-only properties with which the new
* instance is to be initialized.
@@ -84,7 +84,7 @@ export default class AbstractMuteEveryoneDialog<P extends IProps> extends Compon
*
* @returns {boolean}
*/
_onSubmit() {
override _onSubmit() {
const {
dispatch,
exclude
@@ -124,7 +124,7 @@ export function abstractMapStateToProps(state: IReduxState, ownProps: IProps) {
isEveryoneModerator: isEveryoneModerator(state)
} : {
title: t('dialog.muteEveryoneTitle'),
isAudioModerationEnabled: isEnabledFromState(AVM_MEDIA_TYPE.AUDIO, state),
isAudioModerationEnabled: isEnabledFromState(MEDIA_TYPE.AUDIO, state),
isModerationSupported: isSupported()(state),
isEveryoneModerator: isEveryoneModerator(state)
};

View File

@@ -1,130 +0,0 @@
import { IReduxState } from '../../app/types';
import { requestDisableDesktopModeration, requestEnableDesktopModeration } from '../../av-moderation/actions';
import { MEDIA_TYPE as AVM_MEDIA_TYPE } from '../../av-moderation/constants';
import { isEnabledFromState, isSupported } from '../../av-moderation/functions';
import { MEDIA_TYPE } from '../../base/media/constants';
import { getLocalParticipant, getParticipantDisplayName } from '../../base/participants/functions';
import { muteAllParticipants } from '../actions';
import AbstractMuteRemoteParticipantsDesktopDialog, {
type IProps as AbstractProps
} from './AbstractMuteRemoteParticipantsDesktopDialog';
/**
* The type of the React {@code Component} props of
* {@link AbstractMuteEveryonesDesktopDialog}.
*/
export interface IProps extends AbstractProps {
content?: string;
exclude: Array<string>;
isModerationEnabled?: boolean;
isModerationSupported?: boolean;
showAdvancedModerationToggle: boolean;
title: string;
}
interface IState {
content: string;
moderationEnabled?: boolean;
}
/**
*
* An abstract Component with the contents for a dialog that asks for confirmation
* from the user before disabling all remote participants cameras.
*
* @augments AbstractMuteRemoteParticipantsDesktopDialog
*/
export default class AbstractMuteEveryonesDesktopDialog<P extends IProps>
extends AbstractMuteRemoteParticipantsDesktopDialog<P, IState> {
static defaultProps = {
exclude: [],
muteLocal: false
};
/**
* Initializes a new {@code AbstractMuteRemoteParticipantsDesktopDialog} instance.
*
* @param {Object} props - The read-only properties with which the new
* instance is to be initialized.
*/
constructor(props: P) {
super(props);
this.state = {
moderationEnabled: props.isModerationEnabled,
content: props.content || props.t(props.isModerationEnabled
? 'dialog.muteEveryonesDesktopDialogModerationOn' : 'dialog.muteEveryonesDesktopDialog'
)
};
// Bind event handlers so they are only bound once per instance.
this._onSubmit = this._onSubmit.bind(this);
this._onToggleModeration = this._onToggleModeration.bind(this);
}
/**
* Toggles advanced moderation switch.
*
* @returns {void}
*/
_onToggleModeration() {
this.setState(state => {
return {
moderationEnabled: !state.moderationEnabled,
content: this.props.t(state.moderationEnabled
? 'dialog.muteEveryonesDesktopDialog' : 'dialog.muteEveryonesDesktopDialogModerationOn'
)
};
});
}
/**
* Callback to be invoked when the value of this dialog is submitted.
*
* @returns {boolean}
*/
override _onSubmit() {
const {
dispatch,
exclude
} = this.props;
dispatch(muteAllParticipants(exclude, MEDIA_TYPE.SCREENSHARE));
if (this.state.moderationEnabled) {
dispatch(requestEnableDesktopModeration());
} else if (this.state.moderationEnabled !== undefined) {
dispatch(requestDisableDesktopModeration());
}
return true;
}
}
/**
* Maps (parts of) the Redux state to the associated {@code AbstractMuteEveryonesDesktopDialog}'s props.
*
* @param {IReduxState} state - The redux state.
* @param {Object} ownProps - The properties explicitly passed to the component.
* @returns {IProps}
*/
export function abstractMapStateToProps(state: IReduxState, ownProps: IProps) {
const { exclude = [], t } = ownProps;
const isModerationEnabled = isEnabledFromState(AVM_MEDIA_TYPE.DESKTOP, state);
const whom = exclude
// eslint-disable-next-line no-confusing-arrow
.map(id => id === getLocalParticipant(state)?.id
? t('dialog.muteEveryoneSelf')
: getParticipantDisplayName(state, id))
.join(', ');
return whom.length ? {
content: t('dialog.muteEveryoneElsesDesktopDialog'),
title: t('dialog.muteEveryoneElsesDesktopTitle', { whom })
} : {
title: t('dialog.muteEveryonesDesktopTitle'),
isModerationEnabled,
isModerationSupported: isSupported()(state)
};
}

View File

@@ -1,6 +1,5 @@
import { IReduxState } from '../../app/types';
import { requestDisableVideoModeration, requestEnableVideoModeration } from '../../av-moderation/actions';
import { MEDIA_TYPE as AVM_MEDIA_TYPE } from '../../av-moderation/constants';
import { isEnabledFromState, isSupported } from '../../av-moderation/functions';
import { MEDIA_TYPE } from '../../base/media/constants';
import { getLocalParticipant, getParticipantDisplayName } from '../../base/participants/functions';
@@ -110,7 +109,7 @@ export default class AbstractMuteEveryonesVideoDialog<P extends IProps>
*/
export function abstractMapStateToProps(state: IReduxState, ownProps: IProps) {
const { exclude = [], t } = ownProps;
const isVideoModerationEnabled = isEnabledFromState(AVM_MEDIA_TYPE.VIDEO, state);
const isVideoModerationEnabled = isEnabledFromState(MEDIA_TYPE.VIDEO, state);
const whom = exclude
// eslint-disable-next-line no-confusing-arrow

View File

@@ -0,0 +1,58 @@
import { Component } from 'react';
import { WithTranslation } from 'react-i18next';
import { IStore } from '../../app/types';
import { MEDIA_TYPE } from '../../base/media/constants';
import { muteRemote } from '../actions';
/**
* The type of the React {@code Component} props of
* {@link AbstractMuteRemoteParticipantDialog}.
*/
export interface IProps extends WithTranslation {
/**
* The Redux dispatch function.
*/
dispatch: IStore['dispatch'];
/**
* The ID of the remote participant to be muted.
*/
participantID: string;
}
/**
* Abstract dialog to confirm a remote participant mute action.
*
* @augments Component
*/
export default class AbstractMuteRemoteParticipantDialog<P extends IProps = IProps, State=void>
extends Component<P, State> {
/**
* Initializes a new {@code AbstractMuteRemoteParticipantDialog} instance.
*
* @param {Object} props - The read-only properties with which the new
* instance is to be initialized.
*/
constructor(props: P) {
super(props);
// Bind event handlers so they are only bound once per instance.
this._onSubmit = this._onSubmit.bind(this);
}
/**
* Handles the submit button action.
*
* @private
* @returns {boolean} - True (to note that the modal should be closed).
*/
_onSubmit() {
const { dispatch, participantID } = this.props;
dispatch(muteRemote(participantID, MEDIA_TYPE.AUDIO));
return true;
}
}

View File

@@ -1,81 +0,0 @@
import { Component } from 'react';
import { WithTranslation } from 'react-i18next';
import { IReduxState, IStore } from '../../app/types';
import { rejectParticipantDesktop } from '../../av-moderation/actions';
import { MEDIA_TYPE as AVM_MEDIA_TYPE } from '../../av-moderation/constants';
import { isEnabledFromState } from '../../av-moderation/functions';
import { MEDIA_TYPE } from '../../base/media/constants';
import { muteRemote } from '../actions';
/**
* The type of the React {@code Component} props of
* {@link AbstractMuteRemoteParticipantsDesktopDialog}.
*/
export interface IProps extends WithTranslation {
/**
* The Redux dispatch function.
*/
dispatch: IStore['dispatch'];
/**
* Whether or not desktop moderation is on.
*/
isModerationOn: boolean;
/**
* The ID of the remote participant to be muted.
*/
participantID: string;
}
/**
* Abstract dialog to confirm a remote participant desktop mute action.
*
* @augments Component
*/
export default class AbstractMuteRemoteParticipantsDesktopDialog<P extends IProps = IProps, State=any>
extends Component<P, State> {
/**
* Initializes a new {@code AbstractMuteRemoteParticipantsDesktopDialog} instance.
*
* @param {Object} props - The read-only properties with which the new
* instance is to be initialized.
*/
constructor(props: P) {
super(props);
// Bind event handlers so they are only bound once per instance.
this._onSubmit = this._onSubmit.bind(this);
}
/**
* Handles the submit button action.
*
* @private
* @returns {boolean} - True (to note that the modal should be closed).
*/
_onSubmit() {
const { dispatch, participantID } = this.props;
dispatch(muteRemote(participantID, MEDIA_TYPE.SCREENSHARE));
dispatch(rejectParticipantDesktop(participantID));
return true;
}
}
/**
* Maps (parts of) the redux state to the associated
* {@code AbstractDialogContainer}'s props.
*
* @param {IReduxState} state - The redux state.
* @private
* @returns {Object}
*/
export function abstractMapStateToProps(state: IReduxState) {
return {
isModerationOn: isEnabledFromState(AVM_MEDIA_TYPE.DESKTOP, state)
};
}

View File

@@ -3,7 +3,6 @@ import { WithTranslation } from 'react-i18next';
import { IReduxState, IStore } from '../../app/types';
import { rejectParticipantVideo } from '../../av-moderation/actions';
import { MEDIA_TYPE as AVM_MEDIA_TYPE } from '../../av-moderation/constants';
import { isEnabledFromState } from '../../av-moderation/functions';
import { MEDIA_TYPE } from '../../base/media/constants';
import { muteRemote } from '../actions';
@@ -76,6 +75,6 @@ export default class AbstractMuteRemoteParticipantsVideoDialog<P extends IProps
*/
export function abstractMapStateToProps(state: IReduxState) {
return {
isVideoModerationOn: isEnabledFromState(AVM_MEDIA_TYPE.VIDEO, state)
isVideoModerationOn: isEnabledFromState(MEDIA_TYPE.VIDEO, state)
};
}

View File

@@ -2,12 +2,13 @@ import { connect } from 'react-redux';
import { IReduxState } from '../../../app/types';
import { approveParticipant } from '../../../av-moderation/actions';
import { MEDIA_TYPE } from '../../../av-moderation/constants';
import { isForceMuted, isSupported } from '../../../av-moderation/functions';
import { isSupported } from '../../../av-moderation/functions';
import { translate } from '../../../base/i18n/functions';
import { IconMic, IconVideo } from '../../../base/icons/svg';
import { MEDIA_TYPE } from '../../../base/media/constants';
import { getParticipantById, isLocalParticipantModerator } from '../../../base/participants/functions';
import AbstractButton, { IProps as AbstractButtonProps } from '../../../base/toolbox/components/AbstractButton';
import { isForceMuted } from '../../../participants-pane/functions';
export interface IProps extends AbstractButtonProps {

View File

@@ -2,8 +2,8 @@ import React, { useCallback, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { useDispatch } from 'react-redux';
import { approveParticipantAudio, approveParticipantDesktop, approveParticipantVideo } from '../../../av-moderation/actions';
import { IconMic, IconScreenshare, IconVideo } from '../../../base/icons/svg';
import { approveParticipantAudio, approveParticipantVideo } from '../../../av-moderation/actions';
import { IconMic, IconVideo } from '../../../base/icons/svg';
import { MEDIA_TYPE, MediaType } from '../../../base/media/constants';
import ContextMenuItem from '../../../base/ui/components/web/ContextMenuItem';
import { NOTIFY_CLICK_MODE } from '../../../toolbox/types';
@@ -36,8 +36,6 @@ const AskToUnmuteButton = ({
dispatch(approveParticipantAudio(participantID));
} else if (buttonType === MEDIA_TYPE.VIDEO) {
dispatch(approveParticipantVideo(participantID));
} else if (buttonType === MEDIA_TYPE.SCREENSHARE) {
dispatch(approveParticipantDesktop(participantID));
}
}, [ buttonType, dispatch, notifyClick, notifyMode, participantID ]);
@@ -46,8 +44,6 @@ const AskToUnmuteButton = ({
return t('participantsPane.actions.askUnmute');
} else if (buttonType === MEDIA_TYPE.VIDEO) {
return t('participantsPane.actions.allowVideo');
} else if (buttonType === MEDIA_TYPE.SCREENSHARE) {
return t('participantsPane.actions.allowDesktop');
}
return '';
@@ -58,8 +54,6 @@ const AskToUnmuteButton = ({
return IconMic;
} else if (buttonType === MEDIA_TYPE.VIDEO) {
return IconVideo;
} else if (buttonType === MEDIA_TYPE.SCREENSHARE) {
return IconScreenshare;
}
}, [ buttonType ]);

View File

@@ -1,67 +0,0 @@
import React, { useCallback, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { useDispatch, useSelector } from 'react-redux';
import { createRemoteVideoMenuButtonEvent } from '../../../analytics/AnalyticsEvents';
import { sendAnalytics } from '../../../analytics/functions';
import { IReduxState } from '../../../app/types';
import { openDialog } from '../../../base/dialog/actions';
import { IconScreenshare } from '../../../base/icons/svg';
import { MEDIA_TYPE } from '../../../base/media/constants';
import { isRemoteTrackMuted } from '../../../base/tracks/functions.any';
import ContextMenuItem from '../../../base/ui/components/web/ContextMenuItem';
import { NOTIFY_CLICK_MODE } from '../../../toolbox/types';
import { IButtonProps } from '../../types';
import MuteRemoteParticipantsDesktopDialog from './MuteRemoteParticipantsDesktopDialog';
/**
* Implements a React {@link Component} which displays a button for disabling
* the desktop share of a participant in the conference.
*
* @returns {JSX.Element|null}
*/
const MuteDesktopButton = ({
notifyClick,
notifyMode,
participantID
}: IButtonProps): JSX.Element | null => {
const { t } = useTranslation();
const dispatch = useDispatch();
const tracks = useSelector((state: IReduxState) => state['features/base/tracks']);
// TODO: review if we shouldn't be using isParticipantMediaMuted.
const trackMuted = useMemo(
() => isRemoteTrackMuted(tracks, MEDIA_TYPE.SCREENSHARE, participantID),
[ isRemoteTrackMuted, participantID, tracks ]
);
const handleClick = useCallback(() => {
notifyClick?.();
if (notifyMode === NOTIFY_CLICK_MODE.PREVENT_AND_NOTIFY) {
return;
}
sendAnalytics(createRemoteVideoMenuButtonEvent(
'desktop.mute.button',
{
'participant_id': participantID
}));
dispatch(openDialog(MuteRemoteParticipantsDesktopDialog, { participantID }));
}, [ dispatch, notifyClick, notifyClick, participantID, sendAnalytics ]);
if (trackMuted) {
return null;
}
return (
<ContextMenuItem
accessibilityLabel = { t('participantsPane.actions.stopDesktop') }
className = 'mutedesktoplink'
icon = { IconScreenshare }
onClick = { handleClick }
text = { t('participantsPane.actions.stopDesktop') } />
);
};
export default MuteDesktopButton;

View File

@@ -1,48 +0,0 @@
import React, { useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { useDispatch } from 'react-redux';
import { createToolbarEvent } from '../../../analytics/AnalyticsEvents';
import { sendAnalytics } from '../../../analytics/functions';
import { openDialog } from '../../../base/dialog/actions';
import { IconScreenshare } from '../../../base/icons/svg';
import ContextMenuItem from '../../../base/ui/components/web/ContextMenuItem';
import { NOTIFY_CLICK_MODE } from '../../../toolbox/types';
import { IButtonProps } from '../../types';
import MuteEveryonesDesktopDialog from './MuteEveryonesDesktopDialog';
/**
* Implements a React {@link Component} which displays a button for audio muting
* every participant in the conference except the one with the given
* participantID.
*
* @returns {JSX.Element}
*/
const MuteEveryoneElsesDesktopButton = ({
notifyClick,
notifyMode,
participantID
}: IButtonProps): JSX.Element => {
const { t } = useTranslation();
const dispatch = useDispatch();
const handleClick = useCallback(() => {
notifyClick?.();
if (notifyMode === NOTIFY_CLICK_MODE.PREVENT_AND_NOTIFY) {
return;
}
sendAnalytics(createToolbarEvent('mute.everyoneelsesdesktop.pressed'));
dispatch(openDialog(MuteEveryonesDesktopDialog, { exclude: [ participantID ] }));
}, [ notifyClick, notifyMode, participantID ]);
return (
<ContextMenuItem
accessibilityLabel = { t('toolbar.accessibilityLabel.muteEveryoneElsesDesktopStream') }
icon = { IconScreenshare }
onClick = { handleClick }
text = { t('videothumbnail.domuteDesktopOfOthers') } />
);
};
export default MuteEveryoneElsesDesktopButton;

View File

@@ -1,52 +0,0 @@
import React from 'react';
import { connect } from 'react-redux';
import { translate } from '../../../base/i18n/functions';
import Dialog from '../../../base/ui/components/web/Dialog';
import Switch from '../../../base/ui/components/web/Switch';
import AbstractMuteEveryonesDesktopDialog, { type IProps, abstractMapStateToProps }
from '../AbstractMuteEveryonesDesktopDialog';
/**
* A React Component with the contents for a dialog that asks for confirmation
* from the user before disabling all remote participants cameras.
*
* @augments AbstractMuteEveryonesDesktopDialog
*/
class MuteEveryonesDesktopDialog extends AbstractMuteEveryonesDesktopDialog<IProps> {
/**
* Implements React's {@link Component#render()}.
*
* @inheritdoc
* @returns {ReactElement}
*/
override render() {
return (
<Dialog
ok = {{ translationKey: 'dialog.muteParticipantsDesktopButton' }}
onSubmit = { this._onSubmit }
title = { this.props.title }>
<div className = 'mute-dialog'>
{this.state.content}
{ this.props.isModerationSupported && this.props.exclude.length === 0 && (
<>
<div className = 'separator-line' />
<div className = 'control-row'>
<label htmlFor = 'moderation-switch'>
{this.props.t('dialog.moderationDesktopLabel')}
</label>
<Switch
checked = { !this.state.moderationEnabled }
id = 'moderation-switch'
onChange = { this._onToggleModeration } />
</div>
</>
)}
</div>
</Dialog>
);
}
}
export default translate(connect(abstractMapStateToProps)(MuteEveryonesDesktopDialog));

View File

@@ -1,40 +0,0 @@
import React from 'react';
import { connect } from 'react-redux';
import { translate } from '../../../base/i18n/functions';
import Dialog from '../../../base/ui/components/web/Dialog';
import AbstractMuteRemoteParticipantsDesktopDialog, {
abstractMapStateToProps
} from '../AbstractMuteRemoteParticipantsDesktopDialog';
/**
* A React Component with the contents for a dialog that asks for confirmation
* from the user before disabling a remote participants camera.
*
* @augments Component
*/
class MuteRemoteParticipantsDesktopDialog extends AbstractMuteRemoteParticipantsDesktopDialog {
/**
* Implements React's {@link Component#render()}.
*
* @inheritdoc
* @returns {ReactElement}
*/
override render() {
return (
<Dialog
ok = {{ translationKey: 'dialog.muteParticipantsDesktopButton' }}
onSubmit = { this._onSubmit }
titleKey = 'dialog.muteParticipantsDesktopTitle'>
<div>
{this.props.t(this.props.isModerationOn
? 'dialog.muteParticipantsDesktopBodyModerationOn'
: 'dialog.muteParticipantsDesktopBody'
) }
</div>
</Dialog>
);
}
}
export default translate(connect(abstractMapStateToProps)(MuteRemoteParticipantsDesktopDialog));

View File

@@ -4,15 +4,14 @@ import { useDispatch, useSelector } from 'react-redux';
import { makeStyles } from 'tss-react/mui';
import { IReduxState, IStore } from '../../../app/types';
import { MEDIA_TYPE as AVM_MEDIA_TYPE } from '../../../av-moderation/constants';
import { isSupported as isAvModerationSupported, isForceMuted } from '../../../av-moderation/functions';
import { isSupported as isAvModerationSupported } from '../../../av-moderation/functions';
import Avatar from '../../../base/avatar/components/Avatar';
import { isIosMobileBrowser, isMobileBrowser } from '../../../base/environment/utils';
import { MEDIA_TYPE } from '../../../base/media/constants';
import { PARTICIPANT_ROLE } from '../../../base/participants/constants';
import { getLocalParticipant, hasRaisedHand, isPrivateChatEnabled } from '../../../base/participants/functions';
import { IParticipant } from '../../../base/participants/types';
import { isParticipantAudioMuted } from '../../../base/tracks/functions.any';
import { isParticipantAudioMuted, isParticipantVideoMuted } from '../../../base/tracks/functions.any';
import ContextMenu from '../../../base/ui/components/web/ContextMenu';
import ContextMenuItemGroup from '../../../base/ui/components/web/ContextMenuItemGroup';
import { getBreakoutRooms, getCurrentRoomId, isInBreakoutRoom } from '../../../breakout-rooms/functions';
@@ -21,7 +20,7 @@ import { displayVerification } from '../../../e2ee/functions';
import { setVolume } from '../../../filmstrip/actions.web';
import { isStageFilmstripAvailable } from '../../../filmstrip/functions.web';
import { QUICK_ACTION_BUTTON } from '../../../participants-pane/constants';
import { getQuickActionButtonType } from '../../../participants-pane/functions';
import { getQuickActionButtonType, isForceMuted } from '../../../participants-pane/functions';
import { requestRemoteControl, stopController } from '../../../remote-control/actions';
import { getParticipantMenuButtonsWithNotifyClick, showOverflowDrawer } from '../../../toolbox/functions.web';
import { NOTIFY_CLICK_MODE } from '../../../toolbox/types';
@@ -35,9 +34,7 @@ import GrantModeratorButton from './GrantModeratorButton';
import KickButton from './KickButton';
import LowerHandButton from './LowerHandButton';
import MuteButton from './MuteButton';
import MuteDesktopButton from './MuteDesktopButton';
import MuteEveryoneElseButton from './MuteEveryoneElseButton';
import MuteEveryoneElsesDesktopButton from './MuteEveryoneElsesDesktopButton';
import MuteEveryoneElsesVideoButton from './MuteEveryoneElsesVideoButton';
import MuteVideoButton from './MuteVideoButton';
import PrivateMessageMenuButton from './PrivateMessageMenuButton';
@@ -137,10 +134,9 @@ const ParticipantContextMenu = ({
const localParticipant = useSelector(getLocalParticipant);
const _isModerator = Boolean(localParticipant?.role === PARTICIPANT_ROLE.MODERATOR);
const _isVideoForceMuted = useSelector<IReduxState>(state =>
isForceMuted(participant, AVM_MEDIA_TYPE.VIDEO, state));
const _isDesktopForceMuted = useSelector<IReduxState>(state =>
isForceMuted(participant, AVM_MEDIA_TYPE.DESKTOP, state));
isForceMuted(participant, MEDIA_TYPE.VIDEO, state));
const _isAudioMuted = useSelector((state: IReduxState) => isParticipantAudioMuted(participant, state));
const _isVideoMuted = useSelector((state: IReduxState) => isParticipantVideoMuted(participant, state));
const _overflowDrawer: boolean = useSelector(showOverflowDrawer);
const { remoteVideoMenu = {}, disableRemoteMute, startSilent, customParticipantMenuButtons }
= useSelector((state: IReduxState) => state['features/base/config']);
@@ -194,7 +190,7 @@ const ParticipantContextMenu = ({
() => !_overflowDrawer && !thumbnailMenu,
[ _overflowDrawer, thumbnailMenu ]);
const quickActionButtonType = useSelector((state: IReduxState) =>
getQuickActionButtonType(participant, state));
getQuickActionButtonType(participant, _isAudioMuted, _isVideoMuted, state));
const buttons: JSX.Element[] = [];
const buttons2: JSX.Element[] = [];
@@ -233,13 +229,6 @@ const ParticipantContextMenu = ({
buttonType = { MEDIA_TYPE.VIDEO } />
);
}
if (_isDesktopForceMuted
&& !(isClickedFromParticipantPane && quickActionButtonType === QUICK_ACTION_BUTTON.ALLOW_DESKTOP)) {
buttons.push(<AskToUnmuteButton
{ ...getButtonProps(BUTTONS.ALLOW_DESKTOP) }
buttonType = { MEDIA_TYPE.SCREENSHARE } />
);
}
}
if (!disableRemoteMute && !participant.isSilent) {
@@ -251,10 +240,6 @@ const ParticipantContextMenu = ({
buttons.push(<MuteVideoButton { ...getButtonProps(BUTTONS.MUTE_VIDEO) } />);
}
buttons.push(<MuteEveryoneElsesVideoButton { ...getButtonProps(BUTTONS.MUTE_OTHERS_VIDEO) } />);
if (!(isClickedFromParticipantPane && quickActionButtonType === QUICK_ACTION_BUTTON.STOP_DESKTOP)) {
buttons.push(<MuteDesktopButton { ...getButtonProps(BUTTONS.MUTE_DESKTOP) } />);
}
buttons.push(<MuteEveryoneElsesDesktopButton { ...getButtonProps(BUTTONS.MUTE_OTHERS_DESKTOP) } />);
}
if (raisedHands) {

View File

@@ -17,7 +17,6 @@ export const VOLUME_SLIDER_SCALE = 100;
* Participant context menu button keys.
*/
export const PARTICIPANT_MENU_BUTTONS = {
ALLOW_DESKTOP: 'allow-desktop',
ALLOW_VIDEO: 'allow-video',
ASK_UNMUTE: 'ask-unmute',
CONN_STATUS: 'conn-status',
@@ -28,9 +27,7 @@ export const PARTICIPANT_MENU_BUTTONS = {
KICK: 'kick',
LOWER_PARTICIPANT_HAND: 'lower-participant-hand',
MUTE: 'mute',
MUTE_DESKTOP: 'mute-desktop',
MUTE_OTHERS: 'mute-others',
MUTE_OTHERS_DESKTOP: 'mute-others-desktop',
MUTE_OTHERS_VIDEO: 'mute-others-video',
MUTE_VIDEO: 'mute-video',
PIN_TO_STAGE: 'pinToStage',

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