Compare commits

...

73 Commits

Author SHA1 Message Date
Hristo Terezov
cb621f8e32 feat(visitors): Private messages to main participants. 2025-07-25 17:26:06 -05:00
Hristo Terezov
3c80cfddd7 chore(deps) lib-jitsi-meet@latest
https://github.com/jitsi/lib-jitsi-meet/compare/v2030.0.0+b225c920...v2033.0.0+bf3e3a8e
2025-07-25 17:26:06 -05:00
Horatiu Muresan
557f6defb8 chore(analytics) Add getter for amplitude deviceId (#16268) 2025-07-25 20:41:29 +03:00
raduanastase8x8
52fa36f930 chore(wcag) Create valid structure for audio menu (#16007) 2025-07-24 19:40:50 +03:00
damencho
b050e5f5e8 fix: Fixes table equals missing param name. 2025-07-24 15:00:09 +03:00
damencho
bf8d83953b fix: Fixes table equals.
Was checking only for added or removed keys, but not for modified values.
2025-07-24 14:11:50 +03:00
Horatiu Muresan
f16bf466eb feat(external-api) Add camera capture function (#16238) 2025-07-23 17:22:48 +03:00
damencho
29ea811527 fix(av-moderation): Updates the whitelist with every moderator.
When a moderator joins or someone is granted moderation we update the whitelist for any media type for which moderation is enabled. The updated whitelist is sent to all the moderators including the newly joined or granted one.
2025-07-23 10:53:15 +03:00
Calin-Teodor
435d034fdb fix(toolbox/native): update SvgCssUri import 2025-07-23 10:50:59 +03:00
Calinteodor
419baa7ab7 feat(android): init RIMHs app before on create (#15887)
Initialise ReactInstanceManagerHolder during application startup, making it ready before onCreate() is called.
2025-07-22 13:05:54 +03:00
damencho
9eb7b7bb01 fix: Showing go-live notification.
Handle the case when a local participant becomes moderator after metadata is updated.
2025-07-22 11:19:59 +03:00
Hristo Terezov
19ee989cda fix(visitors): Add fallback display names for empty visitor names
Visitors with empty or undefined names now show the configured
defaultRemoteDisplayName or 'Fellow Jitster' as fallback, matching
the behavior of regular remote participants.
2025-07-22 07:27:52 +03:00
ltorje-8x8
ab1dcc5375 fix(go-live): unsubscribe from topics before closing if not done already (#16244) 2025-07-21 16:47:24 -05:00
damencho
3047b4c8c4 fix: Fixes updating local UI startMuted state. 2025-07-21 22:49:35 +03:00
damencho
2afce3d151 fix: Fixes restoring startmuted in av mod. 2025-07-21 16:37:23 +03:00
damencho
1cea9b1786 fix: Avoids sending two metadata updates.
When setting startMuted we are sending two metadata updates.
2025-07-21 16:37:23 +03:00
damencho
2b7299ae05 fix: Drops not needed default values when filtering. 2025-07-21 16:37:09 +03:00
damencho
4b50f13e96 fix: Filters stanza on cloned copy. 2025-07-21 16:37:09 +03:00
Saúl Ibarra Corretgé
c639acebcf fix(polls) more resilient parsing of payloads, take 2 2025-07-21 15:10:56 +02:00
Horatiu Muresan
1a34ed9a2d fix(i18n) Fix showing Afrikaans when set language is not found (#16245)
- fix translates sort
2025-07-17 15:14:52 +03:00
Hristo Terezov
0939e207eb fix(go-live): waiting not updated correctly.
We were comparing if the number of waiting participants have changed with the wrong property from the state - the number of visitors. The result was that we won't update the state when the new waiting value matches the number of visitors already in the state. Most of the times this will be 0 and we would never go to 0.
2025-07-15 20:54:12 -05:00
Hristo Terezov
8c3ea05ae6 fix(go-live): Disconnect on page close.
Currently we don't close the socket for the participants in the queue when the page is closed.
2025-07-15 18:32:21 -05:00
bgrozev
daf8a929b1 fix: Fix hideDisplayNameForAll. (#16239)
Remove filtering on the receive side, because:
1. It's not applied to visitors, and should be for the "all" case
2. We don't want to strip stats-id from stanzas sent to jicofo
2025-07-15 10:49:04 -05:00
bgrozev
2f3df2c66f fix: Fix setting whitelist when av_moderation is initially enabled. (#16235) 2025-07-14 18:32:51 -05:00
Mihaela Dumitru
d8d1f8331e fix(lang) add missing desktop sharing keys (#16234) 2025-07-14 18:08:41 -05:00
ltorje-8x8
0e69336f94 JIT-14750 Do not show names to visitors (#16231)
* JIT-14750 Do not show names to visitors

* apply review

* change name and email too

* fix: Fix filtering initial presence to vnodes.

* Also strip stats-id and identity.user.name.

* Move filtering logic to a util, filter all identity in main room

---------

Co-authored-by: Boris Grozev <boris@jitsi.org>
2025-07-14 16:00:25 -05:00
Calin-Teodor
ede8ae6cb9 chore(android/sdk): fix compileOnly set dependency related to rn-video 2025-07-14 11:46:42 +03:00
Hristo Terezov
7e57156d2a fix(deeplinking): Prevent web specific files beeing included in native build.
Adds .web suffixes to all web specific files to prevent beeing included in the native build. Before this it seems those files were included in the build but by some chance nothing was failing.
2025-07-11 16:47:50 -05:00
Hristo Terezov
6742435487 fix: GUM prompt not displayed after deeplinking page.
When we open a custom scheme URL before the window load event has been fired it seems that GUM prompt is not displayed after this due to Chrome bug. See more details here https://issues.chromium.org/issues/41398687.

The result in Jitsi Meet is the following:
If the user is joining a call for first time and haven't granted A/V permissions and lands on the deeplinking page we try to open the desktop app via redirect to a custom scheme URL. If the user chooses cancel and "Launch in web" we go to the prejoin screen and proceed with the initial GUM. At this point any GUM call won't display the permission prompt due to the browser bug and will go on forever making it impossible for the user to unmute camera or microphone.
2025-07-11 16:47:50 -05:00
Horatiu Muresan
99f34aaef4 fix(visitors-queue): style adjustments for native (#16228)
Co-authored-by: Calin-Teodor <calin.chitu@8x8.com>
2025-07-11 17:48:05 +03:00
Horatiu Muresan
69f9838c03 feat(visitors-queue) Add leave meeting button (#16225)
* feat(visitors-queue) Add leave meeting button

* fixes
2025-07-11 09:13:14 -05:00
Saúl Ibarra Corretgé
dbfd24261d fix(participants-pane) use icon to indicate non-moderator actions
Use a X when an action cannot be performed by such user
2025-07-11 16:00:45 +02:00
Saúl Ibarra Corretgé
2305ae85a0 feat(av-moderation) implement screen-sharing moderation 2025-07-11 16:00:45 +02:00
damencho
31a30f1118 feat(av-moderation): Adds desktop media type. 2025-07-11 16:00:45 +02:00
damencho
eacf7addb2 feat: Adds a room option to hide display name.
Options to hide it for non-moderators and for all.
2025-07-11 16:46:46 +03:00
Saúl Ibarra Corretgé
2cf788ebee chore(deps) lib-jitsi-meet@latest
https://github.com/jitsi/lib-jitsi-meet/compare/v2029.0.0+30b123e3...v2030.0.0+b225c920
2025-07-11 15:01:27 +02:00
damencho
6bd3ed5ae4 feat(visitors): Adds showing shared files in the meeting. 2025-07-10 19:34:37 +03:00
Calinteodor
b511f4b8df dep(react-native): Update to 0.77.2 (#16160)
* This is a huge update, mostly because we updated Gradle on the Android side, which includes a more strict bundle process for third party modules. On iOS, even though new architecture is disabled, we had to be explicit about it because of this react native update and because some updated dependencies have it enabled by default and are using turbo modules which are not available, YET, in our project.
2025-07-10 14:56:43 +03:00
Jaya Allamsetty
ead019f71b chore(deps) lib-jitsi-meet@latest
https://github.com/jitsi/lib-jitsi-meet/compare/v2025.0.0+49eb29a8...v2029.0.0+30b123e3
2025-07-09 20:32:03 -04:00
damencho
7a97d15e89 feat(conference): Clears any error from previous attempts.
When you see the error, you may click join on pre-join again, which may succeeded, so clear previous errors.
2025-07-09 14:14:49 +03:00
damencho
1acb99d763 fix(av-moderation): Fixes auto starting av moderation, notify everyone. 2025-07-08 21:18:44 +03:00
damencho
adbe990867 fix(visitors): A join case with live rooms. 2025-07-08 19:10:28 +03:00
Saúl Ibarra Corretgé
a4367567ab fix(amplitude) adjust to new SDK API changes
Ref: https://amplitude.com/docs/sdks/analytics/browser/migrate-from-javascript-sdk-to-browser-sdk-2-0
2025-07-08 17:40:46 +02:00
damencho
7f56cbc4ce fix(av-moderation): Fixes auto starting av moderation.
There are some startMuted policies we set when starting it.
2025-07-08 16:18:57 +03:00
damencho
d636d084c8 fix(visitors): Fixes empty array case and wrong json.
When there is empty array cjson produces array: {} while prosody's json impl checks is it array and produces the correct value (array: []). Prosody impl is a little bit slower, but this is not a hot path and those are not huge json strings.
2025-07-08 13:31:05 +03:00
damencho
298567be48 fix(visitors): Updates docs, drops s2soutinjection.
That module was initially dropped with 354a3c002a.
2025-07-08 10:25:23 +03:00
Boris Grozev
c233629e51 fix: Do not merge participants and moderators into room metadata. 2025-07-08 06:00:35 +03:00
Saúl Ibarra Corretgé
75b5702a7e fix(file-sharing) fix resetting the state for share file input
Otherwise re-uploading the same file would not work because the input
element doesn't change state, as the value would remain the same.
2025-07-07 15:36:09 +02:00
benasm7
540f01d47e fix(virtual-background): Fix i18n for a device error.
* Reusing existing translation string for virtual background error notification, instead of current hardcoded english value.

* Update VirtualBackgroundPreview.tsx
2025-07-07 07:57:48 -05:00
Robert Oanta
5c7ed6a8b3 feat(av_moderation): handle av_can_unmute policy 2025-07-07 15:33:28 +03:00
damencho
3c5d33fefa fix(visitors): Avoid go live to overwrite other settings. 2025-07-07 15:33:14 +03:00
ltorje-8x8
be04236834 feat(visitors): Fixes nil error about 'get_visitors_room_metadata'
* Attempt to call a nil value (global 'get_visitors_room_metadata')

* make the linter happy

* more trailing whitespace + cleanup

* apply review

* use default false
2025-07-07 05:31:13 -05:00
Saúl Ibarra Corretgé
ec1bfe73b3 fix(amplitude) sync device ID on web too
Note the use of jitsiLocalStorage since we also need to consider the
case when local storage is performed in the host page when in an
iframe.
2025-07-07 11:39:04 +02:00
Saúl Ibarra Corretgé
d2ed9ffef6 fix(transcribing) fix overriding transcribing state
Skip updating the transcribing state when the 'audio-recording-enabled'
property is not provided.

This fixes a race when a transcriber is already in the room, we'll see
it before properties are updated (sometimes) and without checking for
undefined we'd flip the local value to false.
2025-07-04 17:15:06 +02:00
Saúl Ibarra Corretgé
6141ff78f8 fix(rn,embed) remove 8x8 apps from isEmbedded check
For all intents and purposes 8x8 apps are integrating the SDK as a 3rd
party.

Yes, we are a 1st party of sorts, but that's ok because 8x8.vc allows
embedding.
2025-07-04 15:31:14 +02:00
Saúl Ibarra Corretgé
c6a75fb9ed fix(file-sharing) hide upload button for visitors 2025-07-04 13:19:26 +02:00
Andrei Gavrilescu
3438438219 feat(recording): enable consent dialog on spot (#16179)
* enable consent dialog on spot

* lint fix

* move spot consent behind config flag

* revert copilot magic
2025-07-04 11:45:01 +03:00
Matteo
7cedea6740 lang: update Italian translation 2025-07-04 10:37:49 +02:00
Hristo Terezov
69f26c8a38 fix(participant-pane): Don't show the Viewers label twice. 2025-07-03 19:00:38 -05:00
Hristo Terezov
92a4750d0e fix(VisitorsList): use separate stomp instance. 2025-07-03 19:00:38 -05:00
Hristo Terezov
370a884765 fix(visitors): avoid lost deltas when subscribing 2025-07-03 19:00:38 -05:00
Hristo Terezov
877fc98eef feat(visitors-list): Add to participant pane. 2025-07-03 07:52:09 -05:00
Jaya Allamsetty
7bed0b36bd chore(deps) lib-jitsi-meet@latest
https://github.com/jitsi/lib-jitsi-meet/compare/v2024.0.0+006b25e4...v2025.0.0+49eb29a8
2025-07-02 22:30:40 -04:00
damencho
cd5aed37e9 feat(filesharing): Adds a nil check.
In case of file failing to upload we try to remove it, but there was nothing indicated as added before that.
2025-07-02 15:01:01 -05:00
damencho
b8dad082df chore(deps) lib-jitsi-meet@latest
https://github.com/jitsi/lib-jitsi-meet/compare/v2021.0.0+5a044f1a...v2024.0.0+006b25e4
2025-07-02 14:20:22 -05:00
damencho
f84f98e8e5 fix(visitors): Allow joining queue when not prefer to be visitor. 2025-07-02 14:20:22 -05:00
damencho
d1328d68f2 fix(visitors): Deny access when room is not live and there is a list of participants. 2025-07-02 14:20:22 -05:00
damencho
43d5c1e3ba feat(visitors): Adds allow promotion setting per room. 2025-07-02 14:20:22 -05:00
damencho
22ed00724d fix(visitors): Checks mainMeetingParticipants array to allow joins.
squash: Change checks in find table.
2025-07-02 14:20:22 -05:00
Horatiu Muresan
0b095f36eb fix(file-sharing) Keep original filename on file download (#16183) 2025-07-02 16:49:42 +03:00
Jaya Allamsetty
327376d85e chore(deps) lib-jitsi-meet@latest
https://github.com/jitsi/lib-jitsi-meet/compare/v2018.0.0+1773bcff...v2021.0.0+5a044f1a
2025-07-01 13:02:28 -04:00
Saúl Ibarra Corretgé
f28bd67ff4 fix(PressureObserver) adapt to API changes
Also set a sampling intervakl of 30s to avoid too chatty logs.
2025-07-01 16:47:15 +02:00
Horatiu Muresan
3a54c3418b feat(filmstrip) Add always visible resize bar and initial width (#16181) 2025-07-01 16:07:47 +03:00
166 changed files with 8019 additions and 4776 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:v13.0
container: reactnativecommunity/react-native-android:v18.0
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4

View File

@@ -1,4 +1,5 @@
apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
// Crashlytics integration is done as part of Firebase now, so it gets
// automagically activated with google-services.json
@@ -66,9 +67,18 @@ android {
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
sourceCompatibility rootProject.ext.javaVersion
targetCompatibility rootProject.ext.javaVersion
}
kotlinOptions {
jvmTarget = rootProject.ext.jvmTargetVersion
}
kotlin {
jvmToolchain(rootProject.ext.jvmToolchainVersion)
}
namespace 'org.jitsi.meet'
}

View File

@@ -4,3 +4,8 @@
-keepattributes *Annotation*
-keepattributes SourceFile,LineNumberTable
-keep public class * extends java.lang.Exception
# R8 missing classes - suppress warnings
-dontwarn com.facebook.fresco.ui.common.LoggingListener
-dontwarn com.facebook.memory.config.MemorySpikeConfig
-dontwarn kotlinx.parcelize.Parcelize

View File

@@ -5,46 +5,52 @@ import org.gradle.util.VersionNumber
// sub-projects/modules.
buildscript {
ext {
kotlinVersion = "2.0.21"
gradlePluginVersion = "8.4.2"
buildToolsVersion = "34.0.0"
compileSdkVersion = 34
minSdkVersion = 26
targetSdkVersion = 34
supportLibVersion = "28.0.0"
ndkVersion = "27.1.12297006"
// The Maven artifact groupId of the third-party react-native modules which
// Jitsi Meet SDK for Android depends on and which are not available in
// third-party Maven repositories so we have to deploy to a Maven repository
// of ours.
moduleGroupId = 'com.facebook.react'
// Maven repo where artifacts will be published
mavenRepo = System.env.MVN_REPO ?: ""
mavenUser = System.env.MVN_USER ?: ""
mavenPassword = System.env.MVN_PASSWORD ?: ""
// Libre build
libreBuild = (System.env.LIBRE_BUILD ?: "false").toBoolean()
googleServicesEnabled = project.file('app/google-services.json').exists() && !libreBuild
//React Native and Hermes Version
rnVersion = "0.77.2"
// Java dependencies
javaVersion = JavaVersion.VERSION_17
jvmToolchainVersion = 17
jvmTargetVersion = '17'
}
repositories {
google()
mavenCentral()
}
dependencies {
classpath 'com.android.tools.build:gradle:7.4.2'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$rootProject.ext.kotlinVersion"
classpath "com.android.tools.build:gradle:$rootProject.ext.gradlePluginVersion"
classpath 'com.google.gms:google-services:4.4.0'
classpath 'com.google.firebase:firebase-crashlytics-gradle:2.9.9'
}
}
ext {
kotlinVersion = "1.9.24"
buildToolsVersion = "34.0.0"
compileSdkVersion = 34
minSdkVersion = 26
targetSdkVersion = 34
supportLibVersion = "28.0.0"
ndkVersion = "26.1.10909125"
// The Maven artifact groupId of the third-party react-native modules which
// Jitsi Meet SDK for Android depends on and which are not available in
// third-party Maven repositories so we have to deploy to a Maven repository
// of ours.
moduleGroupId = 'com.facebook.react'
// Maven repo where artifacts will be published
mavenRepo = System.env.MVN_REPO ?: ""
mavenUser = System.env.MVN_USER ?: ""
mavenPassword = System.env.MVN_PASSWORD ?: ""
// Libre build
libreBuild = (System.env.LIBRE_BUILD ?: "false").toBoolean()
googleServicesEnabled = project.file('app/google-services.json').exists() && !libreBuild
//React Native and Hermes Version
rnVersion = "0.75.5"
}
allprojects {
repositories {
mavenCentral()
@@ -134,7 +140,7 @@ allprojects {
project.version = "${json.version}-jitsi-${versionQualifierNumber}"
task jitsiAndroidSourcesJar(type: Jar) {
classifier = 'sources'
archiveClassifier = 'sources'
from android.sourceSets.main.java.source
}
@@ -185,16 +191,46 @@ allprojects {
}
}
// Force the version of the Android build tools we have chosen on all
// subprojects. The forcing was introduced for react-native and the third-party
// modules that we utilize such as react-native-background-timer.
// Force the version of the Android build tools we have chosen on all subprojects.
subprojects { subproject ->
afterEvaluate{
if ((subproject.plugins.hasPlugin('android')
|| subproject.plugins.hasPlugin('android-library'))
&& rootProject.ext.has('buildToolsVersion')) {
android {
buildToolsVersion rootProject.ext.buildToolsVersion
buildFeatures {
buildConfig true
}
// Set JVM target across all subprojects
compileOptions {
sourceCompatibility rootProject.ext.javaVersion
targetCompatibility rootProject.ext.javaVersion
}
// Disable lint errors for problematic third-party modules
// react-native-background-timer
// react-native-calendar-events
lint {
abortOnError = false
}
}
}
// Add Kotlin configuration for subprojects that use Kotlin
if (subproject.plugins.hasPlugin('kotlin-android')) {
subproject.kotlin {
jvmToolchain(rootProject.ext.jvmToolchainVersion)
}
// Set Kotlin JVM target
subproject.android {
kotlinOptions {
jvmTarget = rootProject.ext.jvmTargetVersion
}
}
}
}

View File

@@ -11,20 +11,26 @@
# The setting is particularly useful for tweaking memory settings.
# Default value: -Xmx1024m -XX:MaxPermSize=256m
org.gradle.jvmargs=-Xmx2048m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8
org.gradle.jvmargs=-Xmx4048m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8
# When configured, Gradle will run in incubating parallel mode.
# This option should only be used with decoupled projects. More details, visit
# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
# org.gradle.parallel=true
# This one fixes a weird WebRTC runtime problem on some devices.
# https://github.com/jitsi/jitsi-meet/issues/7911#issuecomment-714323255
android.enableDexingArtifactTransform.desugaring=false
android.useAndroidX=true
android.enableJetifier=true
android.bundle.enableUncompressedNativeLibs=false
# Use this property to enable support to the new architecture.
# This will allow you to use TurboModules and the Fabric render in
# your application. You should enable this flag either if you want
# to write custom TurboModules/Fabric components OR use libraries that
# are providing them.
newArchEnabled=false
# Use this property to enable or disable the Hermes JS engine.
hermesEnabled=true
appVersion=99.0.0
sdkVersion=0.0.0

View File

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

View File

@@ -50,6 +50,7 @@ dependencies {
implementation 'com.squareup.duktape:duktape-android:1.3.0'
implementation 'com.google.code.gson:gson:2.8.6'
implementation 'androidx.startup:startup-runtime:1.1.0'
implementation 'com.google.j2objc:j2objc-annotations:3.0.0'
// Only add these packages if we are NOT doing a LIBRE_BUILD
if (!rootProject.ext.libreBuild) {
@@ -83,7 +84,7 @@ dependencies {
implementation project(':react-native-screens')
implementation project(':react-native-slider')
implementation project(':react-native-sound')
implementation project(':react-native-splash-screen')
implementation project(':react-native-splash-view')
implementation project(':react-native-svg')
implementation project(':react-native-video')
implementation project(':react-native-webview')
@@ -137,8 +138,16 @@ android.libraryVariants.all { def variant ->
def devEnabled = !targetName.toLowerCase().contains("release")
// Run the bundler
// Use full path to node to avoid PATH issues in Gradle
def nodePath = System.getenv('NVM_BIN') ? "${System.getenv('NVM_BIN')}/node" : "node"
// Debug: Print the node path and environment
println "Using node path: ${nodePath}"
println "NVM_BIN: ${System.getenv('NVM_BIN')}"
println "Working directory: ${reactRoot}"
commandLine(
"node",
nodePath,
"node_modules/react-native/scripts/bundle.js",
"--platform", "android",
"--dev", "${devEnabled}",
@@ -151,6 +160,70 @@ android.libraryVariants.all { def variant ->
enabled !devEnabled
}
// GRADLE REQUIREMENTS (Gradle 8.7+ / AGP 8.5.0+):
// This task requires explicit dependencies on resource tasks from all React Native modules
// due to Gradle's strict validation of task dependencies.
// Without these dependencies,
// builds will fail with errors like:
// "Task ':sdk:bundleReleaseJsAndAssets' uses the output of task ':react-native-amplitude:packageReleaseResources'
// without declaring a dependency on it."
// The automatic dependency resolution below ensures all required resource tasks are properly
// declared as dependencies before this task executes.
if (variant.name.toLowerCase().contains("release")) {
rootProject.subprojects.each { subproject ->
if (
subproject.name.startsWith("react-native-") ||
subproject.name.startsWith("@react-native-") ||
subproject.name.startsWith("@giphy/")
) {
[
"packageReleaseResources",
"generateReleaseResValues",
"generateReleaseResources",
"generateReleaseBuildConfig",
"processReleaseManifest",
"writeReleaseAarMetadata",
"generateReleaseRFile",
"compileReleaseLibraryResources",
"compileReleaseJavaWithJavac",
"javaPreCompileRelease",
"bundleLibCompileToJarRelease",
"exportReleaseConsumerProguardFiles",
"mergeReleaseGeneratedProguardFiles",
"mergeReleaseJniLibFolders",
"mergeReleaseShaders",
"packageReleaseAssets",
"processReleaseJavaRes",
"prepareReleaseArtProfile",
"copyReleaseJniLibsProjectOnly",
"extractDeepLinksRelease",
"createFullJarRelease",
"generateReleaseLintModel",
"writeReleaseLintModelMetadata",
"generateReleaseLintVitalModel",
"lintVitalAnalyzeRelease",
"lintReportRelease",
"lintAnalyzeRelease",
"lintReportDebug",
"lintAnalyzeDebug"
].each { taskName ->
if (subproject.tasks.findByName(taskName)) {
currentBundleTask.dependsOn(subproject.tasks.named(taskName))
}
}
// Also depend on the main build task to ensure all sub-tasks are completed
if (subproject.tasks.findByName("build")) {
currentBundleTask.dependsOn(subproject.tasks.named("build"))
}
}
}
}
currentBundleTask.ext.generatedResFolders = files(resourcesDir).builtBy(currentBundleTask)
currentBundleTask.ext.generatedAssetsFolders = files(jsBundleDir).builtBy(currentBundleTask)
variant.registerGeneratedResFolders(currentBundleTask.generatedResFolders)

View File

@@ -23,8 +23,10 @@ import androidx.annotation.NonNull;
import androidx.startup.Initializer;
import com.facebook.soloader.SoLoader;
import com.facebook.react.soloader.OpenSourceMergedSoMapping;
import org.wonday.orientation.OrientationActivityLifecycle;
import java.io.IOException;
import java.util.Collections;
import java.util.List;
@@ -35,7 +37,11 @@ public class JitsiInitializer implements Initializer<Boolean> {
public Boolean create(@NonNull Context context) {
Log.d(this.getClass().getCanonicalName(), "create");
SoLoader.init(context, /* native exopackage */ false);
try {
SoLoader.init(context, OpenSourceMergedSoMapping.INSTANCE);
} catch (IOException e) {
throw new RuntimeException(e);
}
// Register our uncaught exception handler.
JitsiMeetUncaughtExceptionHandler.register();
@@ -43,6 +49,10 @@ public class JitsiInitializer implements Initializer<Boolean> {
// Register activity lifecycle handler for the orientation locker module.
((Application) context).registerActivityLifecycleCallbacks(OrientationActivityLifecycle.getInstance());
// Initialize ReactInstanceManager during application startup
// This ensures it's ready before any Activity onCreate is called
ReactInstanceManagerHolder.initReactInstanceManager((Application) context);
return true;
}

View File

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

View File

@@ -102,6 +102,10 @@ public class JitsiMeetActivity extends AppCompatActivity
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// ReactInstanceManager is now initialized by JitsiInitializer during application startup
// Just call onHostResume since the manager is already ready
JitsiMeetActivityDelegate.onHostResume(this);
setContentView(R.layout.activity_jitsi_meet);
this.jitsiView = findViewById(R.id.jitsiView);

View File

@@ -17,6 +17,7 @@
package org.jitsi.meet.sdk;
import android.app.Activity;
import android.app.Application;
import android.content.Context;
import android.os.Bundle;
import android.util.AttributeSet;
@@ -196,8 +197,6 @@ public class JitsiMeetView extends FrameLayout {
}
setBackgroundColor(BACKGROUND_COLOR);
ReactInstanceManagerHolder.initReactInstanceManager((Activity) context);
}
/**

View File

@@ -18,6 +18,7 @@ package org.jitsi.meet.sdk;
import android.annotation.SuppressLint;
import android.app.Activity;
import android.app.Application;
import androidx.annotation.Nullable;
@@ -33,7 +34,6 @@ import com.facebook.react.uimanager.ViewManager;
import com.oney.WebRTCModule.EglUtils;
import com.oney.WebRTCModule.WebRTCModuleOptions;
import org.devio.rn.splashscreen.SplashScreenModule;
import org.jitsi.meet.sdk.log.JitsiMeetLogger;
import org.webrtc.EglBase;
@@ -68,7 +68,6 @@ class ReactInstanceManagerHolder {
new JavaScriptSandboxModule(reactContext),
new LocaleDetector(reactContext),
new LogBridgeModule(reactContext),
new SplashScreenModule(reactContext),
new PictureInPictureModule(reactContext),
new ProximityModule(reactContext),
new org.jitsi.meet.sdk.net.NAT64AddrInfoModule(reactContext)));
@@ -90,7 +89,7 @@ class ReactInstanceManagerHolder {
new com.reactnativecommunity.asyncstorage.AsyncStoragePackage(),
new com.ocetnik.timer.BackgroundTimerPackage(),
new com.calendarevents.RNCalendarEventsPackage(),
new com.corbt.keepawake.KCKeepAwakePackage(),
new com.sayem.keepawake.KCKeepAwakePackage(),
new com.facebook.react.shell.MainReactPackage(),
new com.reactnativecommunity.clipboard.ClipboardPackage(),
new com.reactnativecommunity.netinfo.NetInfoPackage(),
@@ -110,6 +109,7 @@ class ReactInstanceManagerHolder {
new com.th3rdwave.safeareacontext.SafeAreaContextPackage(),
new com.horcrux.svg.SvgPackage(),
new org.wonday.orientation.OrientationPackage(),
new com.splashview.SplashViewPackage(),
new ReactPackageAdapter() {
@Override
public List<NativeModule> createNativeModules(ReactApplicationContext reactContext) {
@@ -133,7 +133,7 @@ class ReactInstanceManagerHolder {
// GiphyReactNativeSdkPackage
try {
Class<?> giphyPackageClass = Class.forName("com.giphyreactnativesdk.GiphyReactNativeSdkPackage");
Class<?> giphyPackageClass = Class.forName("com.giphyreactnativesdk.RTNGiphySdkPackage");
Constructor<?> constructor = giphyPackageClass.getConstructor();
packages.add((ReactPackage)constructor.newInstance());
} catch (Exception e) {
@@ -208,9 +208,9 @@ class ReactInstanceManagerHolder {
* time. All {@code ReactRootView} instances will be tied to the one and
* only {@code ReactInstanceManager}.
*
* @param activity {@code Activity} current running Activity.
* @param app {@code Application}
*/
static void initReactInstanceManager(Activity activity) {
static void initReactInstanceManager(Application app) {
if (reactInstanceManager != null) {
return;
}
@@ -232,14 +232,14 @@ class ReactInstanceManagerHolder {
reactInstanceManager
= ReactInstanceManager.builder()
.setApplication(activity.getApplication())
.setCurrentActivity(activity)
.setApplication(app)
.setCurrentActivity(null)
.setBundleAssetName("index.android.bundle")
.setJSMainModulePath("index.android")
.setJavaScriptExecutorFactory(new HermesExecutorFactory())
.addPackages(getReactNativePackages())
.setUseDeveloperSupport(BuildConfig.DEBUG)
.setInitialLifecycleState(LifecycleState.RESUMED)
.setInitialLifecycleState(LifecycleState.BEFORE_CREATE)
.build();
}
}

View File

@@ -1,5 +1,3 @@
rootProject.name = 'jitsi-meet'
include ':app', ':sdk'
include ':react-native-amplitude'
@@ -29,7 +27,7 @@ project(':react-native-google-signin').projectDir = new File(rootProject.project
include ':react-native-immersive-mode'
project(':react-native-immersive-mode').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-immersive-mode/android')
include ':react-native-keep-awake'
project(':react-native-keep-awake').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-keep-awake/android')
project(':react-native-keep-awake').projectDir = new File(rootProject.projectDir, '../node_modules/@sayem314/react-native-keep-awake/android')
include ':react-native-orientation-locker'
project(':react-native-orientation-locker').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-orientation-locker/android')
include ':react-native-pager-view'
@@ -44,8 +42,8 @@ include ':react-native-slider'
project(':react-native-slider').projectDir = new File(rootProject.projectDir, '../node_modules/@react-native-community/slider/android')
include ':react-native-sound'
project(':react-native-sound').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-sound/android')
include ':react-native-splash-screen'
project(':react-native-splash-screen').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-splash-screen/android')
include ':react-native-splash-view'
project(':react-native-splash-view').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-splash-view/android')
include ':react-native-svg'
project(':react-native-svg').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-svg/android')
include ':react-native-video'

View File

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

View File

@@ -18,8 +18,6 @@ import {
maybeRedirectToWelcomePage,
reloadWithStoredParams
} from './react/features/app/actions';
import { showModeratedNotification } from './react/features/av-moderation/actions';
import { shouldShowModeratedNotification } from './react/features/av-moderation/functions';
import {
_conferenceWillJoin,
authStatusChanged,
@@ -52,7 +50,8 @@ import {
commonUserJoinedHandling,
commonUserLeftHandling,
getConferenceOptions,
sendLocalParticipant
sendLocalParticipant,
updateTrackMuteState
} from './react/features/base/conference/functions';
import { getReplaceParticipant, getSsrcRewritingFeatureFlag } from './react/features/base/config/functions';
import { connect } from './react/features/base/connection/actions.web';
@@ -153,7 +152,6 @@ import {
DATA_CHANNEL_CLOSED_NOTIFICATION_ID,
NOTIFICATION_TIMEOUT_TYPE
} from './react/features/notifications/constants';
import { isModerationNotificationDisplayed } from './react/features/notifications/functions';
import { suspendDetected } from './react/features/power-monitor/actions';
import { initPrejoin, isPrejoinPageVisible } from './react/features/prejoin/functions';
import { disableReceiver, stopReceiver } from './react/features/remote-control/actions';
@@ -704,15 +702,6 @@ export default {
return;
}
// check for A/V Moderation when trying to unmute
if (!mute && shouldShowModeratedNotification(MEDIA_TYPE.AUDIO, state)) {
if (!isModerationNotificationDisplayed(MEDIA_TYPE.AUDIO, state)) {
APP.store.dispatch(showModeratedNotification(MEDIA_TYPE.AUDIO));
}
return;
}
await APP.store.dispatch(setAudioMuted(mute, true));
},
@@ -746,12 +735,6 @@ export default {
* dialogs in case of media permissions error.
*/
muteVideo(mute) {
if (this.videoSwitchInProgress) {
logger.warn('muteVideo - unable to perform operations while video switch is in progress');
return;
}
const state = APP.store.getState();
if (!mute
@@ -761,11 +744,6 @@ export default {
return;
}
// check for A/V Moderation when trying to unmute and return early
if (!mute && shouldShowModeratedNotification(MEDIA_TYPE.VIDEO, state)) {
return;
}
APP.store.dispatch(setVideoMuted(mute, VIDEO_MUTISM_AUTHORITY.USER, true));
},
@@ -1019,7 +997,6 @@ export default {
// Restore initial state.
this._localTracksInitialized = false;
this.isSharingScreen = false;
this.roomName = roomName;
const { tryCreateLocalTracks, errors } = this.createInitialLocalTracks(options);
@@ -1198,8 +1175,6 @@ export default {
return Boolean(APP.store.getState()['features/base/audio-only'].enabled);
},
videoSwitchInProgress: false,
/**
* This fields stores a handler which will create a Promise which turns off
* the screen sharing and restores the previous video state (was there
@@ -1228,7 +1203,6 @@ export default {
*/
async _turnScreenSharingOff(didHaveVideo, ignoreDidHaveVideo) {
this._untoggleScreenSharing = null;
this.videoSwitchInProgress = true;
APP.store.dispatch(stopReceiver());
@@ -1280,13 +1254,11 @@ export default {
return promise.then(
() => {
this.videoSwitchInProgress = false;
sendAnalytics(createScreenSharingEvent('stopped',
duration === 0 ? null : duration));
logger.info('Screen sharing stopped.');
},
error => {
this.videoSwitchInProgress = false;
logger.error(`_turnScreenSharingOff failed: ${error}`);
throw error;
@@ -1316,14 +1288,13 @@ export default {
this._untoggleScreenSharing
= this._turnScreenSharingOff.bind(this, didHaveVideo);
const desktopVideoStream = desktopStreams.find(stream => stream.getType() === MEDIA_TYPE.VIDEO);
const desktopAudioStream = desktopStreams.find(stream => stream.getType() === MEDIA_TYPE.AUDIO);
if (desktopAudioStream) {
desktopAudioStream.on(
JitsiTrackEvents.LOCAL_TRACK_STOPPED,
() => {
logger.debug(`Local screensharing audio track stopped. ${this.isSharingScreen}`);
logger.debug('Local screensharing audio track stopped.');
// Handle case where screen share was stopped from the browsers 'screen share in progress'
// window. If audio screen sharing is stopped via the normal UX flow this point shouldn't
@@ -1335,21 +1306,6 @@ export default {
);
}
if (desktopVideoStream) {
desktopVideoStream.on(
JitsiTrackEvents.LOCAL_TRACK_STOPPED,
() => {
logger.debug(`Local screensharing track stopped. ${this.isSharingScreen}`);
// If the stream was stopped during screen sharing
// session then we should switch back to video.
this.isSharingScreen
&& this._untoggleScreenSharing
&& this._untoggleScreenSharing();
}
);
}
return desktopStreams;
}, error => {
throw error;
@@ -1497,10 +1453,6 @@ export default {
room.on(JitsiConferenceEvents.TRACK_MUTE_CHANGED, (track, participantThatMutedUs) => {
if (participantThatMutedUs) {
APP.store.dispatch(participantMutedUs(participantThatMutedUs, track));
if (this.isSharingScreen && track.isVideoTrack()) {
logger.debug('TRACK_MUTE_CHANGED while screen sharing');
this._turnScreenSharingOff(false);
}
}
});
@@ -1712,8 +1664,12 @@ export default {
room.on(
JitsiConferenceEvents.START_MUTED_POLICY_CHANGED,
({ audio, video }) => {
APP.store.dispatch(
onStartMutedPolicyChanged(audio, video));
APP.store.dispatch(onStartMutedPolicyChanged(audio, video));
const state = APP.store.getState();
updateTrackMuteState(state, APP.store.dispatch, true);
updateTrackMuteState(state, APP.store.dispatch, false);
}
);

View File

@@ -117,6 +117,11 @@ var config = {
// Will replace ice candidates IPs with invalid ones in order to fail ice.
// failICE: true,
// When running on Spot TV, this controls whether to show the recording consent dialog.
// If false (default), Spot instances will not show the recording consent dialog.
// If true, Spot instances will show the recording consent dialog like regular clients.
// showSpotConsentDialog: false,
},
// Disables moderator indicators.
@@ -1126,10 +1131,6 @@ var config = {
// The Amplitude APP Key:
// amplitudeAPPKey: '<APP_KEY>',
// Enables Amplitude UTM tracking:
// Default value is false.
// amplitudeIncludeUTM: false,
// Obfuscates room name sent to analytics (amplitude, rtcstats)
// Default value is false.
// obfuscateRoomName: false,
@@ -1779,6 +1780,13 @@ var config = {
// // The minimum number of participants that must be in the call for
// // the top panel layout to be used.
// minParticipantCountForTopPanel: 50,
// // The width of the filmstrip on joining meeting. Can be resized afterwards.
// initialWidth: 400,
// // Whether the draggable resize bar of the filmstrip is always visible. Setting this to true will make
// // the filmstrip always visible in case `disableResizable` is false.
// alwaysShowResizeBar: true,
// },
// Tile view related config options.

View File

@@ -33,6 +33,7 @@ target 'JitsiMeetSDK' do
:path => config[:reactNativePath],
:hermes_enabled => true,
:fabric_enabled => false,
:new_arch_enabled => false,
# An absolute path to your application root.
:app_path => "#{Pod::Config.instance.installation_root}/.."
)
@@ -66,6 +67,7 @@ target 'JitsiMeetSDKLite' do
:path => config[:reactNativePath],
:hermes_enabled => true,
:fabric_enabled => false,
:new_arch_enabled => false,
# An absolute path to your application root.
:app_path => "#{Pod::Config.instance.installation_root}/.."
)
@@ -77,10 +79,12 @@ target 'JitsiMeetSDKLite' do
end
post_install do |installer|
react_native_post_install(
installer,
use_native_modules![:reactNativePath],
:mac_catalyst_enabled => false
:mac_catalyst_enabled => false,
# :ccache_enabled => true
)
installer.pods_project.targets.each do |target|
# https://github.com/CocoaPods/CocoaPods/issues/11402
@@ -99,4 +103,5 @@ post_install do |installer|
# Patch SocketRocket to support TLS 1.3
%x(patch Pods/SocketRocket/SocketRocket/SRSecurityPolicy.m -N < patches/ws-tls13.diff)
end

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.xib in Resources */ = {isa = PBXBuildFile; fileRef = 13B07FB11A68108700A75B9A /* LaunchScreen.xib */; };
13B07FBD1A68108700A75B9A /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 13B07FB11A68108700A75B9A /* LaunchScreen.storyboard */; };
13B07FBF1A68108700A75B9A /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 13B07FB51A68108700A75B9A /* Images.xcassets */; };
2681BB562C7A0B42CFBA6719 /* libPods-JitsiMeet.a in Frameworks */ = {isa = PBXBuildFile; fileRef = D6152FF9E9F7B0E86F70A21D /* libPods-JitsiMeet.a */; };
361974E2A13624D7735D619D /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = 5C1BE20ECD5DEEB48FED90B5 /* PrivacyInfo.xcprivacy */; };
@@ -132,7 +132,7 @@
0BEA5C3C1F7B8F73000D0AB4 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
0BEA5C3E1F7B8F73000D0AB4 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
13B07F961A680F5B00A75B9A /* jitsi-meet.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "jitsi-meet.app"; sourceTree = BUILT_PRODUCTS_DIR; };
13B07FB21A68108700A75B9A /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/LaunchScreen.xib; sourceTree = "<group>"; };
13B07FB21A68108700A75B9A /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = src/Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; };
13B07FB51A68108700A75B9A /* Images.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Images.xcassets; sourceTree = "<group>"; };
13B07FB61A68108700A75B9A /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
3E0F4ED943C0B12BE77F6B45 /* Pods-JitsiMeet.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-JitsiMeet.release.xcconfig"; path = "Target Support Files/Pods-JitsiMeet/Pods-JitsiMeet.release.xcconfig"; sourceTree = "<group>"; };
@@ -250,7 +250,6 @@
DEA0B7132D7EF7590062A9F6 /* AppDelegate.swift */,
13B07FB51A68108700A75B9A /* Images.xcassets */,
13B07FB61A68108700A75B9A /* Info.plist */,
13B07FB11A68108700A75B9A /* LaunchScreen.xib */,
DEA0B7112D7EF16E0062A9F6 /* ViewController.swift */,
);
path = src;
@@ -475,7 +474,7 @@
buildActionMask = 2147483647;
files = (
13B07FBF1A68108700A75B9A /* Images.xcassets in Resources */,
13B07FBD1A68108700A75B9A /* LaunchScreen.xib in Resources */,
13B07FBD1A68108700A75B9A /* LaunchScreen.storyboard in Resources */,
361974E2A13624D7735D619D /* PrivacyInfo.xcprivacy in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
@@ -687,12 +686,12 @@
name = Interface.storyboard;
sourceTree = "<group>";
};
13B07FB11A68108700A75B9A /* LaunchScreen.xib */ = {
13B07FB11A68108700A75B9A /* LaunchScreen.storyboard */ = {
isa = PBXVariantGroup;
children = (
13B07FB21A68108700A75B9A /* Base */,
);
name = LaunchScreen.xib;
name = LaunchScreen.storyboard;
sourceTree = "<group>";
};
/* End PBXVariantGroup section */

View File

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

View File

@@ -0,0 +1,42 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="23504" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" launchScreen="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="01J-lp-oVM">
<device id="retina4_7" orientation="portrait" appearance="light"/>
<dependencies>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="23506"/>
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<scenes>
<!--View Controller-->
<scene sceneID="EHf-IW-A2E">
<objects>
<viewController id="01J-lp-oVM" sceneMemberID="viewController">
<view key="view" contentMode="scaleToFill" id="iN0-l3-epB">
<rect key="frame" x="0.0" y="0.0" width="480" height="480"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<imageView opaque="NO" userInteractionEnabled="NO" contentMode="center" image="LaunchScreen" translatesAutoresizingMaskIntoConstraints="NO" id="4B8-Xf-NDE">
<rect key="frame" x="0.0" y="0.0" width="480" height="480"/>
</imageView>
</subviews>
<color key="backgroundColor" red="0.090196078431372548" green="0.62745098039215685" blue="0.85882352941176465" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<constraints>
<constraint firstAttribute="bottom" secondItem="4B8-Xf-NDE" secondAttribute="bottom" id="aFF-BR-glX"/>
<constraint firstItem="4B8-Xf-NDE" firstAttribute="leading" secondItem="iN0-l3-epB" secondAttribute="leading" id="glR-YN-1GF"/>
<constraint firstAttribute="trailing" secondItem="4B8-Xf-NDE" secondAttribute="trailing" id="tva-gl-jRX"/>
<constraint firstItem="4B8-Xf-NDE" firstAttribute="top" secondItem="iN0-l3-epB" secondAttribute="top" id="yaV-1V-oEh"/>
</constraints>
<nil key="simulatedStatusBarMetrics"/>
<freeformSimulatedSizeMetrics key="simulatedDestinationMetrics"/>
<point key="canvasLocation" x="548" y="455"/>
</view>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="iYj-Kq-Ea1" userLabel="First Responder" sceneMemberID="firstResponder"/>
</objects>
<resources>
<image name="LaunchScreen" width="480" height="480"/>
</resources>
</scene>
</scenes>
</document>

View File

@@ -1,35 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="23504" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" launchScreen="YES" useTraitCollections="YES" colorMatched="YES">
<device id="retina6_12" orientation="portrait" appearance="light"/>
<dependencies>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="23506"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<objects>
<placeholder placeholderIdentifier="IBFilesOwner" id="-1" userLabel="File's Owner"/>
<placeholder placeholderIdentifier="IBFirstResponder" id="-2" customClass="UIResponder"/>
<view contentMode="scaleToFill" id="iN0-l3-epB">
<rect key="frame" x="0.0" y="0.0" width="480" height="480"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<imageView opaque="NO" userInteractionEnabled="NO" contentMode="center" image="LaunchScreen" translatesAutoresizingMaskIntoConstraints="NO" id="4B8-Xf-NDE">
<rect key="frame" x="0.0" y="0.0" width="480" height="480"/>
</imageView>
</subviews>
<color key="backgroundColor" red="0.090196078431372548" green="0.62745098039215685" blue="0.85882352941176465" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<constraints>
<constraint firstAttribute="bottom" secondItem="4B8-Xf-NDE" secondAttribute="bottom" id="aFF-BR-glX"/>
<constraint firstItem="4B8-Xf-NDE" firstAttribute="leading" secondItem="iN0-l3-epB" secondAttribute="leading" id="glR-YN-1GF"/>
<constraint firstAttribute="trailing" secondItem="4B8-Xf-NDE" secondAttribute="trailing" id="tva-gl-jRX"/>
<constraint firstItem="4B8-Xf-NDE" firstAttribute="top" secondItem="iN0-l3-epB" secondAttribute="top" id="yaV-1V-oEh"/>
</constraints>
<nil key="simulatedStatusBarMetrics"/>
<freeformSimulatedSizeMetrics key="simulatedDestinationMetrics"/>
<point key="canvasLocation" x="548" y="455"/>
</view>
</objects>
<resources>
<image name="LaunchScreen" width="480" height="480"/>
</resources>
</document>

View File

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

View File

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

View File

@@ -23,7 +23,6 @@
#import "JitsiMeetView+Private.h"
#import "RCTBridgeWrapper.h"
#import "ReactUtils.h"
#import "RNSplashScreen.h"
#import "ScheenshareEventEmiter.h"
#import <react-native-webrtc/WebRTCModuleOptions.h>
@@ -221,8 +220,17 @@
return nil;
}
- (void)showSplashScreen:(UIView*)rootView {
[RNSplashScreen showSplash:@"LaunchScreen" inRootView:rootView];
- (void)showSplashScreen {
Class splashClass = NSClassFromString(@"SplashView");
if (splashClass && [splashClass respondsToSelector:@selector(sharedInstance)]) {
id splashInstance = [splashClass performSelector:@selector(sharedInstance)];
if (splashInstance && [splashInstance respondsToSelector:@selector(showSplash)]) {
[splashInstance performSelector:@selector(showSplash)];
NSLog(@"✅ Splash Screen Shown Successfully");
}
} else {
NSLog(@"⚠️ SplashView module not found");
}
}
#pragma mark - Property getter / setters

View File

@@ -109,6 +109,7 @@
}
},
"chat": {
"disabled": "L'invio di messaggi in chat è disabilitato.",
"enter": "Entra nella conversazione",
"error": "Errore: il tuo messaggio non è stato inviato. Motivo: {{error}}",
"fieldPlaceHolder": "Scrivi qui il tuo messaggio",
@@ -546,8 +547,10 @@
"downloadFailedDescription": "Si prega di riprovare.",
"downloadFailedTitle": "Download non riuscito",
"downloadFile": "Download",
"dragAndDrop": "Trascina e rilascia i file qui",
"fileAlreadyUploaded": "Questo file è già stato caricato nella riunione",
"dragAndDrop": "Trascina e rilascia i file qui o da qualsiasi altra parte nella schermata",
"fileAlreadyUploaded": "Questo file è già stato caricato nella riunione.",
"fileTooLargeDescription": "Assicurati che il file non superi {{ maxFileSize }}.",
"fileTooLargeTitle": "Il file selezionato è troppo grande",
"removeFile": "Rimuovi",
"uploadFailedDescription": "Si prega di riprovare.",
"uploadFailedTitle": "Caricamento non riuscito",
@@ -906,6 +909,7 @@
"visitorInQueue": " ({{count}} in attesa)",
"visitorRequests": " ({{count}} richiesta/e)",
"visitors": "Spettatori {{count}}",
"visitorsList": "Spettatori ({{count}})",
"waitingLobby": "({{count}}) in attesa"
},
"search": "Cerca partecipanti",
@@ -946,7 +950,7 @@
},
"results": {
"changeVote": "Cambia voto",
"empty": "Non ci sono ancora sondaggi in questa riunione. Crea un sondaggio qui!",
"empty": "Non ci sono ancora sondaggi in questa riunione.",
"hideDetailedResults": "Nascondi dettagli",
"showDetailedResults": "Mostra dettagli",
"vote": "Voti"

View File

@@ -300,6 +300,12 @@
"alreadySharedVideoTitle": "Only one shared video is allowed at a time",
"applicationWindow": "Application window",
"authenticationRequired": "Authentication required",
"cameraCaptureDialog": {
"description": "Take and send a picture using your mobile camera",
"ok": "Open camera",
"reject": "Not now",
"title": "Take a picture"
},
"cameraConstraintFailedError": "Your camera does not satisfy some of the required constraints.",
"cameraNotFoundError": "Camera was not found.",
"cameraNotSendingData": "We are unable to access your camera. Please check if another application is using this device, select another device from the settings menu or try to reload the application.",
@@ -375,22 +381,34 @@
"micTimeoutError": "Could not start audio source. Timeout occurred!",
"micUnknownError": "Cannot use microphone for an unknown reason.",
"moderationAudioLabel": "Allow attendees to unmute themselves",
"moderationDesktopLabel": "Allow non-moderators to share their screen",
"moderationVideoLabel": "Allow non-moderators to start their video",
"muteEveryoneDialog": "The participants can unmute themselves at any time.",
"muteEveryoneDialogModerationOn": "The participants can send a request to speak at any time.",
"muteEveryoneElseDialog": "Once muted, you won't be able to unmute them, but they can unmute themselves at any time.",
"muteEveryoneElseTitle": "Mute everyone except {{whom}}?",
"muteEveryoneElsesDesktopDialog": "Once the share is stopped, you won't be able to restart it, but they can do so at any time.",
"muteEveryoneElsesDesktopTitle": "Stop everyone's screen-share except {{whom}}?",
"muteEveryoneElsesVideoDialog": "Once the camera is disabled, you won't be able to turn it back on, but they can turn it back on at any time.",
"muteEveryoneElsesVideoTitle": "Stop everyone's video except {{whom}}?",
"muteEveryoneSelf": "yourself",
"muteEveryoneStartMuted": "Everyone starts muted from now on",
"muteEveryoneTitle": "Mute everyone?",
"muteEveryonesDesktopDialog": "The participants can share their screen at any time.",
"muteEveryonesDesktopDialogModerationOn": "The participants can send a request to share their screen at any time.",
"muteEveryonesDesktopTitle": "Stop everyone's screen share?",
"muteEveryonesVideoDialog": "The participants can turn on their video at any time.",
"muteEveryonesVideoDialogModerationOn": "The participants can send a request to turn on their video at any time.",
"muteEveryonesVideoDialogOk": "Disable",
"muteEveryonesVideoTitle": "Stop everyone's video?",
"muteParticipantBody": "You won't be able to unmute them, but they can unmute themselves at any time.",
"muteParticipantButton": "Mute",
"muteParticipantsDesktopBody": "You won't be able to start their screen-share, but they can do so at any time.",
"muteParticipantsDesktopBodyModerationOn": "You won't be able to start their screen-share and neither will they.",
"muteParticipantsDesktopButton": "Stop screen sharing",
"muteParticipantsDesktopDialog": "Are you sure you want to turn off this participant's screen-share? You won't be able to restart it, but they can do so at any time.",
"muteParticipantsDesktopDialogModerationOn": "Are you sure you want to turn off this participant's screen-share? You won't be able to turn the screen back on and neither will they.",
"muteParticipantsDesktopTitle": "Disable screen-share of this participant?",
"muteParticipantsVideoBody": "You won't be able to turn the camera back on, but they can turn it back on at any time.",
"muteParticipantsVideoBodyModerationOn": "You won't be able to turn the camera back on and neither will they.",
"muteParticipantsVideoButton": "Stop video",
@@ -767,8 +785,9 @@
"me": "me",
"notify": {
"OldElectronAPPTitle": "Security vulnerability!",
"allowAll": "Allow All",
"allowAudio": "Allow Audio",
"allowBoth": "Both",
"allowDesktop": "Allow screen sharing",
"allowVideo": "Allow Video",
"allowedUnmute": "You can unmute your microphone, start your camera or share your screen.",
"audioUnmuteBlockedDescription": "Mic unmute operation has been temporarily blocked because of system limits.",
@@ -782,6 +801,7 @@
"dataChannelClosedDescription": "The bridge channel is down and thus video quality may be limited to its lowest setting.",
"dataChannelClosedDescriptionWithAudio": "The bridge channel is down and thus disruptions to audio and video may occur.",
"dataChannelClosedWithAudio": "Audio and video quality may be impaired",
"desktopMutedRemotelyTitle": "Your screen sharing has been stopped by {{participantDisplayName}}",
"disabledIframe": "Embedding is only meant for demo purposes, so this call will disconnect in {{timeout}} minutes.",
"disabledIframeSecondaryNative": "Embedding {{domain}} is only meant for demo purposes, so this call will disconnect in {{timeout}} minutes.",
"disabledIframeSecondaryWeb": "Embedding {{domain}} is only meant for demo purposes, so this call will disconnect in {{timeout}} minutes. Please use <a href='{{jaasDomain}}' rel='noopener noreferrer' target='_blank'>Jitsi as a Service</a> for production embedding!",
@@ -862,6 +882,7 @@
"suggestRecordingDescription": "Would you like to start a recording?",
"suggestRecordingTitle": "Record this meeting",
"unmute": "Unmute Audio",
"unmuteScreen": "Start screen sharing",
"unmuteVideo": "Unmute Video",
"videoMutedRemotelyDescription": "You can always turn it on again.",
"videoMutedRemotelyTitle": "Your video has been turned off by {{participantDisplayName}}",
@@ -881,11 +902,14 @@
"admit": "Admit",
"admitAll": "Admit all",
"allow": "Allow non-moderators to:",
"allowDesktop": "Allow screen sharing",
"allowVideo": "Allow video",
"askDesktop": "Ask to share screen",
"askUnmute": "Ask to unmute",
"audioModeration": "Unmute themselves",
"blockEveryoneMicCamera": "Block everyone's mic and camera",
"breakoutRooms": "Breakout rooms",
"desktopModeration": "Start screen sharing",
"goLive": "Go live",
"invite": "Invite someone",
"lowerAllHands": "Lower all hands",
@@ -897,6 +921,8 @@
"muteAll": "Mute all",
"muteEveryoneElse": "Mute everyone else",
"reject": "Reject",
"stopDesktop": "Stop screen sharing",
"stopEveryonesDesktop": "Stop everyone's screen-share",
"stopEveryonesVideo": "Stop everyone's video",
"stopVideo": "Stop video",
"unblockEveryoneMicCamera": "Unblock everyone's mic and camera",
@@ -906,9 +932,11 @@
"headings": {
"lobby": "Lobby ({{count}})",
"participantsList": "Meeting participants ({{count}})",
"viewerRequests": "Viewers requests {{count}}",
"visitorInQueue": " (waiting {{count}})",
"visitorRequests": " (requests {{count}})",
"visitors": "Viewers {{count}}",
"visitorsList": "Viewers ({{count}})",
"waitingLobby": "Waiting in lobby ({{count}})"
},
"search": "Search participants",
@@ -1501,6 +1529,8 @@
"connectionInfo": "Connection Info",
"demote": "Move to viewer",
"domute": "Mute",
"domuteDesktop": "Stop screen-sharing",
"domuteDesktopOfOthers": "Stop screen-sharing for everyone else",
"domuteOthers": "Mute everyone else",
"domuteVideo": "Disable camera",
"domuteVideoOfOthers": "Disable camera of everyone else",

View File

@@ -13,7 +13,7 @@ import {
requestEnableAudioModeration,
requestEnableVideoModeration
} from '../../react/features/av-moderation/actions';
import { isEnabledFromState } from '../../react/features/av-moderation/functions';
import { isEnabledFromState, isForceMuted } from '../../react/features/av-moderation/functions';
import { setAudioOnly } from '../../react/features/base/audio-only/actions';
import {
endConference,
@@ -30,6 +30,7 @@ import { overwriteConfig } from '../../react/features/base/config/actions';
import { getWhitelistedJSON } from '../../react/features/base/config/functions.any';
import { toggleDialog } from '../../react/features/base/dialog/actions';
import { isSupportedBrowser } from '../../react/features/base/environment/environment';
import { isMobileBrowser } from '../../react/features/base/environment/utils';
import { parseJWTFromURLParams } from '../../react/features/base/jwt/functions';
import JitsiMeetJS, { JitsiRecordingConstants } from '../../react/features/base/lib-jitsi-meet';
import { MEDIA_TYPE, VIDEO_TYPE } from '../../react/features/base/media/constants';
@@ -106,14 +107,17 @@ import {
close as closeParticipantsPane,
open as openParticipantsPane
} from '../../react/features/participants-pane/actions';
import { getParticipantsPaneOpen, isForceMuted } from '../../react/features/participants-pane/functions';
import { getParticipantsPaneOpen } from '../../react/features/participants-pane/functions';
import { startLocalVideoRecording, stopLocalVideoRecording } from '../../react/features/recording/actions.any';
import { grantRecordingConsent, grantRecordingConsentAndUnmute } from '../../react/features/recording/actions.web';
import { RECORDING_METADATA_ID, RECORDING_TYPES } from '../../react/features/recording/constants';
import { getActiveSession, supportsLocalRecording } from '../../react/features/recording/functions';
import { startAudioScreenShareFlow, startScreenShareFlow } from '../../react/features/screen-share/actions';
import { isScreenAudioSupported } from '../../react/features/screen-share/functions';
import { toggleScreenshotCaptureSummary } from '../../react/features/screenshot-capture/actions';
import {
openCameraCaptureDialog,
toggleScreenshotCaptureSummary
} from '../../react/features/screenshot-capture/actions';
import { isScreenshotCaptureEnabled } from '../../react/features/screenshot-capture/functions';
import SettingsDialog from '../../react/features/settings/components/web/SettingsDialog';
import { SETTINGS_TABS } from '../../react/features/settings/constants';
@@ -940,6 +944,20 @@ function initCommands() {
});
});
break;
case 'capture-camera-picture' : {
const { cameraFacingMode, descriptionText, titleText } = request;
if (!isMobileBrowser()) {
logger.error('This feature is only supported on mobile');
return;
}
APP.store.dispatch(openCameraCaptureDialog(callback, { cameraFacingMode,
descriptionText,
titleText }));
break;
}
case 'deployment-info':
callback(APP.store.getState()['features/base/config'].deploymentInfo);
break;

View File

@@ -820,6 +820,27 @@ export default class JitsiMeetExternalAPI extends EventEmitter {
});
}
/**
* Captures a picture through OS camera.
*
* @param {string} cameraFacingMode - The OS camera facing mode (environment/user).
* @param {string} descriptionText - The OS camera facing mode (environment/user).
* @param {string} titleText - The OS camera facing mode (environment/user).
* @returns {Promise<string>} - Resolves with a base64 encoded image data of the screenshot.
*/
captureCameraPicture(
cameraFacingMode,
descriptionText,
titleText
) {
return this._transport.sendRequest({
name: 'capture-camera-picture',
cameraFacingMode,
descriptionText,
titleText
});
}
/**
* Removes the listeners and removes the Jitsi Meet frame.
*

6441
package-lock.json generated

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": "2.3.0",
"@giphy/react-native-sdk": "3.3.1",
"@jitsi/excalidraw": "https://github.com/jitsi/excalidraw/releases/download/v0.0.19/jitsi-excalidraw-0.0.19.tgz",
"@jitsi/js-utils": "2.2.1",
"@jitsi/logger": "2.0.2",
@@ -34,13 +34,14 @@
"@react-native-async-storage/async-storage": "1.23.1",
"@react-native-clipboard/clipboard": "1.14.3",
"@react-native-community/netinfo": "11.1.0",
"@react-native-community/slider": "4.4.3",
"@react-native-community/slider": "4.5.6",
"@react-native-google-signin/google-signin": "10.1.0",
"@react-navigation/bottom-tabs": "6.6.0",
"@react-navigation/elements": "1.3.30",
"@react-navigation/material-top-tabs": "6.6.13",
"@react-navigation/native": "6.1.17",
"@react-navigation/stack": "6.4.0",
"@sayem314/react-native-keep-awake": "1.3.1",
"@stomp/stompjs": "7.0.0",
"@svgr/webpack": "6.3.1",
"@tensorflow/tfjs-backend-wasm": "3.13.0",
@@ -69,7 +70,7 @@
"js-md5": "0.6.1",
"js-sha512": "0.8.0",
"jwt-decode": "2.2.0",
"lib-jitsi-meet": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v2018.0.0+1773bcff/lib-jitsi-meet.tgz",
"lib-jitsi-meet": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v2033.0.0+bf3e3a8e/lib-jitsi-meet.tgz",
"lodash-es": "4.17.21",
"null-loader": "4.0.1",
"optional-require": "1.0.3",
@@ -82,35 +83,35 @@
"react-focus-on": "3.8.1",
"react-i18next": "10.11.4",
"react-linkify": "1.0.0-alpha",
"react-native": "0.75.5",
"react-native-background-timer": "2.4.1",
"react-native-calendar-events": "2.2.0",
"react-native-default-preference": "1.4.4",
"react-native": "0.77.2",
"react-native-background-timer": "https://github.com/jitsi/react-native-background-timer.git#d180dfaa4486ae3ee17d01242db92cb3195f4718",
"react-native-calendar-events": "https://github.com/jitsi/react-native-calendar-events.git#47f068dedfed7c0f72042e093f688eb11624eb7b",
"react-native-default-preference": "https://github.com/jitsi/react-native-default-preference.git#c9bf63bdc058e3fa2aa0b87b1ee1af240f44ed02",
"react-native-device-info": "10.9.0",
"react-native-dialog": "https://github.com/jitsi/react-native-dialog/releases/download/v9.2.2-jitsi.1/react-native-dialog-9.2.2.tgz",
"react-native-gesture-handler": "2.18.1",
"react-native-get-random-values": "1.9.0",
"react-native-immersive-mode": "2.0.2",
"react-native-keep-awake": "4.0.0",
"react-native-gesture-handler": "2.24.0",
"react-native-get-random-values": "1.11.0",
"react-native-immersive-mode": "https://github.com/jitsi/react-native-immersive-mode.git#38cc9001db24618bc0c61800f81e889bcfb6ff2c",
"react-native-orientation-locker": "1.6.0",
"react-native-pager-view": "6.4.1",
"react-native-paper": "5.10.3",
"react-native-performance": "5.0.0",
"react-native-safe-area-context": "4.12.0",
"react-native-screens": "3.35.0",
"react-native-sound": "0.11.2",
"react-native-splash-screen": "3.3.0",
"react-native-svg": "13.13.0",
"react-native-performance": "5.1.2",
"react-native-safe-area-context": "5.4.0",
"react-native-screens": "4.11.1",
"react-native-sound": "https://github.com/jitsi/react-native-sound.git#ea13c97b5c2a4ff5e0d9bacbd9ff5e4457fe2c3c",
"react-native-splash-view": "0.0.18",
"react-native-svg": "15.11.2",
"react-native-svg-transformer": "1.2.0",
"react-native-tab-view": "3.5.2",
"react-native-url-polyfill": "2.0.0",
"react-native-video": "6.0.0-alpha.11",
"react-native-video": "6.13.0",
"react-native-watch-connectivity": "1.1.0",
"react-native-webrtc": "124.0.4",
"react-native-webview": "13.8.7",
"react-native-webview": "13.13.5",
"react-native-youtube-iframe": "2.3.0",
"react-redux": "7.2.9",
"react-textarea-autosize": "8.3.0",
"react-virtualized-auto-sizer": "^1.0.26",
"react-window": "1.8.6",
"react-youtube": "10.1.0",
"redux": "4.0.4",
@@ -132,7 +133,11 @@
"@babel/preset-env": "7.25.9",
"@babel/preset-react": "7.25.9",
"@jitsi/eslint-config": "6.0.4",
"@react-native/metro-config": "0.75.5",
"@react-native-community/cli": "15.0.1",
"@react-native-community/cli-platform-android": "15.0.1",
"@react-native-community/cli-platform-ios": "15.0.1",
"@react-native/babel-preset": "0.77.2",
"@react-native/metro-config": "0.77.2",
"@types/amplitude-js": "8.16.5",
"@types/audioworklet": "0.0.29",
"@types/dom-screen-wake-lock": "1.0.1",
@@ -176,7 +181,6 @@
"eslint-plugin-typescript-sort-keys": "3.3.0",
"jetifier": "1.6.4",
"jsonwebtoken": "9.0.2",
"metro-react-native-babel-preset": "0.77.0",
"patch-package": "6.4.7",
"pretty": "2.0.0",
"process": "0.11.10",

View File

@@ -1,11 +0,0 @@
diff --git a/node_modules/@giphy/react-native-sdk/giphy-react-native-sdk.podspec b/node_modules/@giphy/react-native-sdk/giphy-react-native-sdk.podspec
index ddd41ac..a7b143e 100644
--- a/node_modules/@giphy/react-native-sdk/giphy-react-native-sdk.podspec
+++ b/node_modules/@giphy/react-native-sdk/giphy-react-native-sdk.podspec
@@ -16,5 +16,5 @@ Pod::Spec.new do |s|
s.source_files = "ios/**/*.{h,m,mm,swift}"
s.dependency "React-Core"
- s.dependency "Giphy", "2.2.4"
+ s.dependency "Giphy", "2.2.12"
end

View File

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

View File

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

View File

@@ -64,7 +64,8 @@
"@react-native-community/netinfo": "0.0.0",
"@react-native-community/slider": "0.0.0",
"@react-native-google-signin/google-signin": "0.0.0",
"react-native": "*",
"@sayem314/react-native-keep-awake": "0.0.0",
"react-native": "0.0.0",
"react": "*",
"react-native-background-timer": "0.0.0",
"react-native-calendar-events": "0.0.0",
@@ -73,14 +74,13 @@
"react-native-get-random-values": "0.0.0",
"react-native-gesture-handler": "0.0.0",
"react-native-immersive-mode": "0.0.0",
"react-native-keep-awake": "0.0.0",
"react-native-pager-view": "0.0.0",
"react-native-performance": "0.0.0",
"react-native-orientation-locker": "0.0.0",
"react-native-safe-area-context": "0.0.0",
"react-native-screens": "0.0.0",
"react-native-sound": "0.0.0",
"react-native-splash-screen": "0.0.0",
"react-native-splash-view": "0.0.0",
"react-native-svg": "0.0.0",
"react-native-video": "0.0.0",
"react-native-watch-connectivity": "0.0.0",

View File

@@ -63,6 +63,11 @@ export const ACTION_SHORTCUT_TRIGGERED = 'triggered';
*/
export const AUDIO_MUTE = 'audio.mute';
/**
* The name of the keyboard shortcut or toolbar button for muting desktop sharing.
*/
export const DESKTOP_MUTE = 'desktop.mute';
/**
* The name of the keyboard shortcut or toolbar button for muting video.
*/

View File

@@ -84,7 +84,6 @@ export async function createHandlers({ getState }: IStore) {
} = config;
const {
amplitudeAPPKey,
amplitudeIncludeUTM,
blackListedEvents,
scriptURLs,
matomoEndpoint,
@@ -94,7 +93,6 @@ export async function createHandlers({ getState }: IStore) {
const { group, user } = state['features/base/jwt'];
const handlerConstructorOptions = {
amplitudeAPPKey,
amplitudeIncludeUTM,
blackListedEvents,
envType: deploymentInfo?.envType || 'dev',
matomoEndpoint,

View File

@@ -11,7 +11,6 @@ export interface IEvent {
interface IOptions {
amplitudeAPPKey?: string;
amplitudeIncludeUTM?: boolean;
blackListedEvents?: string[];
envType?: string;
group?: string;

View File

@@ -4,21 +4,18 @@ import logger from '../logger';
import AbstractHandler, { IEvent } from './AbstractHandler';
import { fixDeviceID } from './amplitude/fixDeviceID';
import amplitude from './amplitude/lib';
import amplitude, { initAmplitude } from './amplitude/lib';
/**
* Analytics handler for Amplitude.
*/
export default class AmplitudeHandler extends AbstractHandler {
_deviceId: string;
_userId: Object;
/**
* Creates new instance of the Amplitude analytics handler.
*
* @param {Object} options - The amplitude options.
* @param {string} options.amplitudeAPPKey - The Amplitude app key required by the Amplitude API.
* @param {boolean} options.amplitudeIncludeUTM - Whether to include UTM parameters
* @param {string} options.amplitudeAPPKey - The Amplitude app key required by the Amplitude API
* in the Amplitude events.
*/
constructor(options: any) {
@@ -26,54 +23,26 @@ export default class AmplitudeHandler extends AbstractHandler {
const {
amplitudeAPPKey,
amplitudeIncludeUTM: includeUtm = true,
user
} = options;
this._enabled = true;
const onError = (e: Error) => {
logger.error('Error initializing Amplitude', e);
this._enabled = false;
};
// Forces sending all events on exit (flushing) via sendBeacon
const onExitPage = () => {
amplitude.flush();
};
if (navigator.product === 'ReactNative') {
amplitude.init(amplitudeAPPKey);
fixDeviceID(amplitude).then(() => {
const deviceId = amplitude.getDeviceId();
if (deviceId) {
this._deviceId = deviceId;
}
initAmplitude(amplitudeAPPKey, user)
.then(() => {
logger.info('Amplitude initialized');
fixDeviceID(amplitude);
})
.catch(e => {
logger.error('Error initializing Amplitude', e);
this._enabled = false;
});
} else {
const amplitudeOptions: any = {
includeReferrer: true,
includeUtm,
saveParamsReferrerOncePerSession: false,
onError,
onExitPage
};
amplitude.init(amplitudeAPPKey, undefined, amplitudeOptions);
fixDeviceID(amplitude);
}
if (user) {
this._userId = user;
amplitude.setUserId(user);
}
}
/**
* Sets the Amplitude user properties.
*
* @param {Object} userProps - The user portperties.
* @param {Object} userProps - The user properties.
* @returns {void}
*/
setUserProperties(userProps: any) {
@@ -104,7 +73,7 @@ export default class AmplitudeHandler extends AbstractHandler {
const eventName = this._extractName(event) ?? '';
amplitude.logEvent(eventName, event);
amplitude.track(eventName, event);
}
/**
@@ -113,13 +82,6 @@ export default class AmplitudeHandler extends AbstractHandler {
* @returns {Object}
*/
getIdentityProps() {
if (navigator.product === 'ReactNative') {
return {
deviceId: this._deviceId,
userId: this._userId
};
}
return {
sessionId: amplitude.getSessionId(),
deviceId: amplitude.getDeviceId(),

View File

@@ -17,7 +17,7 @@ export async function fixDeviceID(amplitude: Types.ReactNativeClient) {
const current = await DefaultPreference.get('amplitudeDeviceId');
if (current) {
await amplitude.setDeviceId(current);
amplitude.setDeviceId(current);
} else {
const uid = await getUniqueId();
@@ -27,7 +27,7 @@ export async function fixDeviceID(amplitude: Types.ReactNativeClient) {
return;
}
await amplitude.setDeviceId(uid as string);
amplitude.setDeviceId(uid as string);
await DefaultPreference.set('amplitudeDeviceId', uid as string);
}
}

View File

@@ -1,11 +1,46 @@
import { Types } from '@amplitude/analytics-browser';
// @ts-ignore
import { jitsiLocalStorage } from '@jitsi/js-utils';
import logger from '../../logger';
/**
* Key used to store the device id in local storage.
*/
const DEVICE_ID_KEY = '__AMDID';
/**
* Custom logic for setting the correct device id.
*
* @param {Types.BrowserClient} _amplitude - The amplitude instance.
* @param {Types.BrowserClient} amplitude - The amplitude instance.
* @returns {void}
*/
export function fixDeviceID(_amplitude: Types.BrowserClient): Promise<any> {
return new Promise(resolve => resolve(true));
export function fixDeviceID(amplitude: Types.BrowserClient) {
const deviceId = jitsiLocalStorage.getItem(DEVICE_ID_KEY);
if (deviceId) {
// Set the device id in Amplitude.
try {
amplitude.setDeviceId(JSON.parse(deviceId));
} catch (error) {
logger.error('Failed to set device ID in Amplitude', error);
return Promise.resolve(false);
}
} else {
const newDeviceId = amplitude.getDeviceId();
if (newDeviceId) {
jitsiLocalStorage.setItem(DEVICE_ID_KEY, JSON.stringify(newDeviceId));
}
}
}
/**
* Returns the amplitude shared deviceId.
*
* @returns {string} - The amplitude deviceId.
*/
export function getDeviceID() {
return jitsiLocalStorage.getItem(DEVICE_ID_KEY);
}

View File

@@ -1,3 +1,15 @@
import { createInstance } from '@amplitude/analytics-react-native';
import amplitude from '@amplitude/analytics-react-native';
export default createInstance();
export default amplitude;
/**
* Initializes the Amplitude instance.
*
* @param {string} amplitudeAPPKey - The Amplitude app key.
* @param {string | undefined} user - The user ID.
* @returns {Promise} The initialized Amplitude instance.
*/
export function initAmplitude(
amplitudeAPPKey: string, user: string | undefined): Promise<unknown> {
return amplitude.init(amplitudeAPPKey, user, {}).promise;
}

View File

@@ -1,3 +1,38 @@
import { createInstance } from '@amplitude/analytics-browser';
export default createInstance();
const amplitude = createInstance();
export default amplitude;
/**
* Initializes the Amplitude instance.
*
* @param {string} amplitudeAPPKey - The Amplitude app key.
* @param {string | undefined} user - The user ID.
* @returns {Promise} The initialized Amplitude instance.
*/
export function initAmplitude(
amplitudeAPPKey: string, user: string | undefined): Promise<unknown> {
// Forces sending all events on exit (flushing) via sendBeacon.
window.addEventListener('pagehide', () => {
// Set https transport to use sendBeacon API.
amplitude.setTransport('beacon');
// Send all pending events to server.
amplitude.flush();
});
const options = {
autocapture: {
attribution: true,
pageViews: true,
sessions: false,
fileDownloads: false,
formInteractions: false,
elementInteractions: false
},
defaultTracking: false
};
return amplitude.init(amplitudeAPPKey, user, options).promise;
}

View File

@@ -2,7 +2,8 @@ import React, { ComponentType } from 'react';
import { NativeModules, Platform, StyleSheet, View } from 'react-native';
import DeviceInfo from 'react-native-device-info';
import { SafeAreaProvider } from 'react-native-safe-area-context';
import SplashScreen from 'react-native-splash-screen';
// @ts-ignore
import { hideSplash } from 'react-native-splash-view';
import BottomSheetContainer from '../../base/dialog/components/native/BottomSheetContainer';
import DialogContainer from '../../base/dialog/components/native/DialogContainer';
@@ -84,7 +85,7 @@ export class App extends AbstractApp<IProps> {
override async componentDidMount() {
await super.componentDidMount();
SplashScreen.hide();
hideSplash();
const liteTxt = AppInfo.isLiteSDK ? ' (lite)' : '';

View File

@@ -1,8 +1,10 @@
import { IStateful } from '../base/app/types';
import { toState } from '../base/redux/functions';
import { getServerURL } from '../base/settings/functions.web';
import { getJitsiMeetGlobalNS } from '../base/util/helpers';
export * from './functions.any';
import logger from './logger';
/**
* Retrieves the default URL for the app. This can either come from a prop to
@@ -31,3 +33,27 @@ export function getDefaultURL(stateful: IStateful) {
export function getName() {
return interfaceConfig.APP_NAME;
}
/**
* Executes a handler function after the window load event has been received.
* If the app has already loaded, the handler is executed immediately.
* Otherwise, the handler is registered as a 'load' event listener.
*
* @param {Function} handler - The callback function to execute.
* @returns {void}
*/
export function executeAfterLoad(handler: () => void) {
const safeHandler = () => {
try {
handler();
} catch (error) {
logger.error('Error executing handler after load:', error);
}
};
if (getJitsiMeetGlobalNS()?.hasLoaded) {
safeHandler();
} else {
window.addEventListener('load', safeHandler);
}
}

View File

@@ -24,7 +24,6 @@ import '../calendar-sync/middleware';
import '../chat/middleware';
import '../conference/middleware';
import '../connection-indicator/middleware';
import '../deep-linking/middleware';
import '../device-selection/middleware';
import '../display-name/middleware';
import '../dynamic-branding/middleware';

View File

@@ -2,6 +2,7 @@ import '../base/app/middleware';
import '../base/connection/middleware';
import '../base/devices/middleware';
import '../base/media/middleware';
import '../deep-linking/middleware.web';
import '../dynamic-branding/middleware';
import '../e2ee/middleware';
import '../external-api/middleware';

View File

@@ -37,6 +37,15 @@ export const ENABLE_MODERATION = 'ENABLE_MODERATION';
*/
export const REQUEST_DISABLE_AUDIO_MODERATION = 'REQUEST_DISABLE_AUDIO_MODERATION';
/**
* The type of (redux) action which signals that Desktop Moderation disable has been requested.
*
* {
* type: REQUEST_DISABLE_DESKTOP_MODERATION
* }
*/
export const REQUEST_DISABLE_DESKTOP_MODERATION = 'REQUEST_DISABLE_DESKTOP_MODERATION';
/**
* The type of (redux) action which signals that Video Moderation disable has been requested.
*
@@ -55,6 +64,15 @@ export const REQUEST_DISABLE_VIDEO_MODERATION = 'REQUEST_DISABLE_VIDEO_MODERATIO
*/
export const REQUEST_ENABLE_AUDIO_MODERATION = 'REQUEST_ENABLE_AUDIO_MODERATION';
/**
* The type of (redux) action which signals that Desktop Moderation enable has been requested.
*
* {
* type: REQUEST_ENABLE_DESKTOP_MODERATION
* }
*/
export const REQUEST_ENABLE_DESKTOP_MODERATION = 'REQUEST_ENABLE_DESKTOP_MODERATION';
/**
* The type of (redux) action which signals that Video Moderation enable has been requested.
*
@@ -117,7 +135,7 @@ export const PARTICIPANT_REJECTED = 'PARTICIPANT_REJECTED';
/**
* The type of (redux) action which signals that a participant asked to have its audio umuted.
* The type of (redux) action which signals that a participant asked to have its audio unmuted.
*
* {
* type: PARTICIPANT_PENDING_AUDIO

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,11 +16,14 @@ import {
PARTICIPANT_PENDING_AUDIO,
PARTICIPANT_REJECTED,
REQUEST_DISABLE_AUDIO_MODERATION,
REQUEST_DISABLE_DESKTOP_MODERATION,
REQUEST_DISABLE_VIDEO_MODERATION,
REQUEST_ENABLE_AUDIO_MODERATION,
REQUEST_ENABLE_DESKTOP_MODERATION,
REQUEST_ENABLE_VIDEO_MODERATION
} from './actionTypes';
import { isEnabledFromState } from './functions';
import { MEDIA_TYPE, type MediaType } from './constants';
import { isEnabledFromState, isForceMuted } from './functions';
/**
* Action used by moderator to approve audio for a participant.
@@ -42,6 +45,25 @@ export const approveParticipantAudio = (id: string) => (dispatch: IStore['dispat
}
};
/**
* Action used by moderator to approve desktop for a participant.
*
* @param {staring} id - The id of the participant to be approved.
* @returns {void}
*/
export const approveParticipantDesktop = (id: string) => (dispatch: IStore['dispatch'], getState: IStore['getState']) => {
const state = getState();
const { conference } = getConferenceState(state);
const participant = getParticipantById(state, id);
const isDesktopForceMuted = isForceMuted(participant, MEDIA_TYPE.DESKTOP, state);
const isDesktopModerationOn = isEnabledFromState(MEDIA_TYPE.DESKTOP, state);
if (isDesktopModerationOn && isDesktopForceMuted) {
conference?.avModerationApprove(MEDIA_TYPE.DESKTOP, id);
}
};
/**
* Action used by moderator to approve video for a participant.
*
@@ -68,8 +90,11 @@ export const approveParticipantVideo = (id: string) => (dispatch: IStore['dispat
* @returns {void}
*/
export const approveParticipant = (id: string) => (dispatch: IStore['dispatch']) => {
dispatch(approveParticipantAudio(id));
dispatch(approveParticipantVideo(id));
batch(() => {
dispatch(approveParticipantAudio(id));
dispatch(approveParticipantDesktop(id));
dispatch(approveParticipantVideo(id));
});
};
/**
@@ -92,6 +117,26 @@ export const rejectParticipantAudio = (id: string) => (dispatch: IStore['dispatc
}
};
/**
* Action used by moderator to reject desktop for a participant.
*
* @param {staring} id - The id of the participant to be rejected.
* @returns {void}
*/
export const rejectParticipantDesktop = (id: string) => (dispatch: IStore['dispatch'], getState: IStore['getState']) => {
const state = getState();
const { conference } = getConferenceState(state);
const desktopModeration = isEnabledFromState(MEDIA_TYPE.DESKTOP, state);
const participant = getParticipantById(state, id);
const isDesktopForceMuted = isForceMuted(participant, MEDIA_TYPE.DESKTOP, state);
const isModerator = isParticipantModerator(participant);
if (desktopModeration && !isDesktopForceMuted && !isModerator) {
conference?.avModerationReject(MEDIA_TYPE.DESKTOP, id);
}
};
/**
* Action used by moderator to reject video for a participant.
*
@@ -185,6 +230,19 @@ export const requestDisableAudioModeration = () => {
};
};
/**
* Requests disable of video moderation.
*
* @returns {{
* type: REQUEST_DISABLE_DESKTOP_MODERATION
* }}
*/
export const requestDisableDesktopModeration = () => {
return {
type: REQUEST_DISABLE_DESKTOP_MODERATION
};
};
/**
* Requests disable of video moderation.
*
@@ -211,6 +269,19 @@ export const requestEnableAudioModeration = () => {
};
};
/**
* Requests enable of video moderation.
*
* @returns {{
* type: REQUEST_ENABLE_DESKTOP_MODERATION
* }}
*/
export const requestEnableDesktopModeration = () => {
return {
type: REQUEST_ENABLE_DESKTOP_MODERATION
};
};
/**
* Requests enable of video moderation.
*
@@ -313,4 +384,3 @@ export function participantRejected(id: string, mediaType: MediaType) {
mediaType
};
}

View File

@@ -1,18 +1,35 @@
import { MEDIA_TYPE } from '../base/media/constants';
export type MediaType = 'audio' | 'video' | 'desktop';
/**
* Mapping between a media type and the witelist reducer key.
* The set of media types for AV moderation.
*
* @enum {string}
*/
export const MEDIA_TYPE: {
AUDIO: MediaType;
DESKTOP: MediaType;
VIDEO: MediaType;
} = {
AUDIO: 'audio',
DESKTOP: 'desktop',
VIDEO: 'video'
};
/**
* Mapping between a media type and the whitelist reducer key.
*/
export const MEDIA_TYPE_TO_WHITELIST_STORE_KEY: { [key: string]: string; } = {
[MEDIA_TYPE.AUDIO]: 'audioWhitelist',
[MEDIA_TYPE.DESKTOP]: 'desktopWhitelist',
[MEDIA_TYPE.VIDEO]: 'videoWhitelist'
};
/**
* Mapping between a media type and the pending reducer key.
*/
export const MEDIA_TYPE_TO_PENDING_STORE_KEY: { [key: string]: 'pendingAudio' | 'pendingVideo'; } = {
export const MEDIA_TYPE_TO_PENDING_STORE_KEY: { [key: string]: 'pendingAudio' | 'pendingDesktop' | 'pendingVideo'; } = {
[MEDIA_TYPE.AUDIO]: 'pendingAudio',
[MEDIA_TYPE.DESKTOP]: 'pendingDesktop',
[MEDIA_TYPE.VIDEO]: 'pendingVideo'
};
@@ -20,11 +37,15 @@ export const ASKED_TO_UNMUTE_NOTIFICATION_ID = 'asked-to-unmute';
export const ASKED_TO_UNMUTE_SOUND_ID = 'ASKED_TO_UNMUTE_SOUND';
export const AUDIO_MODERATION_NOTIFICATION_ID = 'audio-moderation';
export const DESKTOP_MODERATION_NOTIFICATION_ID = 'desktop-moderation';
export const VIDEO_MODERATION_NOTIFICATION_ID = 'video-moderation';
export const CS_MODERATION_NOTIFICATION_ID = 'screensharing-moderation';
export const AUDIO_RAISED_HAND_NOTIFICATION_ID = 'raise-hand-audio';
export const DESKTOP_RAISED_HAND_NOTIFICATION_ID = 'raise-hand-desktop';
export const VIDEO_RAISED_HAND_NOTIFICATION_ID = 'raise-hand-video';
export const MODERATION_NOTIFICATIONS = {
[MEDIA_TYPE.AUDIO]: AUDIO_MODERATION_NOTIFICATION_ID,
[MEDIA_TYPE.SCREENSHARE]: CS_MODERATION_NOTIFICATION_ID,
[MEDIA_TYPE.DESKTOP]: DESKTOP_MODERATION_NOTIFICATION_ID,
[MEDIA_TYPE.VIDEO]: VIDEO_MODERATION_NOTIFICATION_ID
};

View File

@@ -1,10 +1,14 @@
import { IReduxState } from '../app/types';
import { MEDIA_TYPE, type MediaType } from '../base/media/constants';
import { isLocalParticipantModerator } from '../base/participants/functions';
import { isLocalParticipantModerator, isParticipantModerator } from '../base/participants/functions';
import { IParticipant } from '../base/participants/types';
import { isInBreakoutRoom } from '../breakout-rooms/functions';
import { MEDIA_TYPE_TO_PENDING_STORE_KEY, MEDIA_TYPE_TO_WHITELIST_STORE_KEY } from './constants';
import {
MEDIA_TYPE,
MEDIA_TYPE_TO_PENDING_STORE_KEY,
MEDIA_TYPE_TO_WHITELIST_STORE_KEY,
MediaType
} from './constants';
/**
* Returns this feature's root state.
@@ -29,10 +33,18 @@ const EMPTY_ARRAY: any[] = [];
* @param {IReduxState} state - Global state.
* @returns {boolean}
*/
export const isEnabledFromState = (mediaType: MediaType, state: IReduxState) =>
(mediaType === MEDIA_TYPE.AUDIO
? getState(state)?.audioModerationEnabled
: getState(state)?.videoModerationEnabled) === true;
export const isEnabledFromState = (mediaType: MediaType, state: IReduxState) => {
switch (mediaType) {
case MEDIA_TYPE.AUDIO:
return getState(state)?.audioModerationEnabled === true;
case MEDIA_TYPE.DESKTOP:
return getState(state)?.desktopModerationEnabled === true;
case MEDIA_TYPE.VIDEO:
return getState(state)?.videoModerationEnabled === true;
default:
throw new Error(`Unknown media type: ${mediaType}`);
}
};
/**
* Returns whether moderation is enabled per media type.
@@ -61,11 +73,20 @@ export const isSupported = () => (state: IReduxState) => {
* @returns {boolean}
*/
export const isLocalParticipantApprovedFromState = (mediaType: MediaType, state: IReduxState) => {
const approved = (mediaType === MEDIA_TYPE.AUDIO
? getState(state).audioUnmuteApproved
: getState(state).videoUnmuteApproved) === true;
if (isLocalParticipantModerator(state)) {
return true;
}
return approved || isLocalParticipantModerator(state);
switch (mediaType) {
case MEDIA_TYPE.AUDIO:
return getState(state).audioUnmuteApproved === true;
case MEDIA_TYPE.DESKTOP:
return getState(state).desktopUnmuteApproved === true;
case MEDIA_TYPE.VIDEO:
return getState(state).videoUnmuteApproved === true;
default:
throw new Error(`Unknown media type: ${mediaType}`);
}
};
/**
@@ -134,3 +155,28 @@ export const getParticipantsAskingToAudioUnmute = (state: IReduxState) => {
export const shouldShowModeratedNotification = (mediaType: MediaType, state: IReduxState) =>
isEnabledFromState(mediaType, state)
&& !isLocalParticipantApprovedFromState(mediaType, state);
/**
* Checks if a participant is force muted.
*
* @param {IParticipant|undefined} participant - The participant.
* @param {MediaType} mediaType - The media type.
* @param {IReduxState} state - The redux state.
* @returns {MediaState}
*/
export function isForceMuted(participant: IParticipant | undefined, mediaType: MediaType, state: IReduxState) {
if (isEnabledFromState(mediaType, state)) {
if (participant?.local) {
return !isLocalParticipantApprovedFromState(mediaType, state);
}
// moderators cannot be force muted
if (isParticipantModerator(participant)) {
return false;
}
return !isParticipantApproved(participant?.id ?? '', mediaType)(state);
}
return false;
}

View File

@@ -3,8 +3,12 @@ import { batch } from 'react-redux';
import { APP_WILL_MOUNT, APP_WILL_UNMOUNT } from '../base/app/actionTypes';
import { getConferenceState } from '../base/conference/functions';
import { JitsiConferenceEvents } from '../base/lib-jitsi-meet';
import { MEDIA_TYPE, MediaType } from '../base/media/constants';
import { isAudioMuted, isVideoMuted } from '../base/media/functions';
import { MEDIA_TYPE as TRACK_MEDIA_TYPE } from '../base/media/constants';
import {
isAudioMuted,
isScreenshareMuted,
isVideoMuted
} from '../base/media/functions';
import { PARTICIPANT_UPDATED } from '../base/participants/actionTypes';
import { raiseHand } from '../base/participants/actions';
import {
@@ -30,8 +34,10 @@ import {
PARTICIPANT_APPROVED,
PARTICIPANT_REJECTED,
REQUEST_DISABLE_AUDIO_MODERATION,
REQUEST_DISABLE_DESKTOP_MODERATION,
REQUEST_DISABLE_VIDEO_MODERATION,
REQUEST_ENABLE_AUDIO_MODERATION,
REQUEST_ENABLE_DESKTOP_MODERATION,
REQUEST_ENABLE_VIDEO_MODERATION
} from './actionTypes';
import {
@@ -49,8 +55,10 @@ import {
ASKED_TO_UNMUTE_NOTIFICATION_ID,
ASKED_TO_UNMUTE_SOUND_ID,
AUDIO_MODERATION_NOTIFICATION_ID,
CS_MODERATION_NOTIFICATION_ID,
VIDEO_MODERATION_NOTIFICATION_ID
DESKTOP_MODERATION_NOTIFICATION_ID,
MEDIA_TYPE,
MediaType,
VIDEO_MODERATION_NOTIFICATION_ID,
} from './constants';
import {
isEnabledFromState,
@@ -90,9 +98,9 @@ MiddlewareRegistry.register(({ dispatch, getState }) => next => action => {
uid = VIDEO_MODERATION_NOTIFICATION_ID;
break;
}
case MEDIA_TYPE.SCREENSHARE: {
case MEDIA_TYPE.DESKTOP: {
titleKey = 'notify.moderationInEffectCSTitle';
uid = CS_MODERATION_NOTIFICATION_ID;
uid = DESKTOP_MODERATION_NOTIFICATION_ID;
break;
}
}
@@ -115,6 +123,10 @@ MiddlewareRegistry.register(({ dispatch, getState }) => next => action => {
conference?.disableAVModeration(MEDIA_TYPE.AUDIO);
break;
}
case REQUEST_DISABLE_DESKTOP_MODERATION: {
conference?.disableAVModeration(MEDIA_TYPE.DESKTOP);
break;
}
case REQUEST_DISABLE_VIDEO_MODERATION: {
conference?.disableAVModeration(MEDIA_TYPE.VIDEO);
break;
@@ -123,6 +135,10 @@ MiddlewareRegistry.register(({ dispatch, getState }) => next => action => {
conference?.enableAVModeration(MEDIA_TYPE.AUDIO);
break;
}
case REQUEST_ENABLE_DESKTOP_MODERATION: {
conference?.enableAVModeration(MEDIA_TYPE.DESKTOP);
break;
}
case REQUEST_ENABLE_VIDEO_MODERATION: {
conference?.enableAVModeration(MEDIA_TYPE.VIDEO);
break;
@@ -219,24 +235,37 @@ StateListenerRegistry.register(
const customActionHandler = [];
if ((mediaType === MEDIA_TYPE.AUDIO || getState()['features/av-moderation'].audioUnmuteApproved)
&& isAudioMuted(getState())) {
&& isAudioMuted(getState())) {
customActionNameKey.push('notify.unmute');
customActionHandler.push(() => {
dispatch(muteLocal(false, MEDIA_TYPE.AUDIO));
dispatch(muteLocal(false, TRACK_MEDIA_TYPE.AUDIO));
dispatch(hideNotification(ASKED_TO_UNMUTE_NOTIFICATION_ID));
});
}
if ((mediaType === MEDIA_TYPE.VIDEO || getState()['features/av-moderation'].videoUnmuteApproved)
&& isVideoMuted(getState())) {
customActionNameKey.push('notify.unmuteVideo');
if ((mediaType === MEDIA_TYPE.DESKTOP || getState()['features/av-moderation'].desktopUnmuteApproved)
&& isScreenshareMuted(getState())) {
customActionNameKey.push('notify.unmuteScreen');
customActionHandler.push(() => {
dispatch(muteLocal(false, MEDIA_TYPE.VIDEO));
dispatch(muteLocal(false, TRACK_MEDIA_TYPE.SCREENSHARE));
dispatch(hideNotification(ASKED_TO_UNMUTE_NOTIFICATION_ID));
// lower hand as there will be no audio and change in dominant speaker to clear it
// Since permission is requested by raising the hand, lower it not to rely on dominant speaker detection
// to clear the hand.
dispatch(raiseHand(false));
});
}
if ((mediaType === MEDIA_TYPE.VIDEO || getState()['features/av-moderation'].videoUnmuteApproved)
&& isVideoMuted(getState())) {
customActionNameKey.push('notify.unmuteVideo');
customActionHandler.push(() => {
dispatch(muteLocal(false, TRACK_MEDIA_TYPE.VIDEO));
dispatch(hideNotification(ASKED_TO_UNMUTE_NOTIFICATION_ID));
// Since permission is requested by raising the hand, lower it not to rely on dominant speaker detection
// to clear the hand.
dispatch(raiseHand(false));
});
}

View File

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

View File

@@ -24,20 +24,17 @@ MiddlewareRegistry.register(() => (next: Function) => (action: AnyAction) => {
case APP_WILL_MOUNT: {
// Disable it inside an iframe until Google fixes the origin trial for 3rd party sources:
// https://bugs.chromium.org/p/chromium/issues/detail?id=1504167
if (!isEmbedded() && 'PressureObserver' in globalThis) {
if (!isEmbedded() && 'PressureObserver' in window) {
pressureObserver = new window.PressureObserver(
(records: typeof window.PressureRecord) => {
logger.info('Compute pressure state changed:', JSON.stringify(records));
if (typeof APP !== 'undefined') {
APP.API.notifyComputePressureChanged(records);
}
},
{ sampleRate: 1 }
APP.API.notifyComputePressureChanged(records);
}
);
try {
pressureObserver
.observe('cpu')
.observe('cpu', { sampleInterval: 30_000 })
.catch((e: any) => logger.error('CPU pressure observer failed to start', e));
} catch (e: any) {
logger.error('CPU pressure observer failed to start', e);

View File

@@ -83,7 +83,8 @@ import {
getConferenceState,
getCurrentConference,
getVisitorOptions,
sendLocalParticipant
sendLocalParticipant,
updateTrackMuteState
} from './functions';
import logger from './logger';
import { IConferenceMetadata, IJitsiConference } from './reducer';
@@ -186,6 +187,15 @@ function _addConferenceListeners(conference: IJitsiConference, dispatch: IStore[
(disableVideoMuteChange: boolean) => {
dispatch(setVideoUnmutePermissions(disableVideoMuteChange));
});
conference.on(
JitsiConferenceEvents.START_MUTED_POLICY_CHANGED,
({ audio, video }: { audio: boolean; video: boolean; }) => {
dispatch(onStartMutedPolicyChanged(audio, video));
updateTrackMuteState(state, dispatch, true);
updateTrackMuteState(state, dispatch, false);
}
);
// Dispatches into features/base/tracks follow:
@@ -1013,6 +1023,8 @@ export function setStartMutedPolicy(
audio: startAudioMuted,
video: startVideoMuted
});
dispatch(onStartMutedPolicyChanged(startAudioMuted, startVideoMuted));
};
}

View File

@@ -40,3 +40,8 @@ export const CONFERENCE_LEAVE_REASONS = {
SWITCH_ROOM: 'switch_room',
UNRECOVERABLE_ERROR: 'unrecoverable_error'
};
/**
* The ID of the notification that is shown when the user is muted by focus.
*/
export const START_MUTED_NOTIFICATION_ID = 'start-muted';

View File

@@ -3,9 +3,13 @@ import { upperFirst, words } from 'lodash-es';
import { getName } from '../../app/functions';
import { IReduxState, IStore } from '../../app/types';
import { showNotification } from '../../notifications/actions';
import { NOTIFICATION_TIMEOUT_TYPE } from '../../notifications/constants';
import { determineTranscriptionLanguage } from '../../transcribing/functions';
import { IStateful } from '../app/types';
import { JitsiTrackErrors } from '../lib-jitsi-meet';
import { setAudioMuted, setVideoMuted } from '../media/actions';
import { VIDEO_MUTISM_AUTHORITY } from '../media/constants';
import {
participantJoined,
participantLeft
@@ -22,7 +26,8 @@ import { setObfuscatedRoom } from './actions';
import {
AVATAR_URL_COMMAND,
EMAIL_COMMAND,
JITSI_CONFERENCE_URL_KEY
JITSI_CONFERENCE_URL_KEY,
START_MUTED_NOTIFICATION_ID
} from './constants';
import logger from './logger';
import { IJitsiConference } from './reducer';
@@ -574,3 +579,42 @@ function safeStartCase(s = '') {
(result, word, index) => result + (index ? ' ' : '') + upperFirst(word)
, '');
}
/**
* Updates the mute state of the track based on the start muted policy.
*
* @param {Object|Function} stateful - Either the whole Redux state object or the Redux store's {@code getState} method.
* @param {Function} dispatch - Redux dispatch function.
* @param {boolean} isAudio - Whether the track is audio or video.
* @returns {void}
*/
export function updateTrackMuteState(stateful: IStateful, dispatch: IStore['dispatch'], isAudio: boolean) {
const state = toState(stateful);
const mutedPolicyKey = isAudio ? 'startAudioMutedPolicy' : 'startVideoMutedPolicy';
const mutedPolicyValue = state['features/base/conference'][mutedPolicyKey];
// Currently, the policy only supports force muting others, not unmuting them.
if (!mutedPolicyValue) {
return;
}
let muteStateUpdated = false;
const { muted } = isAudio ? state['features/base/media'].audio : state['features/base/media'].video;
if (isAudio && !Boolean(muted)) {
dispatch(setAudioMuted(mutedPolicyValue, true));
muteStateUpdated = true;
} else if (!isAudio && !Boolean(muted)) {
// TODO: Add a new authority for video mutism for the moderator case.
dispatch(setVideoMuted(mutedPolicyValue, VIDEO_MUTISM_AUTHORITY.USER, true));
muteStateUpdated = true;
}
if (muteStateUpdated) {
dispatch(showNotification({
titleKey: 'notify.mutedTitle',
descriptionKey: 'notify.muted',
uid: START_MUTED_NOTIFICATION_ID // use the same id, to make sure we show one notification
}, NOTIFICATION_TIMEOUT_TYPE.SHORT));
}
}

View File

@@ -16,7 +16,7 @@ import { IStore } from '../../app/types';
import { removeLobbyChatParticipant } from '../../chat/actions.any';
import { openDisplayNamePrompt } from '../../display-name/actions';
import { isVpaasMeeting } from '../../jaas/functions';
import { showErrorNotification, showNotification } from '../../notifications/actions';
import { clearNotifications, showErrorNotification, showNotification } from '../../notifications/actions';
import { NOTIFICATION_TIMEOUT_TYPE } from '../../notifications/constants';
import { INotificationProps } from '../../notifications/types';
import { hasDisplayName } from '../../prejoin/utils';
@@ -25,7 +25,7 @@ import LocalRecordingManager from '../../recording/components/Recording/LocalRec
import { AudioMixerEffect } from '../../stream-effects/audio-mixer/AudioMixerEffect';
import { iAmVisitor } from '../../visitors/functions';
import { overwriteConfig } from '../config/actions';
import { CONNECTION_ESTABLISHED, CONNECTION_FAILED } from '../connection/actionTypes';
import { CONNECTION_ESTABLISHED, CONNECTION_FAILED, CONNECTION_WILL_CONNECT } from '../connection/actionTypes';
import { connectionDisconnected, disconnect } from '../connection/actions';
import { validateJwt } from '../jwt/functions';
import { JitsiConferenceErrors, JitsiConferenceEvents, JitsiConnectionErrors } from '../lib-jitsi-meet';
@@ -72,7 +72,6 @@ import {
} from './functions';
import logger from './logger';
import { IConferenceMetadata } from './reducer';
import './subscriber';
/**
* Handler for before unload event.
@@ -99,6 +98,11 @@ MiddlewareRegistry.register(store => next => action => {
case CONNECTION_FAILED:
return _connectionFailed(store, next, action);
case CONNECTION_WILL_CONNECT:
// we are starting a new join process, let's clear the error notifications if any from any previous attempt
store.dispatch(clearNotifications());
break;
case CONFERENCE_SUBJECT_CHANGED:
return _conferenceSubjectChanged(store, next, action);

View File

@@ -1,61 +0,0 @@
import { IStore } from '../../app/types';
import { showNotification } from '../../notifications/actions';
import { NOTIFICATION_TIMEOUT_TYPE } from '../../notifications/constants';
import { setAudioMuted, setVideoMuted } from '../media/actions';
import { VIDEO_MUTISM_AUTHORITY } from '../media/constants';
import StateListenerRegistry from '../redux/StateListenerRegistry';
let hasShownNotification = false;
/**
* Handles changes in the start muted policy for audio and video tracks in the meta data set for the conference.
*/
StateListenerRegistry.register(
/* selector */ state => state['features/base/conference'].startAudioMutedPolicy,
/* listener */ (startAudioMutedPolicy, store) => {
_updateTrackMuteState(store, true);
});
StateListenerRegistry.register(
/* selector */ state => state['features/base/conference'].startVideoMutedPolicy,
/* listener */(startVideoMutedPolicy, store) => {
_updateTrackMuteState(store, false);
});
/**
* Updates the mute state of the track based on the start muted policy.
*
* @param {IStore} store - The redux store.
* @param {boolean} isAudio - Whether the track is audio or video.
* @returns {void}
*/
function _updateTrackMuteState(store: IStore, isAudio: boolean) {
const { dispatch, getState } = store;
const mutedPolicyKey = isAudio ? 'startAudioMutedPolicy' : 'startVideoMutedPolicy';
const mutedPolicyValue = getState()['features/base/conference'][mutedPolicyKey];
// Currently, the policy only supports force muting others, not unmuting them.
if (!mutedPolicyValue) {
return;
}
let muteStateUpdated = false;
const { muted } = isAudio ? getState()['features/base/media'].audio : getState()['features/base/media'].video;
if (isAudio && !Boolean(muted)) {
dispatch(setAudioMuted(mutedPolicyValue, true));
muteStateUpdated = true;
} else if (!isAudio && !Boolean(muted)) {
// TODO: Add a new authority for video mutism for the moderator case.
dispatch(setVideoMuted(mutedPolicyValue, VIDEO_MUTISM_AUTHORITY.USER, true));
muteStateUpdated = true;
}
if (!hasShownNotification && muteStateUpdated) {
hasShownNotification = true;
dispatch(showNotification({
titleKey: 'notify.mutedTitle',
descriptionKey: 'notify.muted'
}, NOTIFICATION_TIMEOUT_TYPE.SHORT));
}
}

View File

@@ -172,7 +172,6 @@ export interface IConfig {
_screenshotHistoryRegionUrl?: number;
analytics?: {
amplitudeAPPKey?: string;
amplitudeIncludeUTM?: boolean;
blackListedEvents?: string[];
disabled?: boolean;
matomoEndpoint?: string;
@@ -384,10 +383,12 @@ export interface IConfig {
maxFileSize?: number;
};
filmstrip?: {
alwaysShowResizeBar?: boolean;
disableResizable?: boolean;
disableStageFilmstrip?: boolean;
disableTopPanel?: boolean;
disabled?: boolean;
initialWidth?: number;
minParticipantCountForTopPanel?: number;
};
flags?: {
@@ -552,7 +553,7 @@ export interface IConfig {
disableDemote?: boolean;
disableGrantModerator?: boolean;
disableKick?: boolean;
disablePrivateChat?: 'all' | 'allow-moderator-chat';
disablePrivateChat?: 'all' | 'allow-moderator-chat' | 'disable-visitor-chat';
disabled?: boolean;
};
replaceParticipant?: string;
@@ -594,6 +595,7 @@ export interface IConfig {
failICE?: boolean;
noAutoPlayVideo?: boolean;
p2pTestMode?: boolean;
showSpotConsentDialog?: boolean;
skipInterimTranscriptions?: boolean;
testMode?: boolean;
};

View File

@@ -2,6 +2,7 @@ import { AnyAction } from 'redux';
import { IStore } from '../../app/types';
import { SET_DYNAMIC_BRANDING_DATA } from '../../dynamic-branding/actionTypes';
import { setUserFilmstripWidth } from '../../filmstrip/actions.web';
import { getFeatureFlag } from '../flags/functions';
import MiddlewareRegistry from '../redux/MiddlewareRegistry';
import { updateSettings } from '../settings/actions';
@@ -79,12 +80,18 @@ function _setConfig({ dispatch, getState }: IStore, next: Function, action: AnyA
}));
}
if (action.config.filmstrip?.stageFilmstripParticipants !== undefined) {
const { initialWidth, stageFilmstripParticipants } = action.config.filmstrip || {};
if (stageFilmstripParticipants !== undefined) {
dispatch(updateSettings({
maxStageParticipants: action.config.filmstrip.stageFilmstripParticipants
maxStageParticipants: stageFilmstripParticipants
}));
}
if (initialWidth) {
dispatch(setUserFilmstripWidth(initialWidth));
}
dispatch(updateConfig(config));
// FIXME On Web we rely on the global 'config' variable which gets altered

View File

@@ -185,6 +185,15 @@ function _setConfig(state: IConfig, { config }: { config: IConfig; }) {
});
}
const { alwaysShowResizeBar, disableResizable } = config.filmstrip || {};
if (alwaysShowResizeBar && disableResizable) {
config.filmstrip = {
...config.filmstrip,
alwaysShowResizeBar: false
};
}
const newState = merge(
{},
config,

View File

@@ -18,6 +18,7 @@ export const MEET_FEATURES: Record<string, ParticipantFeaturesKey> = {
ROOM: 'room',
SCREEN_SHARING: 'screen-sharing',
SEND_GROUPCHAT: 'send-groupchat',
LIST_VISITORS: 'list-visitors',
SIP_INBOUND_CALL: 'sip-inbound-call',
SIP_OUTBOUND_CALL: 'sip-outbound-call',
TRANSCRIPTION: 'transcription'

View File

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

View File

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

View File

@@ -88,6 +88,16 @@ export function getStartWithVideoMuted(stateful: IStateful) {
return Boolean(getPropertyValue(stateful, 'startWithVideoMuted', START_WITH_AUDIO_VIDEO_MUTED_SOURCES));
}
/**
* Determines whether screen-share is currently muted.
*
* @param {Function|Object} stateful - The redux store, state, or {@code getState} function.
* @returns {boolean}
*/
export function isScreenshareMuted(stateful: IStateful) {
return Boolean(toState(stateful)['features/base/media'].screenshare.muted);
}
/**
* Determines whether video is currently muted.
*

View File

@@ -8,15 +8,17 @@ import {
} from '../../analytics/AnalyticsEvents';
import { sendAnalytics } from '../../analytics/functions';
import { IStore } from '../../app/types';
import { MEDIA_TYPE as AVM_MEDIA_TYPE } from '../../av-moderation/constants';
import { isForceMuted } from '../../av-moderation/functions';
import { APP_STATE_CHANGED } from '../../mobile/background/actionTypes';
import { showWarningNotification } from '../../notifications/actions';
import { NOTIFICATION_TIMEOUT_TYPE } from '../../notifications/constants';
import { isForceMuted } from '../../participants-pane/functions';
import { isScreenMediaShared } from '../../screen-share/functions';
import { SET_AUDIO_ONLY } from '../audio-only/actionTypes';
import { setAudioOnly } from '../audio-only/actions';
import { SET_ROOM } from '../conference/actionTypes';
import { isRoomValid } from '../conference/functions';
import { PARTICIPANT_MUTED_US } from '../participants/actionTypes';
import { getLocalParticipant } from '../participants/functions';
import MiddlewareRegistry from '../redux/MiddlewareRegistry';
import { getPropertyValue } from '../settings/functions.any';
@@ -46,7 +48,8 @@ import {
import {
MEDIA_TYPE,
SCREENSHARE_MUTISM_AUTHORITY,
VIDEO_MUTISM_AUTHORITY
VIDEO_MUTISM_AUTHORITY,
VIDEO_TYPE
} from './constants';
import { getStartWithAudioMuted, getStartWithVideoMuted } from './functions';
import logger from './logger';
@@ -66,6 +69,24 @@ MiddlewareRegistry.register(store => next => action => {
case APP_STATE_CHANGED:
return _appStateChanged(store, next, action);
case PARTICIPANT_MUTED_US: {
const { dispatch } = store;
const { track } = action;
// Sync the media muted state with the track muted state.
if (track.isAudioTrack()) {
dispatch(setAudioMuted(true, /* ensureTrack */ false));
} else if (track.isVideoTrack()) {
if (track.getVideoType() === VIDEO_TYPE.DESKTOP) {
dispatch(setScreenshareMuted(true, SCREENSHARE_MUTISM_AUTHORITY.USER, /* ensureTrack */ false));
} else {
dispatch(setVideoMuted(true, VIDEO_MUTISM_AUTHORITY.USER, /* ensureTrack */ false));
}
}
break;
}
case SET_AUDIO_ONLY:
return _setAudioOnly(store, next, action);
@@ -88,7 +109,7 @@ MiddlewareRegistry.register(store => next => action => {
const state = store.getState();
const participant = getLocalParticipant(state);
if (!action.muted && isForceMuted(participant, MEDIA_TYPE.AUDIO, state)) {
if (!action.muted && isForceMuted(participant, AVM_MEDIA_TYPE.AUDIO, state)) {
return;
}
break;
@@ -113,7 +134,7 @@ MiddlewareRegistry.register(store => next => action => {
const state = store.getState();
const participant = getLocalParticipant(state);
if (!action.muted && isForceMuted(participant, MEDIA_TYPE.SCREENSHARE, state)) {
if (!action.muted && isForceMuted(participant, AVM_MEDIA_TYPE.DESKTOP, state)) {
return;
}
break;
@@ -122,7 +143,7 @@ MiddlewareRegistry.register(store => next => action => {
const state = store.getState();
const participant = getLocalParticipant(state);
if (!action.muted && isForceMuted(participant, MEDIA_TYPE.VIDEO, state)) {
if (!action.muted && isForceMuted(participant, AVM_MEDIA_TYPE.VIDEO, state)) {
return;
}
break;

View File

@@ -112,6 +112,17 @@ export const PARTICIPANT_KICKED = 'PARTICIPANT_KICKED';
*/
export const PARTICIPANT_LEFT = 'PARTICIPANT_LEFT';
/**
* Action to handle case when the remote participant mutes the local participant.
*
* {
* type: PARTICIPANT_MUTED_US,
* participant: Participant,
* track: JitsiLocalTrack
* }
*/
export const PARTICIPANT_MUTED_US = 'PARTICIPANT_MUTED_US';
/**
* Action to handle case when the sources attached to a participant are updated.
*

View File

@@ -17,6 +17,7 @@ import {
PARTICIPANT_JOINED,
PARTICIPANT_KICKED,
PARTICIPANT_LEFT,
PARTICIPANT_MUTED_US,
PARTICIPANT_ROLE_CHANGED,
PARTICIPANT_SOURCES_UPDATED,
PARTICIPANT_UPDATED,
@@ -467,19 +468,10 @@ export function participantUpdated(participant: IParticipant = { id: '' }) {
* @returns {Promise}
*/
export function participantMutedUs(participant: any, track: any) {
return (dispatch: IStore['dispatch'], getState: IStore['getState']) => {
if (!participant) {
return;
}
const isAudio = track.isAudioTrack();
dispatch(showNotification({
titleKey: isAudio ? 'notify.mutedRemotelyTitle' : 'notify.videoMutedRemotelyTitle',
titleArguments: {
participantDisplayName: getParticipantDisplayName(getState, participant.getId())
}
}, NOTIFICATION_TIMEOUT_TYPE.MEDIUM));
return {
type: PARTICIPANT_MUTED_US,
participant,
track
};
}

View File

@@ -2,10 +2,12 @@
import { getGravatarURL } from '@jitsi/js-utils/avatar';
import { IReduxState, IStore } from '../../app/types';
import { isVisitorChatParticipant } from '../../chat/functions';
import { isStageFilmstripAvailable } from '../../filmstrip/functions';
import { isAddPeopleEnabled, isDialOutEnabled } from '../../invite/functions';
import { toggleShareDialog } from '../../share-room/actions';
import { iAmVisitor } from '../../visitors/functions';
import { IVisitorChatParticipant } from '../../visitors/types';
import { IStateful } from '../app/types';
import { GRAVATAR_BASE_URL } from '../avatar/constants';
import { isCORSAvatarURL } from '../avatar/functions';
@@ -392,7 +394,7 @@ export function getMutedStateByParticipantAndMediaType(
if (mediaType === MEDIA_TYPE.AUDIO) {
return Array.from(sources.values())[0].muted;
}
const videoType = mediaType === MEDIA_TYPE.VIDEO ? VIDEO_TYPE.CAMERA : VIDEO_TYPE.SCREENSHARE;
const videoType = mediaType === MEDIA_TYPE.VIDEO ? VIDEO_TYPE.CAMERA : VIDEO_TYPE.DESKTOP;
const source = Array.from(sources.values()).find(src => src.videoType === videoType);
return source?.muted ?? true;
@@ -827,18 +829,28 @@ export const setShareDialogVisiblity = (addPeopleFeatureEnabled: boolean, dispat
/**
* Checks if private chat is enabled for the given participant.
*
* @param {IParticipant|undefined} participant - The participant to check.
* @param {IParticipant|IVisitorChatParticipant|undefined} participant - The participant to check.
* @param {IReduxState} state - The Redux state.
* @returns {boolean} - True if private chat is enabled, false otherwise.
*/
export function isPrivateChatEnabled(participant: IParticipant | undefined, state: IReduxState) {
export function isPrivateChatEnabled(participant: IParticipant | IVisitorChatParticipant | undefined, state: IReduxState) {
const { remoteVideoMenu = {} } = state['features/base/config'];
const { disablePrivateChat } = remoteVideoMenu;
if (participant?.local || state['features/visitors'].iAmVisitor || disablePrivateChat === 'all') {
if ((!isVisitorChatParticipant(participant) && participant?.local) || disablePrivateChat === 'all') {
return false;
}
if (disablePrivateChat === 'disable-visitor-chat') {
// Block if the participant we're trying to message is a visitor
// OR if the local user is a visitor
if (isVisitorChatParticipant(participant) || iAmVisitor(state)) {
return false;
}
return true; // should allow private chat for other participants
}
if (disablePrivateChat === 'allow-moderator-chat') {
return isLocalParticipantModerator(state) || isParticipantModerator(participant);
}

View File

@@ -3,7 +3,19 @@ import { batch } from 'react-redux';
import { AnyAction } from 'redux';
import { IStore } from '../../app/types';
import { approveParticipant, approveParticipantAudio, approveParticipantVideo } from '../../av-moderation/actions';
import {
approveParticipant,
approveParticipantAudio,
approveParticipantDesktop,
approveParticipantVideo
} from '../../av-moderation/actions';
import {
AUDIO_RAISED_HAND_NOTIFICATION_ID,
DESKTOP_RAISED_HAND_NOTIFICATION_ID,
MEDIA_TYPE,
VIDEO_RAISED_HAND_NOTIFICATION_ID
} from '../../av-moderation/constants';
import { isForceMuted } from '../../av-moderation/functions';
import { UPDATE_BREAKOUT_ROOMS } from '../../breakout-rooms/actionTypes';
import { getBreakoutRooms } from '../../breakout-rooms/functions';
import { toggleE2EE } from '../../e2ee/actions';
@@ -15,7 +27,6 @@ import {
RAISE_HAND_NOTIFICATION_ID
} from '../../notifications/constants';
import { open as openParticipantsPane } from '../../participants-pane/actions';
import { isForceMuted } from '../../participants-pane/functions';
import { CALLING, INVITED } from '../../presence-status/constants';
import { RAISE_HAND_SOUND_ID } from '../../reactions/constants';
import { RECORDING_OFF_SOUND_ID, RECORDING_ON_SOUND_ID } from '../../recording/constants';
@@ -26,7 +37,7 @@ import { IJitsiConference } from '../conference/reducer';
import { SET_CONFIG } from '../config/actionTypes';
import { getDisableRemoveRaisedHandOnFocus } from '../config/functions.any';
import { JitsiConferenceEvents } from '../lib-jitsi-meet';
import { MEDIA_TYPE } from '../media/constants';
import { VIDEO_TYPE } from '../media/constants';
import MiddlewareRegistry from '../redux/MiddlewareRegistry';
import StateListenerRegistry from '../redux/StateListenerRegistry';
import { playSound, registerSound, unregisterSound } from '../sounds/actions';
@@ -43,6 +54,7 @@ import {
OVERWRITE_PARTICIPANT_NAME,
PARTICIPANT_JOINED,
PARTICIPANT_LEFT,
PARTICIPANT_MUTED_US,
PARTICIPANT_UPDATED,
RAISE_HAND_UPDATED,
SET_LOCAL_PARTICIPANT_RECORDING_STATUS
@@ -292,6 +304,31 @@ MiddlewareRegistry.register(store => next => action => {
break;
}
case PARTICIPANT_MUTED_US: {
const { dispatch, getState } = store;
const { participant, track } = action;
let titleKey;
if (track.isAudioTrack()) {
titleKey = 'notify.mutedRemotelyTitle';
} else if (track.isVideoTrack()) {
if (track.getVideoType() === VIDEO_TYPE.DESKTOP) {
titleKey = 'notify.desktopMutedRemotelyTitle';
} else {
titleKey = 'notify.videoMutedRemotelyTitle';
}
}
dispatch(showNotification({
titleKey,
titleArguments: {
participantDisplayName: getParticipantDisplayName(getState, participant.getId())
}
}, NOTIFICATION_TIMEOUT_TYPE.MEDIUM));
break;
}
case PARTICIPANT_UPDATED:
return _participantJoinedOrUpdated(store, next, action);
@@ -786,77 +823,124 @@ function _raiseHandUpdated({ dispatch, getState }: IStore, conference: IJitsiCon
APP.API.notifyRaiseHandUpdated(participantId, raisedHandTimestamp);
}
if (!raisedHandTimestamp) {
return;
}
// Display notifications about raised hands.
const isModerator = isLocalParticipantModerator(state);
const participant = getParticipantById(state, participantId);
const participantName = getParticipantDisplayName(state, participantId);
let shouldDisplayAllowAudio = false;
let shouldDisplayAllowVideo = false;
let shouldDisplayAllowDesktop = false;
if (isModerator) {
shouldDisplayAllowAudio = isForceMuted(participant, MEDIA_TYPE.AUDIO, state);
shouldDisplayAllowVideo = isForceMuted(participant, MEDIA_TYPE.VIDEO, state);
shouldDisplayAllowDesktop = isForceMuted(participant, MEDIA_TYPE.DESKTOP, state);
}
let action;
if (shouldDisplayAllowAudio || shouldDisplayAllowVideo) {
action = {
customActionNameKey: [] as string[],
customActionHandler: [] as Function[]
if (shouldDisplayAllowAudio || shouldDisplayAllowVideo || shouldDisplayAllowDesktop) {
const action: {
customActionHandler: Array<() => void>;
customActionNameKey: string[];
} = {
customActionHandler: [],
customActionNameKey: [],
};
// Always add a "allow all" at the end of the list.
action.customActionNameKey.push('notify.allowAll');
action.customActionHandler.push(() => {
dispatch(approveParticipant(participantId));
dispatch(hideNotification(AUDIO_RAISED_HAND_NOTIFICATION_ID));
dispatch(hideNotification(DESKTOP_RAISED_HAND_NOTIFICATION_ID));
dispatch(hideNotification(VIDEO_RAISED_HAND_NOTIFICATION_ID));
});
if (shouldDisplayAllowAudio) {
action.customActionNameKey.push('notify.allowAudio');
action.customActionHandler.push(() => {
const customActionNameKey = action.customActionNameKey.slice();
const customActionHandler = action.customActionHandler.slice();
customActionNameKey.unshift('notify.allowAudio');
customActionHandler.unshift(() => {
dispatch(approveParticipantAudio(participantId));
dispatch(hideNotification(RAISE_HAND_NOTIFICATION_ID));
dispatch(hideNotification(AUDIO_RAISED_HAND_NOTIFICATION_ID));
});
dispatch(showNotification({
title: participantName,
descriptionKey: 'notify.raisedHand',
uid: AUDIO_RAISED_HAND_NOTIFICATION_ID,
customActionNameKey,
customActionHandler,
}, NOTIFICATION_TIMEOUT_TYPE.EXTRA_LONG));
}
if (shouldDisplayAllowVideo) {
action.customActionNameKey.push('notify.allowVideo');
action.customActionHandler.push(() => {
const customActionNameKey = action.customActionNameKey.slice();
const customActionHandler = action.customActionHandler.slice();
customActionNameKey.unshift('notify.allowVideo');
customActionHandler.unshift(() => {
dispatch(approveParticipantVideo(participantId));
dispatch(hideNotification(RAISE_HAND_NOTIFICATION_ID));
dispatch(hideNotification(VIDEO_RAISED_HAND_NOTIFICATION_ID));
});
dispatch(showNotification({
title: participantName,
descriptionKey: 'notify.raisedHand',
uid: VIDEO_RAISED_HAND_NOTIFICATION_ID,
customActionNameKey,
customActionHandler,
}, NOTIFICATION_TIMEOUT_TYPE.EXTRA_LONG));
}
if (shouldDisplayAllowAudio && shouldDisplayAllowVideo) {
action.customActionNameKey.push('notify.allowBoth');
action.customActionHandler.push(() => {
dispatch(approveParticipant(participantId));
dispatch(hideNotification(RAISE_HAND_NOTIFICATION_ID));
if (shouldDisplayAllowDesktop) {
const customActionNameKey = action.customActionNameKey.slice();
const customActionHandler = action.customActionHandler.slice();
customActionNameKey.unshift('notify.allowDesktop');
customActionHandler.unshift(() => {
dispatch(approveParticipantDesktop(participantId));
dispatch(hideNotification(DESKTOP_RAISED_HAND_NOTIFICATION_ID));
});
dispatch(showNotification({
title: participantName,
descriptionKey: 'notify.raisedHand',
uid: DESKTOP_RAISED_HAND_NOTIFICATION_ID,
customActionNameKey,
customActionHandler
}, NOTIFICATION_TIMEOUT_TYPE.EXTRA_LONG));
}
} else {
action = {
customActionNameKey: [ 'notify.viewParticipants' ],
customActionHandler: [ () => dispatch(openParticipantsPane()) ]
};
}
if (raisedHandTimestamp) {
let notificationTitle;
const participantName = getParticipantDisplayName(state, participantId);
const { raisedHandsQueue } = state['features/base/participants'];
if (raisedHandsQueue.length > 1) {
const raisedHands = raisedHandsQueue.length - 1;
notificationTitle = i18n.t('notify.raisedHands', {
participantName,
raisedHands
raisedHands: raisedHandsQueue.length - 1
});
} else {
notificationTitle = participantName;
}
dispatch(showNotification({
titleKey: 'notify.somebody',
title: notificationTitle,
descriptionKey: 'notify.raisedHand',
concatText: true,
uid: RAISE_HAND_NOTIFICATION_ID,
...action
}, NOTIFICATION_TIMEOUT_TYPE.MEDIUM));
dispatch(playSound(RAISE_HAND_SOUND_ID));
customActionNameKey: [ 'notify.viewParticipants' ],
customActionHandler: [ () => dispatch(openParticipantsPane()) ]
}, NOTIFICATION_TIMEOUT_TYPE.LONG));
}
dispatch(playSound(RAISE_HAND_SOUND_ID));
}
/**

View File

@@ -61,6 +61,7 @@ export interface IParticipantFeatures {
'file-upload'?: boolean | string;
'flip'?: boolean | string;
'inbound-call'?: boolean | string;
'list-visitors'?: boolean | string;
'livestreaming'?: boolean | string;
'lobby'?: boolean | string;
'moderation'?: boolean | string;

View File

@@ -2,6 +2,7 @@
import { AUDIO_ONLY_SCREEN_SHARE_NO_TRACK } from '../../../../modules/UI/UIErrors';
import { IReduxState, IStore } from '../../app/types';
import { showModeratedNotification } from '../../av-moderation/actions';
import { MEDIA_TYPE as AVM_MEDIA_TYPE } from '../../av-moderation/constants';
import { shouldShowModeratedNotification } from '../../av-moderation/functions';
import { setNoiseSuppressionEnabled } from '../../noise-suppression/actions';
import { showErrorNotification, showNotification } from '../../notifications/actions';
@@ -55,10 +56,10 @@ export function toggleScreensharing(
shareOptions: IShareOptions = {}) {
return (dispatch: IStore['dispatch'], getState: IStore['getState']) => {
// check for A/V Moderation when trying to start screen sharing
if ((enabled || enabled === undefined) && shouldShowModeratedNotification(MEDIA_TYPE.VIDEO, getState())) {
dispatch(showModeratedNotification(MEDIA_TYPE.SCREENSHARE));
if ((enabled || enabled === undefined) && shouldShowModeratedNotification(AVM_MEDIA_TYPE.DESKTOP, getState())) {
dispatch(showModeratedNotification(AVM_MEDIA_TYPE.DESKTOP));
return Promise.reject();
return Promise.resolve();
}
return _toggleScreenSharing({

View File

@@ -1,10 +1,12 @@
import { IReduxState, IStore } from '../../app/types';
import { getSsrcRewritingFeatureFlag } from '../config/functions.any';
import { JitsiTrackErrors, browser } from '../lib-jitsi-meet';
import { gumPending } from '../media/actions';
import { CAMERA_FACING_MODE, MEDIA_TYPE, MediaType, VIDEO_TYPE } from '../media/constants';
import { IMediaState } from '../media/reducer';
import { IGUMPendingState } from '../media/types';
import {
getMutedStateByParticipantAndMediaType,
getVirtualScreenshareParticipantOwnerId,
isScreenShareParticipant
} from '../participants/functions';
@@ -35,6 +37,10 @@ export function isParticipantMediaMuted(participant: IParticipant | undefined,
return false;
}
if (getSsrcRewritingFeatureFlag(state)) {
return getMutedStateByParticipantAndMediaType(state, participant, mediaType);
}
const tracks = getTrackState(state);
if (participant?.local) {
@@ -53,10 +59,21 @@ export function isParticipantMediaMuted(participant: IParticipant | undefined,
* @param {IReduxState} state - Global state.
* @returns {boolean} - Is audio muted for the participant.
*/
export function isParticipantAudioMuted(participant: IParticipant, state: IReduxState) {
export function isParticipantAudioMuted(participant: IParticipant | undefined, state: IReduxState) {
return isParticipantMediaMuted(participant, MEDIA_TYPE.AUDIO, state);
}
/**
* Checks if the participant is screen-share muted.
*
* @param {IParticipant} participant - Participant reference.
* @param {IReduxState} state - Global state.
* @returns {boolean} - Is screen-share muted for the participant.
*/
export function isParticipantScreenShareMuted(participant: IParticipant | undefined, state: IReduxState) {
return isParticipantMediaMuted(participant, MEDIA_TYPE.SCREENSHARE, state);
}
/**
* Checks if the participant is video muted.
*
@@ -118,6 +135,10 @@ export function getLocalJitsiDesktopTrack(state: IReduxState) {
* @returns {(Track|undefined)}
*/
export function getLocalTrack(tracks: ITrack[], mediaType: MediaType, includePending = false) {
if (mediaType === MEDIA_TYPE.SCREENSHARE) {
return getLocalDesktopTrack(tracks, includePending);
}
return (
getLocalTracks(tracks, includePending)
.find(t => t.mediaType === mediaType));
@@ -216,6 +237,14 @@ export function getTrackByMediaTypeAndParticipant(
tracks: ITrack[],
mediaType: MediaType,
participantId?: string) {
if (!participantId) {
return;
}
if (mediaType === MEDIA_TYPE.SCREENSHARE) {
return getScreenShareTrack(tracks, participantId);
}
return tracks.find(
t => Boolean(t.jitsiTrack) && t.participantId === participantId && t.mediaType === mediaType
);

View File

@@ -188,10 +188,6 @@ function _setMuted(store: IStore, { ensureTrack, muted }: {
const localTrack = _getLocalTrack(store, mediaType, /* includePending */ true);
const state = getState();
if (mediaType === MEDIA_TYPE.SCREENSHARE && !muted) {
return;
}
if (localTrack) {
// The `jitsiTrack` property will have a value only for a localTrack for which `getUserMedia` has already
// completed. If there's no `jitsiTrack`, then the `muted` state will be applied once the `jitsiTrack` is
@@ -203,8 +199,11 @@ function _setMuted(store: IStore, { ensureTrack, muted }: {
.catch(() => dispatch(trackMuteUnmuteFailed(localTrack, muted)));
}
} else if (!muted && ensureTrack) {
// TODO(saghul): reconcile these 2 types.
const createMediaType = mediaType === MEDIA_TYPE.SCREENSHARE ? 'desktop' : mediaType;
typeof APP !== 'undefined' && dispatch(gumPending([ mediaType ], IGUMPendingState.PENDING_UNMUTE));
dispatch(createLocalTracksA({ devices: [ mediaType ] })).then(() => {
dispatch(createLocalTracksA({ devices: [ createMediaType ] })).then(() => {
typeof APP !== 'undefined' && dispatch(gumPending([ mediaType ], IGUMPendingState.NONE));
});
}

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

@@ -14,7 +14,7 @@ export interface IProps {
/**
* Label used for accessibility.
*/
accessibilityLabel: string;
accessibilityLabel?: string;
/**
* The context menu item background color.
@@ -232,7 +232,7 @@ const ContextMenuItem = ({
<div
aria-controls = { controls }
aria-disabled = { disabled }
aria-label = { accessibilityLabel }
aria-label = { accessibilityLabel || undefined }
aria-selected = { role === 'tab' ? selected : undefined }
className = { cx(styles.contextMenuItem,
_overflowDrawer && styles.contextMenuItemDrawer,

View File

@@ -12,13 +12,7 @@ const JITSI_MEET_APPS = [
'org.jitsi.meet',
// Android debug app.
'org.jitsi.meet.debug',
// 8x8 Work (Android).
'org.vom8x8.sipua',
// 8x8 Work (iOS).
'com.yourcompany.Virtual-Office'
'org.jitsi.meet.debug'
];
/**

View File

@@ -4,6 +4,7 @@ import { WithTranslation } from 'react-i18next';
import { IReduxState, IStore } from '../../app/types';
import { getParticipantById } from '../../base/participants/functions';
import { IParticipant } from '../../base/participants/types';
import { IVisitorChatParticipant } from '../../visitors/types';
import { sendMessage, setPrivateMessageRecipient } from '../actions';
interface IProps extends WithTranslation {
@@ -23,6 +24,16 @@ interface IProps extends WithTranslation {
*/
_participant?: IParticipant;
/**
* The display name of the visitor (if applicable).
*/
displayName?: string;
/**
* Whether the message is from a visitor.
*/
isFromVisitor?: boolean;
/**
* The message that is about to be sent.
*/
@@ -67,9 +78,21 @@ export class AbstractChatPrivacyDialog extends PureComponent<IProps> {
* @returns {void}
*/
_onSendPrivateMessage() {
const { message, _onSendMessage, _onSetMessageRecipient, _participant } = this.props;
const { message, _onSendMessage, _onSetMessageRecipient, _participant, isFromVisitor, displayName, participantID } = this.props;
if (isFromVisitor) {
// For visitors, create a participant object since they don't exist in the main participant list
const visitorParticipant = {
id: participantID,
name: displayName,
isVisitor: true
};
_onSetMessageRecipient(visitorParticipant);
} else {
_onSetMessageRecipient(_participant);
}
_onSetMessageRecipient(_participant);
_onSendMessage(message);
return true;
@@ -88,7 +111,7 @@ export function _mapDispatchToProps(dispatch: IStore['dispatch']) {
dispatch(sendMessage(message, true));
},
_onSetMessageRecipient: (participant: IParticipant) => {
_onSetMessageRecipient: (participant: IParticipant | IVisitorChatParticipant) => {
dispatch(setPrivateMessageRecipient(participant));
}
};
@@ -103,6 +126,6 @@ export function _mapDispatchToProps(dispatch: IStore['dispatch']) {
*/
export function _mapStateToProps(state: IReduxState, ownProps: IProps) {
return {
_participant: getParticipantById(state, ownProps.participantID)
_participant: ownProps.isFromVisitor ? undefined : getParticipantById(state, ownProps.participantID)
};
}

View File

@@ -3,7 +3,9 @@ import { WithTranslation } from 'react-i18next';
import { IReduxState, IStore } from '../../app/types';
import { getParticipantDisplayName, isLocalParticipantModerator } from '../../base/participants/functions';
import { getVisitorDisplayName } from '../../visitors/functions';
import { setLobbyChatActiveState, setPrivateMessageRecipient } from '../actions.any';
import { isVisitorChatParticipant } from '../functions';
export interface IProps extends WithTranslation {
@@ -13,6 +15,11 @@ export interface IProps extends WithTranslation {
*/
_isLobbyChatActive: boolean;
/**
* Whether the private message recipient is a visitor.
*/
_isVisitor?: boolean;
/**
* The name of the lobby message recipient, if any.
*/
@@ -72,10 +79,18 @@ export function _mapDispatchToProps(dispatch: IStore['dispatch']) {
*/
export function _mapStateToProps(state: IReduxState, _ownProps: any) {
const { privateMessageRecipient, lobbyMessageRecipient, isLobbyChatActive } = state['features/chat'];
let _privateMessageRecipient;
const _isVisitor = isVisitorChatParticipant(privateMessageRecipient);
if (privateMessageRecipient) {
_privateMessageRecipient = _isVisitor
? getVisitorDisplayName(state, privateMessageRecipient.name)
: getParticipantDisplayName(state, privateMessageRecipient.id);
}
return {
_privateMessageRecipient:
privateMessageRecipient ? getParticipantDisplayName(state, privateMessageRecipient.id) : undefined,
_privateMessageRecipient,
_isVisitor,
_isLobbyChatActive: isLobbyChatActive,
_lobbyMessageRecipient:
isLobbyChatActive && lobbyMessageRecipient ? lobbyMessageRecipient.name : undefined,

View File

@@ -122,15 +122,17 @@ class ChatMessage extends Component<IChatMessageProps> {
* @returns {React.ReactElement<*> | null}
*/
_renderDisplayName() {
const { message, showDisplayName } = this.props;
const { message, showDisplayName, t } = this.props;
if (!showDisplayName) {
return null;
}
const { displayName, isFromVisitor } = message;
return (
<Text style = { styles.senderDisplayName }>
{ message.displayName }
{ `${displayName}${isFromVisitor ? ` ${t('visitors.chatIndicator')}` : ''}` }
</Text>
);
}

View File

@@ -12,7 +12,8 @@ import {
} from '../../../mobile/navigation/components/conference/ConferenceNavigationContainerRef';
import { setLobbyChatActiveState, setPrivateMessageRecipient } from '../../actions.any';
import AbstractMessageRecipient, {
IProps as AbstractProps
IProps as AbstractProps,
_mapStateToProps as _mapStateToPropsAbstract
} from '../AbstractMessageRecipient';
import styles from './styles';
@@ -36,11 +37,6 @@ interface IProps extends AbstractProps {
id: string;
name: string;
} | ILocalParticipant;
/**
* The participant object set for private messaging.
*/
privateMessageRecipient: { name: string; };
}
/**
@@ -96,7 +92,8 @@ class MessageRecipient extends AbstractMessageRecipient<IProps> {
const {
isLobbyChatActive,
lobbyMessageRecipient,
privateMessageRecipient,
_privateMessageRecipient,
_isVisitor,
t
} = this.props;
@@ -120,7 +117,7 @@ class MessageRecipient extends AbstractMessageRecipient<IProps> {
);
}
if (!privateMessageRecipient) {
if (!_privateMessageRecipient) {
return null;
}
@@ -130,7 +127,7 @@ class MessageRecipient extends AbstractMessageRecipient<IProps> {
style = { styles.messageRecipientContainer as ViewStyle }>
<Text style = { styles.messageRecipientText }>
{ t('chat.messageTo', {
recipient: privateMessageRecipient.name
recipient: `${_privateMessageRecipient}${_isVisitor ? ` ${t('visitors.chatIndicator')}` : ''}`
}) }
</Text>
<TouchableHighlight
@@ -157,6 +154,7 @@ function _mapStateToProps(state: IReduxState, _ownProps: any) {
const { lobbyMessageRecipient, isLobbyChatActive } = state['features/chat'];
return {
..._mapStateToPropsAbstract(state, _ownProps),
isLobbyChatActive,
lobbyMessageRecipient
};

View File

@@ -224,11 +224,13 @@ const ChatMessage = ({
* @returns {React$Element<*>}
*/
function _renderDisplayName() {
const { displayName, isFromVisitor = false } = message;
return (
<div
aria-hidden = { true }
className = { cx('display-name', classes.displayName) }>
{message.displayName}
{`${displayName}${isFromVisitor ? ` ${t('visitors.chatIndicator')}` : ''}`}
</div>
);
}
@@ -338,6 +340,8 @@ const ChatMessage = ({
{!shouldDisplayChatMessageMenu && (
<div className = { classes.optionsButtonContainer }>
{isHovered && <MessageMenu
displayName = { message.displayName }
isFromVisitor = { message.isFromVisitor }
isLobbyMessage = { message.lobbyChat }
message = { message.message }
participantId = { message.participantId }
@@ -391,6 +395,8 @@ const ChatMessage = ({
<div>
<div className = { classes.optionsButtonContainer }>
{isHovered && <MessageMenu
displayName = { message.displayName }
isFromVisitor = { message.isFromVisitor }
isLobbyMessage = { message.lobbyChat }
message = { message.message }
participantId = { message.participantId }
@@ -414,7 +420,15 @@ function _mapStateToProps(state: IReduxState, { message }: IProps) {
const { knocking } = state['features/lobby'];
const participant = getParticipantById(state, message.participantId);
const enablePrivateChat = isPrivateChatEnabled(participant, state);
// For visitor private messages, participant will be undefined but we should still allow private chat
// Create a visitor participant object for visitor messages to pass to isPrivateChatEnabled
const participantForCheck = message.isFromVisitor
? { id: message.participantId, name: message.displayName, isVisitor: true as const }
: participant;
const enablePrivateChat = (!message.isFromVisitor || message.privateMessage)
&& isPrivateChatEnabled(participantForCheck, state);
return {
shouldDisplayChatMessageMenu: enablePrivateChat,

View File

@@ -15,6 +15,8 @@ import { handleLobbyChatInitialized, openChat } from '../../actions.web';
export interface IProps {
className?: string;
displayName?: string;
isFromVisitor?: boolean;
isLobbyMessage: boolean;
message: string;
participantId: string;
@@ -58,7 +60,7 @@ const useStyles = makeStyles()(theme => {
};
});
const MessageMenu = ({ message, participantId, isLobbyMessage, shouldDisplayChatMessageMenu }: IProps) => {
const MessageMenu = ({ message, participantId, isFromVisitor, isLobbyMessage, shouldDisplayChatMessageMenu, displayName }: IProps) => {
const dispatch = useDispatch();
const { classes, cx } = useStyles();
const { t } = useTranslation();
@@ -82,10 +84,23 @@ const MessageMenu = ({ message, participantId, isLobbyMessage, shouldDisplayChat
if (isLobbyMessage) {
dispatch(handleLobbyChatInitialized(participantId));
} else {
dispatch(openChat(participant));
// For visitor messages, participant will be undefined but we can still open chat
// using the participantId which contains the visitor's original JID
if (isFromVisitor) {
// Handle visitor participant that doesn't exist in main participant list
const visitorParticipant = {
id: participantId,
name: displayName,
isVisitor: true
};
dispatch(openChat(visitorParticipant));
} else {
dispatch(openChat(participant));
}
}
handleClose();
}, [ dispatch, isLobbyMessage, participant, participantId ]);
}, [ dispatch, isLobbyMessage, participant, participantId, displayName ]);
const handleCopyClick = useCallback(() => {
copyText(message)

View File

@@ -48,6 +48,7 @@ const useStyles = makeStyles()(theme => {
const MessageRecipient = ({
_privateMessageRecipient,
_isLobbyChatActive,
_isVisitor,
_lobbyMessageRecipient,
_onRemovePrivateMessageRecipient,
_onHideLobbyChatRecipient,
@@ -80,9 +81,9 @@ const MessageRecipient = ({
id = 'chat-recipient'
role = 'alert'>
<span className = { classes.text }>
{t(_isLobbyChatActive ? 'chat.lobbyChatMessageTo' : 'chat.messageTo', {
recipient: _isLobbyChatActive ? _lobbyMessageRecipient : _privateMessageRecipient
})}
{ _isLobbyChatActive
? t('chat.lobbyChatMessageTo', { recipient: _lobbyMessageRecipient })
: t('chat.messageTo', { recipient: `${_privateMessageRecipient}${_isVisitor ? ` ${t('visitors.chatIndicator')}` : ''}` }) }
</span>
<Button
accessibilityLabel = { t('dialog.close') }

View File

@@ -9,10 +9,12 @@ import { getLocalizedDateFormatter } from '../base/i18n/dateUtil';
import i18next from '../base/i18n/i18next';
import { MEET_FEATURES } from '../base/jwt/constants';
import { isJwtFeatureEnabled } from '../base/jwt/functions';
import { getParticipantById } from '../base/participants/functions';
import { getParticipantById, isPrivateChatEnabled } from '../base/participants/functions';
import { IParticipant } from '../base/participants/types';
import { escapeRegexp } from '../base/util/helpers';
import { getParticipantsPaneWidth } from '../participants-pane/functions';
import { VIDEO_SPACE_MIN_SIZE } from '../video-layout/constants';
import { IVisitorChatParticipant } from '../visitors/types';
import { MESSAGE_TYPE_ERROR, MESSAGE_TYPE_LOCAL, TIMESTAMP_FORMAT } from './constants';
import { IMessage } from './types';
@@ -178,9 +180,24 @@ export function getCanReplyToMessage(state: IReduxState, message: IMessage) {
const { knocking } = state['features/lobby'];
const participant = getParticipantById(state, message.participantId);
return Boolean(participant)
// Check if basic reply conditions are met
const basicCanReply = (Boolean(participant) || message.isFromVisitor)
&& (message.privateMessage || (message.lobbyChat && !knocking))
&& message.messageType !== MESSAGE_TYPE_LOCAL;
if (!basicCanReply) {
return false;
}
// Check private chat configuration for visitor messages
if (message.isFromVisitor) {
const visitorParticipant = { id: message.participantId, name: message.displayName, isVisitor: true as const };
return isPrivateChatEnabled(visitorParticipant, state);
}
// For non-visitor messages, use the regular participant
return isPrivateChatEnabled(participant, state);
}
/**
@@ -190,8 +207,19 @@ export function getCanReplyToMessage(state: IReduxState, message: IMessage) {
* @returns {string}
*/
export function getPrivateNoticeMessage(message: IMessage) {
let recipient;
if (message.messageType === MESSAGE_TYPE_LOCAL) {
// For messages sent by local user, show the recipient name
// For visitor messages, use the visitor's display name with indicator
recipient = message.sentToVisitor ? `${message.recipient} ${i18next.t('visitors.chatIndicator')}` : message.recipient;
} else {
// For messages received from others, show "you"
recipient = i18next.t('chat.you');
}
return i18next.t('chat.privateNotice', {
recipient: message.messageType === MESSAGE_TYPE_LOCAL ? message.recipient : i18next.t('chat.you')
recipient
});
}
@@ -225,3 +253,15 @@ export function getChatMaxSize(state: IReduxState) {
return Math.max(clientWidth - getParticipantsPaneWidth(state) - VIDEO_SPACE_MIN_SIZE, 0);
}
/**
* Type guard to check if a participant is a visitor chat participant.
*
* @param {IParticipant | IVisitorChatParticipant | undefined} participant - The participant to check.
* @returns {boolean} - True if the participant is a visitor chat participant.
*/
export function isVisitorChatParticipant(
participant?: IParticipant | IVisitorChatParticipant
): participant is IVisitorChatParticipant {
return Boolean(participant && 'isVisitor' in participant && participant.isVisitor === true);
}

View File

@@ -33,8 +33,7 @@ import { pushReactions } from '../reactions/actions.any';
import { ENDPOINT_REACTION_NAME } from '../reactions/constants';
import { getReactionMessageFromBuffer, isReactionsEnabled } from '../reactions/functions.any';
import { showToolbox } from '../toolbox/actions';
import './subscriber';
import { getVisitorDisplayName } from '../visitors/functions';
import {
ADD_MESSAGE,
@@ -55,8 +54,9 @@ import {
MESSAGE_TYPE_REMOTE,
MESSAGE_TYPE_SYSTEM
} from './constants';
import { getUnreadCount, isSendGroupChatDisabled } from './functions';
import { getUnreadCount, isSendGroupChatDisabled, isVisitorChatParticipant } from './functions';
import { INCOMING_MSG_SOUND_FILE } from './sounds';
import './subscriber';
/**
* Timeout for when to show the privacy notice after a private message was received.
@@ -186,6 +186,9 @@ MiddlewareRegistry.register(store => next => action => {
if (participant) {
action.participant = participant;
} else if (isVisitorChatParticipant(privateMessageRecipient)) {
// Handle visitor participants that don't exist in the main participant list
action.participant = privateMessageRecipient;
}
}
} else if (focusedTab === ChatTabs.POLLS) {
@@ -204,14 +207,17 @@ MiddlewareRegistry.register(store => next => action => {
// recipient. This logic tries to mitigate this risk.
const shouldSendPrivateMessageTo = _shouldSendPrivateMessageTo(state, action);
const participantExists = shouldSendPrivateMessageTo
&& getParticipantById(state, shouldSendPrivateMessageTo);
if (shouldSendPrivateMessageTo) {
const participantExists = getParticipantById(state, shouldSendPrivateMessageTo.id);
if (shouldSendPrivateMessageTo && participantExists) {
dispatch(openDialog(ChatPrivacyDialog, {
message: action.message,
participantID: shouldSendPrivateMessageTo
}));
if (participantExists || shouldSendPrivateMessageTo.isFromVisitor) {
dispatch(openDialog(ChatPrivacyDialog, {
message: action.message,
participantID: shouldSendPrivateMessageTo.id,
isFromVisitor: shouldSendPrivateMessageTo.isFromVisitor,
displayName: shouldSendPrivateMessageTo.name
}));
}
} else {
// Sending the message if privacy notice doesn't need to be shown.
@@ -227,10 +233,10 @@ MiddlewareRegistry.register(store => next => action => {
type: LOBBY_CHAT_MESSAGE,
message: action.message
}, lobbyMessageRecipient.id);
_persistSentPrivateMessage(store, lobbyMessageRecipient.id, action.message, true);
_persistSentPrivateMessage(store, lobbyMessageRecipient, action.message, true);
} else if (privateMessageRecipient) {
conference.sendPrivateTextMessage(privateMessageRecipient.id, action.message);
_persistSentPrivateMessage(store, privateMessageRecipient.id, action.message);
conference.sendPrivateTextMessage(privateMessageRecipient.id, action.message, 'body', isVisitorChatParticipant(privateMessageRecipient));
_persistSentPrivateMessage(store, privateMessageRecipient, action.message);
} else {
conference.sendTextMessage(action.message);
}
@@ -317,7 +323,7 @@ function _addChatMsgListener(conference: IJitsiConference, store: IStore) {
JitsiConferenceEvents.MESSAGE_RECEIVED,
/* eslint-disable max-params */
(participantId: string, message: string, timestamp: number,
displayName: string, isGuest: boolean, messageId: string) => {
displayName: string, isFromVisitor: boolean, messageId: string) => {
/* eslint-enable max-params */
_onConferenceMessageReceived(store, {
// in case of messages coming from visitors we can have unknown id
@@ -325,7 +331,7 @@ function _addChatMsgListener(conference: IJitsiConference, store: IStore) {
message,
timestamp,
displayName,
isGuest,
isFromVisitor,
messageId,
privateMessage: false });
@@ -350,13 +356,15 @@ function _addChatMsgListener(conference: IJitsiConference, store: IStore) {
conference.on(
JitsiConferenceEvents.PRIVATE_MESSAGE_RECEIVED,
(participantId: string, message: string, timestamp: number, messageId: string) => {
(participantId: string, message: string, timestamp: number, messageId: string, displayName?: string, isFromVisitor?: boolean) => {
_onConferenceMessageReceived(store, {
participantId,
message,
timestamp,
displayName,
messageId,
privateMessage: true
privateMessage: true,
isFromVisitor
});
}
);
@@ -375,8 +383,8 @@ function _addChatMsgListener(conference: IJitsiConference, store: IStore) {
* @returns {void}
*/
function _onConferenceMessageReceived(store: IStore,
{ displayName, isGuest, message, messageId, participantId, privateMessage, timestamp }: {
displayName?: string; isGuest?: boolean; message: string; messageId?: string;
{ displayName, isFromVisitor, message, messageId, participantId, privateMessage, timestamp }: {
displayName?: string; isFromVisitor?: boolean; message: string; messageId?: string;
participantId: string; privateMessage: boolean; timestamp: number; }
) {
@@ -390,7 +398,7 @@ function _onConferenceMessageReceived(store: IStore,
}
_handleReceivedMessage(store, {
displayName,
isGuest,
isFromVisitor,
participantId,
message,
privateMessage,
@@ -505,8 +513,8 @@ function getLobbyChatDisplayName(state: IReduxState, participantId: string) {
* @returns {void}
*/
function _handleReceivedMessage({ dispatch, getState }: IStore,
{ displayName, isGuest, lobbyChat, message, messageId, participantId, privateMessage, timestamp }: {
displayName?: string; isGuest?: boolean; lobbyChat: boolean; message: string;
{ displayName, isFromVisitor, lobbyChat, message, messageId, participantId, privateMessage, timestamp }: {
displayName?: string; isFromVisitor?: boolean; lobbyChat: boolean; message: string;
messageId?: string; participantId: string; privateMessage: boolean; timestamp: number; },
shouldPlaySound = true,
isReaction = false
@@ -525,9 +533,17 @@ function _handleReceivedMessage({ dispatch, getState }: IStore,
const participant = getParticipantById(state, participantId) || { local: undefined };
const localParticipant = getLocalParticipant(getState);
let displayNameToShow = lobbyChat
? getLobbyChatDisplayName(state, participantId)
: displayName || getParticipantDisplayName(state, participantId);
let _displayName, displayNameToShow;
if (lobbyChat) {
displayNameToShow = _displayName = getLobbyChatDisplayName(state, participantId);
} else if (isFromVisitor) {
_displayName = getVisitorDisplayName(state, displayName);
displayNameToShow = `${_displayName} ${i18next.t('visitors.chatIndicator')}`;
} else {
displayNameToShow = _displayName = getParticipantDisplayName(state, participantId);
}
const hasRead = participant.local || isChatOpen;
const timestampToDate = timestamp ? new Date(timestamp) : new Date();
const millisecondsTimestamp = timestampToDate.getTime();
@@ -536,12 +552,8 @@ function _handleReceivedMessage({ dispatch, getState }: IStore,
const shouldShowNotification = userSelectedNotifications?.['notify.chatMessages']
&& !hasRead && !isReaction && (!timestamp || lobbyChat);
if (isGuest) {
displayNameToShow = `${displayNameToShow} ${i18next.t('visitors.chatIndicator')}`;
}
dispatch(addMessage({
displayName: displayNameToShow,
displayName: _displayName,
hasRead,
participantId,
messageType: participant.local ? MESSAGE_TYPE_LOCAL : MESSAGE_TYPE_REMOTE,
@@ -551,7 +563,8 @@ function _handleReceivedMessage({ dispatch, getState }: IStore,
recipient: getParticipantDisplayName(state, localParticipant?.id ?? ''),
timestamp: millisecondsTimestamp,
messageId,
isReaction
isReaction,
isFromVisitor
}));
if (shouldShowNotification) {
@@ -574,6 +587,15 @@ function _handleReceivedMessage({ dispatch, getState }: IStore,
}
}
/**
* Interface for recipient objects used in private messaging.
*/
interface IRecipient {
id: string;
isVisitor?: boolean;
name?: string;
}
/**
* Persists the sent private messages as if they were received over the muc.
*
@@ -582,12 +604,12 @@ function _handleReceivedMessage({ dispatch, getState }: IStore,
* But those messages should be in the store as well, otherwise they don't appear in the chat window.
*
* @param {Store} store - The Redux store.
* @param {string} recipientID - The ID of the recipient the private message was sent to.
* @param {IRecipient} recipient - The recipient the private message was sent to.
* @param {string} message - The sent message.
* @param {boolean} isLobbyPrivateMessage - Is a lobby message.
* @returns {void}
*/
function _persistSentPrivateMessage({ dispatch, getState }: IStore, recipientID: string,
function _persistSentPrivateMessage({ dispatch, getState }: IStore, recipient: IRecipient,
message: string, isLobbyPrivateMessage = false) {
const state = getState();
const localParticipant = getLocalParticipant(state);
@@ -598,6 +620,13 @@ function _persistSentPrivateMessage({ dispatch, getState }: IStore, recipientID:
const displayName = getParticipantDisplayName(state, localParticipant.id);
const { lobbyMessageRecipient } = state['features/chat'];
const recipientName
= recipient.isVisitor
? getVisitorDisplayName(state, recipient.name)
: (isLobbyPrivateMessage
? lobbyMessageRecipient?.name
: getParticipantDisplayName(getState, recipient?.id));
dispatch(addMessage({
displayName,
hasRead: true,
@@ -606,20 +635,19 @@ function _persistSentPrivateMessage({ dispatch, getState }: IStore, recipientID:
message,
privateMessage: !isLobbyPrivateMessage,
lobbyChat: isLobbyPrivateMessage,
recipient: isLobbyPrivateMessage
? lobbyMessageRecipient?.name
: getParticipantDisplayName(getState, recipientID),
recipient: recipientName,
sentToVisitor: recipient.isVisitor,
timestamp: Date.now()
}));
}
/**
* Returns the ID of the participant who we may have wanted to send the message
* Returns the participant info for who we may have wanted to send the message
* that we're about to send.
*
* @param {Object} state - The Redux state.
* @param {Object} action - The action being dispatched now.
* @returns {string?}
* @returns {IRecipient?} - The recipient info or undefined if no notice should be shown.
*/
function _shouldSendPrivateMessageTo(state: IReduxState, action: AnyAction) {
if (action.ignorePrivacy) {
@@ -651,7 +679,11 @@ function _shouldSendPrivateMessageTo(state: IReduxState, action: AnyAction) {
if (lastMessage.privateMessage) {
// We show the notice if the last received message was private.
return lastMessage.participantId;
return {
id: lastMessage.participantId,
isFromVisitor: Boolean(lastMessage.isFromVisitor),
name: lastMessage.displayName
};
}
// But messages may come rapidly, we want to protect our users from mis-sending a message
@@ -666,7 +698,11 @@ function _shouldSendPrivateMessageTo(state: IReduxState, action: AnyAction) {
? recentPrivateMessages[0] : recentPrivateMessages[recentPrivateMessages.length - 1];
if (recentPrivateMessage) {
return recentPrivateMessage.participantId;
return {
id: recentPrivateMessage.participantId,
isFromVisitor: Boolean(recentPrivateMessage.isFromVisitor),
name: recentPrivateMessage.displayName
};
}
return undefined;

View File

@@ -1,6 +1,7 @@
import { UPDATE_CONFERENCE_METADATA } from '../base/conference/actionTypes';
import { ILocalParticipant, IParticipant } from '../base/participants/types';
import ReducerRegistry from '../base/redux/ReducerRegistry';
import { IVisitorChatParticipant } from '../visitors/types';
import {
ADD_MESSAGE,
@@ -51,7 +52,7 @@ export interface IChatState {
} | ILocalParticipant;
messages: IMessage[];
nbUnreadMessages: number;
privateMessageRecipient?: IParticipant;
privateMessageRecipient?: IParticipant | IVisitorChatParticipant;
width: {
current: number;
userSet: number | null;
@@ -64,6 +65,7 @@ ReducerRegistry.register<IChatState>('features/chat', (state = DEFAULT_STATE, ac
const newMessage: IMessage = {
displayName: action.displayName,
error: action.error,
isFromVisitor: Boolean(action.isFromVisitor),
participantId: action.participantId,
isReaction: action.isReaction,
messageId: action.messageId,
@@ -73,6 +75,7 @@ ReducerRegistry.register<IChatState>('features/chat', (state = DEFAULT_STATE, ac
privateMessage: action.privateMessage,
lobbyChat: action.lobbyChat,
recipient: action.recipient,
sentToVisitor: Boolean(action.sentToVisitor),
timestamp: action.timestamp
};

View File

@@ -5,6 +5,7 @@ import { IStore } from '../app/types';
export interface IMessage {
displayName: string;
error?: Object;
isFromVisitor?: boolean;
isReaction: boolean;
lobbyChat: boolean;
message: string;
@@ -14,6 +15,7 @@ export interface IMessage {
privateMessage: boolean;
reactions: Map<string, Set<string>>;
recipient: string;
sentToVisitor?: boolean;
timestamp: number;
}

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';
import { _openDesktopApp } from './openDesktopApp.web';
/**
* 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';
import { openDesktopApp } from './functions.web';
/**
* Implements the middleware of the deep linking feature.

View File

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

View File

@@ -234,6 +234,7 @@ const FileSharing = () => {
const handleFileSelect = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
if (e.target.files) {
processFiles(e.target.files as FileList, store);
e.target.value = ''; // Reset the input value to allow re-uploading the same file
}
}, [ processFiles ]);

View File

@@ -10,6 +10,7 @@ import { MEET_FEATURES } from '../base/jwt/constants';
import { isJwtFeatureEnabled } from '../base/jwt/functions';
import { showErrorNotification } from '../notifications/actions';
import { NOTIFICATION_TIMEOUT_TYPE, NOTIFICATION_TYPE } from '../notifications/constants';
import { iAmVisitor } from '../visitors/functions';
import { uploadFiles } from './actions';
import { MAX_FILE_SIZE } from './constants';
@@ -150,5 +151,7 @@ export const processFiles = (fileList: FileList | File[], store: IStore) => {
* @returns {boolean} Indication of whether local user can upload files.
*/
export function isFileUploadingEnabled(state: IReduxState): boolean {
return isJwtFeatureEnabled(state, MEET_FEATURES.FILE_UPLOAD, false) && isFileSharingEnabled(state);
return !iAmVisitor(state)
&& isJwtFeatureEnabled(state, MEET_FEATURES.FILE_UPLOAD, false)
&& isFileSharingEnabled(state);
}

View File

@@ -14,6 +14,7 @@ import { addFile, removeFile, updateFileProgress } from './actions';
import { getFileExtension } from './functions.any';
import logger from './logger';
import { IFileMetadata } from './types';
import { downloadFile } from './utils';
/**
@@ -122,14 +123,14 @@ MiddlewareRegistry.register(store => next => action => {
}
}))
.then((response: any) => response.json())
.then((data: { presignedUrl: any; }) => {
const url = data.presignedUrl;
.then((data: { fileName: string; presignedUrl: string; }) => {
const { presignedUrl, fileName } = data;
if (!url) {
if (!presignedUrl) {
throw new Error('No presigned URL found in the response.');
}
window.open(url, '_blank', 'noreferrer,noopener');
return downloadFile(presignedUrl, fileName);
})
.catch((error: any) => {
logger.warn('Could not download file:', error);

View File

@@ -0,0 +1,27 @@
const generateDownloadUrl = async (url: string) => {
const resp = await fetch(url);
const respBlob = await resp.blob();
const blob = new Blob([ respBlob ]);
return URL.createObjectURL(blob);
};
export const downloadFile = async (url: string, fileName: string) => {
const dowloadUrl = await generateDownloadUrl(url);
const link = document.createElement('a');
if (fileName) {
link.download = fileName;
}
link.href = dowloadUrl;
document.body.appendChild(link);
link.click();
link.remove();
// fix for certain browsers
setTimeout(() => {
URL.revokeObjectURL(dowloadUrl);
}, 0);
};

View File

@@ -179,6 +179,9 @@ function styles(theme: Theme, props: IProps) {
'&.top-panel-filmstrip': {
flexDirection: 'column' as const
},
'&.always-show-resize-bar': {
backgroundColor: BACKGROUND_COLOR
}
},
@@ -245,6 +248,10 @@ function styles(theme: Theme, props: IProps) {
* The type of the React {@code Component} props of {@link Filmstrip}.
*/
export interface IProps extends WithTranslation {
/**
* Whether to always show the resize bar on filmstrip. This will make the filmstrip always visible.
*/
_alwaysShowResizeBar?: boolean;
/**
* Additional CSS class names top add to the root.
@@ -533,6 +540,7 @@ class Filmstrip extends PureComponent <IProps, IState> {
override render() {
const filmstripStyle: any = { };
const {
_alwaysShowResizeBar,
_currentLayout,
_disableSelfView,
_filmstripDisabled,
@@ -649,11 +657,12 @@ class Filmstrip extends PureComponent <IProps, IState> {
{_resizableFilmstrip
? <div
className = { clsx('resizable-filmstrip', classes.resizableFilmstripContainer,
_topPanelFilmstrip && 'top-panel-filmstrip') }>
_topPanelFilmstrip && 'top-panel-filmstrip',
_alwaysShowResizeBar && 'always-show-resize-bar') }>
<div
className = { clsx('dragHandleContainer',
classes.dragHandleContainer,
isMouseDown && 'visible',
(isMouseDown || _alwaysShowResizeBar) && 'visible',
_topPanelFilmstrip && 'top-panel')
}
onMouseDown = { this._onDragHandleMouseDown }>
@@ -1081,7 +1090,7 @@ class Filmstrip extends PureComponent <IProps, IState> {
function _mapStateToProps(state: IReduxState, ownProps: any) {
const { _hasScroll = false, filmstripType, _topPanelFilmstrip, _remoteParticipants } = ownProps;
const { toolbarButtons } = state['features/toolbox'];
const { iAmRecorder } = state['features/base/config'];
const { iAmRecorder, filmstrip: { alwaysShowResizeBar } = {} } = state['features/base/config'];
const { topPanelHeight, topPanelVisible, visible, width: verticalFilmstripWidth } = state['features/filmstrip'];
const { localScreenShare } = state['features/base/participants'];
const reduceHeight = state['features/toolbox'].visible && toolbarButtons?.length;
@@ -1134,7 +1143,8 @@ function _mapStateToProps(state: IReduxState, ownProps: any) {
_topPanelVisible,
_verticalFilmstripWidth: verticalFilmstripWidth.current,
_verticalViewMaxWidth: getVerticalViewMaxWidth(state),
_videosClassName: videosClassName
_videosClassName: videosClassName,
_alwaysShowResizeBar: alwaysShowResizeBar
};
}

View File

@@ -81,13 +81,13 @@ export function shouldRemoteVideosBeVisible(state: IReduxState) {
// in the filmstrip.
const participantCount = getParticipantCountWithFake(state);
let pinnedParticipant;
const { disable1On1Mode } = state['features/base/config'];
const { disable1On1Mode, filmstrip: { alwaysShowResizeBar } = {} } = state['features/base/config'];
const { contextMenuOpened } = state['features/base/responsive-ui'];
return Boolean(
contextMenuOpened
|| participantCount > 2
|| alwaysShowResizeBar
// Always show the filmstrip when there is another participant to
// show and the local video is pinned, or the toolbar is displayed.
|| (participantCount > 1

View File

@@ -1,4 +1,4 @@
import KeepAwake from 'react-native-keep-awake';
import { activateKeepAwake, deactivateKeepAwake } from '@sayem314/react-native-keep-awake';
import { getCurrentConference } from '../../base/conference/functions';
import StateListenerRegistry from '../../base/redux/StateListenerRegistry';
@@ -28,8 +28,8 @@ StateListenerRegistry.register(
*/
function _setWakeLock(wakeLock: boolean) {
if (wakeLock) {
KeepAwake.activate();
activateKeepAwake();
} else {
KeepAwake.deactivate();
deactivateKeepAwake();
}
}

View File

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

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