Compare commits

..

70 Commits

Author SHA1 Message Date
damencho
11a6e94541 debug: longer waits 2. 2025-08-19 16:59:38 -05:00
damencho
e1e262fb68 debug: longer wait. 2025-08-19 16:31:03 -05:00
damencho
b0a96b32d2 fix(jiconop): Fixes loading it under different virtual hosts. 2025-08-19 15:59:34 -05:00
bgrozev
dac9b5e244 test: Check for send/receive independently. (#16356)
This allows the logs to show which one definitely failed.
2025-08-19 15:31:52 -05:00
damencho
d15cfd845a fix(config): Drops legacy config prejoinPageEnabled. 2025-08-19 08:41:04 -05:00
bgrozev
91e4ac1665 ref: Extract test configuration code to TestsConfig.ts (#16329)
* ref: Move iFrameUsesJaas to TestsConfig.

* ref: Move room name prefix/suffix to config.

* ref: Move JaaS configuration to TestsConfig.

* ref: Move iframe config to TestsConfig.

* ref: Move webproxy config to TestsConfig.

* ref: Move JWT config to TestsConfig.

* doc: Document some of the IContext fields.

* Add a debug config option.
2025-08-18 13:32:41 -05:00
damencho
fda42e5230 fix: More fixes sending metadata to jicofo.
f1a0012 was not enough to address the issue.
2025-08-18 11:24:07 -05:00
Calin-Teodor
142d4441c1 feat(chat): add tooltip for each chat screen tab 2025-08-18 16:33:15 +03:00
Mihaela Dumitru
5814c4dda7 fix(dynamic-branding) expand background color option to support a wider range of configurations (#16334) 2025-08-18 11:38:09 +03:00
Jaya Allamsetty
8c1dc03363 chore(deps) lib-jitsi-meet@latest
https://github.com/jitsi/lib-jitsi-meet/compare/v2048.0.0+4d9a138b...v2051.0.0+ccc06e83
2025-08-14 10:49:20 -04:00
Jaya Allamsetty
ff6fc198f1 chore(deps) lib-jitsi-meet@latest
https://github.com/jitsi/lib-jitsi-meet/compare/v2044.0.0+437abe32...v2048.0.0+4d9a138b
2025-08-13 20:22:14 -04:00
damencho
f1a0012fc1 fix: Fixes sending metadata to jicofo.
In cases like waiting-for-host lobby, jicofo can leave the room and rejoin later, without the room being destroyed. We need to make sure the metadata will reach jicofo on second attempt.
2025-08-11 16:32:25 +03:00
Дамян Минков
85522aea25 feat(tests): Updates join logic. (#16320)
* fix: Fix example file.

* fix: Fix using webhook proxy with iframe and jaas tests.

* fix: Fixes detecting max users notification.

* ref: Clear _joinParticipant to not depend on participant names.

* ref: Use joinParticipant in jaas tests.

* ref: Drops JAAS_DOMAIN as BASE_URL is used.

* fix: Drops ctx from function parameters.

* ref: Drops not needed context members.

* ref: Drops extra function.

* ref: Participant.joinConference to use roomName from options.

* doc: Updates docs.

* fix: Adds roomName to joinOptions.

Make it possible to override the generated value.
2025-08-11 06:52:16 -05:00
Saúl Ibarra Corretgé
000c370c64 fix(prejoin) no initial tracks when using URL override to disable it
It's still possible to disable it, but when not in an iframe, audio and
video tracks will not be created.

When in an iframe, it's ok to let it happen, since the host sit is the
one where permissions need to be granted, thanks to permission
delegation.

Fixes: https://github.com/jitsi/jitsi-meet/issues/16262
Ref: https://zimzi.substack.com/p/jitsi-privacy-flaw-that-enables-one
2025-08-08 23:06:01 +02:00
Mihaela Dumitru
a762d585b8 fix(accessibility) return focus to share file button after upload modal closes (#16312) 2025-08-08 19:34:10 +03:00
Mihaela Dumitru
ded8f22363 fix(accessibility) add ARIA attributes to file upload progress bar (#16311) 2025-08-08 19:33:24 +03:00
Mihaela Dumitru
c3e1c9d568 fix(accessibility) show upload successful notification (#16309) 2025-08-08 17:15:03 +03:00
Mihaela Dumitru
8901132af9 fix(accessibility) announce error and warning notifications immediately (#16307) 2025-08-08 17:14:35 +03:00
Mihaela Dumitru
71f92f6e17 fix(accessibility) improve notification action button accessibility (#16306) 2025-08-08 17:14:17 +03:00
Mihaela Dumitru
76166df81a fix(accessibility) remove nested button structure in file sharing drop zone (#16304) 2025-08-08 17:13:06 +03:00
Mihaela Dumitru
eb2ba39289 fix(accessibility) use semantic list for uploaded files (#16317) 2025-08-08 17:12:34 +03:00
bgrozev
048b089acd ref: Refactor tests (#16315)
* ref: Move iframe tests to iframe/.

* ref: Pass iFrameApi as Participant option.

* ref: Extract IParticipantJoinOptions.

* ref: Remove displayName from IJoinOptions (unused).

* ref: Move preferGenerateToken out of Participant.
2025-08-08 01:58:44 -05:00
Jaya Allamsetty
b774f18f80 chore(deps) lib-jitsi-meet@latest
https://github.com/jitsi/lib-jitsi-meet/compare/v2036.0.0+b6142c70...v2044.0.0+437abe32
2025-08-07 19:29:39 -04:00
Zaid0412
dbe4e6a784 feat: disable Giphy analytics to prevent beforeunload handlers (#16314) 2025-08-07 16:34:07 +02:00
raduanastase8x8
d2e52d2c2a ref(Theme): Changes typography values to rem (#16021)
Replaces hard-coded pixel values with relative rem units across UI components to improve typography responsiveness and maintainability.

Co-authored-by: Hristo Terezov <hristo@jitsi.org>
2025-08-06 19:07:27 -05:00
bgrozev
b5ad984dab ref: Allow tests to specify the browsers they use in TestProperties. (#16313)
* ref: Allow tests to specify the browsers they use in TestProperties.
2025-08-06 08:47:50 -05:00
Mihaela Dumitru
81ce664ad7 fix(i18n) improve label (#16301) 2025-08-06 12:19:03 +03:00
bgrozev
181ef92e1f Add a test for jaas passcode, refactor tests. (#16303)
* ref: Don't use global context for local state.

* ref: Don't use global context to store the pin.

* feat: Add a test for setting passcode via settings provisioning.

* Use local state.

* Remove "data" from context.

* ref: Rename a function.

* test: Fail quick when join muc fails, assert specific errors (e.g. "token expired").
2025-08-06 04:00:59 -05:00
Horatiu Muresan
79dbc2d1ee feat(chat-web) add chat recipient picker (#16298) 2025-08-05 10:06:04 +03:00
Jaya Allamsetty
f56ce78b9d chore(deps) lib-jitsi-meet@latest
https://github.com/jitsi/lib-jitsi-meet/compare/v2033.0.0+bf3e3a8e...v2036.0.0+b6142c70
2025-08-04 16:29:32 -04:00
Дамян Минков
8269b88796 feat(prosody): Adds docs for added room fields. (#16299)
* feat(prosody): Adds docs for added room fields.

* squash: Drop comment.
2025-08-04 14:56:21 -05:00
bgrozev
252ef4604a test: Add JaaS-specific tests: join MUC, visitors, maxOccupants. (#16270)
* test: Add tests for joining a JaaS MUC with different token options.
* ref: Refactor token generation and usage
* ref: Reduce usage of global context 
* test: Add a maxOccupants jaas test.
2025-08-04 04:28:38 -05:00
Hristo Terezov
fc816aa149 fix(ChatMessage): context menu position
Before the chat message context menu was appearing on the left if the private chat message was disabled. The fix makes the context menu appear on the left only for messages from the local partcipant which are the only messages rendered to the right (therefore the context menu have to appear on the left side). For all other messages the context menu should appear on the right side because the message is positioned on the left side.
2025-08-02 10:19:16 -05:00
Hristo Terezov
6de18fe82d fix(participants-pane): restore scrolling and fix context menu clipping
The participant pane lost its scrolling capability when commit 2305ae85a removed the overflowY: 'auto' property from the container styles. This prevented users from scrolling through long lists of participants, breakout rooms, or visitors when the content exceeded the available height.

Additionally, context menus were being clipped on the left side due to the overflow constraints. This became apparent after the av-moderation feature added longer menu items like "Stop screen-sharing for everyone else".

Fix:
- Restore overflowY: 'auto' to enable vertical scrolling
- Add maxWidth constraint (285px) to context menus to prevent horizontal clipping
- Allow menu text to wrap to multiple lines instead of being cut off
- Add TODO comment for future portal-based implementation

This temporary solution provides both functional scrolling and fully readable context menus until a proper architectural change can be implemented to portal context menus outside the scrollable container.
2025-08-01 09:48:06 -05:00
Hristo Terezov
5b7e3bb2d7 doc(config): disablePrivateChat visitor value 2025-07-31 14:39:17 -05:00
Mihaela Dumitru
bc08b38791 fix(config) revise option description 2025-07-31 14:47:58 +03:00
Edgars Voroboks
6613f630d7 fix(lang): Update Latvian language translation 2025-07-31 10:14:14 +03:00
Calinteodor
719b6d68c8 chore(android): 16 kb page size alignment (#16276)
* Most libraries are aligned, only duktape needs to be replaced.
2025-07-30 15:52:40 +03:00
val11n1
6a62c5120f fix(rn) fix iOS rendering when launched locked 2025-07-28 23:59:26 +02:00
Oğuzhan Selim Temiz
64270f3015 fix(react-native-sdk): resolve Android build configuration issues
- Move namespace declaration to correct location in build.gradle
- Remove deprecated package attribute from AndroidManifest.xml
- Update README with gradle plugin version requirement
- Fix Android namespace configuration for React Native SDK

These changes resolve installation and build errors when integrating
the Jitsi Meet React Native SDK into new projects.

Fixes: SDK installation failures on Android with newer Gradle versions
2025-07-28 10:55:55 +03:00
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
259 changed files with 4190 additions and 1727 deletions

View File

@@ -28,6 +28,14 @@ android {
ndk {
abiFilters 'armeabi-v7a', 'arm64-v8a', 'x86', 'x86_64'
}
externalNativeBuild {
cmake {
arguments "-DANDROID_SUPPORT_FLEXIBLE_PAGE_SIZES=ON", "-DANDROID_STL=c++_shared"
cppFlags "-std=c++17"
cFlags "-DANDROID_PLATFORM=android-26"
}
}
}
signingConfigs {
@@ -46,8 +54,8 @@ android {
applicationIdSuffix ".debug"
}
release {
// Uncomment the following line for singing a test release build.
//signingConfig signingConfigs.debug
// Uncomment the following line for signing a test release build.
// signingConfig signingConfigs.debug
minifyEnabled true
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules-release.pro'
buildConfigField "boolean", "GOOGLE_SERVICES_ENABLED", "${googleServicesEnabled}"
@@ -102,8 +110,6 @@ dependencies {
gradle.projectsEvaluated {
// Dropbox integration
//
def dropboxAppKey
if (project.file('dropbox.key').exists()) {
dropboxAppKey = project.file('dropbox.key').text.trim() - 'db-'
@@ -174,7 +180,6 @@ gradle.projectsEvaluated {
packageTask.dependsOn(currentRunPackagerTask)
}
}
if (googleServicesEnabled) {

View File

@@ -6,6 +6,5 @@
-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

@@ -96,8 +96,3 @@
# Rule to avoid build errors related to SVGs.
-keep public class com.horcrux.svg.** {*;}
# https://github.com/facebook/fresco/issues/2638
-keep public class com.facebook.imageutils.** {
public *;
}

View File

@@ -7,11 +7,11 @@ import org.gradle.util.VersionNumber
buildscript {
ext {
kotlinVersion = "2.0.21"
gradlePluginVersion = "8.4.2"
buildToolsVersion = "34.0.0"
compileSdkVersion = 34
gradlePluginVersion = "8.6.0"
buildToolsVersion = "35.0.0"
compileSdkVersion = 35
minSdkVersion = 26
targetSdkVersion = 34
targetSdkVersion = 35
supportLibVersion = "28.0.0"
ndkVersion = "27.1.12297006"
@@ -75,26 +75,6 @@ allprojects {
}
}
// Due to a dependency conflict between React Native and the Fresco library used by GiphySDK,
// GIFs appear as static images instead of animating
// https://github.com/Giphy/giphy-react-native-sdk/commit/7fe466ed6fddfaec95f9cbc959d33bd75ad8f900
configurations.configureEach {
resolutionStrategy {
forcedModules = [
'com.facebook.fresco:fresco:3.2.0',
'com.facebook.fresco:animated-gif:3.2.0',
'com.facebook.fresco:animated-base:3.2.0',
'com.facebook.fresco:animated-drawable:3.2.0',
'com.facebook.fresco:animated-webp:3.2.0',
'com.facebook.fresco:webpsupport:3.2.0',
'com.facebook.fresco:imagepipeline-okhttp3:3.2.0',
'com.facebook.fresco:middleware:3.2.0',
'com.facebook.fresco:nativeimagetranscoder:3.2.0'
]
}
}
// Third-party react-native modules which Jitsi Meet SDK for Android depends
// on and which are not available in third-party Maven repositories need to
// be deployed in a Maven repository of ours.

View File

@@ -0,0 +1,113 @@
#!/bin/bash
progname="${0##*/}"
progname="${progname%.sh}"
# usage: check_elf_alignment.sh [path to *.so files|path to *.apk]
cleanup_trap() {
if [ -n "${tmp}" -a -d "${tmp}" ]; then
rm -rf ${tmp}
fi
exit $1
}
usage() {
echo "Host side script to check the ELF alignment of shared libraries."
echo "Shared libraries are reported ALIGNED when their ELF regions are"
echo "16 KB or 64 KB aligned. Otherwise they are reported as UNALIGNED."
echo
echo "Usage: ${progname} [input-path|input-APK|input-APEX]"
}
if [ ${#} -ne 1 ]; then
usage
exit
fi
case ${1} in
--help | -h | -\?)
usage
exit
;;
*)
dir="${1}"
;;
esac
if ! [ -f "${dir}" -o -d "${dir}" ]; then
echo "Invalid file: ${dir}" >&2
exit 1
fi
if [[ "${dir}" == *.apk ]]; then
trap 'cleanup_trap' EXIT
echo
echo "Recursively analyzing $dir"
echo
if { zipalign --help 2>&1 | grep -q "\-P <pagesize_kb>"; }; then
echo "=== APK zip-alignment ==="
zipalign -v -c -P 16 4 "${dir}" | egrep 'lib/arm64-v8a|lib/x86_64|Verification'
echo "========================="
else
echo "NOTICE: Zip alignment check requires build-tools version 35.0.0-rc3 or higher."
echo " You can install the latest build-tools by running the below command"
echo " and updating your \$PATH:"
echo
echo " sdkmanager \"build-tools;35.0.0-rc3\""
fi
dir_filename=$(basename "${dir}")
tmp=$(mktemp -d -t "${dir_filename%.apk}_out_XXXXX")
unzip "${dir}" lib/* -d "${tmp}" >/dev/null 2>&1
dir="${tmp}"
fi
if [[ "${dir}" == *.apex ]]; then
trap 'cleanup_trap' EXIT
echo
echo "Recursively analyzing $dir"
echo
dir_filename=$(basename "${dir}")
tmp=$(mktemp -d -t "${dir_filename%.apex}_out_XXXXX")
deapexer extract "${dir}" "${tmp}" || { echo "Failed to deapex." && exit 1; }
dir="${tmp}"
fi
RED="\e[31m"
GREEN="\e[32m"
ENDCOLOR="\e[0m"
unaligned_libs=()
echo
echo "=== ELF alignment ==="
matches="$(find "${dir}" -type f)"
IFS=$'\n'
for match in $matches; do
# We could recursively call this script or rewrite it to though.
[[ "${match}" == *".apk" ]] && echo "WARNING: doesn't recursively inspect .apk file: ${match}"
[[ "${match}" == *".apex" ]] && echo "WARNING: doesn't recursively inspect .apex file: ${match}"
[[ $(file "${match}") == *"ELF"* ]] || continue
res="$(objdump -p "${match}" | grep LOAD | awk '{ print $NF }' | head -1)"
if [[ $res =~ 2\*\*(1[4-9]|[2-9][0-9]|[1-9][0-9]{2,}) ]]; then
echo -e "${match}: ${GREEN}ALIGNED${ENDCOLOR} ($res)"
else
echo -e "${match}: ${RED}UNALIGNED${ENDCOLOR} ($res)"
unaligned_libs+=("${match}")
fi
done
if [ ${#unaligned_libs[@]} -gt 0 ]; then
echo -e "${RED}Found ${#unaligned_libs[@]} unaligned libs (only arm64-v8a/x86_64 libs need to be aligned).${ENDCOLOR}"
elif [ -n "${dir_filename}" ]; then
echo -e "ELF Verification Successful"
fi
echo "====================="

View File

@@ -44,12 +44,12 @@ dependencies {
api "com.facebook.react:react-android:$rootProject.ext.rnVersion"
api "com.facebook.react:hermes-android:$rootProject.ext.rnVersion"
implementation 'com.facebook.fresco:animated-gif:2.5.0'
implementation 'com.dropbox.core:dropbox-core-sdk:4.0.1'
implementation 'com.jakewharton.timber:timber:5.0.1'
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) {

View File

@@ -49,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

@@ -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;
@@ -207,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;
}
@@ -231,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

@@ -50,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';
@@ -1663,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

@@ -761,7 +761,7 @@ var config = {
// hideDominantSpeakerBadge: false,
// Default language for the user interface. Cannot be overwritten.
// DEPRECATED! Use the `lang` iframe option directly instead.
// For iframe integrations, use the `lang` option directly instead.
// defaultLanguage: 'en',
// Disables profile and the edit of all fields from the profile settings (display name and email)
@@ -791,7 +791,6 @@ var config = {
// Configs for prejoin page.
// prejoinConfig: {
// // When 'true', it shows an intermediate page before joining, where the user can configure their devices.
// // This replaces `prejoinPageEnabled`. Defaults to true.
// enabled: true,
// // Hides the participant name editing field in the prejoin screen.
// // If requireDisplayName is also set as true, a name should still be provided through
@@ -1363,7 +1362,9 @@ var config = {
// disableGrantModerator: true,
// // If set to 'all' the 'Private chat' button will be disabled for all participants.
// // If set to 'allow-moderator-chat' the 'Private chat' button will be available for chats with moderators.
// disablePrivateChat: 'all' | 'allow-moderator-chat',
// // If set to 'disable-visitor-chat' the 'Private chat' button will be disabled for visitor-main participant
// // conversations.
// disablePrivateChat: 'all' | 'allow-moderator-chat' | 'disable-visitor-chat',
// },

View File

@@ -90,7 +90,7 @@ $welcomePageHeaderContainerMargin: $welcomePageHeaderContainerMarginTop auto 0;
$welcomePageHeaderTextTitleMarginBottom: 0;
$welcomePageHeaderTextTitleFontSize: 2.625rem;
$welcomePageHeaderTextTitleFontWeight: normal;
$welcomePageHeaderTextTitleLineHeight: 50px;
$welcomePageHeaderTextTitleLineHeight: 3.125rem;
$welcomePageHeaderTextTitleOpacity: 1;
$welcomePageEnterRoomDisplay: flex;

16
giphy-analytics-stub.js Normal file
View File

@@ -0,0 +1,16 @@
// Stub replacement for @giphy/js-analytics to prevent beforeunload handlers
// This completely disables all Giphy analytics functionality
export const pingback = () => {
// Completely disabled - do nothing
};
export const mergeAttributes = (attributes, newAttributes) => {
// Return merged attributes without any analytics calls
return { ...attributes,
...newAttributes };
};
// Ensure no beforeunload handlers are ever registered
export default pingback;

View File

@@ -64,11 +64,11 @@ PODS:
- GoogleUtilities/UserDefaults (~> 7.7)
- PromisesObjC (< 3.0, >= 1.2)
- fmt (11.0.2)
- Giphy (2.2.13):
- Giphy (2.2.16):
- libwebp
- giphy-react-native-sdk (3.3.1):
- giphy-react-native-sdk (4.1.0):
- DoubleConversion
- Giphy (= 2.2.13)
- Giphy (= 2.2.16)
- glog
- hermes-engine
- RCT-Folly (= 2024.11.18.00)
@@ -1382,9 +1382,9 @@ PODS:
- React-Core
- react-native-netinfo (11.1.0):
- React-Core
- react-native-orientation-locker (1.6.0):
- react-native-orientation-locker (1.5.0):
- React-Core
- react-native-pager-view (6.4.1):
- react-native-pager-view (6.8.1):
- DoubleConversion
- glog
- hermes-engine
@@ -1407,7 +1407,7 @@ PODS:
- Yoga
- react-native-performance (5.1.2):
- React-Core
- react-native-safe-area-context (5.4.0):
- react-native-safe-area-context (5.5.2):
- React-Core
- react-native-slider (4.5.6):
- DoubleConversion
@@ -1794,7 +1794,7 @@ PODS:
- React-Core
- RNDefaultPreference (1.4.4):
- React-Core
- RNDeviceInfo (10.9.0):
- RNDeviceInfo (12.1.0):
- React-Core
- RNGestureHandler (2.24.0):
- DoubleConversion
@@ -2201,8 +2201,8 @@ SPEC CHECKSUMS:
FirebaseCrashlytics: feb07e4e9187be3c23c6a846cce4824e5ce2dd0b
FirebaseInstallations: 40bd9054049b2eae9a2c38ef1c3dd213df3605cd
fmt: a40bb5bd0294ea969aaaba240a927bd33d878cdd
Giphy: a11dd02b51ac2ec37b881de1717ebf2cc8e9df62
giphy-react-native-sdk: 678a115ea5a47a43d39d1b61703e0d08b1e48917
Giphy: 55914215541027873875757f350530e6d8986fba
giphy-react-native-sdk: 733177b2537b527cfa55979c396cc1f2046eb457
glog: eb93e2f488219332457c3c4eafd2738ddc7e80b8
GoogleAppMeasurement: 4c19f031220c72464d460c9daa1fb5d1acce958e
GoogleDataTransport: 6c09b596d841063d76d4288cc2d2f42cc36e1e2a
@@ -2249,10 +2249,10 @@ SPEC CHECKSUMS:
react-native-get-random-values: d16467cf726c618e9c7a8c3c39c31faa2244bbba
react-native-keep-awake: 03b74eebe4f2bb5e8478fc8f420651a92463b6f8
react-native-netinfo: 5364263f903da576bdef9c84a76fe243ab06812c
react-native-orientation-locker: ee8bb2177365ca74f51dc1e11218fe544634d523
react-native-pager-view: 68e8a65a607a6f91a1e25865002192c3c4f53fcf
react-native-orientation-locker: dbd3f6ddbe9e62389cb0807dc2af63f6c36dec36
react-native-pager-view: 11662c698c8f11d39e05891316d2a144fa00adc4
react-native-performance: 125a96c145e29918b55b45ce25cbba54f1e24dcd
react-native-safe-area-context: 9d72abf6d8473da73033b597090a80b709c0b2f1
react-native-safe-area-context: 0f7bf11598f9a61b7ceac8dc3f59ef98697e99e1
react-native-slider: 1205801a8d29b28cacc14eef08cb120015cdafcb
react-native-video: eb861d67a71dfef1bbf6086a811af5f338b13781
react-native-webrtc: 2261a482150195092246fe70b3aff976f2e11ec5
@@ -2290,7 +2290,7 @@ SPEC CHECKSUMS:
RNCAsyncStorage: aa75595c1aefa18f868452091fa0c411a516ce11
RNCClipboard: 7c3e3b5f71d84ef61690ad377b6c50cf27864ff5
RNDefaultPreference: ee13d69e6693d193cd223d10e15e5b3c012d31ba
RNDeviceInfo: 8af23685571b7867d8dc15fb89e7fb5fa8607e1e
RNDeviceInfo: 723e97dd98af9b7913477e7a40252c15517c258c
RNGestureHandler: 9f3109e11ed88fe5bed280bf7762b25e4c52f396
RNGoogleSignin: 30e1aee80140dc0706cd78a4951c411376c88329
RNScreens: 9ef996b6041d0960a4794a845f7d0808b171b4ef

View File

@@ -30,6 +30,14 @@
<string>35F9.1</string>
</array>
</dict>
<dict>
<key>NSPrivacyAccessedAPIType</key>
<string>NSPrivacyAccessedAPICategoryDiskSpace</string>
<key>NSPrivacyAccessedAPITypeReasons</key>
<array>
<string>85F4.1</string>
</array>
</dict>
</array>
<key>NSPrivacyCollectedDataTypes</key>
<array/>

View File

@@ -109,6 +109,7 @@
}
},
"chat": {
"disabled": "Tērzēšanas ziņojumu sūtīšana ir atspējota.",
"enter": "Ienākt istabā",
"error": "Kļūda: Jūsu ziņa netika nosūtīta. Cēlonis: {{error}}",
"fieldPlaceHolder": "Rakstiet ziņu šeit",
@@ -124,7 +125,8 @@
"title": "Ierakstiet vārdu, lai izmantotu tērzēšanā",
"titleWithCC": "Ievadiet segvārdu, lai izmantotu tērzēšanā un slēptos subtitros",
"titleWithPolls": "Ierakstiet segvārdu, lai izmantotu tērzēšanā un aptaujās",
"titleWithPollsAndCC": "Ievadiet segvārdu, lai izmantotu tērzēšanā, aptaujās un slēptos subtitros"
"titleWithPollsAndCC": "Ievadiet segvārdu, lai izmantotu tērzēšanā, aptaujās un slēptos subtitros",
"titleWithPollsAndCCAndFileSharing": "Ievadiet segvārdu, lai izmantotu tērzēšanā, aptaujās, slēptos subtitros un failos"
},
"noMessagesMessage": "Sapulcē pagaidām nav nevienas ziņas. Uzsāciet saraksti!",
"privateNotice": "Privāta ziņa adresātam {{recipient}}",
@@ -134,12 +136,14 @@
"tabs": {
"chat": "Tērzēšana",
"closedCaptions": "Slēptie subtitri",
"fileSharing": "Faili",
"polls": "Aptaujas"
},
"title": "Tērzēšana",
"titleWithCC": "Tērzēšana un slēptie subtitri",
"titleWithCC": "Tērzēšana un Slēptie subtitri",
"titleWithFeatures": "Tērzēšana un",
"titleWithFileSharing": "Faili",
"titleWithPolls": "Tērzēšana un Aptaujas",
"titleWithPollsAndCC": "Tērzēšana, Aptaujas un Slēptie subtitri",
"you": "jūs"
},
"chromeExtensionBanner": {
@@ -296,6 +300,12 @@
"alreadySharedVideoTitle": "Atļauts tikai viens kopīgots videoklips",
"applicationWindow": "Lietotnes logs",
"authenticationRequired": "Nepieciešama autentifikācija",
"cameraCaptureDialog": {
"description": "Uzņemt un nosūtīt attēlu, izmantojot mobilā tālruņa kameru",
"ok": "Atvērt kameru",
"reject": "Ne tagad",
"title": "Uzņemt attēlu"
},
"cameraConstraintFailedError": "Kamera neatbilst noteiktajām prasībām.",
"cameraNotFoundError": "Kamera nav atrasta.",
"cameraNotSendingData": "Nevar piekļūt jūsu kamerai. Lūdzu, pārbaudiet, vai šo ierīci neizmanto cita programma, iestatījumu izvēlnē atlasiet citu ierīci vai mēģiniet atkārtoti ielādēt programmu.",
@@ -371,22 +381,34 @@
"micTimeoutError": "Nevarēja palaist audio avotu. Iestājās noildze!",
"micUnknownError": "Nevar izmantot mikrofonu nezināma iemesla dēļ.",
"moderationAudioLabel": "Atļaut dalībniekiem ieslēgt savu mikrofonu",
"moderationDesktopLabel": "Atļaut lietotājiem, kas nav moderatori, kopīgot savu ekrānu",
"moderationVideoLabel": "Atļaut dalībniekiem ieslēgt savu kameru",
"muteEveryoneDialog": "Dalībnieki paši var ieslēgt savu mikrofonu.",
"muteEveryoneDialogModerationOn": "Dalībnieki var nosūtīt pieprasījumu ieslēgt savu mikrofonu.",
"muteEveryoneElseDialog": "Kad skaņa būs izslēgta, jūs nevarēsiet to ieslēgt atpakaļ, taču dalībnieki to varēs izdarīt paši.",
"muteEveryoneElseTitle": "Vai izslēgt skaņu visiem, izņemot {{whom}}?",
"muteEveryoneElsesDesktopDialog": "Kad kopīgošana būs apturēta, jūs vairs nevarēsiet to ieslēgt atpakaļ, bet viņi to varēs izdarīt jebkurā laikā.",
"muteEveryoneElsesDesktopTitle": "Apturēt ekrāna kopīgošanu visiem, izņemot {{kam}}?",
"muteEveryoneElsesVideoDialog": "Kad video būs izslēgts, jūs nevarēsiet to ieslēgt atpakaļ, taču dalībnieki to varēs izdarīt paši.",
"muteEveryoneElsesVideoTitle": "Vai izslēgt video visiem, izņemot {{whom}}?",
"muteEveryoneSelf": "jūs",
"muteEveryoneStartMuted": "No šī brīža visi jauni dalībnieki pieslēdzas ar izslēgt skaņu",
"muteEveryoneTitle": "Vai izslēgt skaņu visiem?",
"muteEveryonesDesktopDialog": "Dalībnieki var kopīgot savu ekrānu jebkurā laikā.",
"muteEveryonesDesktopDialogModerationOn": "Dalībnieki jebkurā laikā var nosūtīt pieprasījumu kopīgot savu ekrānu.",
"muteEveryonesDesktopTitle": "Vai pārtraukt ekrāna kopīgošanu visiem?",
"muteEveryonesVideoDialog": "Dalībnieki var ieslēgt savu video.",
"muteEveryonesVideoDialogModerationOn": "Dalībnieki var nosūtīt pieprasījumu ieslēgt viņu video.",
"muteEveryonesVideoDialogOk": "Atspējot",
"muteEveryonesVideoTitle": "Vai apturēt ikviena video?",
"muteParticipantBody": "Jūs nevariet viņiem ieslēgt skaņu, bet viņi paši to var izdarīt jebkurā laikā.",
"muteParticipantButton": "Izslēgt skaņu",
"muteParticipantsDesktopBody": "Jūs nevarēsiet sākt viņu ekrāna kopīgošanu, bet viņi to varēs izdarīt jebkurā laikā.",
"muteParticipantsDesktopBodyModerationOn": "Jūs nevarēsiet sākt viņu ekrāna kopīgošanu, un arī viņi to nevarēs izdarīt.",
"muteParticipantsDesktopButton": "Pārtraukt ekrāna kopīgošanu",
"muteParticipantsDesktopDialog": "Vai tiešām vēlaties izslēgt šī dalībnieka ekrāna kopīgošanu? Jūs vairs nevarēsiet ieslēgt to atpakaļ, bet viņš to varēs izdarīt jebkurā laikā.",
"muteParticipantsDesktopDialogModerationOn": "Vai tiešām vēlaties izslēgt šī dalībnieka ekrāna kopīgošanu? Jūs vairs nevarēsiet ieslēgt to atpakaļ, un arī viņš to nevarēs izdarīt.",
"muteParticipantsDesktopTitle": "Atspējot ekrāna kopīgošanu šim dalībniekam?",
"muteParticipantsVideoBody": "Jūs nevarēsiet kameru ieslēgt atpakaļ, taču viņi paši to varēs izdarīt jebkurā laikā.",
"muteParticipantsVideoBodyModerationOn": "Ne Jūs, ne dalībnieki nevarēsiet ieslēgt kameru atpakaļ.",
"muteParticipantsVideoButton": "Pārtraukt video",
@@ -539,6 +561,19 @@
"veryBad": "Ļoti Slikta",
"veryGood": "Ļoti Laba"
},
"fileSharing": {
"downloadFailedDescription": "Lūdzu, mēģiniet vēlreiz.",
"downloadFailedTitle": "Lejuplādes kļūda",
"downloadFile": "Lejuplādēt",
"dragAndDrop": "Velciet un palaidiet failus šeit, vai jebkurā ekrāna vietā",
"fileAlreadyUploaded": "Fails jau ir augšupielādēts šajā sanāksmē.",
"fileTooLargeDescription": "Lūdzu, pārliecinieties, vai faila lielums nepārsniedz {{ maxFileSize }}.",
"fileTooLargeTitle": "Izvēlētais fails ir pārāk liels",
"removeFile": "Noņemt",
"uploadFailedDescription": "Lūdzu, mēģiniet vēlreiz.",
"uploadFailedTitle": "Augšuplādes kļūda",
"uploadFile": "Kopīgot failu"
},
"filmstrip": {
"accessibilityLabel": {
"heading": "Video sīktēli"
@@ -750,8 +785,9 @@
"me": "es",
"notify": {
"OldElectronAPPTitle": "Drošības ievainojamība!",
"allowAll": "Atļaut visu",
"allowAudio": "Atļaut audio",
"allowBoth": "Abus",
"allowDesktop": "Atļaut ekrāna kopīgošanu",
"allowVideo": "Atļaut video",
"allowedUnmute": "Varat ieslēgt mikrofona skaņu, ieslēgt kameru vai kopīgot ekrānu.",
"audioUnmuteBlockedDescription": "Mikrofona ieslēgšanas darbība ir īslaicīgi bloķēta sistēmas ierobežojumu dēļ.",
@@ -765,6 +801,7 @@
"dataChannelClosedDescription": "Savienojuma kanāls nedarbojas, tāpēc video kvalitāte var būt ierobežota līdz zemākajam iestatījumam.",
"dataChannelClosedDescriptionWithAudio": "Savienojuma kanāls nedarbojas, tāpēc var rasties audio un video traucējumi.",
"dataChannelClosedWithAudio": "Audio un video kvalitāte var būt traucēta",
"desktopMutedRemotelyTitle": "",
"disabledIframe": "Iegulšana ir paredzēta tikai demonstrācijas nolūkiem, tāpēc šis zvans tiks atvienots pēc {{timeout}} minūtēm.",
"disabledIframeSecondaryNative": "Domēna {{domain}} iegulšana ir paredzēta tikai demonstrācijas nolūkiem, tāpēc šis zvans tiks pārtraukts pēc {{timeout}} minūtēm.",
"disabledIframeSecondaryWeb": "Domēna {{domain}} iegulšana ir paredzēta tikai demonstrācijas nolūkiem, tāpēc šis zvans tiks pārtraukts pēc {{timeout}} minūtēm. Lūdzu, produkcijas videi izmantojiet <a href='{{jaasDomain}}' rel='noopener noreferrer' target='_blank'>Jitsi as a Service</a>!",
@@ -845,6 +882,7 @@
"suggestRecordingDescription": "Vai vēlaties sākt ierakstīšanu?",
"suggestRecordingTitle": "Ierakstīt sanāksmi",
"unmute": "Ieslēgt mikrofonu",
"unmuteScreen": "",
"unmuteVideo": "Ieslēgt video",
"videoMutedRemotelyDescription": "Jūs vienmēr varat to atkal ieslēgt.",
"videoMutedRemotelyTitle": "{{participantDisplayName}} izslēdza jūsu video",
@@ -864,11 +902,14 @@
"admit": "Apstiprināt",
"admitAll": "Apstiprināt visus",
"allow": "Atļaut dalībniekiem:",
"allowDesktop": "Atļaut ekrāna kopīgošanu",
"allowVideo": "Atļaut video",
"askDesktop": "Lūgt kopīgot ekrānu",
"askUnmute": "Lūgt ieslēgt skaņu",
"audioModeration": "Ieslēgt savu skaņu",
"blockEveryoneMicCamera": "Bloķēt visiem mikrofonu un kameru",
"breakoutRooms": "Grupu istabas",
"desktopModeration": "Sākt ekrāna kopīgošanu",
"goLive": "Sākt",
"invite": "Uzaicināt",
"lowerAllHands": "Nolaist visas paceltās rokas",
@@ -880,6 +921,8 @@
"muteAll": "Apklusināt visus",
"muteEveryoneElse": "Apklusināt pārējos",
"reject": "Noraidīt",
"stopDesktop": "Pārtraukt ekrāna kopīgošanu",
"stopEveryonesDesktop": "Pārtraukt visiem ekrāna kopīgošanu",
"stopEveryonesVideo": "Izslēgt visiem video",
"stopVideo": "Izslēgt video",
"unblockEveryoneMicCamera": "Atbloķēt visiem mikrofonu un kameru",
@@ -889,9 +932,11 @@
"headings": {
"lobby": "Vestibils ({{count}})",
"participantsList": "Sapulces dalībnieki ({{count}})",
"viewerRequests": "Apmeklētāju pieprasījumi {{count}}",
"visitorInQueue": " (gaida {{count}})",
"visitorRequests": " (pieprasījumi {{count}})",
"visitors": "Apmeklētāji ({{count}})",
"visitors": "Apmeklētāji {{count}}",
"visitorsList": "Apmeklētāji ({{count}})",
"waitingLobby": "Gaida vestibilā ({{count}})"
},
"search": "Meklēt dalībniekus",
@@ -1484,6 +1529,8 @@
"connectionInfo": "Informācija par savienojumu",
"demote": "Pārveidot par apmeklētāju",
"domute": "Izlsēgt skaņu",
"domuteDesktop": "Pārtraukt ekrāna kopīgošanu",
"domuteDesktopOfOthers": "Pārtraukt ekrāna kopīgošanu visiem pārējiem",
"domuteOthers": "Izslēgt skaņu visiem pārējiem",
"domuteVideo": "Izslēgt kameru",
"domuteVideoOfOthers": "Izslēgt video visiem pārējiem",

View File

@@ -112,6 +112,7 @@
"disabled": "Sending chat messages is disabled.",
"enter": "Enter room",
"error": "Error: your message was not sent. Reason: {{error}}",
"everyone": "Everyone",
"fieldPlaceHolder": "Aa",
"lobbyChatMessageTo": "Lobby chat message to {{recipient}}",
"message": "Message",
@@ -300,6 +301,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.",
@@ -374,7 +381,8 @@
"micPermissionDeniedError": "You have not granted permission to use your microphone. You can still join the conference but others won't hear you. Use the camera button in the address bar to fix this.",
"micTimeoutError": "Could not start audio source. Timeout occurred!",
"micUnknownError": "Cannot use microphone for an unknown reason.",
"moderationAudioLabel": "Allow attendees to unmute themselves",
"moderationAudioLabel": "Allow non-moderators 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.",
@@ -387,6 +395,9 @@
"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",
@@ -559,6 +570,8 @@
"fileAlreadyUploaded": "File has already been uploaded to this meeting.",
"fileTooLargeDescription": "Please make sure the file does not exceed {{ maxFileSize }}.",
"fileTooLargeTitle": "The selected file is too large",
"fileUploadProgress": "File upload progress",
"fileUploadedSuccessfully": "File uploaded successfully",
"removeFile": "Remove",
"uploadFailedDescription": "Please try again.",
"uploadFailedTitle": "Upload failed",

View File

@@ -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';
@@ -113,7 +114,10 @@ import { RECORDING_METADATA_ID, RECORDING_TYPES } from '../../react/features/rec
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.
*

574
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -23,7 +23,7 @@
"@emotion/styled": "11.10.6",
"@giphy/js-fetch-api": "4.9.3",
"@giphy/react-components": "6.9.4",
"@giphy/react-native-sdk": "3.3.1",
"@giphy/react-native-sdk": "4.1.0",
"@jitsi/excalidraw": "https://github.com/jitsi/excalidraw/releases/download/v0.0.19/jitsi-excalidraw-0.0.19.tgz",
"@jitsi/js-utils": "2.2.1",
"@jitsi/logger": "2.0.2",
@@ -59,6 +59,7 @@
"dayjs": "1.11.13",
"dropbox": "10.7.0",
"focus-visible": "5.1.0",
"glob": "11.0.3",
"grapheme-splitter": "1.0.4",
"i18n-iso-countries": "6.8.0",
"i18next": "17.0.6",
@@ -70,7 +71,7 @@
"js-md5": "0.6.1",
"js-sha512": "0.8.0",
"jwt-decode": "2.2.0",
"lib-jitsi-meet": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v2030.0.0+b225c920/lib-jitsi-meet.tgz",
"lib-jitsi-meet": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v2051.0.0+ccc06e83/lib-jitsi-meet.tgz",
"lodash-es": "4.17.21",
"null-loader": "4.0.1",
"optional-require": "1.0.3",
@@ -87,16 +88,16 @@
"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-device-info": "12.1.0",
"react-native-dialog": "https://github.com/jitsi/react-native-dialog/releases/download/v9.2.2-jitsi.1/react-native-dialog-9.2.2.tgz",
"react-native-gesture-handler": "2.24.0",
"react-native-get-random-values": "1.11.0",
"react-native-immersive-mode": "https://github.com/jitsi/react-native-immersive-mode.git#38cc9001db24618bc0c61800f81e889bcfb6ff2c",
"react-native-orientation-locker": "1.6.0",
"react-native-pager-view": "6.4.1",
"react-native-orientation-locker": "https://github.com/jitsi/react-native-orientation-locker.git#fe095651d819cf134624f786b61fc8667862178a",
"react-native-pager-view": "6.8.1",
"react-native-paper": "5.10.3",
"react-native-performance": "5.1.2",
"react-native-safe-area-context": "5.4.0",
"react-native-safe-area-context": "5.5.2",
"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",
@@ -111,7 +112,7 @@
"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-virtualized-auto-sizer": "1.0.26",
"react-window": "1.8.6",
"react-youtube": "10.1.0",
"redux": "4.0.4",

View File

@@ -1,14 +0,0 @@
diff --git a/node_modules/@giphy/js-analytics/dist/send-pingback.js b/node_modules/@giphy/js-analytics/dist/send-pingback.js
index 989f0ff..52471cb 100644
--- a/node_modules/@giphy/js-analytics/dist/send-pingback.js
+++ b/node_modules/@giphy/js-analytics/dist/send-pingback.js
@@ -10,6 +10,9 @@ var global_1 = __importDefault(require("./global"));
var environment = (global_1.default === null || global_1.default === void 0 ? void 0 : global_1.default.GIPHY_PINGBACK_URL) || 'https://pingback.giphy.com';
var pingBackUrl = "".concat(environment, "/v2/pingback?apikey=l0HlIwPWyBBUDAUgM");
var sendPingback = function (events) {
+ // Disabled.
+ return Promise.resolve();
+
var headers = (0, js_util_1.getGiphySDKRequestHeaders)();
/* istanbul ignore next */
headers === null || headers === void 0 ? void 0 : headers.set('Content-Type', 'application/json');

View File

@@ -70,6 +70,7 @@ cd ios && pod install && cd ..
## Android
- In your build.gradle have at least `minSdkVersion = 26`
- In your build.gradle have `gradlePluginVersion = "8.4.2"` or higher
- In `android/app/src/debug/AndroidManifest.xml` and `android/app/src/main/AndroidManifest.xml`, under the `</application>` tag, include
```xml
<uses-permission android:name="android.permission.RECORD_AUDIO" />

View File

@@ -7,7 +7,6 @@ buildscript {
dependencies {
classpath "com.android.tools.build:gradle:$rootProject.ext.gradlePluginVersion"
}
namespace 'org.jitsi.meet.reactnativesdk'
}
def isNewArchitectureEnabled() {
@@ -29,6 +28,7 @@ def getExtOrIntegerDefault(name) {
}
android {
namespace 'org.jitsi.meet.sdk'
compileSdkVersion getExtOrIntegerDefault('compileSdkVersion')
defaultConfig {

View File

@@ -1,4 +1,3 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="org.jitsi.meet.sdk">
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
</manifest>

View File

@@ -35,3 +35,12 @@ export function fixDeviceID(amplitude: Types.BrowserClient) {
}
}
}
/**
* Returns the amplitude shared deviceId.
*
* @returns {string} - The amplitude deviceId.
*/
export function getDeviceID() {
return jitsiLocalStorage.getItem(DEVICE_ID_KEY);
}

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

@@ -2,7 +2,7 @@ import React, { useCallback } from 'react';
import { makeStyles } from 'tss-react/mui';
import Icon from '../../../icons/components/Icon';
import { withPixelLineHeight } from '../../../styles/functions.web';
import { pixelsToRem } from '../../../ui/functions.any';
import { isIcon } from '../../functions';
import { IAvatarProps } from '../../types';
import { PRESENCE_AVAILABLE_COLOR, PRESENCE_AWAY_COLOR, PRESENCE_BUSY_COLOR, PRESENCE_IDLE_COLOR } from '../styles';
@@ -50,9 +50,8 @@ const useStyles = makeStyles()(theme => {
avatar: {
backgroundColor: '#AAA',
borderRadius: '50%',
fontWeight: '600',
color: theme.palette?.text01 || '#fff',
...withPixelLineHeight(theme.typography?.heading1 ?? {}),
...(theme.typography?.heading1 ?? {}),
fontSize: 'inherit',
objectFit: 'cover',
textAlign: 'center',
@@ -137,7 +136,7 @@ const StatelessAvatar = ({
const _getAvatarStyle = (backgroundColor?: string) => {
return {
background: backgroundColor || undefined,
fontSize: size ? size * 0.4 : '180%',
fontSize: size ? pixelsToRem(size * 0.4) : '180%',
height: size || '100%',
width: size || '100%'
};

View File

@@ -4,13 +4,12 @@ import { makeStyles } from 'tss-react/mui';
import Icon from '../icons/components/Icon';
import { IconCheck, IconCopy } from '../icons/svg';
import { withPixelLineHeight } from '../styles/functions.web';
import { copyText } from '../util/copyText.web';
const useStyles = makeStyles()(theme => {
return {
copyButton: {
...withPixelLineHeight(theme.typography.bodyShortBold),
...theme.typography.bodyShortBold,
borderRadius: theme.shape.borderRadius,
display: 'flex',
justifyContent: 'flex-start',

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

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

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

@@ -521,7 +521,6 @@ export interface IConfig {
preCallTestEnabled?: boolean;
preCallTestICEUrl?: string;
};
prejoinPageEnabled?: boolean;
raisedHands?: {
disableLowerHandByModerator?: boolean;
disableLowerHandNotification?: boolean;
@@ -553,7 +552,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;

View File

@@ -202,7 +202,6 @@ export default [
'prejoinConfig.enabled',
'prejoinConfig.hideDisplayName',
'prejoinConfig.hideExtraJoinButtons',
'prejoinPageEnabled',
'raisedHands',
'recordingService',
'requireDisplayName',

View File

@@ -7,6 +7,7 @@ import { isEmpty, mergeWith, pick } from 'lodash-es';
import { IReduxState } from '../../app/types';
import { getLocalParticipant } from '../participants/functions';
import { isEmbedded } from '../util/embedUtils';
import { parseURLParams } from '../util/parseURLParams';
import { IConfig } from './configType';
@@ -335,7 +336,7 @@ export function setConfigFromURLParams(
overrideConfigJSON(config, interfaceConfig, json);
// Print warning about depricated URL params
// Print warning about deprecated URL params
if ('interfaceConfig.SUPPORT_URL' in params) {
logger.warn('Using SUPPORT_URL interfaceConfig URL overwrite is deprecated.'
+ ' Please use supportUrl from advanced branding!');
@@ -371,6 +372,13 @@ export function setConfigFromURLParams(
logger.warn('Using liveStreaming config URL overwrite and/or LIVE_STREAMING_HELP_LINK interfaceConfig URL'
+ ' overwrite is deprecated. Please use liveStreaming from advanced branding!');
}
// When not in an iframe, start without media if the pre-join page is not enabled.
if (!isEmbedded()
&& 'config.prejoinConfig.enabled' in params && config.prejoinConfig?.enabled === false) {
logger.warn('Using prejoinConfig.enabled config URL overwrite implies starting without media.');
config.disableInitialGUM = true;
}
}
/* eslint-enable max-params */

View File

@@ -405,13 +405,6 @@ function _translateLegacyConfig(oldValue: IConfig) {
newValue.welcomePage.disabled = !oldValue.enableWelcomePage;
}
newValue.prejoinConfig = oldValue.prejoinConfig || {};
if (oldValue.hasOwnProperty('prejoinPageEnabled')
&& !newValue.prejoinConfig.hasOwnProperty('enabled')
) {
newValue.prejoinConfig.enabled = oldValue.prejoinPageEnabled;
}
newValue.disabledSounds = newValue.disabledSounds || [];
if (oldValue.disableJoinLeaveSounds) {

View File

@@ -2,7 +2,6 @@ import React, { useCallback } from 'react';
import { makeStyles } from 'tss-react/mui';
import Icon from '../../../icons/components/Icon';
import { withPixelLineHeight } from '../../../styles/functions.web';
import { COLORS } from '../../constants';
interface IProps {
@@ -55,7 +54,7 @@ interface IProps {
const useStyles = makeStyles()(theme => {
return {
label: {
...withPixelLineHeight(theme.typography.labelRegular),
...theme.typography.labelRegular,
alignItems: 'center',
background: theme.palette.ui04,
borderRadius: '4px',

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

@@ -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';
@@ -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,6 @@ import { makeStyles } from 'tss-react/mui';
import Icon from '../../../icons/components/Icon';
import { IconArrowDown } from '../../../icons/svg';
import { withPixelLineHeight } from '../../../styles/functions.web';
interface IProps {
@@ -82,7 +81,7 @@ interface IProps {
const useStyles = makeStyles()(theme => {
return {
actionButton: {
...withPixelLineHeight(theme.typography.bodyLongBold),
...theme.typography.bodyLongBold,
borderRadius: theme.shape.borderRadius,
boxSizing: 'border-box',
color: theme.palette.text01,
@@ -115,7 +114,7 @@ const useStyles = makeStyles()(theme => {
'&.text': {
width: 'auto',
fontSize: '13px',
fontSize: '0.875rem',
margin: '0',
padding: '0'
},
@@ -135,7 +134,7 @@ const useStyles = makeStyles()(theme => {
[theme.breakpoints.down(400)]: {
fontSize: 16,
fontSize: '1rem',
marginBottom: 8,
padding: '11px 16px'
}

View File

@@ -5,7 +5,6 @@ import { makeStyles } from 'tss-react/mui';
import Icon from '../../../icons/components/Icon';
import { IconArrowDown, IconCloseCircle, IconWifi1Bar, IconWifi2Bars, IconWifi3Bars } from '../../../icons/svg';
import { withPixelLineHeight } from '../../../styles/functions.web';
import { PREJOIN_DEFAULT_CONTENT_WIDTH } from '../../../ui/components/variables';
import Spinner from '../../../ui/components/web/Spinner';
import { runPreCallTest } from '../../actions.web';
@@ -16,7 +15,7 @@ const useStyles = makeStyles()(theme => {
return {
connectionStatus: {
color: '#fff',
...withPixelLineHeight(theme.typography.bodyShortRegular),
...theme.typography.bodyShortRegular,
position: 'absolute',
width: '100%',

View File

@@ -10,7 +10,6 @@ import Toolbox from '../../../../toolbox/components/web/Toolbox';
import { isButtonEnabled } from '../../../../toolbox/functions.web';
import { getConferenceName } from '../../../conference/functions';
import { PREMEETING_BUTTONS, THIRD_PARTY_PREJOIN_BUTTONS } from '../../../config/constants';
import { withPixelLineHeight } from '../../../styles/functions.web';
import Tooltip from '../../../tooltip/components/Tooltip';
import { isPreCallTestEnabled } from '../../functions';
@@ -152,7 +151,7 @@ const useStyles = makeStyles()(theme => {
width: '100%'
},
title: {
...withPixelLineHeight(theme.typography.heading4),
...theme.typography.heading4,
color: `${theme.palette.text01}!important`,
marginBottom: theme.spacing(3),
textAlign: 'center',
@@ -168,7 +167,7 @@ const useStyles = makeStyles()(theme => {
},
roomName: {
...withPixelLineHeight(theme.typography.heading5),
...theme.typography.heading5,
color: theme.palette.text01,
display: 'inline-block',
overflow: 'hidden',

View File

@@ -2,8 +2,6 @@ import React from 'react';
import { useTranslation } from 'react-i18next';
import { makeStyles } from 'tss-react/mui';
import { withPixelLineHeight } from '../../../styles/functions.web';
const useStyles = makeStyles()(theme => {
return {
warning: {
@@ -11,7 +9,7 @@ const useStyles = makeStyles()(theme => {
color: theme.palette.text03,
display: 'flex',
justifyContent: 'center',
...withPixelLineHeight(theme.typography.bodyShortRegular),
...theme.typography.bodyShortRegular,
marginBottom: theme.spacing(3),
marginTop: theme.spacing(2),
paddingLeft: theme.spacing(3),

View File

@@ -4,7 +4,6 @@ import { useDispatch, useSelector } from 'react-redux';
import { makeStyles } from 'tss-react/mui';
import { IReduxState } from '../../../../app/types';
import { withPixelLineHeight } from '../../../styles/functions.web';
import Checkbox from '../../../ui/components/web/Checkbox';
import getUnsafeRoomText from '../../../util/getUnsafeRoomText.web';
import { setUnsafeRoomConsent } from '../../actions.web';
@@ -14,7 +13,7 @@ const useStyles = makeStyles()(theme => {
warning: {
backgroundColor: theme.palette.warning01,
color: theme.palette.text04,
...withPixelLineHeight(theme.typography.bodyShortRegular),
...theme.typography.bodyShortRegular,
padding: theme.spacing(3),
borderRadius: theme.shape.borderRadius,
marginBottom: theme.spacing(3)

View File

@@ -6,6 +6,7 @@ import { translate } from '../../../i18n/functions';
import Icon from '../../../icons/components/Icon';
import Tooltip from '../../../tooltip/components/Tooltip';
import { TOOLTIP_POSITION } from '../../../ui/constants.any';
import { pixelsToRem } from '../../../ui/functions.any';
/**
* The type of the React {@code Component} props of {@link BaseIndicator}.
@@ -40,7 +41,7 @@ interface IProps extends WithTranslation {
/**
* The font size for the icon.
*/
iconSize: string | number;
iconSize: number;
/**
* The ID attribute to set on the root element of the component.
@@ -88,10 +89,10 @@ const BaseIndicator = ({
tooltipPosition = 'top'
}: IProps) => {
const { classes: styles } = useStyles();
const style: { fontSize?: string | number; } = {};
const style: { fontSize?: string; } = {};
if (iconSize) {
style.fontSize = iconSize;
style.fontSize = pixelsToRem(iconSize);
}
return (

View File

@@ -3,7 +3,6 @@ import { useTranslation } from 'react-i18next';
import { useSelector } from 'react-redux';
import { makeStyles } from 'tss-react/mui';
import { withPixelLineHeight } from '../../../styles/functions.web';
import Button from '../../../ui/components/web/Button';
import { getSupportUrl } from '../../functions';
@@ -15,7 +14,7 @@ const useStyles = makeStyles()(theme => {
borderRadius: `${Number(theme.shape.borderRadius)}px`,
boxShadow: '0px 1px 2px rgba(41, 41, 41, 0.25)',
color: theme.palette.text01,
...withPixelLineHeight(theme.typography.bodyShortRegular),
...theme.typography.bodyShortRegular,
padding: `${theme.spacing(3)} 10`,
'& .retry-button': {
margin: '16px auto 0 auto'

View File

@@ -22,17 +22,3 @@ export function getFixedPlatformStyle(style?: StyleType | StyleType[]) {
return style;
}
/**
* Sets the line height of a CSS Object group in pixels.
* By default lineHeight is unitless in CSS, but not in RN.
*
* @param {Object} base - The base object containing the `lineHeight` property.
* @returns {Object}
*/
export function withPixelLineHeight(base: any) {
return {
...base,
lineHeight: `${base.lineHeight}px`
};
}

View File

@@ -6,7 +6,6 @@ import { makeStyles } from 'tss-react/mui';
import { IReduxState } from '../../../app/types';
import { isMobileBrowser } from '../../environment/utils';
import Popover from '../../popover/components/Popover.web';
import { withPixelLineHeight } from '../../styles/functions.web';
import { TOOLTIP_POSITION } from '../../ui/constants.any';
import { hideTooltip, showTooltip } from '../actions';
@@ -26,7 +25,7 @@ const useStyles = makeStyles()(theme => {
backgroundColor: theme.palette.uiBackground,
borderRadius: '3px',
padding: theme.spacing(2),
...withPixelLineHeight(theme.typography.labelRegular),
...theme.typography.labelRegular,
color: theme.palette.text01,
position: 'relative',

View File

@@ -118,8 +118,8 @@ export const colorMap = {
export const font = {
weightRegular: '400',
weightSemiBold: '600'
weightRegular: 400,
weightSemiBold: 600
};
export const shape = {
@@ -136,64 +136,64 @@ export const typography = {
labelBold: 'labelBold01',
bodyShortRegularSmall: {
fontSize: 10,
lineHeight: 16,
fontSize: '0.625rem',
lineHeight: '1rem',
fontWeight: font.weightRegular,
letterSpacing: 0
},
bodyShortRegular: {
fontSize: 14,
lineHeight: 20,
fontSize: '0.875rem',
lineHeight: '1.25rem',
fontWeight: font.weightRegular,
letterSpacing: 0
},
bodyShortBold: {
fontSize: 14,
lineHeight: 20,
fontSize: '0.875rem',
lineHeight: '1.25rem',
fontWeight: font.weightSemiBold,
letterSpacing: 0
},
bodyShortRegularLarge: {
fontSize: 16,
lineHeight: 22,
fontSize: '1rem',
lineHeight: '1.375rem',
fontWeight: font.weightRegular,
letterSpacing: 0
},
bodyShortBoldLarge: {
fontSize: 16,
lineHeight: 22,
fontSize: '1rem',
lineHeight: '1.375rem',
fontWeight: font.weightSemiBold,
letterSpacing: 0
},
bodyLongRegular: {
fontSize: 14,
lineHeight: 24,
fontSize: '0.875rem',
lineHeight: '1.5rem',
fontWeight: font.weightRegular,
letterSpacing: 0
},
bodyLongRegularLarge: {
fontSize: 16,
lineHeight: 26,
fontSize: '1rem',
lineHeight: '1.625rem',
fontWeight: font.weightRegular,
letterSpacing: 0
},
bodyLongBold: {
fontSize: 14,
lineHeight: 24,
fontSize: '0.875rem',
lineHeight: '1.5rem',
fontWeight: font.weightSemiBold,
letterSpacing: 0
},
bodyLongBoldLarge: {
fontSize: 16,
lineHeight: 26,
fontSize: '1rem',
lineHeight: '1.625rem',
fontWeight: font.weightSemiBold,
letterSpacing: 0
},
@@ -203,29 +203,29 @@ export const typography = {
heading2: 'heading02',
heading3: {
fontSize: 32,
lineHeight: 40,
fontSize: '2rem',
lineHeight: '2.5rem',
fontWeight: font.weightSemiBold,
letterSpacing: 0
},
heading4: {
fontSize: 28,
lineHeight: 36,
fontSize: '1.75rem',
lineHeight: '2.25rem',
fontWeight: font.weightSemiBold,
letterSpacing: 0
},
heading5: {
fontSize: 20,
lineHeight: 28,
fontSize: '1.25rem',
lineHeight: '1.75rem',
fontWeight: font.weightSemiBold,
letterSpacing: 0
},
heading6: {
fontSize: 16,
lineHeight: 26,
fontSize: '1rem',
lineHeight: '1.625rem',
fontWeight: font.weightSemiBold,
letterSpacing: 0
}

View File

@@ -4,7 +4,6 @@ import { useTranslation } from 'react-i18next';
import { keyframes } from 'tss-react';
import { makeStyles } from 'tss-react/mui';
import { withPixelLineHeight } from '../../../styles/functions.web';
import { isElementInTheViewport } from '../../functions.web';
import { DialogTransitionContext } from './DialogTransition';
@@ -16,7 +15,7 @@ const useStyles = makeStyles()(theme => {
height: '100%',
position: 'fixed',
color: theme.palette.text01,
...withPixelLineHeight(theme.typography.bodyLongRegular),
...theme.typography.bodyLongRegular,
top: 0,
left: 0,
display: 'flex',

View File

@@ -3,7 +3,6 @@ import { useTranslation } from 'react-i18next';
import { makeStyles } from 'tss-react/mui';
import Icon from '../../../icons/components/Icon';
import { withPixelLineHeight } from '../../../styles/functions.web';
import { BUTTON_TYPES } from '../../constants.web';
import { IButtonProps } from '../types';
@@ -57,7 +56,7 @@ const useStyles = makeStyles()(theme => {
alignItems: 'center',
justifyContent: 'center',
border: 0,
...withPixelLineHeight(theme.typography.bodyShortBold),
...theme.typography.bodyShortBold,
transition: 'background .2s',
cursor: 'pointer',
@@ -151,7 +150,7 @@ const useStyles = makeStyles()(theme => {
small: {
padding: '8px 16px',
...withPixelLineHeight(theme.typography.labelBold),
...theme.typography.labelBold,
'&.iconButton': {
padding: theme.spacing(1)
@@ -162,7 +161,7 @@ const useStyles = makeStyles()(theme => {
large: {
padding: '13px 16px',
...withPixelLineHeight(theme.typography.bodyShortBoldLarge),
...theme.typography.bodyShortBoldLarge,
'&.iconButton': {
padding: '12px'

View File

@@ -4,7 +4,6 @@ import { makeStyles } from 'tss-react/mui';
import { isMobileBrowser } from '../../../environment/utils';
import Icon from '../../../icons/components/Icon';
import { IconCheck } from '../../../icons/svg';
import { withPixelLineHeight } from '../../../styles/functions.web';
interface ICheckboxProps {
@@ -42,13 +41,13 @@ interface ICheckboxProps {
const useStyles = makeStyles()(theme => {
return {
formControl: {
...withPixelLineHeight(theme.typography.bodyLongRegular),
...theme.typography.bodyLongRegular,
color: theme.palette.text01,
display: 'inline-flex',
alignItems: 'center',
'&.is-mobile': {
...withPixelLineHeight(theme.typography.bodyLongRegularLarge)
...theme.typography.bodyLongRegularLarge
}
},

View File

@@ -8,7 +8,6 @@ import Drawer from '../../../../toolbox/components/web/Drawer';
import JitsiPortal from '../../../../toolbox/components/web/JitsiPortal';
import { showOverflowDrawer } from '../../../../toolbox/functions.web';
import participantsPaneTheme from '../../../components/themes/participantsPaneTheme.json';
import { withPixelLineHeight } from '../../../styles/functions.web';
import { spacing } from '../../Tokens';
@@ -139,7 +138,7 @@ const useStyles = makeStyles()(theme => {
borderRadius: `${Number(theme.shape.borderRadius)}px`,
boxShadow: '0px 1px 2px rgba(41, 41, 41, 0.25)',
color: theme.palette.text01,
...withPixelLineHeight(theme.typography.bodyShortRegular),
...theme.typography.bodyShortRegular,
marginTop: '48px',
position: 'absolute',
right: `${participantsPaneTheme.panePadding}px`,
@@ -159,7 +158,7 @@ const useStyles = makeStyles()(theme => {
paddingTop: '16px',
'& > div': {
...withPixelLineHeight(theme.typography.bodyShortRegularLarge),
...theme.typography.bodyShortRegularLarge,
'& svg': {
fill: theme.palette.icon01

View File

@@ -4,7 +4,6 @@ import { makeStyles } from 'tss-react/mui';
import { showOverflowDrawer } from '../../../../toolbox/functions.web';
import Icon from '../../../icons/components/Icon';
import { withPixelLineHeight } from '../../../styles/functions.web';
import { TEXT_OVERFLOW_TYPES } from '../../constants.any';
import TextWithOverflow from './TextWithOverflow';
@@ -14,7 +13,7 @@ export interface IProps {
/**
* Label used for accessibility.
*/
accessibilityLabel: string;
accessibilityLabel?: string;
/**
* The context menu item background color.
@@ -174,12 +173,12 @@ const useStyles = makeStyles()(theme => {
},
text: {
...withPixelLineHeight(theme.typography.bodyShortRegular),
...theme.typography.bodyShortRegular,
color: theme.palette.text01
},
drawerText: {
...withPixelLineHeight(theme.typography.bodyShortRegularLarge)
...theme.typography.bodyShortRegularLarge
}
};
});
@@ -232,7 +231,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

@@ -5,7 +5,6 @@ import { makeStyles } from 'tss-react/mui';
import { hideDialog } from '../../../dialog/actions';
import { IconCloseLarge } from '../../../icons/svg';
import { withPixelLineHeight } from '../../../styles/functions.web';
import { operatesWithEnterKey } from '../../functions.web';
import BaseDialog, { IProps as IBaseDialogProps } from './BaseDialog';
@@ -26,7 +25,7 @@ const useStyles = makeStyles()(theme => {
title: {
color: theme.palette.text01,
...withPixelLineHeight(theme.typography.heading5),
...theme.typography.heading5,
margin: 0,
padding: 0
},

View File

@@ -6,7 +6,6 @@ import { makeStyles } from 'tss-react/mui';
import { IReduxState } from '../../../../app/types';
import { hideDialog } from '../../../dialog/actions';
import { IconArrowBack, IconCloseLarge } from '../../../icons/svg';
import { withPixelLineHeight } from '../../../styles/functions.web';
import BaseDialog, { IProps as IBaseProps } from './BaseDialog';
import Button from './Button';
@@ -70,7 +69,7 @@ const useStyles = makeStyles()(theme => {
},
title: {
...withPixelLineHeight(theme.typography.heading5),
...theme.typography.heading5,
color: `${theme.palette.text01} !important`,
margin: 0,
padding: 0

View File

@@ -5,7 +5,6 @@ import { makeStyles } from 'tss-react/mui';
import { isMobileBrowser } from '../../../environment/utils';
import Icon from '../../../icons/components/Icon';
import { IconCloseCircle } from '../../../icons/svg';
import { withPixelLineHeight } from '../../../styles/functions.web';
import { IInputProps } from '../types';
import { HiddenDescription } from './HiddenDescription';
@@ -51,11 +50,11 @@ const useStyles = makeStyles()(theme => {
label: {
color: theme.palette.text01,
...withPixelLineHeight(theme.typography.bodyShortRegular),
...theme.typography.bodyShortRegular,
marginBottom: theme.spacing(2),
'&.is-mobile': {
...withPixelLineHeight(theme.typography.bodyShortRegularLarge)
...theme.typography.bodyShortRegularLarge
}
},
@@ -68,7 +67,7 @@ const useStyles = makeStyles()(theme => {
backgroundColor: theme.palette.ui03,
background: theme.palette.ui03,
color: theme.palette.text01,
...withPixelLineHeight(theme.typography.bodyShortRegular),
...theme.typography.bodyShortRegular,
padding: '10px 16px',
borderRadius: theme.shape.borderRadius,
border: 0,
@@ -92,7 +91,7 @@ const useStyles = makeStyles()(theme => {
'&.is-mobile': {
height: '48px',
padding: '13px 16px',
...withPixelLineHeight(theme.typography.bodyShortRegularLarge)
...theme.typography.bodyShortRegularLarge
},
'&.icon-input': {
@@ -139,11 +138,11 @@ const useStyles = makeStyles()(theme => {
bottomLabel: {
marginTop: theme.spacing(2),
...withPixelLineHeight(theme.typography.labelRegular),
...theme.typography.labelRegular,
color: theme.palette.text02,
'&.is-mobile': {
...withPixelLineHeight(theme.typography.bodyShortRegular)
...theme.typography.bodyShortRegular
},
'&.error': {

View File

@@ -4,7 +4,6 @@ import { makeStyles } from 'tss-react/mui';
import { ACTION_TRIGGER } from '../../../../participants-pane/constants';
import participantsPaneTheme from '../../../components/themes/participantsPaneTheme.json';
import { isMobileBrowser } from '../../../environment/utils';
import { withPixelLineHeight } from '../../../styles/functions.web';
interface IProps {
@@ -86,7 +85,7 @@ const useStyles = makeStyles()(theme => {
alignItems: 'center',
color: theme.palette.text01,
display: 'flex',
...withPixelLineHeight(theme.typography.bodyShortBold),
...theme.typography.bodyShortBold,
margin: `0 -${participantsPaneTheme.panePadding}px`,
padding: `${theme.spacing(2)} ${participantsPaneTheme.panePadding}px`,
position: 'relative',
@@ -110,7 +109,7 @@ const useStyles = makeStyles()(theme => {
},
[`@media(max-width: ${participantsPaneTheme.MD_BREAKPOINT})`]: {
...withPixelLineHeight(theme.typography.bodyShortBoldLarge),
...theme.typography.bodyShortBoldLarge,
padding: `${theme.spacing(3)} ${participantsPaneTheme.panePadding}px`
}
},

View File

@@ -2,7 +2,6 @@ import React, { useCallback, useMemo, useRef } from 'react';
import { makeStyles } from 'tss-react/mui';
import { IconCloseLarge } from '../../../icons/svg';
import { withPixelLineHeight } from '../../../styles/functions.web';
import { MultiSelectItem } from '../types';
import ClickableIcon from './ClickableIcon';
@@ -42,7 +41,7 @@ const useStyles = makeStyles()(theme => {
backgroundColor: theme.palette.ui01,
border: `1px solid ${theme.palette.ui04}`,
borderRadius: `${Number(theme.shape.borderRadius)}px`,
...withPixelLineHeight(theme.typography.bodyShortRegular),
...theme.typography.bodyShortRegular,
zIndex: 2,
maxHeight: `${MULTI_SELECT_HEIGHT}px`,
overflowY: 'auto',

View File

@@ -4,7 +4,6 @@ import { makeStyles } from 'tss-react/mui';
import { isMobileBrowser } from '../../../environment/utils';
import Icon from '../../../icons/components/Icon';
import { IconArrowDown } from '../../../icons/svg';
import { withPixelLineHeight } from '../../../styles/functions.web';
interface ISelectProps {
@@ -18,6 +17,11 @@ interface ISelectProps {
*/
className?: string;
/**
* Class name for additional styles for container.
*/
containerClassName?: string;
/**
* Whether or not the select is disabled.
*/
@@ -67,11 +71,11 @@ const useStyles = makeStyles()(theme => {
label: {
color: theme.palette.text01,
...withPixelLineHeight(theme.typography.bodyShortRegular),
...theme.typography.bodyShortRegular,
marginBottom: theme.spacing(2),
'&.is-mobile': {
...withPixelLineHeight(theme.typography.bodyShortRegularLarge)
...theme.typography.bodyShortRegularLarge
}
},
@@ -83,7 +87,7 @@ const useStyles = makeStyles()(theme => {
backgroundColor: theme.palette.ui03,
borderRadius: `${theme.shape.borderRadius}px`,
width: '100%',
...withPixelLineHeight(theme.typography.bodyShortRegular),
...theme.typography.bodyShortRegular,
color: theme.palette.text01,
padding: '10px 16px',
paddingRight: '42px',
@@ -103,7 +107,7 @@ const useStyles = makeStyles()(theme => {
},
'&.is-mobile': {
...withPixelLineHeight(theme.typography.bodyShortRegularLarge),
...theme.typography.bodyShortRegularLarge,
padding: '12px 16px',
paddingRight: '46px'
},
@@ -127,11 +131,11 @@ const useStyles = makeStyles()(theme => {
bottomLabel: {
marginTop: theme.spacing(2),
...withPixelLineHeight(theme.typography.labelRegular),
...theme.typography.labelRegular,
color: theme.palette.text02,
'&.is-mobile': {
...withPixelLineHeight(theme.typography.bodyShortRegular)
...theme.typography.bodyShortRegular
},
'&.error': {
@@ -143,6 +147,7 @@ const useStyles = makeStyles()(theme => {
const Select = ({
bottomLabel,
containerClassName,
className,
disabled,
error,
@@ -155,7 +160,7 @@ const Select = ({
const isMobile = isMobileBrowser();
return (
<div className = { classes.container }>
<div className = { cx(classes.container, containerClassName) }>
{label && <label
className = { cx(classes.label, isMobile && 'is-mobile') }
htmlFor = { id } >

View File

@@ -3,7 +3,6 @@ import { makeStyles } from 'tss-react/mui';
import { isMobileBrowser } from '../../../environment/utils';
import Icon from '../../../icons/components/Icon';
import { withPixelLineHeight } from '../../../styles/functions.web';
interface ITabProps {
accessibilityLabel: string;
@@ -18,6 +17,7 @@ interface ITabProps {
icon?: Function;
id: string;
label?: string;
title?: string;
}>;
}
@@ -28,7 +28,7 @@ const useStyles = makeStyles()(theme => {
},
tab: {
...withPixelLineHeight(theme.typography.bodyShortBold),
...theme.typography.bodyShortBold,
color: theme.palette.text02,
flex: 1,
padding: '14px',
@@ -65,12 +65,12 @@ const useStyles = makeStyles()(theme => {
},
'&.is-mobile': {
...withPixelLineHeight(theme.typography.bodyShortBoldLarge)
...theme.typography.bodyShortBoldLarge
}
},
badge: {
...withPixelLineHeight(theme.typography.labelBold),
...theme.typography.labelBold,
color: theme.palette.text04,
padding: `0 ${theme.spacing(1)}`,
borderRadius: '100%',
@@ -127,26 +127,34 @@ const Tabs = ({
aria-label = { accessibilityLabel }
className = { cx(classes.container, className) }
role = 'tablist'>
{tabs.map((tab, index) => (
<button
aria-controls = { tab.controlsId }
aria-label = { tab.accessibilityLabel }
aria-selected = { selected === tab.id }
className = { cx(classes.tab, selected === tab.id && 'selected', isMobile && 'is-mobile') }
disabled = { tab.disabled }
id = { tab.id }
key = { tab.id }
onClick = { onClick(tab.id) }
onKeyDown = { onKeyDown(index) }
role = 'tab'
tabIndex = { selected === tab.id ? undefined : -1 }>
{tab.icon && <Icon
className = { classes.icon }
src = { tab.icon } />}
{tab.label}
{tab.countBadge && <span className = { classes.badge }>{tab.countBadge}</span>}
</button>
))}
{
tabs.map((tab, index) => (
<button
aria-controls = { tab.controlsId }
aria-label = { tab.accessibilityLabel }
aria-selected = { selected === tab.id }
className = { cx(classes.tab, selected === tab.id && 'selected', isMobile && 'is-mobile') }
disabled = { tab.disabled }
id = { tab.id }
key = { tab.id }
onClick = { onClick(tab.id) }
onKeyDown = { onKeyDown(index) }
role = 'tab'
tabIndex = { selected === tab.id ? undefined : -1 }
title = { tab.title }>
{
tab.icon && <Icon
className = { classes.icon }
src = { tab.icon } />
}
{ tab.label }
{
tab.countBadge && <span className = { classes.badge }>
{ tab.countBadge }
</span>
}
</button>
))}
</div>
);
};

View File

@@ -32,7 +32,7 @@ export const commonStyles = (theme: Theme) => {
marginTop: theme.spacing(3),
'& label': {
fontSize: '14px'
fontSize: '0.875rem'
}
}
},
@@ -42,10 +42,10 @@ export const commonStyles = (theme: Theme) => {
color: theme.palette.text01,
cursor: 'pointer',
display: 'flex',
fontSize: 14,
fontSize: '0.875rem',
fontWeight: 400,
height: 40,
lineHeight: '24px',
lineHeight: '1.5rem',
padding: '8px 16px',
boxSizing: 'border-box' as const,
'& > div': {
@@ -85,7 +85,7 @@ export const commonStyles = (theme: Theme) => {
'& i': {
display: 'inline',
fontSize: 24
fontSize: '1.5rem'
},
'@media (hover: hover) and (pointer: fine)': {
@@ -120,8 +120,8 @@ export const commonStyles = (theme: Theme) => {
},
'.prejoin-dialog-label': {
fontSize: '15px',
lineHeight: '24px'
fontSize: '1rem',
lineHeight: '1.5rem'
},
'.prejoin-dialog-label-num': {
@@ -156,8 +156,8 @@ export const commonStyles = (theme: Theme) => {
'.prejoin-dialog-title': {
display: 'inline-block',
fontSize: '24px',
lineHeight: '32px'
fontSize: '1.5rem',
lineHeight: '2rem'
},
'.prejoin-dialog-icon': {
@@ -196,7 +196,7 @@ export const commonStyles = (theme: Theme) => {
'.prejoin-dialog-delimiter-txt': {
background: theme.palette.uiBackground,
color: theme.palette.text01,
fontSize: '11px',
fontSize: '0.75rem',
textTransform: 'uppercase' as const,
padding: `0 ${theme.spacing(2)}`
}
@@ -212,7 +212,7 @@ export const commonStyles = (theme: Theme) => {
display: 'flex',
borderRadius: 3,
flexDirection: 'column' as const,
fontSize: 24,
fontSize: '1.5rem',
height: 48,
justifyContent: 'center',
width: 48,
@@ -249,7 +249,7 @@ export const commonStyles = (theme: Theme) => {
color: theme.palette.text01,
cursor: 'pointer',
display: 'inline-block',
lineHeight: '48px',
lineHeight: '3rem',
textAlign: 'center' as const
},

View File

@@ -0,0 +1,25 @@
// Base font size in pixels (standard is 16px = 1rem)
const BASE_FONT_SIZE = 16;
/**
* Converts rem to pixels.
*
* @param {string} remValue - The value in rem units (e.g. '0.875rem').
* @returns {number}
*/
export function remToPixels(remValue: string): number {
const numericValue = parseFloat(remValue.replace('rem', ''));
return Math.round(numericValue * BASE_FONT_SIZE);
}
/**
* Converts pixels to rem.
*
* @param {number} pixels - The value in pixels.
* @returns {string}
* */
export function pixelsToRem(pixels: number): string {
return `${(pixels / BASE_FONT_SIZE).toFixed(3)}rem`;
}

View File

@@ -1,7 +1,36 @@
import { DefaultTheme } from 'react-native-paper';
import { remToPixels } from './functions.any';
import { createColorTokens } from './utils';
export * from './functions.any';
/**
* Converts all rem to pixels in an object.
*
* @param {Object} obj - The object to convert rem values in.
* @returns {Object}
*/
function convertRemValues(obj: any): any {
const converted: { [key: string]: any; } = {};
if (typeof obj !== 'object' || obj === null) {
return obj;
}
Object.entries(obj).forEach(([ key, value ]) => {
if (typeof value === 'string' && value.includes('rem')) {
converted[key] = remToPixels(value);
} else if (typeof value === 'object' && value !== null) {
converted[key] = convertRemValues(value);
} else {
converted[key] = value;
}
});
return converted;
}
/**
* Creates a React Native Paper theme based on local UI tokens.
*
@@ -16,7 +45,7 @@ export function createNativeTheme({ font, colorMap, shape, spacing, typography }
spacing,
typography: {
font,
...typography
...convertRemValues(typography)
}
};
}

View File

@@ -5,6 +5,8 @@ import { ITypography, IPalette as Palette1 } from '../ui/types';
import { createColorTokens, createTypographyTokens } from './utils';
export * from './functions.any';
declare module '@mui/material/styles' {
// eslint-disable-next-line @typescript-eslint/no-empty-interface
interface Palette extends Palette1 {}

View File

@@ -1,8 +1,8 @@
interface ITypographyType {
fontSize: number;
fontWeight: number; // TODO: revisit this.
fontSize: string;
fontWeight: 'normal' | 'bold' | 'bolder' | 'lighter' | number;
letterSpacing: number;
lineHeight: number;
lineHeight: string;
}
export interface IPalette {

View File

@@ -130,7 +130,7 @@ export default createStyleSheet({
*/
notificationIcon: {
color: 'white',
fontSize: 25
fontSize: '1.5rem'
},
/**
@@ -148,7 +148,7 @@ export default createStyleSheet({
*/
notificationText: {
color: 'white',
fontSize: 13
fontSize: '0.875rem'
},
/**

View File

@@ -163,3 +163,12 @@ export const SET_USER_CHAT_WIDTH = 'SET_USER_CHAT_WIDTH';
* }
*/
export const SET_CHAT_IS_RESIZING = 'SET_CHAT_IS_RESIZING';
/**
* The type of action sets the timestamp of the last private chat recipients list changed.
*
* {
* type: NOTIFY_PRIVATE_RECIPIENTS_CHANGED
* }
*/
export const NOTIFY_PRIVATE_RECIPIENTS_CHANGED = 'NOTIFY_PRIVATE_RECIPIENTS_CHANGED';

View File

@@ -1,6 +1,6 @@
import { IStore } from '../app/types';
import { getCurrentConference } from '../base/conference/functions';
import { getLocalParticipant } from '../base/participants/functions';
import { getLocalParticipant, getParticipantById } from '../base/participants/functions';
import { IParticipant } from '../base/participants/types';
import { LOBBY_CHAT_INITIALIZED } from '../lobby/constants';
@@ -10,6 +10,7 @@ import {
CLEAR_MESSAGES,
CLOSE_CHAT,
EDIT_MESSAGE,
NOTIFY_PRIVATE_RECIPIENTS_CHANGED,
OPEN_CHAT,
REMOVE_LOBBY_CHAT_PARTICIPANT,
SEND_MESSAGE,
@@ -170,6 +171,25 @@ export function setPrivateMessageRecipient(participant?: Object) {
};
}
/**
* Initiates the sending of a private message to the supplied participantId.
*
* @param {string} participantId - The participant id to set the recipient to.
* @returns {{
* participant: IParticipant,
* type: SET_PRIVATE_MESSAGE_RECIPIENT
* }}
*/
export function setPrivateMessageRecipientById(participantId: string) {
return (dispatch: IStore['dispatch'], getState: IStore['getState']) => {
const participant = getParticipantById(getState(), participantId);
if (participant) {
dispatch(setPrivateMessageRecipient(participant));
}
};
}
/**
* Set the value of the currently focused tab.
*
@@ -252,6 +272,22 @@ export function setLobbyChatActiveState(value: boolean) {
};
}
/**
* Notifies the private chat recipients list changed.
*
* @returns {Object}
*/
export function notifyPrivateRecipientsChanged() {
return (dispatch: IStore['dispatch']) => {
const timestamp = Date.now();
return dispatch({
type: NOTIFY_PRIVATE_RECIPIENTS_CHANGED,
payload: timestamp
});
};
}
/**
* Removes lobby type messages.
*

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

@@ -1,20 +1,29 @@
import { throttle } from 'lodash-es';
import React, { useCallback, useEffect, useState } from 'react';
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { connect, useSelector } from 'react-redux';
import { makeStyles } from 'tss-react/mui';
import { IReduxState } from '../../../app/types';
import { translate } from '../../../base/i18n/functions';
import { IconInfo, IconMessage, IconShareDoc, IconSubtitles } from '../../../base/icons/svg';
import { getLocalParticipant } from '../../../base/participants/functions';
import { getLocalParticipant, getRemoteParticipants } from '../../../base/participants/functions';
import Select from '../../../base/ui/components/web/Select';
import Tabs from '../../../base/ui/components/web/Tabs';
import { arePollsDisabled } from '../../../conference/functions.any';
import FileSharing from '../../../file-sharing/components/web/FileSharing';
import { isFileSharingEnabled } from '../../../file-sharing/functions.any';
import PollsPane from '../../../polls/components/web/PollsPane';
import { isCCTabEnabled } from '../../../subtitles/functions.any';
import { sendMessage, setChatIsResizing, setFocusedTab, setUserChatWidth, toggleChat } from '../../actions.web';
import { CHAT_SIZE, ChatTabs, SMALL_WIDTH_THRESHOLD } from '../../constants';
import {
sendMessage,
setChatIsResizing,
setFocusedTab,
setPrivateMessageRecipient,
setPrivateMessageRecipientById,
setUserChatWidth,
toggleChat
} from '../../actions.web';
import { CHAT_SIZE, ChatTabs, OPTION_GROUPCHAT, SMALL_WIDTH_THRESHOLD } from '../../constants';
import { getChatMaxSize } from '../../functions';
import { IChatProps as AbstractProps } from '../../types';
@@ -216,6 +225,10 @@ const useStyles = makeStyles<{ _isResizing: boolean; width: number; }>()((theme,
height: '100px',
width: '3px',
borderRadius: '1px'
},
privateMessageRecipientsList: {
padding: '0 16px 5px'
}
};
});
@@ -245,6 +258,34 @@ const Chat = ({
const [ mousePosition, setMousePosition ] = useState<number | null>(null);
const [ dragChatWidth, setDragChatWidth ] = useState<number | null>(null);
const maxChatWidth = useSelector(getChatMaxSize);
const notifyTimestamp = useSelector((state: IReduxState) =>
state['features/chat'].notifyPrivateRecipientsChangedTimestamp
);
const {
defaultRemoteDisplayName = 'Fellow Jitster'
} = useSelector((state: IReduxState) => state['features/base/config']);
const privateMessageRecipient = useSelector((state: IReduxState) => state['features/chat'].privateMessageRecipient);
const participants = useSelector(getRemoteParticipants);
const options = useMemo(() => {
const o = Array.from(participants?.values() || [])
.filter(p => !p.fakeParticipant)
.map(p => {
return {
value: p.id,
label: p.name ?? defaultRemoteDisplayName
};
});
o.sort((a, b) => a.label.localeCompare(b.label));
o.unshift({
label: t('chat.everyone'),
value: OPTION_GROUPCHAT
});
return o;
}, [ participants, defaultRemoteDisplayName, t, notifyTimestamp ]);
/**
* Handles mouse down on the drag handle.
@@ -376,6 +417,17 @@ const Chat = ({
dispatch(setFocusedTab(id as ChatTabs));
}, [ dispatch ]);
const onSelectedRecipientChange = useCallback((e: React.ChangeEvent<HTMLSelectElement>) => {
const selected = e.target.value;
if (selected === OPTION_GROUPCHAT) {
dispatch(setPrivateMessageRecipient());
} else {
dispatch(setPrivateMessageRecipientById(selected));
}
}, []);
/**
* Returns a React Element for showing chat messages and a form to send new
* chat messages.
@@ -403,6 +455,12 @@ const Chat = ({
<MessageContainer
messages = { _messages } />
<MessageRecipient />
<Select
containerClassName = { cx(classes.privateMessageRecipientsList) }
id = 'select-chat-recipient'
onChange = { onSelectedRecipientChange }
options = { options }
value = { privateMessageRecipient?.id || OPTION_GROUPCHAT } />
<ChatInput
onSend = { onSendMessage } />
</div>
@@ -454,7 +512,8 @@ const Chat = ({
_focusedTab !== ChatTabs.CHAT && _nbUnreadMessages > 0 ? _nbUnreadMessages : undefined,
id: ChatTabs.CHAT,
controlsId: `${ChatTabs.CHAT}-panel`,
icon: IconMessage
icon: IconMessage,
title: t('chat.tabs.chat')
}
];
@@ -464,7 +523,8 @@ const Chat = ({
countBadge: _focusedTab !== ChatTabs.POLLS && _nbUnreadPolls > 0 ? _nbUnreadPolls : undefined,
id: ChatTabs.POLLS,
controlsId: `${ChatTabs.POLLS}-panel`,
icon: IconInfo
icon: IconInfo,
title: t('chat.tabs.polls')
});
}
@@ -474,7 +534,8 @@ const Chat = ({
countBadge: undefined,
id: ChatTabs.CLOSED_CAPTIONS,
controlsId: `${ChatTabs.CLOSED_CAPTIONS}-panel`,
icon: IconSubtitles
icon: IconSubtitles,
title: t('chat.tabs.closedCaptions')
});
}
@@ -484,7 +545,8 @@ const Chat = ({
countBadge: undefined,
id: ChatTabs.FILE_SHARING,
controlsId: `${ChatTabs.FILE_SHARING}-panel`,
icon: IconShareDoc
icon: IconShareDoc,
title: t('chat.tabs.fileSharing')
});
}

View File

@@ -8,7 +8,7 @@ import { translate } from '../../../base/i18n/functions';
import { getParticipantById, getParticipantDisplayName, isPrivateChatEnabled } from '../../../base/participants/functions';
import Popover from '../../../base/popover/components/Popover.web';
import Message from '../../../base/react/components/web/Message';
import { withPixelLineHeight } from '../../../base/styles/functions.web';
import { MESSAGE_TYPE_LOCAL } from '../../constants';
import { getFormattedTimestamp, getMessageText, getPrivateNoticeMessage } from '../../functions';
import { IChatMessageProps } from '../../types';
@@ -16,9 +16,10 @@ import MessageMenu from './MessageMenu';
import ReactButton from './ReactButton';
interface IProps extends IChatMessageProps {
shouldDisplayChatMessageMenu: boolean;
className?: string;
enablePrivateChat?: boolean;
shouldDisplayMenuOnRight?: boolean;
state?: IReduxState;
type: string;
}
const useStyles = makeStyles()((theme: Theme) => {
@@ -128,7 +129,7 @@ const useStyles = makeStyles()((theme: Theme) => {
minHeight: '32px'
},
displayName: {
...withPixelLineHeight(theme.typography.labelBold),
...theme.typography.labelBold,
color: theme.palette.text02,
whiteSpace: 'nowrap',
textOverflow: 'ellipsis',
@@ -137,18 +138,18 @@ const useStyles = makeStyles()((theme: Theme) => {
maxWidth: '130px'
},
userMessage: {
...withPixelLineHeight(theme.typography.bodyShortRegular),
...theme.typography.bodyShortRegular,
color: theme.palette.text01,
whiteSpace: 'pre-wrap',
wordBreak: 'break-word'
},
privateMessageNotice: {
...withPixelLineHeight(theme.typography.labelRegular),
...theme.typography.labelRegular,
color: theme.palette.text02,
marginTop: theme.spacing(1)
},
timestamp: {
...withPixelLineHeight(theme.typography.labelRegular),
...theme.typography.labelRegular,
color: theme.palette.text03,
marginTop: theme.spacing(1),
marginLeft: theme.spacing(1),
@@ -190,11 +191,12 @@ const useStyles = makeStyles()((theme: Theme) => {
});
const ChatMessage = ({
className = '',
message,
state,
showDisplayName,
type,
shouldDisplayChatMessageMenu,
shouldDisplayMenuOnRight,
enablePrivateChat,
knocking,
t
}: IProps) => {
@@ -224,11 +226,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>
);
}
@@ -329,26 +333,28 @@ const ChatMessage = ({
return (
<div
className = { cx(classes.chatMessageWrapper, type) }
className = { cx(classes.chatMessageWrapper, className) }
id = { message.messageId }
onMouseEnter = { handleMouseEnter }
onMouseLeave = { handleMouseLeave }
tabIndex = { -1 }>
<div className = { classes.sideBySideContainer }>
{!shouldDisplayChatMessageMenu && (
{!shouldDisplayMenuOnRight && (
<div className = { classes.optionsButtonContainer }>
{isHovered && <MessageMenu
displayName = { message.displayName }
enablePrivateChat = { Boolean(enablePrivateChat) }
isFromVisitor = { message.isFromVisitor }
isLobbyMessage = { message.lobbyChat }
message = { message.message }
participantId = { message.participantId }
shouldDisplayChatMessageMenu = { shouldDisplayChatMessageMenu } />}
participantId = { message.participantId } />}
</div>
)}
<div
className = { cx(
'chatmessage',
classes.chatMessage,
type,
className,
message.privateMessage && 'privatemessage',
message.lobbyChat && !knocking && 'lobbymessage'
) }>
@@ -379,7 +385,7 @@ const ChatMessage = ({
</div>
</div>
</div>
{shouldDisplayChatMessageMenu && (
{shouldDisplayMenuOnRight && (
<div className = { classes.sideBySideContainer }>
{!message.privateMessage && !message.lobbyChat && <div>
<div className = { classes.optionsButtonContainer }>
@@ -391,10 +397,12 @@ const ChatMessage = ({
<div>
<div className = { classes.optionsButtonContainer }>
{isHovered && <MessageMenu
displayName = { message.displayName }
enablePrivateChat = { Boolean(enablePrivateChat) }
isFromVisitor = { message.isFromVisitor }
isLobbyMessage = { message.lobbyChat }
message = { message.message }
participantId = { message.participantId }
shouldDisplayChatMessageMenu = { shouldDisplayChatMessageMenu } />}
participantId = { message.participantId } />}
</div>
</div>
</div>
@@ -414,10 +422,23 @@ 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);
// Only the local messages appear on the right side of the chat therefore only for them the menu has to be on the
// left side.
const shouldDisplayMenuOnRight = message.messageType !== MESSAGE_TYPE_LOCAL;
return {
shouldDisplayChatMessageMenu: enablePrivateChat,
shouldDisplayMenuOnRight,
enablePrivateChat,
knocking,
state
};

View File

@@ -71,12 +71,11 @@ const ChatMessageGroup = ({ className = '', messages }: IProps) => {
<div className = { `${classes.messageGroup} chat-message-group ${className}` }>
{messages.map((message, i) => (
<ChatMessage
className = { className }
key = { i }
message = { message }
shouldDisplayChatMessageMenu = { false }
showDisplayName = { i === 0 }
showTimestamp = { i === messages.length - 1 }
type = { className } />
showTimestamp = { i === messages.length - 1 } />
))}
</div>
</div>

View File

@@ -6,7 +6,6 @@ import { makeStyles } from 'tss-react/mui';
import { IReduxState } from '../../../app/types';
import Icon from '../../../base/icons/components/Icon';
import { IconSubtitles } from '../../../base/icons/svg';
import { withPixelLineHeight } from '../../../base/styles/functions.web';
import Button from '../../../base/ui/components/web/Button';
import { groupMessagesBySender } from '../../../base/util/messageGrouping';
import { setRequestingSubtitles } from '../../../subtitles/actions.any';
@@ -67,7 +66,7 @@ const useStyles = makeStyles()(theme => {
}
},
emptyState: {
...withPixelLineHeight(theme.typography.bodyLongBold),
...theme.typography.bodyLongBold,
color: theme.palette.text02
}
};

View File

@@ -15,10 +15,12 @@ import { handleLobbyChatInitialized, openChat } from '../../actions.web';
export interface IProps {
className?: string;
displayName?: string;
enablePrivateChat: boolean;
isFromVisitor?: boolean;
isLobbyMessage: boolean;
message: string;
participantId: string;
shouldDisplayChatMessageMenu: boolean;
}
const useStyles = makeStyles()(theme => {
@@ -46,7 +48,7 @@ const useStyles = makeStyles()(theme => {
color: 'white',
padding: '4px 8px',
borderRadius: '4px',
fontSize: '12px',
fontSize: '0.75rem',
zIndex: 1000,
opacity: 0,
transition: 'opacity 0.3s ease-in-out',
@@ -58,7 +60,7 @@ const useStyles = makeStyles()(theme => {
};
});
const MessageMenu = ({ message, participantId, isLobbyMessage, shouldDisplayChatMessageMenu }: IProps) => {
const MessageMenu = ({ message, participantId, isFromVisitor, isLobbyMessage, enablePrivateChat, 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)
@@ -115,7 +130,7 @@ const MessageMenu = ({ message, participantId, isLobbyMessage, shouldDisplayChat
const popoverContent = (
<div className = { classes.menuPanel }>
{shouldDisplayChatMessageMenu && (
{enablePrivateChat && (
<div
className = { classes.menuItem }
onClick = { handlePrivateClick }>

View File

@@ -4,7 +4,6 @@ import { connect } from 'react-redux';
import { makeStyles } from 'tss-react/mui';
import { IconCloseLarge } from '../../../base/icons/svg';
import { withPixelLineHeight } from '../../../base/styles/functions.web';
import Button from '../../../base/ui/components/web/Button';
import { BUTTON_TYPES } from '../../../base/ui/constants.any';
import {
@@ -24,7 +23,7 @@ const useStyles = makeStyles()(theme => {
alignItems: 'center',
backgroundColor: theme.palette.support05,
borderRadius: theme.shape.borderRadius,
...withPixelLineHeight(theme.typography.bodyShortRegular),
...theme.typography.bodyShortRegular,
color: theme.palette.text01
},
@@ -48,6 +47,7 @@ const useStyles = makeStyles()(theme => {
const MessageRecipient = ({
_privateMessageRecipient,
_isLobbyChatActive,
_isVisitor,
_lobbyMessageRecipient,
_onRemovePrivateMessageRecipient,
_onHideLobbyChatRecipient,
@@ -80,9 +80,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

@@ -5,7 +5,6 @@ import { makeStyles } from 'tss-react/mui';
import { translate } from '../../../base/i18n/functions';
import Icon from '../../../base/icons/components/Icon';
import { IconArrowDown } from '../../../base/icons/svg';
import { withPixelLineHeight } from '../../../base/styles/functions.web';
import BaseTheme from '../../../base/ui/components/BaseTheme.web';
export interface INewMessagesButtonProps extends WithTranslation {
@@ -53,7 +52,7 @@ const useStyles = makeStyles()(theme => {
},
textContainer: {
...withPixelLineHeight(theme.typography.bodyShortRegular),
...theme.typography.bodyShortRegular,
color: theme.palette.text04,
paddingLeft: '8px'
}

View File

@@ -3,7 +3,6 @@ import { useSelector } from 'react-redux';
import { makeStyles } from 'tss-react/mui';
import { getParticipantDisplayName } from '../../../base/participants/functions';
import { withPixelLineHeight } from '../../../base/styles/functions.web';
import { ISubtitle } from '../../../subtitles/types';
/**
@@ -39,7 +38,7 @@ const useStyles = makeStyles()(theme => {
},
messageHeader: {
...withPixelLineHeight(theme.typography.labelBold),
...theme.typography.labelBold,
color: theme.palette.text02,
whiteSpace: 'nowrap',
textOverflow: 'ellipsis',
@@ -49,14 +48,14 @@ const useStyles = makeStyles()(theme => {
},
messageText: {
...withPixelLineHeight(theme.typography.bodyShortRegular),
...theme.typography.bodyShortRegular,
color: theme.palette.text01,
whiteSpace: 'pre-wrap',
wordBreak: 'break-word'
},
timestamp: {
...withPixelLineHeight(theme.typography.labelRegular),
...theme.typography.labelRegular,
color: theme.palette.text03,
marginTop: theme.spacing(1)
},

View File

@@ -55,3 +55,5 @@ export const TIMESTAMP_FORMAT = 'H:mm';
* The namespace for system messages.
*/
export const MESSAGE_TYPE_SYSTEM = 'system_chat_message';
export const OPTION_GROUPCHAT = 'groupchat';

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

@@ -15,11 +15,13 @@ import {
JitsiConferenceErrors,
JitsiConferenceEvents
} from '../base/lib-jitsi-meet';
import { PARTICIPANT_JOINED, PARTICIPANT_LEFT, PARTICIPANT_UPDATED } from '../base/participants/actionTypes';
import {
getLocalParticipant,
getParticipantById,
getParticipantDisplayName
} from '../base/participants/functions';
import { IParticipant } from '../base/participants/types';
import MiddlewareRegistry from '../base/redux/MiddlewareRegistry';
import StateListenerRegistry from '../base/redux/StateListenerRegistry';
import { playSound, registerSound, unregisterSound } from '../base/sounds/actions';
@@ -33,8 +35,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,
@@ -44,7 +45,14 @@ import {
SEND_REACTION,
SET_FOCUSED_TAB
} from './actionTypes';
import { addMessage, addMessageReaction, clearMessages, closeChat, setPrivateMessageRecipient } from './actions.any';
import {
addMessage,
addMessageReaction,
clearMessages,
closeChat,
notifyPrivateRecipientsChanged,
setPrivateMessageRecipient
} from './actions.any';
import { ChatPrivacyDialog } from './components';
import {
ChatTabs,
@@ -55,8 +63,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 +195,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) {
@@ -195,6 +207,19 @@ MiddlewareRegistry.register(store => next => action => {
break;
}
case PARTICIPANT_JOINED:
case PARTICIPANT_LEFT:
case PARTICIPANT_UPDATED: {
if (_shouldNotifyPrivateRecipientsChanged(store, action)) {
const result = next(action);
dispatch(notifyPrivateRecipientsChanged());
return result;
}
break;
}
case SEND_MESSAGE: {
const state = store.getState();
const conference = getCurrentConference(state);
@@ -204,14 +229,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 +255,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);
}
@@ -297,6 +325,27 @@ StateListenerRegistry.register(
}
);
/**
* Checks whether a notification for private chat recipients is needed.
*
* @param {IStore} store - The redux store.
* @param {{ participant: IParticipant, type: string }} action - The action.
* @returns {boolean}
*/
function _shouldNotifyPrivateRecipientsChanged(
store: IStore, action: { participant: IParticipant; type: string; }
) {
const { type, participant } = action;
if ([ PARTICIPANT_LEFT, PARTICIPANT_JOINED ].includes(type)) {
return true;
}
const { id, name } = participant;
return name !== getParticipantDisplayName(store, id);
}
/**
* Registers listener for {@link JitsiConferenceEvents.MESSAGE_RECEIVED} that
* will perform various chat related activities.
@@ -317,7 +366,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 +374,7 @@ function _addChatMsgListener(conference: IJitsiConference, store: IStore) {
message,
timestamp,
displayName,
isGuest,
isFromVisitor,
messageId,
privateMessage: false });
@@ -350,13 +399,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 +426,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 +441,7 @@ function _onConferenceMessageReceived(store: IStore,
}
_handleReceivedMessage(store, {
displayName,
isGuest,
isFromVisitor,
participantId,
message,
privateMessage,
@@ -505,8 +556,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 +576,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 +595,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 +606,8 @@ function _handleReceivedMessage({ dispatch, getState }: IStore,
recipient: getParticipantDisplayName(state, localParticipant?.id ?? ''),
timestamp: millisecondsTimestamp,
messageId,
isReaction
isReaction,
isFromVisitor
}));
if (shouldShowNotification) {
@@ -574,6 +630,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 +647,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 +663,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 +678,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 +722,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 +741,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,
@@ -8,6 +9,7 @@ import {
CLEAR_MESSAGES,
CLOSE_CHAT,
EDIT_MESSAGE,
NOTIFY_PRIVATE_RECIPIENTS_CHANGED,
OPEN_CHAT,
REMOVE_LOBBY_CHAT_PARTICIPANT,
SET_CHAT_IS_RESIZING,
@@ -25,6 +27,7 @@ const DEFAULT_STATE = {
groupChatWithPermissions: false,
isOpen: false,
messages: [],
notifyPrivateRecipientsChangedTimestamp: undefined,
reactions: {},
nbUnreadMessages: 0,
privateMessageRecipient: undefined,
@@ -51,7 +54,8 @@ export interface IChatState {
} | ILocalParticipant;
messages: IMessage[];
nbUnreadMessages: number;
privateMessageRecipient?: IParticipant;
notifyPrivateRecipientsChangedTimestamp?: number;
privateMessageRecipient?: IParticipant | IVisitorChatParticipant;
width: {
current: number;
userSet: number | null;
@@ -64,6 +68,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 +78,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
};
@@ -259,6 +265,11 @@ ReducerRegistry.register<IChatState>('features/chat', (state = DEFAULT_STATE, ac
isResizing: action.resizing
};
}
case NOTIFY_PRIVATE_RECIPIENTS_CHANGED:
return {
...state,
notifyPrivateRecipientsChangedTimestamp: action.payload
};
}
return state;

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;
}
@@ -60,11 +62,6 @@ export interface IChatMessageProps extends WithTranslation {
*/
message: IMessage;
/**
* Whether the chat message menu is visible or not.
*/
shouldDisplayChatMessageMenu?: boolean;
/**
* Whether or not the avatar image of the participant which sent the message
* should be displayed.

View File

@@ -579,7 +579,7 @@ function _mapStateToProps(state: IReduxState, _ownProps: any) {
const { startCarMode } = state['features/base/settings'];
const { enabled: audioOnlyEnabled } = state['features/base/audio-only'];
const brandingStyles = backgroundColor ? {
backgroundColor
background: backgroundColor
} : undefined;
return {

View File

@@ -1,13 +1,12 @@
import React from 'react';
import { makeStyles } from 'tss-react/mui';
import { withPixelLineHeight } from '../../../base/styles/functions.web';
import { IDisplayProps } from '../ConferenceTimer';
const useStyles = makeStyles()(theme => {
return {
timer: {
...withPixelLineHeight(theme.typography.labelRegular),
...theme.typography.labelRegular,
color: theme.palette.text01,
padding: '6px 8px',
backgroundColor: 'rgba(0, 0, 0, 0.8)',

View File

@@ -11,7 +11,7 @@ const useStyles = makeStyles()(theme => {
},
text: {
fontSize: '20px'
fontSize: '1.25rem'
}
};
});

View File

@@ -4,13 +4,12 @@ import { useSelector } from 'react-redux';
import { makeStyles } from 'tss-react/mui';
import { getConferenceName } from '../../../base/conference/functions';
import { withPixelLineHeight } from '../../../base/styles/functions.web';
import Tooltip from '../../../base/tooltip/components/Tooltip';
const useStyles = makeStyles()(theme => {
return {
container: {
...withPixelLineHeight(theme.typography.bodyLongRegular),
...theme.typography.bodyLongRegular,
color: theme.palette.text01,
padding: '2px 16px',
backgroundColor: 'rgba(0, 0, 0, 0.6)',

View File

@@ -8,7 +8,6 @@ import { MIN_ASSUMED_BANDWIDTH_BPS } from '../../../../../modules/API/constants'
import { IReduxState } from '../../../app/types';
import { setAssumedBandwidthBps as saveAssumedBandwidthBps } from '../../../base/conference/actions';
import { IconInfoCircle } from '../../../base/icons/svg';
import { withPixelLineHeight } from '../../../base/styles/functions.web';
import Dialog from '../../../base/ui/components/web/Dialog';
import Input from '../../../base/ui/components/web/Input';
@@ -20,7 +19,7 @@ const useStyles = makeStyles()(theme => {
info: {
background: theme.palette.ui01,
...withPixelLineHeight(theme.typography.labelRegular),
...theme.typography.labelRegular,
color: theme.palette.text02,
marginTop: theme.spacing(2)
},

View File

@@ -19,6 +19,7 @@ import {
getVirtualScreenshareParticipantTrack
} from '../../../base/tracks/functions';
import { ITrack } from '../../../base/tracks/types';
import { pixelsToRem } from '../../../base/ui/functions.any';
import {
isTrackStreamingStatusInactive,
isTrackStreamingStatusInterrupted
@@ -355,7 +356,7 @@ class ConnectionIndicator extends AbstractConnectionIndicator<IProps, IState> {
return (
<div
style = {{ fontSize: iconSize }}>
style = {{ fontSize: pixelsToRem(iconSize) }}>
<span className = 'sr-only'>{ t('videothumbnail.connectionInfo') }</span>
<ConnectionIndicatorIcon
classes = { classes }

View File

@@ -222,7 +222,7 @@ const useStyles = makeStyles()(theme => {
},
connectionStatsTable: {
'&, & > table': {
fontSize: '12px',
fontSize: '0.75rem',
fontWeight: 400,
'& td': {

View File

@@ -12,7 +12,6 @@ import { getLegalUrls } from '../../base/config/functions.any';
import { isSupportedBrowser } from '../../base/environment/environment';
import { translate, translateToHTML } from '../../base/i18n/functions';
import Platform from '../../base/react/Platform.web';
import { withPixelLineHeight } from '../../base/styles/functions.web';
import Button from '../../base/ui/components/web/Button';
import { BUTTON_TYPES } from '../../base/ui/constants.any';
import {
@@ -46,15 +45,15 @@ const useStyles = makeStyles()((theme: Theme) => {
},
launchingMeetingLabel: {
marginBottom: 16,
...withPixelLineHeight(theme.typography.heading4)
...theme.typography.heading4
},
roomName: {
marginBottom: 32,
...withPixelLineHeight(theme.typography.heading5)
...theme.typography.heading5
},
descriptionLabel: {
marginBottom: 32,
...withPixelLineHeight(theme.typography.bodyLongRegular)
...theme.typography.bodyLongRegular
},
buttonsContainer: {
display: 'flex',
@@ -71,7 +70,7 @@ const useStyles = makeStyles()((theme: Theme) => {
},
label: {
marginTop: 40,
...withPixelLineHeight(theme.typography.labelRegular),
...theme.typography.labelRegular,
color: theme.palette.text02,
'& a': {
color: theme.palette.link01

View File

@@ -12,7 +12,6 @@ import { IDeeplinkingConfig, IDeeplinkingMobileConfig } from '../../base/config/
import { isSupportedMobileBrowser } from '../../base/environment/environment';
import { translate } from '../../base/i18n/functions';
import Platform from '../../base/react/Platform.web';
import { withPixelLineHeight } from '../../base/styles/functions.web';
import Button from '../../base/ui/components/web/Button';
import DialInSummary from '../../invite/components/dial-in-summary/web/DialInSummary';
import { openWebApp } from '../actions';
@@ -51,10 +50,10 @@ const useStyles = makeStyles()((theme: Theme) => {
marginTop: 24,
textAlign: 'center',
marginBottom: 32,
...withPixelLineHeight(theme.typography.heading5)
...theme.typography.heading5
},
roomNameLabel: {
...withPixelLineHeight(theme.typography.bodyLongRegularLarge)
...theme.typography.bodyLongRegularLarge
},
joinMeetWrapper: {
marginTop: 24,
@@ -63,7 +62,7 @@ const useStyles = makeStyles()((theme: Theme) => {
labelDescription: {
textAlign: 'center',
marginTop: 16,
...withPixelLineHeight(theme.typography.bodyShortRegularLarge)
...theme.typography.bodyShortRegularLarge
},
linkWrapper: {
display: 'flex',
@@ -74,7 +73,7 @@ const useStyles = makeStyles()((theme: Theme) => {
},
linkLabel: {
color: theme.palette.link01,
...withPixelLineHeight(theme.typography.bodyLongBoldLarge)
...theme.typography.bodyLongBoldLarge
},
supportedBrowserContent: {
marginTop: 16,
@@ -84,7 +83,7 @@ const useStyles = makeStyles()((theme: Theme) => {
justifyContent: 'center'
},
labelOr: {
...withPixelLineHeight(theme.typography.bodyShortRegularLarge)
...theme.typography.bodyShortRegularLarge
},
separator: {
marginTop: '32px',

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