Compare commits

...

34 Commits

Author SHA1 Message Date
Calin-Teodor
2d2bae6ec1 feat(toolbox): moved action to appropriate feature and fixed naming 2024-08-09 16:19:13 +03:00
Saúl Ibarra Corretgé
fa6dc292c1 fix(polls) improve message validation (#14991)
* fix(polls) improve message validation

- Prevent creation of too many polls
- Discard absurdly large payloads

* Update resources/prosody-plugins/mod_polls.lua

Co-authored-by: Дамян Минков <damencho@jitsi.org>

---------

Co-authored-by: Дамян Минков <damencho@jitsi.org>
2024-08-09 06:23:43 -05:00
Saúl Ibarra Corretgé
b5ac40b32a fix(build) don't run clean after compilation
It prevents the bundle analyzer from working because the stats files are
placed in the build dir.

Clean *before* building instead.
2024-08-09 12:03:03 +02:00
Mengyuan Liu
8299aa498b feat(raise-hand) group options in config.js 2024-08-09 11:39:55 +02:00
Saúl Ibarra Corretgé
ce22adfe64 fix(ios) SDK release script fixes
- Run script with tracing
- Allow empty commits (we might need a rebuild)
- Fix tag name in lite SDK
2024-08-08 07:49:26 +02:00
Jaya Allamsetty
01bcccdd99 chore(deps) lib-jitsi-meet@latest
https://github.com/jitsi/lib-jitsi-meet/compare/v1846.0.0+a97a8dff...v1847.0.0+03eef6de
2024-08-07 22:21:47 -04:00
Mengyuan Liu
673a54adb1 feat(raise-han) add CTA for opening participants pane on notification 2024-08-07 17:42:49 +02:00
Calin-Teodor
f48c03e314 feat(ios): readded git tag for release sdk scritps 2024-08-07 16:09:37 +03:00
Calinteodor
7641ddad68 feat(chat/native): set limit for linkifying and replacing non unicode messages (#14979)
* feat(chat): set limit for linkifying and replacing non unicode emoji messages
2024-08-07 14:10:28 +03:00
damencho
26021b2dd0 Revert "feat: Presence identity module to support other auth mechanisms."
This reverts commit a6457db819.
2024-08-07 09:52:48 +03:00
damencho
a6457db819 feat: Presence identity module to support other auth mechanisms. 2024-08-06 19:27:52 +03:00
Patrick He
8bfa65987d feat(chat) use the original message ID for processing
This is a prerequisite for operations that rely on previous messages, such as reactions.
2024-08-06 15:30:37 +02:00
Calin-Teodor
b1c0cc5322 feat(android/ios): we no longer need to set a tag 2024-08-06 14:07:59 +03:00
Saúl Ibarra Corretgé
67cbef0d7a chore(deps) lib-jitsi-meet@latest
https://github.com/jitsi/lib-jitsi-meet/compare/v1845.0.0+515a927c...v1846.0.0+a97a8dff
2024-08-06 11:04:20 +02:00
Calinteodor
b2e06c1c94 feat(chat): revert char_limit for input (#14973)
* feat(chat): revert char_limit for message input
2024-08-06 11:12:30 +03:00
damencho
70a05e487e fix(i18n): Ignore case when normalizing languages.
Fixes #14764.
2024-08-06 11:06:52 +03:00
Calinteodor
c2a446a79b feat(toolbox/web): updated imports and fixed jsdocs (#14966)
* feat(toolbox/web): updated imports extensions and fixed some jsdocs
2024-08-06 10:51:18 +03:00
Calin-Teodor
56df23af35 dep(react-native): update for some related dependencies to latest 2024-08-06 10:50:41 +03:00
Mihaela Dumitru
1c12f5cf2a fix(visitors) remove default notification (#14970) 2024-08-05 17:55:25 +03:00
Calin-Teodor
06b727ae1a feat(chat): fixed linter 2024-08-02 14:25:41 +03:00
Calin-Teodor
a2aca30d57 feat(notifications): set numberOfLines for content text description and title 2024-08-02 14:25:41 +03:00
Calin-Teodor
68106feb57 feat(chat): set CHAR_LIMIT for chat input 2024-08-02 14:25:41 +03:00
Calin-Teodor
3066fc1d31 feat(participants): revert prev change 2024-08-02 14:17:40 +03:00
Calin-Teodor
4834fb7b6f feat(participants): check for raisedHandsQueue length before notifying participant to speak 2024-08-02 14:17:40 +03:00
Raphaël Badawi
1e101afe5a feat(invite) add email autocomplete in invite (#14610)
* feat(invite) add email value in peopleSearchQueryTypes and peopleSearchToken config

* feat(invite) pass custom auth token in search directory (e. g. for email directory)

* feat(invite) autocomplete and invitations working with custom auth token (e. g. email invite type), invite icons updated

* feat(invite) remove newly documented config from undocumented settings list

* feat(invite) jwt are now passed in the invite requests headers

* feat(invite) linter-related formatting

* feat(invite) fix default user icon regression

* feat(invite) last lint issues

* feat(invite) pass alternate token in header, not in params

* Fixes lint error

---------

Co-authored-by: Raphaël Badawi <raphael.badawi@ceo-vision.com>
Co-authored-by: Дамян Минков <damencho@jitsi.org>
2024-08-01 21:01:12 +03:00
Hristo Terezov
4d79bbb5d8 feat(window.loaded): Add new metric. (#14965) 2024-08-01 12:43:25 -04:00
Saúl Ibarra Corretgé
e31aff5afd Revert "feat(external-api) add deployment information to ready event"
This reverts commit 6727004930.
2024-07-31 17:44:56 +02:00
Calin-Teodor
7bc9913b29 feat(base/participants): fix max callstack error 2024-07-31 17:46:39 +03:00
Saúl Ibarra Corretgé
2483d901d6 feat(external-api) add "name" property to participant-kicked-out event 2024-07-31 16:21:48 +02:00
Saúl Ibarra Corretgé
6ff7995cee fix(participants) skip notification when kicker is the local participant 2024-07-31 16:21:48 +02:00
Saúl Ibarra Corretgé
5d563402d0 chore(deps) lib-jitsi-meet@latest
https://github.com/jitsi/lib-jitsi-meet/compare/v1844.0.0+a9b6dd7e...v1845.0.0+515a927c
2024-07-31 16:21:36 +02:00
Saúl Ibarra Corretgé
6727004930 feat(external-api) add deployment information to ready event 2024-07-31 16:19:56 +02:00
Mengyuan Liu
c04000ea20 feat(raise-hand) notify next speaker (#14904) 2024-07-31 12:38:49 +03:00
Mihaela Dumitru
23be14697c fix(whiteboard) remove limit dialog for jibri (#14947) 2024-07-30 16:39:09 +03:00
57 changed files with 591 additions and 264 deletions

View File

@@ -24,9 +24,9 @@ else
WEBPACK_DEV_SERVER = ./node_modules/.bin/webpack serve --mode development
endif
all: compile deploy clean
all: compile deploy
compile:
compile: clean
NODE_OPTIONS=--max-old-space-size=8192 \
$(WEBPACK)

View File

@@ -10,7 +10,6 @@ MVN_HTTP=0
DEFAULT_SDK_VERSION=$(grep sdkVersion ${THIS_DIR}/../gradle.properties | cut -d"=" -f2)
SDK_VERSION=${OVERRIDE_SDK_VERSION:-${DEFAULT_SDK_VERSION}}
JSC_VERSION="r"$(jq -r '.dependencies."jsc-android"' ${THIS_DIR}/../../node_modules/react-native/package.json | cut -d . -f 1 | cut -c 2-)
DO_GIT_TAG=${GIT_TAG:-0}
if [[ $THE_MVN_REPO == http* ]]; then
MVN_HTTP=1
@@ -67,15 +66,12 @@ pushd ${THIS_DIR}/../
./gradlew publish
popd
if [[ $DO_GIT_TAG == 1 ]]; then
# The artifacts are now on the Maven repo, commit them
# The artifacts are now on the Maven repo, commit them
if [[ $MVN_HTTP == 0 ]]; then
pushd ${MVN_REPO_PATH}
git add -A .
git commit -m "Jitsi Meet SDK + dependencies: ${SDK_VERSION}"
popd
# Tag the release
git tag android-sdk-${SDK_VERSION}
fi
# Done!

View File

@@ -232,9 +232,26 @@ var config = {
// Sets the preferred resolution (height) for local video. Defaults to 720.
// resolution: 720,
// DEPRECATED. Please use raisedHands.disableRemoveRaisedHandOnFocus instead.
// Specifies whether the raised hand will hide when someone becomes a dominant speaker or not
// disableRemoveRaisedHandOnFocus: false,
// Specifies which raised hand related config should be set.
// raisedHands: {
// // Specifies whether the raised hand can be lowered by moderator.
// disableLowerHandByModerator: false,
// // Specifies whether there is a notification before hiding the raised hand
// // when someone becomes the dominant speaker.
// disableLowerHandNotification: true,
// // Specifies whether there is a notification when you are the next speaker in line.
// disableNextSpeakerNotification: false,
// // Specifies whether the raised hand will hide when someone becomes a dominant speaker or not.
// disableRemoveRaisedHandOnFocus: false,
// },
// speakerStats: {
// // Specifies whether the speaker stats is enable or not.
// disabled: false,
@@ -1549,6 +1566,17 @@ var config = {
// and will automatically redirect to the token service to get the token for the meeting.
// tokenAuthUrlAutoRedirect: false
// You can put an array of values to target different entity types in the invite dialog.
// Valid values are "phone", "room", "sip", "user", "videosipgw" and "email"
// peopleSearchQueryTypes: ["user", "email"],
// Directory endpoint which is called for invite dialog autocomplete
// peopleSearchUrl: "https://myservice.com/api/people",
// Endpoint which is called to send invitation requests
// inviteServiceUrl: "https://myservice.com/api/invite",
// For external entities (e. g. email), the localStorage key holding the token value for directory authentication
// peopleSearchTokenLocation: "mytoken",
// List of undocumented settings used in jitsi-meet
/**
_immediateReloadThreshold
@@ -1565,8 +1593,6 @@ var config = {
iAmRecorder
iAmSipGateway
microsoftApiApplicationClientID
peopleSearchQueryTypes
peopleSearchUrl
requireDisplayName
*/

View File

@@ -69,6 +69,11 @@
window.indexLoadedTime = window.performance.now();
console.log("(TIME) index.html loaded:\t", indexLoadedTime);
window.addEventListener('load', function() {
window.loadedEventTime = window.performance.now();
console.log("(TIME) window loaded event:\t", loadedEventTime);
});
// XXX the code below listeners for errors and displays an error message
// in the document body when any of the required files fails to load.
// The intention is to prevent from displaying broken page.

View File

@@ -1022,7 +1022,7 @@ PODS:
- React-Core
- react-native-performance (5.0.0):
- React-Core
- react-native-safe-area-context (4.7.1):
- react-native-safe-area-context (4.10.8):
- React-Core
- react-native-slider (4.4.3):
- React-Core
@@ -1214,7 +1214,7 @@ PODS:
- React-Core
- RNDeviceInfo (10.9.0):
- React-Core
- RNGestureHandler (2.17.1):
- RNGestureHandler (2.18.1):
- glog
- RCT-Folly (= 2022.05.16.00)
- React-Core
@@ -1558,7 +1558,7 @@ SPEC CHECKSUMS:
react-native-orientation-locker: 4409c5b12b65f942e75449872b4f078b6f27af81
react-native-pager-view: 0ccb8bf60e2ebd38b1f3669fa3650ecce81db2df
react-native-performance: 47ac22ebf2aa24f324a96a5825581f6ce18c09e8
react-native-safe-area-context: 9697629f7b2cda43cf52169bb7e0767d330648c2
react-native-safe-area-context: b7daa1a8df36095a032dff095a1ea8963cb48371
react-native-slider: 1cdd6ba29675df21f30544253bf7351d3c2d68c4
react-native-splash-screen: 4312f786b13a81b5169ef346d76d33bc0c6dc457
react-native-video: 472b7c366eaaaa0207e546d9a50410df89790bcf
@@ -1589,7 +1589,7 @@ SPEC CHECKSUMS:
RNCClipboard: 0a720adef5ec193aa0e3de24c3977222c7e52a37
RNDefaultPreference: 08bdb06cfa9188d5da97d4642dac745218d7fb31
RNDeviceInfo: 02ea8b23e2280fa18e00a06d7e62804d74028579
RNGestureHandler: 67d3f1f69d4d0c98d6e83f4229e3bbf997d1dc72
RNGestureHandler: 1155b1898ceddefeebf77792927360d44fe11e77
RNGoogleSignin: a6a612cce56a45ab701c5c5c6e36f5390522d100
RNScreens: e842cdccb23c0a084bd6307f6fa83fd1c1738029
RNSound: 6c156f925295bdc83e8e422e7d8b38d33bc71852

View File

@@ -1,14 +1,12 @@
#!/bin/bash
set -e -u
set -e -u -x
THIS_DIR=$(cd -P "$(dirname "$(readlink "${BASH_SOURCE[0]}" || echo "${BASH_SOURCE[0]}")")" && pwd)
PROJECT_REPO=$(realpath ${THIS_DIR}/../..)
RELEASE_REPO=$(realpath ${THIS_DIR}/../../../jitsi-meet-ios-sdk-releases)
DEFAULT_SDK_VERSION=$(/usr/libexec/PlistBuddy -c "Print CFBundleShortVersionString" ${THIS_DIR}/../sdk/src/Lite-Info.plist)
SDK_VERSION=${OVERRIDE_SDK_VERSION:-${DEFAULT_SDK_VERSION}}
DO_GIT_TAG=${GIT_TAG:-0}
echo "Releasing Jitsi Meet SDK Lite ${SDK_VERSION}"
@@ -50,9 +48,6 @@ xcodebuild -create-xcframework \
-framework ios/sdk/out/ios-device.xcarchive/Products/Library/Frameworks/JitsiMeetSDK.framework \
-framework ios/sdk/out/ios-simulator.xcarchive/Products/Library/Frameworks/JitsiMeetSDK.framework \
-output ios/sdk/out/JitsiMeetSDK.xcframework
if [[ $DO_GIT_TAG == 1 ]]; then
git tag ios-sdk-lite-${SDK_VERSION}
fi
popd
pushd ${RELEASE_REPO}
@@ -61,11 +56,9 @@ pushd ${RELEASE_REPO}
cp -a ${PROJECT_REPO}/ios/sdk/out/JitsiMeetSDK.xcframework lite/Frameworks/
# Add all files to git
if [[ $DO_GIT_TAG == 1 ]]; then
git add -A .
git commit -m "${SDK_VERSION} lite"
git tag "${SDK_VERSION}-lite"
fi
git add -A .
git commit --allow-empty -m "${SDK_VERSION} lite"
git tag "${SDK_VERSION}-lite"
popd

View File

@@ -1,14 +1,12 @@
#!/bin/bash
set -e -u
set -e -u -x
THIS_DIR=$(cd -P "$(dirname "$(readlink "${BASH_SOURCE[0]}" || echo "${BASH_SOURCE[0]}")")" && pwd)
PROJECT_REPO=$(realpath ${THIS_DIR}/../..)
RELEASE_REPO=$(realpath ${THIS_DIR}/../../../jitsi-meet-ios-sdk-releases)
DEFAULT_SDK_VERSION=$(/usr/libexec/PlistBuddy -c "Print CFBundleShortVersionString" ${THIS_DIR}/../sdk/src/Info.plist)
SDK_VERSION=${OVERRIDE_SDK_VERSION:-${DEFAULT_SDK_VERSION}}
DO_GIT_TAG=${GIT_TAG:-0}
echo "Releasing Jitsi Meet SDK ${SDK_VERSION}"
@@ -50,9 +48,6 @@ xcodebuild -create-xcframework \
-framework ios/sdk/out/ios-device.xcarchive/Products/Library/Frameworks/JitsiMeetSDK.framework \
-framework ios/sdk/out/ios-simulator.xcarchive/Products/Library/Frameworks/JitsiMeetSDK.framework \
-output ios/sdk/out/JitsiMeetSDK.xcframework
if [[ $DO_GIT_TAG == 1 ]]; then
git tag ios-sdk-${SDK_VERSION}
fi
popd
pushd ${RELEASE_REPO}
@@ -61,11 +56,9 @@ pushd ${RELEASE_REPO}
cp -a ${PROJECT_REPO}/ios/sdk/out/JitsiMeetSDK.xcframework Frameworks/
# Add all files to git
if [[ $DO_GIT_TAG == 1 ]]; then
git add -A .
git commit -m "${SDK_VERSION}"
git tag ${SDK_VERSION}
fi
git add -A .
git commit --allow-empty -m "${SDK_VERSION}"
git tag "${SDK_VERSION}"
popd

View File

@@ -786,6 +786,7 @@
"newDeviceAction": "Use",
"newDeviceAudioTitle": "New audio device detected",
"newDeviceCameraTitle": "New camera detected",
"nextToSpeak": "You are the next in line to speak",
"noiseSuppressionDesktopAudioDescription": "Noise suppression can't be enabled while sharing desktop audio, please disable it and try again.",
"noiseSuppressionFailedTitle": "Failed to start noise suppression",
"noiseSuppressionStereoDescription": "Stereo audio noise suppression is not currently supported.",
@@ -820,6 +821,7 @@
"videoUnmuteBlockedDescription": "Camera unmute and desktop sharing operation have been temporarily blocked because of system limits.",
"videoUnmuteBlockedTitle": "Camera unmute and desktop sharing blocked!",
"viewLobby": "View lobby",
"viewParticipants": "View participants",
"viewVisitors": "View visitors",
"waitingParticipants": "{{waitingParticipants}} people",
"waitingVisitors": "Visitors waiting in queue: {{waitingVisitors}}",
@@ -1502,7 +1504,6 @@
"labelTooltip": "Number of visitors: {{count}}",
"notification": {
"demoteDescription": "Sent here by {{actor}}, raise your hand to participate",
"description": "To participate raise your hand",
"noMainParticipantsDescription": "A participant needs to start the meeting. Please try again in a bit.",
"noMainParticipantsTitle": "This meeting hasnt started yet.",
"noVisitorLobby": "You cannot join while there is a lobby enabled for the meeting.",

View File

@@ -1338,14 +1338,14 @@ class API {
* @returns {void}
*/
notifyReceivedChatMessage(
{ body, id, nick, privateMessage, ts } = {}) {
if (APP.conference.isLocalId(id)) {
{ body, from, nick, privateMessage, ts } = {}) {
if (APP.conference.isLocalId(from)) {
return;
}
this._sendEvent({
name: 'incoming-message',
from: id,
from,
message: body,
nick,
privateMessage,
@@ -1814,9 +1814,9 @@ class API {
* Notify external application of a participant, remote or local, being
* removed from the conference by another participant.
*
* @param {string} kicked - The ID of the participant removed from the
* @param {Object} kicked - The participant removed from the
* conference.
* @param {string} kicker - The ID of the participant that removed the
* @param {Object} kicker - The participant that removed the
* other participant.
* @returns {void}
*/

56
package-lock.json generated
View File

@@ -61,7 +61,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/v1844.0.0+a9b6dd7e/lib-jitsi-meet.tgz",
"lib-jitsi-meet": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v1847.0.0+03eef6de/lib-jitsi-meet.tgz",
"lodash": "4.17.21",
"moment": "2.29.4",
"moment-duration-format": "2.2.2",
@@ -82,15 +82,15 @@
"react-native-default-preference": "1.4.4",
"react-native-device-info": "10.9.0",
"react-native-dialog": "https://github.com/jitsi/react-native-dialog/releases/download/v9.2.2-jitsi.1/react-native-dialog-9.2.2.tgz",
"react-native-gesture-handler": "2.17.1",
"react-native-gesture-handler": "2.18.1",
"react-native-get-random-values": "1.9.0",
"react-native-immersive-mode": "2.0.1",
"react-native-immersive-mode": "2.0.2",
"react-native-keep-awake": "4.0.0",
"react-native-orientation-locker": "1.6.0",
"react-native-pager-view": "6.2.0",
"react-native-paper": "5.10.3",
"react-native-performance": "5.0.0",
"react-native-safe-area-context": "4.7.1",
"react-native-safe-area-context": "4.10.8",
"react-native-screens": "3.32.0",
"react-native-sound": "0.11.2",
"react-native-splash-screen": "3.3.0",
@@ -12489,8 +12489,8 @@
},
"node_modules/lib-jitsi-meet": {
"version": "0.0.0",
"resolved": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v1844.0.0+a9b6dd7e/lib-jitsi-meet.tgz",
"integrity": "sha512-ZLOF2lmTgMU+eEltT7UyZ9LY16g+IjA4sWfdFafbTxKub99Db8/B56vfeN3AEXy4ldtslifhgP2Cx0x+CX3Esg==",
"resolved": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v1847.0.0+03eef6de/lib-jitsi-meet.tgz",
"integrity": "sha512-HnJCDEFRNFQcgG3JeF7+xYrAdwYQt/HZwe9QhuMhEMXOiL2oSj/prWuAWTq1Ew2OtGKQVHEO8ntfYnG9M9vy6g==",
"hasInstallScript": true,
"license": "Apache-2.0",
"dependencies": {
@@ -15307,9 +15307,9 @@
}
},
"node_modules/react-native-gesture-handler": {
"version": "2.17.1",
"resolved": "https://registry.npmjs.org/react-native-gesture-handler/-/react-native-gesture-handler-2.17.1.tgz",
"integrity": "sha512-pWfniN6NuVKUq40KACuD3NCMe+bWNQCpD3cmxL6aLSCTwPKYmf7l4Lp0/E/almpjvxhybJZtFLU0w4tmxnIKaA==",
"version": "2.18.1",
"resolved": "https://registry.npmjs.org/react-native-gesture-handler/-/react-native-gesture-handler-2.18.1.tgz",
"integrity": "sha512-WF2fxQ5kTaxHghlkBM4YxO86SyGWVwrSNgJ1E8z/ZtL2xD5B3bg5agvuVFfOzvceC114yq71s6E9vKPz94ZxRw==",
"license": "MIT",
"dependencies": {
"@egjs/hammerjs": "^2.0.17",
@@ -15334,9 +15334,10 @@
}
},
"node_modules/react-native-immersive-mode": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/react-native-immersive-mode/-/react-native-immersive-mode-2.0.1.tgz",
"integrity": "sha512-2wlL7VIHl4rr4gwgnUp9K1UvsN7J5VCGqoAvBWQXvB4xn7XaoDEl6z9vqaqOiEdC6aAh2d/7zqcJz+dfcR2ELw==",
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/react-native-immersive-mode/-/react-native-immersive-mode-2.0.2.tgz",
"integrity": "sha512-W4YBpf/seURr6pdt91bKWtlQhwoZYgQN1nllj/y+EkwKs5xbu/IiRxlCs/8ML7dyA8wbOFaXR24AC4HOrvxapw==",
"license": "MIT",
"peerDependencies": {
"react-native": ">=0.60.5"
}
@@ -15350,6 +15351,7 @@
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/react-native-orientation-locker/-/react-native-orientation-locker-1.6.0.tgz",
"integrity": "sha512-D3IOtAcaAi6S2w0Y1EUnr16I47isosQbE7F67fAu9A+gE67NkyKaJ9HL5EsZ+Uc7+7m+NsuBjx3dxuANNy8rVA==",
"license": "MIT",
"peerDependencies": {
"react": ">=16.13.1",
"react-native": ">=0.63.2",
@@ -15365,6 +15367,7 @@
"version": "6.2.0",
"resolved": "https://registry.npmjs.org/react-native-pager-view/-/react-native-pager-view-6.2.0.tgz",
"integrity": "sha512-pf9OnL/Tkr+5s4Gjmsn7xh91PtJLDa6qxYa/bmtUhd/+s4cQdWQ8DIFoOFghwZIHHHwVdWtoXkp6HtpjN+r20g==",
"license": "MIT",
"peerDependencies": {
"react": "*",
"react-native": "*"
@@ -15395,9 +15398,10 @@
}
},
"node_modules/react-native-safe-area-context": {
"version": "4.7.1",
"resolved": "https://registry.npmjs.org/react-native-safe-area-context/-/react-native-safe-area-context-4.7.1.tgz",
"integrity": "sha512-X2pJG2ttmAbiGlItWedvDkZg1T1ikmEDiz+7HsiIwAIm2UbFqlhqn+B1JF53mSxPzdNaDcCQVHRNPvj8oFu6Yg==",
"version": "4.10.8",
"resolved": "https://registry.npmjs.org/react-native-safe-area-context/-/react-native-safe-area-context-4.10.8.tgz",
"integrity": "sha512-Jx1lovhvIdYygg0UsMCBUJN0Wvj9GlA5bbcBLzjZf93uJpNHzaiHC4hR280+sNVK1+/pMHEyEkXVHDZE5JWn0w==",
"license": "MIT",
"peerDependencies": {
"react": "*",
"react-native": "*"
@@ -27986,8 +27990,8 @@
}
},
"lib-jitsi-meet": {
"version": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v1844.0.0+a9b6dd7e/lib-jitsi-meet.tgz",
"integrity": "sha512-ZLOF2lmTgMU+eEltT7UyZ9LY16g+IjA4sWfdFafbTxKub99Db8/B56vfeN3AEXy4ldtslifhgP2Cx0x+CX3Esg==",
"version": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v1847.0.0+03eef6de/lib-jitsi-meet.tgz",
"integrity": "sha512-HnJCDEFRNFQcgG3JeF7+xYrAdwYQt/HZwe9QhuMhEMXOiL2oSj/prWuAWTq1Ew2OtGKQVHEO8ntfYnG9M9vy6g==",
"requires": {
"@jitsi/js-utils": "2.2.1",
"@jitsi/logger": "2.0.2",
@@ -30105,9 +30109,9 @@
"integrity": "sha512-MKbuBbovO8eGiAM9i6o0nrdBXivhRpzPQ+aVBXGJEPMH7RrCSNUKaCoEpkjfGHlTxjZimi6WjDCjjzCRSHlV1A=="
},
"react-native-gesture-handler": {
"version": "2.17.1",
"resolved": "https://registry.npmjs.org/react-native-gesture-handler/-/react-native-gesture-handler-2.17.1.tgz",
"integrity": "sha512-pWfniN6NuVKUq40KACuD3NCMe+bWNQCpD3cmxL6aLSCTwPKYmf7l4Lp0/E/almpjvxhybJZtFLU0w4tmxnIKaA==",
"version": "2.18.1",
"resolved": "https://registry.npmjs.org/react-native-gesture-handler/-/react-native-gesture-handler-2.18.1.tgz",
"integrity": "sha512-WF2fxQ5kTaxHghlkBM4YxO86SyGWVwrSNgJ1E8z/ZtL2xD5B3bg5agvuVFfOzvceC114yq71s6E9vKPz94ZxRw==",
"requires": {
"@egjs/hammerjs": "^2.0.17",
"hoist-non-react-statics": "^3.3.0",
@@ -30124,9 +30128,9 @@
}
},
"react-native-immersive-mode": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/react-native-immersive-mode/-/react-native-immersive-mode-2.0.1.tgz",
"integrity": "sha512-2wlL7VIHl4rr4gwgnUp9K1UvsN7J5VCGqoAvBWQXvB4xn7XaoDEl6z9vqaqOiEdC6aAh2d/7zqcJz+dfcR2ELw=="
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/react-native-immersive-mode/-/react-native-immersive-mode-2.0.2.tgz",
"integrity": "sha512-W4YBpf/seURr6pdt91bKWtlQhwoZYgQN1nllj/y+EkwKs5xbu/IiRxlCs/8ML7dyA8wbOFaXR24AC4HOrvxapw=="
},
"react-native-keep-awake": {
"version": "4.0.0",
@@ -30159,9 +30163,9 @@
"integrity": "sha512-nZJdliNFaA7vmu9PbYGSlwguRQVsuWX8CtchUUxZvjd/i9B3OYYveajIcuic3oQjWyTydLeDlhXRwpOUlj2Npw=="
},
"react-native-safe-area-context": {
"version": "4.7.1",
"resolved": "https://registry.npmjs.org/react-native-safe-area-context/-/react-native-safe-area-context-4.7.1.tgz",
"integrity": "sha512-X2pJG2ttmAbiGlItWedvDkZg1T1ikmEDiz+7HsiIwAIm2UbFqlhqn+B1JF53mSxPzdNaDcCQVHRNPvj8oFu6Yg=="
"version": "4.10.8",
"resolved": "https://registry.npmjs.org/react-native-safe-area-context/-/react-native-safe-area-context-4.10.8.tgz",
"integrity": "sha512-Jx1lovhvIdYygg0UsMCBUJN0Wvj9GlA5bbcBLzjZf93uJpNHzaiHC4hR280+sNVK1+/pMHEyEkXVHDZE5JWn0w=="
},
"react-native-screens": {
"version": "3.32.0",

View File

@@ -67,7 +67,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/v1844.0.0+a9b6dd7e/lib-jitsi-meet.tgz",
"lib-jitsi-meet": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v1847.0.0+03eef6de/lib-jitsi-meet.tgz",
"lodash": "4.17.21",
"moment": "2.29.4",
"moment-duration-format": "2.2.2",
@@ -88,15 +88,15 @@
"react-native-default-preference": "1.4.4",
"react-native-device-info": "10.9.0",
"react-native-dialog": "https://github.com/jitsi/react-native-dialog/releases/download/v9.2.2-jitsi.1/react-native-dialog-9.2.2.tgz",
"react-native-gesture-handler": "2.17.1",
"react-native-gesture-handler": "2.18.1",
"react-native-get-random-values": "1.9.0",
"react-native-immersive-mode": "2.0.1",
"react-native-immersive-mode": "2.0.2",
"react-native-keep-awake": "4.0.0",
"react-native-orientation-locker": "1.6.0",
"react-native-pager-view": "6.2.0",
"react-native-paper": "5.10.3",
"react-native-performance": "5.0.0",
"react-native-safe-area-context": "4.7.1",
"react-native-safe-area-context": "4.10.8",
"react-native-screens": "3.32.0",
"react-native-sound": "0.11.2",
"react-native-splash-screen": "3.3.0",

View File

@@ -48,6 +48,11 @@ export interface IProps {
*/
colorBase?: string;
/**
* Indicates the default icon for the avatar.
*/
defaultIcon?: string;
/**
* Display name of the entity to render an avatar for (if any). This is handy when we need
* an avatar for a non-participant entity (e.g. A recent list item).
@@ -112,6 +117,7 @@ class Avatar<P extends IProps> extends PureComponent<P, IState> {
* @static
*/
static defaultProps = {
defaultIcon: IconUser,
dynamicColor: true
};
@@ -172,6 +178,7 @@ class Avatar<P extends IProps> extends PureComponent<P, IState> {
_loadableAvatarUrlUseCORS,
className,
colorBase,
defaultIcon,
dynamicColor,
id,
size,
@@ -229,7 +236,7 @@ class Avatar<P extends IProps> extends PureComponent<P, IState> {
}
if (navigator.product !== 'ReactNative') {
avatarProps.iconUser = IconUser;
avatarProps.iconUser = defaultIcon;
}
return (

View File

@@ -477,6 +477,7 @@ export interface IConfig {
};
pcStatsInterval?: number;
peopleSearchQueryTypes?: string[];
peopleSearchTokenLocation?: string;
peopleSearchUrl?: string;
preferBosh?: boolean;
preferVisitor?: boolean;
@@ -487,6 +488,12 @@ export interface IConfig {
hideExtraJoinButtons?: Array<string>;
};
prejoinPageEnabled?: boolean;
raisedHands?: {
disableLowerHandByModerator?: boolean;
disableLowerHandNotification?: boolean;
disableNextSpeakerNotification?: boolean;
disableRemoveRaisedHandOnFocus?: boolean;
};
readOnlyName?: boolean;
recordingLimit?: {
appName?: string;

View File

@@ -201,6 +201,7 @@ export default [
'preferVisitor',
'prejoinConfig',
'prejoinPageEnabled',
'raisedHands',
'recordingService',
'requireDisplayName',
'remoteVideoMenu',

View File

@@ -6,6 +6,7 @@ import { safeJsonParse } from '@jitsi/js-utils/json';
import _ from 'lodash';
import { IReduxState } from '../../app/types';
import { getLocalParticipant } from '../participants/functions';
import { parseURLParams } from '../util/parseURLParams';
import { IConfig } from './configType';
@@ -88,7 +89,37 @@ export function getFeatureFlag(state: IReduxState, featureFlag: string) {
* @returns {boolean}
*/
export function getDisableRemoveRaisedHandOnFocus(state: IReduxState) {
return state['features/base/config']?.disableRemoveRaisedHandOnFocus || false;
return state['features/base/config']?.raisedHands?.disableRemoveRaisedHandOnFocus || false;
}
/**
* Selector used to get the disableLowerHandByModerator.
*
* @param {Object} state - The global state.
* @returns {boolean}
*/
export function getDisableLowerHandByModerator(state: IReduxState) {
return state['features/base/config']?.raisedHands?.disableLowerHandByModerator || false;
}
/**
* Selector used to get the disableLowerHandNotification.
*
* @param {Object} state - The global state.
* @returns {boolean}
*/
export function getDisableLowerHandNotification(state: IReduxState) {
return state['features/base/config']?.raisedHands?.disableLowerHandNotification || true;
}
/**
* Selector used to get the disableNextSpeakerNotification.
*
* @param {Object} state - The global state.
* @returns {boolean}
*/
export function getDisableNextSpeakerNotification(state: IReduxState) {
return state['features/base/config']?.raisedHands?.disableNextSpeakerNotification || false;
}
/**
@@ -184,6 +215,31 @@ export function isNameReadOnly(state: IReduxState): boolean {
|| state['features/base/config'].readOnlyName);
}
/**
* Selector for determining if the participant is the next one in the queue to speak.
*
* @param {Object} state - The state of the app.
* @returns {boolean}
*/
export function isNextToSpeak(state: IReduxState): boolean {
const raisedHandsQueue = state['features/base/participants'].raisedHandsQueue || [];
const participantId = getLocalParticipant(state)?.id;
return participantId === raisedHandsQueue[0]?.id;
}
/**
* Selector for determining if the next to speak participant in the queue has been notified.
*
* @param {Object} state - The state of the app.
* @returns {boolean}
*/
export function hasBeenNotified(state: IReduxState): boolean {
const raisedHandsQueue = state['features/base/participants'].raisedHandsQueue;
return Boolean(raisedHandsQueue[0]?.hasBeenNotified);
}
/**
* Selector for determining if the display name is visible.
*

View File

@@ -442,6 +442,12 @@ function _translateLegacyConfig(oldValue: IConfig) {
newValue.disabledSounds.unshift('INCOMING_MSG_SOUND');
}
newValue.raisedHands = newValue.raisedHands || {};
if (oldValue.disableRemoveRaisedHandOnFocus) {
newValue.raisedHands.disableRemoveRaisedHandOnFocus = oldValue.disableRemoveRaisedHandOnFocus;
}
if (oldValue.stereo || oldValue.opusMaxAverageBitrate) {
newValue.audioQuality = {
opusMaxAverageBitrate: oldValue.audioQuality?.opusMaxAverageBitrate ?? oldValue.opusMaxAverageBitrate,

View File

@@ -59,7 +59,7 @@ export default {
function normalizeLanguage(language: string) {
const [ lang, variant ] = language.replace('_', '-').split('-');
if (!variant || lang === variant) {
if (!variant || lang.toUpperCase() === variant.toUpperCase()) {
return lang;
}

View File

@@ -46,7 +46,7 @@ export function getJwtName(state: IReduxState) {
* @param {string} feature - The feature we want to check.
* @param {boolean} ifNoToken - Default value if there is no token.
* @param {boolean} ifNotInFeatures - Default value if features prop exists but does not have the {@code feature}.
* @returns {bolean}
* @returns {boolean}
*/
export function isJwtFeatureEnabled(state: IReduxState, feature: string, ifNoToken = false, ifNotInFeatures = false) {
const { jwt } = state['features/base/jwt'];

View File

@@ -1,3 +1,12 @@
/**
* Create an action to mark the participant as notified to speak next.
*
* {
* type: NOTIFIED_TO_SPEAK
* }
*/
export const NOTIFIED_TO_SPEAK = 'NOTIFIED_TO_SPEAK';
/**
* Create an action for when dominant speaker changes.
*

View File

@@ -544,23 +544,27 @@ export function createVirtualScreenshareParticipant(sourceName: string, local: b
*/
export function participantKicked(kicker: any, kicked: any) {
return (dispatch: IStore['dispatch'], getState: IStore['getState']) => {
const state = getState();
const localParticipant = getLocalParticipant(state);
const kickedId = kicked.getId();
const kickerId = kicker.getId();
dispatch({
type: PARTICIPANT_KICKED,
kicked: kicked.getId(),
kicker: kicker?.getId()
kicked: kickedId,
kicker: kickerId
});
if (kicked.isReplaced?.()) {
if (kicked.isReplaced?.() || kickerId === localParticipant?.id) {
return;
}
dispatch(showNotification({
titleArguments: {
kicked:
getParticipantDisplayName(getState, kicked.getId()),
getParticipantDisplayName(state, kickedId),
kicker:
getParticipantDisplayName(getState, kicker.getId())
getParticipantDisplayName(state, kickerId)
},
titleKey: 'notify.kickParticipant'
}, NOTIFICATION_TIMEOUT_TYPE.MEDIUM));

View File

@@ -14,6 +14,7 @@ import {
NOTIFICATION_TIMEOUT_TYPE,
RAISE_HAND_NOTIFICATION_ID
} from '../../notifications/constants';
import { open as openParticipantsPane } from '../../participants-pane/actions';
import { isForceMuted } from '../../participants-pane/functions';
import { CALLING, INVITED } from '../../presence-status/constants';
import { RAISE_HAND_SOUND_ID } from '../../reactions/constants';
@@ -783,10 +784,19 @@ function _raiseHandUpdated({ dispatch, getState }: IStore, conference: IJitsiCon
|| isForceMuted(participant, MEDIA_TYPE.VIDEO, state);
}
const action = shouldDisplayAllowAction ? {
customActionNameKey: [ 'notify.allowAction' ],
customActionHandler: [ () => dispatch(approveParticipant(participantId)) ]
} : {};
let action;
if (shouldDisplayAllowAction) {
action = {
customActionNameKey: [ 'notify.allowAction' ],
customActionHandler: [ () => dispatch(approveParticipant(participantId)) ]
};
} else {
action = {
customActionNameKey: [ 'notify.viewParticipants' ],
customActionHandler: [ () => dispatch(openParticipantsPane()) ]
};
}
if (raisedHandTimestamp) {
let notificationTitle;
@@ -810,7 +820,7 @@ function _raiseHandUpdated({ dispatch, getState }: IStore, conference: IJitsiCon
concatText: true,
uid: RAISE_HAND_NOTIFICATION_ID,
...action
}, shouldDisplayAllowAction ? NOTIFICATION_TIMEOUT_TYPE.MEDIUM : NOTIFICATION_TIMEOUT_TYPE.SHORT));
}, NOTIFICATION_TIMEOUT_TYPE.MEDIUM));
dispatch(playSound(RAISE_HAND_SOUND_ID));
}
}

View File

@@ -6,6 +6,7 @@ import { set } from '../redux/functions';
import {
DOMINANT_SPEAKER_CHANGED,
NOTIFIED_TO_SPEAK,
OVERWRITE_PARTICIPANT_NAME,
PARTICIPANT_ID_CHANGED,
PARTICIPANT_JOINED,
@@ -92,7 +93,7 @@ export interface IParticipantsState {
numberOfParticipantsNotSupportingE2EE: number;
overwrittenNameList: { [id: string]: string; };
pinnedParticipant?: string;
raisedHandsQueue: Array<{ id: string; raisedHandTimestamp: number; }>;
raisedHandsQueue: Array<{ hasBeenNotified?: boolean; id: string; raisedHandTimestamp: number; }>;
remote: Map<string, IParticipant>;
remoteVideoSources: Set<string>;
sortedRemoteParticipants: Map<string, string>;
@@ -114,6 +115,23 @@ export interface IParticipantsState {
ReducerRegistry.register<IParticipantsState>('features/base/participants',
(state = DEFAULT_STATE, action): IParticipantsState => {
switch (action.type) {
case NOTIFIED_TO_SPEAK: {
return {
...state,
raisedHandsQueue: state.raisedHandsQueue.map((item, index) => {
if (index === 0) {
return {
...item,
hasBeenNotified: true
};
}
return item;
})
};
}
case PARTICIPANT_ID_CHANGED: {
const { local } = state;

View File

@@ -1,11 +1,20 @@
import _ from 'lodash';
import { batch } from 'react-redux';
import { IStore } from '../../app/types';
import { hideNotification, showNotification } from '../../notifications/actions';
import { NOTIFICATION_TIMEOUT_TYPE, RAISE_HAND_NOTIFICATION_ID } from '../../notifications/constants';
import { getCurrentConference } from '../conference/functions';
import { getSsrcRewritingFeatureFlag } from '../config/functions.any';
import {
getDisableNextSpeakerNotification,
getSsrcRewritingFeatureFlag,
hasBeenNotified,
isNextToSpeak } from '../config/functions.any';
import { VIDEO_TYPE } from '../media/constants';
import StateListenerRegistry from '../redux/StateListenerRegistry';
import { NOTIFIED_TO_SPEAK } from './actionTypes';
import { createVirtualScreenshareParticipant, participantLeft } from './actions';
import {
getParticipantById,
@@ -25,6 +34,21 @@ StateListenerRegistry.register(
&& _updateScreenshareParticipantsBasedOnPresence(store)
);
StateListenerRegistry.register(
/* selector */ state => state['features/base/participants'].raisedHandsQueue,
/* listener */ (raisedHandsQueue, store) => {
if (raisedHandsQueue.length
&& isNextToSpeak(store.getState())
&& !hasBeenNotified(store.getState())
&& !getDisableNextSpeakerNotification(store.getState())) {
_notifyNextSpeakerInRaisedHandQueue(store);
}
if (!raisedHandsQueue[0]) {
store.dispatch(hideNotification(RAISE_HAND_NOTIFICATION_ID));
}
}
);
/**
* Compares the old and new screenshare lists provided and creates/removes the virtual screenshare participant
* tiles accodingly.
@@ -121,3 +145,23 @@ function _updateScreenshareParticipantsBasedOnPresence(store: IStore): void {
_createOrRemoveVirtualParticipants(previousScreenshareSourceNames, currentScreenshareSourceNames, store);
}
/**
* Handles notifying the next speaker in the raised hand queue.
*
* @param {*} store - The redux store.
* @returns {void}
*/
function _notifyNextSpeakerInRaisedHandQueue(store: IStore): void {
const { dispatch } = store;
batch(() => {
dispatch(showNotification({
titleKey: 'notify.nextToSpeak',
maxLines: 2
}, NOTIFICATION_TIMEOUT_TYPE.MEDIUM));
dispatch({
type: NOTIFIED_TO_SPEAK
});
});
}

View File

@@ -83,12 +83,12 @@ MiddlewareRegistry.register(({ dispatch, getState }) => next => action => {
const { messages } = getState()['features/chat'];
messages?.forEach(m => {
if (m.messageType === MESSAGE_TYPE_REMOTE && !getParticipantById(getState(), m.id)) {
if (m.messageType === MESSAGE_TYPE_REMOTE && !getParticipantById(getState(), m.participantId)) {
const rooms: IRooms = action.rooms;
for (const room of Object.values(rooms)) {
const participants = room.participants || {};
const matchedJid = Object.keys(participants).find(jid => jid.endsWith(m.id));
const matchedJid = Object.keys(participants).find(jid => jid.endsWith(m.participantId));
if (matchedJid) {
m.displayName = participants[matchedJid].displayName;

View File

@@ -36,13 +36,13 @@ export default class AbstractMessageContainer<P extends IProps, S> extends Compo
for (let i = 0; i < messagesCount; i++) {
const message = this.props.messages[i];
if (message.id === currentGroupParticipantId) {
if (message.participantId === currentGroupParticipantId) {
currentGrouping.push(message);
} else {
currentGrouping.length && groups.push(currentGrouping);
currentGrouping = [ message ];
currentGroupParticipantId = message.id;
currentGroupParticipantId = message.participantId;
}
}

View File

@@ -7,7 +7,7 @@ import Avatar from '../../../base/avatar/components/Avatar';
import { translate } from '../../../base/i18n/functions';
import Linkify from '../../../base/react/components/native/Linkify';
import { isGifMessage } from '../../../gifs/functions.native';
import { MESSAGE_TYPE_ERROR, MESSAGE_TYPE_LOCAL } from '../../constants';
import { CHAR_LIMIT, MESSAGE_TYPE_ERROR, MESSAGE_TYPE_LOCAL } from '../../constants';
import {
getCanReplyToMessage,
getFormattedTimestamp,
@@ -73,7 +73,7 @@ class ChatMessage extends Component<IChatMessageProps> {
messageBubbleStyle.push(styles.lobbyMessageBubble);
}
const messageText = replaceNonUnicodeEmojis(getMessageText(this.props.message));
const messageText = getMessageText(this.props.message);
return (
<View style = { styles.messageWrapper as ViewStyle } >
@@ -84,13 +84,7 @@ class ChatMessage extends Component<IChatMessageProps> {
{ this._renderDisplayName() }
{ isGifMessage(messageText)
? <GifMessage message = { messageText } />
: (
<Linkify
linkStyle = { styles.chatLink }
style = { styles.chatMessage }>
{ messageText }
</Linkify>
)}
: this._renderMessageTextComponent(messageText) }
{ this._renderPrivateNotice() }
</View>
{ this._renderPrivateReplyButton() }
@@ -104,7 +98,7 @@ class ChatMessage extends Component<IChatMessageProps> {
/**
* Renders the avatar of the sender.
*
* @returns {React$Element<*>}
* @returns {React.ReactElement<*>}
*/
_renderAvatar() {
const { message } = this.props;
@@ -113,7 +107,7 @@ class ChatMessage extends Component<IChatMessageProps> {
<View style = { styles.avatarWrapper }>
{ this.props.showAvatar && <Avatar
displayName = { message.displayName }
participantId = { message.id }
participantId = { message.participantId }
size = { styles.avatarWrapper.width } />
}
</View>
@@ -123,7 +117,7 @@ class ChatMessage extends Component<IChatMessageProps> {
/**
* Renders the display name of the sender if necessary.
*
* @returns {React$Element<*> | null}
* @returns {React.ReactElement<*> | null}
*/
_renderDisplayName() {
const { message, showDisplayName } = this.props;
@@ -139,10 +133,37 @@ class ChatMessage extends Component<IChatMessageProps> {
);
}
/**
* Renders the message text based on number of characters.
*
* @param {string} messageText - The message text.
* @returns {React.ReactElement<*>}
*/
_renderMessageTextComponent(messageText: string) {
if (messageText.length >= CHAR_LIMIT) {
return (
<Text
selectable = { true }
style = { styles.chatMessage }>
{ messageText }
</Text>
);
}
return (
<Linkify
linkStyle = { styles.chatLink }
style = { styles.chatMessage }>
{ replaceNonUnicodeEmojis(messageText) }
</Linkify>
);
}
/**
* Renders the message privacy notice, if necessary.
*
* @returns {React$Element<*> | null}
* @returns {React.ReactElement<*> | null}
*/
_renderPrivateNotice() {
const { message, knocking } = this.props;
@@ -161,7 +182,7 @@ class ChatMessage extends Component<IChatMessageProps> {
/**
* Renders the private reply button, if necessary.
*
* @returns {React$Element<*> | null}
* @returns {React.ReactElement<*> | null}
*/
_renderPrivateReplyButton() {
const { message, canReply } = this.props;
@@ -175,7 +196,7 @@ class ChatMessage extends Component<IChatMessageProps> {
<View style = { styles.replyContainer as ViewStyle }>
<PrivateMessageButton
isLobbyMessage = { lobbyChat }
participantID = { message.id }
participantID = { message.participantId }
reply = { true }
showLabel = { false }
toggledStyles = { styles.replyStyles } />
@@ -186,7 +207,7 @@ class ChatMessage extends Component<IChatMessageProps> {
/**
* Renders the time at which the message was sent, if necessary.
*
* @returns {React$Element<*> | null}
* @returns {React.ReactElement<*> | null}
*/
_renderTimestamp() {
if (!this.props.showTimestamp) {
@@ -205,6 +226,7 @@ class ChatMessage extends Component<IChatMessageProps> {
* Maps part of the redux state to the props of this component.
*
* @param {Object} state - The Redux state.
* @param {IChatMessageProps} message - Message object.
* @returns {IProps}
*/
function _mapStateToProps(state: IReduxState, { message }: IChatMessageProps) {

View File

@@ -197,7 +197,7 @@ const ChatMessage = ({
className = { classes.replyButtonContainer }>
<PrivateMessageButton
isLobbyMessage = { message.lobbyChat }
participantID = { message.id } />
participantID = { message.participantId } />
</div>
)}
</div>

View File

@@ -66,7 +66,7 @@ const ChatMessageGroup = ({ className = '', messages }: IProps) => {
<div className = { clsx(classes.groupContainer, className) }>
<Avatar
className = { clsx(classes.avatar, 'avatar') }
participantId = { messages[0].id }
participantId = { messages[0].participantId }
size = { 32 } />
<div className = { `${classes.messageGroup} chat-message-group ${className}` }>
{messages.map((message, i) => (

View File

@@ -1,3 +1,8 @@
/**
* Maximum number of characters allowed.
*/
export const CHAR_LIMIT = 500;
/**
* The size of the chat. Equal to $sidebarWidth SCSS variable.
*/

View File

@@ -172,7 +172,7 @@ export function getMessageText(message: IMessage) {
*/
export function getCanReplyToMessage(state: IReduxState, message: IMessage) {
const { knocking } = state['features/lobby'];
const participant = getParticipantById(state, message.id);
const participant = getParticipantById(state, message.participantId);
return Boolean(participant)
&& (message.privateMessage || (message.lobbyChat && !knocking))

View File

@@ -125,7 +125,7 @@ MiddlewareRegistry.register(store => next => action => {
store.dispatch(pushReactions(data.reactions));
_handleReceivedMessage(store, {
id: participant.getId(),
participantId: participant.getId(),
message: getReactionMessageFromBuffer(data.reactions),
privateMessage: false,
lobbyChat: false,
@@ -137,12 +137,12 @@ MiddlewareRegistry.register(store => next => action => {
}
case NON_PARTICIPANT_MESSAGE_RECEIVED: {
const { id, json: data } = action;
const { participantId, json: data } = action;
if (data?.type === MESSAGE_TYPE_SYSTEM && data.message) {
_handleReceivedMessage(store, {
displayName: data.displayName ?? i18next.t('chat.systemDisplayName'),
id,
participantId,
lobbyChat: false,
message: data.message,
privateMessage: true,
@@ -213,7 +213,7 @@ MiddlewareRegistry.register(store => next => action => {
case ADD_REACTION_MESSAGE: {
if (localParticipant?.id) {
_handleReceivedMessage(store, {
id: localParticipant.id,
participantId: localParticipant.id,
message: action.message,
privateMessage: false,
timestamp: Date.now(),
@@ -274,25 +274,30 @@ function _addChatMsgListener(conference: IJitsiConference, store: IStore) {
conference.on(
JitsiConferenceEvents.MESSAGE_RECEIVED,
// eslint-disable-next-line max-params
(id: string, message: string, timestamp: number, displayName: string, isGuest?: boolean) => {
/* eslint-disable max-params */
(participantId: string, message: string, timestamp: number,
displayName: string, isGuest: boolean, messageId: string) => {
/* eslint-enable max-params */
_onConferenceMessageReceived(store, {
id: id || displayName, // in case of messages coming from visitors we can have unknown id
// in case of messages coming from visitors we can have unknown id
participantId: participantId || displayName,
message,
timestamp,
displayName,
isGuest,
messageId,
privateMessage: false });
}
);
conference.on(
JitsiConferenceEvents.PRIVATE_MESSAGE_RECEIVED,
(id: string, message: string, timestamp: number) => {
(participantId: string, message: string, timestamp: number, messageId: string) => {
_onConferenceMessageReceived(store, {
id,
participantId,
message,
timestamp,
messageId,
privateMessage: true
});
}
@@ -311,25 +316,29 @@ function _addChatMsgListener(conference: IJitsiConference, store: IStore) {
* @param {Object} message - The message object.
* @returns {void}
*/
function _onConferenceMessageReceived(store: IStore, { displayName, id, isGuest, message, timestamp, privateMessage }: {
displayName?: string; id: string; isGuest?: boolean;
message: string; privateMessage: boolean; timestamp: number; }) {
function _onConferenceMessageReceived(store: IStore,
{ displayName, isGuest, message, messageId, participantId, privateMessage, timestamp }: {
displayName?: string; isGuest?: boolean; message: string; messageId?: string;
participantId: string; privateMessage: boolean; timestamp: number; }
) {
const isGif = isGifMessage(message);
if (isGif) {
_handleGifMessageReceived(store, id, message);
_handleGifMessageReceived(store, participantId, message);
if (getGifDisplayMode(store.getState()) === 'tile') {
return;
}
}
_handleReceivedMessage(store, {
displayName,
id,
isGuest,
participantId,
message,
privateMessage,
lobbyChat: false,
timestamp
timestamp,
messageId
}, true, isGif);
}
@@ -337,14 +346,14 @@ function _onConferenceMessageReceived(store: IStore, { displayName, id, isGuest,
* Handles a received gif message.
*
* @param {Object} store - Redux store.
* @param {string} id - Id of the participant that sent the message.
* @param {string} participantId - Id of the participant that sent the message.
* @param {string} message - The message sent.
* @returns {void}
*/
function _handleGifMessageReceived(store: IStore, id: string, message: string) {
function _handleGifMessageReceived(store: IStore, participantId: string, message: string) {
const url = message.substring(GIF_PREFIX.length, message.length - 1);
store.dispatch(addGif(id, url));
store.dispatch(addGif(participantId, url));
}
/**
@@ -374,7 +383,7 @@ function _handleChatError({ dispatch }: IStore, error: Error) {
export function handleLobbyMessageReceived(message: string, participantId: string) {
return async (dispatch: IStore['dispatch'], getState: IStore['getState']) => {
_handleReceivedMessage({ dispatch,
getState }, { id: participantId,
getState }, { participantId,
message,
privateMessage: false,
lobbyChat: true,
@@ -387,18 +396,18 @@ export function handleLobbyMessageReceived(message: string, participantId: strin
* Function to get lobby chat user display name.
*
* @param {Store} state - The Redux store.
* @param {string} id - The knocking participant id.
* @param {string} participantId - The knocking participant id.
* @returns {string}
*/
function getLobbyChatDisplayName(state: IReduxState, id: string) {
function getLobbyChatDisplayName(state: IReduxState, participantId: string) {
const { knockingParticipants } = state['features/lobby'];
const { lobbyMessageRecipient } = state['features/chat'];
if (id === lobbyMessageRecipient?.id) {
if (participantId === lobbyMessageRecipient?.id) {
return lobbyMessageRecipient.name;
}
const knockingParticipant = knockingParticipants.find(p => p.id === id);
const knockingParticipant = knockingParticipants.find(p => p.id === participantId);
if (knockingParticipant) {
return knockingParticipant.name;
@@ -417,9 +426,9 @@ function getLobbyChatDisplayName(state: IReduxState, id: string) {
* @returns {void}
*/
function _handleReceivedMessage({ dispatch, getState }: IStore,
{ displayName, id, isGuest, message, privateMessage, timestamp, lobbyChat }: {
displayName?: string; id: string; isGuest?: boolean; lobbyChat: boolean;
message: string; privateMessage: boolean; timestamp: number; },
{ displayName, isGuest, lobbyChat, message, messageId, participantId, privateMessage, timestamp }: {
displayName?: string; isGuest?: boolean; lobbyChat: boolean; message: string;
messageId?: string; participantId: string; privateMessage: boolean; timestamp: number; },
shouldPlaySound = true,
isReaction = false
) {
@@ -434,12 +443,12 @@ function _handleReceivedMessage({ dispatch, getState }: IStore,
// Provide a default for the case when a message is being
// backfilled for a participant that has left the conference.
const participant = getParticipantById(state, id) || { local: undefined };
const participant = getParticipantById(state, participantId) || { local: undefined };
const localParticipant = getLocalParticipant(getState);
let displayNameToShow = lobbyChat
? getLobbyChatDisplayName(state, id)
: displayName || getParticipantDisplayName(state, id);
? getLobbyChatDisplayName(state, participantId)
: displayName || getParticipantDisplayName(state, participantId);
const hasRead = participant.local || isChatOpen;
const timestampToDate = timestamp ? new Date(timestamp) : new Date();
const millisecondsTimestamp = timestampToDate.getTime();
@@ -455,13 +464,14 @@ function _handleReceivedMessage({ dispatch, getState }: IStore,
dispatch(addMessage({
displayName: displayNameToShow,
hasRead,
id,
participantId,
messageType: participant.local ? MESSAGE_TYPE_LOCAL : MESSAGE_TYPE_REMOTE,
message,
privateMessage,
lobbyChat,
recipient: getParticipantDisplayName(state, localParticipant?.id ?? ''),
timestamp: millisecondsTimestamp,
messageId,
isReaction
}));
@@ -477,7 +487,7 @@ function _handleReceivedMessage({ dispatch, getState }: IStore,
APP.API.notifyReceivedChatMessage({
body: message,
id,
from: participantId,
nick: displayNameToShow,
privateMessage,
ts: timestamp
@@ -512,7 +522,7 @@ function _persistSentPrivateMessage({ dispatch, getState }: IStore, recipientID:
dispatch(addMessage({
displayName,
hasRead: true,
id: localParticipant.id,
participantId: localParticipant.id,
messageType: MESSAGE_TYPE_LOCAL,
message,
privateMessage: !isLobbyPrivateMessage,
@@ -562,7 +572,7 @@ function _shouldSendPrivateMessageTo(state: IReduxState, action: AnyAction) {
if (lastMessage.privateMessage) {
// We show the notice if the last received message was private.
return lastMessage.id;
return lastMessage.participantId;
}
// But messages may come rapidly, we want to protect our users from mis-sending a message
@@ -577,7 +587,7 @@ function _shouldSendPrivateMessageTo(state: IReduxState, action: AnyAction) {
? recentPrivateMessages[0] : recentPrivateMessages[recentPrivateMessages.length - 1];
if (recentPrivateMessage) {
return recentPrivateMessage.id;
return recentPrivateMessage.participantId;
}
return undefined;

View File

@@ -1,5 +1,3 @@
import { v4 as uuidv4 } from 'uuid';
import { ILocalParticipant, IParticipant } from '../base/participants/types';
import ReducerRegistry from '../base/redux/ReducerRegistry';
@@ -48,9 +46,9 @@ ReducerRegistry.register<IChatState>('features/chat', (state = DEFAULT_STATE, ac
const newMessage: IMessage = {
displayName: action.displayName,
error: action.error,
id: action.id,
participantId: action.participantId,
isReaction: action.isReaction,
messageId: uuidv4(),
messageId: action.messageId,
messageType: action.messageType,
message: action.message,
privateMessage: action.privateMessage,

View File

@@ -5,12 +5,12 @@ import { IStore } from '../app/types';
export interface IMessage {
displayName: string;
error?: Object;
id: string;
isReaction: boolean;
lobbyChat: boolean;
message: string;
messageId: string;
messageType: string;
participantId: string;
privateMessage: boolean;
recipient: string;
timestamp: number;

View File

@@ -13,6 +13,7 @@ import {
ENDPOINT_MESSAGE_RECEIVED
} from '../base/conference/actionTypes';
import { getCurrentConference } from '../base/conference/functions';
import { getDisableLowerHandByModerator } from '../base/config/functions.any';
import { getURLWithoutParamsNormalized } from '../base/connection/utils';
import { hideDialog } from '../base/dialog/actions';
import { isDialogOpen } from '../base/dialog/functions';
@@ -75,9 +76,11 @@ MiddlewareRegistry.register(store => next => action => {
}
case ENDPOINT_MESSAGE_RECEIVED: {
const { participant, data } = action;
const { dispatch } = store;
const { dispatch, getState } = store;
if (data.name === LOWER_HAND_MESSAGE && participant.isModerator()) {
if (data.name === LOWER_HAND_MESSAGE
&& participant.isModerator()
&& !getDisableLowerHandByModerator(getState())) {
dispatch(raiseHand(false));
}
break;

View File

@@ -21,7 +21,8 @@ import {
import {
getDominantSpeakerParticipant,
getLocalParticipant,
getParticipantById
getParticipantById,
getParticipantDisplayName
} from '../base/participants/functions';
import MiddlewareRegistry from '../base/redux/MiddlewareRegistry';
import { getBaseUrl } from '../base/util/helpers';
@@ -133,15 +134,29 @@ MiddlewareRegistry.register(store => next => action => {
APP.API.notifyDataChannelOpened();
break;
case KICKED_OUT:
case KICKED_OUT: {
const state = store.getState();
const localParticipant = getLocalParticipant(state);
if (!localParticipant) {
break;
}
const pId = action.participant.getId();
APP.API.notifyKickedOut(
{
id: getLocalParticipant(store.getState())?.id,
id: localParticipant.id,
name: getParticipantDisplayName(state, localParticipant.id),
local: true
},
{ id: action.participant ? action.participant.getId() : undefined }
{
id: pId,
name: getParticipantDisplayName(state, pId)
}
);
break;
}
case NOTIFY_CAMERA_ERROR:
if (action.error) {
@@ -156,14 +171,28 @@ MiddlewareRegistry.register(store => next => action => {
}
break;
case PARTICIPANT_KICKED:
case PARTICIPANT_KICKED: {
const state = store.getState();
const kickedParticipant = getParticipantById(state, action.kicked);
const kickerParticipant = getParticipantById(state, action.kicker);
if (!kickerParticipant || !kickedParticipant) {
break;
}
APP.API.notifyKickedOut(
{
id: action.kicked,
local: false
id: kickedParticipant.id,
local: kickedParticipant.local,
name: getParticipantDisplayName(state, kickedParticipant.id)
},
{ id: action.kicker });
{
id: kickerParticipant.id,
local: kickerParticipant.local,
name: getParticipantDisplayName(state, kickerParticipant.id)
});
break;
}
case PARTICIPANT_LEFT: {
const { participant } = action;

View File

@@ -82,14 +82,19 @@ export function invite(
const { conference, password } = state['features/base/conference'];
if (typeof conference === 'undefined') {
// Only keep invitees which can get an invite request from Jitsi UI
const jitsiInvitees = invitees.filter(({ type }) => type !== INVITE_TYPES.EMAIL);
// Invite will fail before CONFERENCE_JOIN. The request will be
// cached in order to be executed on CONFERENCE_JOIN.
return new Promise(resolve => {
dispatch(addPendingInviteRequest({
invitees,
callback: (failedInvitees: any) => resolve(failedInvitees)
}));
});
if (jitsiInvitees.length) {
return new Promise(resolve => {
dispatch(addPendingInviteRequest({
invitees: jitsiInvitees,
callback: (failedInvitees: any) => resolve(failedInvitees)
}));
});
}
}
let allInvitePromises: Promise<any>[] = [];
@@ -112,10 +117,12 @@ export function invite(
// For each number, dial out. On success, remove the number from
// {@link invitesLeftToSend}.
const phoneInvitePromises = phoneNumbers.map(item => {
const numberToInvite = item.number;
const phoneInvitePromises = typeof conference === 'undefined'
? []
: phoneNumbers.map(item => {
const numberToInvite = item.number;
return conference.dial(numberToInvite)
return conference.dial(numberToInvite)
.then(() => {
invitesLeftToSend
= invitesLeftToSend.filter(
@@ -123,13 +130,13 @@ export function invite(
})
.catch((error: Error) =>
logger.error('Error inviting phone number:', error));
});
});
allInvitePromises = allInvitePromises.concat(phoneInvitePromises);
const usersAndRooms
= invitesLeftToSend.filter(
({ type }) => [ INVITE_TYPES.USER, INVITE_TYPES.ROOM ].includes(type));
({ type }) => [ INVITE_TYPES.USER, INVITE_TYPES.EMAIL, INVITE_TYPES.ROOM ].includes(type));
if (usersAndRooms.length) {
// Send a request to invite all the rooms and users. On success,
@@ -139,12 +146,12 @@ export function invite(
(callFlowsEnabled
? inviteServiceCallFlowsUrl : inviteServiceUrl) ?? '',
inviteUrl,
jwt,
usersAndRooms)
usersAndRooms,
state)
.then(() => {
invitesLeftToSend
= invitesLeftToSend.filter(
({ type }) => ![ INVITE_TYPES.USER, INVITE_TYPES.ROOM ].includes(type));
({ type }) => ![ INVITE_TYPES.USER, INVITE_TYPES.EMAIL, INVITE_TYPES.ROOM ].includes(type));
})
.catch(error => {
dispatch(setCalleeInfoVisible(false));

View File

@@ -61,6 +61,11 @@ export interface IProps {
*/
_peopleSearchQueryTypes: Array<string>;
/**
* The localStorage key holding the alternative token for people directory.
*/
_peopleSearchTokenLocation: string;
/**
* The URL pointing to the service allowing for people search.
*/
@@ -254,6 +259,7 @@ export default class AbstractAddPeopleDialog<P extends IProps, S extends IState>
_jwt: jwt,
_peopleSearchQueryTypes: peopleSearchQueryTypes,
_peopleSearchUrl: peopleSearchUrl,
_peopleSearchTokenLocation: peopleSearchTokenLocation,
_region: region,
_sipInviteEnabled: sipInviteEnabled
} = this.props;
@@ -266,6 +272,7 @@ export default class AbstractAddPeopleDialog<P extends IProps, S extends IState>
jwt,
peopleSearchQueryTypes,
peopleSearchUrl,
peopleSearchTokenLocation,
region,
sipInviteEnabled
};
@@ -295,7 +302,8 @@ export function _mapStateToProps(state: IReduxState) {
dialOutAuthUrl,
dialOutRegionUrl,
peopleSearchQueryTypes,
peopleSearchUrl
peopleSearchUrl,
peopleSearchTokenLocation
} = state['features/base/config'];
return {
@@ -308,6 +316,7 @@ export function _mapStateToProps(state: IReduxState) {
_jwt: state['features/base/jwt'].jwt ?? '',
_peopleSearchQueryTypes: peopleSearchQueryTypes ?? [],
_peopleSearchUrl: peopleSearchUrl ?? '',
_peopleSearchTokenLocation: peopleSearchTokenLocation ?? '',
_region: getMeetingRegion(state),
_sipInviteEnabled: isSipInviteEnabled(state)
};

View File

@@ -19,6 +19,7 @@ import Icon from '../../../../base/icons/components/Icon';
import {
IconCheck,
IconCloseCircle,
IconEnvelope,
IconPhoneRinging,
IconSearch,
IconShare
@@ -260,6 +261,12 @@ class AddPeopleDialog extends AbstractAddPeopleDialog<IProps, IState> {
key: item.id || item.user_id,
title: item.name
};
case INVITE_TYPES.EMAIL:
return {
avatar: item.avatar || IconEnvelope,
key: item.id || item.user_id,
title: item.name
};
default:
return null;
}
@@ -273,7 +280,11 @@ class AddPeopleDialog extends AbstractAddPeopleDialog<IProps, IState> {
* @returns {string}
*/
_keyExtractor(item: any) {
return item.type === INVITE_TYPES.USER ? item.id || item.user_id : item.number;
if (item.type === INVITE_TYPES.USER || item.type === INVITE_TYPES.EMAIL) {
return item.id || item.user_id;
}
return item.number;
}
/**
@@ -451,6 +462,7 @@ class AddPeopleDialog extends AbstractAddPeopleDialog<IProps, IState> {
selected = inviteItems.find(_.matchesProperty('number', item.number));
break;
case INVITE_TYPES.USER:
case INVITE_TYPES.EMAIL:
selected = item.id
? inviteItems.find(_.matchesProperty('id', item.id))
: inviteItems.find(_.matchesProperty('user_id', item.user_id));

View File

@@ -7,7 +7,7 @@ import { IReduxState, IStore } from '../../../../app/types';
import Avatar from '../../../../base/avatar/components/Avatar';
import { translate } from '../../../../base/i18n/functions';
import Icon from '../../../../base/icons/components/Icon';
import { IconPhoneRinging } from '../../../../base/icons/svg';
import { IconEnvelope, IconPhoneRinging, IconUser } from '../../../../base/icons/svg';
import MultiSelectAutocomplete from '../../../../base/react/components/web/MultiSelectAutocomplete';
import Button from '../../../../base/ui/components/web/Button';
import { BUTTON_TYPES } from '../../../../base/ui/constants.any';
@@ -302,9 +302,12 @@ class InviteContactsForm extends AbstractAddPeopleDialog<IProps, IState> {
* @returns {ReactElement}
*/
_getAvatar(user: any, className = 'avatar-small') {
const defaultIcon = user.type === INVITE_TYPES.EMAIL ? IconEnvelope : IconUser;
return (
<Avatar
className = { className }
defaultIcon = { defaultIcon }
size = { 32 }
status = { user.status }
url = { user.avatar } />
@@ -325,7 +328,7 @@ class InviteContactsForm extends AbstractAddPeopleDialog<IProps, IState> {
_parseQueryResults(response: IInvitee[] = []) {
const { t, _dialOutEnabled } = this.props;
const userTypes = [ INVITE_TYPES.USER, INVITE_TYPES.VIDEO_ROOM, INVITE_TYPES.ROOM ];
const userTypes = [ INVITE_TYPES.USER, INVITE_TYPES.EMAIL, INVITE_TYPES.VIDEO_ROOM, INVITE_TYPES.ROOM ];
const users = response.filter(item => userTypes.includes(item.type));
const userDisplayItems: any = [];

View File

@@ -48,6 +48,7 @@ export const SIP_ADDRESS_REGEX = /^[+a-zA-Z0-9]+(?:([^\s>:@]+)(?::([^\s@>]+))?@)
* Different invite types mapping.
*/
export const INVITE_TYPES = {
EMAIL: 'email',
PHONE: 'phone',
ROOM: 'room',
SIP: 'sip',

View File

@@ -1,3 +1,6 @@
// @ts-expect-error
import { jitsiLocalStorage } from '@jitsi/js-utils';
import { IReduxState } from '../app/types';
import { IStateful } from '../base/app/types';
import { getRoomName } from '../base/conference/functions';
@@ -143,6 +146,11 @@ export type GetInviteResultsOptions = {
*/
peopleSearchQueryTypes: Array<string>;
/**
* Key in localStorage holding the alternative token for people directory.
*/
peopleSearchTokenLocation?: string;
/**
* The url to query for people.
*/
@@ -181,6 +189,7 @@ export function getInviteResultsForQuery(
dialOutEnabled,
peopleSearchQueryTypes,
peopleSearchUrl,
peopleSearchTokenLocation,
region,
sipInviteEnabled,
jwt
@@ -193,7 +202,8 @@ export function getInviteResultsForQuery(
peopleSearchUrl,
jwt,
text,
peopleSearchQueryTypes);
peopleSearchQueryTypes,
peopleSearchTokenLocation);
} else {
peopleSearchPromise = Promise.resolve([]);
}
@@ -411,29 +421,40 @@ export function getInviteTypeCounts(inviteItems: IInvitee[] = []) {
* @param {string} inviteServiceUrl - The invite service that generates the
* invitation.
* @param {string} inviteUrl - The url to the conference.
* @param {string} jwt - The jwt token to pass to the search service.
* @param {Immutable.List} inviteItems - The list of the "user" or "room" type
* items to invite.
* @param {IReduxState} state - Global state.
* @returns {Promise} - The promise created by the request.
*/
export function invitePeopleAndChatRooms(
inviteServiceUrl: string,
inviteUrl: string,
jwt: string,
inviteItems: Array<Object>
inviteItems: Array<Object>,
state: IReduxState
): Promise<any> {
if (!inviteItems || inviteItems.length === 0) {
return Promise.resolve();
}
// Parse all the query strings of the search directory endpoint
const { jwt = '' } = state['features/base/jwt'];
const { peopleSearchTokenLocation } = state['features/base/config'];
let token = jwt;
// If token is empty, check for alternate token
if (!token && peopleSearchTokenLocation) {
token = jitsiLocalStorage.getItem(peopleSearchTokenLocation) ?? '';
}
const headers = {
...jwt ? { 'Authorization': `Bearer ${jwt}` } : {},
...token ? { 'Authorization': `Bearer ${token}` } : {},
'Content-Type': 'application/json'
};
return fetch(
`${inviteServiceUrl}`,
inviteServiceUrl,
{
body: JSON.stringify({
'invited': inviteItems,
@@ -452,9 +473,14 @@ export function invitePeopleAndChatRooms(
* @returns {boolean} Indication of whether adding people is currently enabled.
*/
export function isAddPeopleEnabled(state: IReduxState): boolean {
const { peopleSearchUrl } = state['features/base/config'];
const {
peopleSearchUrl,
peopleSearchTokenLocation
} = state['features/base/config'];
return Boolean(state['features/base/jwt'].jwt && Boolean(peopleSearchUrl) && !isVpaasMeeting(state));
const hasToken = Boolean(state['features/base/jwt'].jwt || Boolean(peopleSearchTokenLocation));
return Boolean(hasToken && Boolean(peopleSearchUrl) && !isVpaasMeeting(state));
}
/**
@@ -534,21 +560,30 @@ function isPhoneNumberRegex(): RegExp {
* @param {string} jwt - The jwt token to pass to the search service.
* @param {string} text - Text to search.
* @param {Array<string>} queryTypes - Array with the query types that will be
* executed - "conferenceRooms" | "user" | "room".
* executed - "conferenceRooms" | "user" | "room" | "email".
* @param {string} peopleSearchTokenLocation - The localStorage key holding the token value for alternate auth.
* @returns {Promise} - The promise created by the request.
*/
export function searchDirectory( // eslint-disable-line max-params
serviceUrl: string,
jwt: string,
text: string,
queryTypes: Array<string> = [ 'conferenceRooms', 'user', 'room' ]
queryTypes: Array<string> = [ 'conferenceRooms', 'user', 'room', 'email' ],
peopleSearchTokenLocation?: string
): Promise<Array<{ type: string; }>> {
const query = encodeURIComponent(text);
const queryTypesString = encodeURIComponent(JSON.stringify(queryTypes));
let token = jwt;
// If token is empty, check for alternate token
if (!token && peopleSearchTokenLocation) {
token = jitsiLocalStorage.getItem(peopleSearchTokenLocation) ?? '';
}
const headers = {
...jwt ? { 'Authorization': `Bearer ${jwt}` } : {}
...token ? { 'Authorization': `Bearer ${token}` } : {}
};
return fetch(`${serviceUrl}?query=${query}&queryTypes=${

View File

@@ -18,14 +18,3 @@ export const READY_TO_CLOSE = 'READY_TO_CLOSE';
*/
export const SCREEN_SHARE_PARTICIPANTS_UPDATED
= 'SCREEN_SHARE_PARTICIPANTS_UPDATED';
/**
* The type of (redux) action which signals that a custom button from the overflow menu was pressed.
*
* @returns {{
* type: CUSTOM_OVERFLOW_MENU_BUTTON_PRESSED,
* id: string,
* text: string
* }}
*/
export const CUSTOM_OVERFLOW_MENU_BUTTON_PRESSED = 'CUSTOM_OVERFLOW_MENU_BUTTON_PRESSED';

View File

@@ -1,8 +1,4 @@
import {
CUSTOM_OVERFLOW_MENU_BUTTON_PRESSED,
READY_TO_CLOSE,
SCREEN_SHARE_PARTICIPANTS_UPDATED
} from './actionTypes';
import { READY_TO_CLOSE, SCREEN_SHARE_PARTICIPANTS_UPDATED } from './actionTypes';
/**
@@ -35,22 +31,3 @@ export function setParticipantsWithScreenShare(participantIds: Array<string>) {
participantIds
};
}
/**
* Creates a (redux) action which that a custom overflow menu button was pressed.
*
* @param {string} id - The id for the custom button.
* @param {string} text - The label for the custom button.
* @returns {{
* type: CUSTOM_OVERFLOW_MENU_BUTTON_PRESSED,
* id: string,
* text: string
* }}
*/
export function customOverflowMenuButtonPressed(id: string, text: string) {
return {
type: CUSTOM_OVERFLOW_MENU_BUTTON_PRESSED,
id,
text
};
}

View File

@@ -52,15 +52,13 @@ import { ITrack } from '../../base/tracks/types';
import { CLOSE_CHAT, OPEN_CHAT } from '../../chat/actionTypes';
import { closeChat, openChat, sendMessage, setPrivateMessageRecipient } from '../../chat/actions.native';
import { setRequestingSubtitles } from '../../subtitles/actions.any';
import { CUSTOM_OVERFLOW_MENU_BUTTON_PRESSED } from '../../toolbox/actionTypes';
import { muteLocal } from '../../video-menu/actions.native';
import { ENTER_PICTURE_IN_PICTURE } from '../picture-in-picture/actionTypes';
// @ts-ignore
import { isExternalAPIAvailable } from '../react-native-sdk/functions';
import {
CUSTOM_OVERFLOW_MENU_BUTTON_PRESSED,
READY_TO_CLOSE
} from './actionTypes';
import { READY_TO_CLOSE } from './actionTypes';
import { setParticipantsWithScreenShare } from './actions';
import { participantToParticipantInfo, sendEvent } from './functions';
import logger from './logger';
@@ -82,12 +80,6 @@ const CHAT_TOGGLED = 'CHAT_TOGGLED';
*/
const CONFERENCE_TERMINATED = 'CONFERENCE_TERMINATED';
/**
* Event which will be emitted on the native side to indicate that the custom overflow menu button was pressed.
*/
const CUSTOM_MENU_BUTTON_PRESSED = 'CUSTOM_MENU_BUTTON_PRESSED';
/**
* Event which will be emitted on the native side to indicate a message was received
* through the channel.
@@ -199,7 +191,7 @@ externalAPIEnabled && MiddlewareRegistry.register(store => next => action => {
sendEvent(
store,
CUSTOM_MENU_BUTTON_PRESSED,
CUSTOM_OVERFLOW_MENU_BUTTON_PRESSED,
{
id,
text

View File

@@ -14,6 +14,7 @@ import BaseTheme from '../../../base/ui/components/BaseTheme.native';
import Button from '../../../base/ui/components/native/Button';
import IconButton from '../../../base/ui/components/native/IconButton';
import { BUTTON_MODES, BUTTON_TYPES } from '../../../base/ui/constants.native';
import { CHAR_LIMIT } from '../../../chat/constants';
import { replaceNonUnicodeEmojis } from '../../../chat/functions';
import { NOTIFICATION_ICON, NOTIFICATION_TYPE } from '../../constants';
import { INotificationProps } from '../../types';
@@ -153,6 +154,7 @@ const Notification = ({
return (
<>
<Text
numberOfLines = { 1 }
style = { styles.contentTextTitle as TextStyle }>
{ titleText }
</Text>
@@ -160,8 +162,9 @@ const Notification = ({
descriptionArray.map((line, index) => (
<Text
key = { index }
numberOfLines = { 3 }
style = { styles.contentText }>
{ replaceNonUnicodeEmojis(line) }
{ line.length >= CHAR_LIMIT ? line : replaceNonUnicodeEmojis(line) }
</Text>
))
}
@@ -170,7 +173,9 @@ const Notification = ({
}
return (
<Text style = { styles.contentTextTitle as TextStyle }>
<Text
numberOfLines = { 1 }
style = { styles.contentTextTitle as TextStyle }>
{ titleText }
</Text>
);

View File

@@ -13,7 +13,6 @@ const notification = {
borderLeftWidth: BaseTheme.spacing[1],
display: 'flex',
flexDirection: 'row',
height: 'auto',
marginVertical: BaseTheme.spacing[1],
maxWidth: 416,
width: '100%'

View File

@@ -7,6 +7,17 @@
*/
export const CLEAR_TOOLBOX_TIMEOUT = 'CLEAR_TOOLBOX_TIMEOUT';
/**
* The type of (redux) action which signals that a custom button from the overflow menu was pressed.
*
* @returns {{
* type: CUSTOM_OVERFLOW_MENU_BUTTON_PRESSED,
* id: string,
* text: string
* }}
*/
export const CUSTOM_OVERFLOW_MENU_BUTTON_PRESSED = 'CUSTOM_OVERFLOW_MENU_BUTTON_PRESSED';
/**
* The type of (redux) action which updates whether the conference is or is not
* currently in full screen view.

View File

@@ -1,3 +1,5 @@
import { CUSTOM_OVERFLOW_MENU_BUTTON_PRESSED } from './actionTypes';
export * from './actions.any';
/**
@@ -22,3 +24,22 @@ export function showToolbox(_timeout?: number): any {
export function setOverflowMenuVisible(_visible: boolean): any {
return {};
}
/**
* Creates a (redux) action which that a custom overflow menu button was pressed.
*
* @param {string} id - The id for the custom button.
* @param {string} text - The label for the custom button.
* @returns {{
* type: CUSTOM_OVERFLOW_MENU_BUTTON_PRESSED,
* id: string,
* text: string
* }}
*/
export function customOverflowMenuButtonPressed(id: string, text: string) {
return {
type: CUSTOM_OVERFLOW_MENU_BUTTON_PRESSED,
id,
text
};
}

View File

@@ -11,7 +11,6 @@ import SettingsButton from '../../../base/settings/components/native/SettingsBut
import BreakoutRoomsButton
from '../../../breakout-rooms/components/native/BreakoutRoomsButton';
import SharedDocumentButton from '../../../etherpad/components/SharedDocumentButton.native';
import { customOverflowMenuButtonPressed } from '../../../mobile/external-api/actions';
import ReactionMenu from '../../../reactions/components/native/ReactionMenu';
import { shouldDisplayReactionsButtons } from '../../../reactions/functions.any';
import LiveStreamButton from '../../../recording/components/LiveStream/native/LiveStreamButton';
@@ -25,6 +24,7 @@ import ClosedCaptionButton from '../../../subtitles/components/native/ClosedCapt
import TileViewButton from '../../../video-layout/components/TileViewButton';
import styles from '../../../video-menu/components/native/styles';
import WhiteboardButton from '../../../whiteboard/components/native/WhiteboardButton';
import { customOverflowMenuButtonPressed } from '../../actions.native';
import { getMovableButtons } from '../../functions.native';
import AudioOnlyButton from './AudioOnlyButton';

View File

@@ -9,10 +9,10 @@ import { IconArrowUp } from '../../../base/icons/svg';
import JitsiMeetJS from '../../../base/lib-jitsi-meet/_';
import { IGUMPendingState } from '../../../base/media/types';
import ToolboxButtonWithIcon from '../../../base/toolbox/components/web/ToolboxButtonWithIcon';
import { toggleAudioSettings } from '../../../settings/actions';
import { toggleAudioSettings } from '../../../settings/actions.web';
import AudioSettingsPopup from '../../../settings/components/web/audio/AudioSettingsPopup';
import { getAudioSettingsVisibility } from '../../../settings/functions';
import { isAudioSettingsButtonDisabled } from '../../functions';
import { getAudioSettingsVisibility } from '../../../settings/functions.web';
import { isAudioSettingsButtonDisabled } from '../../functions.web';
import AudioMuteButton from './AudioMuteButton';

View File

@@ -10,17 +10,17 @@ import AbstractButton, { IProps as AbstractButtonProps } from '../../../base/too
import { startScreenShareFlow } from '../../../screen-share/actions.web';
import { isScreenVideoShared } from '../../../screen-share/functions';
import { closeOverflowMenuIfOpen } from '../../actions.web';
import { isDesktopShareButtonDisabled } from '../../functions';
import { isDesktopShareButtonDisabled } from '../../functions.web';
interface IProps extends AbstractButtonProps {
/**
* Whether or not screensharing is initialized.
* Whether or not screen-sharing is initialized.
*/
_desktopSharingEnabled: boolean;
/**
* Whether or not the local participant is screensharing.
* Whether or not the local participant is screen-sharing.
*/
_screensharing: boolean;
}
@@ -101,7 +101,7 @@ class ShareDesktopButton extends AbstractButton<IProps> {
* @returns {Object}
*/
const mapStateToProps = (state: IReduxState) => {
// Disable the screenshare button if the video sender limit is reached and there is no video or media share in
// Disable the screen-share button if the video sender limit is reached and there is no video or media share in
// progress.
const desktopSharingEnabled
= JitsiMeetJS.isDesktopSharingEnabled() && !isDesktopShareButtonDisabled(state);

View File

@@ -20,7 +20,7 @@ import { useFeedbackButton } from '../feedback/hooks.web';
import { setGifMenuVisibility } from '../gifs/actions';
import { isGifEnabled } from '../gifs/function.any';
import InviteButton from '../invite/components/add-people-dialog/web/InviteButton';
import { registerShortcut, unregisterShortcut } from '../keyboard-shortcuts/actions.any';
import { registerShortcut, unregisterShortcut } from '../keyboard-shortcuts/actions.web';
import { useKeyboardShortcutsButton } from '../keyboard-shortcuts/hooks.web';
import NoiseSuppressionButton from '../noise-suppression/components/NoiseSuppressionButton';
import {
@@ -37,13 +37,13 @@ import { toggleReactionsMenuVisibility } from '../reactions/actions.web';
import RaiseHandContainerButton from '../reactions/components/web/RaiseHandContainerButtons';
import { REACTIONS } from '../reactions/constants';
import { shouldDisplayReactionsButtons } from '../reactions/functions.any';
import { useReactionsButton } from '../reactions/hooks';
import { useReactionsButton } from '../reactions/hooks.web';
import { useLiveStreamingButton, useRecordingButton } from '../recording/hooks.web';
import { isSalesforceEnabled } from '../salesforce/functions';
import { startScreenShareFlow } from '../screen-share/actions.web';
import ShareAudioButton from '../screen-share/components/web/ShareAudioButton';
import { isScreenAudioSupported, isScreenVideoShared } from '../screen-share/functions';
import { useSecurityDialogButton } from '../security/hooks';
import { useSecurityDialogButton } from '../security/hooks.web';
import SettingsButton from '../settings/components/web/SettingsButton';
import SharedVideoButton from '../shared-video/components/web/SharedVideoButton';
import SpeakerStats from '../speaker-stats/components/web/SpeakerStats';
@@ -51,7 +51,7 @@ import { isSpeakerStatsDisabled } from '../speaker-stats/functions';
import { useSpeakerStatsButton } from '../speaker-stats/hooks.web';
import { useClosedCaptionButton } from '../subtitles/hooks.web';
import { toggleTileView } from '../video-layout/actions.any';
import { shouldDisplayTileView } from '../video-layout/functions.any';
import { shouldDisplayTileView } from '../video-layout/functions.web';
import { useTileViewButton } from '../video-layout/hooks';
import VideoQualityButton from '../video-quality/components/VideoQualityButton.web';
import VideoQualityDialog from '../video-quality/components/VideoQualityDialog.web';

View File

@@ -108,4 +108,3 @@ export function muteAllParticipants(exclude: Array<string>, mediaType: MediaType
});
};
}

View File

@@ -72,26 +72,26 @@ MiddlewareRegistry.register(({ dispatch, getState }) => next => action => {
const { conference } = action;
if (getState()['features/visitors'].iAmVisitor) {
dispatch(openDialog(JoinMeetingDialog));
const { demoteActorDisplayName } = getState()['features/visitors'];
dispatch(setVisitorDemoteActor(undefined));
const notificationParams: INotificationProps = {
titleKey: 'visitors.notification.title',
descriptionKey: 'visitors.notification.description'
};
if (demoteActorDisplayName) {
notificationParams.descriptionKey = 'visitors.notification.demoteDescription';
notificationParams.descriptionArguments = {
actor: demoteActorDisplayName
const notificationParams: INotificationProps = {
titleKey: 'visitors.notification.title',
descriptionKey: 'visitors.notification.demoteDescription',
descriptionArguments: {
actor: demoteActorDisplayName
}
};
batch(() => {
dispatch(showNotification(notificationParams, NOTIFICATION_TIMEOUT_TYPE.STICKY));
dispatch(setVisitorDemoteActor(undefined));
});
} else {
dispatch(openDialog(JoinMeetingDialog));
}
// check for demote actor and update notification
dispatch(showNotification(notificationParams, NOTIFICATION_TIMEOUT_TYPE.STICKY));
} else {
dispatch(setVisitorsSupported(conference.isVisitorsSupported()));
conference.on(JitsiConferenceEvents.VISITORS_SUPPORTED_CHANGED, (value: boolean) => {

View File

@@ -69,10 +69,12 @@ MiddlewareRegistry.register((store: IStore) => (next: Function) => (action: AnyA
const existingCollabDetails = getCollabDetails(state);
const enforceUserLimit = shouldEnforceUserLimit(state);
const notifyUserLimit = shouldNotifyUserLimit(state);
const iAmRecorder = Boolean(state['features/base/config'].iAmRecorder);
if (enforceUserLimit) {
dispatch(restrictWhiteboard(false));
dispatch(openDialog(WhiteboardLimitDialog));
iAmRecorder && setTimeout(() => dispatch(hideDialog(WhiteboardLimitDialog)), 3000);
return next(action);
}

View File

@@ -51,6 +51,10 @@ globalNS.connectionTimes = {
'index.loaded': window.indexLoadedTime
};
window.addEventListener('load', () => {
globalNS.connectionTimes['window.loaded'] = window.loadedEventTime;
});
document.addEventListener('DOMContentLoaded', () => {
const now = window.performance.now();

View File

@@ -11,6 +11,9 @@ local muc = module:depends("muc");
local NS_NICK = 'http://jabber.org/protocol/nick';
local is_healthcheck_room = util.is_healthcheck_room;
local POLLS_LIMIT = 128;
local POLL_PAYLOAD_LIMIT = 1024;
-- Checks if the given stanza contains a JSON message,
-- and that the message type pertains to the polls feature.
-- If yes, returns the parsed message. Otherwise, returns nil.
@@ -22,6 +25,10 @@ local function get_poll_message(stanza)
if json_data == nil then
return nil;
end
if string.len(json_data) >= POLL_PAYLOAD_LIMIT then
module:log('error', 'Poll payload too large, discarding. Sender: %s to:%s', stanza.attr.from, stanza.attr.to);
return nil;
end
local data, error = json.decode(json_data);
if not data or (data.type ~= "new-poll" and data.type ~= "answer-poll") then
if error then
@@ -72,6 +79,7 @@ module:hook("muc-room-created", function(event)
room.polls = {
by_id = {};
order = {};
count = 0;
};
end);
@@ -100,6 +108,11 @@ module:hook("message/bare", function(event)
return
end
if room.polls.count >= POLLS_LIMIT then
module:log("error", "Too many polls created in %s", room.jid)
return
end
local answers = {}
local compact_answers = {}
for i, name in ipairs(data.answers) do
@@ -117,6 +130,7 @@ module:hook("message/bare", function(event)
room.polls.by_id[data.pollId] = poll
table.insert(room.polls.order, poll)
room.polls.count = room.polls.count + 1;
local pollData = {
event = event,