mirror of
https://gitcode.com/GitHub_Trending/ji/jitsi-meet.git
synced 2026-05-18 21:47:46 +00:00
Compare commits
26 Commits
8689
...
visitors-e
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a24ddf205c | ||
|
|
7a97d15e89 | ||
|
|
1acb99d763 | ||
|
|
adbe990867 | ||
|
|
a4367567ab | ||
|
|
7f56cbc4ce | ||
|
|
d636d084c8 | ||
|
|
298567be48 | ||
|
|
c233629e51 | ||
|
|
75b5702a7e | ||
|
|
540f01d47e | ||
|
|
5c7ed6a8b3 | ||
|
|
3c5d33fefa | ||
|
|
be04236834 | ||
|
|
ec1bfe73b3 | ||
|
|
d2ed9ffef6 | ||
|
|
6141ff78f8 | ||
|
|
c6a75fb9ed | ||
|
|
3438438219 | ||
|
|
7cedea6740 | ||
|
|
69f26c8a38 | ||
|
|
92a4750d0e | ||
|
|
370a884765 | ||
|
|
877fc98eef | ||
|
|
7bed0b36bd | ||
|
|
cd5aed37e9 |
@@ -117,6 +117,11 @@ var config = {
|
||||
|
||||
// Will replace ice candidates IPs with invalid ones in order to fail ice.
|
||||
// failICE: true,
|
||||
|
||||
// When running on Spot TV, this controls whether to show the recording consent dialog.
|
||||
// If false (default), Spot instances will not show the recording consent dialog.
|
||||
// If true, Spot instances will show the recording consent dialog like regular clients.
|
||||
// showSpotConsentDialog: false,
|
||||
},
|
||||
|
||||
// Disables moderator indicators.
|
||||
@@ -1126,10 +1131,6 @@ var config = {
|
||||
// The Amplitude APP Key:
|
||||
// amplitudeAPPKey: '<APP_KEY>',
|
||||
|
||||
// Enables Amplitude UTM tracking:
|
||||
// Default value is false.
|
||||
// amplitudeIncludeUTM: false,
|
||||
|
||||
// Obfuscates room name sent to analytics (amplitude, rtcstats)
|
||||
// Default value is false.
|
||||
// obfuscateRoomName: false,
|
||||
|
||||
@@ -109,6 +109,7 @@
|
||||
}
|
||||
},
|
||||
"chat": {
|
||||
"disabled": "L'invio di messaggi in chat è disabilitato.",
|
||||
"enter": "Entra nella conversazione",
|
||||
"error": "Errore: il tuo messaggio non è stato inviato. Motivo: {{error}}",
|
||||
"fieldPlaceHolder": "Scrivi qui il tuo messaggio",
|
||||
@@ -546,8 +547,10 @@
|
||||
"downloadFailedDescription": "Si prega di riprovare.",
|
||||
"downloadFailedTitle": "Download non riuscito",
|
||||
"downloadFile": "Download",
|
||||
"dragAndDrop": "Trascina e rilascia i file qui",
|
||||
"fileAlreadyUploaded": "Questo file è già stato caricato nella riunione",
|
||||
"dragAndDrop": "Trascina e rilascia i file qui o da qualsiasi altra parte nella schermata",
|
||||
"fileAlreadyUploaded": "Questo file è già stato caricato nella riunione.",
|
||||
"fileTooLargeDescription": "Assicurati che il file non superi {{ maxFileSize }}.",
|
||||
"fileTooLargeTitle": "Il file selezionato è troppo grande",
|
||||
"removeFile": "Rimuovi",
|
||||
"uploadFailedDescription": "Si prega di riprovare.",
|
||||
"uploadFailedTitle": "Caricamento non riuscito",
|
||||
@@ -906,6 +909,7 @@
|
||||
"visitorInQueue": " ({{count}} in attesa)",
|
||||
"visitorRequests": " ({{count}} richiesta/e)",
|
||||
"visitors": "Spettatori {{count}}",
|
||||
"visitorsList": "Spettatori ({{count}})",
|
||||
"waitingLobby": "({{count}}) in attesa"
|
||||
},
|
||||
"search": "Cerca partecipanti",
|
||||
@@ -946,7 +950,7 @@
|
||||
},
|
||||
"results": {
|
||||
"changeVote": "Cambia voto",
|
||||
"empty": "Non ci sono ancora sondaggi in questa riunione. Crea un sondaggio qui!",
|
||||
"empty": "Non ci sono ancora sondaggi in questa riunione.",
|
||||
"hideDetailedResults": "Nascondi dettagli",
|
||||
"showDetailedResults": "Mostra dettagli",
|
||||
"vote": "Voti"
|
||||
|
||||
@@ -906,9 +906,11 @@
|
||||
"headings": {
|
||||
"lobby": "Lobby ({{count}})",
|
||||
"participantsList": "Meeting participants ({{count}})",
|
||||
"viewerRequests": "Viewers requests {{count}}",
|
||||
"visitorInQueue": " (waiting {{count}})",
|
||||
"visitorRequests": " (requests {{count}})",
|
||||
"visitors": "Viewers {{count}}",
|
||||
"visitorsList": "Viewers ({{count}})",
|
||||
"waitingLobby": "Waiting in lobby ({{count}})"
|
||||
},
|
||||
"search": "Search participants",
|
||||
|
||||
26
package-lock.json
generated
26
package-lock.json
generated
@@ -63,7 +63,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/v2024.0.0+006b25e4/lib-jitsi-meet.tgz",
|
||||
"lib-jitsi-meet": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v2025.0.0+49eb29a8/lib-jitsi-meet.tgz",
|
||||
"lodash-es": "4.17.21",
|
||||
"null-loader": "4.0.1",
|
||||
"optional-require": "1.0.3",
|
||||
@@ -105,6 +105,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-window": "1.8.6",
|
||||
"react-youtube": "10.1.0",
|
||||
"redux": "4.0.4",
|
||||
@@ -17748,8 +17749,8 @@
|
||||
},
|
||||
"node_modules/lib-jitsi-meet": {
|
||||
"version": "0.0.0",
|
||||
"resolved": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v2024.0.0+006b25e4/lib-jitsi-meet.tgz",
|
||||
"integrity": "sha512-MEd0J58ZW2U16tvbO9ydA3M8+yYKRctuFpYa92KzkuObvKTQTYkAgrBHTtymcoO62+7cGJVP51ds33drPdROLA==",
|
||||
"resolved": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v2025.0.0+49eb29a8/lib-jitsi-meet.tgz",
|
||||
"integrity": "sha512-21uWTA9II38ldYZGCxBC2kqFsMrAGUGZxUQKcugmncfZ7SCQbWGUMopvcZC0RhSdeL6Mm+RAafc/HcX369egQA==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@jitsi/js-utils": "2.4.6",
|
||||
@@ -22298,6 +22299,16 @@
|
||||
"react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/react-virtualized-auto-sizer": {
|
||||
"version": "1.0.26",
|
||||
"resolved": "https://registry.npmjs.org/react-virtualized-auto-sizer/-/react-virtualized-auto-sizer-1.0.26.tgz",
|
||||
"integrity": "sha512-CblNyiNVw2o+hsa5/49NH2ogGxZ+t+3aweRvNSq7TVjDIlwk7ir4lencEg5HxHeSzwNarSkNkiu0qJSOXtxm5A==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"react": "^15.3.0 || ^16.0.0-alpha || ^17.0.0 || ^18.0.0 || ^19.0.0",
|
||||
"react-dom": "^15.3.0 || ^16.0.0-alpha || ^17.0.0 || ^18.0.0 || ^19.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/react-window": {
|
||||
"version": "1.8.6",
|
||||
"resolved": "https://registry.npmjs.org/react-window/-/react-window-1.8.6.tgz",
|
||||
@@ -38792,8 +38803,8 @@
|
||||
}
|
||||
},
|
||||
"lib-jitsi-meet": {
|
||||
"version": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v2024.0.0+006b25e4/lib-jitsi-meet.tgz",
|
||||
"integrity": "sha512-MEd0J58ZW2U16tvbO9ydA3M8+yYKRctuFpYa92KzkuObvKTQTYkAgrBHTtymcoO62+7cGJVP51ds33drPdROLA==",
|
||||
"version": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v2025.0.0+49eb29a8/lib-jitsi-meet.tgz",
|
||||
"integrity": "sha512-21uWTA9II38ldYZGCxBC2kqFsMrAGUGZxUQKcugmncfZ7SCQbWGUMopvcZC0RhSdeL6Mm+RAafc/HcX369egQA==",
|
||||
"requires": {
|
||||
"@jitsi/js-utils": "2.4.6",
|
||||
"@jitsi/logger": "2.0.2",
|
||||
@@ -41943,6 +41954,11 @@
|
||||
"tslib": "^2.1.0"
|
||||
}
|
||||
},
|
||||
"react-virtualized-auto-sizer": {
|
||||
"version": "1.0.26",
|
||||
"resolved": "https://registry.npmjs.org/react-virtualized-auto-sizer/-/react-virtualized-auto-sizer-1.0.26.tgz",
|
||||
"integrity": "sha512-CblNyiNVw2o+hsa5/49NH2ogGxZ+t+3aweRvNSq7TVjDIlwk7ir4lencEg5HxHeSzwNarSkNkiu0qJSOXtxm5A=="
|
||||
},
|
||||
"react-window": {
|
||||
"version": "1.8.6",
|
||||
"resolved": "https://registry.npmjs.org/react-window/-/react-window-1.8.6.tgz",
|
||||
|
||||
@@ -69,7 +69,7 @@
|
||||
"js-md5": "0.6.1",
|
||||
"js-sha512": "0.8.0",
|
||||
"jwt-decode": "2.2.0",
|
||||
"lib-jitsi-meet": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v2024.0.0+006b25e4/lib-jitsi-meet.tgz",
|
||||
"lib-jitsi-meet": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v2025.0.0+49eb29a8/lib-jitsi-meet.tgz",
|
||||
"lodash-es": "4.17.21",
|
||||
"null-loader": "4.0.1",
|
||||
"optional-require": "1.0.3",
|
||||
@@ -111,6 +111,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-window": "1.8.6",
|
||||
"react-youtube": "10.1.0",
|
||||
"redux": "4.0.4",
|
||||
|
||||
@@ -84,7 +84,6 @@ export async function createHandlers({ getState }: IStore) {
|
||||
} = config;
|
||||
const {
|
||||
amplitudeAPPKey,
|
||||
amplitudeIncludeUTM,
|
||||
blackListedEvents,
|
||||
scriptURLs,
|
||||
matomoEndpoint,
|
||||
@@ -94,7 +93,6 @@ export async function createHandlers({ getState }: IStore) {
|
||||
const { group, user } = state['features/base/jwt'];
|
||||
const handlerConstructorOptions = {
|
||||
amplitudeAPPKey,
|
||||
amplitudeIncludeUTM,
|
||||
blackListedEvents,
|
||||
envType: deploymentInfo?.envType || 'dev',
|
||||
matomoEndpoint,
|
||||
|
||||
@@ -11,7 +11,6 @@ export interface IEvent {
|
||||
|
||||
interface IOptions {
|
||||
amplitudeAPPKey?: string;
|
||||
amplitudeIncludeUTM?: boolean;
|
||||
blackListedEvents?: string[];
|
||||
envType?: string;
|
||||
group?: string;
|
||||
|
||||
@@ -4,21 +4,18 @@ import logger from '../logger';
|
||||
|
||||
import AbstractHandler, { IEvent } from './AbstractHandler';
|
||||
import { fixDeviceID } from './amplitude/fixDeviceID';
|
||||
import amplitude from './amplitude/lib';
|
||||
import amplitude, { initAmplitude } from './amplitude/lib';
|
||||
|
||||
/**
|
||||
* Analytics handler for Amplitude.
|
||||
*/
|
||||
export default class AmplitudeHandler extends AbstractHandler {
|
||||
_deviceId: string;
|
||||
_userId: Object;
|
||||
|
||||
/**
|
||||
* Creates new instance of the Amplitude analytics handler.
|
||||
*
|
||||
* @param {Object} options - The amplitude options.
|
||||
* @param {string} options.amplitudeAPPKey - The Amplitude app key required by the Amplitude API.
|
||||
* @param {boolean} options.amplitudeIncludeUTM - Whether to include UTM parameters
|
||||
* @param {string} options.amplitudeAPPKey - The Amplitude app key required by the Amplitude API
|
||||
* in the Amplitude events.
|
||||
*/
|
||||
constructor(options: any) {
|
||||
@@ -26,54 +23,26 @@ export default class AmplitudeHandler extends AbstractHandler {
|
||||
|
||||
const {
|
||||
amplitudeAPPKey,
|
||||
amplitudeIncludeUTM: includeUtm = true,
|
||||
user
|
||||
} = options;
|
||||
|
||||
this._enabled = true;
|
||||
|
||||
const onError = (e: Error) => {
|
||||
logger.error('Error initializing Amplitude', e);
|
||||
this._enabled = false;
|
||||
};
|
||||
|
||||
// Forces sending all events on exit (flushing) via sendBeacon
|
||||
const onExitPage = () => {
|
||||
amplitude.flush();
|
||||
};
|
||||
|
||||
if (navigator.product === 'ReactNative') {
|
||||
amplitude.init(amplitudeAPPKey);
|
||||
fixDeviceID(amplitude).then(() => {
|
||||
const deviceId = amplitude.getDeviceId();
|
||||
|
||||
if (deviceId) {
|
||||
this._deviceId = deviceId;
|
||||
}
|
||||
initAmplitude(amplitudeAPPKey, user)
|
||||
.then(() => {
|
||||
logger.info('Amplitude initialized');
|
||||
fixDeviceID(amplitude);
|
||||
})
|
||||
.catch(e => {
|
||||
logger.error('Error initializing Amplitude', e);
|
||||
this._enabled = false;
|
||||
});
|
||||
} else {
|
||||
const amplitudeOptions: any = {
|
||||
includeReferrer: true,
|
||||
includeUtm,
|
||||
saveParamsReferrerOncePerSession: false,
|
||||
onError,
|
||||
onExitPage
|
||||
};
|
||||
|
||||
amplitude.init(amplitudeAPPKey, undefined, amplitudeOptions);
|
||||
fixDeviceID(amplitude);
|
||||
}
|
||||
|
||||
if (user) {
|
||||
this._userId = user;
|
||||
amplitude.setUserId(user);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the Amplitude user properties.
|
||||
*
|
||||
* @param {Object} userProps - The user portperties.
|
||||
* @param {Object} userProps - The user properties.
|
||||
* @returns {void}
|
||||
*/
|
||||
setUserProperties(userProps: any) {
|
||||
@@ -104,7 +73,7 @@ export default class AmplitudeHandler extends AbstractHandler {
|
||||
|
||||
const eventName = this._extractName(event) ?? '';
|
||||
|
||||
amplitude.logEvent(eventName, event);
|
||||
amplitude.track(eventName, event);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -113,13 +82,6 @@ export default class AmplitudeHandler extends AbstractHandler {
|
||||
* @returns {Object}
|
||||
*/
|
||||
getIdentityProps() {
|
||||
if (navigator.product === 'ReactNative') {
|
||||
return {
|
||||
deviceId: this._deviceId,
|
||||
userId: this._userId
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
sessionId: amplitude.getSessionId(),
|
||||
deviceId: amplitude.getDeviceId(),
|
||||
|
||||
@@ -17,7 +17,7 @@ export async function fixDeviceID(amplitude: Types.ReactNativeClient) {
|
||||
const current = await DefaultPreference.get('amplitudeDeviceId');
|
||||
|
||||
if (current) {
|
||||
await amplitude.setDeviceId(current);
|
||||
amplitude.setDeviceId(current);
|
||||
} else {
|
||||
const uid = await getUniqueId();
|
||||
|
||||
@@ -27,7 +27,7 @@ export async function fixDeviceID(amplitude: Types.ReactNativeClient) {
|
||||
return;
|
||||
}
|
||||
|
||||
await amplitude.setDeviceId(uid as string);
|
||||
amplitude.setDeviceId(uid as string);
|
||||
await DefaultPreference.set('amplitudeDeviceId', uid as string);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,37 @@
|
||||
import { Types } from '@amplitude/analytics-browser';
|
||||
// @ts-ignore
|
||||
import { jitsiLocalStorage } from '@jitsi/js-utils';
|
||||
|
||||
import logger from '../../logger';
|
||||
|
||||
/**
|
||||
* Key used to store the device id in local storage.
|
||||
*/
|
||||
const DEVICE_ID_KEY = '__AMDID';
|
||||
|
||||
/**
|
||||
* Custom logic for setting the correct device id.
|
||||
*
|
||||
* @param {Types.BrowserClient} _amplitude - The amplitude instance.
|
||||
* @param {Types.BrowserClient} amplitude - The amplitude instance.
|
||||
* @returns {void}
|
||||
*/
|
||||
export function fixDeviceID(_amplitude: Types.BrowserClient): Promise<any> {
|
||||
return new Promise(resolve => resolve(true));
|
||||
export function fixDeviceID(amplitude: Types.BrowserClient) {
|
||||
const deviceId = jitsiLocalStorage.getItem(DEVICE_ID_KEY);
|
||||
|
||||
if (deviceId) {
|
||||
// Set the device id in Amplitude.
|
||||
try {
|
||||
amplitude.setDeviceId(JSON.parse(deviceId));
|
||||
} catch (error) {
|
||||
logger.error('Failed to set device ID in Amplitude', error);
|
||||
|
||||
return Promise.resolve(false);
|
||||
}
|
||||
} else {
|
||||
const newDeviceId = amplitude.getDeviceId();
|
||||
|
||||
if (newDeviceId) {
|
||||
jitsiLocalStorage.setItem(DEVICE_ID_KEY, JSON.stringify(newDeviceId));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,15 @@
|
||||
import { createInstance } from '@amplitude/analytics-react-native';
|
||||
import amplitude from '@amplitude/analytics-react-native';
|
||||
|
||||
export default createInstance();
|
||||
export default amplitude;
|
||||
|
||||
/**
|
||||
* Initializes the Amplitude instance.
|
||||
*
|
||||
* @param {string} amplitudeAPPKey - The Amplitude app key.
|
||||
* @param {string | undefined} user - The user ID.
|
||||
* @returns {Promise} The initialized Amplitude instance.
|
||||
*/
|
||||
export function initAmplitude(
|
||||
amplitudeAPPKey: string, user: string | undefined): Promise<unknown> {
|
||||
return amplitude.init(amplitudeAPPKey, user, {}).promise;
|
||||
}
|
||||
|
||||
@@ -1,3 +1,38 @@
|
||||
import { createInstance } from '@amplitude/analytics-browser';
|
||||
|
||||
export default createInstance();
|
||||
const amplitude = createInstance();
|
||||
|
||||
export default amplitude;
|
||||
|
||||
/**
|
||||
* Initializes the Amplitude instance.
|
||||
*
|
||||
* @param {string} amplitudeAPPKey - The Amplitude app key.
|
||||
* @param {string | undefined} user - The user ID.
|
||||
* @returns {Promise} The initialized Amplitude instance.
|
||||
*/
|
||||
export function initAmplitude(
|
||||
amplitudeAPPKey: string, user: string | undefined): Promise<unknown> {
|
||||
|
||||
// Forces sending all events on exit (flushing) via sendBeacon.
|
||||
window.addEventListener('pagehide', () => {
|
||||
// Set https transport to use sendBeacon API.
|
||||
amplitude.setTransport('beacon');
|
||||
// Send all pending events to server.
|
||||
amplitude.flush();
|
||||
});
|
||||
|
||||
const options = {
|
||||
autocapture: {
|
||||
attribution: true,
|
||||
pageViews: true,
|
||||
sessions: false,
|
||||
fileDownloads: false,
|
||||
formInteractions: false,
|
||||
elementInteractions: false
|
||||
},
|
||||
defaultTracking: false
|
||||
};
|
||||
|
||||
return amplitude.init(amplitudeAPPKey, user, options).promise;
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@ import { IStore } from '../../app/types';
|
||||
import { removeLobbyChatParticipant } from '../../chat/actions.any';
|
||||
import { openDisplayNamePrompt } from '../../display-name/actions';
|
||||
import { isVpaasMeeting } from '../../jaas/functions';
|
||||
import { showErrorNotification, showNotification } from '../../notifications/actions';
|
||||
import { clearNotifications, showErrorNotification, showNotification } from '../../notifications/actions';
|
||||
import { NOTIFICATION_TIMEOUT_TYPE } from '../../notifications/constants';
|
||||
import { INotificationProps } from '../../notifications/types';
|
||||
import { hasDisplayName } from '../../prejoin/utils';
|
||||
@@ -25,7 +25,7 @@ import LocalRecordingManager from '../../recording/components/Recording/LocalRec
|
||||
import { AudioMixerEffect } from '../../stream-effects/audio-mixer/AudioMixerEffect';
|
||||
import { iAmVisitor } from '../../visitors/functions';
|
||||
import { overwriteConfig } from '../config/actions';
|
||||
import { CONNECTION_ESTABLISHED, CONNECTION_FAILED } from '../connection/actionTypes';
|
||||
import { CONNECTION_ESTABLISHED, CONNECTION_FAILED, CONNECTION_WILL_CONNECT } from '../connection/actionTypes';
|
||||
import { connectionDisconnected, disconnect } from '../connection/actions';
|
||||
import { validateJwt } from '../jwt/functions';
|
||||
import { JitsiConferenceErrors, JitsiConferenceEvents, JitsiConnectionErrors } from '../lib-jitsi-meet';
|
||||
@@ -99,6 +99,11 @@ MiddlewareRegistry.register(store => next => action => {
|
||||
case CONNECTION_FAILED:
|
||||
return _connectionFailed(store, next, action);
|
||||
|
||||
case CONNECTION_WILL_CONNECT:
|
||||
// we are starting a new join process, let's clear the error notifications if any from any previous attempt
|
||||
store.dispatch(clearNotifications());
|
||||
break;
|
||||
|
||||
case CONFERENCE_SUBJECT_CHANGED:
|
||||
return _conferenceSubjectChanged(store, next, action);
|
||||
|
||||
|
||||
@@ -172,7 +172,6 @@ export interface IConfig {
|
||||
_screenshotHistoryRegionUrl?: number;
|
||||
analytics?: {
|
||||
amplitudeAPPKey?: string;
|
||||
amplitudeIncludeUTM?: boolean;
|
||||
blackListedEvents?: string[];
|
||||
disabled?: boolean;
|
||||
matomoEndpoint?: string;
|
||||
@@ -596,6 +595,7 @@ export interface IConfig {
|
||||
failICE?: boolean;
|
||||
noAutoPlayVideo?: boolean;
|
||||
p2pTestMode?: boolean;
|
||||
showSpotConsentDialog?: boolean;
|
||||
skipInterimTranscriptions?: boolean;
|
||||
testMode?: boolean;
|
||||
};
|
||||
|
||||
@@ -18,6 +18,7 @@ export const MEET_FEATURES: Record<string, ParticipantFeaturesKey> = {
|
||||
ROOM: 'room',
|
||||
SCREEN_SHARING: 'screen-sharing',
|
||||
SEND_GROUPCHAT: 'send-groupchat',
|
||||
LIST_VISITORS: 'list-visitors',
|
||||
SIP_INBOUND_CALL: 'sip-inbound-call',
|
||||
SIP_OUTBOUND_CALL: 'sip-outbound-call',
|
||||
TRANSCRIPTION: 'transcription'
|
||||
|
||||
@@ -61,6 +61,7 @@ export interface IParticipantFeatures {
|
||||
'file-upload'?: boolean | string;
|
||||
'flip'?: boolean | string;
|
||||
'inbound-call'?: boolean | string;
|
||||
'list-visitors'?: boolean | string;
|
||||
'livestreaming'?: boolean | string;
|
||||
'lobby'?: boolean | string;
|
||||
'moderation'?: boolean | string;
|
||||
|
||||
@@ -12,13 +12,7 @@ const JITSI_MEET_APPS = [
|
||||
'org.jitsi.meet',
|
||||
|
||||
// Android debug app.
|
||||
'org.jitsi.meet.debug',
|
||||
|
||||
// 8x8 Work (Android).
|
||||
'org.vom8x8.sipua',
|
||||
|
||||
// 8x8 Work (iOS).
|
||||
'com.yourcompany.Virtual-Office'
|
||||
'org.jitsi.meet.debug'
|
||||
];
|
||||
|
||||
/**
|
||||
|
||||
@@ -234,6 +234,7 @@ const FileSharing = () => {
|
||||
const handleFileSelect = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
if (e.target.files) {
|
||||
processFiles(e.target.files as FileList, store);
|
||||
e.target.value = ''; // Reset the input value to allow re-uploading the same file
|
||||
}
|
||||
}, [ processFiles ]);
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ import { MEET_FEATURES } from '../base/jwt/constants';
|
||||
import { isJwtFeatureEnabled } from '../base/jwt/functions';
|
||||
import { showErrorNotification } from '../notifications/actions';
|
||||
import { NOTIFICATION_TIMEOUT_TYPE, NOTIFICATION_TYPE } from '../notifications/constants';
|
||||
import { iAmVisitor } from '../visitors/functions';
|
||||
|
||||
import { uploadFiles } from './actions';
|
||||
import { MAX_FILE_SIZE } from './constants';
|
||||
@@ -150,5 +151,7 @@ export const processFiles = (fileList: FileList | File[], store: IStore) => {
|
||||
* @returns {boolean} Indication of whether local user can upload files.
|
||||
*/
|
||||
export function isFileUploadingEnabled(state: IReduxState): boolean {
|
||||
return isJwtFeatureEnabled(state, MEET_FEATURES.FILE_UPLOAD, false) && isFileSharingEnabled(state);
|
||||
return !iAmVisitor(state)
|
||||
&& isJwtFeatureEnabled(state, MEET_FEATURES.FILE_UPLOAD, false)
|
||||
&& isFileSharingEnabled(state);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,169 @@
|
||||
/* eslint-disable react/no-multi-comp */
|
||||
import React, { useCallback, useEffect, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import AutoSizer from 'react-virtualized-auto-sizer';
|
||||
import { FixedSizeList } from 'react-window';
|
||||
import { makeStyles } from 'tss-react/mui';
|
||||
|
||||
import Icon from '../../../base/icons/components/Icon';
|
||||
import { IconArrowDown, IconArrowUp } from '../../../base/icons/svg';
|
||||
import { withPixelLineHeight } from '../../../base/styles/functions.web';
|
||||
import { normalizeAccents } from '../../../base/util/strings.web';
|
||||
import { subscribeVisitorsList } from '../../../visitors/actions';
|
||||
import {
|
||||
getVisitorsCount,
|
||||
getVisitorsList,
|
||||
isVisitorsListEnabled,
|
||||
isVisitorsListSubscribed,
|
||||
shouldDisplayCurrentVisitorsList
|
||||
} from '../../../visitors/functions';
|
||||
import { ACTION_TRIGGER, MEDIA_STATE } from '../../constants';
|
||||
|
||||
import ParticipantItem from './ParticipantItem';
|
||||
|
||||
/**
|
||||
* Props for the {@code CurrentVisitorsList} component.
|
||||
*/
|
||||
interface IProps {
|
||||
searchString: string;
|
||||
}
|
||||
|
||||
const useStyles = makeStyles()(theme => {
|
||||
return {
|
||||
container: {
|
||||
marginTop: theme.spacing(3),
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
minHeight: 0,
|
||||
flexGrow: 1
|
||||
},
|
||||
heading: {
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
cursor: 'pointer',
|
||||
padding: `${theme.spacing(1)} 0`,
|
||||
...withPixelLineHeight(theme.typography.bodyShortBold),
|
||||
color: theme.palette.text02,
|
||||
flexShrink: 0
|
||||
},
|
||||
arrowContainer: {
|
||||
backgroundColor: theme.palette.ui03,
|
||||
width: '24px',
|
||||
height: '24px',
|
||||
borderRadius: '6px',
|
||||
marginLeft: theme.spacing(2),
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
border: 'none'
|
||||
},
|
||||
listContainer: {
|
||||
flex: 1,
|
||||
minHeight: '200px',
|
||||
maxHeight: '100%'
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
/**
|
||||
* Renders the visitors list inside the participants pane.
|
||||
*
|
||||
* @param {IProps} props - Component props.
|
||||
* @returns {React$Element<any>} The component.
|
||||
*/
|
||||
export default function CurrentVisitorsList({ searchString }: IProps) {
|
||||
const visitorsCount = useSelector(getVisitorsCount);
|
||||
const visitors = useSelector(getVisitorsList);
|
||||
const featureEnabled = useSelector(isVisitorsListEnabled);
|
||||
const shouldDisplayList = useSelector(shouldDisplayCurrentVisitorsList);
|
||||
const { t } = useTranslation();
|
||||
const { classes } = useStyles();
|
||||
const dispatch = useDispatch();
|
||||
const [ collapsed, setCollapsed ] = useState(true);
|
||||
const isSubscribed = useSelector(isVisitorsListSubscribed);
|
||||
|
||||
const toggleCollapsed = useCallback(() => {
|
||||
setCollapsed(c => {
|
||||
const newCollapsed = !c;
|
||||
|
||||
if (featureEnabled && !newCollapsed && !isSubscribed) {
|
||||
dispatch(subscribeVisitorsList());
|
||||
}
|
||||
|
||||
return newCollapsed;
|
||||
});
|
||||
}, [ dispatch, isSubscribed, featureEnabled ]);
|
||||
|
||||
useEffect(() => {
|
||||
if (featureEnabled && searchString) {
|
||||
setCollapsed(false);
|
||||
if (!isSubscribed) {
|
||||
dispatch(subscribeVisitorsList());
|
||||
}
|
||||
}
|
||||
}, [ searchString, dispatch, isSubscribed, featureEnabled ]);
|
||||
|
||||
if (!shouldDisplayList) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const filtered = visitors.filter(v =>
|
||||
normalizeAccents(v.name).toLowerCase().includes(normalizeAccents(searchString).toLowerCase())
|
||||
);
|
||||
|
||||
// ListItem height is 56px including padding so the item size
|
||||
// for virtualization needs to match it exactly to avoid clipping.
|
||||
const itemSize = 56;
|
||||
|
||||
const Row = ({ index, style }: { index: number; style: any; }) => {
|
||||
const v = filtered[index];
|
||||
|
||||
return (
|
||||
<div style = { style }>
|
||||
<ParticipantItem
|
||||
actionsTrigger = { ACTION_TRIGGER.HOVER }
|
||||
audioMediaState = { MEDIA_STATE.NONE }
|
||||
displayName = { v.name }
|
||||
participantID = { v.id }
|
||||
videoMediaState = { MEDIA_STATE.NONE } />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
const styles = {
|
||||
overflowX: 'hidden' as const,
|
||||
overflowY: 'auto' as const,
|
||||
};
|
||||
|
||||
return (
|
||||
<div className = { classes.container }>
|
||||
<div
|
||||
className = { classes.heading }
|
||||
onClick = { toggleCollapsed }>
|
||||
<span>{ t('participantsPane.headings.visitorsList', { count: visitorsCount }) }</span>
|
||||
<span className = { classes.arrowContainer }>
|
||||
<Icon
|
||||
size = { 14 }
|
||||
src = { collapsed ? IconArrowDown : IconArrowUp } />
|
||||
</span>
|
||||
</div>
|
||||
{!collapsed && (
|
||||
<div className = { classes.listContainer }>
|
||||
<AutoSizer>
|
||||
{ ({ height, width }) => (
|
||||
<FixedSizeList
|
||||
height = { Math.max(height, 200) }
|
||||
itemCount = { filtered.length }
|
||||
itemSize = { itemSize }
|
||||
style = { styles }
|
||||
width = { width }>
|
||||
{ Row }
|
||||
</FixedSizeList>
|
||||
)}
|
||||
</AutoSizer>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -15,6 +15,7 @@ import { BUTTON_TYPES } from '../../../base/ui/constants.web';
|
||||
import { findAncestorByClass } from '../../../base/ui/functions.web';
|
||||
import { isAddBreakoutRoomButtonVisible } from '../../../breakout-rooms/functions';
|
||||
import MuteEveryoneDialog from '../../../video-menu/components/web/MuteEveryoneDialog';
|
||||
import { shouldDisplayCurrentVisitorsList } from '../../../visitors/functions';
|
||||
import { close } from '../../actions.web';
|
||||
import {
|
||||
getParticipantsPaneOpen,
|
||||
@@ -24,6 +25,7 @@ import {
|
||||
import { AddBreakoutRoomButton } from '../breakout-rooms/components/web/AddBreakoutRoomButton';
|
||||
import { RoomList } from '../breakout-rooms/components/web/RoomList';
|
||||
|
||||
import CurrentVisitorsList from './CurrentVisitorsList';
|
||||
import { FooterContextMenu } from './FooterContextMenu';
|
||||
import LobbyParticipants from './LobbyParticipants';
|
||||
import MeetingParticipants from './MeetingParticipants';
|
||||
@@ -73,6 +75,8 @@ const useStyles = makeStyles<IStylesProps>()((theme, { isChatOpen }) => {
|
||||
overflowY: 'auto',
|
||||
position: 'relative',
|
||||
padding: `0 ${participantsPaneTheme.panePadding}px`,
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
|
||||
'&::-webkit-scrollbar': {
|
||||
display: 'none'
|
||||
@@ -129,6 +133,7 @@ const ParticipantsPane = () => {
|
||||
const paneOpen = useSelector(getParticipantsPaneOpen);
|
||||
const isBreakoutRoomsSupported = useSelector((state: IReduxState) => state['features/base/conference'])
|
||||
.conference?.getBreakoutRooms()?.isSupported();
|
||||
const showCurrentVisitorsList = useSelector(shouldDisplayCurrentVisitorsList);
|
||||
const showAddRoomButton = useSelector(isAddBreakoutRoomButtonVisible);
|
||||
const showFooter = useSelector(isLocalParticipantModerator);
|
||||
const showMuteAllButton = useSelector(isMuteAllVisible);
|
||||
@@ -193,6 +198,7 @@ const ParticipantsPane = () => {
|
||||
setSearchString = { setSearchString } />
|
||||
{isBreakoutRoomsSupported && <RoomList searchString = { searchString } />}
|
||||
{showAddRoomButton && <AddBreakoutRoomButton />}
|
||||
{showCurrentVisitorsList && <CurrentVisitorsList searchString = { searchString } />}
|
||||
</div>
|
||||
{showFooter && (
|
||||
<div className = { classes.footer }>
|
||||
|
||||
@@ -9,7 +9,8 @@ import {
|
||||
getPromotionRequests,
|
||||
getVisitorsCount,
|
||||
getVisitorsInQueueCount,
|
||||
isVisitorsLive
|
||||
isVisitorsLive,
|
||||
shouldDisplayCurrentVisitorsList
|
||||
} from '../../../visitors/functions';
|
||||
|
||||
import { VisitorsItem } from './VisitorsItem';
|
||||
@@ -74,6 +75,7 @@ export default function VisitorsList() {
|
||||
const visitorsInQueueCount = useSelector(getVisitorsInQueueCount);
|
||||
const isLive = useSelector(isVisitorsLive);
|
||||
const showVisitorsInQueue = visitorsInQueueCount > 0 && isLive === false;
|
||||
const showCurrentVisitorsList = useSelector(shouldDisplayCurrentVisitorsList);
|
||||
|
||||
const { t } = useTranslation();
|
||||
const { classes, cx } = useStyles();
|
||||
@@ -91,15 +93,19 @@ export default function VisitorsList() {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (showCurrentVisitorsList && requests.length <= 0 && !showVisitorsInQueue) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className = { classes.headingContainer }>
|
||||
<div
|
||||
className = { cx(classes.heading, classes.headingW) }
|
||||
id = 'visitor-list-header' >
|
||||
{ t('participantsPane.headings.visitors', { count: visitorsCount })}
|
||||
{ !showCurrentVisitorsList && t('participantsPane.headings.visitors', { count: visitorsCount })}
|
||||
{ requests.length > 0
|
||||
&& t('participantsPane.headings.visitorRequests', { count: requests.length }) }
|
||||
&& t(`participantsPane.headings.${showCurrentVisitorsList ? 'viewerRequests' : 'visitorRequests'}`, { count: requests.length }) }
|
||||
{ showVisitorsInQueue
|
||||
&& t('participantsPane.headings.visitorInQueue', { count: visitorsInQueueCount }) }
|
||||
</div>
|
||||
@@ -116,17 +122,19 @@ export default function VisitorsList() {
|
||||
onClick = { goLiveCb }>{ t('participantsPane.actions.goLive') }</div>
|
||||
}
|
||||
</div>
|
||||
<div
|
||||
className = { classes.container }
|
||||
id = 'visitor-list'>
|
||||
{
|
||||
requests.map(r => (
|
||||
<VisitorsItem
|
||||
key = { r.from }
|
||||
request = { r } />)
|
||||
)
|
||||
}
|
||||
</div>
|
||||
{ requests.length > 0
|
||||
&& <div
|
||||
className = { classes.container }
|
||||
id = 'visitor-list'>
|
||||
{
|
||||
requests.map(r => (
|
||||
<VisitorsItem
|
||||
key = { r.from }
|
||||
request = { r } />)
|
||||
)
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -443,14 +443,16 @@ export function shouldRequireRecordingConsent(recorderSession: any, state: IRedu
|
||||
= state['features/dynamic-branding'] || {};
|
||||
const { conference } = state['features/base/conference'] || {};
|
||||
const { requireConsent, skipConsentInMeeting } = state['features/base/config'].recordings || {};
|
||||
const { iAmRecorder } = state['features/base/config'];
|
||||
const { iAmRecorder, testing: { showSpotConsentDialog = false } = {} } = state['features/base/config'];
|
||||
const { consentRequested } = state['features/recording'];
|
||||
|
||||
if (iAmRecorder) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (isSpotTV(state)) {
|
||||
// For Spot TV instances, check the showSpotConsentDialog config parameter
|
||||
// If showSpotConsentDialog is false (or undefined, defaulting to false), don't show consent dialog
|
||||
if (isSpotTV(state) && !showSpotConsentDialog) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
@@ -46,13 +46,17 @@ ReducerRegistry.register<ITranscribingState>('features/transcribing',
|
||||
(state = _getInitialState(), action): ITranscribingState => {
|
||||
switch (action.type) {
|
||||
case CONFERENCE_PROPERTIES_CHANGED: {
|
||||
const audioRecordingEnabled = action.properties?.['audio-recording-enabled'] === 'true';
|
||||
const audioRecording = action.properties?.['audio-recording-enabled'];
|
||||
|
||||
if (state.isTranscribing !== audioRecordingEnabled) {
|
||||
return {
|
||||
...state,
|
||||
isTranscribing: audioRecordingEnabled
|
||||
};
|
||||
if (typeof audioRecording !== 'undefined') {
|
||||
const audioRecordingEnabled = audioRecording === 'true';
|
||||
|
||||
if (state.isTranscribing !== audioRecordingEnabled) {
|
||||
return {
|
||||
...state,
|
||||
isTranscribing: audioRecordingEnabled
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return state;
|
||||
|
||||
@@ -185,7 +185,7 @@ class VirtualBackgroundPreview extends PureComponent<IProps, IState> {
|
||||
this.props.dispatch(
|
||||
showWarningNotification({
|
||||
titleKey: 'virtualBackground.backgroundEffectError',
|
||||
description: 'Failed to access camera device.'
|
||||
descriptionKey: 'deviceError.cameraError'
|
||||
}, NOTIFICATION_TIMEOUT_TYPE.LONG)
|
||||
);
|
||||
logger.error('Failed to access camera device. Error on apply background effect.');
|
||||
|
||||
127
react/features/visitors/VisitorsListWebsocketClient.ts
Normal file
127
react/features/visitors/VisitorsListWebsocketClient.ts
Normal file
@@ -0,0 +1,127 @@
|
||||
import { Client } from '@stomp/stompjs';
|
||||
|
||||
import logger from './logger';
|
||||
import { WebsocketClient } from './websocket-client';
|
||||
|
||||
/**
|
||||
* Websocket client impl, used for visitors list.
|
||||
* Uses STOMP for authenticating (https://stomp.github.io/).
|
||||
*/
|
||||
export class VisitorsListWebsocketClient extends WebsocketClient {
|
||||
private static client: VisitorsListWebsocketClient;
|
||||
|
||||
/**
|
||||
* Creates a new instance of the VisitorsListWebsocketClient.
|
||||
*
|
||||
* @static
|
||||
* @returns {VisitorsListWebsocketClient}
|
||||
*/
|
||||
static override getInstance(): VisitorsListWebsocketClient {
|
||||
if (!this.client) {
|
||||
this.client = new VisitorsListWebsocketClient();
|
||||
}
|
||||
|
||||
return this.client;
|
||||
}
|
||||
|
||||
/**
|
||||
* Connects to the visitors list with initial queue subscription, then switches to topic deltas.
|
||||
*
|
||||
* @param {string} queueServiceURL - The service URL to use.
|
||||
* @param {string} queueEndpoint - The queue endpoint for initial list.
|
||||
* @param {string} topicEndpoint - The topic endpoint for deltas.
|
||||
* @param {Function} initialCallback - Callback executed with initial visitors list.
|
||||
* @param {Function} deltaCallback - Callback executed with delta updates.
|
||||
* @param {string} token - The token to be used for authorization.
|
||||
* @param {Function?} connectCallback - Callback executed when connected.
|
||||
* @returns {void}
|
||||
*/
|
||||
connectVisitorsList(queueServiceURL: string,
|
||||
queueEndpoint: string,
|
||||
topicEndpoint: string,
|
||||
initialCallback: (visitors: Array<{ n: string; r: string; }>) => void,
|
||||
deltaCallback: (updates: Array<{ n: string; r: string; s: string; }>) => void,
|
||||
token: string | undefined,
|
||||
connectCallback?: () => void) {
|
||||
this.stompClient = new Client({
|
||||
brokerURL: queueServiceURL,
|
||||
forceBinaryWSFrames: true,
|
||||
appendMissingNULLonIncoming: true
|
||||
});
|
||||
|
||||
const errorConnecting = (error: any) => {
|
||||
if (this.retriesCount > 3) {
|
||||
this.stompClient?.deactivate();
|
||||
this.stompClient = undefined;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
this.retriesCount++;
|
||||
|
||||
logger.error(`Error connecting to ${queueServiceURL} ${JSON.stringify(error)}`);
|
||||
};
|
||||
|
||||
this.stompClient.onWebSocketError = errorConnecting;
|
||||
|
||||
this.stompClient.onStompError = frame => {
|
||||
logger.error('STOMP error received', frame);
|
||||
errorConnecting(frame.headers.message);
|
||||
};
|
||||
|
||||
if (token) {
|
||||
this.stompClient.connectHeaders = {
|
||||
Authorization: `Bearer ${token}`
|
||||
};
|
||||
}
|
||||
|
||||
this.stompClient.onConnect = () => {
|
||||
if (!this.stompClient) {
|
||||
return;
|
||||
}
|
||||
|
||||
logger.debug('Connected to visitors list websocket');
|
||||
connectCallback?.();
|
||||
|
||||
let initialReceived = false;
|
||||
const cachedDeltas: Array<{ n: string; r: string; s: string; }> = [];
|
||||
|
||||
// Subscribe first for deltas so we don't miss any while waiting for the initial list
|
||||
this.stompClient.subscribe(topicEndpoint, deltaMessage => {
|
||||
try {
|
||||
const updates: Array<{ n: string; r: string; s: string; }> = JSON.parse(deltaMessage.body);
|
||||
|
||||
if (!initialReceived) {
|
||||
cachedDeltas.push(...updates);
|
||||
} else {
|
||||
deltaCallback(updates);
|
||||
}
|
||||
} catch (e) {
|
||||
logger.error(`Error parsing visitors delta response: ${deltaMessage}`, e);
|
||||
}
|
||||
});
|
||||
|
||||
// Subscribe for the initial list after topic subscription is active
|
||||
const queueSubscription = this.stompClient.subscribe(queueEndpoint, message => {
|
||||
try {
|
||||
const visitors: Array<{ n: string; r: string; }> = JSON.parse(message.body);
|
||||
|
||||
logger.debug(`Received initial visitors list with ${visitors.length} visitors`);
|
||||
initialReceived = true;
|
||||
initialCallback(visitors);
|
||||
|
||||
queueSubscription.unsubscribe();
|
||||
|
||||
if (cachedDeltas.length) {
|
||||
deltaCallback(cachedDeltas);
|
||||
cachedDeltas.length = 0;
|
||||
}
|
||||
} catch (e) {
|
||||
logger.error(`Error parsing initial visitors response: ${message}`, e);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
this.stompClient.activate();
|
||||
}
|
||||
}
|
||||
@@ -68,3 +68,14 @@ export const SET_VISITOR_DEMOTE_ACTOR = 'SET_VISITOR_DEMOTE_ACTOR';
|
||||
* }
|
||||
*/
|
||||
export const SET_VISITORS_SUPPORTED = 'SET_VISITORS_SUPPORTED';
|
||||
|
||||
/**
|
||||
* The type of (redux) action which updates the current visitors list.
|
||||
*/
|
||||
export const UPDATE_VISITORS_LIST = 'UPDATE_VISITORS_LIST';
|
||||
|
||||
/**
|
||||
* Action dispatched when the visitors list is expanded for the first time
|
||||
* and the client should subscribe for updates.
|
||||
*/
|
||||
export const SUBSCRIBE_VISITORS_LIST = 'SUBSCRIBE_VISITORS_LIST';
|
||||
|
||||
@@ -11,7 +11,9 @@ import {
|
||||
SET_IN_VISITORS_QUEUE,
|
||||
SET_VISITORS_SUPPORTED,
|
||||
SET_VISITOR_DEMOTE_ACTOR,
|
||||
SUBSCRIBE_VISITORS_LIST,
|
||||
UPDATE_VISITORS_IN_QUEUE_COUNT,
|
||||
UPDATE_VISITORS_LIST,
|
||||
VISITOR_PROMOTION_REQUEST
|
||||
} from './actionTypes';
|
||||
import logger from './logger';
|
||||
@@ -216,6 +218,32 @@ export function updateVisitorsInQueueCount(count: number) {
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the current list of visitors.
|
||||
*
|
||||
* @param {Array<Object>} visitors - The visitors list.
|
||||
* @returns {{
|
||||
* type: UPDATE_VISITORS_LIST,
|
||||
* }}
|
||||
*/
|
||||
export function updateVisitorsList(visitors: Array<{ id: string; name: string; }>) {
|
||||
return {
|
||||
type: UPDATE_VISITORS_LIST,
|
||||
visitors
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Signals the start of the visitors list websocket subscription.
|
||||
*
|
||||
* @returns {{ type: SUBSCRIBE_VISITORS_LIST }}
|
||||
*/
|
||||
export function subscribeVisitorsList() {
|
||||
return {
|
||||
type: SUBSCRIBE_VISITORS_LIST
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Closes the overflow menu if opened.
|
||||
*
|
||||
@@ -227,6 +255,7 @@ export function goLive() {
|
||||
const { conference } = getState()['features/base/conference'];
|
||||
|
||||
conference?.getMetadataHandler().setMetadata('visitors', {
|
||||
...(conference?.getMetadataHandler().getMetadata()?.visitors || {}),
|
||||
live: true
|
||||
});
|
||||
};
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import { IReduxState } from '../app/types';
|
||||
import { IStateful } from '../base/app/types';
|
||||
import { MEET_FEATURES } from '../base/jwt/constants';
|
||||
import { isJwtFeatureEnabled } from '../base/jwt/functions';
|
||||
import { toState } from '../base/redux/functions';
|
||||
|
||||
/**
|
||||
@@ -68,6 +70,26 @@ export function isVisitorsSupported(stateful: IStateful) {
|
||||
return toState(stateful)['features/visitors'].supported;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the current visitor list.
|
||||
*
|
||||
* @param {IStateful} stateful - The redux store or {@code getState} function.
|
||||
* @returns {Array<Object>}
|
||||
*/
|
||||
export function getVisitorsList(stateful: IStateful) {
|
||||
return toState(stateful)['features/visitors'].visitors ?? [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether the visitors list websocket subscription has been requested.
|
||||
*
|
||||
* @param {IStateful} stateful - The redux store or {@code getState} function.
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export function isVisitorsListSubscribed(stateful: IStateful) {
|
||||
return toState(stateful)['features/visitors'].visitorsListSubscribed;
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether visitor mode is live.
|
||||
*
|
||||
@@ -89,3 +111,31 @@ export function isVisitorsLive(stateful: IStateful) {
|
||||
export function showVisitorsQueue(stateful: IStateful) {
|
||||
return toState(stateful)['features/visitors'].inQueue;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the visitors list feature is enabled based on JWT and config.js.
|
||||
*
|
||||
* @param {IReduxState} state - The redux state.
|
||||
* @returns {boolean} Whether the feature is allowed.
|
||||
*/
|
||||
export function isVisitorsListEnabled(state: IReduxState): boolean {
|
||||
const { visitors: visitorsConfig } = state['features/base/config'];
|
||||
|
||||
if (!visitorsConfig?.queueService) { // if the queue service is not configured, we can't retrieve the visitors list
|
||||
return false;
|
||||
}
|
||||
|
||||
return isJwtFeatureEnabled(state, MEET_FEATURES.LIST_VISITORS, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines whether the current visitors list should be displayed.
|
||||
*
|
||||
* @param {IStateful} stateful - The redux store or {@code getState} function.
|
||||
* @returns {boolean} Whether the visitors list should be shown.
|
||||
*/
|
||||
export function shouldDisplayCurrentVisitorsList(stateful: IStateful): boolean {
|
||||
const state = toState(stateful);
|
||||
|
||||
return isVisitorsListEnabled(state) && getVisitorsCount(state) > 0;
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import { IStore } from '../app/types';
|
||||
import { IStateful } from '../base/app/types';
|
||||
import {
|
||||
CONFERENCE_JOINED,
|
||||
CONFERENCE_WILL_LEAVE,
|
||||
ENDPOINT_MESSAGE_RECEIVED,
|
||||
UPDATE_CONFERENCE_METADATA
|
||||
} from '../base/conference/actionTypes';
|
||||
@@ -35,7 +36,8 @@ import { INotificationProps } from '../notifications/types';
|
||||
import { open as openParticipantsPane } from '../participants-pane/actions';
|
||||
import { joinConference } from '../prejoin/actions';
|
||||
|
||||
import { UPDATE_VISITORS_IN_QUEUE_COUNT } from './actionTypes';
|
||||
import { VisitorsListWebsocketClient } from './VisitorsListWebsocketClient';
|
||||
import { SUBSCRIBE_VISITORS_LIST, UPDATE_VISITORS_IN_QUEUE_COUNT } from './actionTypes';
|
||||
import {
|
||||
approveRequest,
|
||||
clearPromotionRequest,
|
||||
@@ -45,10 +47,11 @@ import {
|
||||
setInVisitorsQueue,
|
||||
setVisitorDemoteActor,
|
||||
setVisitorsSupported,
|
||||
updateVisitorsInQueueCount
|
||||
updateVisitorsInQueueCount,
|
||||
updateVisitorsList
|
||||
} from './actions';
|
||||
import { JoinMeetingDialog } from './components';
|
||||
import { getPromotionRequests, getVisitorsInQueueCount } from './functions';
|
||||
import { getPromotionRequests, getVisitorsInQueueCount, isVisitorsListEnabled } from './functions';
|
||||
import logger from './logger';
|
||||
import { WebsocketClient } from './websocket-client';
|
||||
|
||||
@@ -135,6 +138,17 @@ MiddlewareRegistry.register(({ dispatch, getState }) => next => action => {
|
||||
|
||||
break;
|
||||
}
|
||||
case CONFERENCE_WILL_LEAVE: {
|
||||
WebsocketClient.getInstance().disconnect();
|
||||
VisitorsListWebsocketClient.getInstance().disconnect();
|
||||
break;
|
||||
}
|
||||
case SUBSCRIBE_VISITORS_LIST: {
|
||||
if (isVisitorsListEnabled(getState()) && !VisitorsListWebsocketClient.getInstance().isActive()) {
|
||||
_subscribeVisitorsList(getState, dispatch);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case ENDPOINT_MESSAGE_RECEIVED: {
|
||||
const { data } = action;
|
||||
|
||||
@@ -314,6 +328,71 @@ function _subscribeQueueStats(stateful: IStateful, dispatch: IStore['dispatch'])
|
||||
toState(stateful)['features/base/jwt'].jwt);
|
||||
}
|
||||
|
||||
/**
|
||||
* Subscribes to the visitors list via WebSocket for real-time updates. This function establishes a WebSocket
|
||||
* connection to track visitors in a conference.
|
||||
*
|
||||
* @param {IStore.getState} getState - Function to retrieve the current Redux state.
|
||||
* @param {IStore.dispatch} dispatch - Function to dispatch Redux actions.
|
||||
* @returns {void}
|
||||
*/
|
||||
function _subscribeVisitorsList(getState: IStore['getState'], dispatch: IStore['dispatch']) {
|
||||
const state = getState();
|
||||
const { visitors: visitorsConfig } = state['features/base/config'];
|
||||
const conference = state['features/base/conference'].conference;
|
||||
const meetingId = conference?.getMeetingUniqueId();
|
||||
const localParticipant = getLocalParticipant(state);
|
||||
const participantId = localParticipant?.id;
|
||||
|
||||
if (!visitorsConfig?.queueService || !meetingId || !participantId) {
|
||||
logger.warn(`Missing required data for visitors list subscription', ${JSON.stringify({
|
||||
queueService: visitorsConfig?.queueService,
|
||||
meetingId,
|
||||
participantId: participantId ? 'participantId present' : 'participantId missing'
|
||||
})}`);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const queueEndpoint = `/secured/conference/visitors-list/queue/${meetingId}/${participantId}`;
|
||||
const topicEndpoint = `/secured/conference/visitors-list/topic/${meetingId}`;
|
||||
|
||||
logger.debug('Starting visitors list subscription');
|
||||
|
||||
VisitorsListWebsocketClient.getInstance()
|
||||
.connectVisitorsList(
|
||||
`wss://${visitorsConfig.queueService}/visitors-list/websocket`,
|
||||
queueEndpoint,
|
||||
topicEndpoint,
|
||||
// Initial list callback - replace entire list
|
||||
initialVisitors => {
|
||||
const visitors = initialVisitors.map(v => ({ id: v.r, name: v.n }));
|
||||
|
||||
dispatch(updateVisitorsList(visitors));
|
||||
},
|
||||
// Delta updates callback - apply incremental changes
|
||||
updates => {
|
||||
let visitors = [ ...(getState()['features/visitors'].visitors ?? []) ];
|
||||
|
||||
updates.forEach(u => {
|
||||
if (u.s === 'j') {
|
||||
const index = visitors.findIndex(v => v.id === u.r);
|
||||
|
||||
if (index === -1) {
|
||||
visitors.push({ id: u.r, name: u.n });
|
||||
} else {
|
||||
visitors[index] = { id: u.r, name: u.n };
|
||||
}
|
||||
} else if (u.s === 'l') {
|
||||
visitors = visitors.filter(v => v.id !== u.r);
|
||||
}
|
||||
});
|
||||
|
||||
dispatch(updateVisitorsList(visitors));
|
||||
},
|
||||
getState()['features/base/jwt'].jwt);
|
||||
}
|
||||
|
||||
/**
|
||||
* Function to handle the promotion notification.
|
||||
*
|
||||
|
||||
@@ -7,10 +7,12 @@ import {
|
||||
SET_IN_VISITORS_QUEUE,
|
||||
SET_VISITORS_SUPPORTED,
|
||||
SET_VISITOR_DEMOTE_ACTOR,
|
||||
SUBSCRIBE_VISITORS_LIST,
|
||||
UPDATE_VISITORS_IN_QUEUE_COUNT,
|
||||
UPDATE_VISITORS_LIST,
|
||||
VISITOR_PROMOTION_REQUEST
|
||||
} from './actionTypes';
|
||||
import { IPromotionRequest } from './types';
|
||||
import { IPromotionRequest, IVisitor } from './types';
|
||||
|
||||
const DEFAULT_STATE = {
|
||||
count: 0,
|
||||
@@ -19,7 +21,9 @@ const DEFAULT_STATE = {
|
||||
inQueueCount: 0,
|
||||
showNotification: false,
|
||||
supported: false,
|
||||
promotionRequests: []
|
||||
promotionRequests: [],
|
||||
visitors: [] as IVisitor[],
|
||||
visitorsListSubscribed: false
|
||||
};
|
||||
|
||||
export interface IVisitorsState {
|
||||
@@ -30,6 +34,8 @@ export interface IVisitorsState {
|
||||
inQueueCount?: number;
|
||||
promotionRequests: IPromotionRequest[];
|
||||
supported: boolean;
|
||||
visitors: IVisitor[];
|
||||
visitorsListSubscribed: boolean;
|
||||
}
|
||||
ReducerRegistry.register<IVisitorsState>('features/visitors', (state = DEFAULT_STATE, action): IVisitorsState => {
|
||||
switch (action.type) {
|
||||
@@ -90,6 +96,18 @@ ReducerRegistry.register<IVisitorsState>('features/visitors', (state = DEFAULT_S
|
||||
supported: action.value
|
||||
};
|
||||
}
|
||||
case SUBSCRIBE_VISITORS_LIST: {
|
||||
return {
|
||||
...state,
|
||||
visitorsListSubscribed: true
|
||||
};
|
||||
}
|
||||
case UPDATE_VISITORS_LIST: {
|
||||
return {
|
||||
...state,
|
||||
visitors: action.visitors
|
||||
};
|
||||
}
|
||||
case VISITOR_PROMOTION_REQUEST: {
|
||||
const currentRequests = state.promotionRequests || [];
|
||||
|
||||
|
||||
@@ -2,3 +2,8 @@ export interface IPromotionRequest {
|
||||
from: string;
|
||||
nick: string;
|
||||
}
|
||||
|
||||
export interface IVisitor {
|
||||
id: string;
|
||||
name: string;
|
||||
}
|
||||
|
||||
@@ -20,11 +20,11 @@ export interface VisitorResponse extends QueueServiceResponse {
|
||||
* Uses STOMP for authenticating (https://stomp.github.io/).
|
||||
*/
|
||||
export class WebsocketClient {
|
||||
private stompClient: Client | undefined;
|
||||
protected stompClient: Client | undefined;
|
||||
|
||||
private static instance: WebsocketClient;
|
||||
|
||||
private retriesCount = 0;
|
||||
protected retriesCount = 0;
|
||||
|
||||
private _connectCount = 0;
|
||||
|
||||
|
||||
@@ -127,7 +127,9 @@ Component 'conference.vX.meet.jitsi' 'muc'
|
||||
'muc_domain_mapper';
|
||||
'muc_meeting_id';
|
||||
'fmuc';
|
||||
's2soutinjection';
|
||||
's2s_bidi';
|
||||
's2s_whitelist';
|
||||
's2sout_override';
|
||||
}
|
||||
muc_room_default_presence_broadcast = {
|
||||
visitor = false;
|
||||
|
||||
@@ -5,6 +5,7 @@ local internal_room_jid_match_rewrite = util.internal_room_jid_match_rewrite;
|
||||
local room_jid_match_rewrite = util.room_jid_match_rewrite;
|
||||
local process_host_module = util.process_host_module;
|
||||
local table_shallow_copy = util.table_shallow_copy;
|
||||
local is_admin = util.is_admin;
|
||||
local array = require "util.array";
|
||||
local json = require 'cjson.safe';
|
||||
local st = require 'util.stanza';
|
||||
@@ -151,6 +152,29 @@ function notify_jid_approved(jid, from, room, mediaType)
|
||||
send_json_message(jid, json_message);
|
||||
end
|
||||
|
||||
function start_av_moderation(room, mediaType, occupant)
|
||||
if not room.av_moderation then
|
||||
room.av_moderation = {};
|
||||
room.av_moderation_actors = {};
|
||||
end
|
||||
room.av_moderation[mediaType] = array{};
|
||||
|
||||
-- We want to set startMuted policy in metadata, in case of new participants are joining to respect
|
||||
-- it, that will be enforced by jicofo
|
||||
local startMutedMetadata = room.jitsiMetadata.startMuted or {};
|
||||
|
||||
-- We want to keep the previous value of startMuted for this mediaType if av moderation is disabled
|
||||
-- to be able to restore
|
||||
local av_moderation_startMuted_restore = room.av_moderation_startMuted_restore or {};
|
||||
av_moderation_startMuted_restore = startMutedMetadata[mediaType];
|
||||
room.av_moderation_startMuted_restore = av_moderation_startMuted_restore;
|
||||
|
||||
startMutedMetadata[mediaType] = true;
|
||||
room.jitsiMetadata.startMuted = startMutedMetadata;
|
||||
|
||||
room.av_moderation_actors[mediaType] = occupant.nick;
|
||||
end
|
||||
|
||||
-- receives messages from clients to the component sending A/V moderation enable/disable commands or adding
|
||||
-- jids to the whitelist
|
||||
function on_message(event)
|
||||
@@ -208,26 +232,7 @@ function on_message(event)
|
||||
module:log('warn', 'Concurrent moderator enable/disable request or something is out of sync');
|
||||
return true;
|
||||
else
|
||||
if not room.av_moderation then
|
||||
room.av_moderation = {};
|
||||
room.av_moderation_actors = {};
|
||||
end
|
||||
room.av_moderation[mediaType] = array{};
|
||||
|
||||
-- We want to set startMuted policy in metadata, in case of new participants are joining to respect
|
||||
-- it, that will be enforced by jicofo
|
||||
local startMutedMetadata = room.jitsiMetadata.startMuted or {};
|
||||
|
||||
-- We want to keep the previous value of startMuted for this mediaType if av moderation is disabled
|
||||
-- to be able to restore
|
||||
local av_moderation_startMuted_restore = room.av_moderation_startMuted_restore or {};
|
||||
av_moderation_startMuted_restore = startMutedMetadata[mediaType];
|
||||
room.av_moderation_startMuted_restore = av_moderation_startMuted_restore;
|
||||
|
||||
startMutedMetadata[mediaType] = true;
|
||||
room.jitsiMetadata.startMuted = startMutedMetadata;
|
||||
|
||||
room.av_moderation_actors[mediaType] = occupant.nick;
|
||||
start_av_moderation(room, mediaType, occupant);
|
||||
end
|
||||
else
|
||||
enabled = false;
|
||||
@@ -317,10 +322,30 @@ end
|
||||
function occupant_joined(event)
|
||||
local room, occupant = event.room, event.occupant;
|
||||
|
||||
if is_healthcheck_room(room.jid) then
|
||||
if is_healthcheck_room(room.jid) or is_admin(occupant.bare_jid) then
|
||||
return;
|
||||
end
|
||||
|
||||
-- when first moderator joins if av_can_unmute from password preset is set to false, we enable av moderation for both
|
||||
-- audio and video, and set the first moderator as the actor that enabled it
|
||||
if room._data.av_can_unmute ~= nil
|
||||
and not room._data.av_first_moderator_joined
|
||||
|
||||
-- occupant.role is not reflecting the actual role after set_affiliation is used in same occupant_joined event
|
||||
and room:get_role(occupant.nick) == 'moderator' then
|
||||
|
||||
if not room._data.av_can_unmute then
|
||||
for _,mediaType in pairs({'audio', 'video'}) do
|
||||
start_av_moderation(room, mediaType, occupant);
|
||||
|
||||
notify_occupants_enable(nil, true, room, occupant.nick, mediaType);
|
||||
end
|
||||
|
||||
room._data.av_first_moderator_joined = true;
|
||||
return;
|
||||
end
|
||||
end
|
||||
|
||||
if room.av_moderation then
|
||||
for _,mediaType in pairs({'audio', 'video'}) do
|
||||
if room.av_moderation[mediaType] then
|
||||
|
||||
@@ -130,6 +130,10 @@ function on_message(event)
|
||||
return true;
|
||||
end
|
||||
|
||||
if not room.jitsi_shared_files then
|
||||
return;
|
||||
end
|
||||
|
||||
room.jitsi_shared_files[message.attr.fileId] = nil;
|
||||
|
||||
local json_msg, error = json.encode({
|
||||
|
||||
@@ -3,9 +3,10 @@
|
||||
-- Component "metadata.jitmeet.example.com" "room_metadata_component"
|
||||
-- muc_component = "conference.jitmeet.example.com"
|
||||
-- breakout_rooms_component = "breakout.jitmeet.example.com"
|
||||
local array = require 'util.array';
|
||||
local filters = require 'util.filters';
|
||||
local jid_node = require 'util.jid'.node;
|
||||
local json = require 'cjson.safe';
|
||||
local json = require 'util.json';
|
||||
local st = require 'util.stanza';
|
||||
local jid = require 'util.jid';
|
||||
|
||||
@@ -78,24 +79,25 @@ function send_metadata(occupant, room, json_msg)
|
||||
|
||||
-- we want to send the main meeting participants only to jicofo
|
||||
if is_admin(occupant.bare_jid) then
|
||||
local participants = {};
|
||||
local participants;
|
||||
local moderators = array();
|
||||
|
||||
if room._data.mainMeetingParticipants then
|
||||
table_add(participants, room._data.mainMeetingParticipants);
|
||||
if room._data.participants then
|
||||
participants = array();
|
||||
participants:append(room._data.participants);
|
||||
end
|
||||
|
||||
if room._data.moderator_id then
|
||||
table.insert(participants, room._data.moderator_id);
|
||||
moderators:push(room._data.moderator_id);
|
||||
end
|
||||
|
||||
if room._data.moderators then
|
||||
table_add(participants, room._data.moderators);
|
||||
moderators:append(room._data.moderators);
|
||||
end
|
||||
|
||||
if #participants > 0 then
|
||||
metadata_to_send = table_shallow_copy(metadata_to_send);
|
||||
metadata_to_send.mainMeetingParticipants = participants;
|
||||
end
|
||||
metadata_to_send = table_shallow_copy(metadata_to_send);
|
||||
metadata_to_send.participants = participants;
|
||||
metadata_to_send.moderators = moderators;
|
||||
end
|
||||
|
||||
json_msg = getMetadataJSON(room, metadata_to_send);
|
||||
|
||||
@@ -1,93 +0,0 @@
|
||||
-- Using version https://hg.prosody.im/prosody-modules/file/4fb922aa0ace/mod_s2soutinjection/mod_s2soutinjection.lua
|
||||
local st = require"util.stanza";
|
||||
local new_outgoing = require"core.s2smanager".new_outgoing;
|
||||
local bounce_sendq = module:depends"s2s".route_to_new_session.bounce_sendq;
|
||||
local initialize_filters = require "util.filters".initialize;
|
||||
|
||||
local portmanager = require "core.portmanager";
|
||||
|
||||
local addclient = require "net.server".addclient;
|
||||
|
||||
module:depends("s2s");
|
||||
|
||||
local sessions = module:shared("sessions");
|
||||
|
||||
local injected = module:get_option("s2s_connect_overrides");
|
||||
|
||||
-- The proxy_listener handles connection while still connecting to the proxy,
|
||||
-- then it hands them over to the normal listener (in mod_s2s)
|
||||
local proxy_listener = { default_port = nil, default_mode = "*a", default_interface = "*" };
|
||||
|
||||
function proxy_listener.onconnect(conn)
|
||||
local session = sessions[conn];
|
||||
|
||||
-- needed in mod_rate_limit
|
||||
session.ip = conn:ip();
|
||||
|
||||
-- Now the real s2s listener can take over the connection.
|
||||
local listener = portmanager.get_service("s2s").listener;
|
||||
|
||||
local log = session.log;
|
||||
|
||||
local filter = initialize_filters(session);
|
||||
|
||||
session.version = 1;
|
||||
|
||||
session.sends2s = function (t)
|
||||
-- log("debug", "sending (s2s over proxy): %s", (t.top_tag and t:top_tag()) or t:match("^[^>]*>?"));
|
||||
if t.name then
|
||||
t = filter("stanzas/out", t);
|
||||
end
|
||||
if t then
|
||||
t = filter("bytes/out", tostring(t));
|
||||
if t then
|
||||
return conn:write(tostring(t));
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
session.open_stream = function ()
|
||||
session.sends2s(st.stanza("stream:stream", {
|
||||
xmlns='jabber:server', ["xmlns:db"]='jabber:server:dialback',
|
||||
["xmlns:stream"]='http://etherx.jabber.org/streams',
|
||||
from=session.from_host, to=session.to_host, version='1.0', ["xml:lang"]='en'}):top_tag());
|
||||
end
|
||||
|
||||
conn.setlistener(conn, listener);
|
||||
|
||||
listener.register_outgoing(conn, session);
|
||||
|
||||
listener.onconnect(conn);
|
||||
end
|
||||
|
||||
function proxy_listener.register_outgoing(conn, session)
|
||||
session.direction = "outgoing";
|
||||
sessions[conn] = session;
|
||||
end
|
||||
|
||||
function proxy_listener.ondisconnect(conn, err)
|
||||
sessions[conn] = nil;
|
||||
end
|
||||
|
||||
module:hook("route/remote", function(event)
|
||||
local from_host, to_host, stanza = event.from_host, event.to_host, event.stanza;
|
||||
local inject = injected and injected[to_host];
|
||||
if not inject then return end
|
||||
-- module:log("debug", "opening a new outgoing connection for this stanza");
|
||||
local host_session = new_outgoing(from_host, to_host);
|
||||
|
||||
-- Store in buffer
|
||||
host_session.bounce_sendq = bounce_sendq;
|
||||
host_session.sendq = { {tostring(stanza), stanza.attr.type ~= "error" and stanza.attr.type ~= "result" and st.reply(stanza)} };
|
||||
-- host_session.log("debug", "stanza [%s] queued until connection complete", tostring(stanza.name));
|
||||
|
||||
local host, port = inject[1] or inject, tonumber(inject[2]) or 5269;
|
||||
|
||||
local conn = addclient(host, port, proxy_listener, "*a");
|
||||
|
||||
proxy_listener.register_outgoing(conn, host_session);
|
||||
|
||||
host_session.conn = conn;
|
||||
return true;
|
||||
end, -2);
|
||||
|
||||
@@ -23,6 +23,9 @@ local new_id = require 'util.id'.medium;
|
||||
local json = require 'cjson.safe';
|
||||
local inspect = require 'inspect';
|
||||
|
||||
-- Debug flag
|
||||
local DEBUG = false;
|
||||
|
||||
-- will be initialized once the main virtual host module is initialized
|
||||
local token_util;
|
||||
|
||||
@@ -63,6 +66,17 @@ local visitors_promotion_requests = {};
|
||||
local cache = require 'util.cache';
|
||||
local sent_iq_cache = cache.new(200);
|
||||
|
||||
-- Function to get visitors room metadata
|
||||
local function get_visitors_room_metadata(room)
|
||||
if not room.jitsiMetadata then
|
||||
room.jitsiMetadata = {};
|
||||
end
|
||||
if not room.jitsiMetadata.visitors then
|
||||
room.jitsiMetadata.visitors = {};
|
||||
end
|
||||
return room.jitsiMetadata.visitors;
|
||||
end
|
||||
|
||||
-- Sends a json-message to the destination jid
|
||||
-- @param to_jid the destination jid
|
||||
-- @param json_message the message content to send
|
||||
@@ -73,6 +87,11 @@ function send_json_message(to_jid, json_message)
|
||||
end
|
||||
|
||||
local function request_promotion_received(room, from_jid, from_vnode, nick, time, user_id, group_id, force_promote_requested)
|
||||
if DEBUG then
|
||||
module:log('debug', 'Received promotion request from %s for room %s, nick: %s, time: %s, user_id: %s, group_id: %s, force_promote_requested: %s',
|
||||
from_jid, room.jid, nick, time, user_id, group_id, force_promote_requested);
|
||||
end
|
||||
|
||||
-- if visitors is enabled for the room
|
||||
if visitors_promotion_map[room.jid] then
|
||||
local force_promote = auto_allow_promotion or get_visitors_room_metadata(room).autoPromote;
|
||||
@@ -231,16 +250,6 @@ function get_visitors_languages(room)
|
||||
return count, languages:sort():concat(',');
|
||||
end
|
||||
|
||||
local function get_visitors_room_metadata(room)
|
||||
if not room.jitsiMetadata then
|
||||
room.jitsiMetadata = {};
|
||||
end
|
||||
if not room.jitsiMetadata.visitors then
|
||||
room.jitsiMetadata.visitors = {};
|
||||
end
|
||||
return room.jitsiMetadata.visitors;
|
||||
end
|
||||
|
||||
-- listens for iq request for promotion and forward it to moderators in the meeting for approval
|
||||
-- or auto-allow it if such the config is set enabling it
|
||||
local function stanza_handler(event)
|
||||
@@ -250,6 +259,10 @@ local function stanza_handler(event)
|
||||
return;
|
||||
end
|
||||
|
||||
if DEBUG then
|
||||
module:log('debug', 'Received stanza %s from %s', stanza, origin.full_jid);
|
||||
end
|
||||
|
||||
if stanza.attr.type == 'result' and sent_iq_cache:get(stanza.attr.id) then
|
||||
sent_iq_cache:set(stanza.attr.id, nil);
|
||||
return true;
|
||||
@@ -367,6 +380,11 @@ local function process_promotion_response(room, id, approved)
|
||||
return;
|
||||
end
|
||||
|
||||
if DEBUG then
|
||||
module:log('debug', 'Processing promotion response for room %s, id %s, approved %s',
|
||||
room.jid, id, approved);
|
||||
end
|
||||
|
||||
-- lets reply to participant that requested promotion
|
||||
local username = new_id():lower();
|
||||
visitors_promotion_map[room.jid][username] = {
|
||||
@@ -400,13 +418,24 @@ end
|
||||
-- if room metadata does not have visitors.live set to `true` and there are no occupants in the meeting
|
||||
-- it will skip calling goLive endpoint
|
||||
local function go_live(room)
|
||||
|
||||
if DEBUG then
|
||||
module:log('debug', 'Checking if room %s is live', room.jid);
|
||||
end
|
||||
|
||||
if room._jitsi_go_live_sent then
|
||||
if DEBUG then
|
||||
module:log('debug', 'Room %s already sent go live request, skipping', room.jid);
|
||||
end
|
||||
return;
|
||||
end
|
||||
|
||||
-- if missing we assume room is live, only skip if it is marked explicitly as false
|
||||
if room.jitsiMetadata and room.jitsiMetadata.visitors
|
||||
and room.jitsiMetadata.visitors.live ~= nil and room.jitsiMetadata.visitors.live == false then
|
||||
if DEBUG then
|
||||
module:log('debug', 'Room %s is not live, skipping go live request', room.jid);
|
||||
end
|
||||
return;
|
||||
end
|
||||
|
||||
@@ -420,6 +449,9 @@ local function go_live(room)
|
||||
|
||||
-- when there is an occupant then go live
|
||||
if not has_occupant then
|
||||
if DEBUG then
|
||||
module:log('debug', 'Room %s has no occupants, skipping go live request', room.jid);
|
||||
end
|
||||
return;
|
||||
end
|
||||
|
||||
@@ -461,6 +493,10 @@ process_host_module(muc_domain_prefix..'.'..muc_domain_base, function(host_modul
|
||||
local room, stanza, occupant, session = event.room, event.stanza, event.occupant, event.origin;
|
||||
|
||||
if is_healthcheck_room(room.jid) or is_admin(occupant.bare_jid) then
|
||||
if DEBUG then
|
||||
module:log('debug', 'Skipping visitor checks for healthcheck room %s or admin %s',
|
||||
room.jid, occupant.bare_jid);
|
||||
end
|
||||
return;
|
||||
end
|
||||
|
||||
@@ -479,8 +515,13 @@ process_host_module(muc_domain_prefix..'.'..muc_domain_base, function(host_modul
|
||||
or ignore_list:contains(jid.host(stanza.attr.from)) -- jibri or other domains to ignore
|
||||
or is_sip_jigasi(stanza)
|
||||
or is_sip_jibri_join(stanza)
|
||||
or table_find(room._data.mainMeetingParticipants, session.jitsi_meet_context_user and session.jitsi_meet_context_user.id) then
|
||||
return;
|
||||
or table_find(room._data.moderators, session.jitsi_meet_context_user and session.jitsi_meet_context_user.id)
|
||||
or (room._data.moderator_id and room._data.moderator_id == (session.jitsi_meet_context_user and session.jitsi_meet_context_user.id))
|
||||
or table_find(room._data.participants, session.jitsi_meet_context_user and session.jitsi_meet_context_user.id) then
|
||||
if DEBUG then
|
||||
module:log('debug', 'Auto-allowing visitor %s in room %s', stanza.attr.from, room.jid);
|
||||
end
|
||||
return;
|
||||
end
|
||||
|
||||
if visitors_promotion_map[room.jid] then
|
||||
@@ -517,15 +558,14 @@ process_host_module(muc_domain_prefix..'.'..muc_domain_base, function(host_modul
|
||||
:tag('no-main-participants', { xmlns = 'jitsi:visitors' }));
|
||||
return true;
|
||||
end
|
||||
elseif is_live == false
|
||||
and room._data.mainMeetingParticipants
|
||||
and #room._data.mainMeetingParticipants > 0 then
|
||||
-- This is non jaas room which is not live and has a list of participants
|
||||
-- allowed to participate in the main room, but this participant is not one of them
|
||||
elseif room._data.participants then
|
||||
-- This is non jaas room which has a list of participants allowed to participate in the main room
|
||||
-- but this occupant is not one of them and the room is either not live or has no participants joined
|
||||
session.log('warn',
|
||||
'Deny user join in the main not live meeting, not in the list of main participants');
|
||||
session.send(st.error_reply(
|
||||
stanza, 'cancel', 'not-allowed', 'Tried to join the main (not live) room')
|
||||
stanza, 'cancel', 'not-allowed',
|
||||
'Tried to join the main (not live or without main participants) room')
|
||||
:tag('not-live-room', { xmlns = 'jitsi:visitors' }));
|
||||
return true;
|
||||
end
|
||||
@@ -539,8 +579,16 @@ process_host_module(muc_domain_prefix..'.'..muc_domain_base, function(host_modul
|
||||
host_module:hook('muc-occupant-joined', function (event)
|
||||
local room, occupant = event.room, event.occupant;
|
||||
|
||||
if DEBUG then
|
||||
module:log('debug', 'Occupant %s joined room %s', occupant.jid, room.jid);
|
||||
end
|
||||
|
||||
if is_healthcheck_room(room.jid) or is_admin(occupant.bare_jid) or occupant.role ~= 'moderator' -- luacheck: ignore
|
||||
or not visitors_promotion_requests[event.room.jid] then
|
||||
if DEBUG then
|
||||
module:log('debug', 'Skipping visitor checks for healthcheck room %s or admin %s or not moderator %s',
|
||||
room.jid, occupant.bare_jid, occupant.role);
|
||||
end
|
||||
return;
|
||||
end
|
||||
|
||||
@@ -666,6 +714,13 @@ process_host_module(muc_domain_prefix..'.'..muc_domain_base, function(host_modul
|
||||
host_module:context(host):hook("muc-config-form", function(event)
|
||||
table.insert(event.form, visitorsEnabledField);
|
||||
end);
|
||||
|
||||
if not room.jitsiMetadata then
|
||||
room.jitsiMetadata = {};
|
||||
end
|
||||
room.jitsiMetadata.visitorsEnabled = true;
|
||||
module:context(muc_domain_prefix..'.'..muc_domain_base)
|
||||
:fire_event('room-metadata-changed', { room = room; });
|
||||
end
|
||||
end);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user